diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts index 561491bc64..e44dda8681 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; /** - * This class represents a resolver that requests a specific bitstreamFormat before the route is activated + * Method for resolving an bitstreamFormat based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state + * @param {BitstreamFormatDataService} bitstreamFormatDataService The BitstreamFormatDataService + * @returns Observable<> Emits the found bitstreamFormat based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class BitstreamFormatsResolver { - constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { - } - - /** - * Method for resolving an bitstreamFormat 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 bitstreamFormat based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamFormatDataService.findById(route.params.id) - .pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const BitstreamFormatsResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService), +): Observable> => { + return bitstreamFormatDataService.findById(route.params.id) + .pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index 7f6f5c1740..f57153b75e 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -24,32 +25,20 @@ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific bitstream before the route is activated + * Method for resolving a bitstream based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {BitstreamDataService} bitstreamService + * @returns Observable<> Emits the found bitstream based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class BitstreamPageResolver { - constructor(private bitstreamService: BitstreamDataService) { - } - - /** - * Method for resolving a bitstream 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 bitstream based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamService.findById(route.params.id, true, false, ...this.followLinks) - .pipe( - getFirstCompletedRemoteData(), - ); - } - /** - * Method that returns the follow links to already resolve - * 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 - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } -} +export const BitstreamPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + return bitstreamService.findById(route.params.id, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW) + .pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts index 9fd2dc315a..408f9a245f 100644 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts +++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts @@ -7,7 +7,7 @@ import { RequestEntryState } from '../core/data/request-entry-state.model'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; describe(`LegacyBitstreamUrlResolver`, () => { - let resolver: LegacyBitstreamUrlResolver; + let resolver: any; let bitstreamDataService: BitstreamDataService; let testScheduler; let remoteDataMocks; @@ -33,7 +33,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { bitstreamDataService = { findByItemHandle: () => undefined, } as any; - resolver = new LegacyBitstreamUrlResolver(bitstreamDataService); + resolver = LegacyBitstreamUrlResolver; }); describe(`resolve`, () => { @@ -51,7 +51,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, route.params.sequence_id, @@ -78,7 +78,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, route.queryParams.sequenceId, @@ -100,7 +100,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle, and filename from the route`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, undefined, @@ -123,7 +123,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { c: remoteDataMocks.Error, }; - expectObservable(resolver.resolve(route, state)).toBe(expected, values); + expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); }); }); it(`...succeeded`, () => { @@ -138,7 +138,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { c: remoteDataMocks.Success, }; - expectObservable(resolver.resolve(route, state)).toBe(expected, values); + expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); }); }); }); diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts index de5af179c4..d2e7a70b9b 100644 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts +++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,41 +13,34 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { hasNoValue } from '../shared/empty.util'; /** - * This class resolves a bitstream based on the DSpace 6 XMLUI or JSPUI bitstream download URLs + * Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the + * bitstream + * + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {BitstreamDataService} bitstreamDataService + * @returns Observable<> Emits the found bitstream based on the parameters in + * current route, or an error if something went wrong */ -@Injectable({ - providedIn: 'root', -}) -export class LegacyBitstreamUrlResolver { - constructor(protected bitstreamDataService: BitstreamDataService) { +export const LegacyBitstreamUrlResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamDataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const prefix = route.params.prefix; + const suffix = route.params.suffix; + const filename = route.params.filename; + + let sequenceId = route.params.sequence_id; + if (hasNoValue(sequenceId)) { + sequenceId = route.queryParams.sequenceId; } - /** - * Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the - * bitstream - * - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found bitstream based on the parameters in - * current route, or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable> { - const prefix = route.params.prefix; - const suffix = route.params.suffix; - const filename = route.params.filename; - - let sequenceId = route.params.sequence_id; - if (hasNoValue(sequenceId)) { - sequenceId = route.queryParams.sequenceId; - } - - return this.bitstreamDataService.findByItemHandle( - `${prefix}/${suffix}`, - sequenceId, - filename, - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} + return bitstreamDataService.findByItemHandle( + `${prefix}/${suffix}`, + sequenceId, + filename, + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts index 7650376955..34a054edfd 100644 --- a/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts +++ b/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -19,30 +20,28 @@ import { import { hasValue } from '../shared/empty.util'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {DSpaceObjectDataService} dataService + * @returns BreadcrumbConfig object */ -@Injectable({ providedIn: 'root' }) -export class BrowseByDSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) { +export const BrowseByDSOBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: DSpaceObjectDataService = inject(DSpaceObjectDataService), +): Observable> => { + const uuid = route.queryParams.scope; + if (hasValue(uuid)) { + return dataService.findById(uuid).pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map((object: Community | Collection) => { + return { provider: breadcrumbService, key: object, url: getDSORoute(object) }; + }), + ); } - - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.queryParams.scope; - if (hasValue(uuid)) { - return this.dataService.findById(uuid).pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((object: Community | Collection) => { - return { provider: this.breadcrumbService, key: object, url: getDSORoute(object) }; - }), - ); - } - return undefined; - } -} + return undefined; +}; diff --git a/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts index 3107f9aad5..0a477681c3 100644 --- a/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts +++ b/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts @@ -1,32 +1,23 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; /** - * This class resolves a BreadcrumbConfig object with an i18n key string for a route - * It adds the metadata field of the current browse-by page + * Method for resolving a browse-by i18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object for a browse-by page */ -@Injectable({ providedIn: 'root' }) -export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { - super(breadcrumbService); - } - - /** - * Method for resolving a browse-by i18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object for a browse-by page - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id; - route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey }); - return super.resolve(route, state); - } -} +export const BrowseByI18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): BreadcrumbConfig => { + const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id; + route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey }); + return I18nBreadcrumbResolver(route, state) as BreadcrumbConfig; +}; diff --git a/src/app/collection-page/collection-page.resolver.spec.ts b/src/app/collection-page/collection-page.resolver.spec.ts index 32d8952ff7..60ef0d5eb7 100644 --- a/src/app/collection-page/collection-page.resolver.spec.ts +++ b/src/app/collection-page/collection-page.resolver.spec.ts @@ -1,3 +1,4 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; @@ -5,7 +6,7 @@ import { CollectionPageResolver } from './collection-page.resolver'; describe('CollectionPageResolver', () => { describe('resolve', () => { - let resolver: CollectionPageResolver; + let resolver: any; let collectionService: any; let store: any; const uuid = '1234-65487-12354-1235'; @@ -17,12 +18,11 @@ describe('CollectionPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new CollectionPageResolver(collectionService, store); + resolver = CollectionPageResolver; }); it('should resolve a collection with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) - .pipe(first()) + (resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, collectionService, store) as Observable).pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); diff --git a/src/app/collection-page/collection-page.resolver.ts b/src/app/collection-page/collection-page.resolver.ts index 56d9e91fc1..e75adc29b8 100644 --- a/src/app/collection-page/collection-page.resolver.ts +++ b/src/app/collection-page/collection-page.resolver.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -28,37 +30,32 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific collection before the route is activated + * Method for resolving a collection based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param collectionService + * @param store + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class CollectionPageResolver { - constructor( - private collectionService: CollectionDataService, - private store: Store, - ) { - } +export const CollectionPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + collectionService: CollectionDataService = inject(CollectionDataService), + store: Store = inject(Store), +): Observable> => { + const collectionRD$ = collectionService.findById( + route.params.id, + true, + false, + ...COLLECTION_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a collection 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 collection based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const collectionRD$ = this.collectionService.findById( - route.params.id, - true, - false, - ...COLLECTION_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + collectionRD$.subscribe((collectionRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); + }); - collectionRD$.subscribe((collectionRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); - }); - - return collectionRD$; - } -} + return collectionRD$; +}; diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts index ced1ca9323..53a3110acb 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -1,27 +1,24 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ItemTemplatePageResolver } from './item-template-page.resolver'; describe('ItemTemplatePageResolver', () => { describe('resolve', () => { - let resolver: ItemTemplatePageResolver; + let resolver: any; let itemTemplateService: any; - let dsoNameService: DSONameServiceMock; const uuid = '1234-65487-12354-1235'; beforeEach(() => { itemTemplateService = { findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }), }; - dsoNameService = new DSONameServiceMock(); - resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService); + resolver = ItemTemplatePageResolver; }); it('should resolve an item template with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + (resolver({ params: { id: uuid } } as any, undefined, itemTemplateService) as Observable) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts index 05f0f050d6..69a850db6e 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -1,38 +1,23 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { followLink } from '../../shared/utils/follow-link-config.model'; -/** - * This class represents a resolver that requests a specific collection's item template before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemTemplatePageResolver { - constructor( - public dsoNameService: DSONameService, - private itemTemplateService: ItemTemplateDataService, - ) { - } - - /** - * Method for resolving a collection's item template 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 template based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const ItemTemplatePageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService), +): Observable> => { + return itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/community-page/community-page.resolver.spec.ts b/src/app/community-page/community-page.resolver.spec.ts index 70bb0075e3..b13f66b077 100644 --- a/src/app/community-page/community-page.resolver.spec.ts +++ b/src/app/community-page/community-page.resolver.spec.ts @@ -1,3 +1,4 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; @@ -5,7 +6,7 @@ import { CommunityPageResolver } from './community-page.resolver'; describe('CommunityPageResolver', () => { describe('resolve', () => { - let resolver: CommunityPageResolver; + let resolver: any; let communityService: any; let store: any; const uuid = '1234-65487-12354-1235'; @@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new CommunityPageResolver(communityService, store); + resolver = CommunityPageResolver; }); it('should resolve a community with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) + (resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, communityService, store) as Observable) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/community-page/community-page.resolver.ts b/src/app/community-page/community-page.resolver.ts index b505064786..f9b52258ac 100644 --- a/src/app/community-page/community-page.resolver.ts +++ b/src/app/community-page/community-page.resolver.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { CommunityDataService } from '../core/data/community-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -28,37 +30,32 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific community before the route is activated + * Method for resolving a community based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {CommunityDataService} communityService + * @param {Store} store + * @returns Observable<> Emits the found community based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class CommunityPageResolver { - constructor( - private communityService: CommunityDataService, - private store: Store, - ) { - } +export const CommunityPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + store: Store = inject(Store), +): Observable> => { + const communityRD$ = communityService.findById( + route.params.id, + true, + false, + ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a community 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 community based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const communityRD$ = this.communityService.findById( - route.params.id, - true, - false, - ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + communityRD$.subscribe((communityRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, communityRD.payload)); + }); - communityRD$.subscribe((communityRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); - }); - - return communityRD$; - } -} + return communityRD$; +}; diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index f003bd0f85..6191bdb1f7 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,31 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor( - protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { - super(breadcrumbService, dataService); - } +export const BitstreamBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService), + dataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * 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 - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } - -} diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index d337da22c7..91d7943883 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,29 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CollectionDataService } from '../data/collection-data.service'; import { Collection } from '../shared/collection.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Collection + * The resolve function that resolves the BreadcrumbConfig object for a Collection */ -@Injectable({ - providedIn: 'root', -}) -export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { - super(breadcrumbService, dataService); - } +export const CollectionBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CollectionDataService = inject(CollectionDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * 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 - */ - get followLinks(): FollowLinkConfig[] { - return COLLECTION_PAGE_LINKS_TO_FOLLOW; - } -} diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 4cbffe9a6a..c2e667369b 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -1,29 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +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 { 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 { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Community + * The resolve function that resolves the BreadcrumbConfig object for a Community */ -@Injectable({ - providedIn: 'root', -}) -export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * 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 - */ - get followLinks(): FollowLinkConfig[] { - return COMMUNITY_PAGE_LINKS_TO_FOLLOW; - } -} +export const CommunityBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CommunityDataService = inject(CommunityDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + 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 59fda031b2..e27da93d41 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -3,11 +3,10 @@ import { getTestScheduler } from 'jasmine-marbles'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { Collection } from '../shared/collection.model'; import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; describe('DSOBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: DSOBreadcrumbResolver; + let resolver: any; let collectionService: any; let dsoBreadcrumbService: any; let testCollection: Collection; @@ -17,18 +16,18 @@ describe('DSOBreadcrumbResolver', () => { beforeEach(() => { uuid = '1234-65487-12354-1235'; - breadcrumbUrl = '/collections/' + uuid; - currentUrl = breadcrumbUrl + '/edit'; + breadcrumbUrl = `/collections/${uuid}`; + currentUrl = `${breadcrumbUrl}/edit`; testCollection = Object.assign(new Collection(), { uuid }); dsoBreadcrumbService = {}; collectionService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection), + findById: () => createSuccessfulRemoteDataObject$(testCollection), }; - resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService); + resolver = CollectionBreadcrumbResolver; }); it('should resolve a breadcrumb config for the correct DSO', () => { - const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any); + const resolvedConfig = resolver({ params: { id: uuid } } as any, { url: currentUrl } as any, dsoBreadcrumbService, collectionService); const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl }; getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); }); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index b6f72f937a..cb1f96b103 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,4 +1,3 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, @@ -10,7 +9,6 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { getFirstCompletedRemoteData, @@ -19,45 +17,33 @@ import { import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export abstract class DSOBreadcrumbResolver { - protected constructor( - protected breadcrumbService: DSOBreadcrumbsService, - protected dataService: IdentifiableDataService, - ) { - } - - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.params.id; - return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - map((object: T) => { - if (hasValue(object)) { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return { provider: this.breadcrumbService, key: object, url: url }; - } else { - return undefined; - } - }), - ); - } - - /** - * Method that returns the follow links to already resolve - * 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 - */ - abstract get followLinks(): FollowLinkConfig[]; -} +export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { + const uuid = route.params.id; + 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 }; + } else { + return undefined; + } + }), + ); +}; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index 05ef2969f7..132c7abb08 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -3,7 +3,7 @@ import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; describe('I18nBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: I18nBreadcrumbResolver; + let resolver: any; let i18nBreadcrumbService: any; let i18nKey: string; let route: any; @@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => { }; expectedPath = new URLCombiner(parentSegment, segment).toString(); i18nBreadcrumbService = {}; - resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); + resolver = I18nBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, {} as any); + const resolvedConfig = resolver(route, {} as any, i18nBreadcrumbService); const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, i18nBreadcrumbService); }).toThrow(); }); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index 62004a0d7f..536eb0194b 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -10,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils'; import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {I18nBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class I18nBreadcrumbResolver { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { +export const I18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService), +): BreadcrumbConfig => { + const key = route.data.breadcrumbKey; + if (hasNoValue(key)) { + throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); } - - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const key = route.data.breadcrumbKey; - if (hasNoValue(key)) { - throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); - } - const fullPath = currentPathFromSnapshot(route); - return { provider: this.breadcrumbService, key: key, url: fullPath }; - } -} + const fullPath = currentPathFromSnapshot(route); + return { provider: breadcrumbService, key: key, url: fullPath }; +}; diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index f609cbf3bc..28eb9efbf5 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -1,29 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ItemDataService } from '../data/item-data.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * 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 - */ - get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; - } -} +export const ItemBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: ItemDataService = inject(ItemDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts index db89d02f75..778d6090d6 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts @@ -2,7 +2,7 @@ import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; describe('NavigationBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: NavigationBreadcrumbResolver; + let resolver: any; let NavigationBreadcrumbService: any; let i18nKey: string; let relatedI18nKey: string; @@ -40,11 +40,11 @@ describe('NavigationBreadcrumbResolver', () => { }; expectedPath = '/base/example:/base'; NavigationBreadcrumbService = {}; - resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); + resolver = NavigationBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, state); + const resolvedConfig = resolver(route, state, NavigationBreadcrumbService); const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts index d2e5d9682c..811c537268 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -8,49 +9,44 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route and related parents + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {NavigationBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class NavigationBreadcrumbResolver { - - private parentRoutes: ActivatedRouteSnapshot[] = []; - constructor(protected breadcrumbService: NavigationBreadcrumbsService) { - } - - /** - * Method to collect all parent routes snapshot from current route snapshot - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - */ - private getParentRoutes(route: ActivatedRouteSnapshot): void { - if (route.parent) { - this.parentRoutes.push(route.parent); - this.getParentRoutes(route.parent); - } - } - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - this.getParentRoutes(route); - const relatedRoutes = route.data.relatedRoutes; - const parentPaths = this.parentRoutes.map(parent => parent.routeConfig?.path); - const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); - const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; - const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); +export const NavigationBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService), +): BreadcrumbConfig => { + const parentRoutes: ActivatedRouteSnapshot[] = []; + getParentRoutes(route, parentRoutes); + const relatedRoutes = route.data.relatedRoutes; + const parentPaths = parentRoutes.map(parent => parent.routeConfig?.path); + const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); + const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; + const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); - const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${current.data.breadcrumbKey}`; - }, route.data.breadcrumbKey); - const combinedUrls = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${baseUrl}${current.path}`; - }, state.url); + const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${current.data.breadcrumbKey}`; + }, route.data.breadcrumbKey); + const combinedUrls = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${baseUrl}${current.path}`; + }, state.url); - return { provider: this.breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; + return { provider: breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; +}; + +/** + * Method to collect all parent routes snapshot from current route snapshot + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {ActivatedRouteSnapshot[]} parentRoutes + */ +function getParentRoutes(route: ActivatedRouteSnapshot, parentRoutes: ActivatedRouteSnapshot[]): void { + if (route.parent) { + parentRoutes.push(route.parent); + getParentRoutes(route.parent, parentRoutes); } } diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts index 7a0e9d43ed..f0ae3c59c6 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -2,7 +2,7 @@ import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcru describe('PublicationClaimBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: PublicationClaimBreadcrumbResolver; + let resolver: any; let publicationClaimBreadcrumbService: any; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; @@ -19,11 +19,11 @@ describe('PublicationClaimBreadcrumbResolver', () => { }, }; publicationClaimBreadcrumbService = {}; - resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); + resolver = PublicationClaimBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath } as any); + const resolvedConfig = resolver(route as any, { url: fullPath } as any, publicationClaimBreadcrumbService); const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts index 6289d2dd0f..db2fe0dceb 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -1,28 +1,18 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class PublicationClaimBreadcrumbResolver { - constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { - } - - /** - * Method that resolve Publication Claim item into a breadcrumb - * The parameter are retrieved by the url since part of the Publication Claim route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const targetId = route.paramMap.get('targetId').split(':')[1]; - return { provider: this.breadcrumbService, key: targetId }; - } -} +export const PublicationClaimBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService), +): BreadcrumbConfig => { + const targetId = route.paramMap.get('targetId').split(':')[1]; + return { provider: breadcrumbService, key: targetId }; +}; diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts index ea6045c85e..1965e50cf6 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -2,7 +2,7 @@ import { QualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcru describe('QualityAssuranceBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: QualityAssuranceBreadcrumbResolver; + let resolver: any; let qualityAssuranceBreadcrumbService: any; let route: any; const fullPath = '/test/quality-assurance/'; @@ -19,11 +19,11 @@ describe('QualityAssuranceBreadcrumbResolver', () => { }, }; qualityAssuranceBreadcrumbService = {}; - resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); + resolver = QualityAssuranceBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath + 'testSourceId' } as any); + const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, qualityAssuranceBreadcrumbService); const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts index 31df029a0b..ef01ef10cf 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -1,36 +1,27 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class QualityAssuranceBreadcrumbResolver { - constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} +export const QualityAssuranceBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService), +): BreadcrumbConfig => { + const sourceId = route.paramMap.get('sourceId'); + const topicId = route.paramMap.get('topicId'); + let key = sourceId; - /** - * Method that resolve QA item into a breadcrumb - * The parameter are retrieved by the url since part of the QA route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const sourceId = route.paramMap.get('sourceId'); - const topicId = route.paramMap.get('topicId'); - let key = sourceId; - - if (topicId) { - key += `:${topicId}`; - } - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(sourceId)); - - return { provider: this.breadcrumbService, key, url }; + if (topicId) { + key += `:${topicId}`; } -} + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(sourceId)); + + return { provider: breadcrumbService, key, url }; +}; diff --git a/src/app/core/submission/resolver/submission-object.resolver.ts b/src/app/core/submission/resolver/submission-object.resolver.ts index 2779193fb1..4ddd9dea93 100644 --- a/src/app/core/submission/resolver/submission-object.resolver.ts +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -1,45 +1,37 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router'; -import { Store } from '@ngrx/store'; 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'; /** - * This class represents a resolver that requests a specific item before the route is activated + * 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 + * @param {IdentifiableDataService } dataService + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class SubmissionObjectResolver { - constructor( - protected dataService: IdentifiableDataService, - protected store: Store, - ) { - } - - /** - * 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> { - const itemRD$ = this.dataService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), - getFirstCompletedRemoteData(), - ); - return itemRD$; - } -} +export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, dataService: IdentifiableDataService) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + dataService: IdentifiableDataService, +): Observable> => { + return dataService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/home-page/home-page.resolver.ts b/src/app/home-page/home-page.resolver.ts index eafc4b2846..a25973fe45 100644 --- a/src/app/home-page/home-page.resolver.ts +++ b/src/app/home-page/home-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -9,21 +10,10 @@ import { take } from 'rxjs/operators'; import { SiteDataService } from '../core/data/site-data.service'; import { Site } from '../core/shared/site.model'; -/** - * The class that resolve the Site object for a route - */ -@Injectable({ providedIn: 'root' }) -export class HomePageResolver { - constructor(private siteService: SiteDataService) { - } - - /** - * Method for resolving a site object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable Emits the found Site object, or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | Site { - return this.siteService.find().pipe(take(1)); - } -} +export const HomePageResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + siteService: SiteDataService = inject(SiteDataService), +): Observable => { + return siteService.find().pipe(take(1)); +}; diff --git a/src/app/item-page/item-page.resolver.spec.ts b/src/app/item-page/item-page.resolver.spec.ts index 76b7dae774..4a59af8ce0 100644 --- a/src/app/item-page/item-page.resolver.spec.ts +++ b/src/app/item-page/item-page.resolver.spec.ts @@ -19,7 +19,7 @@ describe('ItemPageResolver', () => { }); describe('resolve', () => { - let resolver: ItemPageResolver; + let resolver: any; let itemService: any; let store: any; let router: any; @@ -42,15 +42,19 @@ describe('ItemPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new ItemPageResolver(itemService, store, router); + resolver = ItemPageResolver; }); it('should redirect to the correct route for the entity type', (done) => { spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(router, 'navigateByUrl').and.callThrough(); - resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/items/${uuid}`).toString() } as any) - .pipe(first()) + resolver({ params: { id: uuid } } as any, + { url: router.parseUrl(`/items/${uuid}`).toString() } as any, + router, + itemService, + store, + ).pipe(first()) .subscribe( () => { expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl(`/entities/${entityType}/${uuid}`).toString()); @@ -63,8 +67,13 @@ describe('ItemPageResolver', () => { spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(router, 'navigateByUrl').and.callThrough(); - resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any) - .pipe(first()) + resolver( + { params: { id: uuid } } as any, + { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any, + router, + itemService, + store, + ).pipe(first()) .subscribe( () => { expect(router.navigateByUrl).not.toHaveBeenCalled(); diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 5e5bc3cc55..01e4a3db09 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -8,54 +9,64 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { AppState } from '../app.reducer'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; import { Item } from '../core/shared/item.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { hasValue } from '../shared/empty.util'; -import { ItemResolver } from './item.resolver'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from './item.resolver'; import { getItemPageRoute } from './item-page-routing-paths'; /** - * This class represents a resolver that requests a specific item before the route is activated and will redirect to the - * entity page + * 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 + * @param {Router} router + * @param {ItemDataService} itemService + * @param {Store} store + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ItemPageResolver extends ItemResolver { - constructor( - protected itemService: ItemDataService, - protected store: Store, - protected router: Router, - ) { - super(itemService, store, router); - } +export const ItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + itemService: ItemDataService = inject(ItemDataService), + store: Store = inject(Store), +): Observable> => { + const itemRD$ = itemService.findById( + route.params.id, + true, + false, + ...ITEM_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * 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 super.resolve(route, state).pipe( - map((rd: RemoteData) => { - if (rd.hasSucceeded && hasValue(rd.payload)) { - const thisRoute = state.url; + itemRD$.subscribe((itemRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); - // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas - // or semicolons) and thisRoute has been encoded with that function. If we want to compare - // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure - // the same characters are encoded the same way. - const itemRoute = this.router.parseUrl(getItemPageRoute(rd.payload)).toString(); + return itemRD$.pipe( + map((rd: RemoteData) => { + if (rd.hasSucceeded && hasValue(rd.payload)) { + const thisRoute = state.url; - if (!thisRoute.startsWith(itemRoute)) { - const itemId = rd.payload.uuid; - const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); - this.router.navigateByUrl(itemRoute + subRoute); - } + // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas + // or semicolons) and thisRoute has been encoded with that function. If we want to compare + // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure + // the same characters are encoded the same way. + const itemRoute = router.parseUrl(getItemPageRoute(rd.payload)).toString(); + + if (!thisRoute.startsWith(itemRoute)) { + const itemId = rd.payload.uuid; + const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); + router.navigateByUrl(itemRoute + subRoute); } - return rd; - }), - ); - } -} + } + return rd; + }), + ); +}; diff --git a/src/app/item-page/item.resolver.ts b/src/app/item-page/item.resolver.ts index 385ba81dc4..c41fe86d0c 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -31,38 +32,24 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ followLink('thumbnail'), ]; -/** - * This class represents a resolver that requests a specific item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemResolver { - constructor( - protected itemService: ItemDataService, - protected store: Store, - protected router: Router, - ) { - } +export const ItemResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemService: ItemDataService = inject(ItemDataService), + store: Store = inject(Store), +): Observable> => { + const itemRD$ = itemService.findById( + route.params.id, + true, + false, + ...ITEM_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * 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> { - const itemRD$ = this.itemService.findById(route.params.id, - true, - false, - ...ITEM_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + itemRD$.subscribe((itemRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); - itemRD$.subscribe((itemRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); - }); - - return itemRD$; - } -} + return itemRD$; +}; diff --git a/src/app/item-page/version-page/version.resolver.ts b/src/app/item-page/version-page/version.resolver.ts index 1f37f2ae67..1e20e1fefc 100644 --- a/src/app/item-page/version-page/version.resolver.ts +++ b/src/app/item-page/version-page/version.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../../app.reducer'; import { RemoteData } from '../../core/data/remote-data'; import { VersionDataService } from '../../core/data/version-data.service'; import { ResolvedAction } from '../../core/resolving/resolver.actions'; @@ -26,37 +27,31 @@ export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific version before the route is activated + * Method for resolving a version based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {VersionDataService} versionService + * @param {Store} store + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class VersionResolver { - constructor( - protected versionService: VersionDataService, - protected store: Store, - protected router: Router, - ) { - } +export const VersionResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + versionService: VersionDataService = inject(VersionDataService), + store: Store = inject(Store), +): Observable> => { + const versionRD$ = versionService.findById(route.params.id, + true, + false, + ...VERSION_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a version 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> { - const versionRD$ = this.versionService.findById(route.params.id, - true, - false, - ...VERSION_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + versionRD$.subscribe((versionRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, versionRD.payload)); + }); - versionRD$.subscribe((versionRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, versionRD.payload)); - }); - - return versionRD$; - } -} + return versionRD$; +}; diff --git a/src/app/menu-resolver.service.ts b/src/app/menu-resolver.service.ts new file mode 100644 index 0000000000..144cad4547 --- /dev/null +++ b/src/app/menu-resolver.service.ts @@ -0,0 +1,846 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { + combineLatest, + combineLatest as observableCombineLatest, + Observable, +} from 'rxjs'; +import { + filter, + find, + map, + take, +} from 'rxjs/operators'; + +import { PUBLICATION_CLAIMS_PATH } from './admin/admin-notifications/admin-notifications-routing-paths'; +import { BrowseService } from './core/browse/browse.service'; +import { ConfigurationDataService } from './core/data/configuration-data.service'; +import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from './core/data/feature-authorization/feature-id'; +import { PaginatedList } from './core/data/paginated-list.model'; +import { + METADATA_EXPORT_SCRIPT_NAME, + METADATA_IMPORT_SCRIPT_NAME, + ScriptDataService, +} from './core/data/processes/script-data.service'; +import { RemoteData } from './core/data/remote-data'; +import { BrowseDefinition } from './core/shared/browse-definition.model'; +import { ConfigurationProperty } from './core/shared/configuration-property.model'; +import { getFirstCompletedRemoteData } from './core/shared/operators'; +import { ThemedCreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component'; +import { ThemedCreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component'; +import { ThemedCreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; +import { ThemedEditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component'; +import { ThemedEditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component'; +import { ThemedEditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component'; +import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component'; +import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; +import { hasValue } from './shared/empty.util'; +import { MenuService } from './shared/menu/menu.service'; +import { MenuID } from './shared/menu/menu-id.model'; +import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model'; +import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model'; +import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model'; +import { MenuItemType } from './shared/menu/menu-item-type.model'; +import { MenuState } from './shared/menu/menu-state.model'; + +/** + * Creates all of the app's menus + */ +@Injectable({ + providedIn: 'root', +}) +export class MenuResolverService { + constructor( + protected menuService: MenuService, + protected browseService: BrowseService, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, + protected scriptDataService: ScriptDataService, + protected configurationDataService: ConfigurationDataService, + ) { + } + + /** + * Initialize all menus + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return combineLatest([ + this.createPublicMenu$(), + this.createAdminMenu$(), + ]).pipe( + map((menusDone: boolean[]) => menusDone.every(Boolean)), + ); + } + + /** + * Wait for a specific menu to appear + * @param id the ID of the menu to wait for + * @return an Observable that emits true as soon as the menu is created + */ + protected waitForMenu$(id: MenuID): Observable { + return this.menuService.getMenu(id).pipe( + find((menu: MenuState) => hasValue(menu)), + map(() => true), + ); + } + + /** + * Initialize all menu sections and items for {@link MenuID.PUBLIC} + */ + createPublicMenu$(): Observable { + const menuList: any[] = [ + /* Communities & Collections tree */ + { + id: `browse_global_communities_and_collections`, + active: false, + visible: true, + index: 0, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_communities_and_collections`, + link: `/community-list`, + } as LinkMenuItemModel, + }, + ]; + // Read the different Browse-By types from config and add them to the browse menu + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => { + menuList.push({ + id: `browse_global_by_${browseDef.id}`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_by_${browseDef.id}`, + link: `/browse/${browseDef.id}`, + } as LinkMenuItemModel, + }); + }); + menuList.push( + /* Browse */ + { + id: 'browse_global', + active: false, + visible: true, + index: 1, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.browse_global', + } as TextMenuItemModel, + }, + ); + } + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + + return this.waitForMenu$(MenuID.PUBLIC); + } + + /** + * Initialize all menu sections and items for {@link MenuID.ADMIN} + */ + createAdminMenu$() { + this.createMainMenuSections(); + this.createSiteAdministratorMenuSections(); + this.createExportMenuSections(); + this.createImportMenuSections(); + this.createAccessControlMenuSections(); + this.createReportMenuSections(); + + return this.waitForMenu$(MenuID.ADMIN); + } + + /** + * Initialize the main menu sections. + * edit_community / edit_collection is only included if the current user is a Community or Collection admin + */ + createMainMenuSections() { + combineLatest([ + this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), + this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.authorizationService.isAuthorized(FeatureID.CanSubmit), + this.authorizationService.isAuthorized(FeatureID.CanEditItem), + this.authorizationService.isAuthorized(FeatureID.CanSeeQA), + this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled), + ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem, canSeeQa, isCoarNotifyEnabled]) => { + const newSubMenuList = [ + { + id: 'new_community', + parentID: 'new', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_community', + function: () => { + this.modalService.open(ThemedCreateCommunityParentSelectorComponent); + }, + } as OnClickMenuItemModel, + }, + { + id: 'new_collection', + parentID: 'new', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_collection', + function: () => { + this.modalService.open(ThemedCreateCollectionParentSelectorComponent); + }, + } as OnClickMenuItemModel, + }, + { + id: 'new_item', + parentID: 'new', + active: false, + visible: canSubmit, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_item', + function: () => { + this.modalService.open(ThemedCreateItemParentSelectorComponent); + }, + } as OnClickMenuItemModel, + }, + { + id: 'new_process', + parentID: 'new', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.new_process', + link: '/processes/new', + } as LinkMenuItemModel, + },/* ldn_services */ + { + id: 'ldn_services_new', + parentID: 'new', + active: false, + visible: isSiteAdmin && isCoarNotifyEnabled, + model: { + type: MenuItemType.LINK, + text: 'menu.section.services_new', + link: '/admin/ldn/services/new', + } as LinkMenuItemModel, + icon: '', + }, + ]; + const editSubMenuList = [ + /* Edit */ + { + id: 'edit_community', + parentID: 'edit', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_community', + function: () => { + this.modalService.open(ThemedEditCommunitySelectorComponent); + }, + } as OnClickMenuItemModel, + }, + { + id: 'edit_collection', + parentID: 'edit', + active: false, + visible: isCollectionAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_collection', + function: () => { + this.modalService.open(ThemedEditCollectionSelectorComponent); + }, + } as OnClickMenuItemModel, + }, + { + id: 'edit_item', + parentID: 'edit', + active: false, + visible: canEditItem, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_item', + function: () => { + this.modalService.open(ThemedEditItemSelectorComponent); + }, + } as OnClickMenuItemModel, + }, + ]; + const newSubMenu = { + id: 'new', + active: false, + visible: newSubMenuList.some(subMenu => subMenu.visible), + model: { + type: MenuItemType.TEXT, + text: 'menu.section.new', + } as TextMenuItemModel, + icon: 'plus', + index: 0, + }; + const editSubMenu = { + id: 'edit', + active: false, + visible: editSubMenuList.some(subMenu => subMenu.visible), + model: { + type: MenuItemType.TEXT, + text: 'menu.section.edit', + } as TextMenuItemModel, + icon: 'pencil-alt', + index: 1, + }; + + const menuList = [ + ...newSubMenuList, + newSubMenu, + ...editSubMenuList, + editSubMenu, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'new_item_version', + // parentID: 'new', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.new_item_version', + // link: '' + // } as LinkMenuItemModel, + // }, + + /* Statistics */ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'statistics_task', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.statistics_task', + // link: '' + // } as LinkMenuItemModel, + // icon: 'chart-bar', + // index: 8 + // }, + + /* Control Panel */ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'control_panel', + // active: false, + // visible: isSiteAdmin, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.control_panel', + // link: '' + // } as LinkMenuItemModel, + // icon: 'cogs', + // index: 9 + // }, + + /* Processes */ + { + id: 'processes', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.processes', + link: '/processes', + } as LinkMenuItemModel, + icon: 'terminal', + index: 10, + }, + /* COAR Notify section */ + { + id: 'coar_notify', + active: false, + visible: isSiteAdmin && isCoarNotifyEnabled, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.coar_notify', + } as TextMenuItemModel, + icon: 'inbox', + index: 13, + }, + { + id: 'notify_dashboard', + active: false, + parentID: 'coar_notify', + visible: isSiteAdmin && isCoarNotifyEnabled, + model: { + type: MenuItemType.LINK, + text: 'menu.section.notify_dashboard', + link: '/admin/notify-dashboard', + } as LinkMenuItemModel, + }, + /* LDN Services */ + { + id: 'ldn_services', + active: false, + parentID: 'coar_notify', + visible: isSiteAdmin && isCoarNotifyEnabled, + model: { + type: MenuItemType.LINK, + text: 'menu.section.services', + link: '/admin/ldn/services', + } as LinkMenuItemModel, + }, + { + id: 'health', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.health', + link: '/health', + } as LinkMenuItemModel, + icon: 'heartbeat', + index: 11, + }, + /* Notifications */ + { + id: 'notifications', + active: false, + visible: canSeeQa || isSiteAdmin, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.notifications', + } as TextMenuItemModel, + icon: 'bell', + index: 4, + }, + { + id: 'notifications_quality-assurance', + parentID: 'notifications', + active: false, + visible: canSeeQa, + model: { + type: MenuItemType.LINK, + text: 'menu.section.quality-assurance', + link: '/notifications/quality-assurance', + } as LinkMenuItemModel, + }, + { + id: 'notifications_publication-claim', + parentID: 'notifications', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.notifications_publication-claim', + link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH, + } as LinkMenuItemModel, + }, + /* Admin Search */ + ]; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not + * the export scripts exist and the current user is allowed to execute them + */ + createExportMenuSections() { + const menuList = [ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_community', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_community', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_collection', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_collection', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_item', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_item', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + ]; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); + + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME), + ]).pipe( + filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists), + take(1), + ).subscribe(() => { + // Hides the export menu for unauthorised people + // If in the future more sub-menus are added, + // it should be reviewed if they need to be in this subscribe + this.menuService.addSection(MenuID.ADMIN, { + id: 'export', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.export', + } as TextMenuItemModel, + icon: 'file-export', + index: 3, + shouldPersistOnRouteChange: true, + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'export_metadata', + parentID: 'export', + active: true, + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.export_metadata', + function: () => { + this.modalService.open(ExportMetadataSelectorComponent); + }, + } as OnClickMenuItemModel, + shouldPersistOnRouteChange: true, + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'export_batch', + parentID: 'export', + active: false, + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.export_batch', + function: () => { + this.modalService.open(ExportBatchSelectorComponent); + }, + } as OnClickMenuItemModel, + shouldPersistOnRouteChange: true, + }); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not + * the import scripts exist and the current user is allowed to execute them + */ + createImportMenuSections() { + const menuList = []; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); + + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME), + ]).pipe( + filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), + take(1), + ).subscribe(() => { + // Hides the import menu for unauthorised people + // If in the future more sub-menus are added, + // it should be reviewed if they need to be in this subscribe + this.menuService.addSection(MenuID.ADMIN, { + id: 'import', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.import', + } as TextMenuItemModel, + icon: 'file-import', + index: 2, + shouldPersistOnRouteChange: true, + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'import_metadata', + parentID: 'import', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_metadata', + link: '/admin/metadata-import', + } as LinkMenuItemModel, + shouldPersistOnRouteChange: true, + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'import_batch', + parentID: 'import', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_batch', + link: '/admin/batch-import', + } as LinkMenuItemModel, + shouldPersistOnRouteChange: true, + }); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator + */ + createSiteAdministratorMenuSections() { + this.authorizationService.isAuthorized(FeatureID.AdministratorOf) + .subscribe((authorized) => { + const menuList = [ + { + id: 'admin_search', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.admin_search', + link: '/admin/search', + } as LinkMenuItemModel, + icon: 'search', + index: 5, + }, + /* Registries */ + { + id: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.registries', + } as TextMenuItemModel, + icon: 'list', + index: 6, + }, + { + id: 'registries_metadata', + parentID: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_metadata', + link: 'admin/registries/metadata', + } as LinkMenuItemModel, + }, + { + id: 'registries_format', + parentID: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_format', + link: 'admin/registries/bitstream-formats', + } as LinkMenuItemModel, + }, + + /* Curation tasks */ + { + id: 'curation_tasks', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.curation_task', + link: 'admin/curation-tasks', + } as LinkMenuItemModel, + icon: 'filter', + index: 7, + }, + + /* Workflow */ + { + id: 'workflow', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.workflow', + link: '/admin/workflow', + } as LinkMenuItemModel, + icon: 'user-check', + index: 11, + }, + { + id: 'system_wide_alert', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.system-wide-alert', + link: '/admin/system-wide-alert', + } as LinkMenuItemModel, + icon: 'exclamation-circle', + index: 12, + }, + ]; + + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + } + + /** + * Create menu sections dependent on whether or not the current user can manage access control groups + */ + createAccessControlMenuSections() { + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.authorizationService.isAuthorized(FeatureID.CanManageGroups), + ]).subscribe(([isSiteAdmin, canManageGroups]) => { + const menuList = [ + /* Access Control */ + { + id: 'access_control_people', + parentID: 'access_control', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_people', + link: '/access-control/epeople', + } as LinkMenuItemModel, + }, + { + id: 'access_control_groups', + parentID: 'access_control', + active: false, + visible: canManageGroups, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_groups', + link: '/access-control/groups', + } as LinkMenuItemModel, + }, + { + id: 'access_control_bulk', + parentID: 'access_control', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_bulk', + link: '/access-control/bulk-access', + } as LinkMenuItemModel, + }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'access_control_authorizations', + // parentID: 'access_control', + // active: false, + // visible: authorized, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.access_control_authorizations', + // link: '' + // } as LinkMenuItemModel, + // }, + { + id: 'access_control', + active: false, + visible: canManageGroups || isSiteAdmin, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.access_control', + } as TextMenuItemModel, + icon: 'key', + index: 4, + }, + ]; + + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator + */ + createReportMenuSections() { + observableCombineLatest([ + this.configurationDataService.findByPropertyName('contentreport.enable').pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'), + ), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + ]).subscribe(([isSiteAdmin]) => { + const menuList = [ + { + id: 'reports', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.reports', + } as TextMenuItemModel, + icon: 'file-alt', + index: 5, + }, + /* Collections Report */ + { + id: 'reports_collections', + parentID: 'reports', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.collections', + link: '/admin/reports/collections', + } as LinkMenuItemModel, + icon: 'user-check', + }, + /* Queries Report */ + { + id: 'reports_queries', + parentID: 'reports', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.queries', + link: '/admin/reports/queries', + } as LinkMenuItemModel, + icon: 'user-check', + }, + ]; + + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + } +} diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index 0a1a050637..bc35655f01 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -19,7 +19,6 @@ import { ConfigurationDataService } from './core/data/configuration-data.service import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; import { FeatureID } from './core/data/feature-authorization/feature-id'; import { ScriptDataService } from './core/data/processes/script-data.service'; -import { MenuResolver } from './menu.resolver'; import { MenuService } from './shared/menu/menu.service'; import { MenuID } from './shared/menu/menu-id.model'; import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils'; @@ -27,6 +26,7 @@ import { ConfigurationDataServiceStub } from './shared/testing/configuration-dat import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { createPaginatedList } from './shared/testing/utils.test'; import createSpy = jasmine.createSpy; +import { MenuResolverService } from './menu-resolver.service'; const BOOLEAN = { t: true, f: false }; const MENU_STATE = { @@ -39,7 +39,7 @@ const BROWSE_DEFINITIONS = [ ]; describe('MenuResolver', () => { - let resolver: MenuResolver; + let resolver: MenuResolverService; let menuService; let browseService; @@ -79,12 +79,12 @@ describe('MenuResolver', () => { { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ScriptDataService, useValue: scriptService }, { provide: ConfigurationDataService, useValue: configurationDataService }, - { - provide: NgbModal, useValue: mockNgbModal }, + { provide: NgbModal, useValue: mockNgbModal }, + MenuResolverService, ], schemas: [NO_ERRORS_SCHEMA], }); - resolver = TestBed.inject(MenuResolver); + resolver = TestBed.inject(MenuResolverService); })); it('should be created', () => { diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 2be5c3d3d5..ec99ee753d 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -1,846 +1,21 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { - combineLatest, - combineLatest as observableCombineLatest, - Observable, -} from 'rxjs'; -import { - filter, - find, - map, - take, -} from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { MenuResolverService } from './menu-resolver.service'; -import { PUBLICATION_CLAIMS_PATH } from './admin/admin-notifications/admin-notifications-routing-paths'; -import { BrowseService } from './core/browse/browse.service'; -import { ConfigurationDataService } from './core/data/configuration-data.service'; -import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from './core/data/feature-authorization/feature-id'; -import { PaginatedList } from './core/data/paginated-list.model'; -import { - METADATA_EXPORT_SCRIPT_NAME, - METADATA_IMPORT_SCRIPT_NAME, - ScriptDataService, -} from './core/data/processes/script-data.service'; -import { RemoteData } from './core/data/remote-data'; -import { BrowseDefinition } from './core/shared/browse-definition.model'; -import { ConfigurationProperty } from './core/shared/configuration-property.model'; -import { getFirstCompletedRemoteData } from './core/shared/operators'; -import { ThemedCreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component'; -import { ThemedCreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component'; -import { ThemedCreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; -import { ThemedEditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component'; -import { ThemedEditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component'; -import { ThemedEditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component'; -import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component'; -import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; -import { hasValue } from './shared/empty.util'; -import { MenuService } from './shared/menu/menu.service'; -import { MenuID } from './shared/menu/menu-id.model'; -import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model'; -import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model'; -import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model'; -import { MenuItemType } from './shared/menu/menu-item-type.model'; -import { MenuState } from './shared/menu/menu-state.model'; /** - * Creates all of the app's menus + * Initialize all menus */ -@Injectable({ - providedIn: 'root', -}) -export class MenuResolver { - constructor( - protected menuService: MenuService, - protected browseService: BrowseService, - protected authorizationService: AuthorizationDataService, - protected modalService: NgbModal, - protected scriptDataService: ScriptDataService, - protected configurationDataService: ConfigurationDataService, - ) { - } - - /** - * Initialize all menus - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return combineLatest([ - this.createPublicMenu$(), - this.createAdminMenu$(), - ]).pipe( - map((menusDone: boolean[]) => menusDone.every(Boolean)), - ); - } - - /** - * Wait for a specific menu to appear - * @param id the ID of the menu to wait for - * @return an Observable that emits true as soon as the menu is created - */ - protected waitForMenu$(id: MenuID): Observable { - return this.menuService.getMenu(id).pipe( - find((menu: MenuState) => hasValue(menu)), - map(() => true), - ); - } - - /** - * Initialize all menu sections and items for {@link MenuID.PUBLIC} - */ - createPublicMenu$(): Observable { - const menuList: any[] = [ - /* Communities & Collections tree */ - { - id: `browse_global_communities_and_collections`, - active: false, - visible: true, - index: 0, - model: { - type: MenuItemType.LINK, - text: `menu.section.browse_global_communities_and_collections`, - link: `/community-list`, - } as LinkMenuItemModel, - }, - ]; - // Read the different Browse-By types from config and add them to the browse menu - this.browseService.getBrowseDefinitions() - .pipe(getFirstCompletedRemoteData>()) - .subscribe((browseDefListRD: RemoteData>) => { - if (browseDefListRD.hasSucceeded) { - browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => { - menuList.push({ - id: `browse_global_by_${browseDef.id}`, - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: `menu.section.browse_global_by_${browseDef.id}`, - link: `/browse/${browseDef.id}`, - } as LinkMenuItemModel, - }); - }); - menuList.push( - /* Browse */ - { - id: 'browse_global', - active: false, - visible: true, - index: 1, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.browse_global', - } as TextMenuItemModel, - }, - ); - } - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - - return this.waitForMenu$(MenuID.PUBLIC); - } - - /** - * Initialize all menu sections and items for {@link MenuID.ADMIN} - */ - createAdminMenu$() { - this.createMainMenuSections(); - this.createSiteAdministratorMenuSections(); - this.createExportMenuSections(); - this.createImportMenuSections(); - this.createAccessControlMenuSections(); - this.createReportMenuSections(); - - return this.waitForMenu$(MenuID.ADMIN); - } - - /** - * Initialize the main menu sections. - * edit_community / edit_collection is only included if the current user is a Community or Collection admin - */ - createMainMenuSections() { - combineLatest([ - this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), - this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.authorizationService.isAuthorized(FeatureID.CanSubmit), - this.authorizationService.isAuthorized(FeatureID.CanEditItem), - this.authorizationService.isAuthorized(FeatureID.CanSeeQA), - this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled), - ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem, canSeeQa, isCoarNotifyEnabled]) => { - const newSubMenuList = [ - { - id: 'new_community', - parentID: 'new', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_community', - function: () => { - this.modalService.open(ThemedCreateCommunityParentSelectorComponent); - }, - } as OnClickMenuItemModel, - }, - { - id: 'new_collection', - parentID: 'new', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_collection', - function: () => { - this.modalService.open(ThemedCreateCollectionParentSelectorComponent); - }, - } as OnClickMenuItemModel, - }, - { - id: 'new_item', - parentID: 'new', - active: false, - visible: canSubmit, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_item', - function: () => { - this.modalService.open(ThemedCreateItemParentSelectorComponent); - }, - } as OnClickMenuItemModel, - }, - { - id: 'new_process', - parentID: 'new', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.new_process', - link: '/processes/new', - } as LinkMenuItemModel, - },/* ldn_services */ - { - id: 'ldn_services_new', - parentID: 'new', - active: false, - visible: isSiteAdmin && isCoarNotifyEnabled, - model: { - type: MenuItemType.LINK, - text: 'menu.section.services_new', - link: '/admin/ldn/services/new', - } as LinkMenuItemModel, - icon: '', - }, - ]; - const editSubMenuList = [ - /* Edit */ - { - id: 'edit_community', - parentID: 'edit', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_community', - function: () => { - this.modalService.open(ThemedEditCommunitySelectorComponent); - }, - } as OnClickMenuItemModel, - }, - { - id: 'edit_collection', - parentID: 'edit', - active: false, - visible: isCollectionAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_collection', - function: () => { - this.modalService.open(ThemedEditCollectionSelectorComponent); - }, - } as OnClickMenuItemModel, - }, - { - id: 'edit_item', - parentID: 'edit', - active: false, - visible: canEditItem, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_item', - function: () => { - this.modalService.open(ThemedEditItemSelectorComponent); - }, - } as OnClickMenuItemModel, - }, - ]; - const newSubMenu = { - id: 'new', - active: false, - visible: newSubMenuList.some(subMenu => subMenu.visible), - model: { - type: MenuItemType.TEXT, - text: 'menu.section.new', - } as TextMenuItemModel, - icon: 'plus', - index: 0, - }; - const editSubMenu = { - id: 'edit', - active: false, - visible: editSubMenuList.some(subMenu => subMenu.visible), - model: { - type: MenuItemType.TEXT, - text: 'menu.section.edit', - } as TextMenuItemModel, - icon: 'pencil-alt', - index: 1, - }; - - const menuList = [ - ...newSubMenuList, - newSubMenu, - ...editSubMenuList, - editSubMenu, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'new_item_version', - // parentID: 'new', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.new_item_version', - // link: '' - // } as LinkMenuItemModel, - // }, - - /* Statistics */ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'statistics_task', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.statistics_task', - // link: '' - // } as LinkMenuItemModel, - // icon: 'chart-bar', - // index: 8 - // }, - - /* Control Panel */ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'control_panel', - // active: false, - // visible: isSiteAdmin, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.control_panel', - // link: '' - // } as LinkMenuItemModel, - // icon: 'cogs', - // index: 9 - // }, - - /* Processes */ - { - id: 'processes', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.processes', - link: '/processes', - } as LinkMenuItemModel, - icon: 'terminal', - index: 10, - }, - /* COAR Notify section */ - { - id: 'coar_notify', - active: false, - visible: isSiteAdmin && isCoarNotifyEnabled, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.coar_notify', - } as TextMenuItemModel, - icon: 'inbox', - index: 13, - }, - { - id: 'notify_dashboard', - active: false, - parentID: 'coar_notify', - visible: isSiteAdmin && isCoarNotifyEnabled, - model: { - type: MenuItemType.LINK, - text: 'menu.section.notify_dashboard', - link: '/admin/notify-dashboard', - } as LinkMenuItemModel, - }, - /* LDN Services */ - { - id: 'ldn_services', - active: false, - parentID: 'coar_notify', - visible: isSiteAdmin && isCoarNotifyEnabled, - model: { - type: MenuItemType.LINK, - text: 'menu.section.services', - link: '/admin/ldn/services', - } as LinkMenuItemModel, - }, - { - id: 'health', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.health', - link: '/health', - } as LinkMenuItemModel, - icon: 'heartbeat', - index: 11, - }, - /* Notifications */ - { - id: 'notifications', - active: false, - visible: canSeeQa || isSiteAdmin, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.notifications', - } as TextMenuItemModel, - icon: 'bell', - index: 4, - }, - { - id: 'notifications_quality-assurance', - parentID: 'notifications', - active: false, - visible: canSeeQa, - model: { - type: MenuItemType.LINK, - text: 'menu.section.quality-assurance', - link: '/notifications/quality-assurance', - } as LinkMenuItemModel, - }, - { - id: 'notifications_publication-claim', - parentID: 'notifications', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.notifications_publication-claim', - link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH, - } as LinkMenuItemModel, - }, - /* Admin Search */ - ]; - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not - * the export scripts exist and the current user is allowed to execute them - */ - createExportMenuSections() { - const menuList = [ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_community', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_community', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_collection', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_collection', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_item', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_item', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - ]; - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); - - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME), - ]).pipe( - filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists), - take(1), - ).subscribe(() => { - // Hides the export menu for unauthorised people - // If in the future more sub-menus are added, - // it should be reviewed if they need to be in this subscribe - this.menuService.addSection(MenuID.ADMIN, { - id: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.export', - } as TextMenuItemModel, - icon: 'file-export', - index: 3, - shouldPersistOnRouteChange: true, - }); - this.menuService.addSection(MenuID.ADMIN, { - id: 'export_metadata', - parentID: 'export', - active: true, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.export_metadata', - function: () => { - this.modalService.open(ExportMetadataSelectorComponent); - }, - } as OnClickMenuItemModel, - shouldPersistOnRouteChange: true, - }); - this.menuService.addSection(MenuID.ADMIN, { - id: 'export_batch', - parentID: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.export_batch', - function: () => { - this.modalService.open(ExportBatchSelectorComponent); - }, - } as OnClickMenuItemModel, - shouldPersistOnRouteChange: true, - }); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not - * the import scripts exist and the current user is allowed to execute them - */ - createImportMenuSections() { - const menuList = []; - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); - - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME), - ]).pipe( - filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), - take(1), - ).subscribe(() => { - // Hides the import menu for unauthorised people - // If in the future more sub-menus are added, - // it should be reviewed if they need to be in this subscribe - this.menuService.addSection(MenuID.ADMIN, { - id: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.import', - } as TextMenuItemModel, - icon: 'file-import', - index: 2, - shouldPersistOnRouteChange: true, - }); - this.menuService.addSection(MenuID.ADMIN, { - id: 'import_metadata', - parentID: 'import', - active: true, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_metadata', - link: '/admin/metadata-import', - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true, - }); - this.menuService.addSection(MenuID.ADMIN, { - id: 'import_batch', - parentID: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_batch', - link: '/admin/batch-import', - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true, - }); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator - */ - createSiteAdministratorMenuSections() { - this.authorizationService.isAuthorized(FeatureID.AdministratorOf) - .subscribe((authorized) => { - const menuList = [ - { - id: 'admin_search', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.admin_search', - link: '/admin/search', - } as LinkMenuItemModel, - icon: 'search', - index: 5, - }, - /* Registries */ - { - id: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.registries', - } as TextMenuItemModel, - icon: 'list', - index: 6, - }, - { - id: 'registries_metadata', - parentID: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_metadata', - link: 'admin/registries/metadata', - } as LinkMenuItemModel, - }, - { - id: 'registries_format', - parentID: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_format', - link: 'admin/registries/bitstream-formats', - } as LinkMenuItemModel, - }, - - /* Curation tasks */ - { - id: 'curation_tasks', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.curation_task', - link: 'admin/curation-tasks', - } as LinkMenuItemModel, - icon: 'filter', - index: 7, - }, - - /* Workflow */ - { - id: 'workflow', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.workflow', - link: '/admin/workflow', - } as LinkMenuItemModel, - icon: 'user-check', - index: 11, - }, - { - id: 'system_wide_alert', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.system-wide-alert', - link: '/admin/system-wide-alert', - } as LinkMenuItemModel, - icon: 'exclamation-circle', - index: 12, - }, - ]; - - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - } - - /** - * Create menu sections dependent on whether or not the current user can manage access control groups - */ - createAccessControlMenuSections() { - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.authorizationService.isAuthorized(FeatureID.CanManageGroups), - ]).subscribe(([isSiteAdmin, canManageGroups]) => { - const menuList = [ - /* Access Control */ - { - id: 'access_control_people', - parentID: 'access_control', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_people', - link: '/access-control/epeople', - } as LinkMenuItemModel, - }, - { - id: 'access_control_groups', - parentID: 'access_control', - active: false, - visible: canManageGroups, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_groups', - link: '/access-control/groups', - } as LinkMenuItemModel, - }, - { - id: 'access_control_bulk', - parentID: 'access_control', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_bulk', - link: '/access-control/bulk-access', - } as LinkMenuItemModel, - }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'access_control_authorizations', - // parentID: 'access_control', - // active: false, - // visible: authorized, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.access_control_authorizations', - // link: '' - // } as LinkMenuItemModel, - // }, - { - id: 'access_control', - active: false, - visible: canManageGroups || isSiteAdmin, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.access_control', - } as TextMenuItemModel, - icon: 'key', - index: 4, - }, - ]; - - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator - */ - createReportMenuSections() { - observableCombineLatest([ - this.configurationDataService.findByPropertyName('contentreport.enable').pipe( - getFirstCompletedRemoteData(), - map((res: RemoteData) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'), - ), - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - ]).subscribe(([isSiteAdmin]) => { - const menuList = [ - { - id: 'reports', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.reports', - } as TextMenuItemModel, - icon: 'file-alt', - index: 5, - }, - /* Collections Report */ - { - id: 'reports_collections', - parentID: 'reports', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.reports.collections', - link: '/admin/reports/collections', - } as LinkMenuItemModel, - icon: 'user-check', - }, - /* Queries Report */ - { - id: 'reports_queries', - parentID: 'reports', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.reports.queries', - link: '/admin/reports/queries', - } as LinkMenuItemModel, - icon: 'user-check', - }, - ]; - - menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - } -} +export const MenuResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + menuResolverService: MenuResolverService = inject(MenuResolverService), +): Observable => { + return menuResolverService.resolve(route, state); +}; diff --git a/src/app/process-page/process-breadcrumb.resolver.spec.ts b/src/app/process-page/process-breadcrumb.resolver.spec.ts index e4bdcba537..6384b01917 100644 --- a/src/app/process-page/process-breadcrumb.resolver.spec.ts +++ b/src/app/process-page/process-breadcrumb.resolver.spec.ts @@ -5,7 +5,7 @@ import { Process } from './processes/process.model'; describe('ProcessBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: ProcessBreadcrumbResolver; + let resolver: any; let processDataService: ProcessDataService; let processBreadcrumbService: any; let process: Process; @@ -19,11 +19,16 @@ describe('ProcessBreadcrumbResolver', () => { processDataService = { findById: () => createSuccessfulRemoteDataObject$(process), } as any; - resolver = new ProcessBreadcrumbResolver(processBreadcrumbService, processDataService); + resolver = ProcessBreadcrumbResolver; }); it('should resolve the breadcrumb config', (done) => { - const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: process }, params: { id: id } } as any, { url: path } as any); + const resolvedConfig = resolver( + { data: { breadcrumbKey: process }, params: { id: id } } as any, + { url: path } as any, + processBreadcrumbService, + processDataService, + ); const expectedConfig = { provider: processBreadcrumbService, key: process, url: path }; resolvedConfig.subscribe((config) => { expect(config).toEqual(expectedConfig); @@ -33,7 +38,7 @@ describe('ProcessBreadcrumbResolver', () => { it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, processBreadcrumbService, processDataService); }).toThrow(); }); }); diff --git a/src/app/process-page/process-breadcrumb.resolver.ts b/src/app/process-page/process-breadcrumb.resolver.ts index 3e1043241f..a23d790c71 100644 --- a/src/app/process-page/process-breadcrumb.resolver.ts +++ b/src/app/process-page/process-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -15,30 +16,28 @@ import { ProcessBreadcrumbsService } from './process-breadcrumbs.service'; import { Process } from './processes/process.model'; /** - * This class represents a resolver that requests a specific process before the route is activated + * Method for resolving a process based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param breadcrumbService + * @param processService + * @returns Observable<> Emits the found process based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ProcessBreadcrumbResolver { - constructor(protected breadcrumbService: ProcessBreadcrumbsService, private processService: ProcessDataService) { - } +export const ProcessBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: ProcessBreadcrumbsService = inject(ProcessBreadcrumbsService), + processService: ProcessDataService = inject(ProcessDataService), +): Observable> => { + const id = route.params.id; - /** - * Method for resolving a process 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 process based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const id = route.params.id; - - return this.processService.findById(route.params.id, true, false, followLink('script')).pipe( - getFirstCompletedRemoteData(), - map((object: RemoteData) => { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(id)) + id; - return { provider: this.breadcrumbService, key: object.payload, url: url }; - }), - ); - } -} + return processService.findById(route.params.id, true, false, followLink('script')).pipe( + getFirstCompletedRemoteData(), + map((object: RemoteData) => { + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(id)).concat(id); + return { provider: breadcrumbService, key: object.payload, url: url }; + }), + ); +}; diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index b5b0ccb28e..029e1dfd8f 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -16,23 +17,19 @@ export const PROCESS_PAGE_FOLLOW_LINKS = [ ]; /** - * This class represents a resolver that requests a specific process before the route is activated + * Method for resolving a process based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {ProcessDataService} processService + * @returns Observable<> Emits the found process based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ProcessPageResolver { - constructor(private processService: ProcessDataService) { - } - - /** - * Method for resolving a process 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 process based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const ProcessPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + processService: ProcessDataService = inject(ProcessDataService), +): Observable> => { + return processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts index 3374fd619b..c1f869a72e 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -14,22 +14,18 @@ export interface AssuranceEventsPageParams { } /** - * This class represents a resolver that retrieve the route data before the route is activated. + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns AdminQualityAssuranceEventsPageParams Emits the route parameters */ -@Injectable({ providedIn: 'root' }) -export class QualityAssuranceEventsPageResolver { - - /** - * Method for resolving the parameters in the current route. - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns AdminQualityAssuranceEventsPageParams Emits the route parameters - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AssuranceEventsPageParams { - return { - pageId: route.queryParams.pageId, - pageSize: parseInt(route.queryParams.pageSize, 10), - currentPage: parseInt(route.queryParams.page, 10), - }; - } -} +export const ItemResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): AssuranceEventsPageParams => { + return { + pageId: route.queryParams.pageId, + pageSize: parseInt(route.queryParams.pageSize, 10), + currentPage: parseInt(route.queryParams.page, 10), + }; +}; diff --git a/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts index 8019f57f90..4d2d24dcf9 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts @@ -1,53 +1,53 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { QualityAssuranceSourceObject } from '../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceService } from '../../notifications/qa/source/quality-assurance-source.service'; /** - * This class represents a resolver that retrieve the route data before the route is activated. + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param router + * @param qualityAssuranceSourceService + * @param appConfig + * @returns Observable */ -@Injectable({ providedIn: 'root' }) -export class SourceDataResolver { - private pageSize = environment.qualityAssuranceConfig.pageSize; - /** - * Initialize the effect class variables. - * @param {QualityAssuranceSourceService} qualityAssuranceSourceService - */ - constructor( - private qualityAssuranceSourceService: QualityAssuranceSourceService, - private router: Router, - ) { } - /** - * Method for resolving the parameters in the current route. - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.qualityAssuranceSourceService.getSources(this.pageSize, 0).pipe( - map((sources: PaginatedList) => { - if (sources.page.length === 1) { - this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]); - } - return sources.page; - })); - } +export const SourceDataResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + qualityAssuranceSourceService: QualityAssuranceSourceService = inject(QualityAssuranceSourceService), + appConfig: AppConfig = inject(APP_CONFIG), +): Observable => { + const pageSize = appConfig.qualityAssuranceConfig.pageSize; - /** - * - * @param route url path - * @returns url path - */ - getResolvedUrl(route: ActivatedRouteSnapshot): string { - return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/'); - } + return qualityAssuranceSourceService.getSources(pageSize, 0).pipe( + map((sources: PaginatedList) => { + if (sources.page.length === 1) { + router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]); + } + return sources.page; + })); +}; + +/** + * + * @param route url path + * @returns url path + */ +function getResolvedUrl(route: ActivatedRouteSnapshot): string { + return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/'); } diff --git a/src/app/register-email-form/registration.resolver.spec.ts b/src/app/register-email-form/registration.resolver.spec.ts index e286a548de..3515383da3 100644 --- a/src/app/register-email-form/registration.resolver.spec.ts +++ b/src/app/register-email-form/registration.resolver.spec.ts @@ -6,7 +6,7 @@ import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { RegistrationResolver } from './registration.resolver'; describe('RegistrationResolver', () => { - let resolver: RegistrationResolver; + let resolver: any; let epersonRegistrationService: EpersonRegistrationService; const token = 'test-token'; @@ -16,11 +16,11 @@ describe('RegistrationResolver', () => { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { searchByToken: createSuccessfulRemoteDataObject$(registration), }); - resolver = new RegistrationResolver(epersonRegistrationService); + resolver = RegistrationResolver; }); describe('resolve', () => { it('should resolve a registration based on the token', (done) => { - resolver.resolve({ params: { token: token } } as any, undefined) + resolver({ params: { token: token } } as any, undefined, epersonRegistrationService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/register-email-form/registration.resolver.ts b/src/app/register-email-form/registration.resolver.ts index e301b9a4a0..43a8a2f624 100644 --- a/src/app/register-email-form/registration.resolver.ts +++ b/src/app/register-email-form/registration.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -10,19 +11,13 @@ import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Registration } from '../core/shared/registration.model'; -@Injectable({ providedIn: 'root' }) -/** - * Resolver to resolve a Registration object based on the provided token - */ -export class RegistrationResolver { - - constructor(private epersonRegistrationService: EpersonRegistrationService) { - } - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const token = route.params.token; - return this.epersonRegistrationService.searchByToken(token).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const RegistrationResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService), +): Observable> => { + const token = route.params.token; + return epersonRegistrationService.searchByToken(token).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/request-copy/request-copy.resolver.ts b/src/app/request-copy/request-copy.resolver.ts index 2661ed4dfb..7617239df5 100644 --- a/src/app/request-copy/request-copy.resolver.ts +++ b/src/app/request-copy/request-copy.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -10,21 +11,12 @@ import { RemoteData } from '../core/data/remote-data'; import { ItemRequest } from '../core/shared/item-request.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -/** - * Resolves an {@link ItemRequest} from the token found in the route's parameters - */ -@Injectable({ providedIn: 'root' }) -export class RequestCopyResolver { - - constructor( - private itemRequestDataService: ItemRequestDataService, - ) { - } - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> | Promise> | RemoteData { - return this.itemRequestDataService.findById(route.params.token).pipe( - getFirstCompletedRemoteData(), - ); - } - -} +export const RequestCopyResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService), +): Observable> => { + return itemRequestDataService.findById(route.params.token).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts b/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts new file mode 100644 index 0000000000..e6bbeac619 --- /dev/null +++ b/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts @@ -0,0 +1,324 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; +import { + combineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { getDSORoute } from '../../app-routing-paths'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { Community } from '../../core/shared/community.model'; +import { Item } from '../../core/shared/item.model'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../../core/shared/operators'; +import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../empty.util'; +import { MenuService } from '../menu/menu.service'; +import { MenuID } from '../menu/menu-id.model'; +import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; +import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; +import { MenuItemType } from '../menu/menu-item-type.model'; +import { MenuSection } from '../menu/menu-section.model'; +import { NotificationsService } from '../notifications/notifications.service'; +import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; +import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; +import { + DsoWithdrawnReinstateModalService, + REQUEST_REINSTATE, + REQUEST_WITHDRAWN, +} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; + +/** + * Creates the menus for the dspace object pages + */ +@Injectable({ + providedIn: 'root', +}) +export class DSOEditMenuResolverService { + + constructor( + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected menuService: MenuService, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, + protected dsoVersioningModalService: DsoVersioningModalService, + protected researcherProfileService: ResearcherProfileDataService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, + private correctionTypeDataService: CorrectionTypeDataService, + ) { + } + + /** + * Initialise all dspace object related menus + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> { + let id = route.params.id; + if (hasNoValue(id) && hasValue(route.queryParams.scope)) { + id = route.queryParams.scope; + } + if (hasNoValue(id)) { + // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data + return observableOf({ ...route.data?.menu }); + } else { + return this.dSpaceObjectDataService.findById(id, true, false).pipe( + getFirstCompletedRemoteData(), + switchMap((dsoRD) => { + if (dsoRD.hasSucceeded) { + const dso = dsoRD.payload; + return combineLatest(this.getDsoMenus(dso, route, state)).pipe( + // Menu sections are retrieved as an array of arrays and flattened into a single array + map((combinedMenus) => [].concat.apply([], combinedMenus)), + map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), + map((menus) => { + return { + ...route.data?.menu, + [MenuID.DSO_EDIT]: menus, + }; + }), + ); + } else { + return observableOf({ ...route.data?.menu }); + } + }), + ); + } + } + + /** + * Return all the menus for a dso based on the route and state + */ + getDsoMenus(dso, route, state): Observable[] { + return [ + this.getItemMenu(dso), + this.getComColMenu(dso), + this.getCommonMenu(dso, state), + ]; + } + + /** + * Get the common menus between all dspace objects + */ + protected getCommonMenu(dso, state): Observable { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, dso.self), + ]).pipe( + map(([canEditItem]) => { + return [ + { + id: 'edit-dso', + active: false, + visible: canEditItem, + model: { + type: MenuItemType.LINK, + text: this.getDsoType(dso) + '.page.edit', + link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(), + } as LinkMenuItemModel, + icon: 'pencil-alt', + index: 2, + }, + ]; + }), + ); + } + + /** + * Get item specific menus + */ + protected getItemMenu(dso): Observable { + if (dso instanceof Item) { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), + this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), + this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), + this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), + this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), + this.correctionTypeDataService.findByItem(dso.uuid, false).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload()), + ]).pipe( + map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { + const isPerson = this.getDsoType(dso) === 'person'; + return [ + { + id: 'orcid-dso', + active: false, + visible: isPerson && canSynchronizeWithOrcid, + model: { + type: MenuItemType.LINK, + text: 'item.page.orcid.tooltip', + link: new URLCombiner(getDSORoute(dso), 'orcid').toString(), + } as LinkMenuItemModel, + icon: 'orcid fab fa-lg', + index: 0, + }, + { + id: 'version-dso', + active: false, + visible: canCreateVersion, + model: { + type: MenuItemType.ONCLICK, + text: versionTooltip, + disabled: disableVersioning, + function: () => { + this.dsoVersioningModalService.openCreateVersionModal(dso); + }, + } as OnClickMenuItemModel, + icon: 'code-branch', + index: 1, + }, + { + id: 'claim-dso', + active: false, + visible: isPerson && canClaimItem, + model: { + type: MenuItemType.ONCLICK, + text: 'item.page.claim.button', + function: () => { + this.claimResearcher(dso); + }, + } as OnClickMenuItemModel, + icon: 'hand-paper', + index: 3, + }, + { + id: 'withdrawn-item', + active: false, + visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN), + model: { + type: MenuItemType.ONCLICK, + text:'item.page.withdrawn', + function: () => { + this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived); + }, + } as OnClickMenuItemModel, + icon: 'eye-slash', + index: 4, + }, + { + id: 'reinstate-item', + active: false, + visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE), + model: { + type: MenuItemType.ONCLICK, + text:'item.page.reinstate', + function: () => { + this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived); + }, + } as OnClickMenuItemModel, + icon: 'eye', + index: 5, + }, + ]; + }), + ); + } else { + return observableOf([]); + } + } + + /** + * Get Community/Collection-specific menus + */ + protected getComColMenu(dso): Observable { + if (dso instanceof Community || dso instanceof Collection) { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), + ]).pipe( + map(([canSubscribe]) => { + return [ + { + id: 'subscribe', + active: false, + visible: canSubscribe, + model: { + type: MenuItemType.ONCLICK, + text: 'subscriptions.tooltip', + function: () => { + const modalRef = this.modalService.open(SubscriptionModalComponent); + modalRef.componentInstance.dso = dso; + }, + } as OnClickMenuItemModel, + icon: 'bell', + index: 4, + }, + ]; + }), + ); + } else { + return observableOf([]); + } + } + + /** + * Claim a researcher by creating a profile + * Shows notifications and/or hides the menu section on success/error + */ + protected claimResearcher(dso) { + this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self) + .subscribe((id: string) => { + if (isNotEmpty(id)) { + this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), + this.translate.get('researcherprofile.success.claim.body')); + this.authorizationService.invalidateAuthorizationsRequestCache(); + this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid); + } else { + this.notificationsService.error( + this.translate.get('researcherprofile.error.claim.title'), + this.translate.get('researcherprofile.error.claim.body')); + } + }); + } + + /** + * Retrieve the dso or entity type for an object to be used in generic messages + */ + protected getDsoType(dso) { + const renderType = dso.getRenderTypes()[0]; + if (typeof renderType === 'string' || renderType instanceof String) { + return renderType.toLowerCase(); + } else { + return dso.type.toString().toLowerCase(); + } + } + + /** + * Add the dso uuid to all provided menu ids and parent ids + */ + protected addDsoUuidToMenuIDs(menus, dso) { + return menus.map((menu) => { + Object.assign(menu, { + id: menu.id + '-' + dso.uuid, + }); + if (hasValue(menu.parentID)) { + Object.assign(menu, { + parentID: menu.parentID + '-' + dso.uuid, + }); + } + return menu; + }); + } + +} diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index 0ff9227b68..8ed5ee9af2 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts @@ -40,7 +40,7 @@ import { } from '../remote-data.utils'; import { MenuServiceStub } from '../testing/menu-service.stub'; import { createPaginatedList } from '../testing/utils.test'; -import { DSOEditMenuResolver } from './dso-edit-menu.resolver'; +import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service'; import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; @@ -50,7 +50,7 @@ describe('DSOEditMenuResolver', () => { id: 'some menu', }; - let resolver: DSOEditMenuResolver; + let resolver: DSOEditMenuResolverService; let dSpaceObjectDataService; let menuService; @@ -189,12 +189,12 @@ describe('DSOEditMenuResolver', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService }, { provide: CorrectionTypeDataService, useValue: correctionsDataService }, - { - provide: NgbModal, useValue: mockNgbModal }, + { provide: NgbModal, useValue: mockNgbModal }, + DSOEditMenuResolverService, ], schemas: [NO_ERRORS_SCHEMA], }); - resolver = TestBed.inject(DSOEditMenuResolver); + resolver = TestBed.inject(DSOEditMenuResolverService); spyOn(menuService, 'addSection'); })); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts index f11c474ea5..6058e63241 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -1,324 +1,21 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { - combineLatest, - Observable, - of as observableOf, -} from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; +import { Observable } from 'rxjs'; -import { getDSORoute } from '../../app-routing-paths'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; -import { Collection } from '../../core/shared/collection.model'; -import { Community } from '../../core/shared/community.model'; -import { Item } from '../../core/shared/item.model'; -import { - getFirstCompletedRemoteData, - getRemoteDataPayload, -} from '../../core/shared/operators'; -import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; -import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { - hasNoValue, - hasValue, - isNotEmpty, -} from '../empty.util'; -import { MenuService } from '../menu/menu.service'; -import { MenuID } from '../menu/menu-id.model'; -import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; -import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; -import { MenuItemType } from '../menu/menu-item-type.model'; import { MenuSection } from '../menu/menu-section.model'; -import { NotificationsService } from '../notifications/notifications.service'; -import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; -import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; -import { - DsoWithdrawnReinstateModalService, - REQUEST_REINSTATE, - REQUEST_WITHDRAWN, -} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; +import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service'; /** - * Creates the menus for the dspace object pages + * Initialise all dspace object related menus */ -@Injectable({ - providedIn: 'root', -}) -export class DSOEditMenuResolver { - - constructor( - protected dSpaceObjectDataService: DSpaceObjectDataService, - protected menuService: MenuService, - protected authorizationService: AuthorizationDataService, - protected modalService: NgbModal, - protected dsoVersioningModalService: DsoVersioningModalService, - protected researcherProfileService: ResearcherProfileDataService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, - protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, - private correctionTypeDataService: CorrectionTypeDataService, - ) { - } - - /** - * Initialise all dspace object related menus - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> { - let id = route.params.id; - if (hasNoValue(id) && hasValue(route.queryParams.scope)) { - id = route.queryParams.scope; - } - if (hasNoValue(id)) { - // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data - return observableOf({ ...route.data?.menu }); - } else { - return this.dSpaceObjectDataService.findById(id, true, false).pipe( - getFirstCompletedRemoteData(), - switchMap((dsoRD) => { - if (dsoRD.hasSucceeded) { - const dso = dsoRD.payload; - return combineLatest(this.getDsoMenus(dso, route, state)).pipe( - // Menu sections are retrieved as an array of arrays and flattened into a single array - map((combinedMenus) => [].concat.apply([], combinedMenus)), - map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), - map((menus) => { - return { - ...route.data?.menu, - [MenuID.DSO_EDIT]: menus, - }; - }), - ); - } else { - return observableOf({ ...route.data?.menu }); - } - }), - ); - } - } - - /** - * Return all the menus for a dso based on the route and state - */ - getDsoMenus(dso, route, state): Observable[] { - return [ - this.getItemMenu(dso), - this.getComColMenu(dso), - this.getCommonMenu(dso, state), - ]; - } - - /** - * Get the common menus between all dspace objects - */ - protected getCommonMenu(dso, state): Observable { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, dso.self), - ]).pipe( - map(([canEditItem]) => { - return [ - { - id: 'edit-dso', - active: false, - visible: canEditItem, - model: { - type: MenuItemType.LINK, - text: this.getDsoType(dso) + '.page.edit', - link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(), - } as LinkMenuItemModel, - icon: 'pencil-alt', - index: 2, - }, - ]; - }), - ); - } - - /** - * Get item specific menus - */ - protected getItemMenu(dso): Observable { - if (dso instanceof Item) { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), - this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), - this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), - this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), - this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), - this.correctionTypeDataService.findByItem(dso.uuid, false).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload()), - ]).pipe( - map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { - const isPerson = this.getDsoType(dso) === 'person'; - return [ - { - id: 'orcid-dso', - active: false, - visible: isPerson && canSynchronizeWithOrcid, - model: { - type: MenuItemType.LINK, - text: 'item.page.orcid.tooltip', - link: new URLCombiner(getDSORoute(dso), 'orcid').toString(), - } as LinkMenuItemModel, - icon: 'orcid fab fa-lg', - index: 0, - }, - { - id: 'version-dso', - active: false, - visible: canCreateVersion, - model: { - type: MenuItemType.ONCLICK, - text: versionTooltip, - disabled: disableVersioning, - function: () => { - this.dsoVersioningModalService.openCreateVersionModal(dso); - }, - } as OnClickMenuItemModel, - icon: 'code-branch', - index: 1, - }, - { - id: 'claim-dso', - active: false, - visible: isPerson && canClaimItem, - model: { - type: MenuItemType.ONCLICK, - text: 'item.page.claim.button', - function: () => { - this.claimResearcher(dso); - }, - } as OnClickMenuItemModel, - icon: 'hand-paper', - index: 3, - }, - { - id: 'withdrawn-item', - active: false, - visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.withdrawn', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye-slash', - index: 4, - }, - { - id: 'reinstate-item', - active: false, - visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.reinstate', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye', - index: 5, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Get Community/Collection-specific menus - */ - protected getComColMenu(dso): Observable { - if (dso instanceof Community || dso instanceof Collection) { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), - ]).pipe( - map(([canSubscribe]) => { - return [ - { - id: 'subscribe', - active: false, - visible: canSubscribe, - model: { - type: MenuItemType.ONCLICK, - text: 'subscriptions.tooltip', - function: () => { - const modalRef = this.modalService.open(SubscriptionModalComponent); - modalRef.componentInstance.dso = dso; - }, - } as OnClickMenuItemModel, - icon: 'bell', - index: 4, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Claim a researcher by creating a profile - * Shows notifications and/or hides the menu section on success/error - */ - protected claimResearcher(dso) { - this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self) - .subscribe((id: string) => { - if (isNotEmpty(id)) { - this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), - this.translate.get('researcherprofile.success.claim.body')); - this.authorizationService.invalidateAuthorizationsRequestCache(); - this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid); - } else { - this.notificationsService.error( - this.translate.get('researcherprofile.error.claim.title'), - this.translate.get('researcherprofile.error.claim.body')); - } - }); - } - - /** - * Retrieve the dso or entity type for an object to be used in generic messages - */ - protected getDsoType(dso) { - const renderType = dso.getRenderTypes()[0]; - if (typeof renderType === 'string' || renderType instanceof String) { - return renderType.toLowerCase(); - } else { - return dso.type.toString().toLowerCase(); - } - } - - /** - * Add the dso uuid to all provided menu ids and parent ids - */ - protected addDsoUuidToMenuIDs(menus, dso) { - return menus.map((menu) => { - Object.assign(menu, { - id: menu.id + '-' + dso.uuid, - }); - if (hasValue(menu.parentID)) { - Object.assign(menu, { - parentID: menu.parentID + '-' + dso.uuid, - }); - } - return menu; - }); - } - -} +export const DSOEditMenuResolver: ResolveFn<{ [key: string]: MenuSection[] }> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + menuResolverService: DSOEditMenuResolverService = inject(DSOEditMenuResolverService), +): Observable<{ [key: string]: MenuSection[] }> => { + return menuResolverService.resolve(route, state); +}; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 2aa5485371..1ef9a9e350 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -53,6 +53,7 @@ import { DynamicFormValidationService, DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; +import { DynamicFormControlMapFn } from '@ng-dynamic-forms/core/lib/service/dynamic-form-component.service'; import { Store } from '@ngrx/store'; import { TranslateModule, @@ -74,7 +75,6 @@ import { import { APP_CONFIG, AppConfig, - DynamicFormControlFn, } from '../../../../../config/app-config.interface'; import { AppState } from '../../../../app.reducer'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -214,7 +214,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo public formBuilderService: FormBuilderService, private submissionService: SubmissionService, @Inject(APP_CONFIG) protected appConfig: AppConfig, - @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlFn, + @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn, ) { super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; diff --git a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts index 131824d144..ad28618cf1 100644 --- a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts +++ b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts @@ -1,21 +1,18 @@ import { - Inject, - Injectable, + inject, InjectionToken, Injector, } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { - APP_DATA_SERVICES_MAP, - LazyDataServicesMap, -} from '../../../../config/app-config.interface'; +import { LazyDataServicesMap } from '../../../../config/app-config.interface'; import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { lazyService } from '../../../core/lazy-service'; @@ -25,40 +22,36 @@ import { ResourceType } from '../../../core/shared/resource-type'; import { isEmpty } from '../../empty.util'; /** - * This class represents a resolver that requests a specific item before the route is activated + * 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 + * @param dataServiceMap + * @param parentInjector + * @param router + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ResourcePolicyTargetResolver { +export const ResourcePolicyTargetResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + dataServiceMap: InjectionToken = inject(InjectionToken), + parentInjector: Injector = inject(Injector), + router: Router = inject(Router), +): Observable> => { + const targetType = route.queryParamMap.get('targetType'); + const policyTargetId = route.queryParamMap.get('policyTargetId'); - constructor( - private parentInjector: Injector, - private router: Router, - @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { + if (isEmpty(targetType) || isEmpty(policyTargetId)) { + router.navigateByUrl('/404', { skipLocationChange: true }); } - /** - * 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> { - const targetType = route.queryParamMap.get('targetType'); - const policyTargetId = route.queryParamMap.get('policyTargetId'); + const resourceType: ResourceType = new ResourceType(targetType); + const lazyProvider$: Observable> = lazyService(dataServiceMap[resourceType.value], parentInjector); - if (isEmpty(targetType) || isEmpty(policyTargetId)) { - this.router.navigateByUrl('/404', { skipLocationChange: true }); - } - - const resourceType: ResourceType = new ResourceType(targetType); - const lazyProvider$: Observable> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); - - return lazyProvider$.pipe( - switchMap((dataService: IdentifiableDataService) => { - return dataService.findById(policyTargetId); - }), - getFirstCompletedRemoteData(), - ); - } -} + return lazyProvider$.pipe( + switchMap((dataService: IdentifiableDataService) => { + return dataService.findById(policyTargetId); + }), + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts index e72b061956..225c8383ec 100644 --- a/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts +++ b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -14,30 +15,27 @@ import { isEmpty } from '../../empty.util'; import { followLink } from '../../utils/follow-link-config.model'; /** - * This class represents a resolver that requests a specific item before the route is activated + * 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 + * @param {Router} router + * @param {ResourcePolicyDataService} resourcePolicyService + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ResourcePolicyResolver { +export const ResourcePolicyResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + resourcePolicyService: ResourcePolicyDataService = inject(ResourcePolicyDataService), +): Observable> => { + const policyId = route.queryParamMap.get('policyId'); - constructor(private resourcePolicyService: ResourcePolicyDataService, private router: Router) { + if (isEmpty(policyId)) { + router.navigateByUrl('/404', { skipLocationChange: true }); } - /** - * 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> { - const policyId = route.queryParamMap.get('policyId'); - - if (isEmpty(policyId)) { - this.router.navigateByUrl('/404', { skipLocationChange: true }); - } - - return this.resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe( - getFirstCompletedRemoteData(), - ); - } -} + return resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index 3d6d0f7b9d..375059a48d 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,23 +13,19 @@ import { SuggestionTargetDataService } from '../core/notifications/target/sugges import { hasValue } from '../shared/empty.util'; /** - * This class represents a resolver that requests a specific collection before the route is activated + * Method for resolving a suggestion target based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {SuggestionTargetDataService} suggestionsDataService + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class SuggestionsPageResolver { - constructor(private suggestionsDataService: SuggestionTargetDataService) { - } - - /** - * Method for resolving a suggestion target 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 collection based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( - find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), - ); - } -} +export const SuggestionsPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), +): Observable> => { + return suggestionsDataService.getTargetById(route.params.targetId).pipe( + find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + ); +}; 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 5bf6aec2ff..c4bdafd255 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 @@ -6,7 +6,7 @@ import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; describe('ItemFromWorkflowResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkflowResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkflowResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkflowResolver(wfiService, null); + resolver = ItemFromWorkflowResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { 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 79756f0f7a..9ed5f8f991 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts @@ -1,20 +1,21 @@ -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -/** - * This class represents a resolver that requests a specific item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemFromWorkflowResolver extends SubmissionObjectResolver { - constructor( - private workflowItemService: WorkflowItemDataService, - protected store: Store, - ) { - super(workflowItemService, store); - } +export const ItemFromWorkflowResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workflowItemService); +}; -} diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts index 21f9e6e00a..efb09505ee 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -6,7 +6,7 @@ import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; describe('WorkflowItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkflowItemPageResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkflowItemPageResolver(wfiService); + resolver = WorkflowItemPageResolver; }); it('should resolve a workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts index aa654bfaa5..6f0b7b986b 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,28 +12,17 @@ import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; -/** - * This class represents a resolver that requests a specific workflow item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class WorkflowItemPageResolver { - constructor(private workflowItemService: WorkflowItemDataService) { - } - - /** - * Method for resolving a workflow 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 workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workflowItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const WorkflowItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return workflowItemService.findById( + route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; 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 77232762c3..90eca80c64 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 @@ -6,7 +6,7 @@ import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; describe('ItemFromWorkspaceResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkspaceResolver; + let resolver: any; let wfiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkspaceResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkspaceResolver(wfiService, null); + resolver = ItemFromWorkspaceResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { 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 0bd5193d19..78a8299afd 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -1,20 +1,23 @@ -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; /** - * This class represents a resolver that requests a specific item before the route is activated + * This method represents a resolver that requests a specific item before the route is activated */ -@Injectable({ providedIn: 'root' }) -export class ItemFromWorkspaceResolver extends SubmissionObjectResolver { - constructor( - private workspaceItemService: WorkspaceitemDataService, - protected store: Store, - ) { - super(workspaceItemService, store); - } - -} +export const ItemFromWorkspaceResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workspaceItemService); +}; diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts index 0502da186b..c2d902c165 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts @@ -6,7 +6,7 @@ import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; describe('WorkflowItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkspaceItemPageResolver; + let resolver: any; let wsiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wsiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkspaceItemPageResolver(wsiService); + resolver = WorkspaceItemPageResolver; }); it('should resolve a workspace item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wsiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts index cb0772d475..f8c2d9cc3c 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts @@ -1,38 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; /** - * This class represents a resolver that requests a specific workflow item before the route is activated + * Method for resolving a workflow item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {WorkspaceitemDataService} workspaceItemService + * @returns Observable<> Emits the found workflow item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class WorkspaceItemPageResolver { - constructor(private workspaceItemService: WorkspaceitemDataService) { - } - - /** - * Method for resolving a workflow 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 workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workspaceItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const WorkspaceItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return workspaceItemService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 703da3734c..2de471f20e 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -3,7 +3,6 @@ import { makeStateKey, Type, } from '@angular/core'; -import { DynamicFormControl } from '@ng-dynamic-forms/core/lib/component/dynamic-form-control-interface'; import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model'; import { HALDataService } from '../app/core/data/base/hal-data-service.interface'; @@ -78,11 +77,8 @@ export interface LazyDataServicesMap { [type: string]: () => Promise>> } -export type DynamicFormControlFn = (model: string) => Type; export const APP_DATA_SERVICES_MAP: InjectionToken = new InjectionToken('APP_DATA_SERVICES_MAP'); -export const APP_DYNAMIC_FORM_CONTROL_FN: InjectionToken = new InjectionToken('APP_DYNAMIC_FORM_CONTROL_FN'); - export { APP_CONFIG, APP_CONFIG_STATE,