[DURACOM-234] Migrate fo functional resolver

This commit is contained in:
Giuseppe Digilio
2024-03-28 23:34:37 +01:00
parent 549609e2ea
commit 1f4aca800b
59 changed files with 2057 additions and 2144 deletions

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -11,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; 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<<RemoteData<BitstreamFormat>> Emits the found bitstreamFormat based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const BitstreamFormatsResolver: ResolveFn<RemoteData<BitstreamFormat>> = (
export class BitstreamFormatsResolver { route: ActivatedRouteSnapshot,
constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { state: RouterStateSnapshot,
} bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService),
): Observable<RemoteData<BitstreamFormat>> => {
/** return bitstreamFormatDataService.findById(route.params.id)
* Method for resolving an bitstreamFormat based on the parameters in the current route .pipe(
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot getFirstCompletedRemoteData(),
* @param {RouterStateSnapshot} state The current RouterStateSnapshot );
* @returns Observable<<RemoteData<BitstreamFormat>> 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<RemoteData<BitstreamFormat>> {
return this.bitstreamFormatDataService.findById(route.params.id)
.pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -24,32 +25,20 @@ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
]; ];
/** /**
* 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<<RemoteData<Item>> Emits the found bitstream based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const BitstreamPageResolver: ResolveFn<RemoteData<Bitstream>> = (
export class BitstreamPageResolver { route: ActivatedRouteSnapshot,
constructor(private bitstreamService: BitstreamDataService) { state: RouterStateSnapshot,
} bitstreamService: BitstreamDataService = inject(BitstreamDataService),
): Observable<RemoteData<Bitstream>> => {
/** return bitstreamService.findById(route.params.id, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW)
* Method for resolving a bitstream based on the parameters in the current route .pipe(
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot getFirstCompletedRemoteData(),
* @param {RouterStateSnapshot} state The current RouterStateSnapshot );
* @returns Observable<<RemoteData<Item>> 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<RemoteData<Bitstream>> {
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<Bitstream>[] {
return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -7,7 +7,7 @@ import { RequestEntryState } from '../core/data/request-entry-state.model';
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
describe(`LegacyBitstreamUrlResolver`, () => { describe(`LegacyBitstreamUrlResolver`, () => {
let resolver: LegacyBitstreamUrlResolver; let resolver: any;
let bitstreamDataService: BitstreamDataService; let bitstreamDataService: BitstreamDataService;
let testScheduler; let testScheduler;
let remoteDataMocks; let remoteDataMocks;
@@ -33,7 +33,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
bitstreamDataService = { bitstreamDataService = {
findByItemHandle: () => undefined, findByItemHandle: () => undefined,
} as any; } as any;
resolver = new LegacyBitstreamUrlResolver(bitstreamDataService); resolver = LegacyBitstreamUrlResolver;
}); });
describe(`resolve`, () => { describe(`resolve`, () => {
@@ -51,7 +51,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
}); });
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
route.params.sequence_id, 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`, () => { it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
route.queryParams.sequenceId, route.queryParams.sequenceId,
@@ -100,7 +100,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
}); });
it(`should call findByItemHandle with the handle, and filename from the route`, () => { it(`should call findByItemHandle with the handle, and filename from the route`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
undefined, undefined,
@@ -123,7 +123,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Error, c: remoteDataMocks.Error,
}; };
expectObservable(resolver.resolve(route, state)).toBe(expected, values); expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
}); });
}); });
it(`...succeeded`, () => { it(`...succeeded`, () => {
@@ -138,7 +138,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Success, c: remoteDataMocks.Success,
}; };
expectObservable(resolver.resolve(route, state)).toBe(expected, values); expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
}); });
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -12,41 +13,34 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { hasNoValue } from '../shared/empty.util'; 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<<RemoteData<Item>> Emits the found bitstream based on the parameters in
* current route, or an error if something went wrong
*/ */
@Injectable({ export const LegacyBitstreamUrlResolver: ResolveFn<RemoteData<Bitstream>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class LegacyBitstreamUrlResolver { bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
constructor(protected bitstreamDataService: BitstreamDataService) { ): Observable<RemoteData<Bitstream>> => {
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 bitstreamDataService.findByItemHandle(
* Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the `${prefix}/${suffix}`,
* bitstream sequenceId,
* filename,
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot ).pipe(
* @param {RouterStateSnapshot} state The current RouterStateSnapshot getFirstCompletedRemoteData(),
* @returns Observable<<RemoteData<Item>> Emits the found bitstream based on the parameters in );
* current route, or an error if something went wrong };
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<RemoteData<Bitstream>> {
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(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -19,30 +20,28 @@ import {
import { hasValue } from '../shared/empty.util'; 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 const BrowseByDSOBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community | Collection>> = (
export class BrowseByDSOBreadcrumbResolver { route: ActivatedRouteSnapshot,
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) { state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: DSpaceObjectDataService = inject(DSpaceObjectDataService),
): Observable<BreadcrumbConfig<Community | Collection>> => {
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) };
}),
);
} }
return undefined;
/** };
* 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<BreadcrumbConfig<Community | Collection>> {
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;
}
}

View File

@@ -1,32 +1,23 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; 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 * Method for resolving a browse-by i18n breadcrumb configuration object
* It adds the metadata field of the current browse-by page * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object for a browse-by page
*/ */
@Injectable({ providedIn: 'root' }) export const BrowseByI18nBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver { route: ActivatedRouteSnapshot,
constructor(protected breadcrumbService: I18nBreadcrumbsService) { state: RouterStateSnapshot,
super(breadcrumbService); ): BreadcrumbConfig<string> => {
} const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
/** return I18nBreadcrumbResolver(route, state) as BreadcrumbConfig<string>;
* 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<string> {
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
return super.resolve(route, state);
}
}

View File

@@ -1,3 +1,4 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
@@ -5,7 +6,7 @@ import { CollectionPageResolver } from './collection-page.resolver';
describe('CollectionPageResolver', () => { describe('CollectionPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: CollectionPageResolver; let resolver: any;
let collectionService: any; let collectionService: any;
let store: any; let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -17,12 +18,11 @@ describe('CollectionPageResolver', () => {
store = jasmine.createSpyObj('store', { store = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
}); });
resolver = new CollectionPageResolver(collectionService, store); resolver = CollectionPageResolver;
}); });
it('should resolve a collection with the correct id', (done) => { it('should resolve a collection 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, collectionService, store) as Observable<any>).pipe(first())
.pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {
expect(resolved.payload.id).toEqual(uuid); expect(resolved.payload.id).toEqual(uuid);

View File

@@ -1,11 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions'; import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -28,37 +30,32 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
]; ];
/** /**
* 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<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const CollectionPageResolver: ResolveFn<RemoteData<Collection>> = (
export class CollectionPageResolver { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private collectionService: CollectionDataService, collectionService: CollectionDataService = inject(CollectionDataService),
private store: Store<any>, store: Store<AppState> = inject(Store<AppState>),
) { ): Observable<RemoteData<Collection>> => {
} const collectionRD$ = collectionService.findById(
route.params.id,
true,
false,
...COLLECTION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
* Method for resolving a collection based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Collection>> 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<RemoteData<Collection>> {
const collectionRD$ = this.collectionService.findById(
route.params.id,
true,
false,
...COLLECTION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => { return collectionRD$;
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); };
});
return collectionRD$;
}
}

View File

@@ -1,27 +1,24 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; 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 { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ItemTemplatePageResolver } from './item-template-page.resolver'; import { ItemTemplatePageResolver } from './item-template-page.resolver';
describe('ItemTemplatePageResolver', () => { describe('ItemTemplatePageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: ItemTemplatePageResolver; let resolver: any;
let itemTemplateService: any; let itemTemplateService: any;
let dsoNameService: DSONameServiceMock;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
beforeEach(() => { beforeEach(() => {
itemTemplateService = { itemTemplateService = {
findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }), findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }),
}; };
dsoNameService = new DSONameServiceMock(); resolver = ItemTemplatePageResolver;
resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService);
}); });
it('should resolve an item template with the correct id', (done) => { 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<any>)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,38 +1,23 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
/** export const ItemTemplatePageResolver: ResolveFn<RemoteData<Item>> = (
* This class represents a resolver that requests a specific collection's item template before the route is activated route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService),
export class ItemTemplatePageResolver { ): Observable<RemoteData<Item>> => {
constructor( return itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
public dsoNameService: DSONameService, getFirstCompletedRemoteData(),
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<<RemoteData<Collection>> 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<RemoteData<Item>> {
return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,3 +1,4 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
@@ -5,7 +6,7 @@ import { CommunityPageResolver } from './community-page.resolver';
describe('CommunityPageResolver', () => { describe('CommunityPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: CommunityPageResolver; let resolver: any;
let communityService: any; let communityService: any;
let store: any; let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => {
store = jasmine.createSpyObj('store', { store = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
}); });
resolver = new CommunityPageResolver(communityService, store); resolver = CommunityPageResolver;
}); });
it('should resolve a community with the correct id', (done) => { 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<any>)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,11 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions'; import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -28,37 +30,32 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Community>[] = [
]; ];
/** /**
* 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<<RemoteData<Community>> Emits the found community based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const CommunityPageResolver: ResolveFn<RemoteData<Community>> = (
export class CommunityPageResolver { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private communityService: CommunityDataService, communityService: CommunityDataService = inject(CommunityDataService),
private store: Store<any>, store: Store<AppState> = inject(Store<AppState>),
) { ): Observable<RemoteData<Community>> => {
} const communityRD$ = communityService.findById(
route.params.id,
true,
false,
...COMMUNITY_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** communityRD$.subscribe((communityRD: RemoteData<Community>) => {
* Method for resolving a community based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, communityRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Community>> 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<RemoteData<Community>> {
const communityRD$ = this.communityService.findById(
route.params.id,
true,
false,
...COMMUNITY_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
communityRD$.subscribe((communityRD: RemoteData<Community>) => { return communityRD$;
this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); };
});
return communityRD$;
}
}

View File

@@ -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 { 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BitstreamDataService } from '../data/bitstream-data.service'; import { BitstreamDataService } from '../data/bitstream-data.service';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; 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({ export const BitstreamBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Bitstream>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver<Bitstream> { breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService),
constructor( dataService: BitstreamDataService = inject(BitstreamDataService),
protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { ): Observable<BreadcrumbConfig<Bitstream>> => {
super(breadcrumbService, dataService); const linksToFollow: FollowLinkConfig<DSpaceObject>[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
} return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Bitstream>>;
};
/**
* 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<Bitstream>[] {
return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -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 { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CollectionDataService } from '../data/collection-data.service'; import { CollectionDataService } from '../data/collection-data.service';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const CollectionBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Collection>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { dataService: CollectionDataService = inject(CollectionDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Collection>> => {
} const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Collection>>;
};
/**
* 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<Collection>[] {
return COLLECTION_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -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 { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CommunityDataService } from '../data/community-data.service'; import { CommunityDataService } from '../data/community-data.service';
import { Community } from '../shared/community.model'; import { Community } from '../shared/community.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const CommunityBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { dataService: CommunityDataService = inject(CommunityDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Community>> => {
} const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
/** route,
* Method that returns the follow links to already resolve state,
* The self links defined in this list are expected to be requested somewhere in the near future breadcrumbService,
* Requesting them as embeds will limit the number of requests dataService,
*/ ...linksToFollow,
get followLinks(): FollowLinkConfig<Community>[] { ) as Observable<BreadcrumbConfig<Community>>;
return COMMUNITY_PAGE_LINKS_TO_FOLLOW; };
}
}

