From 5783d472b386a7d8d8aad599e81ad10f7118019e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Sat, 13 Jun 2026 10:04:36 +0200 Subject: [PATCH 1/9] Created new menu providers for collection and community page. --- .../menu/providers/add-sub-collection.menu.ts | 56 +++++++++++++++++++ .../menu/providers/add-sub-community.menu.ts | 56 +++++++++++++++++++ .../menu/providers/submit-new-item.menu.ts | 56 +++++++++++++++++++ src/assets/i18n/en.json5 | 6 ++ 4 files changed, 174 insertions(+) create mode 100644 src/app/shared/menu/providers/add-sub-collection.menu.ts create mode 100644 src/app/shared/menu/providers/add-sub-community.menu.ts create mode 100644 src/app/shared/menu/providers/submit-new-item.menu.ts diff --git a/src/app/shared/menu/providers/add-sub-collection.menu.ts b/src/app/shared/menu/providers/add-sub-collection.menu.ts new file mode 100644 index 00000000000..eb4879e9204 --- /dev/null +++ b/src/app/shared/menu/providers/add-sub-collection.menu.ts @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Injectable } from '@angular/core'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; + +/** + * Menu provider to create the "Edit" option in the DSO edit menu + */ +@Injectable() +export class AddSubCollectionMenu extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self), + ]).pipe( + map(([canEditCommunity]) => { + return [ + { + visible: canEditCommunity, + model: { + type: MenuItemType.LINK, + text: 'community.add.sub-collection', + link: '/collections/create', + queryParams: { + parent: dso.uuid, + }, + } as LinkMenuItemModel, + icon: 'plus', + }, + ] as PartialMenuSection[]; + }), + ); + } +} diff --git a/src/app/shared/menu/providers/add-sub-community.menu.ts b/src/app/shared/menu/providers/add-sub-community.menu.ts new file mode 100644 index 00000000000..f483ce8ada7 --- /dev/null +++ b/src/app/shared/menu/providers/add-sub-community.menu.ts @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Injectable } from '@angular/core'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; + +/** + * Menu provider to create the "Edit" option in the DSO edit menu + */ +@Injectable() +export class AddSubCommunityMenu extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self), + ]).pipe( + map(([canEditCommunity]) => { + return [ + { + visible: canEditCommunity, + model: { + type: MenuItemType.LINK, + text: 'community.add.sub-community', + link: '/communities/create', + queryParams: { + parent: dso.uuid, + }, + } as LinkMenuItemModel, + icon: 'plus', + }, + ] as PartialMenuSection[]; + }), + ); + } +} diff --git a/src/app/shared/menu/providers/submit-new-item.menu.ts b/src/app/shared/menu/providers/submit-new-item.menu.ts new file mode 100644 index 00000000000..1c794509004 --- /dev/null +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Injectable } from '@angular/core'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; + +/** + * Menu provider to create the "Edit" option in the DSO edit menu + */ +@Injectable() +export class SubmitNewItemMenu extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.CanSubmit, dso.self), + ]).pipe( + map(([canSubmitItem]) => { + return [ + { + visible: canSubmitItem, + model: { + type: MenuItemType.LINK, + text: 'collection.submit.item', + link: '/submit', + queryParams: { + collection: dso.uuid, + }, + } as LinkMenuItemModel, + icon: 'plus', + }, + ] as PartialMenuSection[]; + }), + ); + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 9613e02b72a..ee8e3b75647 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -7887,4 +7887,10 @@ "bitstream.related.isReplacedBy": "Is replaced by", "bitstream.related.deleted": "deleted", + + "community.add.sub-community": "Add Community", + + "community.add.sub-collection": "Add Collection", + + "collection.submit.item": "Submit item", } From 6fdf1855a810b3afd955b6f24f6a800487c2b6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Sat, 13 Jun 2026 10:04:49 +0200 Subject: [PATCH 2/9] Added appMenu entries. --- src/app/app.menus.ts | 12 ++++++++++++ .../shared/menu/providers/submit-new-item.menu.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 9cec10f95e9..60800a97f61 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -9,6 +9,8 @@ import { buildMenuStructure } from './shared/menu/menu.structure'; import { MenuID } from './shared/menu/menu-id.model'; import { MenuRoute } from './shared/menu/menu-route.model'; import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu'; +import { AddSubCollectionMenu } from './shared/menu/providers/add-sub-collection.menu'; +import { AddSubCommunityMenu } from './shared/menu/providers/add-sub-community.menu'; import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu'; import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu'; import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu'; @@ -35,6 +37,7 @@ import { NotificationsMenuProvider } from './shared/menu/providers/notifications import { ProcessesMenuProvider } from './shared/menu/providers/processes.menu'; import { RegistriesMenuProvider } from './shared/menu/providers/registries.menu'; import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu'; +import { SubmitNewItemMenu } from './shared/menu/providers/submit-new-item.menu'; import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; @@ -95,6 +98,15 @@ export const MENUS = buildMenuStructure({ MenuRoute.COLLECTION_PAGE, MenuRoute.ITEM_PAGE, ), + AddSubCommunityMenu.onRoute( + MenuRoute.COMMUNITY_PAGE, + ), + AddSubCollectionMenu.onRoute( + MenuRoute.COMMUNITY_PAGE, + ), + SubmitNewItemMenu.onRoute( + MenuRoute.COLLECTION_PAGE, + ), WithdrawnReinstateItemMenuProvider.onRoute( MenuRoute.ITEM_PAGE, ), diff --git a/src/app/shared/menu/providers/submit-new-item.menu.ts b/src/app/shared/menu/providers/submit-new-item.menu.ts index 1c794509004..a1c4d1bdf9a 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -47,7 +47,7 @@ export class SubmitNewItemMenu extends DSpaceObjectPageMenuProvider { collection: dso.uuid, }, } as LinkMenuItemModel, - icon: 'plus', + icon: 'down-to-bracket', }, ] as PartialMenuSection[]; }), From 49f5fd1a749e5fc1c412947e0406c56b990a7133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Sun, 14 Jun 2026 06:48:49 +0200 Subject: [PATCH 3/9] Combine sub-objects menu providers. --- src/app/app.menus.ts | 8 +-- .../menu/providers/add-sub-community.menu.ts | 56 ------------------- ...ection.menu.ts => add-sub-objects.menu.ts} | 18 +++++- .../menu/providers/submit-new-item.menu.ts | 2 +- 4 files changed, 18 insertions(+), 66 deletions(-) delete mode 100644 src/app/shared/menu/providers/add-sub-community.menu.ts rename src/app/shared/menu/providers/{add-sub-collection.menu.ts => add-sub-objects.menu.ts} (77%) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 60800a97f61..d2c83b47e46 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -9,8 +9,6 @@ import { buildMenuStructure } from './shared/menu/menu.structure'; import { MenuID } from './shared/menu/menu-id.model'; import { MenuRoute } from './shared/menu/menu-route.model'; import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu'; -import { AddSubCollectionMenu } from './shared/menu/providers/add-sub-collection.menu'; -import { AddSubCommunityMenu } from './shared/menu/providers/add-sub-community.menu'; import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu'; import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu'; import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu'; @@ -41,6 +39,7 @@ import { SubmitNewItemMenu } from './shared/menu/providers/submit-new-item.menu' import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; +import {AddSubObjectsMenu} from "./shared/menu/providers/add-sub-objects.menu"; /** * Represents and builds the menu structure for the three available menus (public navbar, admin sidebar and the dso edit @@ -98,10 +97,7 @@ export const MENUS = buildMenuStructure({ MenuRoute.COLLECTION_PAGE, MenuRoute.ITEM_PAGE, ), - AddSubCommunityMenu.onRoute( - MenuRoute.COMMUNITY_PAGE, - ), - AddSubCollectionMenu.onRoute( + AddSubObjectsMenu.onRoute( MenuRoute.COMMUNITY_PAGE, ), SubmitNewItemMenu.onRoute( diff --git a/src/app/shared/menu/providers/add-sub-community.menu.ts b/src/app/shared/menu/providers/add-sub-community.menu.ts deleted file mode 100644 index f483ce8ada7..00000000000 --- a/src/app/shared/menu/providers/add-sub-community.menu.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -import { Injectable } from '@angular/core'; -import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; -import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; -import { - combineLatest, - Observable, -} from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { LinkMenuItemModel } from '../menu-item/models/link.model'; -import { MenuItemType } from '../menu-item-type.model'; -import { PartialMenuSection } from '../menu-provider.model'; -import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; - -/** - * Menu provider to create the "Edit" option in the DSO edit menu - */ -@Injectable() -export class AddSubCommunityMenu extends DSpaceObjectPageMenuProvider { - constructor( - protected authorizationDataService: AuthorizationDataService, - ) { - super(); - } - - public getSectionsForContext(dso: DSpaceObject): Observable { - return combineLatest([ - this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self), - ]).pipe( - map(([canEditCommunity]) => { - return [ - { - visible: canEditCommunity, - model: { - type: MenuItemType.LINK, - text: 'community.add.sub-community', - link: '/communities/create', - queryParams: { - parent: dso.uuid, - }, - } as LinkMenuItemModel, - icon: 'plus', - }, - ] as PartialMenuSection[]; - }), - ); - } -} diff --git a/src/app/shared/menu/providers/add-sub-collection.menu.ts b/src/app/shared/menu/providers/add-sub-objects.menu.ts similarity index 77% rename from src/app/shared/menu/providers/add-sub-collection.menu.ts rename to src/app/shared/menu/providers/add-sub-objects.menu.ts index eb4879e9204..9ceee8e6001 100644 --- a/src/app/shared/menu/providers/add-sub-collection.menu.ts +++ b/src/app/shared/menu/providers/add-sub-objects.menu.ts @@ -24,7 +24,7 @@ import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; * Menu provider to create the "Edit" option in the DSO edit menu */ @Injectable() -export class AddSubCollectionMenu extends DSpaceObjectPageMenuProvider { +export class AddSubObjectsMenu extends DSpaceObjectPageMenuProvider { constructor( protected authorizationDataService: AuthorizationDataService, ) { @@ -35,10 +35,22 @@ export class AddSubCollectionMenu extends DSpaceObjectPageMenuProvider { return combineLatest([ this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self), ]).pipe( - map(([canEditCommunity]) => { + map(([canEditObject]) => { return [ { - visible: canEditCommunity, + visible: canEditObject, + model: { + type: MenuItemType.LINK, + text: 'community.add.sub-community', + link: '/communities/create', + queryParams: { + parent: dso.uuid, + }, + } as LinkMenuItemModel, + icon: 'plus', + }, + { + visible: canEditObject, model: { type: MenuItemType.LINK, text: 'community.add.sub-collection', diff --git a/src/app/shared/menu/providers/submit-new-item.menu.ts b/src/app/shared/menu/providers/submit-new-item.menu.ts index a1c4d1bdf9a..1c794509004 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -47,7 +47,7 @@ export class SubmitNewItemMenu extends DSpaceObjectPageMenuProvider { collection: dso.uuid, }, } as LinkMenuItemModel, - icon: 'down-to-bracket', + icon: 'plus', }, ] as PartialMenuSection[]; }), From 12f4d2533499a5bf399d22e6ecebfbc2283f640f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Tue, 23 Jun 2026 06:47:27 +0200 Subject: [PATCH 4/9] Created spec and e2e tests. --- cypress/e2e/collection-page.cy.ts | 19 ++++- .../providers/add-sub-objects.menu.spec.ts | 79 +++++++++++++++++++ .../providers/submit-new-item.menu.spec.ts | 67 ++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/menu/providers/add-sub-objects.menu.spec.ts create mode 100644 src/app/shared/menu/providers/submit-new-item.menu.spec.ts diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index d12536d332a..d7eed8005c4 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -1,9 +1,11 @@ import { testA11y } from 'cypress/support/utils'; +const COLLECTION_PAGE = '/collections/'.concat(Cypress.expose('DSPACE_TEST_COLLECTION')); + describe('Collection Page', () => { it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + cy.visit('/collections/'.concat(Cypress.expose('DSPACE_TEST_COLLECTION'))); // tag must be loaded cy.get('ds-collection-page').should('be.visible'); @@ -12,3 +14,18 @@ describe('Collection Page', () => { testA11y('ds-collection-page'); }); }); + +describe('Collection Page -> Collection-edit menu', () => { + beforeEach(() => { + // All tests start with visiting the Collection Page + cy.visit(COLLECTION_PAGE); + + // These page elements are restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.expose('DSPACE_TEST_ADMIN_USER'), Cypress.expose('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('Edit menu should exist for admins.', () => { + // tag must be loaded + cy.get('ds-dso-edit-menu').should('be.visible'); + }); +}); diff --git a/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts b/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts new file mode 100644 index 00000000000..f783fd07b1c --- /dev/null +++ b/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts @@ -0,0 +1,79 @@ +import { TestBed } from '@angular/core/testing'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { Community } from '@dspace/core/shared/community.model'; +import { COMMUNITY } from '@dspace/core/shared/community.resource-type'; +import { of } from 'rxjs'; + +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectEditMenuProvider } from './dso-edit.menu'; + +describe('DSpaceObjectEditMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'community.add.sub-community', + link: '/communities/create', + queryParams: { + parent: 'test-uuid', + }, + }, + icon: 'plus', + }, + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'community.add.sub-collection', + link: '/collections/create', + queryParams: { + parent: 'test-uuid', + }, + }, + icon: 'plus', + }, + ]; + + let provider: DSpaceObjectEditMenuProvider; + + const dso: Community = Object.assign(new Community(), { + type: COMMUNITY.value, + uuid: 'test-uuid', + _links: { self: { href: 'self-link' } }, + }); + + + let authorizationService; + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': of(true), + }); + + TestBed.configureTestingModule({ + providers: [ + DSpaceObjectEditMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationService }, + ], + }); + provider = TestBed.inject(DSpaceObjectEditMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/submit-new-item.menu.spec.ts b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts new file mode 100644 index 00000000000..25e23cc812f --- /dev/null +++ b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts @@ -0,0 +1,67 @@ +import { TestBed } from '@angular/core/testing'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { Collection } from '@dspace/core/shared/collection.model'; +import { COLLECTION } from '@dspace/core/shared/collection.resource-type'; +import { of } from 'rxjs'; + +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectEditMenuProvider } from './dso-edit.menu'; + +describe('DSpaceObjectEditMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'collection.submit.item', + link: '/submit', + queryParams: { + collection: 'test-uuid', + }, + }, + icon: 'plus', + }, + ]; + + let provider: DSpaceObjectEditMenuProvider; + + const dso: Collection = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'test-uuid', + _links: { self: { href: 'self-link' } }, + }); + + + let authorizationService; + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': of(true), + }); + + TestBed.configureTestingModule({ + providers: [ + DSpaceObjectEditMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationService }, + ], + }); + provider = TestBed.inject(DSpaceObjectEditMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + +}); From 718209b37722fbe0458b76cdb94a1c84a218de0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Thu, 25 Jun 2026 16:00:16 +0200 Subject: [PATCH 5/9] Fixed names and spec tests. --- src/app/app.menus.ts | 8 ++++---- .../shared/menu/providers/add-sub-objects.menu.spec.ts | 10 +++++----- src/app/shared/menu/providers/add-sub-objects.menu.ts | 2 +- .../shared/menu/providers/submit-new-item.menu.spec.ts | 10 +++++----- src/app/shared/menu/providers/submit-new-item.menu.ts | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index d2c83b47e46..e349e2245ff 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -9,6 +9,7 @@ import { buildMenuStructure } from './shared/menu/menu.structure'; import { MenuID } from './shared/menu/menu-id.model'; import { MenuRoute } from './shared/menu/menu-route.model'; import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu'; +import { AddSubObjectsMenuProvider } from './shared/menu/providers/add-sub-objects.menu'; import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu'; import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu'; import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu'; @@ -35,11 +36,10 @@ import { NotificationsMenuProvider } from './shared/menu/providers/notifications import { ProcessesMenuProvider } from './shared/menu/providers/processes.menu'; import { RegistriesMenuProvider } from './shared/menu/providers/registries.menu'; import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu'; -import { SubmitNewItemMenu } from './shared/menu/providers/submit-new-item.menu'; +import { SubmitNewItemMenuProvider } from './shared/menu/providers/submit-new-item.menu'; import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; -import {AddSubObjectsMenu} from "./shared/menu/providers/add-sub-objects.menu"; /** * Represents and builds the menu structure for the three available menus (public navbar, admin sidebar and the dso edit @@ -97,10 +97,10 @@ export const MENUS = buildMenuStructure({ MenuRoute.COLLECTION_PAGE, MenuRoute.ITEM_PAGE, ), - AddSubObjectsMenu.onRoute( + AddSubObjectsMenuProvider.onRoute( MenuRoute.COMMUNITY_PAGE, ), - SubmitNewItemMenu.onRoute( + SubmitNewItemMenuProvider.onRoute( MenuRoute.COLLECTION_PAGE, ), WithdrawnReinstateItemMenuProvider.onRoute( diff --git a/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts b/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts index f783fd07b1c..7419a4289a5 100644 --- a/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts +++ b/src/app/shared/menu/providers/add-sub-objects.menu.spec.ts @@ -6,9 +6,9 @@ import { of } from 'rxjs'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; -import { DSpaceObjectEditMenuProvider } from './dso-edit.menu'; +import { AddSubObjectsMenuProvider } from './add-sub-objects.menu'; -describe('DSpaceObjectEditMenuProvider', () => { +describe('AddSubObjectsMenuProvider', () => { const expectedSections: PartialMenuSection[] = [ { @@ -37,7 +37,7 @@ describe('DSpaceObjectEditMenuProvider', () => { }, ]; - let provider: DSpaceObjectEditMenuProvider; + let provider: AddSubObjectsMenuProvider; const dso: Community = Object.assign(new Community(), { type: COMMUNITY.value, @@ -56,11 +56,11 @@ describe('DSpaceObjectEditMenuProvider', () => { TestBed.configureTestingModule({ providers: [ - DSpaceObjectEditMenuProvider, + AddSubObjectsMenuProvider, { provide: AuthorizationDataService, useValue: authorizationService }, ], }); - provider = TestBed.inject(DSpaceObjectEditMenuProvider); + provider = TestBed.inject(AddSubObjectsMenuProvider); }); it('should be created', () => { diff --git a/src/app/shared/menu/providers/add-sub-objects.menu.ts b/src/app/shared/menu/providers/add-sub-objects.menu.ts index 9ceee8e6001..ca69808beac 100644 --- a/src/app/shared/menu/providers/add-sub-objects.menu.ts +++ b/src/app/shared/menu/providers/add-sub-objects.menu.ts @@ -24,7 +24,7 @@ import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; * Menu provider to create the "Edit" option in the DSO edit menu */ @Injectable() -export class AddSubObjectsMenu extends DSpaceObjectPageMenuProvider { +export class AddSubObjectsMenuProvider extends DSpaceObjectPageMenuProvider { constructor( protected authorizationDataService: AuthorizationDataService, ) { diff --git a/src/app/shared/menu/providers/submit-new-item.menu.spec.ts b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts index 25e23cc812f..b7275ab0f6c 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.spec.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts @@ -6,9 +6,9 @@ import { of } from 'rxjs'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; -import { DSpaceObjectEditMenuProvider } from './dso-edit.menu'; +import { SubmitNewItemMenuProvider } from './submit-new-item.menu'; -describe('DSpaceObjectEditMenuProvider', () => { +describe('SubmitNewItemMenuProvider', () => { const expectedSections: PartialMenuSection[] = [ { @@ -25,7 +25,7 @@ describe('DSpaceObjectEditMenuProvider', () => { }, ]; - let provider: DSpaceObjectEditMenuProvider; + let provider: SubmitNewItemMenuProvider; const dso: Collection = Object.assign(new Collection(), { type: COLLECTION.value, @@ -44,11 +44,11 @@ describe('DSpaceObjectEditMenuProvider', () => { TestBed.configureTestingModule({ providers: [ - DSpaceObjectEditMenuProvider, + SubmitNewItemMenuProvider, { provide: AuthorizationDataService, useValue: authorizationService }, ], }); - provider = TestBed.inject(DSpaceObjectEditMenuProvider); + provider = TestBed.inject(SubmitNewItemMenuProvider); }); it('should be created', () => { diff --git a/src/app/shared/menu/providers/submit-new-item.menu.ts b/src/app/shared/menu/providers/submit-new-item.menu.ts index 1c794509004..803d72c6b4e 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -24,7 +24,7 @@ import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; * Menu provider to create the "Edit" option in the DSO edit menu */ @Injectable() -export class SubmitNewItemMenu extends DSpaceObjectPageMenuProvider { +export class SubmitNewItemMenuProvider extends DSpaceObjectPageMenuProvider { constructor( protected authorizationDataService: AuthorizationDataService, ) { From 88a0ff088a29d2b5f6768a2850b82ac9f418c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Thu, 25 Jun 2026 20:41:35 +0200 Subject: [PATCH 6/9] Create configurable Submit button on collection pages. --- config/config.example.yml | 2 ++ .../collection-page.component.html | 4 ++++ .../collection-page.component.ts | 17 +++++++++++++++++ src/assets/i18n/en.json5 | 2 ++ src/config/collection-page-config.interface.ts | 1 + src/config/default-app-config.ts | 1 + src/environments/environment.test.ts | 1 + .../collection-page.component.ts | 6 +++++- 8 files changed, 33 insertions(+), 1 deletion(-) diff --git a/config/config.example.yml b/config/config.example.yml index 6f6c67d1ae6..3c1b5e48f8c 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -478,6 +478,8 @@ collection: showSidebar: true edit: undoTimeout: 10000 # 10 seconds + # Whether to show a submit button on item pages instead of having an entry in the dso-edit menu. + showSubmitButton: false # Theme Config themes: diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 407adeec926..78ee84c27d2 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -34,6 +34,10 @@ [hasInnerHtml]="true" [title]="'collection.page.news'"> + @if (showSubmitButton$ | async) { + + + } diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index f9ec6606999..697cb26ffe7 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -2,13 +2,19 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, + Inject, OnInit, } from '@angular/core'; import { ActivatedRoute, Router, + RouterLink, RouterOutlet, } from '@angular/router'; +import { + APP_CONFIG, + AppConfig, +} from '@dspace/config/app-config.interface'; import { AuthService } from '@dspace/core/auth/auth.service'; import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; import { SortOptions } from '@dspace/core/cache/models/sort-options.model'; @@ -63,6 +69,7 @@ import { VarDirective } from '../shared/utils/var.directive'; ComcolPageLogoComponent, DsoEditMenuComponent, ErrorComponent, + RouterLink, RouterOutlet, ThemedComcolPageBrowseByComponent, ThemedComcolPageContentComponent, @@ -88,12 +95,18 @@ export class CollectionPageComponent implements OnInit { */ collectionPageRoute$: Observable; + /** + * Whether to show a submit button for users on the collection page. + */ + showSubmitButton$: Observable; + constructor( protected route: ActivatedRoute, protected router: Router, protected authService: AuthService, protected authorizationDataService: AuthorizationDataService, public dsoNameService: DSONameService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { } @@ -114,6 +127,10 @@ export class CollectionPageComponent implements OnInit { getAllSucceededRemoteDataPayload(), map((collection) => getCollectionPageRoute(collection.id)), ); + + this.showSubmitButton$ = this.authorizationDataService.isAuthorized(FeatureID.CanSubmit).pipe( + map(authorized => authorized && this.appConfig.collection.showSubmitButton), + ); } isNotEmpty(object: any) { diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ee8e3b75647..14ce9f0da54 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -7893,4 +7893,6 @@ "community.add.sub-collection": "Add Collection", "collection.submit.item": "Submit item", + + "collection.page.submit-button": "Submit item", } diff --git a/src/config/collection-page-config.interface.ts b/src/config/collection-page-config.interface.ts index 5aec06daea2..c477d1aef71 100644 --- a/src/config/collection-page-config.interface.ts +++ b/src/config/collection-page-config.interface.ts @@ -9,6 +9,7 @@ export interface CollectionPageConfig extends Config { edit: { undoTimeout: number; }; + showSubmitButton: boolean; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index fd703d4d024..839e9d90090 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -456,6 +456,7 @@ export class DefaultAppConfig implements AppConfig { edit: { undoTimeout: 10000, // 10 seconds }, + showSubmitButton: false, }; suggestion: SuggestionConfig[] = [ diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 0ae82e957a1..1a9c33bbb78 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -359,6 +359,7 @@ export const environment: BuildConfig = { edit: { undoTimeout: 10000, // 10 seconds }, + showSubmitButton: false, }, themes: [ { diff --git a/src/themes/custom/app/collection-page/collection-page.component.ts b/src/themes/custom/app/collection-page/collection-page.component.ts index dd9435c3ae1..02b191e3d09 100644 --- a/src/themes/custom/app/collection-page/collection-page.component.ts +++ b/src/themes/custom/app/collection-page/collection-page.component.ts @@ -3,7 +3,10 @@ import { ChangeDetectionStrategy, Component, } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { + RouterLink, + RouterOutlet, +} from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { CollectionPageComponent as BaseComponent } from '../../../../app/collection-page/collection-page.component'; @@ -38,6 +41,7 @@ import { VarDirective } from '../../../../app/shared/utils/var.directive'; ComcolPageLogoComponent, DsoEditMenuComponent, ErrorComponent, + RouterLink, RouterOutlet, ThemedComcolPageBrowseByComponent, ThemedComcolPageContentComponent, From 2cccbccdc79374fd92bb5663a47365313a355e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Fri, 26 Jun 2026 07:13:39 +0200 Subject: [PATCH 7/9] Updated SubmitNewItemMenuProvider in order to depend on the showSubmitButton configuration. --- .../providers/submit-new-item.menu.spec.ts | 18 +++++++++++++++--- .../menu/providers/submit-new-item.menu.ts | 12 ++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/app/shared/menu/providers/submit-new-item.menu.spec.ts b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts index b7275ab0f6c..a8085a3e911 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.spec.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts @@ -1,9 +1,14 @@ import { TestBed } from '@angular/core/testing'; +import { APP_CONFIG } from '@dspace/config/app-config.interface'; import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; import { Collection } from '@dspace/core/shared/collection.model'; import { COLLECTION } from '@dspace/core/shared/collection.resource-type'; -import { of } from 'rxjs'; +import { + Observable, + of, +} from 'rxjs'; +import { environment } from '../../../../environments/environment'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { SubmitNewItemMenuProvider } from './submit-new-item.menu'; @@ -34,7 +39,7 @@ describe('SubmitNewItemMenuProvider', () => { }); - let authorizationService; + let authorizationService: { isAuthorized: Observable ; }; beforeEach(() => { @@ -46,6 +51,7 @@ describe('SubmitNewItemMenuProvider', () => { providers: [ SubmitNewItemMenuProvider, { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: APP_CONFIG, useValue: environment }, ], }); provider = TestBed.inject(SubmitNewItemMenuProvider); @@ -62,6 +68,12 @@ describe('SubmitNewItemMenuProvider', () => { done(); }); }); - }); + it('should set visibility depending on authorization and collection.showSubmitButton', done => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections[0].visible).toEqual(authorizationService.isAuthorized && !environment.collection.showSubmitButton); + done(); + }); + }); + }); }); diff --git a/src/app/shared/menu/providers/submit-new-item.menu.ts b/src/app/shared/menu/providers/submit-new-item.menu.ts index 803d72c6b4e..668096944db 100644 --- a/src/app/shared/menu/providers/submit-new-item.menu.ts +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -5,7 +5,14 @@ * * http://www.dspace.org/license/ */ -import { Injectable } from '@angular/core'; +import { + Inject, + Injectable, +} from '@angular/core'; +import { + APP_CONFIG, + AppConfig, +} from '@dspace/config/app-config.interface'; import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; @@ -27,6 +34,7 @@ import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; export class SubmitNewItemMenuProvider extends DSpaceObjectPageMenuProvider { constructor( protected authorizationDataService: AuthorizationDataService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { super(); } @@ -38,7 +46,7 @@ export class SubmitNewItemMenuProvider extends DSpaceObjectPageMenuProvider { map(([canSubmitItem]) => { return [ { - visible: canSubmitItem, + visible: canSubmitItem && !this.appConfig.collection.showSubmitButton, model: { type: MenuItemType.LINK, text: 'collection.submit.item', From eb93984da8f077695584c47299841663145108c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Fri, 26 Jun 2026 08:36:14 +0200 Subject: [PATCH 8/9] Switch to cy.env in order to hide sensitive data. --- cypress/e2e/collection-page.cy.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index d7eed8005c4..a4d938d0756 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -5,7 +5,7 @@ const COLLECTION_PAGE = '/collections/'.concat(Cypress.expose('DSPACE_TEST_COLLE describe('Collection Page', () => { it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(Cypress.expose('DSPACE_TEST_COLLECTION'))); + cy.visit(COLLECTION_PAGE); // tag must be loaded cy.get('ds-collection-page').should('be.visible'); @@ -21,7 +21,9 @@ describe('Collection Page -> Collection-edit menu', () => { cy.visit(COLLECTION_PAGE); // These page elements are restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(Cypress.expose('DSPACE_TEST_ADMIN_USER'), Cypress.expose('DSPACE_TEST_ADMIN_PASSWORD')); + cy.env(['DSPACE_TEST_ADMIN_USER', 'DSPACE_TEST_ADMIN_PASSWORD']).then(({ DSPACE_TEST_ADMIN_USER, DSPACE_TEST_ADMIN_PASSWORD }) => { + cy.loginViaForm(DSPACE_TEST_ADMIN_USER, DSPACE_TEST_ADMIN_PASSWORD); + }); }); it('Edit menu should exist for admins.', () => { From 450971aecc51a016f0e5cdd79c78ce0cd2b50f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Martin=20L=C3=B6hden?= Date: Sat, 27 Jun 2026 05:57:39 +0200 Subject: [PATCH 9/9] Fixed cypress e2e test. --- cypress/e2e/collection-page.cy.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index a4d938d0756..6269772532a 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -1,6 +1,7 @@ import { testA11y } from 'cypress/support/utils'; -const COLLECTION_PAGE = '/collections/'.concat(Cypress.expose('DSPACE_TEST_COLLECTION')); +const COLLECTION_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); +const LOGIN_PAGE = '/login'; describe('Collection Page', () => { @@ -17,13 +18,16 @@ describe('Collection Page', () => { describe('Collection Page -> Collection-edit menu', () => { beforeEach(() => { - // All tests start with visiting the Collection Page - cy.visit(COLLECTION_PAGE); + // All tests start with visiting the Login Page + cy.visit(LOGIN_PAGE); // These page elements are restricted, so we will be shown the login form. Fill it out & submit. cy.env(['DSPACE_TEST_ADMIN_USER', 'DSPACE_TEST_ADMIN_PASSWORD']).then(({ DSPACE_TEST_ADMIN_USER, DSPACE_TEST_ADMIN_PASSWORD }) => { cy.loginViaForm(DSPACE_TEST_ADMIN_USER, DSPACE_TEST_ADMIN_PASSWORD); }); + + // Now we can visit the collection page: + cy.visit(COLLECTION_PAGE); }); it('Edit menu should exist for admins.', () => {