diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 3ba0b39a0e..2953b1ca3c 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -180,6 +180,7 @@ import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidAuthService } from './orcid/orcid-auth.service'; import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service'; import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service'; +import { SubmissionParentBreadcrumbsService } from './submission/submission-parent-breadcrumb.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -250,6 +251,7 @@ const PROVIDERS = [ NotificationsService, WorkspaceitemDataService, WorkflowItemDataService, + SubmissionParentBreadcrumbsService, UploaderService, DSpaceObjectDataService, ConfigurationDataService, 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 0000000000..b4aa3586eb --- /dev/null +++ b/src/app/core/submission/resolver/submission-links-to-follow.ts @@ -0,0 +1,14 @@ +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 09561fbafa..b059b69e4d 100644 --- a/src/app/core/submission/resolver/submission-object.resolver.ts +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -1,12 +1,11 @@ -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { Store } from '@ngrx/store'; import { switchMap } from 'rxjs/operators'; import { RemoteData } from '../../data/remote-data'; import { getFirstCompletedRemoteData } from '../../shared/operators'; import { IdentifiableDataService } from '../../data/base/identifiable-data.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 @@ -15,7 +14,6 @@ import { IdentifiableDataService } from '../../data/base/identifiable-data.servi export class SubmissionObjectResolver implements Resolve> { constructor( protected dataService: IdentifiableDataService, - protected store: Store, ) { } @@ -30,7 +28,7 @@ export class SubmissionObjectResolver implements Resolve> { const itemRD$ = this.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 0000000000..554437ed94 --- /dev/null +++ b/src/app/core/submission/resolver/submission-parent-breadcrumb.resolver.ts @@ -0,0 +1,43 @@ +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../shared/operators'; +import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +import { BreadcrumbConfig } from '../../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionParentBreadcrumbsService } from '../submission-parent-breadcrumb.service'; +import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow'; +import { SubmissionObject } from '../models/submission-object.model'; + +/** + * 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 0000000000..d7e02e878f --- /dev/null +++ b/src/app/core/submission/submission-parent-breadcrumb.service.ts @@ -0,0 +1,59 @@ +import { BreadcrumbsProviderService } from '../breadcrumbs/breadcrumbsProviderService'; +import { Injectable } from '@angular/core'; +import { Observable, switchMap, combineLatest, of as observableOf } from 'rxjs'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Collection } from '../shared/collection.model'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { SubmissionObject } from './models/submission-object.model'; +import { RemoteData } from '../data/remote-data'; +import { DSOBreadcrumbsService } from '../breadcrumbs/dso-breadcrumbs.service'; +import { getDSORoute } from '../../app-routing-paths'; +import { SubmissionService } from '../../submission/submission.service'; +import { CollectionDataService } from '../data/collection-data.service'; +import { hasValue } from '../../shared/empty.util'; + +/** + * Service to calculate the parent {@link DSpaceObject} breadcrumbs for a {@link SubmissionObject} + */ +@Injectable() +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 9eb8cf110a..610570e8c0 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { Observable, of as observableOf, Subscription, timer as observableTimer } from 'rxjs'; import { catchError, concatMap, distinctUntilChanged, filter, find, map, startWith, take, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; +import { Store, MemoizedSelector, createSelector, select } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { submissionSelector, SubmissionState } from './submission.reducers'; @@ -47,6 +47,20 @@ import { SubmissionJsonPatchOperationsService } from '../core/submission/submiss import { SubmissionSectionObject } from './objects/submission-section-object.model'; import { SubmissionError } from './objects/submission-error.model'; +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. */ @@ -96,10 +110,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 0000000000..f4fb543605 --- /dev/null +++ b/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workflow item + */ +@Injectable() +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/item-from-workflow.resolver.spec.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts index 1ef87ad10f..40cec6a3e5 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts @@ -19,7 +19,7 @@ describe('ItemFromWorkflowResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi) } as any; - resolver = new ItemFromWorkflowResolver(wfiService, null); + resolver = new ItemFromWorkflowResolver(wfiService); }); it('should resolve a an item from from the workflow item with the correct id', (done) => { diff --git a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts index bacf515656..8a749b8988 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; -import { Store } from '@ngrx/store'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; @@ -10,12 +9,12 @@ import { SubmissionObjectResolver } from '../core/submission/resolver/submission * This class represents a resolver that requests a specific item before the route is activated */ @Injectable() -export class ItemFromWorkflowResolver extends SubmissionObjectResolver implements Resolve> { +export class ItemFromWorkflowResolver extends SubmissionObjectResolver implements Resolve> { + constructor( - private workflowItemService: WorkflowItemDataService, - protected store: Store + protected dataService: WorkflowItemDataService, ) { - super(workflowItemService, store); + super(dataService); } } diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts index 9c24bacb98..1a0dcf6fe7 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -15,13 +15,17 @@ import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/t import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; +import { ItemFromWorkflowBreadcrumbResolver } from './item-from-workflow-breadcrumb.resolver'; @NgModule({ imports: [ RouterModule.forChild([ { path: ':id', - resolve: { wfi: WorkflowItemPageResolver }, + resolve: { + breadcrumb: ItemFromWorkflowBreadcrumbResolver, + wfi: WorkflowItemPageResolver, + }, children: [ { canActivate: [AuthenticatedGuard], @@ -64,7 +68,11 @@ import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item- }] ) ], - providers: [WorkflowItemPageResolver, ItemFromWorkflowResolver] + providers: [ + ItemFromWorkflowBreadcrumbResolver, + ItemFromWorkflowResolver, + WorkflowItemPageResolver, + ], }) /** * This module defines the default component to load when navigating to the workflowitems edit page path. 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 0000000000..2ec872521a --- /dev/null +++ b/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workspace item + */ +@Injectable() +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/item-from-workspace.resolver.spec.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts index c14344d70d..f350be4f66 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts @@ -19,7 +19,7 @@ describe('ItemFromWorkspaceResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi) } as any; - resolver = new ItemFromWorkspaceResolver(wfiService, null); + resolver = new ItemFromWorkspaceResolver(wfiService); }); it('should resolve a an item from from the workflow item with the correct id', (done) => { diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts index 60e1fe6a87..2c76a426cf 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; -import { Store } from '@ngrx/store'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; @@ -10,12 +9,12 @@ import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data. * This class represents a resolver that requests a specific item before the route is activated */ @Injectable() -export class ItemFromWorkspaceResolver extends SubmissionObjectResolver implements Resolve> { - constructor( - private workspaceItemService: WorkspaceitemDataService, - protected store: Store - ) { - super(workspaceItemService, store); - } +export class ItemFromWorkspaceResolver extends SubmissionObjectResolver implements Resolve> { + + constructor( + protected dataService: WorkspaceitemDataService, + ) { + super(dataService); + } } diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts index cc76634c03..345c76e295 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts @@ -7,6 +7,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; +import { ItemFromWorkspaceBreadcrumbResolver } from './item-from-workspace-breadcrumb.resolver'; @NgModule({ imports: [ @@ -14,7 +15,10 @@ import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: ':id', - resolve: { wsi: WorkspaceItemPageResolver }, + resolve: { + breadcrumb: ItemFromWorkspaceBreadcrumbResolver, + wsi: WorkspaceItemPageResolver, + }, children: [ { canActivate: [AuthenticatedGuard], @@ -39,7 +43,11 @@ import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; } ]) ], - providers: [WorkspaceItemPageResolver, ItemFromWorkspaceResolver] + providers: [ + ItemFromWorkspaceBreadcrumbResolver, + ItemFromWorkspaceResolver, + WorkspaceItemPageResolver, + ], }) /** * This module defines the default component to load when navigating to the workspaceitems edit page path