View File

@@ -3,11 +3,10 @@ import { getTestScheduler } from 'jasmine-marbles';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
describe('DSOBreadcrumbResolver', () => { describe('DSOBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: DSOBreadcrumbResolver<Collection>; let resolver: any;
let collectionService: any; let collectionService: any;
let dsoBreadcrumbService: any; let dsoBreadcrumbService: any;
let testCollection: Collection; let testCollection: Collection;
@@ -17,18 +16,18 @@ describe('DSOBreadcrumbResolver', () => {
beforeEach(() => { beforeEach(() => {
uuid = '1234-65487-12354-1235'; uuid = '1234-65487-12354-1235';
breadcrumbUrl = '/collections/' + uuid; breadcrumbUrl = `/collections/${uuid}`;
currentUrl = breadcrumbUrl + '/edit'; currentUrl = `${breadcrumbUrl}/edit`;
testCollection = Object.assign(new Collection(), { uuid }); testCollection = Object.assign(new Collection(), { uuid });
dsoBreadcrumbService = {}; dsoBreadcrumbService = {};
collectionService = { 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', () => { 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 }; const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl };
getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig });
}); });

View File

@@ -1,4 +1,3 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
RouterStateSnapshot, RouterStateSnapshot,
@@ -10,7 +9,6 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { ChildHALResource } from '../shared/child-hal-resource.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
@@ -19,45 +17,33 @@ import {
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService<DSpaceObject>, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]) => Observable<BreadcrumbConfig<DSpaceObject>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> { breadcrumbService: DSOBreadcrumbsService,
protected constructor( dataService: IdentifiableDataService<DSpaceObject>,
protected breadcrumbService: DSOBreadcrumbsService, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]
protected dataService: IdentifiableDataService<T>, ): Observable<BreadcrumbConfig<DSpaceObject>> => {
) { const uuid = route.params.id;
} return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
getFirstCompletedRemoteData(),
/** getRemoteDataPayload(),
* Method for resolving a breadcrumb config object map((object: DSpaceObject) => {
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot if (hasValue(object)) {
* @param {RouterStateSnapshot} state The current RouterStateSnapshot const fullPath = state.url;
* @returns BreadcrumbConfig object const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid);
*/ return { provider: breadcrumbService, key: object, url: url };
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> { } else {
const uuid = route.params.id; return undefined;
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<T>[];
}

View File

@@ -3,7 +3,7 @@ import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
describe('I18nBreadcrumbResolver', () => { describe('I18nBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: I18nBreadcrumbResolver; let resolver: any;
let i18nBreadcrumbService: any; let i18nBreadcrumbService: any;
let i18nKey: string; let i18nKey: string;
let route: any; let route: any;
@@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => {
}; };
expectedPath = new URLCombiner(parentSegment, segment).toString(); expectedPath = new URLCombiner(parentSegment, segment).toString();
i18nBreadcrumbService = {}; i18nBreadcrumbService = {};
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); resolver = I18nBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });
it('should resolve throw an error when no breadcrumbKey is defined', () => { it('should resolve throw an error when no breadcrumbKey is defined', () => {
expect(() => { expect(() => {
resolver.resolve({ data: {} } as any, undefined); resolver({ data: {} } as any, undefined, i18nBreadcrumbService);
}).toThrow(); }).toThrow();
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -10,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; 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({ export const I18nBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class I18nBreadcrumbResolver { breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService),
constructor(protected breadcrumbService: I18nBreadcrumbsService) { ): BreadcrumbConfig<string> => {
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: breadcrumbService, key: key, url: fullPath };
* 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<string> {
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 };
}
}

View File

@@ -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 { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { ItemDataService } from '../data/item-data.service'; import { ItemDataService } from '../data/item-data.service';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const ItemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { dataService: ItemDataService = inject(ItemDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Item>> => {
} const linksToFollow: FollowLinkConfig<DSpaceObject>[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
/** route,
* Method that returns the follow links to already resolve state,
* The self links defined in this list are expected to be requested somewhere in the near future breadcrumbService,
* Requesting them as embeds will limit the number of requests dataService,
*/ ...linksToFollow,
get followLinks(): FollowLinkConfig<Item>[] { ) as Observable<BreadcrumbConfig<Item>>;
return ITEM_PAGE_LINKS_TO_FOLLOW; };
}
}

View File

@@ -2,7 +2,7 @@ import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver';
describe('NavigationBreadcrumbResolver', () => { describe('NavigationBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: NavigationBreadcrumbResolver; let resolver: any;
let NavigationBreadcrumbService: any; let NavigationBreadcrumbService: any;
let i18nKey: string; let i18nKey: string;
let relatedI18nKey: string; let relatedI18nKey: string;
@@ -40,11 +40,11 @@ describe('NavigationBreadcrumbResolver', () => {
}; };
expectedPath = '/base/example:/base'; expectedPath = '/base/example:/base';
NavigationBreadcrumbService = {}; NavigationBreadcrumbService = {};
resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); resolver = NavigationBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -8,49 +9,44 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; 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({ export const NavigationBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class NavigationBreadcrumbResolver { breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService),
): BreadcrumbConfig<string> => {
private parentRoutes: ActivatedRouteSnapshot[] = []; const parentRoutes: ActivatedRouteSnapshot[] = [];
constructor(protected breadcrumbService: NavigationBreadcrumbsService) { 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));
* Method to collect all parent routes snapshot from current route snapshot const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path;
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length);
*/
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<string> {
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);
const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => {
return `${previous}:${current.data.breadcrumbKey}`; return `${previous}:${current.data.breadcrumbKey}`;
}, route.data.breadcrumbKey); }, route.data.breadcrumbKey);
const combinedUrls = relatedParentRoutes.reduce((previous, current) => { const combinedUrls = relatedParentRoutes.reduce((previous, current) => {
return `${previous}:${baseUrl}${current.path}`; return `${previous}:${baseUrl}${current.path}`;
}, state.url); }, 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);
} }
} }

View File

@@ -2,7 +2,7 @@ import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcru
describe('PublicationClaimBreadcrumbResolver', () => { describe('PublicationClaimBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: PublicationClaimBreadcrumbResolver; let resolver: any;
let publicationClaimBreadcrumbService: any; let publicationClaimBreadcrumbService: any;
const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a';
@@ -19,11 +19,11 @@ describe('PublicationClaimBreadcrumbResolver', () => {
}, },
}; };
publicationClaimBreadcrumbService = {}; publicationClaimBreadcrumbService = {};
resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); resolver = PublicationClaimBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,28 +1,18 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
@Injectable({ export const PublicationClaimBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class PublicationClaimBreadcrumbResolver { breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService),
constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { ): BreadcrumbConfig<string> => {
} const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: breadcrumbService, key: targetId };
/** };
* 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<string> {
const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: this.breadcrumbService, key: targetId };
}
}

View File

@@ -2,7 +2,7 @@ import { QualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcru
describe('QualityAssuranceBreadcrumbResolver', () => { describe('QualityAssuranceBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: QualityAssuranceBreadcrumbResolver; let resolver: any;
let qualityAssuranceBreadcrumbService: any; let qualityAssuranceBreadcrumbService: any;
let route: any; let route: any;
const fullPath = '/test/quality-assurance/'; const fullPath = '/test/quality-assurance/';
@@ -19,11 +19,11 @@ describe('QualityAssuranceBreadcrumbResolver', () => {
}, },
}; };
qualityAssuranceBreadcrumbService = {}; qualityAssuranceBreadcrumbService = {};
resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); resolver = QualityAssuranceBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,36 +1,27 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service';
@Injectable({ export const QualityAssuranceBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class QualityAssuranceBreadcrumbResolver { breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService),
constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} ): BreadcrumbConfig<string> => {
const sourceId = route.paramMap.get('sourceId');
const topicId = route.paramMap.get('topicId');
let key = sourceId;
/** if (topicId) {
* Method that resolve QA item into a breadcrumb key += `:${topicId}`;
* 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<string> {
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 };
} }
} const fullPath = state.url;
const url = fullPath.substring(0, fullPath.indexOf(sourceId));
return { provider: breadcrumbService, key, url };
};

View File

@@ -1,45 +1,37 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { Item } from '../../shared/item.model';
import { getFirstCompletedRemoteData } from '../../shared/operators'; 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<SubmissionObject> } dataService
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, dataService: IdentifiableDataService<SubmissionObject>) => Observable<RemoteData<Item>> = (
export class SubmissionObjectResolver<T> { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
protected dataService: IdentifiableDataService<any>, dataService: IdentifiableDataService<SubmissionObject>,
protected store: Store<any>, ): Observable<RemoteData<Item>> => {
) { return dataService.findById(route.params.id,
} true,
false,
/** followLink('item'),
* Method for resolving an item based on the parameters in the current route ).pipe(
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot getFirstCompletedRemoteData(),
* @param {RouterStateSnapshot} state The current RouterStateSnapshot switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route, getFirstCompletedRemoteData(),
* or an error if something went wrong );
*/ };
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<T>> {
const itemRD$ = this.dataService.findById(route.params.id,
true,
false,
followLink('item'),
).pipe(
getFirstCompletedRemoteData(),
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<T>>),
getFirstCompletedRemoteData(),
);
return itemRD$;
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -9,21 +10,10 @@ import { take } from 'rxjs/operators';
import { SiteDataService } from '../core/data/site-data.service'; import { SiteDataService } from '../core/data/site-data.service';
import { Site } from '../core/shared/site.model'; import { Site } from '../core/shared/site.model';
/** export const HomePageResolver: ResolveFn<Site> = (
* The class that resolve the Site object for a route route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) siteService: SiteDataService = inject(SiteDataService),
export class HomePageResolver { ): Observable<Site> => {
constructor(private siteService: SiteDataService) { return siteService.find().pipe(take(1));
} };
/**
* Method for resolving a site object
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Site> | Promise<Site> | Site {
return this.siteService.find().pipe(take(1));
}
}

View File

@@ -19,7 +19,7 @@ describe('ItemPageResolver', () => {
}); });
describe('resolve', () => { describe('resolve', () => {
let resolver: ItemPageResolver; let resolver: any;
let itemService: any; let itemService: any;
let store: any; let store: any;
let router: any; let router: any;
@@ -42,15 +42,19 @@ describe('ItemPageResolver', () => {
store = jasmine.createSpyObj('store', { store = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
}); });
resolver = new ItemPageResolver(itemService, store, router); resolver = ItemPageResolver;
}); });
it('should redirect to the correct route for the entity type', (done) => { it('should redirect to the correct route for the entity type', (done) => {
spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(item, 'firstMetadataValue').and.returnValue(entityType);
spyOn(router, 'navigateByUrl').and.callThrough(); spyOn(router, 'navigateByUrl').and.callThrough();
resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/items/${uuid}`).toString() } as any) resolver({ params: { id: uuid } } as any,
.pipe(first()) { url: router.parseUrl(`/items/${uuid}`).toString() } as any,
router,
itemService,
store,
).pipe(first())
.subscribe( .subscribe(
() => { () => {
expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl(`/entities/${entityType}/${uuid}`).toString()); expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl(`/entities/${entityType}/${uuid}`).toString());
@@ -63,8 +67,13 @@ describe('ItemPageResolver', () => {
spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(item, 'firstMetadataValue').and.returnValue(entityType);
spyOn(router, 'navigateByUrl').and.callThrough(); spyOn(router, 'navigateByUrl').and.callThrough();
resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any) resolver(
.pipe(first()) { params: { id: uuid } } as any,
{ url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any,
router,
itemService,
store,
).pipe(first())
.subscribe( .subscribe(
() => { () => {
expect(router.navigateByUrl).not.toHaveBeenCalled(); expect(router.navigateByUrl).not.toHaveBeenCalled();

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -8,54 +9,64 @@ import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppState } from '../app.reducer';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { hasValue } from '../shared/empty.util'; 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'; 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 * Method for resolving an item based on the parameters in the current route
* entity page * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @param {Router} router
* @param {ItemDataService} itemService
* @param {Store<AppState>} store
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const ItemPageResolver: ResolveFn<RemoteData<Item>> = (
export class ItemPageResolver extends ItemResolver { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
protected itemService: ItemDataService, router: Router = inject(Router),
protected store: Store<any>, itemService: ItemDataService = inject(ItemDataService),
protected router: Router, store: Store<AppState> = inject(Store<AppState>),
) { ): Observable<RemoteData<Item>> => {
super(itemService, store, router); const itemRD$ = itemService.findById(
} route.params.id,
true,
false,
...ITEM_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** itemRD$.subscribe((itemRD: RemoteData<Item>) => {
* Method for resolving an item based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, itemRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> 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<RemoteData<Item>> {
return super.resolve(route, state).pipe(
map((rd: RemoteData<Item>) => {
if (rd.hasSucceeded && hasValue(rd.payload)) {
const thisRoute = state.url;
// Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas return itemRD$.pipe(
// or semicolons) and thisRoute has been encoded with that function. If we want to compare map((rd: RemoteData<Item>) => {
// it with itemRoute, we have to run itemRoute through Angular's version as well to ensure if (rd.hasSucceeded && hasValue(rd.payload)) {
// the same characters are encoded the same way. const thisRoute = state.url;
const itemRoute = this.router.parseUrl(getItemPageRoute(rd.payload)).toString();
if (!thisRoute.startsWith(itemRoute)) { // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas
const itemId = rd.payload.uuid; // or semicolons) and thisRoute has been encoded with that function. If we want to compare
const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure
this.router.navigateByUrl(itemRoute + subRoute); // 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;
); }),
} );
} };

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Router, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions'; import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -31,38 +32,24 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
followLink('thumbnail'), followLink('thumbnail'),
]; ];
/** export const ItemResolver: ResolveFn<RemoteData<Item>> = (
* This class represents a resolver that requests a specific item before the route is activated route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) itemService: ItemDataService = inject(ItemDataService),
export class ItemResolver { store: Store<AppState> = inject(Store<AppState>),
constructor( ): Observable<RemoteData<Item>> => {
protected itemService: ItemDataService, const itemRD$ = itemService.findById(
protected store: Store<any>, route.params.id,
protected router: Router, true,
) { false,
} ...ITEM_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** itemRD$.subscribe((itemRD: RemoteData<Item>) => {
* Method for resolving an item based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, itemRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> 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<RemoteData<Item>> {
const itemRD$ = this.itemService.findById(route.params.id,
true,
false,
...ITEM_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
itemRD$.subscribe((itemRD: RemoteData<Item>) => { return itemRD$;
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); };
});
return itemRD$;
}
}

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Router, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../../app.reducer';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { VersionDataService } from '../../core/data/version-data.service'; import { VersionDataService } from '../../core/data/version-data.service';
import { ResolvedAction } from '../../core/resolving/resolver.actions'; import { ResolvedAction } from '../../core/resolving/resolver.actions';
@@ -26,37 +27,31 @@ export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Version>[] = [
]; ];
/** /**
* 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<AppState>} store
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const VersionResolver: ResolveFn<RemoteData<Version>> = (
export class VersionResolver { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
protected versionService: VersionDataService, versionService: VersionDataService = inject(VersionDataService),
protected store: Store<any>, store: Store<AppState> = inject(Store<AppState>),
protected router: Router, ): Observable<RemoteData<Version>> => {
) { const versionRD$ = versionService.findById(route.params.id,
} true,
false,
...VERSION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** versionRD$.subscribe((versionRD: RemoteData<Version>) => {
* Method for resolving a version based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, versionRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> 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<RemoteData<Version>> {
const versionRD$ = this.versionService.findById(route.params.id,
true,
false,
...VERSION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
versionRD$.subscribe((versionRD: RemoteData<Version>) => { return versionRD$;
this.store.dispatch(new ResolvedAction(state.url, versionRD.payload)); };
});
return versionRD$;
}
}

View File

@@ -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<boolean> {
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<boolean> {
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<boolean> {
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<PaginatedList<BrowseDefinition>>())
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
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<ConfigurationProperty>) => 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,
})));
});
}
}

View File

@@ -19,7 +19,6 @@ import { ConfigurationDataService } from './core/data/configuration-data.service
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { FeatureID } from './core/data/feature-authorization/feature-id'; import { FeatureID } from './core/data/feature-authorization/feature-id';
import { ScriptDataService } from './core/data/processes/script-data.service'; import { ScriptDataService } from './core/data/processes/script-data.service';
import { MenuResolver } from './menu.resolver';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/menu-id.model'; import { MenuID } from './shared/menu/menu-id.model';
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils'; 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 { MenuServiceStub } from './shared/testing/menu-service.stub';
import { createPaginatedList } from './shared/testing/utils.test'; import { createPaginatedList } from './shared/testing/utils.test';
import createSpy = jasmine.createSpy; import createSpy = jasmine.createSpy;
import { MenuResolverService } from './menu-resolver.service';
const BOOLEAN = { t: true, f: false }; const BOOLEAN = { t: true, f: false };
const MENU_STATE = { const MENU_STATE = {
@@ -39,7 +39,7 @@ const BROWSE_DEFINITIONS = [
]; ];
describe('MenuResolver', () => { describe('MenuResolver', () => {
let resolver: MenuResolver; let resolver: MenuResolverService;
let menuService; let menuService;
let browseService; let browseService;
@@ -79,12 +79,12 @@ describe('MenuResolver', () => {
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ScriptDataService, useValue: scriptService }, { provide: ScriptDataService, useValue: scriptService },
{ provide: ConfigurationDataService, useValue: configurationDataService }, { provide: ConfigurationDataService, useValue: configurationDataService },
{ { provide: NgbModal, useValue: mockNgbModal },
provide: NgbModal, useValue: mockNgbModal }, MenuResolverService,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}); });
resolver = TestBed.inject(MenuResolver); resolver = TestBed.inject(MenuResolverService);
})); }));
it('should be created', () => { it('should be created', () => {

View File

@@ -1,846 +1,21 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Observable } from 'rxjs';
import {
combineLatest, import { MenuResolverService } from './menu-resolver.service';
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 * Initialize all menus
*/ */
@Injectable({ export const MenuResolver: ResolveFn<boolean> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class MenuResolver { menuResolverService: MenuResolverService = inject(MenuResolverService),
constructor( ): Observable<boolean> => {
protected menuService: MenuService, return menuResolverService.resolve(route, state);
protected browseService: BrowseService, };
protected authorizationService: AuthorizationDataService,
protected modalService: NgbModal,
protected scriptDataService: ScriptDataService,
protected configurationDataService: ConfigurationDataService,
) {
}
/**
* Initialize all menus
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
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<boolean> {
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<boolean> {
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<PaginatedList<BrowseDefinition>>())
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
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<ConfigurationProperty>) => 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,
})));
});
}
}

View File

@@ -5,7 +5,7 @@ import { Process } from './processes/process.model';
describe('ProcessBreadcrumbResolver', () => { describe('ProcessBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: ProcessBreadcrumbResolver; let resolver: any;
let processDataService: ProcessDataService; let processDataService: ProcessDataService;
let processBreadcrumbService: any; let processBreadcrumbService: any;
let process: Process; let process: Process;
@@ -19,11 +19,16 @@ describe('ProcessBreadcrumbResolver', () => {
processDataService = { processDataService = {
findById: () => createSuccessfulRemoteDataObject$(process), findById: () => createSuccessfulRemoteDataObject$(process),
} as any; } as any;
resolver = new ProcessBreadcrumbResolver(processBreadcrumbService, processDataService); resolver = ProcessBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', (done) => { 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 }; const expectedConfig = { provider: processBreadcrumbService, key: process, url: path };
resolvedConfig.subscribe((config) => { resolvedConfig.subscribe((config) => {
expect(config).toEqual(expectedConfig); expect(config).toEqual(expectedConfig);
@@ -33,7 +38,7 @@ describe('ProcessBreadcrumbResolver', () => {
it('should resolve throw an error when no breadcrumbKey is defined', () => { it('should resolve throw an error when no breadcrumbKey is defined', () => {
expect(() => { expect(() => {
resolver.resolve({ data: {} } as any, undefined); resolver({ data: {} } as any, undefined, processBreadcrumbService, processDataService);
}).toThrow(); }).toThrow();
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -15,30 +16,28 @@ import { ProcessBreadcrumbsService } from './process-breadcrumbs.service';
import { Process } from './processes/process.model'; 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<<RemoteData<Process>> Emits the found process based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const ProcessBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Process>> = (
export class ProcessBreadcrumbResolver { route: ActivatedRouteSnapshot,
constructor(protected breadcrumbService: ProcessBreadcrumbsService, private processService: ProcessDataService) { state: RouterStateSnapshot,
} breadcrumbService: ProcessBreadcrumbsService = inject(ProcessBreadcrumbsService),
processService: ProcessDataService = inject(ProcessDataService),
): Observable<BreadcrumbConfig<Process>> => {
const id = route.params.id;
/** return processService.findById(route.params.id, true, false, followLink('script')).pipe(
* Method for resolving a process based on the parameters in the current route getFirstCompletedRemoteData(),
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot map((object: RemoteData<Process>) => {
* @param {RouterStateSnapshot} state The current RouterStateSnapshot const fullPath = state.url;
* @returns Observable<<RemoteData<Process>> Emits the found process based on the parameters in the current route, const url = fullPath.substring(0, fullPath.indexOf(id)).concat(id);
* or an error if something went wrong return { provider: breadcrumbService, key: object.payload, url: url };
*/ }),
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Process>> { );
const id = route.params.id; };
return this.processService.findById(route.params.id, true, false, followLink('script')).pipe(
getFirstCompletedRemoteData(),
map((object: RemoteData<Process>) => {
const fullPath = state.url;
const url = fullPath.substr(0, fullPath.indexOf(id)) + id;
return { provider: this.breadcrumbService, key: object.payload, url: url };
}),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; 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<<RemoteData<Process>> Emits the found process based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const ProcessPageResolver: ResolveFn<RemoteData<Process>> = (
export class ProcessPageResolver { route: ActivatedRouteSnapshot,
constructor(private processService: ProcessDataService) { state: RouterStateSnapshot,
} processService: ProcessDataService = inject(ProcessDataService),
): Observable<RemoteData<Process>> => {
/** return processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe(
* Method for resolving a process based on the parameters in the current route getFirstCompletedRemoteData(),
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot );
* @param {RouterStateSnapshot} state The current RouterStateSnapshot };
* @returns Observable<<RemoteData<Process>> 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<RemoteData<Process>> {
return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } 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 const ItemResolver: ResolveFn<AssuranceEventsPageParams> = (
export class QualityAssuranceEventsPageResolver { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
/** ): AssuranceEventsPageParams => {
* Method for resolving the parameters in the current route. return {
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot pageId: route.queryParams.pageId,
* @param {RouterStateSnapshot} state The current RouterStateSnapshot pageSize: parseInt(route.queryParams.pageSize, 10),
* @returns AdminQualityAssuranceEventsPageParams Emits the route parameters currentPage: parseInt(route.queryParams.page, 10),
*/ };
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AssuranceEventsPageParams { };
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10),
};
}
}

