[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 {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -11,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
/**
* This class represents a resolver that requests a specific bitstreamFormat before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class BitstreamFormatsResolver {
constructor(private bitstreamFormatDataService: BitstreamFormatDataService) {
}
/**
* Method for resolving an bitstreamFormat based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<BitstreamFormat>> {
return this.bitstreamFormatDataService.findById(route.params.id)
export const BitstreamFormatsResolver: ResolveFn<RemoteData<BitstreamFormat>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService),
): Observable<RemoteData<BitstreamFormat>> => {
return bitstreamFormatDataService.findById(route.params.id)
.pipe(
getFirstCompletedRemoteData(),
);
}
}
};

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -24,32 +25,20 @@ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
];
/**
* This class represents a resolver that requests a specific bitstream before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class BitstreamPageResolver {
constructor(private bitstreamService: BitstreamDataService) {
}
/**
* Method for resolving a bitstream based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> {
return this.bitstreamService.findById(route.params.id, true, false, ...this.followLinks)
export const BitstreamPageResolver: ResolveFn<RemoteData<Bitstream>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
bitstreamService: BitstreamDataService = inject(BitstreamDataService),
): Observable<RemoteData<Bitstream>> => {
return bitstreamService.findById(route.params.id, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW)
.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';
describe(`LegacyBitstreamUrlResolver`, () => {
let resolver: LegacyBitstreamUrlResolver;
let resolver: any;
let bitstreamDataService: BitstreamDataService;
let testScheduler;
let remoteDataMocks;
@@ -33,7 +33,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
bitstreamDataService = {
findByItemHandle: () => undefined,
} as any;
resolver = new LegacyBitstreamUrlResolver(bitstreamDataService);
resolver = LegacyBitstreamUrlResolver;
});
describe(`resolve`, () => {
@@ -51,7 +51,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
});
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
testScheduler.run(() => {
resolver.resolve(route, state);
resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`,
route.params.sequence_id,
@@ -78,7 +78,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
});
it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
testScheduler.run(() => {
resolver.resolve(route, state);
resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`,
route.queryParams.sequenceId,
@@ -100,7 +100,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
});
it(`should call findByItemHandle with the handle, and filename from the route`, () => {
testScheduler.run(() => {
resolver.resolve(route, state);
resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`,
undefined,
@@ -123,7 +123,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Error,
};
expectObservable(resolver.resolve(route, state)).toBe(expected, values);
expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
});
});
it(`...succeeded`, () => {
@@ -138,7 +138,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Success,
};
expectObservable(resolver.resolve(route, state)).toBe(expected, values);
expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
});
});
});

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -12,26 +13,20 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { hasNoValue } from '../shared/empty.util';
/**
* This class resolves a bitstream based on the DSpace 6 XMLUI or JSPUI bitstream download URLs
*/
@Injectable({
providedIn: 'root',
})
export class LegacyBitstreamUrlResolver {
constructor(protected bitstreamDataService: BitstreamDataService) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<RemoteData<Bitstream>> {
export const LegacyBitstreamUrlResolver: ResolveFn<RemoteData<Bitstream>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
): Observable<RemoteData<Bitstream>> => {
const prefix = route.params.prefix;
const suffix = route.params.suffix;
const filename = route.params.filename;
@@ -41,12 +36,11 @@ export class LegacyBitstreamUrlResolver {
sequenceId = route.queryParams.sequenceId;
}
return this.bitstreamDataService.findByItemHandle(
return 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 {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -19,30 +20,28 @@ import {
import { hasValue } from '../shared/empty.util';
/**
* The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page
*/
@Injectable({ providedIn: 'root' })
export class BrowseByDSOBreadcrumbResolver {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Community | Collection>> {
export const BrowseByDSOBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community | Collection>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: DSpaceObjectDataService = inject(DSpaceObjectDataService),
): Observable<BreadcrumbConfig<Community | Collection>> => {
const uuid = route.queryParams.scope;
if (hasValue(uuid)) {
return this.dataService.findById(uuid).pipe(
return dataService.findById(uuid).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((object: Community | Collection) => {
return { provider: this.breadcrumbService, key: object, url: getDSORoute(object) };
return { provider: breadcrumbService, key: object, url: getDSORoute(object) };
}),
);
}
return undefined;
}
}
};

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -28,25 +30,21 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
];
/**
* This class represents a resolver that requests a specific collection before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class CollectionPageResolver {
constructor(
private collectionService: CollectionDataService,
private store: Store<any>,
) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
const collectionRD$ = this.collectionService.findById(
export const CollectionPageResolver: ResolveFn<RemoteData<Collection>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
collectionService: CollectionDataService = inject(CollectionDataService),
store: Store<AppState> = inject(Store<AppState>),
): Observable<RemoteData<Collection>> => {
const collectionRD$ = collectionService.findById(
route.params.id,
true,
false,
@@ -56,9 +54,8 @@ export class CollectionPageResolver {
);
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
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 { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ItemTemplatePageResolver } from './item-template-page.resolver';
describe('ItemTemplatePageResolver', () => {
describe('resolve', () => {
let resolver: ItemTemplatePageResolver;
let resolver: any;
let itemTemplateService: any;
let dsoNameService: DSONameServiceMock;
const uuid = '1234-65487-12354-1235';
beforeEach(() => {
itemTemplateService = {
findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }),
};
dsoNameService = new DSONameServiceMock();
resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService);
resolver = ItemTemplatePageResolver;
});
it('should resolve an item template with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
(resolver({ params: { id: uuid } } as any, undefined, itemTemplateService) as Observable<any>)
.pipe(first())
.subscribe(
(resolved) => {

View File

@@ -1,38 +1,23 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { followLink } from '../../shared/utils/follow-link-config.model';
/**
* This class represents a resolver that requests a specific collection's item template before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ItemTemplatePageResolver {
constructor(
public dsoNameService: DSONameService,
private itemTemplateService: ItemTemplateDataService,
) {
}
/**
* Method for resolving a collection's item template based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<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(
export const ItemTemplatePageResolver: ResolveFn<RemoteData<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService),
): Observable<RemoteData<Item>> => {
return 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 { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
@@ -5,7 +6,7 @@ import { CommunityPageResolver } from './community-page.resolver';
describe('CommunityPageResolver', () => {
describe('resolve', () => {
let resolver: CommunityPageResolver;
let resolver: any;
let communityService: any;
let store: any;
const uuid = '1234-65487-12354-1235';
@@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => {
store = jasmine.createSpyObj('store', {
dispatch: {},
});
resolver = new CommunityPageResolver(communityService, store);
resolver = CommunityPageResolver;
});
it('should resolve a community with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any)
(resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, communityService, store) as Observable<any>)
.pipe(first())
.subscribe(
(resolved) => {

View File

@@ -1,11 +1,13 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -28,25 +30,21 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Community>[] = [
];
/**
* This class represents a resolver that requests a specific community before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class CommunityPageResolver {
constructor(
private communityService: CommunityDataService,
private store: Store<any>,
) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
const communityRD$ = this.communityService.findById(
export const CommunityPageResolver: ResolveFn<RemoteData<Community>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
store: Store<AppState> = inject(Store<AppState>),
): Observable<RemoteData<Community>> => {
const communityRD$ = communityService.findById(
route.params.id,
true,
false,
@@ -56,9 +54,8 @@ export class CommunityPageResolver {
);
communityRD$.subscribe((communityRD: RemoteData<Community>) => {
this.store.dispatch(new ResolvedAction(state.url, communityRD.payload));
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 { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BitstreamDataService } from '../data/bitstream-data.service';
import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
/**
* The class that resolves the BreadcrumbConfig object for an Item
* The resolve function that resolves the BreadcrumbConfig object for an Item
*/
@Injectable({
providedIn: 'root',
})
export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver<Bitstream> {
constructor(
protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) {
super(breadcrumbService, dataService);
}
export const BitstreamBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Bitstream>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService),
dataService: BitstreamDataService = inject(BitstreamDataService),
): Observable<BreadcrumbConfig<Bitstream>> => {
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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CollectionDataService } from '../data/collection-data.service';
import { Collection } from '../shared/collection.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
/**
* The class that resolves the BreadcrumbConfig object for a Collection
* The resolve function that resolves the BreadcrumbConfig object for a Collection
*/
@Injectable({
providedIn: 'root',
})
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
super(breadcrumbService, dataService);
}
export const CollectionBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Collection>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: CollectionDataService = inject(CollectionDataService),
): 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CommunityDataService } from '../data/community-data.service';
import { Community } from '../shared/community.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
/**
* The class that resolves the BreadcrumbConfig object for a Community
* The resolve function that resolves the BreadcrumbConfig object for a Community
*/
@Injectable({
providedIn: 'root',
})
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
super(breadcrumbService, dataService);
}
/**
* Method that returns the follow links to already resolve
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
get followLinks(): FollowLinkConfig<Community>[] {
return COMMUNITY_PAGE_LINKS_TO_FOLLOW;
}
}
export const CommunityBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: CommunityDataService = inject(CommunityDataService),
): Observable<BreadcrumbConfig<Community>> => {
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Community>>;
};

View File

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

View File

@@ -1,4 +1,3 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
@@ -10,7 +9,6 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { ChildHALResource } from '../shared/child-hal-resource.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import {
getFirstCompletedRemoteData,
@@ -19,45 +17,33 @@ import {
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
/**
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
*/
@Injectable({
providedIn: 'root',
})
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> {
protected constructor(
protected breadcrumbService: DSOBreadcrumbsService,
protected dataService: IdentifiableDataService<T>,
) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService<DSpaceObject>, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]) => Observable<BreadcrumbConfig<DSpaceObject>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService,
dataService: IdentifiableDataService<DSpaceObject>,
...linksToFollow: FollowLinkConfig<DSpaceObject>[]
): Observable<BreadcrumbConfig<DSpaceObject>> => {
const uuid = route.params.id;
return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe(
return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
map((object: T) => {
map((object: DSpaceObject) => {
if (hasValue(object)) {
const fullPath = state.url;
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
return { provider: this.breadcrumbService, key: object, url: url };
const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid);
return { provider: 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('resolve', () => {
let resolver: I18nBreadcrumbResolver;
let resolver: any;
let i18nBreadcrumbService: any;
let i18nKey: string;
let route: any;
@@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => {
};
expectedPath = new URLCombiner(parentSegment, segment).toString();
i18nBreadcrumbService = {};
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
resolver = I18nBreadcrumbResolver;
});
it('should resolve the breadcrumb config', () => {
const resolvedConfig = resolver.resolve(route, {} as any);
const resolvedConfig = resolver(route, {} as any, i18nBreadcrumbService);
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
expect(resolvedConfig).toEqual(expectedConfig);
});
it('should resolve throw an error when no breadcrumbKey is defined', () => {
expect(() => {
resolver.resolve({ data: {} } as any, undefined);
resolver({ data: {} } as any, undefined, i18nBreadcrumbService);
}).toThrow();
});
});

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
@@ -10,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
/**
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
*/
@Injectable({
providedIn: 'root',
})
export class I18nBreadcrumbResolver {
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
export const I18nBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: I18nBreadcrumbsService = inject(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: this.breadcrumbService, key: key, url: fullPath };
}
}
return { provider: 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { ItemDataService } from '../data/item-data.service';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
/**
* The class that resolves the BreadcrumbConfig object for an Item
* The resolve function that resolves the BreadcrumbConfig object for an Item
*/
@Injectable({
providedIn: 'root',
})
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
super(breadcrumbService, dataService);
}
/**
* Method that returns the follow links to already resolve
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
get followLinks(): FollowLinkConfig<Item>[] {
return ITEM_PAGE_LINKS_TO_FOLLOW;
}
}
export const ItemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: ItemDataService = inject(ItemDataService),
): Observable<BreadcrumbConfig<Item>> => {
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Item>>;
};

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
@@ -8,37 +9,21 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service';
/**
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route and related parents
*/
@Injectable({
providedIn: 'root',
})
export class NavigationBreadcrumbResolver {
private parentRoutes: ActivatedRouteSnapshot[] = [];
constructor(protected breadcrumbService: NavigationBreadcrumbsService) {
}
/**
* Method to collect all parent routes snapshot from current route snapshot
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
*/
private getParentRoutes(route: ActivatedRouteSnapshot): void {
if (route.parent) {
this.parentRoutes.push(route.parent);
this.getParentRoutes(route.parent);
}
}
/**
* Method for resolving an I18n breadcrumb configuration object
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @param {NavigationBreadcrumbsService} breadcrumbService
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
this.getParentRoutes(route);
export const NavigationBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService),
): BreadcrumbConfig<string> => {
const parentRoutes: ActivatedRouteSnapshot[] = [];
getParentRoutes(route, parentRoutes);
const relatedRoutes = route.data.relatedRoutes;
const parentPaths = this.parentRoutes.map(parent => parent.routeConfig?.path);
const parentPaths = parentRoutes.map(parent => parent.routeConfig?.path);
const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path));
const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path;
const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length);
@@ -51,6 +36,17 @@ export class NavigationBreadcrumbResolver {
return `${previous}:${baseUrl}${current.path}`;
}, state.url);
return { provider: this.breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls };
return { provider: breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls };
};
/**
* Method to collect all parent routes snapshot from current route snapshot
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {ActivatedRouteSnapshot[]} parentRoutes
*/
function getParentRoutes(route: ActivatedRouteSnapshot, parentRoutes: ActivatedRouteSnapshot[]): void {
if (route.parent) {
parentRoutes.push(route.parent);
getParentRoutes(route.parent, parentRoutes);
}
}

View File

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

View File

@@ -1,28 +1,18 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
@Injectable({
providedIn: 'root',
})
export class PublicationClaimBreadcrumbResolver {
constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) {
}
/**
* Method that resolve Publication Claim item into a breadcrumb
* The parameter are retrieved by the url since part of the Publication Claim route config
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
export const PublicationClaimBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService),
): BreadcrumbConfig<string> => {
const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: this.breadcrumbService, key: targetId };
}
}
return { provider: breadcrumbService, key: targetId };
};

View File

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

View File

@@ -1,26 +1,18 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service';
@Injectable({
providedIn: 'root',
})
export class QualityAssuranceBreadcrumbResolver {
constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {}
/**
* Method that resolve QA item into a breadcrumb
* The parameter are retrieved by the url since part of the QA route config
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
export const QualityAssuranceBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService),
): BreadcrumbConfig<string> => {
const sourceId = route.paramMap.get('sourceId');
const topicId = route.paramMap.get('topicId');
let key = sourceId;
@@ -29,8 +21,7 @@ export class QualityAssuranceBreadcrumbResolver {
key += `:${topicId}`;
}
const fullPath = state.url;
const url = fullPath.substr(0, fullPath.indexOf(sourceId));
const url = fullPath.substring(0, fullPath.indexOf(sourceId));
return { provider: this.breadcrumbService, key, url };
}
}
return { provider: breadcrumbService, key, url };
};

View File

@@ -1,45 +1,37 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
import { RemoteData } from '../../data/remote-data';
import { Item } from '../../shared/item.model';
import { getFirstCompletedRemoteData } from '../../shared/operators';
import { SubmissionObject } from '../models/submission-object.model';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class SubmissionObjectResolver<T> {
constructor(
protected dataService: IdentifiableDataService<any>,
protected store: Store<any>,
) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<T>> {
const itemRD$ = this.dataService.findById(route.params.id,
export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, dataService: IdentifiableDataService<SubmissionObject>) => Observable<RemoteData<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
dataService: IdentifiableDataService<SubmissionObject>,
): Observable<RemoteData<Item>> => {
return dataService.findById(route.params.id,
true,
false,
followLink('item'),
).pipe(
getFirstCompletedRemoteData(),
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<T>>),
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
getFirstCompletedRemoteData(),
);
return itemRD$;
}
}
};

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
@@ -8,36 +9,47 @@ import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppState } from '../app.reducer';
import { ItemDataService } from '../core/data/item-data.service';
import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions';
import { Item } from '../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { hasValue } from '../shared/empty.util';
import { ItemResolver } from './item.resolver';
import { ITEM_PAGE_LINKS_TO_FOLLOW } from './item.resolver';
import { getItemPageRoute } from './item-page-routing-paths';
/**
* This class represents a resolver that requests a specific item before the route is activated and will redirect to the
* entity page
*/
@Injectable({ providedIn: 'root' })
export class ItemPageResolver extends ItemResolver {
constructor(
protected itemService: ItemDataService,
protected store: Store<any>,
protected router: Router,
) {
super(itemService, store, router);
}
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @param {Router} router
* @param {ItemDataService} itemService
* @param {Store<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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return super.resolve(route, state).pipe(
export const ItemPageResolver: ResolveFn<RemoteData<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router: Router = inject(Router),
itemService: ItemDataService = inject(ItemDataService),
store: Store<AppState> = inject(Store<AppState>),
): Observable<RemoteData<Item>> => {
const itemRD$ = itemService.findById(
route.params.id,
true,
false,
...ITEM_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
store.dispatch(new ResolvedAction(state.url, itemRD.payload));
});
return itemRD$.pipe(
map((rd: RemoteData<Item>) => {
if (rd.hasSucceeded && hasValue(rd.payload)) {
const thisRoute = state.url;
@@ -46,16 +58,15 @@ export class ItemPageResolver extends ItemResolver {
// or semicolons) and thisRoute has been encoded with that function. If we want to compare
// it with itemRoute, we have to run itemRoute through Angular's version as well to ensure
// the same characters are encoded the same way.
const itemRoute = this.router.parseUrl(getItemPageRoute(rd.payload)).toString();
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);
this.router.navigateByUrl(itemRoute + subRoute);
router.navigateByUrl(itemRoute + subRoute);
}
}
return rd;
}),
);
}
}
};

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
Router,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { ItemDataService } from '../core/data/item-data.service';
import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -31,27 +32,14 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
followLink('thumbnail'),
];
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ItemResolver {
constructor(
protected itemService: ItemDataService,
protected store: Store<any>,
protected router: Router,
) {
}
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<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,
export const ItemResolver: ResolveFn<RemoteData<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
itemService: ItemDataService = inject(ItemDataService),
store: Store<AppState> = inject(Store<AppState>),
): Observable<RemoteData<Item>> => {
const itemRD$ = itemService.findById(
route.params.id,
true,
false,
...ITEM_PAGE_LINKS_TO_FOLLOW,
@@ -60,9 +48,8 @@ export class ItemResolver {
);
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload));
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 {
ActivatedRouteSnapshot,
Router,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppState } from '../../app.reducer';
import { RemoteData } from '../../core/data/remote-data';
import { VersionDataService } from '../../core/data/version-data.service';
import { ResolvedAction } from '../../core/resolving/resolver.actions';
@@ -26,26 +27,21 @@ export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Version>[] = [
];
/**
* This class represents a resolver that requests a specific version before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class VersionResolver {
constructor(
protected versionService: VersionDataService,
protected store: Store<any>,
protected router: Router,
) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Version>> {
const versionRD$ = this.versionService.findById(route.params.id,
export const VersionResolver: ResolveFn<RemoteData<Version>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
versionService: VersionDataService = inject(VersionDataService),
store: Store<AppState> = inject(Store<AppState>),
): Observable<RemoteData<Version>> => {
const versionRD$ = versionService.findById(route.params.id,
true,
false,
...VERSION_PAGE_LINKS_TO_FOLLOW,
@@ -54,9 +50,8 @@ export class VersionResolver {
);
versionRD$.subscribe((versionRD: RemoteData<Version>) => {
this.store.dispatch(new ResolvedAction(state.url, versionRD.payload));
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 { FeatureID } from './core/data/feature-authorization/feature-id';
import { ScriptDataService } from './core/data/processes/script-data.service';
import { MenuResolver } from './menu.resolver';
import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/menu-id.model';
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils';
@@ -27,6 +26,7 @@ import { ConfigurationDataServiceStub } from './shared/testing/configuration-dat
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { createPaginatedList } from './shared/testing/utils.test';
import createSpy = jasmine.createSpy;
import { MenuResolverService } from './menu-resolver.service';
const BOOLEAN = { t: true, f: false };
const MENU_STATE = {
@@ -39,7 +39,7 @@ const BROWSE_DEFINITIONS = [
];
describe('MenuResolver', () => {
let resolver: MenuResolver;
let resolver: MenuResolverService;
let menuService;
let browseService;
@@ -79,12 +79,12 @@ describe('MenuResolver', () => {
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{
provide: NgbModal, useValue: mockNgbModal },
{ provide: NgbModal, useValue: mockNgbModal },
MenuResolverService,
],
schemas: [NO_ERRORS_SCHEMA],
});
resolver = TestBed.inject(MenuResolver);
resolver = TestBed.inject(MenuResolverService);
}));
it('should be created', () => {

View File

@@ -1,846 +1,21 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
combineLatest,
combineLatest as observableCombineLatest,
Observable,
} from 'rxjs';
import {
filter,
find,
map,
take,
} from 'rxjs/operators';
import { Observable } from 'rxjs';
import { MenuResolverService } from './menu-resolver.service';
import { PUBLICATION_CLAIMS_PATH } from './admin/admin-notifications/admin-notifications-routing-paths';
import { BrowseService } from './core/browse/browse.service';
import { ConfigurationDataService } from './core/data/configuration-data.service';
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { FeatureID } from './core/data/feature-authorization/feature-id';
import { PaginatedList } from './core/data/paginated-list.model';
import {
METADATA_EXPORT_SCRIPT_NAME,
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService,
} from './core/data/processes/script-data.service';
import { RemoteData } from './core/data/remote-data';
import { BrowseDefinition } from './core/shared/browse-definition.model';
import { ConfigurationProperty } from './core/shared/configuration-property.model';
import { getFirstCompletedRemoteData } from './core/shared/operators';
import { ThemedCreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
import { ThemedCreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
import { ThemedCreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { ThemedEditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
import { ThemedEditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
import { ThemedEditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { hasValue } from './shared/empty.util';
import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/menu-id.model';
import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model';
import { MenuItemType } from './shared/menu/menu-item-type.model';
import { MenuState } from './shared/menu/menu-state.model';
/**
* Creates all of the app's menus
*/
@Injectable({
providedIn: 'root',
})
export class MenuResolver {
constructor(
protected menuService: MenuService,
protected browseService: BrowseService,
protected authorizationService: AuthorizationDataService,
protected modalService: NgbModal,
protected scriptDataService: ScriptDataService,
protected configurationDataService: ConfigurationDataService,
) {
}
/**
* Initialize all menus
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<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,
})));
});
}
}
export const MenuResolver: ResolveFn<boolean> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
menuResolverService: MenuResolverService = inject(MenuResolverService),
): Observable<boolean> => {
return menuResolverService.resolve(route, state);
};

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -16,23 +17,19 @@ export const PROCESS_PAGE_FOLLOW_LINKS = [
];
/**
* This class represents a resolver that requests a specific process before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ProcessPageResolver {
constructor(private processService: ProcessDataService) {
}
/**
* Method for resolving a process based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> {
return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe(
export const ProcessPageResolver: ResolveFn<RemoteData<Process>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
processService: ProcessDataService = inject(ProcessDataService),
): Observable<RemoteData<Process>> => {
return 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 {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
@@ -14,22 +14,18 @@ export interface AssuranceEventsPageParams {
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable({ providedIn: 'root' })
export class QualityAssuranceEventsPageResolver {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminQualityAssuranceEventsPageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AssuranceEventsPageParams {
export const ItemResolver: ResolveFn<AssuranceEventsPageParams> = (
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 {
ActivatedRouteSnapshot,
ResolveFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { QualityAssuranceSourceObject } from '../../core/notifications/qa/models/quality-assurance-source.model';
import { QualityAssuranceSourceService } from '../../notifications/qa/source/quality-assurance-source.service';
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable({ providedIn: 'root' })
export class SourceDataResolver {
private pageSize = environment.qualityAssuranceConfig.pageSize;
/**
* Initialize the effect class variables.
* @param {QualityAssuranceSourceService} qualityAssuranceSourceService
*/
constructor(
private qualityAssuranceSourceService: QualityAssuranceSourceService,
private router: Router,
) { }
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @param router
* @param qualityAssuranceSourceService
* @param appConfig
* @returns Observable<QualityAssuranceSourceObject[]>
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<QualityAssuranceSourceObject[]> {
return this.qualityAssuranceSourceService.getSources(this.pageSize, 0).pipe(
export const SourceDataResolver: ResolveFn<QualityAssuranceSourceObject[]> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router: Router = inject(Router),
qualityAssuranceSourceService: QualityAssuranceSourceService = inject(QualityAssuranceSourceService),
appConfig: AppConfig = inject(APP_CONFIG),
): Observable<QualityAssuranceSourceObject[]> => {
const pageSize = appConfig.qualityAssuranceConfig.pageSize;
return qualityAssuranceSourceService.getSources(pageSize, 0).pipe(
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
if (sources.page.length === 1) {
this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]);
router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]);
}
return sources.page;
}));
}
};
/**
/**
*
* @param route url path
* @returns url path
*/
getResolvedUrl(route: ActivatedRouteSnapshot): string {
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';
describe('RegistrationResolver', () => {
let resolver: RegistrationResolver;
let resolver: any;
let epersonRegistrationService: EpersonRegistrationService;
const token = 'test-token';
@@ -16,11 +16,11 @@ describe('RegistrationResolver', () => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByToken: createSuccessfulRemoteDataObject$(registration),
});
resolver = new RegistrationResolver(epersonRegistrationService);
resolver = RegistrationResolver;
});
describe('resolve', () => {
it('should resolve a registration based on the token', (done) => {
resolver.resolve({ params: { token: token } } as any, undefined)
resolver({ params: { token: token } } as any, undefined, epersonRegistrationService)
.pipe(first())
.subscribe(
(resolved) => {

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -10,21 +11,12 @@ import { RemoteData } from '../core/data/remote-data';
import { ItemRequest } from '../core/shared/item-request.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
/**
* Resolves an {@link ItemRequest} from the token found in the route's parameters
*/
@Injectable({ providedIn: 'root' })
export class RequestCopyResolver {
constructor(
private itemRequestDataService: ItemRequestDataService,
) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ItemRequest>> | Promise<RemoteData<ItemRequest>> | RemoteData<ItemRequest> {
return this.itemRequestDataService.findById(route.params.token).pipe(
export const RequestCopyResolver: ResolveFn<RemoteData<ItemRequest>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService),
): Observable<RemoteData<ItemRequest>> => {
return 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';
import { MenuServiceStub } from '../testing/menu-service.stub';
import { createPaginatedList } from '../testing/utils.test';
import { DSOEditMenuResolver } from './dso-edit-menu.resolver';
import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service';
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
@@ -50,7 +50,7 @@ describe('DSOEditMenuResolver', () => {
id: 'some menu',
};
let resolver: DSOEditMenuResolver;
let resolver: DSOEditMenuResolverService;
let dSpaceObjectDataService;
let menuService;
@@ -189,12 +189,12 @@ describe('DSOEditMenuResolver', () => {
{ provide: NotificationsService, useValue: notificationsService },
{ provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService },
{ provide: CorrectionTypeDataService, useValue: correctionsDataService },
{
provide: NgbModal, useValue: mockNgbModal },
{ provide: NgbModal, useValue: mockNgbModal },
DSOEditMenuResolverService,
],
schemas: [NO_ERRORS_SCHEMA],
});
resolver = TestBed.inject(DSOEditMenuResolver);
resolver = TestBed.inject(DSOEditMenuResolverService);
spyOn(menuService, 'addSection');
}));

View File

@@ -1,324 +1,21 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import {
combineLatest,
Observable,
of as observableOf,
} from 'rxjs';
import {
map,
switchMap,
} from 'rxjs/operators';
import { Observable } from 'rxjs';
import { getDSORoute } from '../../app-routing-paths';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model';
import {
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../core/shared/operators';
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import {
hasNoValue,
hasValue,
isNotEmpty,
} from '../empty.util';
import { MenuService } from '../menu/menu.service';
import { MenuID } from '../menu/menu-id.model';
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model';
import { MenuItemType } from '../menu/menu-item-type.model';
import { MenuSection } from '../menu/menu-section.model';
import { NotificationsService } from '../notifications/notifications.service';
import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component';
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
import {
DsoWithdrawnReinstateModalService,
REQUEST_REINSTATE,
REQUEST_WITHDRAWN,
} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service';
/**
* Creates the menus for the dspace object pages
*/
@Injectable({
providedIn: 'root',
})
export class DSOEditMenuResolver {
constructor(
protected dSpaceObjectDataService: DSpaceObjectDataService,
protected menuService: MenuService,
protected authorizationService: AuthorizationDataService,
protected modalService: NgbModal,
protected dsoVersioningModalService: DsoVersioningModalService,
protected researcherProfileService: ResearcherProfileDataService,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
private correctionTypeDataService: CorrectionTypeDataService,
) {
}
/**
* Initialise all dspace object related menus
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> {
let id = route.params.id;
if (hasNoValue(id) && hasValue(route.queryParams.scope)) {
id = route.queryParams.scope;
}
if (hasNoValue(id)) {
// If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data
return observableOf({ ...route.data?.menu });
} else {
return this.dSpaceObjectDataService.findById(id, true, false).pipe(
getFirstCompletedRemoteData(),
switchMap((dsoRD) => {
if (dsoRD.hasSucceeded) {
const dso = dsoRD.payload;
return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
// Menu sections are retrieved as an array of arrays and flattened into a single array
map((combinedMenus) => [].concat.apply([], combinedMenus)),
map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
map((menus) => {
return {
...route.data?.menu,
[MenuID.DSO_EDIT]: menus,
};
}),
);
} else {
return observableOf({ ...route.data?.menu });
}
}),
);
}
}
/**
* Return all the menus for a dso based on the route and state
*/
getDsoMenus(dso, route, state): Observable<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;
});
}
}
export const DSOEditMenuResolver: ResolveFn<{ [key: string]: MenuSection[] }> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
menuResolverService: DSOEditMenuResolverService = inject(DSOEditMenuResolverService),
): Observable<{ [key: string]: MenuSection[] }> => {
return menuResolverService.resolve(route, state);
};

View File

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

View File

@@ -1,21 +1,18 @@
import {
Inject,
Injectable,
inject,
InjectionToken,
Injector,
} from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
APP_DATA_SERVICES_MAP,
LazyDataServicesMap,
} from '../../../../config/app-config.interface';
import { LazyDataServicesMap } from '../../../../config/app-config.interface';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { lazyService } from '../../../core/lazy-service';
@@ -25,34 +22,31 @@ import { ResourceType } from '../../../core/shared/resource-type';
import { isEmpty } from '../../empty.util';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ResourcePolicyTargetResolver {
constructor(
private parentInjector: Injector,
private router: Router,
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken<LazyDataServicesMap>) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<DSpaceObject>> {
export const ResourcePolicyTargetResolver: ResolveFn<RemoteData<DSpaceObject>> = (
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');
if (isEmpty(targetType) || isEmpty(policyTargetId)) {
this.router.navigateByUrl('/404', { skipLocationChange: true });
router.navigateByUrl('/404', { skipLocationChange: true });
}
const resourceType: ResourceType = new ResourceType(targetType);
const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector);
const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyService(dataServiceMap[resourceType.value], parentInjector);
return lazyProvider$.pipe(
switchMap((dataService: IdentifiableDataService<DSpaceObject>) => {
@@ -60,5 +54,4 @@ export class ResourcePolicyTargetResolver {
}),
getFirstCompletedRemoteData(),
);
}
}
};

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
@@ -14,30 +15,27 @@ import { isEmpty } from '../../empty.util';
import { followLink } from '../../utils/follow-link-config.model';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ResourcePolicyResolver {
constructor(private resourcePolicyService: ResourcePolicyDataService, private router: Router) {
}
/**
* 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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ResourcePolicy>> {
export const ResourcePolicyResolver: ResolveFn<RemoteData<ResourcePolicy>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router: Router = inject(Router),
resourcePolicyService: ResourcePolicyDataService = inject(ResourcePolicyDataService),
): Observable<RemoteData<ResourcePolicy>> => {
const policyId = route.queryParamMap.get('policyId');
if (isEmpty(policyId)) {
this.router.navigateByUrl('/404', { skipLocationChange: true });
router.navigateByUrl('/404', { skipLocationChange: true });
}
return this.resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe(
return 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 {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -12,23 +13,19 @@ import { SuggestionTargetDataService } from '../core/notifications/target/sugges
import { hasValue } from '../shared/empty.util';
/**
* This class represents a resolver that requests a specific collection before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class SuggestionsPageResolver {
constructor(private suggestionsDataService: SuggestionTargetDataService) {
}
/**
* Method for resolving a suggestion target based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @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
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<SuggestionTarget>> {
return this.suggestionsDataService.getTargetById(route.params.targetId).pipe(
export const SuggestionsPageResolver: ResolveFn<RemoteData<SuggestionTarget>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService),
): Observable<RemoteData<SuggestionTarget>> => {
return 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('resolve', () => {
let resolver: ItemFromWorkflowResolver;
let resolver: any;
let wfiService: WorkflowItemDataService;
const uuid = '1234-65487-12354-1235';
const itemUuid = '8888-8888-8888-8888';
@@ -20,11 +20,11 @@ describe('ItemFromWorkflowResolver', () => {
wfiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$(wfi),
} as any;
resolver = new ItemFromWorkflowResolver(wfiService, null);
resolver = ItemFromWorkflowResolver;
});
it('should resolve a an item from from the workflow item with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
resolver({ params: { id: uuid } } as any, undefined, wfiService)
.pipe(first())
.subscribe(
(resolved) => {

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -11,28 +12,17 @@ import { WorkflowItem } from '../core/submission/models/workflowitem.model';
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
import { followLink } from '../shared/utils/follow-link-config.model';
/**
* This class represents a resolver that requests a specific workflow item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class WorkflowItemPageResolver {
constructor(private workflowItemService: WorkflowItemDataService) {
}
/**
* Method for resolving a workflow item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<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,
export const WorkflowItemPageResolver: ResolveFn<RemoteData<WorkflowItem>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService),
): Observable<RemoteData<WorkflowItem>> => {
return 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('resolve', () => {
let resolver: ItemFromWorkspaceResolver;
let resolver: any;
let wfiService: WorkspaceitemDataService;
const uuid = '1234-65487-12354-1235';
const itemUuid = '8888-8888-8888-8888';
@@ -20,11 +20,11 @@ describe('ItemFromWorkspaceResolver', () => {
wfiService = {
findById: (id: string) => createSuccessfulRemoteDataObject$(wfi),
} as any;
resolver = new ItemFromWorkspaceResolver(wfiService, null);
resolver = ItemFromWorkspaceResolver;
});
it('should resolve a an item from from the workflow item with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
resolver({ params: { id: uuid } } as any, undefined, wfiService)
.pipe(first())
.subscribe(
(resolved) => {

View File

@@ -1,20 +1,23 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data';
import { Item } from '../core/shared/item.model';
import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
/**
* This class represents a resolver that requests a specific item before the route is activated
* This method represents a resolver that requests a specific item before the route is activated
*/
@Injectable({ providedIn: 'root' })
export class ItemFromWorkspaceResolver extends SubmissionObjectResolver<Item> {
constructor(
private workspaceItemService: WorkspaceitemDataService,
protected store: Store<any>,
) {
super(workspaceItemService, store);
}
}
export const ItemFromWorkspaceResolver: ResolveFn<RemoteData<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService),
): Observable<RemoteData<Item>> => {
return SubmissionObjectResolver(route, state, workspaceItemService);
};

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import {
makeStateKey,
Type,
} from '@angular/core';
import { DynamicFormControl } from '@ng-dynamic-forms/core/lib/component/dynamic-form-control-interface';
import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
import { HALDataService } from '../app/core/data/base/hal-data-service.interface';
@@ -78,11 +77,8 @@ export interface LazyDataServicesMap {
[type: string]: () => Promise<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_DYNAMIC_FORM_CONTROL_FN: InjectionToken<DynamicFormControlFn> = new InjectionToken<DynamicFormControlFn>('APP_DYNAMIC_FORM_CONTROL_FN');
export {
APP_CONFIG,
APP_CONFIG_STATE,