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/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index d12536d332a..6269772532a 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -1,9 +1,12 @@ import { testA11y } from 'cypress/support/utils'; +const COLLECTION_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); +const LOGIN_PAGE = '/login'; + describe('Collection Page', () => { it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + cy.visit(COLLECTION_PAGE); // tag must be loaded cy.get('ds-collection-page').should('be.visible'); @@ -12,3 +15,23 @@ describe('Collection Page', () => { testA11y('ds-collection-page'); }); }); + +describe('Collection Page -> Collection-edit menu', () => { + beforeEach(() => { + // 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.', () => { + // tag must be loaded + cy.get('ds-dso-edit-menu').should('be.visible'); + }); +}); diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 9cec10f95e9..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,6 +36,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 { 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'; @@ -95,6 +97,12 @@ export const MENUS = buildMenuStructure({ MenuRoute.COLLECTION_PAGE, MenuRoute.ITEM_PAGE, ), + AddSubObjectsMenuProvider.onRoute( + MenuRoute.COMMUNITY_PAGE, + ), + SubmitNewItemMenuProvider.onRoute( + MenuRoute.COLLECTION_PAGE, + ), WithdrawnReinstateItemMenuProvider.onRoute( MenuRoute.ITEM_PAGE, ), 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/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..7419a4289a5 --- /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 { AddSubObjectsMenuProvider } from './add-sub-objects.menu'; + +describe('AddSubObjectsMenuProvider', () => { + + 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: AddSubObjectsMenuProvider; + + 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: [ + AddSubObjectsMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationService }, + ], + }); + provider = TestBed.inject(AddSubObjectsMenuProvider); + }); + + 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/add-sub-objects.menu.ts b/src/app/shared/menu/providers/add-sub-objects.menu.ts new file mode 100644 index 00000000000..ca69808beac --- /dev/null +++ b/src/app/shared/menu/providers/add-sub-objects.menu.ts @@ -0,0 +1,68 @@ +/** + * 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 AddSubObjectsMenuProvider extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self), + ]).pipe( + map(([canEditObject]) => { + return [ + { + 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', + link: '/collections/create', + queryParams: { + parent: dso.uuid, + }, + } as LinkMenuItemModel, + icon: 'plus', + }, + ] as PartialMenuSection[]; + }), + ); + } +} 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..a8085a3e911 --- /dev/null +++ b/src/app/shared/menu/providers/submit-new-item.menu.spec.ts @@ -0,0 +1,79 @@ +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 { + 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'; + +describe('SubmitNewItemMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'collection.submit.item', + link: '/submit', + queryParams: { + collection: 'test-uuid', + }, + }, + icon: 'plus', + }, + ]; + + let provider: SubmitNewItemMenuProvider; + + const dso: Collection = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'test-uuid', + _links: { self: { href: 'self-link' } }, + }); + + + let authorizationService: { isAuthorized: Observable ; }; + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': of(true), + }); + + TestBed.configureTestingModule({ + providers: [ + SubmitNewItemMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: APP_CONFIG, useValue: environment }, + ], + }); + provider = TestBed.inject(SubmitNewItemMenuProvider); + }); + + 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(); + }); + }); + + 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 new file mode 100644 index 00000000000..668096944db --- /dev/null +++ b/src/app/shared/menu/providers/submit-new-item.menu.ts @@ -0,0 +1,64 @@ +/** + * 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 { + 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'; +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 SubmitNewItemMenuProvider extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.CanSubmit, dso.self), + ]).pipe( + map(([canSubmitItem]) => { + return [ + { + visible: canSubmitItem && !this.appConfig.collection.showSubmitButton, + 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..14ce9f0da54 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -7887,4 +7887,12 @@ "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", + + "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,