View File

@@ -1,53 +1,53 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; 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 { PaginatedList } from '../../core/data/paginated-list.model';
import { QualityAssuranceSourceObject } from '../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceObject } from '../../core/notifications/qa/models/quality-assurance-source.model';
import { QualityAssuranceSourceService } from '../../notifications/qa/source/quality-assurance-source.service'; 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<QualityAssuranceSourceObject[]>
*/ */
@Injectable({ providedIn: 'root' }) export const SourceDataResolver: ResolveFn<QualityAssuranceSourceObject[]> = (
export class SourceDataResolver { route: ActivatedRouteSnapshot,
private pageSize = environment.qualityAssuranceConfig.pageSize; state: RouterStateSnapshot,
/** router: Router = inject(Router),
* Initialize the effect class variables. qualityAssuranceSourceService: QualityAssuranceSourceService = inject(QualityAssuranceSourceService),
* @param {QualityAssuranceSourceService} qualityAssuranceSourceService appConfig: AppConfig = inject(APP_CONFIG),
*/ ): Observable<QualityAssuranceSourceObject[]> => {
constructor( const pageSize = appConfig.qualityAssuranceConfig.pageSize;
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<QualityAssuranceSourceObject[]>
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<QualityAssuranceSourceObject[]> {
return this.qualityAssuranceSourceService.getSources(this.pageSize, 0).pipe(
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
if (sources.page.length === 1) {
this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]);
}
return sources.page;
}));
}
/** return qualityAssuranceSourceService.getSources(pageSize, 0).pipe(
* map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
* @param route url path if (sources.page.length === 1) {
* @returns url path router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]);
*/ }
getResolvedUrl(route: ActivatedRouteSnapshot): string { return sources.page;
return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/'); }));
} };
/**
*
* @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('/');
} }

View File

@@ -6,7 +6,7 @@ import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { RegistrationResolver } from './registration.resolver'; import { RegistrationResolver } from './registration.resolver';
describe('RegistrationResolver', () => { describe('RegistrationResolver', () => {
let resolver: RegistrationResolver; let resolver: any;
let epersonRegistrationService: EpersonRegistrationService; let epersonRegistrationService: EpersonRegistrationService;
const token = 'test-token'; const token = 'test-token';
@@ -16,11 +16,11 @@ describe('RegistrationResolver', () => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByToken: createSuccessfulRemoteDataObject$(registration), searchByToken: createSuccessfulRemoteDataObject$(registration),
}); });
resolver = new RegistrationResolver(epersonRegistrationService); resolver = RegistrationResolver;
}); });
describe('resolve', () => { describe('resolve', () => {
it('should resolve a registration based on the token', (done) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -10,19 +11,13 @@ import { RemoteData } from '../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
@Injectable({ providedIn: 'root' }) export const RegistrationResolver: ResolveFn<RemoteData<Registration>> = (
/** route: ActivatedRouteSnapshot,
* Resolver to resolve a Registration object based on the provided token state: RouterStateSnapshot,
*/ epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService),
export class RegistrationResolver { ): Observable<RemoteData<Registration>> => {
const token = route.params.token;
constructor(private epersonRegistrationService: EpersonRegistrationService) { return epersonRegistrationService.searchByToken(token).pipe(
} getFirstCompletedRemoteData(),
);
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> { };
const token = route.params.token;
return this.epersonRegistrationService.searchByToken(token).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -10,21 +11,12 @@ import { RemoteData } from '../core/data/remote-data';
import { ItemRequest } from '../core/shared/item-request.model'; import { ItemRequest } from '../core/shared/item-request.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
/** export const RequestCopyResolver: ResolveFn<RemoteData<ItemRequest>> = (
* Resolves an {@link ItemRequest} from the token found in the route's parameters route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService),
export class RequestCopyResolver { ): Observable<RemoteData<ItemRequest>> => {
return itemRequestDataService.findById(route.params.token).pipe(
constructor( getFirstCompletedRemoteData(),
private itemRequestDataService: ItemRequestDataService, );
) { };
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ItemRequest>> | Promise<RemoteData<ItemRequest>> | RemoteData<ItemRequest> {
return this.itemRequestDataService.findById(route.params.token).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -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<MenuSection[]>[] {
return [
this.getItemMenu(dso),
this.getComColMenu(dso),
this.getCommonMenu(dso, state),
];
}
/**
* Get the common menus between all dspace objects
*/
protected getCommonMenu(dso, state): Observable<MenuSection[]> {
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<MenuSection[]> {
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<MenuSection[]> {
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;
});
}
}

