theme.effects.ts (dspace-angular-dspace-7.0) | : | theme.effects.ts (dspace-angular-dspace-7.1) | ||
---|---|---|---|---|
import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | |||
import { createEffect, Actions, ofType, ROOT_EFFECTS_INIT } from '@ngrx/effects' ; | import { createEffect, Actions, ofType, ROOT_EFFECTS_INIT } from '@ngrx/effects' ; | |||
import { ROUTER_NAVIGATED, RouterNavigatedAction } from '@ngrx/router-store'; | import { map } from 'rxjs/operators'; | |||
import { map, withLatestFrom, expand, switchMap, toArray, startWith, filter } fr | ||||
om 'rxjs/operators'; | ||||
import { SetThemeAction } from './theme.actions'; | import { SetThemeAction } from './theme.actions'; | |||
import { environment } from '../../../environments/environment'; | import { environment } from '../../../environments/environment'; | |||
import { ThemeConfig, themeFactory, Theme, } from '../../../config/theme.model'; | import { hasValue, hasNoValue } from '../empty.util'; | |||
import { hasValue, isNotEmpty, hasNoValue } from '../empty.util'; | ||||
import { NoOpAction } from '../ngrx/no-op.action'; | ||||
import { Store, select } from '@ngrx/store'; | ||||
import { ThemeState } from './theme.reducer'; | ||||
import { currentThemeSelector } from './theme.service'; | ||||
import { of as observableOf, EMPTY, Observable } from 'rxjs'; | ||||
import { ResolverActionTypes, ResolvedAction } from '../../core/resolving/resolv | ||||
er.actions'; | ||||
import { followLink } from '../utils/follow-link-config.model'; | ||||
import { RemoteData } from '../../core/data/remote-data'; | ||||
import { DSpaceObject } from '../../core/shared/dspace-object.model'; | ||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; | ||||
import { LinkService } from '../../core/cache/builders/link.service'; | ||||
import { BASE_THEME_NAME } from './theme.constants'; | import { BASE_THEME_NAME } from './theme.constants'; | |||
export const DEFAULT_THEME_CONFIG = environment.themes.find((themeConfig: any) = > | export const DEFAULT_THEME_CONFIG = environment.themes.find((themeConfig: any) = > | |||
hasNoValue(themeConfig.regex) && | hasNoValue(themeConfig.regex) && | |||
hasNoValue(themeConfig.handle) && | hasNoValue(themeConfig.handle) && | |||
hasNoValue(themeConfig.uuid) | hasNoValue(themeConfig.uuid) | |||
); | ); | |||
@Injectable() | @Injectable() | |||
export class ThemeEffects { | export class ThemeEffects { | |||
/** | /** | |||
* The list of configured themes | ||||
*/ | ||||
themes: Theme[]; | ||||
/** | ||||
* True if at least one theme depends on the route | ||||
*/ | ||||
hasDynamicTheme: boolean; | ||||
/** | ||||
* Initialize with a theme that doesn't depend on the route. | * Initialize with a theme that doesn't depend on the route. | |||
*/ | */ | |||
initTheme$ = createEffect(() => | initTheme$ = createEffect(() => | |||
this.actions$.pipe( | this.actions$.pipe( | |||
ofType(ROOT_EFFECTS_INIT), | ofType(ROOT_EFFECTS_INIT), | |||
map(() => { | map(() => { | |||
if (hasValue(DEFAULT_THEME_CONFIG)) { | if (hasValue(DEFAULT_THEME_CONFIG)) { | |||
return new SetThemeAction(DEFAULT_THEME_CONFIG.name); | return new SetThemeAction(DEFAULT_THEME_CONFIG.name); | |||
} else { | } else { | |||
return new SetThemeAction(BASE_THEME_NAME); | return new SetThemeAction(BASE_THEME_NAME); | |||
} | } | |||
}) | }) | |||
) | ) | |||
); | ); | |||
/** | ||||
* An effect that fires when a route change completes, | ||||
* and determines whether or not the theme should change | ||||
*/ | ||||
updateThemeOnRouteChange$ = createEffect(() => this.actions$.pipe( | ||||
// Listen for when a route change ends | ||||
ofType(ROUTER_NAVIGATED), | ||||
withLatestFrom( | ||||
// Pull in the latest resolved action, or undefined if none was dispatch | ||||
ed yet | ||||
this.actions$.pipe(ofType(ResolverActionTypes.RESOLVED), startWith(undef | ||||
ined)), | ||||
// and the current theme from the store | ||||
this.store.pipe(select(currentThemeSelector)) | ||||
), | ||||
switchMap(([navigatedAction, resolvedAction, currentTheme]: [RouterNavigat | ||||
edAction, ResolvedAction, string]) => { | ||||
if (this.hasDynamicTheme === true && isNotEmpty(this.themes)) { | ||||
const currentRouteUrl = navigatedAction.payload.routerState.url; | ||||
// If resolvedAction exists, and deals with the current url | ||||
if (hasValue(resolvedAction) && resolvedAction.payload.url === current | ||||
RouteUrl) { | ||||
// Start with the resolved dso and go recursively through its parent | ||||
s until you reach the top-level community | ||||
return observableOf(resolvedAction.payload.dso).pipe( | ||||
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()]; | ||||
}) | ||||
) | ||||
); | ||||
/** | ||||
* 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); | ||||
}); | ||||
} | ||||
/** | ||||
* 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() | ||||
); | ||||
} | ||||
constructor( | constructor( | |||
private actions$: Actions, | private actions$: Actions, | |||
private store: Store<ThemeState>, | ||||
private linkService: LinkService, | ||||
) { | ) { | |||
// 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) | ||||
); | ||||
} | } | |||
} | } | |||
End of changes. 6 change blocks. | ||||
172 lines changed or deleted | 2 lines changed or added |