theme.service.ts (dspace-angular-dspace-7.0) | : | theme.service.ts (dspace-angular-dspace-7.1) | ||
---|---|---|---|---|
import { Injectable } from '@angular/core'; | import { Injectable, Inject } from '@angular/core'; | |||
import { Store, createFeatureSelector, createSelector, select } from '@ngrx/stor e'; | import { Store, createFeatureSelector, createSelector, select } from '@ngrx/stor e'; | |||
import { Observable } from 'rxjs/internal/Observable'; | import { Observable } from 'rxjs/internal/Observable'; | |||
import { ThemeState } from './theme.reducer'; | import { ThemeState } from './theme.reducer'; | |||
import { SetThemeAction } from './theme.actions'; | import { SetThemeAction, ThemeActionTypes } from './theme.actions'; | |||
import { take } from 'rxjs/operators'; | import { expand, filter, map, switchMap, take, toArray } from 'rxjs/operators'; | |||
import { hasValue } from '../empty.util'; | import { hasValue, isNotEmpty } from '../empty.util'; | |||
import { RemoteData } from '../../core/data/remote-data'; | ||||
import { DSpaceObject } from '../../core/shared/dspace-object.model'; | ||||
import { | ||||
getFirstCompletedRemoteData, | ||||
getFirstSucceededRemoteData, | ||||
getRemoteDataPayload | ||||
} from '../../core/shared/operators'; | ||||
import { EMPTY, of as observableOf } from 'rxjs'; | ||||
import { Theme, ThemeConfig, themeFactory } from '../../../config/theme.model'; | ||||
import { NO_OP_ACTION_TYPE, NoOpAction } from '../ngrx/no-op.action'; | ||||
import { followLink } from '../utils/follow-link-config.model'; | ||||
import { LinkService } from '../../core/cache/builders/link.service'; | ||||
import { environment } from '../../../environments/environment'; | ||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv | ||||
ice'; | ||||
import { ActivatedRouteSnapshot } from '@angular/router'; | ||||
import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listab | ||||
le-object/listable-object.decorator'; | ||||
export const themeStateSelector = createFeatureSelector<ThemeState>('theme'); | export const themeStateSelector = createFeatureSelector<ThemeState>('theme'); | |||
export const currentThemeSelector = createSelector( | export const currentThemeSelector = createSelector( | |||
themeStateSelector, | themeStateSelector, | |||
(state: ThemeState): string => hasValue(state) ? state.currentTheme : undefine d | (state: ThemeState): string => hasValue(state) ? state.currentTheme : undefine d | |||
); | ); | |||
@Injectable({ | @Injectable({ | |||
providedIn: 'root' | providedIn: 'root' | |||
}) | }) | |||
export class ThemeService { | export class ThemeService { | |||
/** | ||||
* The list of configured themes | ||||
*/ | ||||
themes: Theme[]; | ||||
/** | ||||
* True if at least one theme depends on the route | ||||
*/ | ||||
hasDynamicTheme: boolean; | ||||
constructor( | constructor( | |||
private store: Store<ThemeState>, | private store: Store<ThemeState>, | |||
private linkService: LinkService, | ||||
private dSpaceObjectDataService: DSpaceObjectDataService, | ||||
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig | ||||
) { | ) { | |||
// Create objects from the theme configs in the environment file | ||||
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFact | ||||
ory(themeConfig)); | ||||
this.hasDynamicTheme = environment.themes.some((themeConfig: any) => | ||||
hasValue(themeConfig.regex) || | ||||
hasValue(themeConfig.handle) || | ||||
hasValue(themeConfig.uuid) | ||||
); | ||||
} | } | |||
setTheme(newName: string) { | setTheme(newName: string) { | |||
this.store.dispatch(new SetThemeAction(newName)); | this.store.dispatch(new SetThemeAction(newName)); | |||
} | } | |||
getThemeName(): string { | getThemeName(): string { | |||
let currentTheme: string; | let currentTheme: string; | |||
this.store.pipe( | this.store.pipe( | |||
select(currentThemeSelector), | select(currentThemeSelector), | |||
skipping to change at line 46 | skipping to change at line 82 | |||
); | ); | |||
return currentTheme; | return currentTheme; | |||
} | } | |||
getThemeName$(): Observable<string> { | getThemeName$(): Observable<string> { | |||
return this.store.pipe( | return this.store.pipe( | |||
select(currentThemeSelector) | select(currentThemeSelector) | |||
); | ); | |||
} | } | |||
/** | ||||
* Determine whether or not the theme needs to change depending on the current | ||||
route's URL and snapshot data | ||||
* If the snapshot contains a dso, this will be used to match a theme | ||||
* If the snapshot contains a scope parameters, this will be used to match a t | ||||
heme | ||||
* Otherwise the URL is matched against | ||||
* If none of the above find a match, the theme doesn't change | ||||
* @param currentRouteUrl | ||||
* @param activatedRouteSnapshot | ||||
* @return Observable boolean emitting whether or not the theme has been chang | ||||
ed | ||||
*/ | ||||
updateThemeOnRouteChange$(currentRouteUrl: string, activatedRouteSnapshot: Act | ||||
ivatedRouteSnapshot): Observable<boolean> { | ||||
// and the current theme from the store | ||||
const currentTheme$: Observable<string> = this.store.pipe(select(currentThem | ||||
eSelector)); | ||||
const action$ = currentTheme$.pipe( | ||||
switchMap((currentTheme: string) => { | ||||
const snapshotWithData = this.findRouteData(activatedRouteSnapshot); | ||||
if (this.hasDynamicTheme === true && isNotEmpty(this.themes)) { | ||||
if (hasValue(snapshotWithData) && hasValue(snapshotWithData.data) && h | ||||
asValue(snapshotWithData.data.dso)) { | ||||
const dsoRD: RemoteData<DSpaceObject> = snapshotWithData.data.dso; | ||||
if (dsoRD.hasSucceeded) { | ||||
// Start with the resolved dso and go recursively through its pare | ||||
nts until you reach the top-level community | ||||
return observableOf(dsoRD.payload).pipe( | ||||
this.getAncestorDSOs(), | ||||
map((dsos: DSpaceObject[]) => { | ||||
const dsoMatch = this.matchThemeToDSOs(dsos, currentRouteUrl); | ||||
return this.getActionForMatch(dsoMatch, currentTheme); | ||||
}) | ||||
); | ||||
} | ||||
} | ||||
if (hasValue(activatedRouteSnapshot.queryParams) && hasValue(activated | ||||
RouteSnapshot.queryParams.scope)) { | ||||
const dsoFromScope$: Observable<RemoteData<DSpaceObject>> = this.dSp | ||||
aceObjectDataService.findById(activatedRouteSnapshot.queryParams.scope); | ||||
// Start with the resolved dso and go recursively through its parent | ||||
s until you reach the top-level community | ||||
return dsoFromScope$.pipe( | ||||
getFirstSucceededRemoteData(), | ||||
getRemoteDataPayload(), | ||||
this.getAncestorDSOs(), | ||||
map((dsos: DSpaceObject[]) => { | ||||
const dsoMatch = this.matchThemeToDSOs(dsos, currentRouteUrl); | ||||
return this.getActionForMatch(dsoMatch, currentTheme); | ||||
}) | ||||
); | ||||
} | ||||
// check whether the route itself matches | ||||
const routeMatch = this.themes.find((theme: Theme) => theme.matches(cu | ||||
rrentRouteUrl, undefined)); | ||||
return [this.getActionForMatch(routeMatch, currentTheme)]; | ||||
} | ||||
// If there are no themes configured, do nothing | ||||
return [new NoOpAction()]; | ||||
}), | ||||
take(1), | ||||
); | ||||
action$.pipe( | ||||
filter((action) => action.type !== NO_OP_ACTION_TYPE), | ||||
).subscribe((action) => { | ||||
this.store.dispatch(action); | ||||
}); | ||||
return action$.pipe( | ||||
map((action) => action.type === ThemeActionTypes.SET), | ||||
); | ||||
} | ||||
/** | ||||
* Find a DSpaceObject in one of the provided route snapshots their data | ||||
* Recursively looks for the dso in the routes their child routes until it rea | ||||
ches a dead end or finds one | ||||
* @param routes | ||||
*/ | ||||
findRouteData(...routes: ActivatedRouteSnapshot[]) { | ||||
const result = routes.find((route) => hasValue(route.data) && hasValue(route | ||||
.data.dso)); | ||||
if (hasValue(result)) { | ||||
return result; | ||||
} else { | ||||
const nextLevelRoutes = routes | ||||
.map((route: ActivatedRouteSnapshot) => route.children) | ||||
.reduce((combined: ActivatedRouteSnapshot[], current: ActivatedRouteSnap | ||||
shot[]) => [...combined, ...current]); | ||||
if (isNotEmpty(nextLevelRoutes)) { | ||||
return this.findRouteData(...nextLevelRoutes); | ||||
} else { | ||||
return undefined; | ||||
} | ||||
} | ||||
} | ||||
/** | ||||
* An rxjs operator that will return an array of all the ancestors of the DSpa | ||||
ceObject used as | ||||
* input. The initial DSpaceObject will be the first element of the output arr | ||||
ay, followed by | ||||
* its parent, its grandparent etc | ||||
* | ||||
* @private | ||||
*/ | ||||
private getAncestorDSOs() { | ||||
return (source: Observable<DSpaceObject>): Observable<DSpaceObject[]> => | ||||
source.pipe( | ||||
expand((dso: DSpaceObject) => { | ||||
// Check if the dso exists and has a parent link | ||||
if (hasValue(dso) && typeof (dso as any).getParentLinkKey === 'functio | ||||
n') { | ||||
const linkName = (dso as any).getParentLinkKey(); | ||||
// If it does, retrieve it. | ||||
return this.linkService.resolveLinkWithoutAttaching<DSpaceObject, DS | ||||
paceObject>(dso, followLink(linkName)).pipe( | ||||
getFirstCompletedRemoteData(), | ||||
map((rd: RemoteData<DSpaceObject>) => { | ||||
if (hasValue(rd.payload)) { | ||||
// If there's a parent, use it for the next iteration | ||||
return rd.payload; | ||||
} else { | ||||
// If there's no parent, or an error, return null, which will | ||||
stop recursion | ||||
// in the next iteration | ||||
return null; | ||||
} | ||||
}), | ||||
); | ||||
} | ||||
// The current dso has no value, or no parent. Return EMPTY to stop re | ||||
cursion | ||||
return EMPTY; | ||||
}), | ||||
// only allow through DSOs that have a value | ||||
filter((dso: DSpaceObject) => hasValue(dso)), | ||||
// Wait for recursion to complete, and emit all results at once, in an a | ||||
rray | ||||
toArray() | ||||
); | ||||
} | ||||
/** | ||||
* return the action to dispatch based on the given matching theme | ||||
* | ||||
* @param newTheme The theme to create an action for | ||||
* @param currentThemeName The name of the currently active theme | ||||
* @private | ||||
*/ | ||||
private getActionForMatch(newTheme: Theme, currentThemeName: string): SetTheme | ||||
Action | NoOpAction { | ||||
if (hasValue(newTheme) && newTheme.config.name !== currentThemeName) { | ||||
// If we have a match, and it isn't already the active theme, set it as th | ||||
e new theme | ||||
return new SetThemeAction(newTheme.config.name); | ||||
} else { | ||||
// Otherwise, do nothing | ||||
return new NoOpAction(); | ||||
} | ||||
} | ||||
/** | ||||
* Check the given DSpaceObjects in order to see if they match the configured | ||||
themes in order. | ||||
* If a match is found, the matching theme is returned | ||||
* | ||||
* @param dsos The DSpaceObjects to check | ||||
* @param currentRouteUrl The url for the current route | ||||
* @private | ||||
*/ | ||||
private matchThemeToDSOs(dsos: DSpaceObject[], currentRouteUrl: string): Theme | ||||
{ | ||||
// iterate over the themes in order, and return the first one that matches | ||||
return this.themes.find((theme: Theme) => { | ||||
// iterate over the dsos's in order (most specific one first, so Item, Col | ||||
lection, | ||||
// Community), and return the first one that matches the current theme | ||||
const match = dsos.find((dso: DSpaceObject) => theme.matches(currentRouteU | ||||
rl, dso)); | ||||
return hasValue(match); | ||||
}); | ||||
} | ||||
/** | ||||
* Searches for a ThemeConfig by its name; | ||||
*/ | ||||
getThemeConfigFor(themeName: string): ThemeConfig { | ||||
return this.gtcf(themeName); | ||||
} | ||||
} | } | |||
End of changes. 6 change blocks. | ||||
4 lines changed or deleted | 240 lines changed or added |