View File

@@ -40,7 +40,7 @@ import {
} from '../remote-data.utils'; } from '../remote-data.utils';
import { MenuServiceStub } from '../testing/menu-service.stub'; import { MenuServiceStub } from '../testing/menu-service.stub';
import { createPaginatedList } from '../testing/utils.test'; 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 { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
@@ -50,7 +50,7 @@ describe('DSOEditMenuResolver', () => {
id: 'some menu', id: 'some menu',
}; };
let resolver: DSOEditMenuResolver; let resolver: DSOEditMenuResolverService;
let dSpaceObjectDataService; let dSpaceObjectDataService;
let menuService; let menuService;
@@ -189,12 +189,12 @@ describe('DSOEditMenuResolver', () => {
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
{ provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService }, { provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService },
{ provide: CorrectionTypeDataService, useValue: correctionsDataService }, { provide: CorrectionTypeDataService, useValue: correctionsDataService },
{ { provide: NgbModal, useValue: mockNgbModal },
provide: NgbModal, useValue: mockNgbModal }, DSOEditMenuResolverService,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}); });
resolver = TestBed.inject(DSOEditMenuResolver); resolver = TestBed.inject(DSOEditMenuResolverService);
spyOn(menuService, 'addSection'); spyOn(menuService, 'addSection');
})); }));

