From c4de31ec4d50c137095fb738d383ee9d0cce25a0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 4 Jan 2023 11:55:37 +0100 Subject: [PATCH] 97742: Removing old item-metadata component & adding support for itemtemplate dso-edit-metadata --- .../collection-page/collection-page.module.ts | 4 +- .../edit-item-template-page.component.html | 2 +- .../dso-edit-metadata.component.html | 2 +- .../dso-edit-metadata.component.ts | 14 +- .../metadata-field-selector.component.spec.ts | 21 +- .../metadata-field-selector.component.ts | 28 +- .../edit-item-page/edit-item-page.module.ts | 8 - .../edit-in-place-field.component.html | 71 --- .../edit-in-place-field.component.scss | 13 - .../edit-in-place-field.component.spec.ts | 505 ------------------ .../edit-in-place-field.component.ts | 201 ------- .../item-metadata.component.html | 69 --- .../item-metadata.component.scss | 20 - .../item-metadata.component.spec.ts | 290 ---------- .../item-metadata/item-metadata.component.ts | 135 ----- src/assets/i18n/en.json5 | 60 +++ 16 files changed, 114 insertions(+), 1329 deletions(-) delete mode 100644 src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html delete mode 100644 src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss delete mode 100644 src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts delete mode 100644 src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html delete mode 100644 src/app/item-page/edit-item-page/item-metadata/item-metadata.component.scss delete mode 100644 src/app/item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index 3652823200..ff4c2cc13e 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -15,6 +15,7 @@ import { StatisticsModule } from '../statistics/statistics.module'; import { CollectionFormModule } from './collection-form/collection-form.module'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; import { ComcolModule } from '../shared/comcol/comcol.module'; +import { DsoSharedModule } from '../dso-shared/dso-shared.module'; @NgModule({ imports: [ @@ -24,7 +25,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module'; StatisticsModule.forRoot(), EditItemPageModule, CollectionFormModule, - ComcolModule + ComcolModule, + DsoSharedModule, ], declarations: [ CollectionPageComponent, diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html index f8c5c92e96..7cff79cce5 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -3,7 +3,7 @@

{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}

- +
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 71d3b34e42..51f347e163 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -58,7 +58,7 @@
-
+
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index 7f98720b2c..a2ceaa21d6 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -45,7 +45,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { * Resolved update data-service for the given DSpaceObject (depending on its type, e.g. ItemDataService for an Item) * Used to send the PATCH request */ - updateDataService: UpdateDataService; + @Input() updateDataService: UpdateDataService; /** * Type of the DSpaceObject in String @@ -143,11 +143,13 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { } else { type = this.dso.type; } - const provider = getDataServiceFor(type); - this.updateDataService = Injector.create({ - providers: [], - parent: this.parentInjector - }).get(provider); + if (hasNoValue(this.updateDataService)) { + const provider = getDataServiceFor(type); + this.updateDataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + } this.dsoType = type.value; } diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts index ee4738a322..e0fde0e8f2 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts @@ -7,16 +7,18 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { By } from '@angular/platform-browser'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; describe('MetadataFieldSelectorComponent', () => { let component: MetadataFieldSelectorComponent; let fixture: ComponentFixture; let registryService: RegistryService; + let notificationsService: NotificationsService; let metadataSchema: MetadataSchema; let metadataFields: MetadataField[]; @@ -45,12 +47,14 @@ describe('MetadataFieldSelectorComponent', () => { registryService = jasmine.createSpyObj('registryService', { queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)), }); + notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']); TestBed.configureTestingModule({ declarations: [MetadataFieldSelectorComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ { provide: RegistryService, useValue: registryService }, + { provide: NotificationsService, useValue: notificationsService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -99,5 +103,20 @@ describe('MetadataFieldSelectorComponent', () => { done(); }); }); + + describe('when querying the metadata fields returns an error response', () => { + beforeEach(() => { + (registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed')); + }); + + it('should return an observable false and show a notification', (done) => { + component.mdField = 'dc.description.abstract'; + component.validate().subscribe((result) => { + expect(result).toBeFalse(); + expect(notificationsService.error).toHaveBeenCalled(); + done(); + }); + }); + }); }); }); diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts index 2bbe64b3df..066f671301 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -12,7 +12,7 @@ import { import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { - getAllSucceededRemoteData, getFirstSucceededRemoteData, + getAllSucceededRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteData, metadataFieldsToString } from '../../../core/shared/operators'; import { Observable } from 'rxjs/internal/Observable'; @@ -21,6 +21,9 @@ import { FormControl } from '@angular/forms'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { hasValue } from '../../../shared/empty.util'; import { Subscription } from 'rxjs/internal/Subscription'; +import { of } from 'rxjs/internal/observable/of'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-metadata-field-selector', @@ -97,7 +100,9 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV */ subs: Subscription[] = []; - constructor(protected registryService: RegistryService) { + constructor(protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { } /** @@ -148,11 +153,20 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV */ validate(): Observable { return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe( - getFirstSucceededRemoteData(), - metadataFieldsToString(), - take(1), - map((fields: string[]) => fields.indexOf(this.mdField) > -1), - tap((exists: boolean) => this.showInvalid = !exists), + getFirstCompletedRemoteData(), + switchMap((rd) => { + if (rd.hasSucceeded) { + return of(rd).pipe( + metadataFieldsToString(), + take(1), + map((fields: string[]) => fields.indexOf(this.mdField) > -1), + tap((exists: boolean) => this.showInvalid = !exists), + ); + } else { + this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage); + return [false]; + } + }), ); } diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 71ab64fa33..90322f119a 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -14,8 +14,6 @@ import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract import { ItemPrivateComponent } from './item-private/item-private.component'; import { ItemPublicComponent } from './item-public/item-public.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component'; -import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; -import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component'; import { SearchPageModule } from '../../search-page/search-page.module'; @@ -63,15 +61,12 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; ItemPublicComponent, ItemDeleteComponent, ItemStatusComponent, - ItemMetadataComponent, ItemRelationshipsComponent, ItemBitstreamsComponent, ItemVersionHistoryComponent, - EditInPlaceFieldComponent, ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, PaginatedDragAndDropBitstreamListComponent, - EditInPlaceFieldComponent, EditRelationshipComponent, EditRelationshipListComponent, ItemCollectionMapperComponent, @@ -84,9 +79,6 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; BundleDataService, ObjectValuesPipe ], - exports: [ - ItemMetadataComponent - ] }) export class EditItemPageModule { 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 deleted file mode 100644 index f5543af971..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - -
-
- {{metadata?.value}} -
-
- -
-
- - -
-
- {{metadata?.language}} -
-
- -
-
- - -
- - - - -
- diff --git a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss deleted file mode 100644 index a2a6786b36..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.btn[disabled] { - color: var(--bs-gray-600); - border-color: var(--bs-gray-600); - z-index: 0; // prevent border colors jumping on hover -} - -.metadata-field { - width: var(--ds-edit-item-metadata-field-width); -} - -.language-field { - width: var(--ds-edit-item-language-field-width); -} 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 deleted file mode 100644 index 03e8e5d720..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { TranslateModule } from '@ngx-translate/core'; -import { getTestScheduler } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; -import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service'; -import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; -import { MetadataField } from '../../../../core/metadata/metadata-field.model'; -import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; -import { RegistryService } from '../../../../core/registry/registry.service'; -import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { followLink } from '../../../../shared/utils/follow-link-config.model'; -import { EditInPlaceFieldComponent } from './edit-in-place-field.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; -let de: DebugElement; -let el: HTMLElement; -let metadataFieldService; -let objectUpdatesService; -let paginatedMetadataFields; -const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }); -const mdSchemaRD$ = createSuccessfulRemoteDataObject$(mdSchema); -const mdField1 = Object.assign(new MetadataField(), { - schema: mdSchemaRD$, - element: 'contributor', - qualifier: 'author' -}); -const mdField2 = Object.assign(new MetadataField(), { - schema: mdSchemaRD$, - element: 'title' -}); -const mdField3 = Object.assign(new MetadataField(), { - schema: mdSchemaRD$, - element: 'description', - qualifier: 'abstract', -}); - -const metadatum = Object.assign(new MetadatumViewModel(), { - key: 'dc.description.abstract', - value: 'Example abstract', - language: 'en' -}); - -const url = 'http://test-url.com/test-url'; -const fieldUpdate = { - field: metadatum, - changeType: undefined -}; -let scheduler: TestScheduler; - -describe('EditInPlaceFieldComponent', () => { - - beforeEach(waitForAsync(() => { - scheduler = getTestScheduler(); - - paginatedMetadataFields = buildPaginatedList(undefined, [mdField1, mdField2, mdField3]); - - metadataFieldService = jasmine.createSpyObj({ - queryMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields), - }); - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - saveChangeFieldUpdate: {}, - saveRemoveFieldUpdate: {}, - setEditableFieldUpdate: {}, - setValidFieldUpdate: {}, - removeSingleFieldUpdate: {}, - isEditable: observableOf(false), // should always return something --> its in ngOnInit - isValid: observableOf(true) // should always return something --> its in ngOnInit - } - ); - - TestBed.configureTestingModule({ - imports: [FormsModule, TranslateModule.forRoot()], - declarations: [ - EditInPlaceFieldComponent, - MockDirective(DebounceDirective), - MockComponent(ValidationSuggestionsComponent) - ], - providers: [ - { provide: RegistryService, useValue: metadataFieldService }, - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: MetadataFieldDataService, useValue: {} } - ], schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(EditInPlaceFieldComponent); - comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance - de = fixture.debugElement; - el = de.nativeElement; - - comp.url = url; - comp.fieldUpdate = fieldUpdate; - comp.metadata = metadatum; - }); - - describe('update', () => { - beforeEach(() => { - comp.update(); - fixture.detectChanges(); - }); - - it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { - expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(url, metadatum); - }); - }); - - describe('setEditable', () => { - const editable = false; - beforeEach(() => { - comp.setEditable(editable); - fixture.detectChanges(); - }); - - it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => { - expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid, editable); - }); - }); - - describe('editable is true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should contain input fields or textareas', () => { - const inputField = de.queryAll(By.css('input')); - const textAreas = de.queryAll(By.css('textarea')); - expect(inputField.length + textAreas.length).toBeGreaterThan(0); - }); - }); - - describe('editable is false', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('the div should contain no input fields or textareas', () => { - const inputField = de.queryAll(By.css('input')); - const textAreas = de.queryAll(By.css('textarea')); - expect(inputField.length + textAreas.length).toBe(0); - }); - }); - - describe('isValid is true', () => { - beforeEach(() => { - objectUpdatesService.isValid.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should not contain an error message', () => { - const errorMessages = de.queryAll(By.css('small.text-danger')); - expect(errorMessages.length).toBe(0); - - }); - }); - - describe('isValid is false', () => { - beforeEach(() => { - objectUpdatesService.isValid.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('there should be an error message', () => { - const errorMessages = de.queryAll(By.css('small.text-danger')); - expect(errorMessages.length).toBeGreaterThan(0); - - }); - }); - - describe('remove', () => { - beforeEach(() => { - comp.remove(); - fixture.detectChanges(); - }); - - it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, metadatum); - }); - }); - - describe('removeChangesFromField', () => { - beforeEach(() => { - comp.removeChangesFromField(); - fixture.detectChanges(); - }); - - it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid); - }); - }); - - describe('findMetadataFieldSuggestions', () => { - const query = 'query string'; - - const metadataFieldSuggestions: InputSuggestion[] = - [ - { - displayValue: ('dc.' + mdField1.toString()).split('.').join('.​'), - value: ('dc.' + mdField1.toString()) - }, - { - displayValue: ('dc.' + mdField2.toString()).split('.').join('.​'), - value: ('dc.' + mdField2.toString()) - }, - { - displayValue: ('dc.' + mdField3.toString()).split('.').join('.​'), - value: ('dc.' + mdField3.toString()) - } - ]; - - beforeEach(fakeAsync(() => { - comp.findMetadataFieldSuggestions(query); - tick(); - fixture.detectChanges(); - })); - - it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { - expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema')); - }); - - it('it should set metadataFieldSuggestions to the right value', () => { - const expected = 'a'; - scheduler.expectObservable(comp.metadataFieldSuggestions).toBe(expected, { a: metadataFieldSuggestions }); - }); - }); - - describe('canSetEditable', () => { - describe('when editable is currently true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - - it('canSetEditable should return an observable emitting false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false }); - }); - }); - - describe('when editable is currently false', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - - describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = FieldChangeType.ADD; - fixture.detectChanges(); - }); - it('canSetEditable should return an observable emitting true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: true }); - }); - }); - - describe('when the fieldUpdate\'s changeType is currently REMOVE', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); - it('canSetEditable should return an observable emitting false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false }); - }); - }); - }); - }); - - describe('canSetUneditable', () => { - describe('when editable is currently true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - - it('canSetUneditable should return an observable emitting true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: true }); - }); - }); - - describe('when editable is currently false', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - - it('canSetUneditable should return an observable emitting false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: false }); - }); - }); - }); - - describe('when canSetEditable emits true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(false)); - spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should have an enabled button with an edit icon', () => { - const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled; - expect(editIcon).toBe(false); - }); - }); - - describe('when canSetEditable emits false', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(false)); - spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('the div should have a disabled button with an edit icon', () => { - const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled; - expect(editIcon).toBe(true); - }); - }); - - describe('when canSetUneditable emits true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should have an enabled button with a check icon', () => { - const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled; - expect(checkButtonAttrs).toBe(false); - }); - }); - - describe('when canSetUneditable emits false', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('the div should have a disabled button with a check icon', () => { - const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled; - expect(checkButtonAttrs).toBe(true); - }); - }); - - describe('when canRemove emits true', () => { - beforeEach(() => { - spyOn(comp, 'canRemove').and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should have an enabled button with a trash icon', () => { - const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled; - expect(trashButtonAttrs).toBe(false); - }); - }); - - describe('when canRemove emits false', () => { - beforeEach(() => { - spyOn(comp, 'canRemove').and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('the div should have a disabled button with a trash icon', () => { - const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled; - expect(trashButtonAttrs).toBe(true); - }); - }); - - describe('when canUndo emits true', () => { - beforeEach(() => { - spyOn(comp, 'canUndo').and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - it('the div should have an enabled button with an undo icon', () => { - const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled; - expect(undoIcon).toBe(false); - }); - }); - - describe('when canUndo emits false', () => { - beforeEach(() => { - spyOn(comp, 'canUndo').and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - it('the div should have a disabled button with an undo icon', () => { - const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled; - expect(undoIcon).toBe(true); - }); - }); - - describe('canRemove', () => { - describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = FieldChangeType.UPDATE; - fixture.detectChanges(); - }); - it('canRemove should return an observable emitting true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: true }); - }); - }); - - describe('when the fieldUpdate\'s changeType is currently ADD', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = FieldChangeType.ADD; - fixture.detectChanges(); - }); - it('canRemove should return an observable emitting false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: false }); - }); - }); - }); - - describe('canUndo', () => { - - describe('when editable is currently true', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - comp.fieldUpdate.changeType = undefined; - fixture.detectChanges(); - }); - it('canUndo should return an observable emitting true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: true }); - }); - }); - - describe('when editable is currently false', () => { - describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = FieldChangeType.ADD; - fixture.detectChanges(); - }); - - it('canUndo should return an observable emitting true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: true }); - }); - }); - - describe('when the fieldUpdate\'s changeType is currently undefined', () => { - beforeEach(() => { - comp.fieldUpdate.changeType = undefined; - fixture.detectChanges(); - }); - - it('canUndo should return an observable emitting false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: false }); - }); - }); - }); - - }); - - describe('canEditMetadataField', () => { - describe('when the fieldUpdate\'s changeType is currently ADD', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - comp.fieldUpdate.changeType = FieldChangeType.ADD; - fixture.detectChanges(); - }); - it('can edit metadata field', () => { - const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions')) - .componentInstance.disable; - expect(disabledMetadataField).toBe(false); - }); - }); - describe('when the fieldUpdate\'s changeType is currently REMOVE', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - comp.fieldUpdate.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); - it('can edit metadata field', () => { - const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions')) - .componentInstance.disable; - expect(disabledMetadataField).toBe(true); - }); - }); - describe('when the fieldUpdate\'s changeType is currently UPDATE', () => { - beforeEach(() => { - objectUpdatesService.isEditable.and.returnValue(observableOf(true)); - comp.fieldUpdate.changeType = FieldChangeType.UPDATE; - fixture.detectChanges(); - }); - it('can edit metadata field', () => { - const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions')) - .componentInstance.disable; - expect(disabledMetadataField).toBe(true); - }); - }); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts deleted file mode 100644 index 2782747916..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { - metadataFieldsToString, - getFirstSucceededRemoteData -} from '../../../../core/shared/operators'; -import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; -import { RegistryService } from '../../../../core/registry/registry.service'; -import { cloneDeep } from 'lodash'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; -import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { NgModel } from '@angular/forms'; -import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; -import { followLink } from '../../../../shared/utils/follow-link-config.model'; - -@Component({ - // tslint:disable-next-line:component-selector - selector: '[ds-edit-in-place-field]', - styleUrls: ['./edit-in-place-field.component.scss'], - templateUrl: './edit-in-place-field.component.html', -}) -/** - * Component that displays a single metadatum of an item on the edit page - */ -export class EditInPlaceFieldComponent implements OnInit, OnChanges { - /** - * The current field, value and state of the metadatum - */ - @Input() fieldUpdate: FieldUpdate; - - /** - * The current url of this page - */ - @Input() url: string; - - /** - * The metadatum of this field - */ - @Input() metadata: MetadatumViewModel; - - /** - * Emits whether or not this field is currently editable - */ - editable: Observable; - - /** - * Emits whether or not this field is currently valid - */ - valid: Observable; - - /** - * The current suggestions for the metadatafield when editing - */ - metadataFieldSuggestions: BehaviorSubject = new BehaviorSubject([]); - - constructor( - private registryService: RegistryService, - private objectUpdatesService: ObjectUpdatesService, - ) { - } - - /** - * Sets up an observable that keeps track of the current editable and valid state of this field - */ - ngOnInit(): void { - this.editable = this.objectUpdatesService.isEditable(this.url, this.metadata.uuid); - this.valid = this.objectUpdatesService.isValid(this.url, this.metadata.uuid); - } - - /** - * Sends a new change update for this field to the object updates service - */ - update(ngModel?: NgModel) { - this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata)); - if (hasValue(ngModel)) { - this.checkValidity(ngModel); - } - } - - /** - * Method to check the validity of a form control - * @param ngModel - */ - public checkValidity(ngModel: NgModel) { - ngModel.control.setValue(ngModel.viewModel); - ngModel.control.updateValueAndValidity(); - this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, ngModel.control.valid); - } - - /** - * Sends a new editable state for this field to the service to change it - * @param editable The new editable state for this field - */ - setEditable(editable: boolean) { - this.objectUpdatesService.setEditableFieldUpdate(this.url, this.metadata.uuid, editable); - } - - /** - * Sends a new remove update for this field to the object updates service - */ - remove() { - this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata)); - } - - /** - * Notifies the object updates service that the updates for the current field can be removed - */ - removeChangesFromField() { - this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.metadata.uuid); - } - - /** - * Sets the current metadatafield based on the fieldUpdate input field - */ - ngOnChanges(): void { - this.metadata = cloneDeep(this.fieldUpdate.field) as MetadatumViewModel; - } - - /** - * Requests all metadata fields that contain the query string in their key - * Then sets all found metadata fields as metadataFieldSuggestions - * Ignores fields from metadata schemas "relation" and "relationship" - * @param query The query to look for - */ - findMetadataFieldSuggestions(query: string) { - if (isNotEmpty(query)) { - return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe( - getFirstSucceededRemoteData(), - metadataFieldsToString(), - ).subscribe((fieldNames: string[]) => { - this.setInputSuggestions(fieldNames); - }); - } else { - this.metadataFieldSuggestions.next([]); - } - } - - /** - * Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema - * @param fields list of Metadata fields, which all require a resolved MetadataSchema - */ - setInputSuggestions(fields: string[]) { - this.metadataFieldSuggestions.next( - fields.map((fieldName: string) => { - return { - displayValue: fieldName.split('.').join('.​'), - value: fieldName - }; - }) - ); - } - - /** - * Check if a user should be allowed to edit this field - * @return an observable that emits true when the user should be able to edit this field and false when they should not - */ - canSetEditable(): Observable { - return this.editable.pipe( - map((editable: boolean) => { - if (editable) { - return false; - } else { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; - } - }) - ); - } - - /** - * Check if a user should be allowed to disabled editing this field - * @return an observable that emits true when the user should be able to disable editing this field and false when they should not - */ - canSetUneditable(): Observable { - return this.editable; - } - - /** - * Check if a user should be allowed to remove this field - * @return an observable that emits true when the user should be able to remove this field and false when they should not - */ - canRemove(): Observable { - return observableOf(this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD); - } - - /** - * Check if a user should be allowed to undo changes to this field - * @return an observable that emits true when the user should be able to undo changes to this field and false when they should not - */ - canUndo(): Observable { - return this.editable.pipe( - map((editable: boolean) => this.fieldUpdate.changeType >= 0 || editable) - ); - } - - protected isNotEmpty(value): boolean { - return isNotEmpty(value); - } -} diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html deleted file mode 100644 index 70cd2aaa39..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html +++ /dev/null @@ -1,69 +0,0 @@ - diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.scss b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.scss deleted file mode 100644 index 46ce1a6f4b..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -.button-row { - .btn { - margin-right: calc(0.5 * var(--bs-spacer)); - - &:last-child { - margin-right: 0; - } - - @media screen and (min-width: map-get($grid-breakpoints, sm)) { - min-width: var(--ds-edit-item-button-min-width); - } - } - - &.top .btn { - margin-top: calc(var(--bs-spacer) / 2); - margin-bottom: calc(var(--bs-spacer) / 2); - } - - -} diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts deleted file mode 100644 index 0f01efcc55..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { of as observableOf } from 'rxjs'; -import { getTestScheduler } from 'jasmine-marbles'; -import { ItemMetadataComponent } from './item-metadata.component'; -import { TestScheduler } from 'rxjs/testing'; -import { SharedModule } from '../../../shared/shared.module'; -import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateModule } from '@ngx-translate/core'; -import { ItemDataService } from '../../../core/data/item-data.service'; -import { By } from '@angular/platform-browser'; -import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; -import { NotificationType } from '../../../shared/notifications/models/notification-type'; -import { RouterStub } from '../../../shared/testing/router.stub'; -import { Item } from '../../../core/shared/item.model'; -import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; -import { MetadatumViewModel } from '../../../core/shared/metadata.models'; -import { RegistryService } from '../../../core/registry/registry.service'; -import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; -import { MetadataField } from '../../../core/metadata/metadata-field.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { ObjectCacheService } from '../../../core/cache/object-cache.service'; -import { DSOSuccessResponse } from '../../../core/cache/response.models'; -import { createPaginatedList } from '../../../shared/testing/utils.test'; - -let comp: any; -let fixture: ComponentFixture; -let de: DebugElement; -let el: HTMLElement; -let objectUpdatesService; -const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); -const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); -const successNotification: INotification = new Notification('id', NotificationType.Success, 'success'); -const date = new Date(); -const router = new RouterStub(); -let metadataFieldService; -let paginatedMetadataFields; -let routeStub; -let objectCacheService; - -const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }); -const mdField1 = Object.assign(new MetadataField(), { - schema: mdSchema, - element: 'contributor', - qualifier: 'author' -}); -const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' }); -const mdField3 = Object.assign(new MetadataField(), { - schema: mdSchema, - element: 'description', - qualifier: 'abstract' -}); - -let itemService; -const notificationsService = jasmine.createSpyObj('notificationsService', - { - info: infoNotification, - warning: warningNotification, - success: successNotification - } -); -const metadatum1 = Object.assign(new MetadatumViewModel(), { - key: 'dc.description.abstract', - value: 'Example abstract', - language: 'en' -}); - -const metadatum2 = Object.assign(new MetadatumViewModel(), { - key: 'dc.title', - value: 'Title test', - language: 'de' -}); - -const metadatum3 = Object.assign(new MetadatumViewModel(), { - key: 'dc.contributor.author', - value: 'Shakespeare, William', -}); - -const url = 'http://test-url.com/test-url'; - -router.url = url; - -const fieldUpdate1 = { - field: metadatum1, - changeType: undefined -}; - -const fieldUpdate2 = { - field: metadatum2, - changeType: FieldChangeType.REMOVE -}; - -const fieldUpdate3 = { - field: metadatum3, - changeType: undefined -}; - -const operation1 = { op: 'remove', path: '/metadata/dc.title/1' }; - -let scheduler: TestScheduler; -let item; -describe('ItemMetadataComponent', () => { - beforeEach(waitForAsync(() => { - item = Object.assign(new Item(), { - metadata: { - [metadatum1.key]: [metadatum1], - [metadatum2.key]: [metadatum2], - [metadatum3.key]: [metadatum3] - }, - _links: { - self: { - href: 'https://rest.api/core/items/a36d8bd2-8e8c-4969-9b1f-a574c2064983' - } - } - }, - { - lastModified: date - } - ) - ; - itemService = jasmine.createSpyObj('itemService', { - update: createSuccessfulRemoteDataObject$(item), - commitUpdates: {}, - patch: observableOf(new DSOSuccessResponse(['item-selflink'], 200, 'OK')), - findByHref: createSuccessfulRemoteDataObject$(item) - }); - routeStub = { - data: observableOf({}), - parent: { - data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) - } - }; - paginatedMetadataFields = createPaginatedList([mdField1, mdField2, mdField3]); - - metadataFieldService = jasmine.createSpyObj({ - getAllMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields) - }); - scheduler = getTestScheduler(); - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [metadatum1.uuid]: fieldUpdate1, - [metadatum2.uuid]: fieldUpdate2, - [metadatum3.uuid]: fieldUpdate3 - }), - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([metadatum1, metadatum2, metadatum3]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), // should always return something --> its in ngOnInit - isValidPage: observableOf(true), - createPatch: observableOf([ - operation1 - ]) - } - ); - objectCacheService = jasmine.createSpyObj('objectCacheService', ['addPatch']); - - TestBed.configureTestingModule({ - imports: [SharedModule, TranslateModule.forRoot()], - declarations: [ItemMetadataComponent], - providers: [ - { provide: ItemDataService, useValue: itemService }, - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: Router, useValue: router }, - { provide: ActivatedRoute, useValue: routeStub }, - { provide: NotificationsService, useValue: notificationsService }, - { provide: RegistryService, useValue: metadataFieldService }, - { provide: ObjectCacheService, useValue: objectCacheService }, - ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); - }) - ); - - beforeEach(() => { - fixture = TestBed.createComponent(ItemMetadataComponent); - comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance - de = fixture.debugElement; - el = de.nativeElement; - comp.url = url; - fixture.detectChanges(); - }); - - describe('add', () => { - const md = new MetadatumViewModel(); - beforeEach(() => { - comp.add(md); - }); - - it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { - expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(url, md); - }); - }); - - describe('discard', () => { - beforeEach(() => { - comp.discard(); - }); - - it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => { - expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification); - }); - }); - - describe('reinstate', () => { - beforeEach(() => { - comp.reinstate(); - }); - - it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => { - expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url); - }); - }); - - describe('submit', () => { - beforeEach(() => { - comp.submit(); - }); - - it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => { - expect(objectUpdatesService.createPatch).toHaveBeenCalledWith(url); - expect(itemService.patch).toHaveBeenCalledWith(comp.item, [operation1]); - expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadataAsList); - }); - }); - - describe('hasChanges', () => { - describe('when the objectUpdatesService\'s hasUpdated method returns true', () => { - beforeEach(() => { - objectUpdatesService.hasUpdates.and.returnValue(observableOf(true)); - }); - - it('should return an observable that emits true', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: true }); - }); - }); - - describe('when the objectUpdatesService\'s hasUpdated method returns false', () => { - beforeEach(() => { - objectUpdatesService.hasUpdates.and.returnValue(observableOf(false)); - }); - - it('should return an observable that emits false', () => { - const expected = '(a|)'; - scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: false }); - }); - }); - }); - - describe('changeType is UPDATE', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.UPDATE; - fixture.detectChanges(); - }); - it('the div should have class table-warning', () => { - const element = de.queryAll(By.css('tr'))[1].nativeElement; - expect(element.classList).toContain('table-warning'); - }); - }); - - describe('changeType is ADD', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.ADD; - fixture.detectChanges(); - }); - it('the div should have class table-success', () => { - const element = de.queryAll(By.css('tr'))[1].nativeElement; - expect(element.classList).toContain('table-success'); - }); - }); - - describe('changeType is REMOVE', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); - it('the div should have class table-danger', () => { - const element = de.queryAll(By.css('tr'))[1].nativeElement; - expect(element.classList).toContain('table-danger'); - }); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts deleted file mode 100644 index 5030ef3bb3..0000000000 --- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { Item } from '../../../core/shared/item.model'; -import { ItemDataService } from '../../../core/data/item-data.service'; -import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { cloneDeep } from 'lodash'; -import { first, switchMap } from 'rxjs/operators'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { RemoteData } from '../../../core/data/remote-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models'; -import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; -import { UpdateDataService } from '../../../core/data/update-data.service'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { AlertType } from '../../../shared/alert/aletr-type'; -import { Operation } from 'fast-json-patch'; -import { MetadataPatchOperationService } from '../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service'; - -@Component({ - selector: 'ds-item-metadata', - styleUrls: ['./item-metadata.component.scss'], - templateUrl: './item-metadata.component.html', -}) -/** - * Component for displaying an item's metadata edit page - */ -export class ItemMetadataComponent extends AbstractItemUpdateComponent { - - /** - * The AlertType enumeration - * @type {AlertType} - */ - public AlertTypeEnum = AlertType; - - /** - * A custom update service to use for adding and committing patches - * This will default to the ItemDataService - */ - @Input() updateService: UpdateDataService; - - constructor( - public itemService: ItemDataService, - public objectUpdatesService: ObjectUpdatesService, - public router: Router, - public notificationsService: NotificationsService, - public translateService: TranslateService, - public route: ActivatedRoute, - ) { - super(itemService, objectUpdatesService, router, notificationsService, translateService, route); - } - - /** - * Set up and initialize all fields - */ - ngOnInit(): void { - super.ngOnInit(); - if (hasNoValue(this.updateService)) { - this.updateService = this.itemService; - } - } - - /** - * Initialize the values and updates of the current item's metadata fields - */ - public initializeUpdates(): void { - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); - } - - /** - * Initialize the prefix for notification messages - */ - public initializeNotificationsPrefix(): void { - this.notificationsPrefix = 'item.edit.metadata.notifications.'; - } - - /** - * Sends a new add update for a field to the object updates service - * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum - */ - add(metadata: MetadatumViewModel = new MetadatumViewModel()) { - this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); - } - - /** - * Sends all initial values of this item to the object updates service - */ - public initializeOriginalFields() { - this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, MetadataPatchOperationService); - } - - /** - * Requests all current metadata for this item and requests the item service to update the item - * Makes sure the new version of the item is rendered on the page - */ - public submit() { - this.isValid().pipe(first()).subscribe((isValid) => { - if (isValid) { - this.objectUpdatesService.createPatch(this.url).pipe( - first(), - switchMap((patch: Operation[]) => { - return this.updateService.patch(this.item, patch).pipe( - getFirstCompletedRemoteData() - ); - }) - ).subscribe( - (rd: RemoteData) => { - if (rd.hasFailed) { - this.notificationsService.error(this.getNotificationTitle('error'), rd.errorMessage); - } else { - this.item = rd.payload; - this.checkAndFixMetadataUUIDs(); - this.initializeOriginalFields(); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); - } - } - ); - } else { - this.notificationsService.error(this.getNotificationTitle('invalid'), this.getNotificationContent('invalid')); - } - }); - } - - /** - * Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service) - */ - checkAndFixMetadataUUIDs() { - const metadata = cloneDeep(this.item.metadata); - Object.keys(this.item.metadata).forEach((key: string) => { - metadata[key] = this.item.metadata[key].map((value) => hasValue(value.uuid) ? value : Object.assign(new MetadataValue(), value)); - }); - this.item.metadata = metadata; - } -} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0140bad6dd..fd2b049d54 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1815,6 +1815,8 @@ "item.edit.metadata.headers.value": "Value", + "item.edit.metadata.metadatafield.error": "An error occurred validating the metadata field", + "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", @@ -2270,6 +2272,64 @@ + "itemtemplate.edit.metadata.add-button": "Add", + + "itemtemplate.edit.metadata.discard-button": "Discard", + + "itemtemplate.edit.metadata.edit.buttons.confirm": "Confirm", + + "itemtemplate.edit.metadata.edit.buttons.drag": "Drag to reorder", + + "itemtemplate.edit.metadata.edit.buttons.edit": "Edit", + + "itemtemplate.edit.metadata.edit.buttons.remove": "Remove", + + "itemtemplate.edit.metadata.edit.buttons.undo": "Undo changes", + + "itemtemplate.edit.metadata.edit.buttons.unedit": "Stop editing", + + "itemtemplate.edit.metadata.edit.buttons.virtual": "This is a virtual metadata value, i.e. a value inherited from a related entity. It can’t be modified directly. Add or remove the corresponding relationship in the \"Relationships\" tab", + + "itemtemplate.edit.metadata.empty": "The item template currently doesn't contain any metadata. Click Add to start adding a metadata value.", + + "itemtemplate.edit.metadata.headers.edit": "Edit", + + "itemtemplate.edit.metadata.headers.field": "Field", + + "itemtemplate.edit.metadata.headers.language": "Lang", + + "itemtemplate.edit.metadata.headers.value": "Value", + + "itemtemplate.edit.metadata.metadatafield.error": "An error occurred validating the metadata field", + + "itemtemplate.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", + + "itemtemplate.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + + "itemtemplate.edit.metadata.notifications.discarded.title": "Changed discarded", + + "itemtemplate.edit.metadata.notifications.error.title": "An error occurred", + + "itemtemplate.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + + "itemtemplate.edit.metadata.notifications.invalid.title": "Metadata invalid", + + "itemtemplate.edit.metadata.notifications.outdated.content": "The item template you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + + "itemtemplate.edit.metadata.notifications.outdated.title": "Changed outdated", + + "itemtemplate.edit.metadata.notifications.saved.content": "Your changes to this item template's metadata were saved.", + + "itemtemplate.edit.metadata.notifications.saved.title": "Metadata saved", + + "itemtemplate.edit.metadata.reinstate-button": "Undo", + + "itemtemplate.edit.metadata.reset-order-button": "Undo reorder", + + "itemtemplate.edit.metadata.save-button": "Save", + + + "journal.listelement.badge": "Journal", "journal.page.description": "Description",