diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index 91e9d59992d..889b910d6ae 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -7,6 +7,7 @@ import { browseByGuard } from '../browse-by/browse-by-guard'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { collectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; +import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; @@ -27,12 +28,29 @@ import { itemTemplatePageResolver } from './edit-item-template-page/item-templat import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; - export const ROUTES: Route[] = [ { path: COLLECTION_CREATE_PATH, - component: CreateCollectionPageComponent, canActivate: [authenticatedGuard, createCollectionPageGuard], + children: [ + { + path: '', + component: CreateCollectionPageComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + breadcrumbKey: 'collection.create', + }, + }, + ], + data: { + breadcrumbQueryParam: 'parent', + }, + resolve: { + breadcrumb: communityBreadcrumbResolver, + }, + runGuardsAndResolvers: 'always', }, { path: ':id', diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 995af8274fa..656b96c311e 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -28,8 +28,26 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component' export const ROUTES: Route[] = [ { path: COMMUNITY_CREATE_PATH, - component: CreateCommunityPageComponent, + children: [ + { + path: '', + component: CreateCommunityPageComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + breadcrumbKey: 'community.create', + }, + }, + ], canActivate: [authenticatedGuard, createCommunityPageGuard], + data: { + breadcrumbQueryParam: 'parent', + }, + resolve: { + breadcrumb: communityBreadcrumbResolver, + }, + runGuardsAndResolvers: 'always', }, { path: ':id', diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 1064a1cc191..0c37b5ca4f2 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -8,11 +8,15 @@ import { Observable } from 'rxjs'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver'; +import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { + DSOBreadcrumbResolver, + DSOBreadcrumbResolverByUuid, +} from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** @@ -25,11 +29,22 @@ export const communityBreadcrumbResolver: ResolveFn> dataService: CommunityDataService = inject(CommunityDataService), ): Observable> => { const linksToFollow: FollowLinkConfig[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; - return DSOBreadcrumbResolver( - route, - state, - breadcrumbService, - dataService, - ...linksToFollow, - ) as Observable>; + if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) { + return DSOBreadcrumbResolverByUuid( + route, + state, + route.queryParams[route.data.breadcrumbQueryParam], + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; + } else { + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; + } }; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index ae19128d4e9..c40a14a3231 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -18,7 +18,10 @@ describe('DSOBreadcrumbResolver', () => { uuid = '1234-65487-12354-1235'; breadcrumbUrl = `/collections/${uuid}`; currentUrl = `${breadcrumbUrl}/edit`; - testCollection = Object.assign(new Collection(), { uuid }); + testCollection = Object.assign(new Collection(), { + uuid: uuid, + type: 'collection', + }); dsoBreadcrumbService = {}; collectionService = { findById: () => createSuccessfulRemoteDataObject$(testCollection), diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index cb1f96b1033..992627ddfaa 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -5,6 +5,7 @@ import { import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { getDSORoute } from '../../app-routing-paths'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -32,15 +33,34 @@ export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: Route dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[] ): Observable> => { - const uuid = route.params.id; + return DSOBreadcrumbResolverByUuid(route, state, route.params.id, breadcrumbService, dataService, ...linksToFollow); +}; + +/** + * Method for resolving a breadcrumb config object with the given UUID + * + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {String} uuid The uuid of the DSO object + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object + */ +export const DSOBreadcrumbResolverByUuid: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, uuid: string, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + uuid: string, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { return dataService.findById(uuid, true, false, ...linksToFollow).pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), map((object: DSpaceObject) => { if (hasValue(object)) { - const fullPath = state.url; - const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid); - return { provider: breadcrumbService, key: object, url: url }; + return { provider: breadcrumbService, key: object, url: getDSORoute(object) }; } else { return undefined; } diff --git a/src/app/core/submission/resolver/submission-links-to-follow.ts b/src/app/core/submission/resolver/submission-links-to-follow.ts new file mode 100644 index 00000000000..1ddda024c51 --- /dev/null +++ b/src/app/core/submission/resolver/submission-links-to-follow.ts @@ -0,0 +1,17 @@ +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; +import { WorkflowItem } from '../models/workflowitem.model'; +import { WorkspaceItem } from '../models/workspaceitem.model'; + +/** + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + * + * Needs to be in a separate file to prevent circular dependencies in webpack. + */ +export const SUBMISSION_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('item'), + followLink('collection'), +]; diff --git a/src/app/core/submission/resolver/submission-object.resolver.ts b/src/app/core/submission/resolver/submission-object.resolver.ts index 4ddd9dea935..3ead988c9ba 100644 --- a/src/app/core/submission/resolver/submission-object.resolver.ts +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -5,12 +5,12 @@ import { import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; import { RemoteData } from '../../data/remote-data'; import { Item } from '../../shared/item.model'; import { getFirstCompletedRemoteData } from '../../shared/operators'; import { SubmissionObject } from '../models/submission-object.model'; +import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow'; /** * Method for resolving an item based on the parameters in the current route @@ -28,7 +28,7 @@ export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: Ro return dataService.findById(route.params.id, true, false, - followLink('item'), + ...SUBMISSION_LINKS_TO_FOLLOW, ).pipe( getFirstCompletedRemoteData(), switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), diff --git a/src/app/core/submission/resolver/submission-parent-breadcrumb.resolver.ts b/src/app/core/submission/resolver/submission-parent-breadcrumb.resolver.ts new file mode 100644 index 00000000000..92412be8697 --- /dev/null +++ b/src/app/core/submission/resolver/submission-parent-breadcrumb.resolver.ts @@ -0,0 +1,51 @@ +import { + ActivatedRouteSnapshot, + Resolve, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { BreadcrumbConfig } from '../../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../../shared/operators'; +import { SubmissionObject } from '../models/submission-object.model'; +import { SubmissionParentBreadcrumbsService } from '../submission-parent-breadcrumb.service'; +import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +export abstract class SubmissionParentBreadcrumbResolver implements Resolve> { + + protected constructor( + protected dataService: IdentifiableDataService, + protected breadcrumbService: SubmissionParentBreadcrumbsService, + ) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.dataService.findById(route.params.id, + true, + false, + ...SUBMISSION_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((submissionObject: SubmissionObject) => ({ + provider: this.breadcrumbService, + key: submissionObject, + } as BreadcrumbConfig)), + ); + } +} diff --git a/src/app/core/submission/submission-parent-breadcrumb.service.ts b/src/app/core/submission/submission-parent-breadcrumb.service.ts new file mode 100644 index 00000000000..4241d001926 --- /dev/null +++ b/src/app/core/submission/submission-parent-breadcrumb.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { + combineLatest, + Observable, + of as observableOf, + switchMap, +} from 'rxjs'; + +import { getDSORoute } from '../../app-routing-paths'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { hasValue } from '../../shared/empty.util'; +import { SubmissionService } from '../../submission/submission.service'; +import { BreadcrumbsProviderService } from '../breadcrumbs/breadcrumbsProviderService'; +import { DSOBreadcrumbsService } from '../breadcrumbs/dso-breadcrumbs.service'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { CollectionDataService } from '../data/collection-data.service'; +import { RemoteData } from '../data/remote-data'; +import { Collection } from '../shared/collection.model'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { SubmissionObject } from './models/submission-object.model'; + +/** + * Service to calculate the parent {@link DSpaceObject} breadcrumbs for a {@link SubmissionObject} + */ +@Injectable({ + providedIn: 'root', +}) +export class SubmissionParentBreadcrumbsService implements BreadcrumbsProviderService { + + constructor( + protected dsoNameService: DSONameService, + protected dsoBreadcrumbsService: DSOBreadcrumbsService, + protected submissionService: SubmissionService, + protected collectionService: CollectionDataService, + ) { + } + + /** + * Creates the parent breadcrumb structure for {@link SubmissionObject}s. It also automatically recreates the + * parent breadcrumb structure when you change a {@link SubmissionObject}'s by dispatching a + * {@link ChangeSubmissionCollectionAction}. + * + * @param submissionObject The {@link SubmissionObject} for which the parent breadcrumb structure needs to be created + */ + getBreadcrumbs(submissionObject: SubmissionObject): Observable { + return combineLatest([ + (submissionObject.collection as Observable>).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + ), + this.submissionService.getSubmissionCollectionId(submissionObject.id), + ]).pipe( + switchMap(([collection, latestCollectionId]: [Collection, string]) => { + if (hasValue(latestCollectionId)) { + return this.collectionService.findById(latestCollectionId).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + ); + } else { + return observableOf(collection); + } + }), + switchMap((collection: Collection) => this.dsoBreadcrumbsService.getBreadcrumbs(collection, getDSORoute(collection))), + ); + } + +} diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 312d74bc8a5..19c8893a7ee 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -1,7 +1,12 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; -import { Store } from '@ngrx/store'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { Observable, @@ -71,6 +76,20 @@ import { SubmissionState, } from './submission.reducers'; +function getSubmissionSelector(submissionId: string): MemoizedSelector { + return createSelector( + submissionSelector, + (state: SubmissionState) => state.objects[submissionId], + ); +} + +function getSubmissionCollectionIdSelector(submissionId: string): MemoizedSelector { + return createSelector( + getSubmissionSelector(submissionId), + (submission: SubmissionObjectEntry) => submission?.collection, + ); +} + /** * A service that provides methods used in submission process. */ @@ -120,10 +139,19 @@ export class SubmissionService { * @param collectionId * The collection id */ - changeSubmissionCollection(submissionId, collectionId) { + changeSubmissionCollection(submissionId: string, collectionId: string): void { this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId)); } + /** + * Listen to collection changes for a certain {@link SubmissionObject} + * + * @param submissionId The submission id + */ + getSubmissionCollectionId(submissionId: string): Observable { + return this.store.pipe(select(getSubmissionCollectionIdSelector(submissionId))); + } + /** * Perform a REST call to create a new workspaceitem and return response * diff --git a/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts b/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts new file mode 100644 index 00000000000..1c29d6b8612 --- /dev/null +++ b/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workflow item + */ +@Injectable({ + providedIn: 'root', +}) +export class ItemFromWorkflowBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve> { + + constructor( + protected dataService: WorkflowItemDataService, + protected breadcrumbService: SubmissionParentBreadcrumbsService, + ) { + super(dataService, breadcrumbService); + } + +} diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts index 95e1c69de82..4bc074c4256 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts @@ -6,6 +6,7 @@ import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item- import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; import { itemFromWorkflowResolver } from './item-from-workflow.resolver'; +import { ItemFromWorkflowBreadcrumbResolver } from './item-from-workflow-breadcrumb.resolver'; import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; import { workflowItemPageResolver } from './workflow-item-page.resolver'; import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; @@ -20,7 +21,10 @@ import { export const ROUTES: Routes = [ { path: ':id', - resolve: { wfi: workflowItemPageResolver }, + resolve: { + breadcrumb: ItemFromWorkflowBreadcrumbResolver, + wfi: workflowItemPageResolver, + }, children: [ { canActivate: [authenticatedGuard], diff --git a/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts new file mode 100644 index 00000000000..912d578b454 --- /dev/null +++ b/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workspace item + */ +@Injectable({ + providedIn: 'root', +}) +export class ItemFromWorkspaceBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve> { + + constructor( + protected dataService: WorkspaceitemDataService, + protected breadcrumbService: SubmissionParentBreadcrumbsService, + ) { + super(dataService, breadcrumbService); + } + +} diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts index f648be02092..abab1c5a445 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts @@ -5,6 +5,7 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; import { itemFromWorkspaceResolver } from './item-from-workspace.resolver'; +import { ItemFromWorkspaceBreadcrumbResolver } from './item-from-workspace-breadcrumb.resolver'; import { workspaceItemPageResolver } from './workspace-item-page.resolver'; import { ThemedWorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/themed-workspaceitems-delete-page.component'; import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/workspaceitems-delete-page.component'; @@ -16,7 +17,10 @@ export const ROUTES: Route[] = [ }, { path: ':id', - resolve: { wsi: workspaceItemPageResolver }, + resolve: { + breadcrumb: ItemFromWorkspaceBreadcrumbResolver, + wsi: workspaceItemPageResolver, + }, children: [ { canActivate: [authenticatedGuard], diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d4731f9bb92..9cb33aca9a7 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1108,6 +1108,8 @@ "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", + "collection.create.breadcrumbs": "Create collection", + "collection.browse.logo": "Browse for a collection logo", "collection.create.head": "Create a Collection", @@ -1398,6 +1400,8 @@ "community.subcoms-cols.breadcrumbs": "Subcommunities and Collections", + "community.create.breadcrumbs": "Create Community", + "community.create.head": "Create a Community", "community.create.notifications.success": "Successfully created the Community",