View File

@@ -1,324 +1,21 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Observable } from 'rxjs';
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 { MenuSection } from '../menu/menu-section.model';
import { NotificationsService } from '../notifications/notifications.service'; import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.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 * Initialise all dspace object related menus
*/ */
@Injectable({ export const DSOEditMenuResolver: ResolveFn<{ [key: string]: MenuSection[] }> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class DSOEditMenuResolver { menuResolverService: DSOEditMenuResolverService = inject(DSOEditMenuResolverService),
): Observable<{ [key: string]: MenuSection[] }> => {
constructor( return menuResolverService.resolve(route, state);
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<MenuSection[]>[] {
return [
this.getItemMenu(dso),
this.getComColMenu(dso),
this.getCommonMenu(dso, state),
];
}
/**
* Get the common menus between all dspace objects
*/
protected getCommonMenu(dso, state): Observable<MenuSection[]> {
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<MenuSection[]> {
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<MenuSection[]> {
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;
});
}
}

View File

@@ -53,6 +53,7 @@ import {
DynamicFormValidationService, DynamicFormValidationService,
DynamicTemplateDirective, DynamicTemplateDirective,
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { DynamicFormControlMapFn } from '@ng-dynamic-forms/core/lib/service/dynamic-form-component.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { import {
TranslateModule, TranslateModule,
@@ -74,7 +75,6 @@ import {
import { import {
APP_CONFIG, APP_CONFIG,
AppConfig, AppConfig,
DynamicFormControlFn,
} from '../../../../../config/app-config.interface'; } from '../../../../../config/app-config.interface';
import { AppState } from '../../../../app.reducer'; import { AppState } from '../../../../app.reducer';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
@@ -214,7 +214,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
public formBuilderService: FormBuilderService, public formBuilderService: FormBuilderService,
private submissionService: SubmissionService, private submissionService: SubmissionService,
@Inject(APP_CONFIG) protected appConfig: AppConfig, @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); super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;

View File

@@ -1,21 +1,18 @@
import { import {
Inject, inject,
Injectable,
InjectionToken, InjectionToken,
Injector, Injector,
} from '@angular/core'; } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import { LazyDataServicesMap } from '../../../../config/app-config.interface';
APP_DATA_SERVICES_MAP,
LazyDataServicesMap,
} from '../../../../config/app-config.interface';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { lazyService } from '../../../core/lazy-service'; import { lazyService } from '../../../core/lazy-service';
@@ -25,40 +22,36 @@ import { ResourceType } from '../../../core/shared/resource-type';
import { isEmpty } from '../../empty.util'; 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<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const ResourcePolicyTargetResolver: ResolveFn<RemoteData<DSpaceObject>> = (
export class ResourcePolicyTargetResolver { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
dataServiceMap: InjectionToken<LazyDataServicesMap> = inject(InjectionToken<LazyDataServicesMap>),
parentInjector: Injector = inject(Injector),
router: Router = inject(Router),
): Observable<RemoteData<DSpaceObject>> => {
const targetType = route.queryParamMap.get('targetType');
const policyTargetId = route.queryParamMap.get('policyTargetId');
constructor( if (isEmpty(targetType) || isEmpty(policyTargetId)) {
private parentInjector: Injector, router.navigateByUrl('/404', { skipLocationChange: true });
private router: Router,
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken<LazyDataServicesMap>) {
} }
/** const resourceType: ResourceType = new ResourceType(targetType);
* Method for resolving an item based on the parameters in the current route const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyService(dataServiceMap[resourceType.value], parentInjector);
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> 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<RemoteData<DSpaceObject>> {
const targetType = route.queryParamMap.get('targetType');
const policyTargetId = route.queryParamMap.get('policyTargetId');
if (isEmpty(targetType) || isEmpty(policyTargetId)) { return lazyProvider$.pipe(
this.router.navigateByUrl('/404', { skipLocationChange: true }); switchMap((dataService: IdentifiableDataService<DSpaceObject>) => {
} return dataService.findById(policyTargetId);
}),
const resourceType: ResourceType = new ResourceType(targetType); getFirstCompletedRemoteData(),
const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); );
};
return lazyProvider$.pipe(
switchMap((dataService: IdentifiableDataService<DSpaceObject>) => {
return dataService.findById(policyTargetId);
}),
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -14,30 +15,27 @@ import { isEmpty } from '../../empty.util';
import { followLink } from '../../utils/follow-link-config.model'; 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<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const ResourcePolicyResolver: ResolveFn<RemoteData<ResourcePolicy>> = (
export class ResourcePolicyResolver { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router: Router = inject(Router),
resourcePolicyService: ResourcePolicyDataService = inject(ResourcePolicyDataService),
): Observable<RemoteData<ResourcePolicy>> => {
const policyId = route.queryParamMap.get('policyId');
constructor(private resourcePolicyService: ResourcePolicyDataService, private router: Router) { if (isEmpty(policyId)) {
router.navigateByUrl('/404', { skipLocationChange: true });
} }
/** return resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe(
* Method for resolving an item based on the parameters in the current route getFirstCompletedRemoteData(),
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot );
* @param {RouterStateSnapshot} state The current RouterStateSnapshot };
* @returns Observable<<RemoteData<Item>> 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<RemoteData<ResourcePolicy>> {
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(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -12,23 +13,19 @@ import { SuggestionTargetDataService } from '../core/notifications/target/sugges
import { hasValue } from '../shared/empty.util'; 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<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const SuggestionsPageResolver: ResolveFn<RemoteData<SuggestionTarget>> = (
export class SuggestionsPageResolver { route: ActivatedRouteSnapshot,
constructor(private suggestionsDataService: SuggestionTargetDataService) { state: RouterStateSnapshot,
} suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService),
): Observable<RemoteData<SuggestionTarget>> => {
/** return suggestionsDataService.getTargetById(route.params.targetId).pipe(
* Method for resolving a suggestion target based on the parameters in the current route find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded),
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot );
* @param {RouterStateSnapshot} state The current RouterStateSnapshot };
* @returns Observable<<RemoteData<Collection>> 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<RemoteData<SuggestionTarget>> {
return this.suggestionsDataService.getTargetById(route.params.targetId).pipe(
find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded),
);
}
}

View File

@@ -6,7 +6,7 @@ import { ItemFromWorkflowResolver } from './item-from-workflow.resolver';
describe('ItemFromWorkflowResolver', () => { describe('ItemFromWorkflowResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: ItemFromWorkflowResolver; let resolver: any;
let wfiService: WorkflowItemDataService; let wfiService: WorkflowItemDataService;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
const itemUuid = '8888-8888-8888-8888'; const itemUuid = '8888-8888-8888-8888';
@@ -20,11 +20,11 @@ describe('ItemFromWorkflowResolver', () => {
wfiService = { wfiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), findById: (id: string) => createSuccessfulRemoteDataObject$(wfi),
} as any; } 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) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,20 +1,21 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { Store } from '@ngrx/store'; 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 { Item } from '../core/shared/item.model';
import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver';
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
/** export const ItemFromWorkflowResolver: ResolveFn<RemoteData<Item>> = (
* This class represents a resolver that requests a specific item before the route is activated route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService),
export class ItemFromWorkflowResolver extends SubmissionObjectResolver<Item> { ): Observable<RemoteData<Item>> => {
constructor( return SubmissionObjectResolver(route, state, workflowItemService);
private workflowItemService: WorkflowItemDataService, };
protected store: Store<any>,
) {
super(workflowItemService, store);
}
}

View File

@@ -6,7 +6,7 @@ import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
describe('WorkflowItemPageResolver', () => { describe('WorkflowItemPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: WorkflowItemPageResolver; let resolver: any;
let wfiService: WorkflowItemDataService; let wfiService: WorkflowItemDataService;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => {
wfiService = { wfiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), findById: (id: string) => createSuccessfulRemoteDataObject$({ id }),
} as any; } as any;
resolver = new WorkflowItemPageResolver(wfiService); resolver = WorkflowItemPageResolver;
}); });
it('should resolve a workflow item with the correct id', (done) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; 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 { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
import { followLink } from '../shared/utils/follow-link-config.model'; import { followLink } from '../shared/utils/follow-link-config.model';
/** export const WorkflowItemPageResolver: ResolveFn<RemoteData<WorkflowItem>> = (
* This class represents a resolver that requests a specific workflow item before the route is activated route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService),
export class WorkflowItemPageResolver { ): Observable<RemoteData<WorkflowItem>> => {
constructor(private workflowItemService: WorkflowItemDataService) { return workflowItemService.findById(
} route.params.id,
true,
/** false,
* Method for resolving a workflow item based on the parameters in the current route followLink('item'),
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot ).pipe(
* @param {RouterStateSnapshot} state The current RouterStateSnapshot getFirstCompletedRemoteData(),
* @returns Observable<<RemoteData<Item>> 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<RemoteData<WorkflowItem>> {
return this.workflowItemService.findById(route.params.id,
true,
false,
followLink('item'),
).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -6,7 +6,7 @@ import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver';
describe('ItemFromWorkspaceResolver', () => { describe('ItemFromWorkspaceResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: ItemFromWorkspaceResolver; let resolver: any;
let wfiService: WorkspaceitemDataService; let wfiService: WorkspaceitemDataService;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
const itemUuid = '8888-8888-8888-8888'; const itemUuid = '8888-8888-8888-8888';
@@ -20,11 +20,11 @@ describe('ItemFromWorkspaceResolver', () => {
wfiService = { wfiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), findById: (id: string) => createSuccessfulRemoteDataObject$(wfi),
} as any; } 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) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,20 +1,23 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { Store } from '@ngrx/store'; 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 { Item } from '../core/shared/item.model';
import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; 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 const ItemFromWorkspaceResolver: ResolveFn<RemoteData<Item>> = (
export class ItemFromWorkspaceResolver extends SubmissionObjectResolver<Item> { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private workspaceItemService: WorkspaceitemDataService, workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService),
protected store: Store<any>, ): Observable<RemoteData<Item>> => {
) { return SubmissionObjectResolver(route, state, workspaceItemService);
super(workspaceItemService, store); };
}
}

View File

@@ -6,7 +6,7 @@ import { WorkspaceItemPageResolver } from './workspace-item-page.resolver';
describe('WorkflowItemPageResolver', () => { describe('WorkflowItemPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: WorkspaceItemPageResolver; let resolver: any;
let wsiService: WorkspaceitemDataService; let wsiService: WorkspaceitemDataService;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => {
wsiService = { wsiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), findById: (id: string) => createSuccessfulRemoteDataObject$({ id }),
} as any; } as any;
resolver = new WorkspaceItemPageResolver(wsiService); resolver = WorkspaceItemPageResolver;
}); });
it('should resolve a workspace item with the correct id', (done) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,38 +1,35 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; 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 { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { followLink } from '../shared/utils/follow-link-config.model'; 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<<RemoteData<Item>> Emits the found workflow item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const WorkspaceItemPageResolver: ResolveFn<RemoteData<WorkspaceItem>> = (
export class WorkspaceItemPageResolver { route: ActivatedRouteSnapshot,
constructor(private workspaceItemService: WorkspaceitemDataService) { state: RouterStateSnapshot,
} workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService),
): Observable<RemoteData<WorkspaceItem>> => {
/** return workspaceItemService.findById(route.params.id,
* Method for resolving a workflow item based on the parameters in the current route true,
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot false,
* @param {RouterStateSnapshot} state The current RouterStateSnapshot followLink('item'),
* @returns Observable<<RemoteData<Item>> Emits the found workflow item based on the parameters in the current route, ).pipe(
* or an error if something went wrong getFirstCompletedRemoteData(),
*/ );
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<WorkflowItem>> { };
return this.workspaceItemService.findById(route.params.id,
true,
false,
followLink('item'),
).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -3,7 +3,6 @@ import {
makeStateKey, makeStateKey,
Type, Type,
} from '@angular/core'; } 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 { 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'; import { HALDataService } from '../app/core/data/base/hal-data-service.interface';
@@ -78,11 +77,8 @@ export interface LazyDataServicesMap {
[type: string]: () => Promise<Type<HALDataService<any>>> [type: string]: () => Promise<Type<HALDataService<any>>>
} }
export type DynamicFormControlFn = (model: string) => Type<DynamicFormControl>;
export const APP_DATA_SERVICES_MAP: InjectionToken<LazyDataServicesMap> = new InjectionToken<LazyDataServicesMap>('APP_DATA_SERVICES_MAP'); export const APP_DATA_SERVICES_MAP: InjectionToken<LazyDataServicesMap> = new InjectionToken<LazyDataServicesMap>('APP_DATA_SERVICES_MAP');
export const APP_DYNAMIC_FORM_CONTROL_FN: InjectionToken<DynamicFormControlFn> = new InjectionToken<DynamicFormControlFn>('APP_DYNAMIC_FORM_CONTROL_FN');
export { export {
APP_CONFIG, APP_CONFIG,
APP_CONFIG_STATE, APP_CONFIG_STATE,