From 0d089bef4ab08e7cda596153c345fcc47d66ac90 Mon Sep 17 00:00:00 2001 From: Abel Cota Date: Mon, 17 Aug 2026 14:14:35 -0600 Subject: [PATCH] -Fix 1058, moving Template Item into a new TAB option instead of inside the Edit Metadata TAB --- .../collection-metadata.component.html | 24 --- .../collection-metadata.component.spec.ts | 105 ++---------- .../collection-metadata.component.ts | 120 +------------ .../collection-template-item.component.html | 34 ++++ ...collection-template-item.component.spec.ts | 160 ++++++++++++++++++ .../collection-template-item.component.ts | 150 ++++++++++++++++ .../edit-collection-page-routes.ts | 6 + src/assets/i18n/en.json5 | 4 + 8 files changed, 367 insertions(+), 236 deletions(-) create mode 100644 src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.html create mode 100644 src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.spec.ts create mode 100644 src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.ts diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index 28c5f21869e..504b20c328b 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,27 +1,3 @@ -
- {{ 'collection.edit.template.label' | translate}} -
- @if (!itemTemplateRD?.payload) { - - } - @if (itemTemplateRD?.payload) { - - } - @if (itemTemplateRD?.payload) { - - } -
-
{ let comp: CollectionMetadataComponent; let fixture: ComponentFixture; - let router: Router; - let itemTemplateService: ItemTemplateDataService; - const template = Object.assign(new Item(), { - _links: { - self: { href: 'template-selflink' }, - }, - }); const collection = Object.assign(new Collection(), { uuid: 'collection-id', id: 'collection-id', @@ -53,56 +32,27 @@ describe('CollectionMetadataComponent', () => { self: { href: 'collection-selflink' }, }, }); - const collectionTemplateHref = 'rest/api/test/collections/template'; - - const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', { - findByCollectionID: createSuccessfulRemoteDataObject$(template), - createByCollectionID: createSuccessfulRemoteDataObject$(template), - delete: of(true), - getCollectionEndpoint: of(collectionTemplateHref), - }); - - const notificationsService = jasmine.createSpyObj('notificationsService', { - success: {}, - error: {}, - }); - const requestService = jasmine.createSpyObj('requestService', { - setStaleByHrefSubstring: {}, - }); - - const routerMock = { - events: of(new NavigationEnd(1, 'url', 'url')), - navigate: jasmine.createSpy('navigate'), - }; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + void TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CollectionMetadataComponent], providers: [ { provide: CollectionDataService, useValue: {} }, - { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, { provide: ActivatedRoute, useValue: { parent: { data: of({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, - { provide: NotificationsService, useValue: notificationsService }, - { provide: RequestService, useValue: requestService }, - { provide: Router, useValue: routerMock }, - { provide: AuthService, useValue: new AuthServiceMock() }, - { provide: CommunityDataService, useValue: {} }, - { provide: ObjectCacheService, useValue: {} }, - { provide: APP_DATA_SERVICES_MAP, useValue: {} }, - { provide: APP_CONFIG, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: Router, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CollectionMetadataComponent, { + remove: { imports: [CollectionFormComponent] }, + }) + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(CollectionMetadataComponent); comp = fixture.componentInstance; - itemTemplateService = (comp as any).itemTemplateService; - spyOn(comp, 'ngOnInit'); - spyOn(comp, 'initTemplateItem'); - - routerMock.events = of(new NavigationEnd(1, 'url', 'url')); fixture.detectChanges(); }); @@ -111,39 +61,4 @@ describe('CollectionMetadataComponent', () => { expect((comp as any).frontendURL).toEqual('/collections/'); }); }); - - describe('addItemTemplate', () => { - it('should navigate to the collection\'s itemtemplate page', () => { - comp.addItemTemplate(); - expect(routerMock.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]); - }); - }); - - describe('deleteItemTemplate', () => { - beforeEach(() => { - (itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({})); - comp.deleteItemTemplate(); - }); - - it('should call ItemTemplateService.delete', () => { - expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid); - }); - - describe('when delete returns a success', () => { - it('should display a success notification', () => { - expect(notificationsService.success).toHaveBeenCalled(); - }); - }); - - describe('when delete returns a failure', () => { - beforeEach(() => { - (itemTemplateService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); - comp.deleteItemTemplate(); - }); - - it('should display an error notification', () => { - expect(notificationsService.error).toHaveBeenCalled(); - }); - }); - }); }); diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index c4af7af522c..a850121f241 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -1,46 +1,16 @@ import { AsyncPipe } from '@angular/common'; -import { - ChangeDetectorRef, - Component, - OnInit, -} from '@angular/core'; +import { Component } from '@angular/core'; import { ActivatedRoute, - NavigationEnd, Router, - RouterLink, - Scroll, } from '@angular/router'; import { CollectionDataService } from '@dspace/core/data/collection-data.service'; -import { ItemTemplateDataService } from '@dspace/core/data/item-template-data.service'; -import { RemoteData } from '@dspace/core/data/remote-data'; -import { RequestService } from '@dspace/core/data/request.service'; import { NotificationsService } from '@dspace/core/notification-system/notifications.service'; import { Collection } from '@dspace/core/shared/collection.model'; -import { Item } from '@dspace/core/shared/item.model'; -import { NoContent } from '@dspace/core/shared/NoContent.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload, -} from '@dspace/core/shared/operators'; -import { hasValue } from '@dspace/shared/utils/empty.util'; -import { - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; -import { - combineLatest as combineLatestObservable, - Observable, -} from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; -import { VarDirective } from '../../../shared/utils/var.directive'; import { CollectionFormComponent } from '../../collection-form/collection-form.component'; -import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; /** * Component for editing a collection's metadata @@ -51,103 +21,19 @@ import { getCollectionItemTemplateRoute } from '../../collection-page-routing-pa imports: [ AsyncPipe, CollectionFormComponent, - RouterLink, - TranslateModule, - VarDirective, ], }) -export class CollectionMetadataComponent extends ComcolMetadataComponent implements OnInit { +export class CollectionMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/collections/'; protected type = Collection.type; - /** - * The collection's item template - */ - itemTemplateRD$: Observable>; - public constructor( protected collectionDataService: CollectionDataService, - protected itemTemplateService: ItemTemplateDataService, protected router: Router, protected route: ActivatedRoute, protected notificationsService: NotificationsService, protected translate: TranslateService, - protected requestService: RequestService, - protected chd: ChangeDetectorRef, ) { super(collectionDataService, router, route, notificationsService, translate); } - - /** - * Checking if the navigation is done and if so, initialize the collection's item template, - * to ensure that the item template is always up to date. - * Check when a NavigationEnd event (URL change) or a Scroll event followed by a NavigationEnd event (refresh event), occurs - */ - ngOnInit(): void { - this.router.events.subscribe((event) => { - if ( - event instanceof NavigationEnd || - (event instanceof Scroll && event.routerEvent instanceof NavigationEnd) - ) { - super.ngOnInit(); - this.initTemplateItem(); - this.chd.detectChanges(); - } - }); - } - - /** - * Initialize the collection's item template - */ - initTemplateItem() { - this.itemTemplateRD$ = this.dsoRD$.pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)), - ); - } - - /** - * Add a new item template to the collection and redirect to the item template edit page - */ - addItemTemplate() { - const collection$ = this.dsoRD$.pipe( - getFirstSucceededRemoteDataPayload(), - ); - const template$ = collection$.pipe( - switchMap((collection: Collection) => this.itemTemplateService.createByCollectionID(new Item(), collection.uuid).pipe( - getFirstSucceededRemoteDataPayload(), - )), - ); - const templateHref$ = collection$.pipe( - switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)), - ); - - combineLatestObservable(collection$, template$, templateHref$).subscribe(([collection, template, templateHref]) => { - this.requestService.setStaleByHrefSubstring(templateHref); - this.router.navigate([getCollectionItemTemplateRoute(collection.uuid)]); - }); - } - - /** - * Delete the item template from the collection - */ - deleteItemTemplate() { - this.dsoRD$.pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)), - getFirstSucceededRemoteDataPayload(), - switchMap((template) => { - return this.itemTemplateService.delete(template.uuid); - }), - getFirstCompletedRemoteData(), - map((response: RemoteData) => hasValue(response) && response.hasSucceeded), - ).subscribe((success: boolean) => { - if (success) { - this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success')); - } else { - this.notificationsService.error(null, this.translate.get('collection.edit.template.notifications.delete.error')); - } - this.initTemplateItem(); - }); - } } diff --git a/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.html b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.html new file mode 100644 index 00000000000..7a05f133ff0 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.html @@ -0,0 +1,34 @@ +
+ @if (!editing) { + {{ 'collection.edit.template.label' | translate }} +
+ @if (!itemTemplateRD?.payload) { + + } + @if (itemTemplateRD?.payload) { + + } + @if (itemTemplateRD?.payload) { + + } +
+ } + + @if (editing && itemTemplateRD?.payload) { +

+ {{ 'collection.edit.template.head' | translate: { collection: dsoNameService.getName((dsoRD$ | async)?.payload) } }} +

+ + + } +
\ No newline at end of file diff --git a/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.spec.ts new file mode 100644 index 00000000000..f09a84bfca7 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.spec.ts @@ -0,0 +1,160 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; +import { ItemTemplateDataService } from '@dspace/core/data/item-template-data.service'; +import { RequestService } from '@dspace/core/data/request.service'; +import { NotificationsService } from '@dspace/core/notification-system/notifications.service'; +import { Collection } from '@dspace/core/shared/collection.model'; +import { Item } from '@dspace/core/shared/item.model'; +import { DSONameServiceMock } from '@dspace/core/testing/dso-name.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '@dspace/core/utilities/remote-data.utils'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { ThemedDsoEditMetadataComponent } from '../../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; +import { CollectionTemplateItemComponent } from './collection-template-item.component'; + +describe('CollectionTemplateItemComponent', () => { + let comp: CollectionTemplateItemComponent; + let fixture: ComponentFixture; + let itemTemplateService: ItemTemplateDataService; + + const template = Object.assign(new Item(), { + uuid: 'template-uuid', + _links: { + self: { href: 'template-selflink' }, + }, + }); + const collection = Object.assign(new Collection(), { + uuid: 'collection-id', + id: 'collection-id', + name: 'Fake Collection', + _links: { + self: { href: 'collection-selflink' }, + }, + }); + const collectionTemplateHref = 'rest/api/test/collections/template'; + + const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', { + findByCollectionID: createSuccessfulRemoteDataObject$(template), + createByCollectionID: createSuccessfulRemoteDataObject$(template), + delete: of(true), + getCollectionEndpoint: of(collectionTemplateHref), + }); + + const notificationsService = jasmine.createSpyObj('notificationsService', { + success: {}, + error: {}, + }); + const requestService = jasmine.createSpyObj('requestService', { + setStaleByHrefSubstring: {}, + }); + + beforeEach(waitForAsync(() => { + void TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CollectionTemplateItemComponent], + providers: [ + { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, + { provide: ActivatedRoute, useValue: { parent: { data: of({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: RequestService, useValue: requestService }, + { provide: Router, useValue: {} }, + { provide: DSONameService, useValue: new DSONameServiceMock() }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(CollectionTemplateItemComponent, { + remove: { imports: [ThemedDsoEditMetadataComponent] }, + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionTemplateItemComponent); + comp = fixture.componentInstance; + itemTemplateService = (comp as any).itemTemplateService; + fixture.detectChanges(); + }); + + describe('ngOnInit', () => { + it('should initialize the collection\'s item template', () => { + comp.itemTemplateRD$.subscribe((rd) => { + expect(rd.payload).toEqual(template); + }); + }); + }); + + describe('addItemTemplate', () => { + beforeEach(() => { + comp.addItemTemplate(); + }); + + it('should create an item template for the collection', () => { + expect(itemTemplateService.createByCollectionID).toHaveBeenCalledWith(jasmine.any(Item), collection.uuid); + }); + + it('should start editing the item template', () => { + expect(comp.editing).toBeTrue(); + }); + }); + + describe('editItemTemplate', () => { + it('should start editing the item template', () => { + comp.editItemTemplate(); + expect(comp.editing).toBeTrue(); + }); + }); + + describe('cancelEdit', () => { + beforeEach(() => { + comp.editing = true; + }); + + it('should stop editing the item template', () => { + comp.cancelEdit(); + expect(comp.editing).toBeFalse(); + }); + }); + + describe('deleteItemTemplate', () => { + beforeEach(() => { + (itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({})); + comp.deleteItemTemplate(); + }); + + it('should call ItemTemplateService.delete', () => { + expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid); + }); + + describe('when delete returns a success', () => { + it('should display a success notification', () => { + expect(notificationsService.success).toHaveBeenCalled(); + }); + }); + + describe('when delete returns a failure', () => { + beforeEach(() => { + (itemTemplateService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + comp.deleteItemTemplate(); + }); + + it('should display an error notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.ts b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.ts new file mode 100644 index 00000000000..27c4d5e85eb --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-template-item/collection-template-item.component.ts @@ -0,0 +1,150 @@ +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; +import { ItemTemplateDataService } from '@dspace/core/data/item-template-data.service'; +import { RemoteData } from '@dspace/core/data/remote-data'; +import { RequestService } from '@dspace/core/data/request.service'; +import { NotificationsService } from '@dspace/core/notification-system/notifications.service'; +import { Collection } from '@dspace/core/shared/collection.model'; +import { Item } from '@dspace/core/shared/item.model'; +import { NoContent } from '@dspace/core/shared/NoContent.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '@dspace/core/shared/operators'; +import { hasValue } from '@dspace/shared/utils/empty.util'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { ThemedDsoEditMetadataComponent } from '../../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; +import { VarDirective } from '../../../shared/utils/var.directive'; + +/** + * Component for managing the item template of a collection + */ +@Component({ + selector: 'ds-collection-template-item', + templateUrl: './collection-template-item.component.html', + imports: [ + AsyncPipe, + ThemedDsoEditMetadataComponent, + TranslateModule, + VarDirective, + ], +}) +export class CollectionTemplateItemComponent implements OnInit { + /** + * The collection to manage the item template for + */ + dsoRD$: Observable>; + + /** + * The collection's item template + */ + itemTemplateRD$: Observable>; + + /** + * Whether the user is currently editing the item template + */ + editing = false; + + public constructor( + protected itemTemplateService: ItemTemplateDataService, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected requestService: RequestService, + public dsoNameService: DSONameService, + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe(map((data) => data.dso)); + this.initTemplateItem(); + } + + /** + * Initialize the collection's item template + */ + initTemplateItem() { + this.itemTemplateRD$ = this.dsoRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)), + ); + } + + /** + * Add a new item template to the collection and start editing it + */ + addItemTemplate() { + const collection$ = this.dsoRD$.pipe( + getFirstSucceededRemoteDataPayload(), + ); + const template$ = collection$.pipe( + switchMap((collection: Collection) => this.itemTemplateService.createByCollectionID(new Item(), collection.uuid).pipe( + getFirstSucceededRemoteDataPayload(), + )), + ); + const templateHref$ = collection$.pipe( + switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)), + ); + + combineLatest([collection$, template$, templateHref$]).subscribe(([, , templateHref]) => { + this.requestService.setStaleByHrefSubstring(templateHref); + this.editing = true; + this.initTemplateItem(); + }); + } + + /** + * Start editing the collection's item template + */ + editItemTemplate() { + this.editing = true; + } + + /** + * Stop editing the collection's item template + */ + cancelEdit() { + this.editing = false; + } + + /** + * Delete the item template from the collection + */ + deleteItemTemplate() { + this.dsoRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)), + getFirstSucceededRemoteDataPayload(), + switchMap((template) => { + return this.itemTemplateService.delete(template.uuid); + }), + getFirstCompletedRemoteData(), + map((response: RemoteData) => hasValue(response) && response.hasSucceeded), + ).subscribe((success: boolean) => { + if (success) { + this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success')); + } else { + this.notificationsService.error(null, this.translate.get('collection.edit.template.notifications.delete.error')); + } + this.editing = false; + this.initTemplateItem(); + }); + } +} diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts index 49c0a32e8b7..b05db03ac6a 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts @@ -13,6 +13,7 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; import { CollectionSourceComponent } from './collection-source/collection-source.component'; +import { CollectionTemplateItemComponent } from './collection-template-item/collection-template-item.component'; import { EditCollectionPageComponent } from './edit-collection-page.component'; /** @@ -95,6 +96,11 @@ export const ROUTES: Route[] = [ component: CollectionItemMapperComponent, data: { title: 'collection.edit.tabs.item-mapper.title', hideReturnButton: true, showBreadcrumbs: true }, }, + { + path: 'template', + component: CollectionTemplateItemComponent, + data: { title: 'collection.edit.tabs.template.title', hideReturnButton: true, showBreadcrumbs: true }, + }, ], }, ]; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6551143ff13..0bc738b2c1b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1273,6 +1273,10 @@ "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", + "collection.edit.tabs.template.head": "Template Item", + + "collection.edit.tabs.template.title": "Collection Edit - Template Item", + "collection.edit.item-mapper.cancel": "Cancel", "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"",