diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index 8f0776e4d3..cf226f7733 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -4,7 +4,7 @@ {{metadata?.key?.split('.').join('.​')}}
- + >
{{"item.edit.metadata.metadatafield.invalid" | translate}} diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 60419f41b2..4ecdb21e24 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -20,9 +20,9 @@ import { } from '../../../../shared/remote-data.utils'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { EditInPlaceFieldComponent } from './edit-in-place-field.component'; -import { FilterInputSuggestionsComponent } from '../../../../shared/input-suggestions/filter-suggestions/filter-input-suggestions.component'; import { MockComponent, MockDirective } from 'ng-mocks'; import { DebounceDirective } from '../../../../shared/utils/debounce.directive'; +import { ValidationSuggestionsComponent } from '../../../../shared/input-suggestions/validation-suggestions/validation-suggestions.component'; let comp: EditInPlaceFieldComponent; let fixture: ComponentFixture; @@ -88,7 +88,7 @@ describe('EditInPlaceFieldComponent', () => { declarations: [ EditInPlaceFieldComponent, MockDirective(DebounceDirective), - MockComponent(FilterInputSuggestionsComponent) + MockComponent(ValidationSuggestionsComponent) ], providers: [ { provide: RegistryService, useValue: metadataFieldService }, diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index a1a6951545..6fde34b9a5 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -303,7 +303,7 @@ describe('EPersonDataService', () => { it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => { service.patchPasswordWithToken('test-uuid', 'test-token','test-password'); - const operation = Object.assign({ op: 'replace', path: '/password', value: 'test-password' }); + const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' }); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]); expect(requestService.configure).toHaveBeenCalledWith(expected); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index a1428aee73..5fc4c6497f 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -280,7 +280,7 @@ export class EPersonDataService extends DataService { patchPasswordWithToken(uuid: string, token: string, password: string): Observable { const requestId = this.requestService.generateRequestId(); - const operation = Object.assign({ op: 'replace', path: '/password', value: password }); + const operation = Object.assign({ op: 'add', path: '/password', value: password }); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, uuid)), diff --git a/src/app/core/shared/process-output.resource-type.ts b/src/app/core/shared/process-output.resource-type.ts new file mode 100644 index 0000000000..2e707d0bda --- /dev/null +++ b/src/app/core/shared/process-output.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ProcessOutput + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const PROCESS_OUTPUT_TYPE = new ResourceType('processOutput'); diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 9cb1f1e6af..d59f93254e 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -17,7 +17,7 @@ {{getFileName(file)}} - ({{(file?.sizeBytes) | dsFileSize }}) + ({{(file?.sizeBytes) | dsFileSize }}) @@ -34,9 +34,20 @@
{{ process.processStatus }}
- - - + + + +
{{ (outputLogs$ | async) }}
+

+ {{ 'process.detail.logs.none' | translate }} +

+
- {{'process.detail.back' | translate}} +
+ {{'process.detail.back' | translate}} +
diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index dff481fdc6..b81eedabad 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -1,9 +1,22 @@ +import { HttpClient } from '@angular/common/http'; +import { AuthService } from '../../core/auth/auth.service'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { AuthServiceMock } from '../../shared/mocks/auth.service.mock'; import { ProcessDetailComponent } from './process-detail.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { + async, + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick +} from '@angular/core/testing'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component'; import { Process } from '../processes/process.model'; import { ActivatedRoute } from '@angular/router'; @@ -22,15 +35,21 @@ describe('ProcessDetailComponent', () => { let processService: ProcessDataService; let nameService: DSONameService; + let bitstreamDataService: BitstreamDataService; + let httpClient: HttpClient; let process: Process; let fileName: string; let files: Bitstream[]; + let processOutput; + function init() { + processOutput = 'Process Started' process = Object.assign(new Process(), { processId: 1, scriptName: 'script-name', + processStatus: 'COMPLETED', parameters: [ { name: '-f', @@ -40,7 +59,15 @@ describe('ProcessDetailComponent', () => { name: '-i', value: 'identifier' } - ] + ], + _links: { + self: { + href: 'https://rest.api/processes/1' + }, + output: { + href: 'https://rest.api/processes/1/output' + } + } }); fileName = 'fake-file-name'; files = [ @@ -59,12 +86,24 @@ describe('ProcessDetailComponent', () => { } }) ]; + const logBitstream = Object.assign(new Bitstream(), { + id: 'output.log', + _links: { + content: { href: 'log-selflink' } + } + }); processService = jasmine.createSpyObj('processService', { getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)) }); + bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findByHref: createSuccessfulRemoteDataObject$(logBitstream) + }); nameService = jasmine.createSpyObj('nameService', { getName: fileName }); + httpClient = jasmine.createSpyObj('httpClient', { + get: observableOf(processOutput) + }); } beforeEach(async(() => { @@ -73,26 +112,41 @@ describe('ProcessDetailComponent', () => { declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } }, + { + provide: ActivatedRoute, + useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } + }, { provide: ProcessDataService, useValue: processService }, - { provide: DSONameService, useValue: nameService } + { provide: BitstreamDataService, useValue: bitstreamDataService }, + { provide: DSONameService, useValue: nameService }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: HttpClient, useValue: httpClient }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ProcessDetailComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); + afterEach(fakeAsync(() => { + TestBed.resetTestingModule(); + fixture.destroy(); + flush(); + flushMicrotasks(); + discardPeriodicTasks(); + component = null; + })); it('should display the script\'s name', () => { + fixture.detectChanges(); const name = fixture.debugElement.query(By.css('#process-name')).nativeElement; expect(name.textContent).toContain(process.scriptName); }); it('should display the process\'s parameters', () => { + fixture.detectChanges(); const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement; process.parameters.forEach((param) => { expect(args.textContent).toContain(`${param.name} ${param.value}`) @@ -100,8 +154,57 @@ describe('ProcessDetailComponent', () => { }); it('should display the process\'s output files', () => { + fixture.detectChanges(); const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement; expect(processFiles.textContent).toContain(fileName); }); + describe('if press show output logs', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + })); + it('should trigger showProcessOutputLogs', () => { + expect(component.showProcessOutputLogs).toHaveBeenCalled(); + }); + it('should display the process\'s output logs', () => { + fixture.detectChanges(); + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess.nativeElement.textContent).toContain(processOutput); + }); + }); + + describe('if press show output logs and process has no output logs', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + spyOn(httpClient, 'get').and.returnValue(observableOf(null)); + fixture = TestBed.createComponent(ProcessDetailComponent); + component = fixture.componentInstance; + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('should not display the process\'s output logs', () => { + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess).toBeNull(); + }); + it('should display message saying there are no output logs', () => { + const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement; + expect(noOutputProcess).toBeDefined(); + }); + }); + }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index c4610b70e9..c4b94aa729 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,15 +1,23 @@ -import { Component, OnInit } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Component, NgZone, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; -import { RemoteData } from '../../core/data/remote-data'; -import { Process } from '../processes/process.model'; -import { map, switchMap } from 'rxjs/operators'; -import { getFirstSucceededRemoteDataPayload, redirectOn404Or401 } from '../../core/shared/operators'; -import { AlertType } from '../../shared/alert/aletr-type'; -import { ProcessDataService } from '../../core/data/processes/process-data.service'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { Bitstream } from '../../core/shared/bitstream.model'; +import { finalize, map, mergeMap, switchMap, take, tap } from 'rxjs/operators'; +import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { ProcessDataService } from '../../core/data/processes/process-data.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { getFirstSucceededRemoteDataPayload, redirectOn404Or401 } from '../../core/shared/operators'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { AlertType } from '../../shared/alert/aletr-type'; +import { hasValue } from '../../shared/empty.util'; +import { ProcessStatus } from '../processes/process-status.model'; +import { Process } from '../processes/process.model'; @Component({ selector: 'ds-process-detail', @@ -36,10 +44,33 @@ export class ProcessDetailComponent implements OnInit { */ filesRD$: Observable>>; + /** + * File link that contain the output logs with auth token + */ + outputLogFileUrl$: Observable; + + /** + * The Process's Output logs + */ + outputLogs$: Observable; + + /** + * Boolean on whether or not to show the output logs + */ + showOutputLogs; + /** + * When it's retrieving the output logs from backend, to show loading component + */ + retrievingOutputLogs$: BehaviorSubject; + constructor(protected route: ActivatedRoute, protected router: Router, protected processService: ProcessDataService, - protected nameService: DSONameService) { + protected bitstreamDataService: BitstreamDataService, + protected nameService: DSONameService, + private zone: NgZone, + protected authService: AuthService, + protected http: HttpClient) { } /** @@ -47,8 +78,12 @@ export class ProcessDetailComponent implements OnInit { * Display a 404 if the process doesn't exist */ ngOnInit(): void { + this.showOutputLogs = false; + this.retrievingOutputLogs$ = new BehaviorSubject(false); this.processRD$ = this.route.data.pipe( - map((data) => data.process as RemoteData), + map((data) => { + return data.process as RemoteData + }), redirectOn404Or401(this.router) ); @@ -63,7 +98,68 @@ export class ProcessDetailComponent implements OnInit { * @param bitstream */ getFileName(bitstream: Bitstream) { - return this.nameService.getName(bitstream); + return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown'; + } + + /** + * Retrieves the process logs, while setting the loading subject to true. + * Sets the outputLogs when retrieved and sets the showOutputLogs boolean to show them and hide the button. + */ + showProcessOutputLogs() { + this.retrievingOutputLogs$.next(true); + this.zone.runOutsideAngular(() => { + const processOutputRD$: Observable> = this.processRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((process: Process) => { + return this.bitstreamDataService.findByHref(process._links.output.href); + }) + ); + this.outputLogFileUrl$ = processOutputRD$.pipe( + tap((processOutputFileRD: RemoteData) => { + if (processOutputFileRD.statusCode === 204) { + this.zone.run(() => this.retrievingOutputLogs$.next(false)); + this.showOutputLogs = true; + } + }), + getFirstSucceededRemoteDataPayload(), + mergeMap((processOutput: Bitstream) => { + const url = processOutput._links.content.href; + return this.authService.getShortlivedToken().pipe(take(1), + map((token: string) => { + return hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url; + })); + }) + ) + }); + this.outputLogs$ = this.outputLogFileUrl$.pipe(take(1), + mergeMap((url: string) => { + return this.getTextFile(url); + }), + finalize(() => this.zone.run(() => this.retrievingOutputLogs$.next(false))), + ); + this.outputLogs$.pipe(take(1)).subscribe(); + } + + getTextFile(filename: string): Observable { + // The Observable returned by get() is of type Observable + // because a text response was specified. + // There's no need to pass a type parameter to get(). + return this.http.get(filename, { responseType: 'text' }) + .pipe( + finalize(() => { + this.showOutputLogs = true; + }), + ); + } + + /** + * Whether or not the given process has Completed or Failed status + * @param process Process to check if completed or failed + */ + isProcessFinished(process: Process): boolean { + return (hasValue(process) && hasValue(process.processStatus) && + (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString() + || process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString())); } } diff --git a/src/app/process-page/processes/process.model.ts b/src/app/process-page/processes/process.model.ts index 85de5337e7..74bb82b890 100644 --- a/src/app/process-page/processes/process.model.ts +++ b/src/app/process-page/processes/process.model.ts @@ -1,3 +1,5 @@ +import { Bitstream } from '../../core/shared/bitstream.model'; +import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type'; import { ProcessStatus } from './process-status.model'; import { ProcessParameter } from './process-parameter.model'; import { CacheableObject } from '../../core/cache/object-cache.reducer'; @@ -85,4 +87,11 @@ export class Process implements CacheableObject { */ @link(SCRIPT) script?: Observable>; + + /** + * The output logs created by this Process + * Will be undefined unless the output {@link HALLink} has been resolved. + */ + @link(PROCESS_OUTPUT_TYPE) + output?: Observable>; } diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index 8d78539bab..418626e4d1 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -177,7 +177,7 @@ describe('ProfilePageComponent', () => { component.setPasswordValue('testest'); component.setInvalid(false); - operations = [{op: 'replace', path: '/password', value: 'testest'}]; + operations = [{op: 'add', path: '/password', value: 'testest'}]; result = component.updateSecurity(); }); diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index bc06c49f81..4ae644a633 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -120,7 +120,7 @@ export class ProfilePageComponent implements OnInit { this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general')); } if (!this.invalidSecurity && passEntered) { - const operation = Object.assign({op: 'replace', path: '/password', value: this.password}); + const operation = Object.assign({op: 'add', path: '/password', value: this.password}); this.epersonService.patch(this.currentUser, [operation]).subscribe((response: RestResponse) => { if (response.isSuccessful) { this.notificationsService.success( diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html index 91d8217ade..7a9481f2f1 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html @@ -1,15 +1,14 @@ -
- + + [ngModelOptions]="{standalone: true}" autocomplete="off"/> -
- + \ No newline at end of file diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts index cb36071c28..51664039f7 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts @@ -3,11 +3,9 @@ import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angula import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { MetadataFieldDataService } from '../../../core/data/metadata-field-data.service'; -import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { FilterInputSuggestionsComponent } from './filter-input-suggestions.component'; describe('FilterInputSuggestionsComponent', () => { @@ -23,13 +21,9 @@ describe('FilterInputSuggestionsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule, ReactiveFormsModule], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], declarations: [FilterInputSuggestionsComponent], - providers: [FormsModule, - ReactiveFormsModule, - { provide: MetadataFieldDataService, useValue: {} }, - { provide: ObjectUpdatesService, useValue: {} }, - ], + providers: [], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FilterInputSuggestionsComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.ts b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.ts index 49aa46b757..9e7d84d9ed 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.ts +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.ts @@ -1,8 +1,5 @@ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { MetadatumViewModel } from '../../../core/shared/metadata.models'; -import { MetadataFieldValidator } from '../../utils/metadatafield-validator.directive'; +import { Component, forwardRef, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { InputSuggestionsComponent } from '../input-suggestions.component'; import { InputSuggestion } from '../input-suggestions.model'; @@ -24,39 +21,12 @@ import { InputSuggestion } from '../input-suggestions.model'; /** * Component representing a form with a autocomplete functionality */ -export class FilterInputSuggestionsComponent extends InputSuggestionsComponent implements OnInit { - - form: FormGroup; - - /** - * The current url of this page - */ - @Input() url: string; - - /** - * The metadatum of this field - */ - @Input() metadata: MetadatumViewModel; - +export class FilterInputSuggestionsComponent extends InputSuggestionsComponent { /** * The suggestions that should be shown */ @Input() suggestions: InputSuggestion[] = []; - constructor(private metadataFieldValidator: MetadataFieldValidator, - private objectUpdatesService: ObjectUpdatesService) { - super(); - } - - ngOnInit() { - this.form = new FormGroup({ - metadataNameField: new FormControl(this._value, { - asyncValidators: [this.metadataFieldValidator.validate.bind(this.metadataFieldValidator)], - validators: [Validators.required] - }) - }); - } - onSubmit(data) { this.value = data; this.submitSuggestion.emit(data); @@ -70,15 +40,4 @@ export class FilterInputSuggestionsComponent extends InputSuggestionsComponent i this.queryInput.nativeElement.focus(); return false; } - - /** - * Check if the input is valid according to validator and send (in)valid state to store - * @param form Form with input - */ - checkIfValidInput(form) { - this.valid = !(form.get('metadataNameField').status === 'INVALID' && (form.get('metadataNameField').dirty || form.get('metadataNameField').touched)); - this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, this.valid); - return this.valid; - } - } diff --git a/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.html b/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.html new file mode 100644 index 0000000000..91d8217ade --- /dev/null +++ b/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.html @@ -0,0 +1,24 @@ +
+ + + +
+ diff --git a/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.spec.ts b/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.spec.ts new file mode 100644 index 0000000000..82e838effc --- /dev/null +++ b/src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.spec.ts @@ -0,0 +1,63 @@ +import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MetadataFieldDataService } from '../../../core/data/metadata-field-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ValidationSuggestionsComponent } from './validation-suggestions.component'; + +describe('ValidationSuggestionsComponent', () => { + + let comp: ValidationSuggestionsComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + const suggestions = [{ displayValue: 'suggestion uno', value: 'suggestion uno' }, { + displayValue: 'suggestion dos', + value: 'suggestion dos' + }, { displayValue: 'suggestion tres', value: 'suggestion tres' }]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule, ReactiveFormsModule], + declarations: [ValidationSuggestionsComponent], + providers: [FormsModule, + ReactiveFormsModule, + { provide: MetadataFieldDataService, useValue: {} }, + { provide: ObjectUpdatesService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ValidationSuggestionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ValidationSuggestionsComponent); + + comp = fixture.componentInstance; // LoadingComponent test instance + comp.suggestions = suggestions; + // query for the message