diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.html new file mode 100644 index 00000000000..add817cbf4c --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.html @@ -0,0 +1,39 @@ + + + + +
+
+ + + + +
+
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.spec.ts new file mode 100644 index 00000000000..06b984cfa47 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.spec.ts @@ -0,0 +1,353 @@ +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { MetadataField } from '../../../../core/metadata/metadata-field.model'; +import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { RegistryService } from '../../../../core/registry/registry.service'; +import { Collection } from '../../../../core/shared/collection.model'; +import { ConfidenceType } from '../../../../core/shared/confidence-type'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { Item } from '../../../../core/shared/item.model'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { Vocabulary } from '../../../../core/submission/vocabularies/models/vocabulary.model'; +import { VocabularyService } from '../../../../core/submission/vocabularies/vocabulary.service'; +import { DynamicOneboxModel } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; +import { DynamicScrollableDropdownModel } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { VocabularyServiceStub } from '../../../../shared/testing/vocabulary-service.stub'; +import { DsoEditMetadataValue } from '../../dso-edit-metadata-form'; +import { DsoEditMetadataAuthorityFieldComponent } from './dso-edit-metadata-authority-field.component'; + +describe('DsoEditMetadataAuthorityFieldComponent', () => { + let component: DsoEditMetadataAuthorityFieldComponent; + let fixture: ComponentFixture; + + let vocabularyService: any; + let itemService: ItemDataService; + let registryService: RegistryService; + let notificationsService: NotificationsService; + + let dso: DSpaceObject; + + const collection = Object.assign(new Collection(), { + uuid: 'fake-uuid', + }); + + const item = Object.assign(new Item(), { + _links: { + self: { href: 'fake-item-url/item' }, + }, + id: 'item', + uuid: 'item', + owningCollection: createSuccessfulRemoteDataObject$(collection), + }); + + const mockVocabularyScrollable: Vocabulary = { + id: 'scrollable', + name: 'scrollable', + scrollable: true, + hierarchical: false, + preloadLevel: 0, + type: 'vocabulary', + _links: { + self: { + href: 'self', + }, + entries: { + href: 'entries', + }, + }, + }; + + const mockVocabularyHierarchical: Vocabulary = { + id: 'hierarchical', + name: 'hierarchical', + scrollable: false, + hierarchical: true, + preloadLevel: 2, + type: 'vocabulary', + _links: { + self: { + href: 'self', + }, + entries: { + href: 'entries', + }, + }, + }; + + const mockVocabularySuggester: Vocabulary = { + id: 'suggester', + name: 'suggester', + scrollable: false, + hierarchical: false, + preloadLevel: 0, + type: 'vocabulary', + _links: { + self: { + href: 'self', + }, + entries: { + href: 'entries', + }, + }, + }; + + let editMetadataValue: DsoEditMetadataValue; + let metadataValue: MetadataValue; + let metadataSchema: MetadataSchema; + let metadataFields: MetadataField[]; + + beforeEach(async () => { + itemService = jasmine.createSpyObj('itemService', { + findByHref: createSuccessfulRemoteDataObject$(item), + }); + vocabularyService = new VocabularyServiceStub(); + registryService = jasmine.createSpyObj('registryService', { + queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)), + }); + notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']); + + metadataValue = Object.assign(new MetadataValue(), { + value: 'Regular Name', + language: 'en', + place: 0, + authority: undefined, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + metadataSchema = Object.assign(new MetadataSchema(), { + id: 0, + prefix: 'metadata', + namespace: 'https://example.com/', + }); + metadataFields = [ + Object.assign(new MetadataField(), { + id: 0, + element: 'regular', + qualifier: null, + schema: createSuccessfulRemoteDataObject$(metadataSchema), + }), + ]; + dso = Object.assign(new DSpaceObject(), { + _links: { + self: { href: 'fake-dso-url/dso' }, + }, + }); + + await TestBed.configureTestingModule({ + imports: [ + DsoEditMetadataAuthorityFieldComponent, + TranslateModule.forRoot(), + ], + providers: [ + { provide: VocabularyService, useValue: vocabularyService }, + { provide: ItemDataService, useValue: itemService }, + { provide: RegistryService, useValue: registryService }, + { provide: NotificationsService, useValue: notificationsService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DsoEditMetadataAuthorityFieldComponent); + component = fixture.componentInstance; + component.mdValue = editMetadataValue; + component.dso = dso; + fixture.detectChanges(); + }); + + describe('when the metadata field uses a scrollable vocabulary and is editing', () => { + beforeEach(waitForAsync(() => { + spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyScrollable)); + metadataValue = Object.assign(new MetadataValue(), { + value: 'Authority Controlled value', + language: 'en', + place: 0, + authority: null, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + editMetadataValue.editing = true; + component.mdValue = editMetadataValue; + component.mdField = 'metadata.scrollable'; + component.ngOnInit(); + fixture.detectChanges(); + })); + + it('should render the DsDynamicScrollableDropdownComponent', () => { + expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('ds-dynamic-scrollable-dropdown'))).toBeTruthy(); + }); + + it('getModel should return a DynamicScrollableDropdownModel', () => { + const model = component.getModel(); + + expect(model instanceof DynamicScrollableDropdownModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name); + }); + }); + + describe('when the metadata field uses a hierarchical vocabulary and is editing', () => { + beforeEach(waitForAsync(() => { + spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyHierarchical)); + metadataValue = Object.assign(new MetadataValue(), { + value: 'Authority Controlled value', + language: 'en', + place: 0, + authority: null, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + editMetadataValue.editing = true; + component.mdValue = editMetadataValue; + component.mdField = 'metadata.hierarchical'; + component.ngOnInit(); + fixture.detectChanges(); + })); + + it('should render the DsDynamicOneboxComponent', () => { + expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy(); + }); + + it('getModel should return a DynamicOneboxModel', () => { + const model = component.getModel(); + + expect(model instanceof DynamicOneboxModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name); + }); + }); + + describe('when the metadata field uses a suggester vocabulary and is editing', () => { + beforeEach(waitForAsync(() => { + spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularySuggester)); + spyOn(component.confirm, 'emit'); + metadataValue = Object.assign(new MetadataValue(), { + value: 'Authority Controlled value', + language: 'en', + place: 0, + authority: 'authority-key', + confidence: ConfidenceType.CF_UNCERTAIN, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + editMetadataValue.editing = true; + component.mdValue = editMetadataValue; + component.mdField = 'metadata.suggester'; + component.ngOnInit(); + fixture.detectChanges(); + })); + + it('should render the DsDynamicOneboxComponent', () => { + expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy(); + }); + + it('getModel should return a DynamicOneboxModel', () => { + const model = component.getModel(); + + expect(model instanceof DynamicOneboxModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name); + }); + + describe('authority key edition', () => { + + it('should update confidence to CF_NOVALUE when authority is cleared', () => { + component.mdValue.newValue.authority = ''; + + component.onChangeAuthorityKey(); + + expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_NOVALUE); + expect(component.confirm.emit).toHaveBeenCalledWith(false); + }); + + it('should update confidence to CF_ACCEPTED when authority key is edited', () => { + component.mdValue.newValue.authority = 'newAuthority'; + component.mdValue.originalValue.authority = 'oldAuthority'; + + component.onChangeAuthorityKey(); + + expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED); + expect(component.confirm.emit).toHaveBeenCalledWith(false); + }); + + it('should not update confidence when authority key remains the same', () => { + component.mdValue.newValue.authority = 'sameAuthority'; + component.mdValue.originalValue.authority = 'sameAuthority'; + + component.onChangeAuthorityKey(); + + expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNCERTAIN); + expect(component.confirm.emit).not.toHaveBeenCalled(); + }); + + it('should call onChangeEditingAuthorityStatus with true when clicking the lock button', () => { + spyOn(component, 'onChangeEditingAuthorityStatus'); + const lockButton = fixture.nativeElement.querySelector('#metadata-confirm-btn'); + + lockButton.click(); + + expect(component.onChangeEditingAuthorityStatus).toHaveBeenCalledWith(true); + }); + + it('should disable the input when editingAuthority is false', (done) => { + component.editingAuthority = false; + + fixture.detectChanges(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]'); + expect(inputElement.disabled).toBeTruthy(); + done(); + }); + }); + + it('should enable the input when editingAuthority is true', (done) => { + component.editingAuthority = true; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]'); + expect(inputElement.disabled).toBeFalsy(); + done(); + }); + + + }); + + it('should update mdValue.newValue properties when authority is present', () => { + const event = { + value: 'Some value', + authority: 'Some authority', + }; + + component.onChangeAuthorityField(event); + + expect(component.mdValue.newValue.value).toBe(event.value); + expect(component.mdValue.newValue.authority).toBe(event.authority); + expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED); + expect(component.confirm.emit).toHaveBeenCalledWith(false); + }); + + it('should update mdValue.newValue properties when authority is not present', () => { + const event = { + value: 'Some value', + authority: null, + }; + + component.onChangeAuthorityField(event); + + expect(component.mdValue.newValue.value).toBe(event.value); + expect(component.mdValue.newValue.authority).toBeNull(); + expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNSET); + expect(component.confirm.emit).toHaveBeenCalledWith(false); + }); + + }); + + }); +}); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.ts new file mode 100644 index 00000000000..3dfc63d325c --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component.ts @@ -0,0 +1,303 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import { + FormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + BehaviorSubject, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { RegistryService } from '../../../../core/registry/registry.service'; +import { ConfidenceType } from '../../../../core/shared/confidence-type'; +import { + getFirstCompletedRemoteData, + metadataFieldsToString, +} from '../../../../core/shared/operators'; +import { Vocabulary } from '../../../../core/submission/vocabularies/models/vocabulary.model'; +import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { isNotEmpty } from '../../../../shared/empty.util'; +import { DsDynamicOneboxComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; +import { + DsDynamicOneboxModelConfig, + DynamicOneboxModel, +} from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; +import { DsDynamicScrollableDropdownComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; +import { + DynamicScrollableDropdownModel, + DynamicScrollableDropdownModelConfig, +} from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model'; +import { AuthorityConfidenceStateDirective } from '../../../../shared/form/directives/authority-confidence-state.directive'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { AbstractDsoEditMetadataValueFieldComponent } from '../abstract-dso-edit-metadata-value-field.component'; +import { DsoEditMetadataFieldService } from '../dso-edit-metadata-field.service'; + +/** + * The component used to gather input for authority controlled metadata fields + */ +@Component({ + selector: 'ds-dso-edit-metadata-authority-field', + templateUrl: './dso-edit-metadata-authority-field.component.html', + styleUrls: ['./dso-edit-metadata-authority-field.component.scss'], + standalone: true, + imports: [ + DsDynamicScrollableDropdownComponent, + NgIf, + DsDynamicOneboxComponent, + AuthorityConfidenceStateDirective, + NgbTooltipModule, + AsyncPipe, + TranslateModule, + FormsModule, + ], +}) +export class DsoEditMetadataAuthorityFieldComponent extends AbstractDsoEditMetadataValueFieldComponent implements OnInit, OnChanges { + + /** + * Whether the authority field is currently being edited + */ + public editingAuthority = false; + + /** + * Field group used by authority field + */ + group = new UntypedFormGroup({ authorityField: new UntypedFormControl() }); + + /** + * Model to use for editing authorities values + */ + private model$: BehaviorSubject = new BehaviorSubject(null); + + /** + * Observable with information about the authority vocabulary used + */ + private vocabulary$: Observable; + + /** + * Observables with information about the authority vocabulary type used + */ + isAuthorityControlled$: Observable; + isHierarchicalVocabulary$: Observable; + isScrollableVocabulary$: Observable; + isSuggesterVocabulary$: Observable; + + constructor( + protected cdr: ChangeDetectorRef, + protected dsoEditMetadataFieldService: DsoEditMetadataFieldService, + protected itemService: ItemDataService, + protected notificationsService: NotificationsService, + protected registryService: RegistryService, + protected translate: TranslateService, + ) { + super(); + } + + ngOnInit(): void { + this.initAuthorityProperties(); + } + + /** + * Initialise potential properties of a authority controlled metadata field + */ + initAuthorityProperties(): void { + this.vocabulary$ = this.dsoEditMetadataFieldService.findDsoFieldVocabulary(this.dso, this.mdField); + + this.isAuthorityControlled$ = this.vocabulary$.pipe( + // Create the model used by the authority fields to ensure its existence when the field is initialized + tap((v: Vocabulary) => this.model$.next(this.createModel(v))), + map((result: Vocabulary) => isNotEmpty(result)), + ); + + this.isHierarchicalVocabulary$ = this.vocabulary$.pipe( + map((result: Vocabulary) => isNotEmpty(result) && result.hierarchical), + ); + + this.isScrollableVocabulary$ = this.vocabulary$.pipe( + map((result: Vocabulary) => isNotEmpty(result) && result.scrollable), + ); + + this.isSuggesterVocabulary$ = this.vocabulary$.pipe( + map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable), + ); + + } + + /** + * Returns a {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model based on the + * vocabulary used. + */ + private createModel(vocabulary: Vocabulary): DynamicOneboxModel | DynamicScrollableDropdownModel { + if (isNotEmpty(vocabulary)) { + let formFieldValue: FormFieldMetadataValueObject | string; + if (isNotEmpty(this.mdValue.newValue.value)) { + formFieldValue = new FormFieldMetadataValueObject(); + formFieldValue.value = this.mdValue.newValue.value; + formFieldValue.display = this.mdValue.newValue.value; + if (this.mdValue.newValue.authority) { + formFieldValue.authority = this.mdValue.newValue.authority; + formFieldValue.confidence = this.mdValue.newValue.confidence; + } + } else { + formFieldValue = this.mdValue.newValue.value; + } + + const vocabularyOptions = vocabulary ? { + closed: false, + name: vocabulary.name, + } as VocabularyOptions : null; + + if (!vocabulary.scrollable) { + const model: DsDynamicOneboxModelConfig = { + id: 'authorityField', + label: `${this.dsoType}.edit.metadata.edit.value`, + vocabularyOptions: vocabularyOptions, + metadataFields: [this.mdField], + value: formFieldValue, + repeatable: false, + submissionId: 'edit-metadata', + hasSelectableMetadata: false, + }; + return new DynamicOneboxModel(model); + } else { + const model: DynamicScrollableDropdownModelConfig = { + id: 'authorityField', + label: `${this.dsoType}.edit.metadata.edit.value`, + placeholder: `${this.dsoType}.edit.metadata.edit.value`, + vocabularyOptions: vocabularyOptions, + metadataFields: [this.mdField], + value: formFieldValue, + repeatable: false, + submissionId: 'edit-metadata', + hasSelectableMetadata: false, + maxOptions: 10, + }; + return new DynamicScrollableDropdownModel(model); + } + } else { + return null; + } + } + + /** + * Change callback for the component. Check if the mdField has changed to retrieve whether it is metadata + * that uses a controlled vocabulary and update the related properties + * + * @param {SimpleChanges} changes + */ + ngOnChanges(changes: SimpleChanges): void { + if (isNotEmpty(changes.mdField) && !changes.mdField.firstChange) { + if (isNotEmpty(changes.mdField.currentValue)) { + if (isNotEmpty(changes.mdField.previousValue) && + changes.mdField.previousValue !== changes.mdField.currentValue) { + // Clear authority value in case it has been assigned with the previous metadataField used + this.mdValue.newValue.authority = null; + this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET; + } + + // Only ask if the current mdField have a period character to reduce request + if (changes.mdField.currentValue.includes('.')) { + this.validateMetadataField().subscribe((isValid: boolean) => { + if (isValid) { + this.initAuthorityProperties(); + this.cdr.detectChanges(); + } + }); + } + } + } + } + + /** + * Validate the metadata field to check if it exists on the server and return an observable boolean for success/error + */ + validateMetadataField(): Observable { + return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe( + getFirstCompletedRemoteData(), + switchMap((rd) => { + if (rd.hasSucceeded) { + return observableOf(rd).pipe( + metadataFieldsToString(), + take(1), + map((fields: string[]) => fields.indexOf(this.mdField) > -1), + ); + } else { + this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage); + return [false]; + } + }), + ); + } + + /** + * Process the change of authority field value updating the authority key and confidence as necessary + */ + onChangeAuthorityField(event): void { + this.mdValue.newValue.value = event.value; + if (event.authority) { + this.mdValue.newValue.authority = event.authority; + this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED; + } else { + this.mdValue.newValue.authority = null; + this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET; + } + this.confirm.emit(false); + } + + /** + * Returns the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used + * for the authority field + */ + getModel(): DynamicOneboxModel | DynamicScrollableDropdownModel { + return this.model$.value; + } + + /** + * Change the status of the editingAuthority property + * @param status + */ + onChangeEditingAuthorityStatus(status: boolean) { + this.editingAuthority = status; + } + + /** + * Processes the change in authority value, updating the confidence as necessary. + * If the authority key is cleared, the confidence is set to {@link ConfidenceType.CF_NOVALUE}. + * If the authority key is edited and differs from the original, the confidence is set to {@link ConfidenceType.CF_ACCEPTED}. + */ + onChangeAuthorityKey() { + if (this.mdValue.newValue.authority === '') { + this.mdValue.newValue.confidence = ConfidenceType.CF_NOVALUE; + this.confirm.emit(false); + } else if (this.mdValue.newValue.authority !== this.mdValue.originalValue.authority) { + this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED; + this.confirm.emit(false); + } + } + +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.html index 514f3147bb5..92c7985a619 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.html @@ -1,4 +1,5 @@ - diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.spec.ts index 83bd25cc763..5b92acdfad2 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.spec.ts @@ -17,7 +17,7 @@ describe('DsoEditMetadataEntityFieldComponent', () => { entityTypeService = new EntityTypeDataServiceStub(); await TestBed.configureTestingModule({ - declarations: [ + imports: [ DsoEditMetadataEntityFieldComponent, ], providers: [ diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.ts index 73cf16b2dab..fda6f9b24fc 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component.ts @@ -1,15 +1,19 @@ +import { + AsyncPipe, + NgForOf, +} from '@angular/common'; import { Component, OnInit, } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { EntityTypeDataService } from '../../../../core/data/entity-type-data.service'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; import { AbstractDsoEditMetadataValueFieldComponent } from '../abstract-dso-edit-metadata-value-field.component'; -import { EditMetadataValueFieldType } from '../dso-edit-metadata-field-type.enum'; -import { editMetadataValueFieldComponent } from '../dso-edit-metadata-value-field-loader/dso-edit-metadata-value-field.decorator'; /** * The component used to gather input for entity-type metadata fields @@ -18,8 +22,14 @@ import { editMetadataValueFieldComponent } from '../dso-edit-metadata-value-fiel selector: 'ds-dso-edit-metadata-entity-field', templateUrl: './dso-edit-metadata-entity-field.component.html', styleUrls: ['./dso-edit-metadata-entity-field.component.scss'], + standalone: true, + imports: [ + AsyncPipe, + FormsModule, + NgForOf, + TranslateModule, + ], }) -@editMetadataValueFieldComponent(EditMetadataValueFieldType.ENTITY_TYPE) export class DsoEditMetadataEntityFieldComponent extends AbstractDsoEditMetadataValueFieldComponent implements OnInit { /** diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum.ts index 5a9b3c493ef..cc9c105c4f3 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum.ts @@ -4,4 +4,5 @@ export enum EditMetadataValueFieldType { PLAIN_TEXT = 'PLAIN_TEXT', ENTITY_TYPE = 'ENTITY_TYPE', + AUTHORITY = 'AUTHORITY', } diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.spec.ts new file mode 100644 index 00000000000..daea727838b --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing'; + +import { ItemDataService } from '../../../core/data/item-data.service'; +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; +import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub'; +import { VocabularyServiceStub } from '../../../shared/testing/vocabulary-service.stub'; +import { DsoEditMetadataFieldService } from './dso-edit-metadata-field.service'; + +describe('DsoEditMetadataFieldService', () => { + let service: DsoEditMetadataFieldService; + + let itemService: ItemDataServiceStub; + let vocabularyService: VocabularyServiceStub; + + beforeEach(() => { + itemService = new ItemDataServiceStub(); + vocabularyService = new VocabularyServiceStub(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ItemDataService, useValue: itemService }, + { provide: VocabularyService, useValue: vocabularyService }, + ], + }); + service = TestBed.inject(DsoEditMetadataFieldService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.ts new file mode 100644 index 00000000000..d3ccf323a6e --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-field.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { ItemDataService } from '../../../core/data/item-data.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { Item } from '../../../core/shared/item.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model'; +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; + +@Injectable({ + providedIn: 'root', +}) +export class DsoEditMetadataFieldService { + + constructor( + protected itemService: ItemDataService, + protected vocabularyService: VocabularyService, + ) { + } + + /** + * Find the vocabulary of the given {@link mdField} for the given item. + * + * @param dso The item + * @param mdField The metadata field + */ + findDsoFieldVocabulary(dso: DSpaceObject, mdField: string): Observable { + if (isNotEmpty(mdField)) { + const owningCollection$: Observable = this.itemService.findByHref(dso._links.self.href, true, true, followLink('owningCollection')).pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((item: Item) => item.owningCollection), + getFirstSucceededRemoteDataPayload(), + ); + + return owningCollection$.pipe( + switchMap((c: Collection) => this.vocabularyService.getVocabularyByMetadataAndCollection(mdField, c.uuid).pipe( + getFirstSucceededRemoteDataPayload(), + )), + ); + } else { + return observableOf(undefined); + } + } +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-text-field/dso-edit-metadata-text-field.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-text-field/dso-edit-metadata-text-field.component.html index 97e49ae39e9..a2c754044cf 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-text-field/dso-edit-metadata-text-field.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value-field/dso-edit-metadata-text-field/dso-edit-metadata-text-field.component.html @@ -1,4 +1,5 @@ - - - - + +
-
-
- - - - -
-
{{ mdRepresentationName$ | async }} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index e96959c1d1f..6b70550fae0 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -8,27 +8,12 @@ import { waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; +import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { MetadataField } from 'src/app/core/metadata/metadata-field.model'; -import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model'; -import { RegistryService } from 'src/app/core/registry/registry.service'; -import { ConfidenceType } from 'src/app/core/shared/confidence-type'; -import { Vocabulary } from 'src/app/core/submission/vocabularies/models/vocabulary.model'; -import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service'; -import { DynamicOneboxModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; -import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; -import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; -import { createPaginatedList } from 'src/app/shared/testing/utils.test'; -import { VocabularyServiceStub } from 'src/app/shared/testing/vocabulary-service.stub'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Item } from '../../../core/shared/item.model'; import { MetadataValue, VIRTUAL_METADATA_PREFIX, @@ -37,12 +22,13 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { DsoEditMetadataFieldServiceStub } from '../../../shared/testing/dso-edit-metadata-field.service.stub'; import { VarDirective } from '../../../shared/utils/var.directive'; import { DsoEditMetadataChangeType, DsoEditMetadataValue, } from '../dso-edit-metadata-form'; +import { DsoEditMetadataFieldService } from '../dso-edit-metadata-value-field/dso-edit-metadata-field.service'; import { DsoEditMetadataValueComponent } from './dso-edit-metadata-value.component'; const EDIT_BTN = 'edit'; @@ -57,97 +43,12 @@ describe('DsoEditMetadataValueComponent', () => { let relationshipService: RelationshipDataService; let dsoNameService: DSONameService; - let vocabularyServiceStub: any; - let itemService: ItemDataService; - let registryService: RegistryService; - let notificationsService: NotificationsService; + let dsoEditMetadataFieldService: DsoEditMetadataFieldServiceStub; let editMetadataValue: DsoEditMetadataValue; let metadataValue: MetadataValue; - let dso: DSpaceObject; - - const collection = Object.assign(new Collection(), { - uuid: 'fake-uuid', - }); - - const item = Object.assign(new Item(), { - _links: { - self: { href: 'fake-item-url/item' }, - }, - id: 'item', - uuid: 'item', - owningCollection: createSuccessfulRemoteDataObject$(collection), - }); - - const mockVocabularyScrollable: Vocabulary = { - id: 'scrollable', - name: 'scrollable', - scrollable: true, - hierarchical: false, - preloadLevel: 0, - type: 'vocabulary', - _links: { - self: { - href: 'self', - }, - entries: { - href: 'entries', - }, - }, - }; - - const mockVocabularyHierarchical: Vocabulary = { - id: 'hierarchical', - name: 'hierarchical', - scrollable: false, - hierarchical: true, - preloadLevel: 2, - type: 'vocabulary', - _links: { - self: { - href: 'self', - }, - entries: { - href: 'entries', - }, - }, - }; - - const mockVocabularySuggester: Vocabulary = { - id: 'suggester', - name: 'suggester', - scrollable: false, - hierarchical: false, - preloadLevel: 0, - type: 'vocabulary', - _links: { - self: { - href: 'self', - }, - entries: { - href: 'entries', - }, - }, - }; - - let metadataSchema: MetadataSchema; - let metadataFields: MetadataField[]; function initServices(): void { - metadataSchema = Object.assign(new MetadataSchema(), { - id: 0, - prefix: 'metadata', - namespace: 'http://example.com/', - }); - metadataFields = [ - Object.assign(new MetadataField(), { - id: 0, - element: 'regular', - qualifier: null, - schema: createSuccessfulRemoteDataObject$(metadataSchema), - }), - ]; - relationshipService = jasmine.createSpyObj('relationshipService', { resolveMetadataRepresentation: of( new ItemMetadataRepresentation(metadataValue), @@ -156,14 +57,7 @@ describe('DsoEditMetadataValueComponent', () => { dsoNameService = jasmine.createSpyObj('dsoNameService', { getName: 'Related Name', }); - itemService = jasmine.createSpyObj('itemService', { - findByHref: createSuccessfulRemoteDataObject$(item), - }); - vocabularyServiceStub = new VocabularyServiceStub(); - registryService = jasmine.createSpyObj('registryService', { - queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)), - }); - notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']); + dsoEditMetadataFieldService = new DsoEditMetadataFieldServiceStub(); } beforeEach(waitForAsync(async () => { @@ -174,28 +68,20 @@ describe('DsoEditMetadataValueComponent', () => { authority: undefined, }); editMetadataValue = new DsoEditMetadataValue(metadataValue); - dso = Object.assign(new DSpaceObject(), { - _links: { - self: { href: 'fake-dso-url/dso' }, - }, - }); initServices(); await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - RouterTestingModule.withRoutes([]), + RouterModule.forRoot([]), DsoEditMetadataValueComponent, VarDirective, ], providers: [ { provide: RelationshipDataService, useValue: relationshipService }, { provide: DSONameService, useValue: dsoNameService }, - { provide: VocabularyService, useValue: vocabularyServiceStub }, - { provide: ItemDataService, useValue: itemService }, - { provide: RegistryService, useValue: registryService }, - { provide: NotificationsService, useValue: notificationsService }, + { provide: DsoEditMetadataFieldService, useValue: dsoEditMetadataFieldService }, ], schemas: [NO_ERRORS_SCHEMA], }) @@ -211,7 +97,6 @@ describe('DsoEditMetadataValueComponent', () => { fixture = TestBed.createComponent(DsoEditMetadataValueComponent); component = fixture.componentInstance; component.mdValue = editMetadataValue; - component.dso = dso; component.saving$ = of(false); fixture.detectChanges(); }); @@ -297,219 +182,6 @@ describe('DsoEditMetadataValueComponent', () => { assertButton(DRAG_BTN, true, false); }); - describe('when the metadata field not uses a vocabulary and is editing', () => { - beforeEach(waitForAsync(() => { - spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(null, 204)); - metadataValue = Object.assign(new MetadataValue(), { - value: 'Regular value', - language: 'en', - place: 0, - authority: null, - }); - editMetadataValue = new DsoEditMetadataValue(metadataValue); - editMetadataValue.editing = true; - component.mdValue = editMetadataValue; - component.mdField = 'metadata.regular'; - component.ngOnInit(); - fixture.detectChanges(); - })); - - it('should render a textarea', () => { - expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('textarea'))).toBeTruthy(); - }); - }); - - describe('when the metadata field uses a scrollable vocabulary and is editing', () => { - beforeEach(waitForAsync(() => { - spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyScrollable)); - metadataValue = Object.assign(new MetadataValue(), { - value: 'Authority Controlled value', - language: 'en', - place: 0, - authority: null, - }); - editMetadataValue = new DsoEditMetadataValue(metadataValue); - editMetadataValue.editing = true; - component.mdValue = editMetadataValue; - component.mdField = 'metadata.scrollable'; - component.ngOnInit(); - fixture.detectChanges(); - })); - - it('should render the DsDynamicScrollableDropdownComponent', () => { - expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('ds-dynamic-scrollable-dropdown'))).toBeTruthy(); - }); - - it('getModel should return a DynamicScrollableDropdownModel', () => { - const model = component.getModel(); - - expect(model instanceof DynamicScrollableDropdownModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name); - - }); - }); - - describe('when the metadata field uses a hierarchical vocabulary and is editing', () => { - beforeEach(waitForAsync(() => { - spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyHierarchical)); - metadataValue = Object.assign(new MetadataValue(), { - value: 'Authority Controlled value', - language: 'en', - place: 0, - authority: null, - }); - editMetadataValue = new DsoEditMetadataValue(metadataValue); - editMetadataValue.editing = true; - component.mdValue = editMetadataValue; - component.mdField = 'metadata.hierarchical'; - component.ngOnInit(); - fixture.detectChanges(); - })); - - it('should render the DsDynamicOneboxComponent', () => { - expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy(); - }); - - it('getModel should return a DynamicOneboxModel', () => { - const model = component.getModel(); - - expect(model instanceof DynamicOneboxModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name); - }); - }); - - describe('when the metadata field uses a suggester vocabulary and is editing', () => { - beforeEach(waitForAsync(() => { - spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularySuggester)); - spyOn(component.confirm, 'emit'); - metadataValue = Object.assign(new MetadataValue(), { - value: 'Authority Controlled value', - language: 'en', - place: 0, - authority: 'authority-key', - confidence: ConfidenceType.CF_UNCERTAIN, - }); - editMetadataValue = new DsoEditMetadataValue(metadataValue); - editMetadataValue.editing = true; - component.mdValue = editMetadataValue; - component.mdField = 'metadata.suggester'; - component.ngOnInit(); - fixture.detectChanges(); - })); - - it('should render the DsDynamicOneboxComponent', () => { - expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy(); - }); - - it('getModel should return a DynamicOneboxModel', () => { - const model = component.getModel(); - - expect(model instanceof DynamicOneboxModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name); - }); - - describe('authority key edition', () => { - - it('should update confidence to CF_NOVALUE when authority is cleared', () => { - component.mdValue.newValue.authority = ''; - - component.onChangeAuthorityKey(); - - expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_NOVALUE); - expect(component.confirm.emit).toHaveBeenCalledWith(false); - }); - - it('should update confidence to CF_ACCEPTED when authority key is edited', () => { - component.mdValue.newValue.authority = 'newAuthority'; - component.mdValue.originalValue.authority = 'oldAuthority'; - - component.onChangeAuthorityKey(); - - expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED); - expect(component.confirm.emit).toHaveBeenCalledWith(false); - }); - - it('should not update confidence when authority key remains the same', () => { - component.mdValue.newValue.authority = 'sameAuthority'; - component.mdValue.originalValue.authority = 'sameAuthority'; - - component.onChangeAuthorityKey(); - - expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNCERTAIN); - expect(component.confirm.emit).not.toHaveBeenCalled(); - }); - - it('should call onChangeEditingAuthorityStatus with true when clicking the lock button', () => { - spyOn(component, 'onChangeEditingAuthorityStatus'); - const lockButton = fixture.nativeElement.querySelector('#metadata-confirm-btn'); - - lockButton.click(); - - expect(component.onChangeEditingAuthorityStatus).toHaveBeenCalledWith(true); - }); - - it('should disable the input when editingAuthority is false', (done) => { - component.editingAuthority = false; - - fixture.detectChanges(); - - fixture.detectChanges(); - fixture.whenStable().then(() => { - const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]'); - expect(inputElement.disabled).toBeTruthy(); - done(); - }); - }); - - it('should enable the input when editingAuthority is true', (done) => { - component.editingAuthority = true; - - fixture.detectChanges(); - fixture.whenStable().then(() => { - const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]'); - expect(inputElement.disabled).toBeFalsy(); - done(); - }); - - - }); - - it('should update mdValue.newValue properties when authority is present', () => { - const event = { - value: 'Some value', - authority: 'Some authority', - }; - - component.onChangeAuthorityField(event); - - expect(component.mdValue.newValue.value).toBe(event.value); - expect(component.mdValue.newValue.authority).toBe(event.authority); - expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED); - expect(component.confirm.emit).toHaveBeenCalledWith(false); - }); - - it('should update mdValue.newValue properties when authority is not present', () => { - const event = { - value: 'Some value', - authority: null, - }; - - component.onChangeAuthorityField(event); - - expect(component.mdValue.newValue.value).toBe(event.value); - expect(component.mdValue.newValue.authority).toBeNull(); - expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNSET); - expect(component.confirm.emit).toHaveBeenCalledWith(false); - }); - - }); - - }); - function assertButton(name: string, exists: boolean, disabled: boolean = false): void { describe(`${name} button`, () => { let btn: DebugElement; diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts index ddc254dbec1..fb65fd10b36 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -8,7 +8,6 @@ import { NgIf, } from '@angular/common'; import { - ChangeDetectorRef, Component, EventEmitter, Input, @@ -17,85 +16,48 @@ import { Output, SimpleChanges, } from '@angular/core'; -import { - FormsModule, - UntypedFormControl, - UntypedFormGroup, -} from '@angular/forms'; +import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; -import { - BehaviorSubject, EMPTY, Observable, - of as observableOf, } from 'rxjs'; -import { - map, - switchMap, - take, - tap, -} from 'rxjs/operators'; -import { RegistryService } from 'src/app/core/registry/registry.service'; -import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service'; -import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; +import { map } from 'rxjs/operators'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { MetadataService } from '../../../core/metadata/metadata.service'; -import { Collection } from '../../../core/shared/collection.model'; import { ConfidenceType } from '../../../core/shared/confidence-type'; +import { Context } from '../../../core/shared/context.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Item } from '../../../core/shared/item.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { MetadataRepresentation, MetadataRepresentationType, } from '../../../core/shared/metadata-representation/metadata-representation.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload, - getRemoteDataPayload, - metadataFieldsToString, -} from '../../../core/shared/operators'; import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model'; -import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; -import { isNotEmpty } from '../../../shared/empty.util'; -import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; -import { - DsDynamicOneboxModelConfig, - DynamicOneboxModel, -} from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; -import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; -import { - DynamicScrollableDropdownModel, - DynamicScrollableDropdownModelConfig, -} from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; -import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { hasValue } from '../../../shared/empty.util'; import { AuthorityConfidenceStateDirective } from '../../../shared/form/directives/authority-confidence-state.directive'; import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component'; import { DebounceDirective } from '../../../shared/utils/debounce.directive'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { VarDirective } from '../../../shared/utils/var.directive'; import { DsoEditMetadataChangeType, DsoEditMetadataValue, } from '../dso-edit-metadata-form'; +import { DsoEditMetadataFieldService } from '../dso-edit-metadata-value-field/dso-edit-metadata-field.service'; import { EditMetadataValueFieldType } from '../dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum'; +import { DsoEditMetadataValueFieldLoaderComponent } from '../dso-edit-metadata-value-field/dso-edit-metadata-value-field-loader/dso-edit-metadata-value-field-loader.component'; @Component({ selector: 'ds-dso-edit-metadata-value', styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'], templateUrl: './dso-edit-metadata-value.component.html', standalone: true, - imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective], + imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsoEditMetadataValueFieldLoaderComponent, AuthorityConfidenceStateDirective], }) /** * Component displaying a single editable row for a metadata value @@ -169,12 +131,6 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { */ public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType; - /** - * The ConfidenceType enumeration for access in the component's template - * @type {ConfidenceType} - */ - public ConfidenceTypeEnum = ConfidenceType; - /** * The item this metadata value represents in case it's virtual (if any, otherwise null) */ @@ -190,58 +146,25 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { */ mdRepresentationName$: Observable; - fieldType: EditMetadataValueFieldType; - - /** - * Whether or not the authority field is currently being edited - */ - public editingAuthority = false; - - /** - * Field group used by authority field - * @type {UntypedFormGroup} - */ - group = new UntypedFormGroup({ authorityField : new UntypedFormControl() }); - - /** - * Model to use for editing authorities values - */ - private model$: BehaviorSubject = new BehaviorSubject(null); - - /** - * Observable with information about the authority vocabulary used - */ - private vocabulary$: Observable; + fieldType$: Observable; - /** - * Observables with information about the authority vocabulary type used - */ - private isAuthorityControlled$: Observable; - private isHierarchicalVocabulary$: Observable; - private isScrollableVocabulary$: Observable; - private isSuggesterVocabulary$: Observable; + readonly ConfidenceTypeEnum = ConfidenceType; constructor( protected relationshipService: RelationshipDataService, protected dsoNameService: DSONameService, - protected vocabularyService: VocabularyService, - protected itemService: ItemDataService, - protected cdr: ChangeDetectorRef, - protected registryService: RegistryService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, protected metadataService: MetadataService, + protected dsoEditMetadataFieldService: DsoEditMetadataFieldService, ) { } ngOnInit(): void { this.initVirtualProperties(); - this.initAuthorityProperties(); } ngOnChanges(changes: SimpleChanges): void { if (changes.mdField) { - this.fieldType = this.getFieldType(); + this.fieldType$ = this.getFieldType(); } } @@ -267,239 +190,18 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { /** * Retrieves the {@link EditMetadataValueFieldType} to be displayed for the current field while in edit mode. */ - getFieldType(): EditMetadataValueFieldType { - if (this.mdField === 'dspace.entity.type') { - return EditMetadataValueFieldType.ENTITY_TYPE; - } - return EditMetadataValueFieldType.PLAIN_TEXT; - } - - /** - * Initialise potential properties of a authority controlled metadata field - */ - initAuthorityProperties(): void { - - if (isNotEmpty(this.mdField)) { - - const owningCollection$: Observable = this.itemService.findByHref(this.dso._links.self.href, true, true, followLink('owningCollection')) - .pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - switchMap((item: Item) => item.owningCollection), - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ); - - this.vocabulary$ = owningCollection$.pipe( - switchMap((c: Collection) => this.vocabularyService - .getVocabularyByMetadataAndCollection(this.mdField, c.uuid) - .pipe( - getFirstSucceededRemoteDataPayload(), - )), - ); - } else { - this.vocabulary$ = observableOf(undefined); - } - - this.isAuthorityControlled$ = this.vocabulary$.pipe( - // Create the model used by the authority fields to ensure its existence when the field is initialized - tap((v: Vocabulary) => this.model$.next(this.createModel(v))), - map((result: Vocabulary) => isNotEmpty(result)), - ); - - this.isHierarchicalVocabulary$ = this.vocabulary$.pipe( - map((result: Vocabulary) => isNotEmpty(result) && result.hierarchical), - ); - - this.isScrollableVocabulary$ = this.vocabulary$.pipe( - map((result: Vocabulary) => isNotEmpty(result) && result.scrollable), - ); - - this.isSuggesterVocabulary$ = this.vocabulary$.pipe( - map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable), - ); - - } - - /** - * Returns a {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model based on the - * vocabulary used. - */ - private createModel(vocabulary: Vocabulary): DynamicOneboxModel | DynamicScrollableDropdownModel { - if (isNotEmpty(vocabulary)) { - let formFieldValue; - if (isNotEmpty(this.mdValue.newValue.value)) { - formFieldValue = new FormFieldMetadataValueObject(); - formFieldValue.value = this.mdValue.newValue.value; - formFieldValue.display = this.mdValue.newValue.value; - if (this.mdValue.newValue.authority) { - formFieldValue.authority = this.mdValue.newValue.authority; - formFieldValue.confidence = this.mdValue.newValue.confidence; - } - } else { - formFieldValue = this.mdValue.newValue.value; - } - - const vocabularyOptions = vocabulary ? { - closed: false, - name: vocabulary.name, - } as VocabularyOptions : null; - - if (!vocabulary.scrollable) { - const model: DsDynamicOneboxModelConfig = { - id: 'authorityField', - label: `${this.dsoType}.edit.metadata.edit.value`, - vocabularyOptions: vocabularyOptions, - metadataFields: [this.mdField], - value: formFieldValue, - repeatable: false, - submissionId: 'edit-metadata', - hasSelectableMetadata: false, - }; - return new DynamicOneboxModel(model); - } else { - const model: DynamicScrollableDropdownModelConfig = { - id: 'authorityField', - label: `${this.dsoType}.edit.metadata.edit.value`, - placeholder: `${this.dsoType}.edit.metadata.edit.value`, - vocabularyOptions: vocabularyOptions, - metadataFields: [this.mdField], - value: formFieldValue, - repeatable: false, - submissionId: 'edit-metadata', - hasSelectableMetadata: false, - maxOptions: 10, - }; - return new DynamicScrollableDropdownModel(model); - } - } else { - return null; - } - } - - /** - * Change callback for the component. Check if the mdField has changed to retrieve whether it is metadata - * that uses a controlled vocabulary and update the related properties - * - * @param {SimpleChanges} changes - */ - ngOnChanges(changes: SimpleChanges): void { - if (isNotEmpty(changes.mdField) && !changes.mdField.firstChange) { - if (isNotEmpty(changes.mdField.currentValue) ) { - if (isNotEmpty(changes.mdField.previousValue) && - changes.mdField.previousValue !== changes.mdField.currentValue) { - // Clear authority value in case it has been assigned with the previous metadataField used - this.mdValue.newValue.authority = null; - this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET; - } - - // Only ask if the current mdField have a period character to reduce request - if (changes.mdField.currentValue.includes('.')) { - this.validateMetadataField().subscribe((isValid: boolean) => { - if (isValid) { - this.initAuthorityProperties(); - this.cdr.detectChanges(); - } - }); + getFieldType(): Observable { + return this.dsoEditMetadataFieldService.findDsoFieldVocabulary(this.dso, this.mdField).pipe( + map((vocabulary: Vocabulary) => { + if (hasValue(vocabulary)) { + return EditMetadataValueFieldType.AUTHORITY; } - } - } - } - - /** - * Validate the metadata field to check if it exists on the server and return an observable boolean for success/error - */ - validateMetadataField(): Observable { - return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe( - getFirstCompletedRemoteData(), - switchMap((rd) => { - if (rd.hasSucceeded) { - return observableOf(rd).pipe( - metadataFieldsToString(), - take(1), - map((fields: string[]) => fields.indexOf(this.mdField) > -1), - ); - } else { - this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage); - return [false]; + if (this.mdField === 'dspace.entity.type') { + return EditMetadataValueFieldType.ENTITY_TYPE; } + return EditMetadataValueFieldType.PLAIN_TEXT; }), ); } - /** - * Checks if this field use a authority vocabulary - */ - isAuthorityControlled(): Observable { - return this.isAuthorityControlled$; - } - - /** - * Checks if configured vocabulary is Hierarchical or not - */ - isHierarchicalVocabulary(): Observable { - return this.isHierarchicalVocabulary$; - } - - /** - * Checks if configured vocabulary is Scrollable or not - */ - isScrollableVocabulary(): Observable { - return this.isScrollableVocabulary$; - } - - /** - * Checks if configured vocabulary is Suggester or not - * (a vocabulary not Scrollable and not Hierarchical that uses an autocomplete field) - */ - isSuggesterVocabulary(): Observable { - return this.isSuggesterVocabulary$; - } - - /** - * Process the change of authority field value updating the authority key and confidence as necessary - */ - onChangeAuthorityField(event): void { - this.mdValue.newValue.value = event.value; - if (event.authority) { - this.mdValue.newValue.authority = event.authority; - this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED; - } else { - this.mdValue.newValue.authority = null; - this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET; - } - this.confirm.emit(false); - } - - /** - * Returns the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used - * for the authority field - */ - getModel(): DynamicOneboxModel | DynamicScrollableDropdownModel { - return this.model$.value; - } - - /** - * Change the status of the editingAuthority property - * @param status - */ - onChangeEditingAuthorityStatus(status: boolean) { - this.editingAuthority = status; - } - - /** - * Processes the change in authority value, updating the confidence as necessary. - * If the authority key is cleared, the confidence is set to {@link ConfidenceType.CF_NOVALUE}. - * If the authority key is edited and differs from the original, the confidence is set to {@link ConfidenceType.CF_ACCEPTED}. - */ - onChangeAuthorityKey() { - if (this.mdValue.newValue.authority === '') { - this.mdValue.newValue.confidence = ConfidenceType.CF_NOVALUE; - this.confirm.emit(false); - } else if (this.mdValue.newValue.authority !== this.mdValue.originalValue.authority) { - this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED; - this.confirm.emit(false); - } - } - } diff --git a/src/app/shared/testing/dso-edit-metadata-field.service.stub.ts b/src/app/shared/testing/dso-edit-metadata-field.service.stub.ts new file mode 100644 index 00000000000..8c437e07c98 --- /dev/null +++ b/src/app/shared/testing/dso-edit-metadata-field.service.stub.ts @@ -0,0 +1,18 @@ +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { Vocabulary } from '../../core/submission/vocabularies/models/vocabulary.model'; + +/** + * Stub class of {@link DsoEditMetadataFieldService} + */ +export class DsoEditMetadataFieldServiceStub { + + findDsoFieldVocabulary(_dso: DSpaceObject, _mdField: string): Observable { + return observableOf(undefined); + } + +}