From 39c2aa85eca5c94060bb4618c371a896f8b9e1de Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 18 Jul 2022 15:55:30 +0200 Subject: [PATCH 01/15] 93219: Move APP_INITIALIZER logic into InitService --- src/app/app.module.ts | 25 ++++------ src/app/init.service.ts | 63 +++++++++++++++++++++++++ src/modules/app/browser-app.module.ts | 38 +++------------ src/modules/app/browser-init.service.ts | 54 +++++++++++++++++++++ src/modules/app/server-app.module.ts | 26 ++-------- src/modules/app/server-init.service.ts | 46 ++++++++++++++++++ 6 files changed, 183 insertions(+), 69 deletions(-) create mode 100644 src/app/init.service.ts create mode 100644 src/modules/app/browser-init.service.ts create mode 100644 src/modules/app/server-init.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a1db89b60d..e3f5c46adb 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -7,13 +7,9 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EffectsModule } from '@ngrx/effects'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; -import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; +import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; -import { - DYNAMIC_ERROR_MESSAGES_MATCHER, - DYNAMIC_MATCHER_PROVIDERS, - DynamicErrorMessagesMatcher -} from '@ng-dynamic-forms/core'; +import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core'; import { TranslateModule } from '@ngx-translate/core'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { AppRoutingModule } from './app-routing.module'; @@ -21,7 +17,6 @@ import { AppComponent } from './app.component'; import { appEffects } from './app.effects'; import { appMetaReducers, debugMetaReducers } from './app.metareducers'; import { appReducers, AppState, storeModuleConfig } from './app.reducer'; -import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CoreModule } from './core/core.module'; import { ClientCookieService } from './core/services/client-cookie.service'; import { NavbarModule } from './navbar/navbar.module'; @@ -36,6 +31,7 @@ import { EagerThemesModule } from '../themes/eager-themes.module'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { RootModule } from './root.module'; +import { InitService } from './init.service'; export function getConfig() { return environment; @@ -82,6 +78,12 @@ IMPORTS.push( ); const PROVIDERS = [ + { + provide: APP_INITIALIZER, + useFactory: (initService: InitService) => initService.init(), + deps: [ InitService ], + multi: true, + }, { provide: APP_CONFIG, useFactory: getConfig @@ -101,15 +103,6 @@ const PROVIDERS = [ useClass: DSpaceRouterStateSerializer }, ClientCookieService, - // Check the authentication token when the app initializes - { - provide: APP_INITIALIZER, - useFactory: (store: Store,) => { - return () => store.dispatch(new CheckAuthenticationTokenAction()); - }, - deps: [Store], - multi: true - }, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/init.service.ts b/src/app/init.service.ts new file mode 100644 index 0000000000..c5bd6dddb0 --- /dev/null +++ b/src/app/init.service.ts @@ -0,0 +1,63 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Store } from '@ngrx/store'; +import { AppState } from './app.reducer'; +import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; +import { CorrelationIdService } from './correlation-id/correlation-id.service'; +import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; + +/** + * Performs the initialization of the app. + * + * Should have distinct extensions for server & browser for the specifics. + * Should be provided in AppModule as follows + * ``` + * { + * provide: APP_INITIALIZER + * useFactory: (initService: InitService) => initService.init(), + * deps: [ InitService ], + * multi: true, + * } + * ``` + * + * In order to be injected in the common APP_INITIALIZER, + * concrete subclasses should be provided in their respective app modules as + * ``` + * { provide: InitService, useClass: SpecificInitService } + * ``` + */ +export abstract class InitService { + protected constructor( + protected store: Store, + protected correlationIdService: CorrelationIdService, + protected dspaceTransferState: DSpaceTransferState, + ) { + } + + /** + * Main initialization method, to be used as the APP_INITIALIZER factory. + * + * Note that the body of this method and the callback it returns are called + * at different times. + * This is important to take into account when other providers depend on the + * initialization logic (e.g. APP_BASE_HREF) + */ + abstract init(): () => Promise; + + protected checkAuthenticationToken(): void { + this.store.dispatch(new CheckAuthenticationTokenAction()); + } + + protected initCorrelationId(): void { + this.correlationIdService.initCorrelationId(); + } + + protected async transferAppState(): Promise { + return this.dspaceTransferState.transfer(); + } +} diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 20c68898ae..e50018b51a 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { REQUEST } from '@nguniversal/express-engine/tokens'; @@ -13,7 +13,6 @@ import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module'; -import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { ClientCookieService } from '../../app/core/services/client-cookie.service'; import { CookieService } from '../../app/core/services/cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; @@ -23,21 +22,13 @@ import { StatisticsModule } from '../../app/statistics/statistics.module'; import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { HardRedirectService } from '../../app/core/services/hard-redirect.service'; -import { - BrowserHardRedirectService, - locationProvider, - LocationToken -} from '../../app/core/services/browser-hard-redirect.service'; +import { BrowserHardRedirectService, locationProvider, LocationToken } from '../../app/core/services/browser-hard-redirect.service'; import { LocaleService } from '../../app/core/locale/locale.service'; import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service'; -import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface'; -import { DefaultAppConfig } from '../../config/default-app-config'; -import { extendEnvironmentWithAppConfig } from '../../config/config.util'; -import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; - -import { environment } from '../../environments/environment'; +import { InitService } from 'src/app/init.service'; +import { BrowserInitService } from './browser-init.service'; export const REQ_KEY = makeStateKey('req'); @@ -73,25 +64,8 @@ export function getRequest(transferState: TransferState): any { ], providers: [ { - provide: APP_INITIALIZER, - useFactory: ( - transferState: TransferState, - dspaceTransferState: DSpaceTransferState, - correlationIdService: CorrelationIdService - ) => { - if (transferState.hasKey(APP_CONFIG_STATE)) { - const appConfig = transferState.get(APP_CONFIG_STATE, new DefaultAppConfig()); - // extend environment with app config for browser - extendEnvironmentWithAppConfig(environment, appConfig); - } - return () => - dspaceTransferState.transfer().then((b: boolean) => { - correlationIdService.initCorrelationId(); - return b; - }); - }, - deps: [TransferState, DSpaceTransferState, CorrelationIdService], - multi: true + provide: InitService, + useClass: BrowserInitService, }, { provide: REQUEST, diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts new file mode 100644 index 0000000000..1b050af82a --- /dev/null +++ b/src/modules/app/browser-init.service.ts @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { InitService } from '../../app/init.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../app/app.reducer'; +import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; +import { TransferState } from '@angular/platform-browser'; +import { APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; +import { DefaultAppConfig } from '../../config/default-app-config'; +import { extendEnvironmentWithAppConfig } from '../../config/config.util'; +import { environment } from '../../environments/environment'; +import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; +import { Injectable } from '@angular/core'; + +/** + * Performs client-side initialization. + */ +@Injectable() +export class BrowserInitService extends InitService { + constructor( + protected store: Store, + protected correlationIdService: CorrelationIdService, + protected transferState: TransferState, + protected dspaceTransferState: DSpaceTransferState, + ) { + super(store, correlationIdService, dspaceTransferState); + } + + public init(): () => Promise { + // this method must be called before the callback because APP_BASE_HREF depends on it + this.loadAppConfigFromSSR(); + + return async () => { + await this.transferAppState(); + this.checkAuthenticationToken(); + this.initCorrelationId(); + + return true; + }; + } + + private loadAppConfigFromSSR(): void { + if (this.transferState.hasKey(APP_CONFIG_STATE)) { + const appConfig = this.transferState.get(APP_CONFIG_STATE, new DefaultAppConfig()); + // extend environment with app config for browser + extendEnvironmentWithAppConfig(environment, appConfig); + } + } +} diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 4f8569962c..f61712ccb0 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -1,9 +1,8 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { BrowserModule, TransferState } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ServerModule } from '@angular/platform-server'; -import { RouterModule } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -14,7 +13,6 @@ import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module'; -import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader'; import { CookieService } from '../../app/core/services/cookie.service'; import { ServerCookieService } from '../../app/core/services/server-cookie.service'; @@ -32,10 +30,8 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock'; import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service'; -import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; -import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface'; - -import { environment } from '../../environments/environment'; +import { InitService } from '../../app/init.service'; +import { ServerInitService } from './server-init.service'; export function createTranslateLoader(transferState: TransferState) { return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5'); @@ -60,21 +56,9 @@ export function createTranslateLoader(transferState: TransferState) { AppModule ], providers: [ - // Initialize app config and extend environment { - provide: APP_INITIALIZER, - useFactory: ( - transferState: TransferState, - dspaceTransferState: DSpaceTransferState, - correlationIdService: CorrelationIdService, - ) => { - transferState.set(APP_CONFIG_STATE, environment as AppConfig); - dspaceTransferState.transfer(); - correlationIdService.initCorrelationId(); - return () => true; - }, - deps: [TransferState, DSpaceTransferState, CorrelationIdService], - multi: true + provide: InitService, + useClass: ServerInitService, }, { provide: Angulartics2, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts new file mode 100644 index 0000000000..d5814f10f3 --- /dev/null +++ b/src/modules/app/server-init.service.ts @@ -0,0 +1,46 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { InitService } from '../../app/init.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../app/app.reducer'; +import { TransferState } from '@angular/platform-browser'; +import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; +import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; +import { APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; +import { environment } from '../../environments/environment'; +import { Injectable } from '@angular/core'; + +/** + * Performs server-side initialization. + */ +@Injectable() +export class ServerInitService extends InitService { + constructor( + protected store: Store, + protected correlationIdService: CorrelationIdService, + protected transferState: TransferState, + protected dspaceTransferState: DSpaceTransferState, + ) { + super(store, correlationIdService, dspaceTransferState); + } + + public init(): () => Promise { + return async () => { + this.checkAuthenticationToken(); + this.saveAppConfigForCSR(); + this.transferAppState(); // todo: SSR breaks if we await this (why?) + this.initCorrelationId(); + + return true; + }; + } + + private saveAppConfigForCSR(): void { + this.transferState.set(APP_CONFIG_STATE, environment as AppConfig); + } +} From 372cddfd5e65387fe6714108e9711a6a6467d90e Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 19 Jul 2022 17:58:19 +0200 Subject: [PATCH 02/15] 93219: Support Router in InitService For Router to work properly, APP_BASE_HREF must be resolved _before_ the APP_INITIALIZER factory is called (otherwise Angular will attempt to initialize APP_BASE_HREF too soon) To fix this we add a pre-initialization hook to APP_CONFIG so BrowserInitService can resolve it before APP_INITIALIZER --- src/app/app.module.ts | 18 +---- src/app/init.service.spec.ts | 83 +++++++++++++++++++++++ src/app/init.service.ts | 87 ++++++++++++++++++------- src/config/app-config.interface.ts | 4 ++ src/modules/app/browser-app.module.ts | 7 +- src/modules/app/browser-init.service.ts | 21 +++--- src/modules/app/server-app.module.ts | 6 +- src/modules/app/server-init.service.ts | 2 +- 8 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 src/app/init.service.spec.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e3f5c46adb..b3a5872722 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,6 @@ import { APP_BASE_HREF, CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { AbstractControl } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; @@ -28,14 +28,8 @@ import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { LogInterceptor } from './core/log/log.interceptor'; import { EagerThemesModule } from '../themes/eager-themes.module'; - import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { RootModule } from './root.module'; -import { InitService } from './init.service'; - -export function getConfig() { - return environment; -} export function getBase(appConfig: AppConfig) { return appConfig.ui.nameSpace; @@ -78,16 +72,6 @@ IMPORTS.push( ); const PROVIDERS = [ - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [ InitService ], - multi: true, - }, - { - provide: APP_CONFIG, - useFactory: getConfig - }, { provide: APP_BASE_HREF, useFactory: getBase, diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts new file mode 100644 index 0000000000..7bbd50e4b7 --- /dev/null +++ b/src/app/init.service.spec.ts @@ -0,0 +1,83 @@ +import { InitService } from './init.service'; +import { APP_CONFIG } from 'src/config/app-config.interface'; +import { APP_INITIALIZER } from '@angular/core'; +import objectContaining = jasmine.objectContaining; +import createSpyObj = jasmine.createSpyObj; +import SpyObj = jasmine.SpyObj; + +let spy: SpyObj; + +export class ConcreteInitServiceMock extends InitService { + protected static resolveAppConfig() { + spy.resolveAppConfig(); + } + + protected init(): () => Promise { + spy.init(); + return async () => true; + } +} + + +describe('InitService', () => { + describe('providers', () => { + beforeEach(() => { + spy = createSpyObj('ConcreteInitServiceMock', { + resolveAppConfig: null, + init: null, + }); + }); + + it('should throw error when called on abstract InitService', () => { + expect(() => InitService.providers()).toThrow(); + }); + + it('should correctly set up provider dependencies', () => { + const providers = ConcreteInitServiceMock.providers(); + + expect(providers).toContain(objectContaining({ + provide: InitService, + useClass: ConcreteInitServiceMock + })); + + expect(providers).toContain(objectContaining({ + provide: APP_CONFIG, + })); + + expect(providers).toContain(objectContaining({ + provide: APP_INITIALIZER, + deps: [ InitService ], + multi: true, + })); + }); + + it('should call resolveAppConfig() in APP_CONFIG factory', () => { + const factory = ( + ConcreteInitServiceMock.providers() + .find((p: any) => p.provide === APP_CONFIG) as any + ).useFactory; + + // this factory is called _before_ InitService is instantiated + factory(); + expect(spy.resolveAppConfig).toHaveBeenCalled(); + expect(spy.init).not.toHaveBeenCalled(); + }); + + it('should defer to init() in APP_INITIALIZER factory', () => { + const factory = ( + ConcreteInitServiceMock.providers() + .find((p: any) => p.provide === APP_INITIALIZER) as any + ).useFactory; + + // we don't care about the dependencies here + // @ts-ignore + const instance = new ConcreteInitServiceMock(null, null, null); + + // provider ensures that the right concrete instance is passed to the factory + factory(instance); + expect(spy.resolveAppConfig).not.toHaveBeenCalled(); + expect(spy.init).toHaveBeenCalled(); + }); + }); +}); + diff --git a/src/app/init.service.ts b/src/app/init.service.ts index c5bd6dddb0..d8ecf8d23a 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -6,30 +6,18 @@ * http://www.dspace.org/license/ */ import { Store } from '@ngrx/store'; -import { AppState } from './app.reducer'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; +import { APP_INITIALIZER, Provider, Type } from '@angular/core'; +import { TransferState } from '@angular/platform-browser'; +import { APP_CONFIG } from '../config/app-config.interface'; +import { environment } from '../environments/environment'; +import { AppState } from './app.reducer'; /** * Performs the initialization of the app. - * - * Should have distinct extensions for server & browser for the specifics. - * Should be provided in AppModule as follows - * ``` - * { - * provide: APP_INITIALIZER - * useFactory: (initService: InitService) => initService.init(), - * deps: [ InitService ], - * multi: true, - * } - * ``` - * - * In order to be injected in the common APP_INITIALIZER, - * concrete subclasses should be provided in their respective app modules as - * ``` - * { provide: InitService, useClass: SpecificInitService } - * ``` + * Should be extended to implement server- & browser-specific functionality. */ export abstract class InitService { protected constructor( @@ -40,14 +28,63 @@ export abstract class InitService { } /** - * Main initialization method, to be used as the APP_INITIALIZER factory. - * - * Note that the body of this method and the callback it returns are called - * at different times. - * This is important to take into account when other providers depend on the - * initialization logic (e.g. APP_BASE_HREF) + * The initialization providers to use in `*AppModule` + * - this concrete {@link InitService} + * - {@link APP_CONFIG} with optional pre-initialization hook + * - {@link APP_INITIALIZER} + *
+ * Should only be called on concrete subclasses of InitService for the initialization hooks to work */ - abstract init(): () => Promise; + public static providers(): Provider[] { + if (!InitService.isPrototypeOf(this)) { + throw new Error( + 'Initalization providers should only be generated from concrete subclasses of InitService' + ); + } + return [ + { + provide: InitService, + useClass: this as unknown as Type, + }, + { + provide: APP_CONFIG, + useFactory: (transferState: TransferState) => { + this.resolveAppConfig(transferState); + return environment; + }, + deps: [ TransferState ] + }, + { + provide: APP_INITIALIZER, + useFactory: (initService: InitService) => initService.init(), + deps: [ InitService ], + multi: true, + }, + ]; + } + + /** + * Optional pre-initialization method to ensure that {@link APP_CONFIG} is fully resolved before {@link init} is called. + * + * For example, Router depends on APP_BASE_HREF, which in turn depends on APP_CONFIG. + * In production mode, APP_CONFIG is resolved from the TransferState when the app is initialized. + * If we want to use Router within APP_INITIALIZER, we have to make sure APP_BASE_HREF is resolved beforehand. + * In this case that means that we must transfer the configuration from the SSR state during pre-initialization. + * @protected + */ + protected static resolveAppConfig( + transferState: TransferState + ): void { + // overriden in subclasses if applicable + } + + /** + * Main initialization method. + * @protected + */ + protected abstract init(): () => Promise; + + // Common initialization steps protected checkAuthenticationToken(): void { this.store.dispatch(new CheckAuthenticationTokenAction()); diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 62d0be7216..5b2f0b1eeb 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -36,6 +36,10 @@ interface AppConfig extends Config { mediaViewer: MediaViewerConfig; } +/** + * Injection token for the app configuration. + * Provided in {@link InitService.providers}. + */ const APP_CONFIG = new InjectionToken('APP_CONFIG'); const APP_CONFIG_STATE = makeStateKey('APP_CONFIG_STATE'); diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index e50018b51a..cd3feedad8 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -27,8 +27,8 @@ import { LocaleService } from '../../app/core/locale/locale.service'; import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service'; -import { InitService } from 'src/app/init.service'; import { BrowserInitService } from './browser-init.service'; +import { InitService } from '../../app/init.service'; export const REQ_KEY = makeStateKey('req'); @@ -63,10 +63,7 @@ export function getRequest(transferState: TransferState): any { AppModule ], providers: [ - { - provide: InitService, - useClass: BrowserInitService, - }, + ...BrowserInitService.providers(), { provide: REQUEST, useFactory: getRequest, diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 1b050af82a..5c55795383 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -31,10 +31,17 @@ export class BrowserInitService extends InitService { super(store, correlationIdService, dspaceTransferState); } - public init(): () => Promise { - // this method must be called before the callback because APP_BASE_HREF depends on it - this.loadAppConfigFromSSR(); + protected static resolveAppConfig( + transferState: TransferState, + ) { + if (transferState.hasKey(APP_CONFIG_STATE)) { + const appConfig = transferState.get(APP_CONFIG_STATE, new DefaultAppConfig()); + // extend environment with app config for browser + extendEnvironmentWithAppConfig(environment, appConfig); + } + } + protected init(): () => Promise { return async () => { await this.transferAppState(); this.checkAuthenticationToken(); @@ -43,12 +50,4 @@ export class BrowserInitService extends InitService { return true; }; } - - private loadAppConfigFromSSR(): void { - if (this.transferState.hasKey(APP_CONFIG_STATE)) { - const appConfig = this.transferState.get(APP_CONFIG_STATE, new DefaultAppConfig()); - // extend environment with app config for browser - extendEnvironmentWithAppConfig(environment, appConfig); - } - } } diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index f61712ccb0..2b0462e9a0 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -30,7 +30,6 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock'; import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service'; -import { InitService } from '../../app/init.service'; import { ServerInitService } from './server-init.service'; export function createTranslateLoader(transferState: TransferState) { @@ -56,10 +55,7 @@ export function createTranslateLoader(transferState: TransferState) { AppModule ], providers: [ - { - provide: InitService, - useClass: ServerInitService, - }, + ...ServerInitService.providers(), { provide: Angulartics2, useClass: Angulartics2Mock diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index d5814f10f3..11fcc482ca 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -29,7 +29,7 @@ export class ServerInitService extends InitService { super(store, correlationIdService, dspaceTransferState); } - public init(): () => Promise { + protected init(): () => Promise { return async () => { this.checkAuthenticationToken(); this.saveAppConfigForCSR(); From 5cb737c7f236a546d8b3d63b042cdc9d346927bb Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 22 Jul 2022 10:29:44 +0200 Subject: [PATCH 03/15] 93219: Move general initialization from AppComponent to InitService --- src/app/app.component.spec.ts | 46 +----- src/app/app.component.ts | 75 +-------- src/app/core/auth/auth.service.ts | 6 +- src/app/init.service.spec.ts | 192 +++++++++++++++++++++++- src/app/init.service.ts | 124 ++++++++++++++- src/modules/app/browser-init.service.ts | 52 ++++++- src/modules/app/server-init.service.ts | 44 +++++- 7 files changed, 416 insertions(+), 123 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a892e34a5a..4cf3f3b6e7 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -32,7 +32,6 @@ import { storeModuleConfig } from './app.reducer'; import { LocaleService } from './core/locale/locale.service'; import { authReducer } from './core/auth/auth.reducer'; import { provideMockStore } from '@ngrx/store/testing'; -import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { ThemeService } from './shared/theme-support/theme.service'; import { getMockThemeService } from './shared/mocks/theme-service.mock'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; @@ -46,16 +45,16 @@ const initialState = { core: { auth: { loading: false } } }; +export function getMockLocaleService(): LocaleService { + return jasmine.createSpyObj('LocaleService', { + setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode') + }); +} + describe('App component', () => { let breadcrumbsServiceSpy; - function getMockLocaleService(): LocaleService { - return jasmine.createSpyObj('LocaleService', { - setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode') - }); - } - const getDefaultTestBedConf = () => { breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']); @@ -131,39 +130,6 @@ describe('App component', () => { }); - describe('the constructor', () => { - it('should call breadcrumbsService.listenForRouteChanges', () => { - expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1); - }); - }); - - describe('when GoogleAnalyticsService is provided', () => { - let googleAnalyticsSpy; - - beforeEach(() => { - // NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset - TestBed.resetTestingModule(); - TestBed.configureTestingModule(getDefaultTestBedConf()); - googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ - 'addTrackingIdToPage', - ]); - TestBed.overrideProvider(GoogleAnalyticsService, {useValue: googleAnalyticsSpy}); - fixture = TestBed.createComponent(AppComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create component', () => { - expect(comp).toBeTruthy(); - }); - - describe('the constructor', () => { - it('should call googleAnalyticsService.addTrackingIdToPage()', () => { - expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); - }); - }); - }); - describe('when ThemeService returns a custom theme', () => { let document; let headSpy; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ea2fb9fde6..c7b99b42a8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -18,36 +18,24 @@ import { Router, } from '@angular/router'; -import { isEqual } from 'lodash'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; -import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; - -import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/search/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { isAuthenticationBlocking } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; -import { MenuService } from './shared/menu/menu.service'; -import { HostWindowService } from './shared/host-window.service'; -import { HeadTagConfig, ThemeConfig } from '../config/theme.model'; -import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { HeadTagConfig } from '../config/theme.model'; import { environment } from '../environments/environment'; import { models } from './core/core.module'; -import { LocaleService } from './core/locale/locale.service'; import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util'; -import { KlaroService } from './shared/cookies/klaro.service'; -import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { ThemeService } from './shared/theme-support/theme.service'; import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; -import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { getDefaultThemeConfig } from '../config/config.util'; -import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; @Component({ selector: 'ds-app', @@ -56,11 +44,6 @@ import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent implements OnInit, AfterViewInit { - sidebarVisible: Observable; - slideSidebarOver: Observable; - collapsedSidebarWidth: Observable; - totalSidebarWidth: Observable; - theme: Observable = of({} as any); notificationOptions; models; @@ -90,29 +73,14 @@ export class AppComponent implements OnInit, AfterViewInit { @Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(DOCUMENT) private document: any, @Inject(PLATFORM_ID) private platformId: any, - @Inject(APP_CONFIG) private appConfig: AppConfig, private themeService: ThemeService, private translate: TranslateService, private store: Store, - private metadata: MetadataService, - private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, - private angulartics2DSpace: Angulartics2DSpace, private authService: AuthService, private router: Router, private cssService: CSSVariableService, - private menuService: MenuService, - private windowService: HostWindowService, - private localeService: LocaleService, - private breadcrumbsService: BreadcrumbsService, private modalService: NgbModal, - @Optional() private cookiesService: KlaroService, - @Optional() private googleAnalyticsService: GoogleAnalyticsService, ) { - - if (!isEqual(environment, this.appConfig)) { - throw new Error('environment does not match app config!'); - } - this.notificationOptions = environment.notifications; /* Use models object so all decorators are actually called */ @@ -136,47 +104,18 @@ export class AppComponent implements OnInit, AfterViewInit { }); if (isPlatformBrowser(this.platformId)) { - this.authService.trackTokenExpiration(); this.trackIdleModal(); } - // Load all the languages that are defined as active from the config file - translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); - - // Load the default language from the config file - // translate.setDefaultLang(environment.defaultLanguage); - - // set the current language code - this.localeService.setCurrentLanguageCode(); - - // analytics - if (hasValue(googleAnalyticsService)) { - googleAnalyticsService.addTrackingIdToPage(); - } - angulartics2DSpace.startTracking(); - - metadata.listenForRouteChange(); - breadcrumbsService.listenForRouteChanges(); - - if (environment.debug) { - console.info(environment); - } this.storeCSSVariables(); } ngOnInit() { - this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( + this.isAuthBlocking$ = this.store.pipe( + select(isAuthenticationBlocking), distinctUntilChanged() ); - this.isAuthBlocking$ - .pipe( - filter((isBlocking: boolean) => isBlocking === false), - take(1) - ).subscribe(() => this.initializeKlaro()); - const env: string = environment.production ? 'Production' : 'Development'; - const color: string = environment.production ? 'red' : 'green'; - console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); } @@ -239,12 +178,6 @@ export class AppComponent implements OnInit, AfterViewInit { ); } - private initializeKlaro() { - if (hasValue(this.cookiesService)) { - this.cookiesService.initialize(); - } - } - private loadGlobalThemeConfig(themeName: string): void { this.setThemeCss(themeName); this.setHeadTags(themeName); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 5738948ebd..95b20a1142 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { Observable, of as observableOf } from 'rxjs'; -import { map, startWith, switchMap, take } from 'rxjs/operators'; +import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; @@ -88,6 +88,8 @@ export class AuthService { private translateService: TranslateService ) { this.store.pipe( + // when this service is constructed the store is not fully initialized yet + filter((state: any) => state?.core?.auth !== undefined), select(isAuthenticated), startWith(false) ).subscribe((authenticated: boolean) => this._authenticated = authenticated); @@ -325,7 +327,7 @@ export class AuthService { let token: AuthTokenInfo; let currentlyRefreshingToken = false; this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => { - // If new token is undefined an it wasn't previously => Refresh failed + // If new token is undefined and it wasn't previously => Refresh failed if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) { // Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed')); diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts index 7bbd50e4b7..7fb555f23c 100644 --- a/src/app/init.service.spec.ts +++ b/src/app/init.service.spec.ts @@ -1,12 +1,42 @@ import { InitService } from './init.service'; import { APP_CONFIG } from 'src/config/app-config.interface'; -import { APP_INITIALIZER } from '@angular/core'; +import { APP_INITIALIZER, Injectable } from '@angular/core'; +import { inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { GoogleAnalyticsService } from './statistics/google-analytics.service'; +import { MetadataService } from './core/metadata/metadata.service'; +import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; +import { CommonModule } from '@angular/common'; +import { Store, StoreModule } from '@ngrx/store'; +import { authReducer } from './core/auth/auth.reducer'; +import { storeModuleConfig } from './app.reducer'; +import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock'; +import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { AuthService } from './core/auth/auth.service'; +import { AuthServiceMock } from './shared/mocks/auth.service.mock'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterMock } from './shared/mocks/router.mock'; +import { MockActivatedRoute } from './shared/mocks/active-router.mock'; +import { MenuService } from './shared/menu/menu.service'; +import { LocaleService } from './core/locale/locale.service'; +import { environment } from '../environments/environment'; +import { provideMockStore } from '@ngrx/store/testing'; +import { AppComponent } from './app.component'; +import { RouteService } from './core/services/route.service'; +import { getMockLocaleService } from './app.component.spec'; +import { MenuServiceStub } from './shared/testing/menu-service.stub'; +import { CorrelationIdService } from './correlation-id/correlation-id.service'; +import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; +import { KlaroService } from './shared/cookies/klaro.service'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock'; +import { getTestScheduler } from 'jasmine-marbles'; import objectContaining = jasmine.objectContaining; import createSpyObj = jasmine.createSpyObj; import SpyObj = jasmine.SpyObj; let spy: SpyObj; +@Injectable() export class ConcreteInitServiceMock extends InitService { protected static resolveAppConfig() { spy.resolveAppConfig(); @@ -18,6 +48,15 @@ export class ConcreteInitServiceMock extends InitService { } } +const initialState = { + core: { + auth: { + loading: false, + blocking: true, + } + } +}; + describe('InitService', () => { describe('providers', () => { @@ -79,5 +118,156 @@ describe('InitService', () => { expect(spy.init).toHaveBeenCalled(); }); }); + + describe('common initialization steps', () => { + let correlationIdServiceSpy; + let dspaceTransferStateSpy; + let transferStateSpy; + let metadataServiceSpy; + let breadcrumbsServiceSpy; + + beforeEach(waitForAsync(() => { + correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [ + 'initCorrelationId', + ]); + dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ + 'transfer', + ]); + transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ + 'get', 'hasKey' + ]); + breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [ + 'listenForRouteChanges', + ]); + metadataServiceSpy = jasmine.createSpyObj('metadataService', [ + 'listenForRouteChange', + ]); + + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot(authReducer, storeModuleConfig), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + providers: [ + { provide: InitService, useClass: ConcreteInitServiceMock }, + { provide: CorrelationIdService, useValue: correlationIdServiceSpy }, + { provide: DSpaceTransferState, useValue: dspaceTransferStateSpy }, + { provide: APP_CONFIG, useValue: environment }, + { provide: LocaleService, useValue: getMockLocaleService() }, + { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, + { provide: MetadataService, useValue: metadataServiceSpy }, + { provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: Router, useValue: new RouterMock() }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + { provide: MenuService, useValue: new MenuServiceStub() }, + { provide: KlaroService, useValue: undefined }, + { provide: GoogleAnalyticsService, useValue: undefined }, + provideMockStore({ initialState }), + AppComponent, + RouteService, + ] + }); + })); + + describe('initÀnalytics', () => { + describe('when GoogleAnalyticsService is provided', () => { + let googleAnalyticsSpy; + + beforeEach(() => { + googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ + 'addTrackingIdToPage', + ]); + + TestBed.overrideProvider(GoogleAnalyticsService, { useValue: googleAnalyticsSpy }); + }); + + it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => { + // @ts-ignore + service.initAnalytics(); + expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); + })); + }); + }); + + describe('initRouteListeners', () => { + it('should call listenForRouteChanges', inject([InitService], (service) => { + // @ts-ignore + service.initRouteListeners(); + expect(metadataServiceSpy.listenForRouteChange).toHaveBeenCalledTimes(1); + expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1); + })); + }); + + describe('initKlaro', () => { + const BLOCKING = { + t: { core: { auth: { blocking: true } } }, + f: { core: { auth: { blocking: false } } }, + }; + + it('should not error out if KlaroService is not provided', inject([InitService], (service) => { + // @ts-ignore + service.initKlaro(); + })); + + describe('when KlaroService is provided', () => { + let klaroServiceSpy; + + beforeEach(() => { + klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [ + 'initialize', + ]); + + TestBed.overrideProvider(KlaroService, { useValue: klaroServiceSpy }); + }); + + it('should not initialize Klaro while auth is blocking', () => { + getTestScheduler().run(({ cold, flush}) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + service.initKlaro(); + flush(); + expect(klaroServiceSpy.initialize).not.toHaveBeenCalled(); + }); + }); + + + it('should only initialize Klaro the first time auth is unblocked', () => { + getTestScheduler().run(({ cold, flush}) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + service.initKlaro(); + flush(); + expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('when KlaroService is not provided', () => { + it('should not error out when auth is unblocked', () => { + getTestScheduler().run(({ cold, flush}) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + service.initKlaro(); + flush(); + }); + }); + }); + }); + }); }); diff --git a/src/app/init.service.ts b/src/app/init.service.ts index d8ecf8d23a..e5b04163c0 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -5,25 +5,50 @@ * * http://www.dspace.org/license/ */ -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; -import { APP_INITIALIZER, Provider, Type } from '@angular/core'; +import { APP_INITIALIZER, Inject, Optional, Provider, Type } from '@angular/core'; import { TransferState } from '@angular/platform-browser'; -import { APP_CONFIG } from '../config/app-config.interface'; +import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { environment } from '../environments/environment'; import { AppState } from './app.reducer'; +import { isEqual } from 'lodash'; +import { TranslateService } from '@ngx-translate/core'; +import { LocaleService } from './core/locale/locale.service'; +import { hasValue } from './shared/empty.util'; +import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { GoogleAnalyticsService } from './statistics/google-analytics.service'; +import { MetadataService } from './core/metadata/metadata.service'; +import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; +import { distinctUntilChanged, filter, take, tap } from 'rxjs/operators'; +import { isAuthenticationBlocking } from './core/auth/selectors'; +import { KlaroService } from './shared/cookies/klaro.service'; /** * Performs the initialization of the app. + * * Should be extended to implement server- & browser-specific functionality. + * Initialization steps shared between the server and brower implementations + * can be included in this class. + * + * Note that the service cannot (indirectly) depend on injection tokens that are only available _after_ APP_INITIALIZER. + * For example, NgbModal depends on ApplicationRef and can therefore not be used during initialization. */ export abstract class InitService { protected constructor( protected store: Store, protected correlationIdService: CorrelationIdService, protected dspaceTransferState: DSpaceTransferState, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + protected translate: TranslateService, + protected localeService: LocaleService, + protected angulartics2DSpace: Angulartics2DSpace, + @Optional() protected googleAnalyticsService: GoogleAnalyticsService, + protected metadata: MetadataService, + protected breadcrumbsService: BreadcrumbsService, + @Optional() protected klaroService: KlaroService, ) { } @@ -86,15 +111,108 @@ export abstract class InitService { // Common initialization steps + /** + * Dispatch a {@link CheckAuthenticationTokenAction} to start off the chain of + * actions used to determine whether a user is already logged in. + * @protected + */ protected checkAuthenticationToken(): void { this.store.dispatch(new CheckAuthenticationTokenAction()); } + /** + * Initialize the correlation ID (from cookie, NgRx store or random) + * @protected + */ protected initCorrelationId(): void { this.correlationIdService.initCorrelationId(); } + /** + * Transfer the application's NgRx state between server-side and client-side + * @protected + */ protected async transferAppState(): Promise { return this.dspaceTransferState.transfer(); } + + /** + * Make sure the {@link environment} matches {@link APP_CONFIG} and print + * some information about it to the console + * @protected + */ + protected checkEnvironment(): void { + if (!isEqual(environment, this.appConfig)) { + throw new Error('environment does not match app config!'); + } + + if (environment.debug) { + console.info(environment); + } + + const env: string = environment.production ? 'Production' : 'Development'; + const color: string = environment.production ? 'red' : 'green'; + console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); + } + + /** + * Initialize internationalization services + * - Specify the active languages + * - Set the current locale + * @protected + */ + protected initI18n(): void { + // Load all the languages that are defined as active from the config file + this.translate.addLangs( + environment.languages + .filter((LangConfig) => LangConfig.active === true) + .map((a) => a.code) + ); + + // Load the default language from the config file + // translate.setDefaultLang(environment.defaultLanguage); + + this.localeService.setCurrentLanguageCode(); + } + + /** + * Initialize analytics services + * - Angulartics + * - Google Analytics (if enabled) + * @protected + */ + protected initAnalytics(): void { + if (hasValue(this.googleAnalyticsService)) { + this.googleAnalyticsService.addTrackingIdToPage(); + } + this.angulartics2DSpace.startTracking(); + } + + /** + * Start route-listening subscriptions + * - {@link MetadataService.listenForRouteChange} + * - {@link BreadcrumbsService.listenForRouteChanges} + * @protected + */ + protected initRouteListeners(): void { + this.metadata.listenForRouteChange(); + this.breadcrumbsService.listenForRouteChanges(); + } + + /** + * Initialize Klaro (if enabled) + * @protected + */ + protected initKlaro() { + if (hasValue(this.klaroService)) { + this.store.pipe( + select(isAuthenticationBlocking), + distinctUntilChanged(), + filter((isBlocking: boolean) => isBlocking === false), + take(1) + ).subscribe(() => { + this.klaroService.initialize(); + }); + } + } } diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 5c55795383..f675c55718 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -10,12 +10,21 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { TransferState } from '@angular/platform-browser'; -import { APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; +import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; import { DefaultAppConfig } from '../../config/default-app-config'; import { extendEnvironmentWithAppConfig } from '../../config/config.util'; import { environment } from '../../environments/environment'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; -import { Injectable } from '@angular/core'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { LocaleService } from '../../app/core/locale/locale.service'; +import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; +import { MetadataService } from '../../app/core/metadata/metadata.service'; +import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; +import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; +import { KlaroService } from '../../app/shared/cookies/klaro.service'; +import { AuthService } from '../../app/core/auth/auth.service'; /** * Performs client-side initialization. @@ -27,8 +36,30 @@ export class BrowserInitService extends InitService { protected correlationIdService: CorrelationIdService, protected transferState: TransferState, protected dspaceTransferState: DSpaceTransferState, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + protected translate: TranslateService, + protected localeService: LocaleService, + protected angulartics2DSpace: Angulartics2DSpace, + @Optional() protected googleAnalyticsService: GoogleAnalyticsService, + protected metadata: MetadataService, + protected breadcrumbsService: BreadcrumbsService, + protected cssService: CSSVariableService, + @Optional() protected klaroService: KlaroService, + protected authService: AuthService, ) { - super(store, correlationIdService, dspaceTransferState); + super( + store, + correlationIdService, + dspaceTransferState, + appConfig, + translate, + localeService, + angulartics2DSpace, + googleAnalyticsService, + metadata, + breadcrumbsService, + klaroService, + ); } protected static resolveAppConfig( @@ -47,7 +78,22 @@ export class BrowserInitService extends InitService { this.checkAuthenticationToken(); this.initCorrelationId(); + this.checkEnvironment(); + + this.initI18n(); + this.initAnalytics(); + this.initRouteListeners(); + this.trackAuthTokenExpiration(); + + this.initKlaro(); + return true; }; } + + // Browser-only initialization steps + + private trackAuthTokenExpiration(): void { + this.authService.trackTokenExpiration(); + } } diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 11fcc482ca..fc7fc66bf7 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -11,9 +11,17 @@ import { AppState } from '../../app/app.reducer'; import { TransferState } from '@angular/platform-browser'; import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; -import { APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; +import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; import { environment } from '../../environments/environment'; -import { Injectable } from '@angular/core'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { LocaleService } from '../../app/core/locale/locale.service'; +import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; +import { MetadataService } from '../../app/core/metadata/metadata.service'; +import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; +import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; +import { KlaroService } from '../../app/shared/cookies/klaro.service'; /** * Performs server-side initialization. @@ -25,8 +33,29 @@ export class ServerInitService extends InitService { protected correlationIdService: CorrelationIdService, protected transferState: TransferState, protected dspaceTransferState: DSpaceTransferState, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + protected translate: TranslateService, + protected localeService: LocaleService, + protected angulartics2DSpace: Angulartics2DSpace, + @Optional() protected googleAnalyticsService: GoogleAnalyticsService, + protected metadata: MetadataService, + protected breadcrumbsService: BreadcrumbsService, + protected cssService: CSSVariableService, + @Optional() protected klaroService: KlaroService, ) { - super(store, correlationIdService, dspaceTransferState); + super( + store, + correlationIdService, + dspaceTransferState, + appConfig, + translate, + localeService, + angulartics2DSpace, + googleAnalyticsService, + metadata, + breadcrumbsService, + klaroService, + ); } protected init(): () => Promise { @@ -36,10 +65,19 @@ export class ServerInitService extends InitService { this.transferAppState(); // todo: SSR breaks if we await this (why?) this.initCorrelationId(); + this.checkEnvironment(); + this.initI18n(); + this.initAnalytics(); + this.initRouteListeners(); + + this.initKlaro(); + return true; }; } + // Server-only initialization steps + private saveAppConfigForCSR(): void { this.transferState.set(APP_CONFIG_STATE, environment as AppConfig); } From bdc004f64dbeae554e97cb45158e90bf5ac7b1f3 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 22 Jul 2022 12:12:21 +0200 Subject: [PATCH 04/15] 93219: Move theme/route subscriptions from AppComponent to ThemeService --- src/app/app.component.spec.ts | 31 +-- src/app/app.component.ts | 183 +----------------- src/app/core/shared/distinct-next.ts | 18 ++ src/app/init.service.spec.ts | 3 + src/app/init.service.ts | 4 + src/app/navbar/navbar.component.html | 4 +- src/app/shared/mocks/theme-service.mock.ts | 1 + .../theme-support/theme.service.spec.ts | 57 +++++- src/app/shared/theme-support/theme.service.ts | 176 ++++++++++++++++- src/modules/app/browser-init.service.ts | 4 + src/modules/app/server-init.service.ts | 4 + 11 files changed, 268 insertions(+), 217 deletions(-) create mode 100644 src/app/core/shared/distinct-next.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 4cf3f3b6e7..0e86beeedb 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,7 +1,7 @@ import { Store, StoreModule } from '@ngrx/store'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { CommonModule, DOCUMENT } from '@angular/common'; +import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; @@ -129,33 +129,4 @@ describe('App component', () => { }); }); - - describe('when ThemeService returns a custom theme', () => { - let document; - let headSpy; - - beforeEach(() => { - // NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset - TestBed.resetTestingModule(); - TestBed.configureTestingModule(getDefaultTestBedConf()); - TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')}); - document = TestBed.inject(DOCUMENT); - headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']); - headSpy.getElementsByClassName.and.returnValue([]); - spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]); - fixture = TestBed.createComponent(AppComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should append a link element with the correct attributes to the head element', () => { - const link = document.createElement('link'); - link.setAttribute('rel', 'stylesheet'); - link.setAttribute('type', 'text/css'); - link.setAttribute('class', 'theme-css'); - link.setAttribute('href', '/custom-theme.css'); - - expect(headSpy.appendChild).toHaveBeenCalledWith(link); - }); - }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c7b99b42a8..98e91c9c31 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, take, withLatestFrom } from 'rxjs/operators'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { AfterViewInit, @@ -7,14 +7,12 @@ import { HostListener, Inject, OnInit, - Optional, PLATFORM_ID, } from '@angular/core'; import { - ActivatedRouteSnapshot, NavigationCancel, NavigationEnd, - NavigationStart, ResolveEnd, + NavigationStart, Router, } from '@angular/router'; @@ -28,14 +26,11 @@ import { NativeWindowRef, NativeWindowService } from './core/services/window.ser import { isAuthenticationBlocking } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; -import { HeadTagConfig } from '../config/theme.model'; import { environment } from '../environments/environment'; import { models } from './core/core.module'; -import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util'; import { ThemeService } from './shared/theme-support/theme.service'; -import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; -import { getDefaultThemeConfig } from '../config/config.util'; +import { distinctNext } from './core/shared/distinct-next'; @Component({ selector: 'ds-app', @@ -60,9 +55,7 @@ export class AppComponent implements OnInit, AfterViewInit { /** * Whether or not the theme is in the process of being swapped */ - isThemeLoading$: BehaviorSubject = new BehaviorSubject(false); - - isThemeCSSLoading$: BehaviorSubject = new BehaviorSubject(false); + isThemeLoading$: Observable; /** * Whether or not the idle modal is is currently open @@ -86,27 +79,12 @@ export class AppComponent implements OnInit, AfterViewInit { /* Use models object so all decorators are actually called */ this.models = models; - this.themeService.getThemeName$().subscribe((themeName: string) => { - if (isPlatformBrowser(this.platformId)) { - // the theme css will never download server side, so this should only happen on the browser - this.distinctNext(this.isThemeCSSLoading$, true); - } - if (hasValue(themeName)) { - this.loadGlobalThemeConfig(themeName); - } else { - const defaultThemeConfig = getDefaultThemeConfig(); - if (hasValue(defaultThemeConfig)) { - this.loadGlobalThemeConfig(defaultThemeConfig.name); - } else { - this.loadGlobalThemeConfig(BASE_THEME_NAME); - } - } - }); - if (isPlatformBrowser(this.platformId)) { this.trackIdleModal(); } + this.isThemeLoading$ = this.themeService.isThemeLoading$; + this.storeCSSVariables(); } @@ -135,34 +113,14 @@ export class AppComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - let resolveEndFound = false; this.router.events.subscribe((event) => { if (event instanceof NavigationStart) { - resolveEndFound = false; - this.distinctNext(this.isRouteLoading$, true); - this.distinctNext(this.isThemeLoading$, true); - } else if (event instanceof ResolveEnd) { - resolveEndFound = true; - const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root; - this.themeService.updateThemeOnRouteChange$(event.urlAfterRedirects, activatedRouteSnapShot).pipe( - switchMap((changed) => { - if (changed) { - return this.isThemeCSSLoading$; - } else { - return [false]; - } - }) - ).subscribe((changed) => { - this.distinctNext(this.isThemeLoading$, changed); - }); + distinctNext(this.isRouteLoading$, true); } else if ( event instanceof NavigationEnd || event instanceof NavigationCancel ) { - if (!resolveEndFound) { - this.distinctNext(this.isThemeLoading$, false); - } - this.distinctNext(this.isRouteLoading$, false); + distinctNext(this.isRouteLoading$, false); } }); } @@ -178,119 +136,6 @@ export class AppComponent implements OnInit, AfterViewInit { ); } - private loadGlobalThemeConfig(themeName: string): void { - this.setThemeCss(themeName); - this.setHeadTags(themeName); - } - - /** - * Update the theme css file in - * - * @param themeName The name of the new theme - * @private - */ - private setThemeCss(themeName: string): void { - const head = this.document.getElementsByTagName('head')[0]; - if (hasNoValue(head)) { - return; - } - - // Array.from to ensure we end up with an array, not an HTMLCollection, which would be - // automatically updated if we add nodes later - const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css')); - const link = this.document.createElement('link'); - link.setAttribute('rel', 'stylesheet'); - link.setAttribute('type', 'text/css'); - link.setAttribute('class', 'theme-css'); - link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); - // wait for the new css to download before removing the old one to prevent a - // flash of unstyled content - link.onload = () => { - if (isNotEmpty(currentThemeLinks)) { - currentThemeLinks.forEach((currentThemeLink: any) => { - if (hasValue(currentThemeLink)) { - currentThemeLink.remove(); - } - }); - } - // the fact that this callback is used, proves we're on the browser. - this.distinctNext(this.isThemeCSSLoading$, false); - }; - head.appendChild(link); - } - - private setHeadTags(themeName: string): void { - const head = this.document.getElementsByTagName('head')[0]; - if (hasNoValue(head)) { - return; - } - - // clear head tags - const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag')); - if (hasValue(currentHeadTags)) { - currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove()); - } - - // create new head tags (not yet added to DOM) - const headTagFragment = this.document.createDocumentFragment(); - this.createHeadTags(themeName) - .forEach(newHeadTag => headTagFragment.appendChild(newHeadTag)); - - // add new head tags to DOM - head.appendChild(headTagFragment); - } - - private createHeadTags(themeName: string): HTMLElement[] { - const themeConfig = this.themeService.getThemeConfigFor(themeName); - const headTagConfigs = themeConfig?.headTags; - - if (hasNoValue(headTagConfigs)) { - const parentThemeName = themeConfig?.extends; - if (hasValue(parentThemeName)) { - // inherit the head tags of the parent theme - return this.createHeadTags(parentThemeName); - } - const defaultThemeConfig = getDefaultThemeConfig(); - const defaultThemeName = defaultThemeConfig.name; - if ( - hasNoValue(defaultThemeName) || - themeName === defaultThemeName || - themeName === BASE_THEME_NAME - ) { - // last resort, use fallback favicon.ico - return [ - this.createHeadTag({ - 'tagName': 'link', - 'attributes': { - 'rel': 'icon', - 'href': 'assets/images/favicon.ico', - 'sizes': 'any', - } - }) - ]; - } - - // inherit the head tags of the default theme - return this.createHeadTags(defaultThemeConfig.name); - } - - return headTagConfigs.map(this.createHeadTag.bind(this)); - } - - private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement { - const tag = this.document.createElement(headTagConfig.tagName); - - if (hasValue(headTagConfig.attributes)) { - Object.entries(headTagConfig.attributes) - .forEach(([key, value]) => tag.setAttribute(key, value)); - } - - // 'class' attribute should always be 'theme-head-tag' for removal - tag.setAttribute('class', 'theme-head-tag'); - - return tag; - } - private trackIdleModal() { const isIdle$ = this.authService.isUserIdle(); const isAuthenticated$ = this.authService.isAuthenticated(); @@ -310,16 +155,4 @@ export class AppComponent implements OnInit, AfterViewInit { }); } - /** - * Use nextValue to update a given BehaviorSubject, only if it differs from its current value - * - * @param bs a BehaviorSubject - * @param nextValue the next value for that BehaviorSubject - * @protected - */ - protected distinctNext(bs: BehaviorSubject, nextValue: T): void { - if (bs.getValue() !== nextValue) { - bs.next(nextValue); - } - } } diff --git a/src/app/core/shared/distinct-next.ts b/src/app/core/shared/distinct-next.ts new file mode 100644 index 0000000000..0ee3d237d4 --- /dev/null +++ b/src/app/core/shared/distinct-next.ts @@ -0,0 +1,18 @@ +/* + * something something atmire + */ + +import { BehaviorSubject } from 'rxjs'; + +/** + * Use nextValue to update a given BehaviorSubject, only if it differs from its current value + * + * @param bs a BehaviorSubject + * @param nextValue the next value for that BehaviorSubject + * @protected + */ +export function distinctNext(bs: BehaviorSubject, nextValue: T): void { + if (bs.getValue() !== nextValue) { + bs.next(nextValue); + } +} diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts index 7fb555f23c..c046ad5715 100644 --- a/src/app/init.service.spec.ts +++ b/src/app/init.service.spec.ts @@ -33,6 +33,8 @@ import { getTestScheduler } from 'jasmine-marbles'; import objectContaining = jasmine.objectContaining; import createSpyObj = jasmine.createSpyObj; import SpyObj = jasmine.SpyObj; +import { ThemeService } from './shared/theme-support/theme.service'; +import { getMockThemeService } from './shared/mocks/theme-service.mock'; let spy: SpyObj; @@ -171,6 +173,7 @@ describe('InitService', () => { { provide: MenuService, useValue: new MenuServiceStub() }, { provide: KlaroService, useValue: undefined }, { provide: GoogleAnalyticsService, useValue: undefined }, + { provide: ThemeService, useValue: getMockThemeService() }, provideMockStore({ initialState }), AppComponent, RouteService, diff --git a/src/app/init.service.ts b/src/app/init.service.ts index e5b04163c0..62461212d2 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -25,6 +25,7 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { distinctUntilChanged, filter, take, tap } from 'rxjs/operators'; import { isAuthenticationBlocking } from './core/auth/selectors'; import { KlaroService } from './shared/cookies/klaro.service'; +import { ThemeService } from './shared/theme-support/theme.service'; /** * Performs the initialization of the app. @@ -49,6 +50,7 @@ export abstract class InitService { protected metadata: MetadataService, protected breadcrumbsService: BreadcrumbsService, @Optional() protected klaroService: KlaroService, + protected themeService: ThemeService, ) { } @@ -192,11 +194,13 @@ export abstract class InitService { * Start route-listening subscriptions * - {@link MetadataService.listenForRouteChange} * - {@link BreadcrumbsService.listenForRouteChanges} + * - {@link ThemeService.listenForRouteChanges} * @protected */ protected initRouteListeners(): void { this.metadata.listenForRouteChange(); this.breadcrumbsService.listenForRouteChanges(); + this.themeService.listenForRouteChanges(); } /** diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index fc5d1a2ef3..8531543361 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -6,10 +6,10 @@
- \ No newline at end of file + diff --git a/src/app/shared/mocks/theme-service.mock.ts b/src/app/shared/mocks/theme-service.mock.ts index 058ba993bc..e3c2960e51 100644 --- a/src/app/shared/mocks/theme-service.mock.ts +++ b/src/app/shared/mocks/theme-service.mock.ts @@ -8,6 +8,7 @@ export function getMockThemeService(themeName = 'base', themes?: ThemeConfig[]): getThemeName: themeName, getThemeName$: observableOf(themeName), getThemeConfigFor: undefined, + listenForRouteChanges: undefined, }); if (isNotEmpty(themes)) { diff --git a/src/app/shared/theme-support/theme.service.spec.ts b/src/app/shared/theme-support/theme.service.spec.ts index 84043369c0..43b5964b8c 100644 --- a/src/app/shared/theme-support/theme.service.spec.ts +++ b/src/app/shared/theme-support/theme.service.spec.ts @@ -2,7 +2,7 @@ import { of as observableOf } from 'rxjs'; import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { LinkService } from '../../core/cache/builders/link.service'; -import { cold, hot } from 'jasmine-marbles'; +import { hot } from 'jasmine-marbles'; import { SetThemeAction } from './theme.actions'; import { Theme } from '../../../config/theme.model'; import { provideMockStore } from '@ngrx/store/testing'; @@ -21,7 +21,9 @@ import { import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { ThemeService } from './theme.service'; import { ROUTER_NAVIGATED } from '@ngrx/router-store'; -import { ActivatedRouteSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { CommonModule, DOCUMENT } from '@angular/common'; +import { RouterMock } from '../mocks/router.mock'; /** * LinkService able to mock recursively resolving DSO parent links @@ -84,12 +86,16 @@ describe('ThemeService', () => { findById: () => createSuccessfulRemoteDataObject$(mockCommunity) }; TestBed.configureTestingModule({ + imports: [ + CommonModule, + ], providers: [ ThemeService, { provide: LinkService, useValue: linkService }, provideMockStore({ initialState }), provideMockActions(() => mockActions), - { provide: DSpaceObjectDataService, useValue: mockDsoService } + { provide: DSpaceObjectDataService, useValue: mockDsoService }, + { provide: Router, useValue: new RouterMock() }, ] }); @@ -367,4 +373,49 @@ describe('ThemeService', () => { }); }); }); + + describe('listenForThemeChanges', () => { + let document; + let headSpy; + + beforeEach(() => { + const mockDsoService = { + findById: () => createSuccessfulRemoteDataObject$(mockCommunity) + }; + + TestBed.configureTestingModule({ + imports: [ + CommonModule, + ], + providers: [ + ThemeService, + { provide: LinkService, useValue: linkService }, + provideMockStore({ initialState }), + { provide: DSpaceObjectDataService, useValue: mockDsoService }, + { provide: Router, useValue: new RouterMock() }, + ] + }); + + document = TestBed.inject(DOCUMENT); + headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']); + headSpy.getElementsByClassName.and.returnValue([]); + spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]); + + themeService = TestBed.inject(ThemeService); + spyOn(themeService, 'getThemeName').and.returnValue('custom'); + spyOn(themeService, 'getThemeName$').and.returnValue(observableOf('custom')); + }); + + it('should append a link element with the correct attributes to the head element', () => { + themeService.listenForThemeChanges(true); + + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('class', 'theme-css'); + link.setAttribute('href', '/custom-theme.css'); + + expect(headSpy.appendChild).toHaveBeenCalledWith(link); + }); + }); }); diff --git a/src/app/shared/theme-support/theme.service.ts b/src/app/shared/theme-support/theme.service.ts index 4a4f6ae986..7642b7097e 100644 --- a/src/app/shared/theme-support/theme.service.ts +++ b/src/app/shared/theme-support/theme.service.ts @@ -1,10 +1,10 @@ -import { Injectable, Inject } from '@angular/core'; -import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store'; -import { EMPTY, Observable, of as observableOf } from 'rxjs'; +import { Inject, Injectable } from '@angular/core'; +import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store'; +import { BehaviorSubject, EMPTY, Observable, of as observableOf } from 'rxjs'; import { ThemeState } from './theme.reducer'; import { SetThemeAction, ThemeActionTypes } from './theme.actions'; import { expand, filter, map, switchMap, take, toArray } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../empty.util'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { @@ -12,14 +12,18 @@ import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../core/shared/operators'; -import { Theme, ThemeConfig, themeFactory } from '../../../config/theme.model'; +import { HeadTagConfig, 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.service'; -import { ActivatedRouteSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, ResolveEnd, Router } from '@angular/router'; import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listable-object/listable-object.decorator'; +import { distinctNext } from 'src/app/core/shared/distinct-next'; +import { DOCUMENT } from '@angular/common'; +import { getDefaultThemeConfig } from '../../../config/config.util'; +import { BASE_THEME_NAME } from './theme.constants'; export const themeStateSelector = createFeatureSelector('theme'); @@ -42,11 +46,16 @@ export class ThemeService { */ hasDynamicTheme: boolean; + private _isThemeLoading$ = new BehaviorSubject(false); + private _isThemeCSSLoading$ = new BehaviorSubject(false); + constructor( private store: Store, private linkService: LinkService, private dSpaceObjectDataService: DSpaceObjectDataService, - @Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig + @Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig, + private router: Router, + @Inject(DOCUMENT) private document: any, ) { // Create objects from the theme configs in the environment file this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig)); @@ -78,6 +87,159 @@ export class ThemeService { ); } + get isThemeLoading$(): Observable { + return this._isThemeLoading$; + } + + listenForThemeChanges(isBrowser: boolean): void { + this.getThemeName$().subscribe((themeName: string) => { + if (isBrowser) { + // the theme css will never download server side, so this should only happen on the browser + distinctNext(this._isThemeCSSLoading$, true); + } + if (hasValue(themeName)) { + this.loadGlobalThemeConfig(themeName); + } else { + const defaultThemeConfig = getDefaultThemeConfig(); + if (hasValue(defaultThemeConfig)) { + this.loadGlobalThemeConfig(defaultThemeConfig.name); + } else { + this.loadGlobalThemeConfig(BASE_THEME_NAME); + } + } + }); + } + + listenForRouteChanges(): void { + this.router.events.pipe( + filter(event => event instanceof ResolveEnd), + switchMap((event: ResolveEnd) => this.updateThemeOnRouteChange$(event.urlAfterRedirects, event.state.root)), + switchMap((changed) => { + if (changed) { + return this._isThemeCSSLoading$; + } else { + return [false]; + } + }) + ).subscribe((changed) => { + distinctNext(this._isThemeLoading$, changed); + }); + } + + private loadGlobalThemeConfig(themeName: string): void { + this.setThemeCss(themeName); + this.setHeadTags(themeName); + } + + /** + * Update the theme css file in + * + * @param themeName The name of the new theme + * @private + */ + private setThemeCss(themeName: string): void { + const head = this.document.getElementsByTagName('head')[0]; + if (hasNoValue(head)) { + return; + } + + // Array.from to ensure we end up with an array, not an HTMLCollection, which would be + // automatically updated if we add nodes later + const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css')); + const link = this.document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('class', 'theme-css'); + link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); + // wait for the new css to download before removing the old one to prevent a + // flash of unstyled content + link.onload = () => { + if (isNotEmpty(currentThemeLinks)) { + currentThemeLinks.forEach((currentThemeLink: any) => { + if (hasValue(currentThemeLink)) { + currentThemeLink.remove(); + } + }); + } + // the fact that this callback is used, proves we're on the browser. + distinctNext(this._isThemeCSSLoading$, false); + }; + head.appendChild(link); + } + + private setHeadTags(themeName: string): void { + const head = this.document.getElementsByTagName('head')[0]; + if (hasNoValue(head)) { + return; + } + + // clear head tags + const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag')); + if (hasValue(currentHeadTags)) { + currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove()); + } + + // create new head tags (not yet added to DOM) + const headTagFragment = this.document.createDocumentFragment(); + this.createHeadTags(themeName) + .forEach(newHeadTag => headTagFragment.appendChild(newHeadTag)); + + // add new head tags to DOM + head.appendChild(headTagFragment); + } + + private createHeadTags(themeName: string): HTMLElement[] { + const themeConfig = this.getThemeConfigFor(themeName); + const headTagConfigs = themeConfig?.headTags; + + if (hasNoValue(headTagConfigs)) { + const parentThemeName = themeConfig?.extends; + if (hasValue(parentThemeName)) { + // inherit the head tags of the parent theme + return this.createHeadTags(parentThemeName); + } + const defaultThemeConfig = getDefaultThemeConfig(); + const defaultThemeName = defaultThemeConfig.name; + if ( + hasNoValue(defaultThemeName) || + themeName === defaultThemeName || + themeName === BASE_THEME_NAME + ) { + // last resort, use fallback favicon.ico + return [ + this.createHeadTag({ + 'tagName': 'link', + 'attributes': { + 'rel': 'icon', + 'href': 'assets/images/favicon.ico', + 'sizes': 'any', + } + }) + ]; + } + + // inherit the head tags of the default theme + return this.createHeadTags(defaultThemeConfig.name); + } + + return headTagConfigs.map(this.createHeadTag.bind(this)); + } + + private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement { + const tag = this.document.createElement(headTagConfig.tagName); + + if (hasValue(headTagConfig.attributes)) { + Object.entries(headTagConfig.attributes) + .forEach(([key, value]) => tag.setAttribute(key, value)); + } + + // 'class' attribute should always be 'theme-head-tag' for removal + tag.setAttribute('class', 'theme-head-tag'); + + return tag; + } + + /** * 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 diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index f675c55718..e5718045c6 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -25,6 +25,7 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { AuthService } from '../../app/core/auth/auth.service'; +import { ThemeService } from '../../app/shared/theme-support/theme.service'; /** * Performs client-side initialization. @@ -46,6 +47,7 @@ export class BrowserInitService extends InitService { protected cssService: CSSVariableService, @Optional() protected klaroService: KlaroService, protected authService: AuthService, + protected themeService: ThemeService, ) { super( store, @@ -59,6 +61,7 @@ export class BrowserInitService extends InitService { metadata, breadcrumbsService, klaroService, + themeService, ); } @@ -83,6 +86,7 @@ export class BrowserInitService extends InitService { this.initI18n(); this.initAnalytics(); this.initRouteListeners(); + this.themeService.listenForThemeChanges(true); this.trackAuthTokenExpiration(); this.initKlaro(); diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index fc7fc66bf7..9e23bbeef3 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -22,6 +22,7 @@ import { MetadataService } from '../../app/core/metadata/metadata.service'; import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; +import { ThemeService } from '../../app/shared/theme-support/theme.service'; /** * Performs server-side initialization. @@ -42,6 +43,7 @@ export class ServerInitService extends InitService { protected breadcrumbsService: BreadcrumbsService, protected cssService: CSSVariableService, @Optional() protected klaroService: KlaroService, + protected themeService: ThemeService, ) { super( store, @@ -55,6 +57,7 @@ export class ServerInitService extends InitService { metadata, breadcrumbsService, klaroService, + themeService, ); } @@ -69,6 +72,7 @@ export class ServerInitService extends InitService { this.initI18n(); this.initAnalytics(); this.initRouteListeners(); + this.themeService.listenForThemeChanges(false); this.initKlaro(); From 5ff80a8a02e38ddca2e68f9a7ea80faca4eb0d59 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 22 Jul 2022 13:31:46 +0200 Subject: [PATCH 05/15] 93219: Fold DSpaceTransferState into InitService --- src/app/init.service.spec.ts | 2 -- src/app/init.service.ts | 18 ++++++-------- src/modules/app/browser-app.module.ts | 6 ++--- src/modules/app/browser-init.service.ts | 24 +++++++++++++++---- src/modules/app/server-app.module.ts | 5 ++-- src/modules/app/server-init.service.ts | 21 ++++++++++++---- .../dspace-browser-transfer-state.module.ts | 16 ------------- .../dspace-browser-transfer-state.service.ts | 19 --------------- .../dspace-server-transfer-state.module.ts | 16 ------------- .../dspace-server-transfer-state.service.ts | 20 ---------------- .../dspace-transfer-state.service.ts | 18 -------------- 11 files changed, 48 insertions(+), 117 deletions(-) delete mode 100644 src/modules/transfer-state/dspace-browser-transfer-state.module.ts delete mode 100644 src/modules/transfer-state/dspace-browser-transfer-state.service.ts delete mode 100644 src/modules/transfer-state/dspace-server-transfer-state.module.ts delete mode 100644 src/modules/transfer-state/dspace-server-transfer-state.service.ts delete mode 100644 src/modules/transfer-state/dspace-transfer-state.service.ts diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts index c046ad5715..181fe58700 100644 --- a/src/app/init.service.spec.ts +++ b/src/app/init.service.spec.ts @@ -25,7 +25,6 @@ import { RouteService } from './core/services/route.service'; import { getMockLocaleService } from './app.component.spec'; import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; -import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; import { KlaroService } from './shared/cookies/klaro.service'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock'; @@ -161,7 +160,6 @@ describe('InitService', () => { providers: [ { provide: InitService, useClass: ConcreteInitServiceMock }, { provide: CorrelationIdService, useValue: correlationIdServiceSpy }, - { provide: DSpaceTransferState, useValue: dspaceTransferStateSpy }, { provide: APP_CONFIG, useValue: environment }, { provide: LocaleService, useValue: getMockLocaleService() }, { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 62461212d2..ae04b6a82e 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -8,9 +8,8 @@ import { select, Store } from '@ngrx/store'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; -import { DSpaceTransferState } from '../modules/transfer-state/dspace-transfer-state.service'; import { APP_INITIALIZER, Inject, Optional, Provider, Type } from '@angular/core'; -import { TransferState } from '@angular/platform-browser'; +import { makeStateKey, TransferState } from '@angular/platform-browser'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { environment } from '../environments/environment'; import { AppState } from './app.reducer'; @@ -38,10 +37,15 @@ import { ThemeService } from './shared/theme-support/theme.service'; * For example, NgbModal depends on ApplicationRef and can therefore not be used during initialization. */ export abstract class InitService { + /** + * The state transfer key to use for the NgRx store state + * @protected + */ + protected static NGRX_STATE = makeStateKey('NGRX_STATE'); + protected constructor( protected store: Store, protected correlationIdService: CorrelationIdService, - protected dspaceTransferState: DSpaceTransferState, @Inject(APP_CONFIG) protected appConfig: AppConfig, protected translate: TranslateService, protected localeService: LocaleService, @@ -130,14 +134,6 @@ export abstract class InitService { this.correlationIdService.initCorrelationId(); } - /** - * Transfer the application's NgRx state between server-side and client-side - * @protected - */ - protected async transferAppState(): Promise { - return this.dspaceTransferState.transfer(); - } - /** * Make sure the {@link environment} matches {@link APP_CONFIG} and print * some information about it to the console diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index cd3feedad8..d857b14dbd 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -1,6 +1,6 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; -import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser'; +import { BrowserModule, BrowserTransferStateModule, makeStateKey, TransferState } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { REQUEST } from '@nguniversal/express-engine/tokens'; @@ -12,7 +12,6 @@ import { IdlePreloadModule } from 'angular-idle-preload'; import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; -import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module'; import { ClientCookieService } from '../../app/core/services/client-cookie.service'; import { CookieService } from '../../app/core/services/cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; @@ -28,7 +27,6 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service'; import { BrowserInitService } from './browser-init.service'; -import { InitService } from '../../app/init.service'; export const REQ_KEY = makeStateKey('req'); @@ -52,7 +50,7 @@ export function getRequest(transferState: TransferState): any { StatisticsModule.forRoot(), Angulartics2RouterlessModule.forRoot(), BrowserAnimationsModule, - DSpaceBrowserTransferStateModule, + BrowserTransferStateModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index e5718045c6..28d2e0aad5 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -8,7 +8,6 @@ import { InitService } from '../../app/init.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; -import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { TransferState } from '@angular/platform-browser'; import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; import { DefaultAppConfig } from '../../config/default-app-config'; @@ -26,6 +25,10 @@ import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.ser import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; +import { StoreAction, StoreActionTypes } from '../../app/store.actions'; +import { coreSelector } from '../../app/core/core.selectors'; +import { find, map } from 'rxjs/operators'; +import { isNotEmpty } from '../../app/shared/empty.util'; /** * Performs client-side initialization. @@ -36,7 +39,6 @@ export class BrowserInitService extends InitService { protected store: Store, protected correlationIdService: CorrelationIdService, protected transferState: TransferState, - protected dspaceTransferState: DSpaceTransferState, @Inject(APP_CONFIG) protected appConfig: AppConfig, protected translate: TranslateService, protected localeService: LocaleService, @@ -52,7 +54,6 @@ export class BrowserInitService extends InitService { super( store, correlationIdService, - dspaceTransferState, appConfig, translate, localeService, @@ -77,7 +78,7 @@ export class BrowserInitService extends InitService { protected init(): () => Promise { return async () => { - await this.transferAppState(); + await this.loadAppState(); this.checkAuthenticationToken(); this.initCorrelationId(); @@ -97,6 +98,21 @@ export class BrowserInitService extends InitService { // Browser-only initialization steps + /** + * Retrieve server-side application state from the {@link NGRX_STATE} key and rehydrate the store. + * Resolves once the store is no longer empty. + * @private + */ + private async loadAppState(): Promise { + const state = this.transferState.get(InitService.NGRX_STATE, null); + this.transferState.remove(InitService.NGRX_STATE); + this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state)); + return this.store.select(coreSelector).pipe( + find((core: any) => isNotEmpty(core)), + map(() => true) + ).toPromise(); + } + private trackAuthTokenExpiration(): void { this.authService.trackTokenExpiration(); } diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 2b0462e9a0..c87963b2f8 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -2,7 +2,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule, TransferState } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ServerModule } from '@angular/platform-server'; +import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -12,7 +12,6 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; -import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module'; import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader'; import { CookieService } from '../../app/core/services/cookie.service'; import { ServerCookieService } from '../../app/core/services/server-cookie.service'; @@ -43,7 +42,7 @@ export function createTranslateLoader(transferState: TransferState) { appId: 'dspace-angular' }), NoopAnimationsModule, - DSpaceServerTransferStateModule, + ServerTransferStateModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 9e23bbeef3..a0e0b84769 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -9,7 +9,6 @@ import { InitService } from '../../app/init.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; import { TransferState } from '@angular/platform-browser'; -import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; import { environment } from '../../environments/environment'; @@ -23,6 +22,7 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; +import { take } from 'rxjs/operators'; /** * Performs server-side initialization. @@ -33,7 +33,6 @@ export class ServerInitService extends InitService { protected store: Store, protected correlationIdService: CorrelationIdService, protected transferState: TransferState, - protected dspaceTransferState: DSpaceTransferState, @Inject(APP_CONFIG) protected appConfig: AppConfig, protected translate: TranslateService, protected localeService: LocaleService, @@ -48,7 +47,6 @@ export class ServerInitService extends InitService { super( store, correlationIdService, - dspaceTransferState, appConfig, translate, localeService, @@ -65,7 +63,7 @@ export class ServerInitService extends InitService { return async () => { this.checkAuthenticationToken(); this.saveAppConfigForCSR(); - this.transferAppState(); // todo: SSR breaks if we await this (why?) + this.saveAppState(); this.initCorrelationId(); this.checkEnvironment(); @@ -82,6 +80,21 @@ export class ServerInitService extends InitService { // Server-only initialization steps + /** + * Set the {@link NGRX_STATE} key when state is serialized to be transfered + * @private + */ + private saveAppState() { + this.transferState.onSerialize(InitService.NGRX_STATE, () => { + let state; + this.store.pipe(take(1)).subscribe((saveState: any) => { + state = saveState; + }); + + return state; + }); + } + private saveAppConfigForCSR(): void { this.transferState.set(APP_CONFIG_STATE, environment as AppConfig); } diff --git a/src/modules/transfer-state/dspace-browser-transfer-state.module.ts b/src/modules/transfer-state/dspace-browser-transfer-state.module.ts deleted file mode 100644 index e251d0b3b2..0000000000 --- a/src/modules/transfer-state/dspace-browser-transfer-state.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { BrowserTransferStateModule } from '@angular/platform-browser'; -import { DSpaceBrowserTransferState } from './dspace-browser-transfer-state.service'; -import { DSpaceTransferState } from './dspace-transfer-state.service'; - -@NgModule({ - imports: [ - BrowserTransferStateModule - ], - providers: [ - { provide: DSpaceTransferState, useClass: DSpaceBrowserTransferState } - ] -}) -export class DSpaceBrowserTransferStateModule { - -} diff --git a/src/modules/transfer-state/dspace-browser-transfer-state.service.ts b/src/modules/transfer-state/dspace-browser-transfer-state.service.ts deleted file mode 100644 index 512d6aeb71..0000000000 --- a/src/modules/transfer-state/dspace-browser-transfer-state.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; -import { coreSelector } from 'src/app/core/core.selectors'; -import { StoreAction, StoreActionTypes } from '../../app/store.actions'; -import { DSpaceTransferState } from './dspace-transfer-state.service'; -import { find, map } from 'rxjs/operators'; -import { isNotEmpty } from '../../app/shared/empty.util'; - -@Injectable() -export class DSpaceBrowserTransferState extends DSpaceTransferState { - transfer(): Promise { - const state = this.transferState.get(DSpaceTransferState.NGRX_STATE, null); - this.transferState.remove(DSpaceTransferState.NGRX_STATE); - this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state)); - return this.store.select(coreSelector).pipe( - find((core: any) => isNotEmpty(core)), - map(() => true) - ).toPromise(); - } -} diff --git a/src/modules/transfer-state/dspace-server-transfer-state.module.ts b/src/modules/transfer-state/dspace-server-transfer-state.module.ts deleted file mode 100644 index f8f2631cd0..0000000000 --- a/src/modules/transfer-state/dspace-server-transfer-state.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ServerTransferStateModule } from '@angular/platform-server'; -import { DSpaceServerTransferState } from './dspace-server-transfer-state.service'; -import { DSpaceTransferState } from './dspace-transfer-state.service'; - -@NgModule({ - imports: [ - ServerTransferStateModule - ], - providers: [ - { provide: DSpaceTransferState, useClass: DSpaceServerTransferState } - ] -}) -export class DSpaceServerTransferStateModule { - -} diff --git a/src/modules/transfer-state/dspace-server-transfer-state.service.ts b/src/modules/transfer-state/dspace-server-transfer-state.service.ts deleted file mode 100644 index 96b1e4be38..0000000000 --- a/src/modules/transfer-state/dspace-server-transfer-state.service.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import {take} from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { DSpaceTransferState } from './dspace-transfer-state.service'; - -@Injectable() -export class DSpaceServerTransferState extends DSpaceTransferState { - transfer(): Promise { - this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => { - let state; - this.store.pipe(take(1)).subscribe((saveState: any) => { - state = saveState; - }); - - return state; - }); - - return new Promise(() => true); - } -} diff --git a/src/modules/transfer-state/dspace-transfer-state.service.ts b/src/modules/transfer-state/dspace-transfer-state.service.ts deleted file mode 100644 index 32761866fb..0000000000 --- a/src/modules/transfer-state/dspace-transfer-state.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@angular/core'; -import { makeStateKey, TransferState } from '@angular/platform-browser'; -import { Store } from '@ngrx/store'; -import { AppState } from '../../app/app.reducer'; - -@Injectable() -export abstract class DSpaceTransferState { - - protected static NGRX_STATE = makeStateKey('NGRX_STATE'); - - constructor( - protected transferState: TransferState, - protected store: Store - ) { - } - - abstract transfer(): Promise; -} From 67b4cce25d0b2adae8cb3c803aa886632f26df66 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 26 Jul 2022 09:50:39 +0200 Subject: [PATCH 06/15] 92319: Move Klaro & GA steps to BrowserInitService --- src/app/init.service.ts | 33 ++--------------------- src/modules/app/browser-init.service.ts | 35 +++++++++++++++++++------ src/modules/app/server-init.service.ts | 12 ++------- 3 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/app/init.service.ts b/src/app/init.service.ts index ae04b6a82e..30630e1d84 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -16,14 +16,9 @@ import { AppState } from './app.reducer'; import { isEqual } from 'lodash'; import { TranslateService } from '@ngx-translate/core'; import { LocaleService } from './core/locale/locale.service'; -import { hasValue } from './shared/empty.util'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; -import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { MetadataService } from './core/metadata/metadata.service'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; -import { distinctUntilChanged, filter, take, tap } from 'rxjs/operators'; -import { isAuthenticationBlocking } from './core/auth/selectors'; -import { KlaroService } from './shared/cookies/klaro.service'; import { ThemeService } from './shared/theme-support/theme.service'; /** @@ -50,10 +45,8 @@ export abstract class InitService { protected translate: TranslateService, protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, - @Optional() protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, protected breadcrumbsService: BreadcrumbsService, - @Optional() protected klaroService: KlaroService, protected themeService: ThemeService, ) { } @@ -174,15 +167,10 @@ export abstract class InitService { } /** - * Initialize analytics services - * - Angulartics - * - Google Analytics (if enabled) + * Initialize Angulartics * @protected */ - protected initAnalytics(): void { - if (hasValue(this.googleAnalyticsService)) { - this.googleAnalyticsService.addTrackingIdToPage(); - } + protected initAngulartics(): void { this.angulartics2DSpace.startTracking(); } @@ -198,21 +186,4 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); } - - /** - * Initialize Klaro (if enabled) - * @protected - */ - protected initKlaro() { - if (hasValue(this.klaroService)) { - this.store.pipe( - select(isAuthenticationBlocking), - distinctUntilChanged(), - filter((isBlocking: boolean) => isBlocking === false), - take(1) - ).subscribe(() => { - this.klaroService.initialize(); - }); - } - } } diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 28d2e0aad5..733a776a73 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -6,7 +6,7 @@ * http://www.dspace.org/license/ */ import { InitService } from '../../app/init.service'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; import { TransferState } from '@angular/platform-browser'; import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; @@ -14,7 +14,7 @@ import { DefaultAppConfig } from '../../config/default-app-config'; import { extendEnvironmentWithAppConfig } from '../../config/config.util'; import { environment } from '../../environments/environment'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; -import { Inject, Injectable, Optional } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { LocaleService } from '../../app/core/locale/locale.service'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; @@ -27,8 +27,9 @@ import { AuthService } from '../../app/core/auth/auth.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { coreSelector } from '../../app/core/core.selectors'; -import { find, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map, take } from 'rxjs/operators'; import { isNotEmpty } from '../../app/shared/empty.util'; +import { isAuthenticationBlocking } from '../../app/core/auth/selectors'; /** * Performs client-side initialization. @@ -43,11 +44,11 @@ export class BrowserInitService extends InitService { protected translate: TranslateService, protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, - @Optional() protected googleAnalyticsService: GoogleAnalyticsService, + protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, protected breadcrumbsService: BreadcrumbsService, protected cssService: CSSVariableService, - @Optional() protected klaroService: KlaroService, + protected klaroService: KlaroService, protected authService: AuthService, protected themeService: ThemeService, ) { @@ -58,10 +59,8 @@ export class BrowserInitService extends InitService { translate, localeService, angulartics2DSpace, - googleAnalyticsService, metadata, breadcrumbsService, - klaroService, themeService, ); } @@ -85,7 +84,8 @@ export class BrowserInitService extends InitService { this.checkEnvironment(); this.initI18n(); - this.initAnalytics(); + this.initAngulartics(); + this.initGoogleAnalytics(); this.initRouteListeners(); this.themeService.listenForThemeChanges(true); this.trackAuthTokenExpiration(); @@ -116,4 +116,23 @@ export class BrowserInitService extends InitService { private trackAuthTokenExpiration(): void { this.authService.trackTokenExpiration(); } + + /** + * Initialize Klaro + * @protected + */ + protected initKlaro() { + this.store.pipe( + select(isAuthenticationBlocking), + distinctUntilChanged(), + filter((isBlocking: boolean) => isBlocking === false), + take(1) + ).subscribe(() => { + this.klaroService.initialize(); + }); + } + + protected initGoogleAnalytics() { + this.googleAnalyticsService.addTrackingIdToPage(); + } } diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index a0e0b84769..803dc7a75a 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -12,15 +12,13 @@ import { TransferState } from '@angular/platform-browser'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; import { environment } from '../../environments/environment'; -import { Inject, Injectable, Optional } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { LocaleService } from '../../app/core/locale/locale.service'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; -import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { MetadataService } from '../../app/core/metadata/metadata.service'; import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; -import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { take } from 'rxjs/operators'; @@ -37,11 +35,9 @@ export class ServerInitService extends InitService { protected translate: TranslateService, protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, - @Optional() protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, protected breadcrumbsService: BreadcrumbsService, protected cssService: CSSVariableService, - @Optional() protected klaroService: KlaroService, protected themeService: ThemeService, ) { super( @@ -51,10 +47,8 @@ export class ServerInitService extends InitService { translate, localeService, angulartics2DSpace, - googleAnalyticsService, metadata, breadcrumbsService, - klaroService, themeService, ); } @@ -68,12 +62,10 @@ export class ServerInitService extends InitService { this.checkEnvironment(); this.initI18n(); - this.initAnalytics(); + this.initAngulartics(); this.initRouteListeners(); this.themeService.listenForThemeChanges(false); - this.initKlaro(); - return true; }; } From ca87f0962533910653d9bcf524b212462372fd71 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 29 Jul 2022 15:51:23 +0200 Subject: [PATCH 07/15] Update specs --- src/app/init.service.spec.ts | 93 +---------- src/modules/app/browser-init.service.spec.ts | 155 +++++++++++++++++++ src/modules/app/browser-init.service.ts | 1 - 3 files changed, 158 insertions(+), 91 deletions(-) create mode 100644 src/modules/app/browser-init.service.spec.ts diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts index 181fe58700..2c9edfd4e0 100644 --- a/src/app/init.service.spec.ts +++ b/src/app/init.service.spec.ts @@ -2,11 +2,10 @@ import { InitService } from './init.service'; import { APP_CONFIG } from 'src/config/app-config.interface'; import { APP_INITIALIZER, Injectable } from '@angular/core'; import { inject, TestBed, waitForAsync } from '@angular/core/testing'; -import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { MetadataService } from './core/metadata/metadata.service'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { CommonModule } from '@angular/common'; -import { Store, StoreModule } from '@ngrx/store'; +import { StoreModule } from '@ngrx/store'; import { authReducer } from './core/auth/auth.reducer'; import { storeModuleConfig } from './app.reducer'; import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock'; @@ -25,15 +24,13 @@ import { RouteService } from './core/services/route.service'; import { getMockLocaleService } from './app.component.spec'; import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; -import { KlaroService } from './shared/cookies/klaro.service'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock'; -import { getTestScheduler } from 'jasmine-marbles'; +import { ThemeService } from './shared/theme-support/theme.service'; +import { getMockThemeService } from './shared/mocks/theme-service.mock'; import objectContaining = jasmine.objectContaining; import createSpyObj = jasmine.createSpyObj; import SpyObj = jasmine.SpyObj; -import { ThemeService } from './shared/theme-support/theme.service'; -import { getMockThemeService } from './shared/mocks/theme-service.mock'; let spy: SpyObj; @@ -169,8 +166,6 @@ describe('InitService', () => { { provide: Router, useValue: new RouterMock() }, { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, { provide: MenuService, useValue: new MenuServiceStub() }, - { provide: KlaroService, useValue: undefined }, - { provide: GoogleAnalyticsService, useValue: undefined }, { provide: ThemeService, useValue: getMockThemeService() }, provideMockStore({ initialState }), AppComponent, @@ -179,26 +174,6 @@ describe('InitService', () => { }); })); - describe('initÀnalytics', () => { - describe('when GoogleAnalyticsService is provided', () => { - let googleAnalyticsSpy; - - beforeEach(() => { - googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ - 'addTrackingIdToPage', - ]); - - TestBed.overrideProvider(GoogleAnalyticsService, { useValue: googleAnalyticsSpy }); - }); - - it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => { - // @ts-ignore - service.initAnalytics(); - expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); - })); - }); - }); - describe('initRouteListeners', () => { it('should call listenForRouteChanges', inject([InitService], (service) => { // @ts-ignore @@ -207,68 +182,6 @@ describe('InitService', () => { expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1); })); }); - - describe('initKlaro', () => { - const BLOCKING = { - t: { core: { auth: { blocking: true } } }, - f: { core: { auth: { blocking: false } } }, - }; - - it('should not error out if KlaroService is not provided', inject([InitService], (service) => { - // @ts-ignore - service.initKlaro(); - })); - - describe('when KlaroService is provided', () => { - let klaroServiceSpy; - - beforeEach(() => { - klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [ - 'initialize', - ]); - - TestBed.overrideProvider(KlaroService, { useValue: klaroServiceSpy }); - }); - - it('should not initialize Klaro while auth is blocking', () => { - getTestScheduler().run(({ cold, flush}) => { - TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) }); - const service = TestBed.inject(InitService); - - // @ts-ignore - service.initKlaro(); - flush(); - expect(klaroServiceSpy.initialize).not.toHaveBeenCalled(); - }); - }); - - - it('should only initialize Klaro the first time auth is unblocked', () => { - getTestScheduler().run(({ cold, flush}) => { - TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); - const service = TestBed.inject(InitService); - - // @ts-ignore - service.initKlaro(); - flush(); - expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('when KlaroService is not provided', () => { - it('should not error out when auth is unblocked', () => { - getTestScheduler().run(({ cold, flush}) => { - TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); - const service = TestBed.inject(InitService); - - // @ts-ignore - service.initKlaro(); - flush(); - }); - }); - }); - }); }); }); diff --git a/src/modules/app/browser-init.service.spec.ts b/src/modules/app/browser-init.service.spec.ts new file mode 100644 index 0000000000..05da7d9d36 --- /dev/null +++ b/src/modules/app/browser-init.service.spec.ts @@ -0,0 +1,155 @@ +import { InitService } from '../../app/init.service'; +import { APP_CONFIG } from 'src/config/app-config.interface'; +import { inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; +import { MetadataService } from '../../app/core/metadata/metadata.service'; +import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; +import { CommonModule } from '@angular/common'; +import { Store, StoreModule } from '@ngrx/store'; +import { authReducer } from '../../app/core/auth/auth.reducer'; +import { storeModuleConfig } from '../../app/app.reducer'; +import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock'; +import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { AuthService } from '../../app/core/auth/auth.service'; +import { AuthServiceMock } from '../../app/shared/mocks/auth.service.mock'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterMock } from '../../app/shared/mocks/router.mock'; +import { MockActivatedRoute } from '../../app/shared/mocks/active-router.mock'; +import { MenuService } from '../../app/shared/menu/menu.service'; +import { LocaleService } from '../../app/core/locale/locale.service'; +import { environment } from '../../environments/environment'; +import { provideMockStore } from '@ngrx/store/testing'; +import { AppComponent } from '../../app/app.component'; +import { RouteService } from '../../app/core/services/route.service'; +import { getMockLocaleService } from '../../app/app.component.spec'; +import { MenuServiceStub } from '../../app/shared/testing/menu-service.stub'; +import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; +import { KlaroService } from '../../app/shared/cookies/klaro.service'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../app/shared/mocks/translate-loader.mock'; +import { getTestScheduler } from 'jasmine-marbles'; +import { ThemeService } from '../../app/shared/theme-support/theme.service'; +import { getMockThemeService } from '../../app/shared/mocks/theme-service.mock'; +import { BrowserInitService } from './browser-init.service'; +import { TransferState } from '@angular/platform-browser'; + +const initialState = { + core: { + auth: { + loading: false, + blocking: true, + } + } +}; + +describe('BrowserInitService', () => { + describe('browser-specific initialization steps', () => { + let correlationIdServiceSpy; + let dspaceTransferStateSpy; + let transferStateSpy; + let metadataServiceSpy; + let breadcrumbsServiceSpy; + let klaroServiceSpy; + let googleAnalyticsSpy; + + beforeEach(waitForAsync(() => { + correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [ + 'initCorrelationId', + ]); + dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ + 'transfer', + ]); + transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ + 'get', 'hasKey' + ]); + breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [ + 'listenForRouteChanges', + ]); + metadataServiceSpy = jasmine.createSpyObj('metadataService', [ + 'listenForRouteChange', + ]); + klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [ + 'initialize', + ]); + googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ + 'addTrackingIdToPage', + ]); + + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot(authReducer, storeModuleConfig), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + providers: [ + { provide: InitService, useClass: BrowserInitService }, + { provide: CorrelationIdService, useValue: correlationIdServiceSpy }, + { provide: APP_CONFIG, useValue: environment }, + { provide: LocaleService, useValue: getMockLocaleService() }, + { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, + { provide: MetadataService, useValue: metadataServiceSpy }, + { provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: Router, useValue: new RouterMock() }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + { provide: MenuService, useValue: new MenuServiceStub() }, + { provide: KlaroService, useValue: klaroServiceSpy }, + { provide: GoogleAnalyticsService, useValue: googleAnalyticsSpy }, + { provide: ThemeService, useValue: getMockThemeService() }, + provideMockStore({ initialState }), + AppComponent, + RouteService, + { provide: TransferState, useValue: undefined }, + ] + }); + })); + + describe('initGoogleÀnalytics', () => { + it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => { + // @ts-ignore + service.initGoogleAnalytics(); + expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); + })); + }); + + describe('initKlaro', () => { + const BLOCKING = { + t: { core: { auth: { blocking: true } } }, + f: { core: { auth: { blocking: false } } }, + }; + + it('should not initialize Klaro while auth is blocking', () => { + getTestScheduler().run(({ cold, flush}) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + service.initKlaro(); + flush(); + expect(klaroServiceSpy.initialize).not.toHaveBeenCalled(); + }); + }); + + + it('should only initialize Klaro the first time auth is unblocked', () => { + getTestScheduler().run(({ cold, flush}) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + service.initKlaro(); + flush(); + expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1); + }); + }); + }); + }); +}); + diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 733a776a73..3980b8bc28 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -47,7 +47,6 @@ export class BrowserInitService extends InitService { protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, protected breadcrumbsService: BreadcrumbsService, - protected cssService: CSSVariableService, protected klaroService: KlaroService, protected authService: AuthService, protected themeService: ThemeService, From 250043fde8d8023f71310f6940d7183e9ef28052 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 29 Jul 2022 16:58:11 +0200 Subject: [PATCH 08/15] Fix LGTM issues --- src/app/init.service.ts | 4 ++-- src/modules/app/browser-init.service.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 30630e1d84..d91f6b2dd9 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -5,10 +5,10 @@ * * http://www.dspace.org/license/ */ -import { select, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CorrelationIdService } from './correlation-id/correlation-id.service'; -import { APP_INITIALIZER, Inject, Optional, Provider, Type } from '@angular/core'; +import { APP_INITIALIZER, Inject, Provider, Type } from '@angular/core'; import { makeStateKey, TransferState } from '@angular/platform-browser'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { environment } from '../environments/environment'; diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 3980b8bc28..80619800fb 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -21,7 +21,6 @@ import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-prov import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { MetadataService } from '../../app/core/metadata/metadata.service'; import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; -import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; From 4870d818f685b83aae5fcd9484f6366f4639bdf4 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 9 Aug 2022 15:47:19 +0200 Subject: [PATCH 09/15] Fix license header --- src/app/core/shared/distinct-next.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/core/shared/distinct-next.ts b/src/app/core/shared/distinct-next.ts index 0ee3d237d4..6c629867a4 100644 --- a/src/app/core/shared/distinct-next.ts +++ b/src/app/core/shared/distinct-next.ts @@ -1,5 +1,9 @@ -/* - * something something atmire +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ */ import { BehaviorSubject } from 'rxjs'; From ba7ecf432b77975cdbb8203ada739cfd80aa5007 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 9 Aug 2022 16:03:30 +0200 Subject: [PATCH 10/15] Add TypeDocs to ThemeService --- src/app/shared/theme-support/theme.service.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/app/shared/theme-support/theme.service.ts b/src/app/shared/theme-support/theme.service.ts index 7642b7097e..6d9bffc44e 100644 --- a/src/app/shared/theme-support/theme.service.ts +++ b/src/app/shared/theme-support/theme.service.ts @@ -66,10 +66,17 @@ export class ThemeService { ); } + /** + * Set the current theme + * @param newName + */ setTheme(newName: string) { this.store.dispatch(new SetThemeAction(newName)); } + /** + * The name of the current theme (synchronous) + */ getThemeName(): string { let currentTheme: string; this.store.pipe( @@ -81,16 +88,29 @@ export class ThemeService { return currentTheme; } + /** + * The name of the current theme (asynchronous, tracks changes) + */ getThemeName$(): Observable { return this.store.pipe( select(currentThemeSelector) ); } + /** + * Whether the theme is currently loading + */ get isThemeLoading$(): Observable { return this._isThemeLoading$; } + /** + * Every time the theme is changed + * - if the theme name is valid, load it (CSS + tags) + * - otherwise fall back to {@link getDefaultThemeConfig} or {@link BASE_THEME_NAME} + * Should be called when initializing the app. + * @param isBrowser + */ listenForThemeChanges(isBrowser: boolean): void { this.getThemeName$().subscribe((themeName: string) => { if (isBrowser) { @@ -110,6 +130,10 @@ export class ThemeService { }); } + /** + * For every resolved route, check if it matches a dynamic theme. If it does, load that theme. + * Should be called when initializing the app. + */ listenForRouteChanges(): void { this.router.events.pipe( filter(event => event instanceof ResolveEnd), @@ -126,6 +150,13 @@ export class ThemeService { }); } + /** + * Load a theme's configuration + * - CSS + * - tags + * @param themeName + * @private + */ private loadGlobalThemeConfig(themeName: string): void { this.setThemeCss(themeName); this.setHeadTags(themeName); @@ -167,6 +198,11 @@ export class ThemeService { head.appendChild(link); } + /** + * Update the page to add a theme's tags + * @param themeName the theme in question + * @private + */ private setHeadTags(themeName: string): void { const head = this.document.getElementsByTagName('head')[0]; if (hasNoValue(head)) { @@ -188,6 +224,12 @@ export class ThemeService { head.appendChild(headTagFragment); } + /** + * Create HTML elements for a theme's tags + * (including those defined in the parent theme, if applicable) + * @param themeName the theme in question + * @private + */ private createHeadTags(themeName: string): HTMLElement[] { const themeConfig = this.getThemeConfigFor(themeName); const headTagConfigs = themeConfig?.headTags; @@ -225,6 +267,11 @@ export class ThemeService { return headTagConfigs.map(this.createHeadTag.bind(this)); } + /** + * Create a single tag element + * @param headTagConfig the configuration for this tag + * @private + */ private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement { const tag = this.document.createElement(headTagConfig.tagName); From 5ed369d097e07aab0fd5a128005a57bde6d1b280 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 19 Aug 2022 16:23:20 -0400 Subject: [PATCH 11/15] Make create, edit community/collection/item dialogs theme-able. --- src/app/menu.resolver.ts | 36 +++++++++---------- ...te-collection-parent-selector.component.ts | 28 +++++++++++++++ ...ate-community-parent-selector.component.ts | 27 ++++++++++++++ ...d-create-item-parent-selector.component.ts | 31 ++++++++++++++++ ...emed-edit-collection-selector.component.ts | 27 ++++++++++++++ ...hemed-edit-community-selector.component.ts | 27 ++++++++++++++ .../themed-edit-item-selector.component.ts | 27 ++++++++++++++ 7 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 4c97d3d1b3..f12079f737 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -16,24 +16,24 @@ import { filter, find, map, take } from 'rxjs/operators'; import { hasValue } from './shared/empty.util'; import { FeatureID } from './core/data/feature-authorization/feature-id'; import { - CreateCommunityParentSelectorComponent -} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; + ThemedCreateCommunityParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component'; import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model'; import { - CreateCollectionParentSelectorComponent -} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; + ThemedCreateCollectionParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component'; import { - CreateItemParentSelectorComponent -} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; + ThemedCreateItemParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; import { - EditCommunitySelectorComponent -} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; + ThemedEditCommunitySelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component'; import { - EditCollectionSelectorComponent -} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; + ThemedEditCollectionSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component'; import { - EditItemSelectorComponent -} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; + ThemedEditItemSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component'; import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; @@ -188,7 +188,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.new_community', function: () => { - this.modalService.open(CreateCommunityParentSelectorComponent); + this.modalService.open(ThemedCreateCommunityParentSelectorComponent); } } as OnClickMenuItemModel, }, @@ -201,7 +201,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.new_collection', function: () => { - this.modalService.open(CreateCollectionParentSelectorComponent); + this.modalService.open(ThemedCreateCollectionParentSelectorComponent); } } as OnClickMenuItemModel, }, @@ -214,7 +214,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.new_item', function: () => { - this.modalService.open(CreateItemParentSelectorComponent); + this.modalService.open(ThemedCreateItemParentSelectorComponent); } } as OnClickMenuItemModel, }, @@ -263,7 +263,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.edit_community', function: () => { - this.modalService.open(EditCommunitySelectorComponent); + this.modalService.open(ThemedEditCommunitySelectorComponent); } } as OnClickMenuItemModel, }, @@ -276,7 +276,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.edit_collection', function: () => { - this.modalService.open(EditCollectionSelectorComponent); + this.modalService.open(ThemedEditCollectionSelectorComponent); } } as OnClickMenuItemModel, }, @@ -289,7 +289,7 @@ export class MenuResolver implements Resolve { type: MenuItemType.ONCLICK, text: 'menu.section.edit_item', function: () => { - this.modalService.open(EditItemSelectorComponent); + this.modalService.open(ThemedEditItemSelectorComponent); } } as OnClickMenuItemModel, }, diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts new file mode 100644 index 0000000000..f6598aec99 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts @@ -0,0 +1,28 @@ +import {Component} from "@angular/core"; +import {CreateCollectionParentSelectorComponent} from "./create-collection-parent-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for CreateCollectionParentSelectorComponent + */ +@Component({ + selector: 'ds-themed-create-collection-parent-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedCreateCollectionParentSelectorComponent + extends ThemedComponent { + + protected getComponentName(): string { + return 'CreateCollectionParentSelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./create-collection-parent-selector.component'); + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts new file mode 100644 index 0000000000..92621978b8 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts @@ -0,0 +1,27 @@ +import {Component} from "@angular/core"; +import {CreateCommunityParentSelectorComponent} from "./create-community-parent-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for CreateCommunityParentSelectorComponent + */ +@Component({ + selector: 'ds-themed-create-community-parent-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedCreateCommunityParentSelectorComponent + extends ThemedComponent { + protected getComponentName(): string { + return 'CreateCommunityParentSelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./create-community-parent-selector.component'); + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts new file mode 100644 index 0000000000..70ab0d76ee --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts @@ -0,0 +1,31 @@ +import {Component, Input} from "@angular/core"; +import {CreateItemParentSelectorComponent} from "./create-item-parent-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for CreateItemParentSelectorComponent + */ +@Component({ + selector: 'ds-themed-create-item-parent-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedCreateItemParentSelectorComponent + extends ThemedComponent { + @Input() entityType: string; + + protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType']; + + protected getComponentName(): string { + return 'CreateItemParentSelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./create-item-parent-selector.component'); + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts new file mode 100644 index 0000000000..4e3191d03b --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts @@ -0,0 +1,27 @@ +import {Component} from "@angular/core"; +import {EditCollectionSelectorComponent} from "./edit-collection-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for EditCollectionSelectorComponent + */ +@Component({ + selector: 'ds-themed-edit-collection-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedEditCollectionSelectorComponent + extends ThemedComponent { + protected getComponentName(): string { + return 'EditCollectionSelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./edit-collection-selector.component'); + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts new file mode 100644 index 0000000000..d8232abcdb --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts @@ -0,0 +1,27 @@ +import {Component} from "@angular/core"; +import {EditCommunitySelectorComponent} from "./edit-community-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for EditCommunitySelectorComponent + */ +@Component({ + selector: 'ds-themed-edit-community-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedEditCommunitySelectorComponent + extends ThemedComponent { + protected getComponentName(): string { + return 'EditCommunitySelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./edit-community-selector.component'); + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts new file mode 100644 index 0000000000..e1377e7fb4 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts @@ -0,0 +1,27 @@ +import {Component} from "@angular/core"; +import {EditItemSelectorComponent} from "./edit-item-selector.component"; +import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; + +/** + * Themed wrapper for EditItemSelectorComponent + */ +@Component({ + selector: 'ds-themed-edit-item-selector', + styleUrls: [], + templateUrl: '../../../theme-support/themed.component.html' +}) +export class ThemedEditItemSelectorComponent + extends ThemedComponent { + protected getComponentName(): string { + return 'EditItemSelectorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./edit-item-selector.component'); + } + +} From b2feadc290c5b60dd45ac78b174902b187aae4e5 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 19 Aug 2022 16:47:10 -0400 Subject: [PATCH 12/15] Satisfy lint. --- .../themed-create-collection-parent-selector.component.ts | 6 +++--- .../themed-create-community-parent-selector.component.ts | 6 +++--- .../themed-create-item-parent-selector.component.ts | 6 +++--- .../themed-edit-collection-selector.component.ts | 6 +++--- .../themed-edit-community-selector.component.ts | 6 +++--- .../themed-edit-item-selector.component.ts | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts index f6598aec99..d90cd0ac0d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {CreateCollectionParentSelectorComponent} from "./create-collection-parent-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component} from '@angular/core'; +import {CreateCollectionParentSelectorComponent} from './create-collection-parent-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for CreateCollectionParentSelectorComponent diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts index 92621978b8..24bff97254 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {CreateCommunityParentSelectorComponent} from "./create-community-parent-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component} from '@angular/core'; +import {CreateCommunityParentSelectorComponent} from './create-community-parent-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for CreateCommunityParentSelectorComponent diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts index 70ab0d76ee..49209ea63b 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts @@ -1,6 +1,6 @@ -import {Component, Input} from "@angular/core"; -import {CreateItemParentSelectorComponent} from "./create-item-parent-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component, Input} from '@angular/core'; +import {CreateItemParentSelectorComponent} from './create-item-parent-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for CreateItemParentSelectorComponent diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts index 4e3191d03b..999f466e75 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {EditCollectionSelectorComponent} from "./edit-collection-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component} from '@angular/core'; +import {EditCollectionSelectorComponent} from './edit-collection-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for EditCollectionSelectorComponent diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts index d8232abcdb..e067803444 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {EditCommunitySelectorComponent} from "./edit-community-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component} from '@angular/core'; +import {EditCommunitySelectorComponent} from './edit-community-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for EditCommunitySelectorComponent diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts index e1377e7fb4..6d3b5691c1 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {EditItemSelectorComponent} from "./edit-item-selector.component"; -import {ThemedComponent} from "src/app/shared/theme-support/themed.component"; +import {Component} from '@angular/core'; +import {EditItemSelectorComponent} from './edit-item-selector.component'; +import {ThemedComponent} from 'src/app/shared/theme-support/themed.component'; /** * Themed wrapper for EditItemSelectorComponent From 9f609a2966a834e3c94f695164370a1ad1135546 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 2 Sep 2022 11:53:10 -0400 Subject: [PATCH 13/15] Add themed components to shared module and 'custom' theme. --- src/app/shared/shared.module.ts | 30 +++++++++++++++++++ ...te-collection-parent-selector.component.ts | 11 +++++++ ...e-community-parent-selector.component.html | 19 ++++++++++++ ...e-community-parent-selector.component.scss | 3 ++ ...ate-community-parent-selector.component.ts | 12 ++++++++ ...create-item-parent-selector.component.html | 15 ++++++++++ .../create-item-parent-selector.component.ts | 11 +++++++ .../dso-selector-modal-wrapper.component.html | 11 +++++++ .../edit-collection-selector.component.ts | 11 +++++++ .../edit-community-selector.component.ts | 11 +++++++ .../edit-item-selector.component.ts | 11 +++++++ src/themes/custom/eager-theme.module.ts | 24 +++++++++++++++ 12 files changed, 169 insertions(+) create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f40ddd5b90..9036ff98c5 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -124,12 +124,21 @@ import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.c import { CreateCommunityParentSelectorComponent } from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { + ThemedCreateCommunityParentSelectorComponent +} from './dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component'; import { CreateItemParentSelectorComponent } from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + ThemedCreateItemParentSelectorComponent +} from './dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component'; import { CreateCollectionParentSelectorComponent } from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + ThemedCreateCollectionParentSelectorComponent +} from './dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component'; import { CommunitySearchResultListElementComponent } from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; @@ -139,12 +148,21 @@ import { import { EditItemSelectorComponent } from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; +import { + ThemedEditItemSelectorComponent +} from './dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component'; import { EditCommunitySelectorComponent } from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + ThemedEditCommunitySelectorComponent +} from './dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component'; import { EditCollectionSelectorComponent } from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + ThemedEditCollectionSelectorComponent +} from './dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component'; import { ItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; @@ -395,11 +413,17 @@ const COMPONENTS = [ DsoInputSuggestionsComponent, DSOSelectorComponent, CreateCommunityParentSelectorComponent, + ThemedCreateCommunityParentSelectorComponent, CreateCollectionParentSelectorComponent, + ThemedCreateCollectionParentSelectorComponent, CreateItemParentSelectorComponent, + ThemedCreateItemParentSelectorComponent, EditCommunitySelectorComponent, + ThemedEditCommunitySelectorComponent, EditCollectionSelectorComponent, + ThemedEditCollectionSelectorComponent, EditItemSelectorComponent, + ThemedEditItemSelectorComponent, CommunitySearchResultListElementComponent, CollectionSearchResultListElementComponent, BrowseByComponent, @@ -491,11 +515,17 @@ const ENTRY_COMPONENTS = [ StartsWithDateComponent, StartsWithTextComponent, CreateCommunityParentSelectorComponent, + ThemedCreateCommunityParentSelectorComponent, CreateCollectionParentSelectorComponent, + ThemedCreateCollectionParentSelectorComponent, CreateItemParentSelectorComponent, + ThemedCreateItemParentSelectorComponent, EditCommunitySelectorComponent, + ThemedEditCommunitySelectorComponent, EditCollectionSelectorComponent, + ThemedEditCollectionSelectorComponent, EditItemSelectorComponent, + ThemedEditItemSelectorComponent, PlainTextMetadataListElementComponent, ItemMetadataListElementComponent, MetadataRepresentationListElementComponent, diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts new file mode 100644 index 0000000000..6df8807cae --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { + CreateCollectionParentSelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; + +@Component({ + selector: 'ds-create-collection-parent-selector', + templateUrl: '../dso-selector-modal-wrapper.component.html', +}) +export class CreateCollectionParentSelectorComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html new file mode 100644 index 0000000000..84fdd34c01 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html @@ -0,0 +1,19 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss new file mode 100644 index 0000000000..0daf4cfa5f --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss @@ -0,0 +1,3 @@ +#create-community-or-separator { + top: 0; +} \ No newline at end of file diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts new file mode 100644 index 0000000000..bb2785eab9 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { + CreateCommunityParentSelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; + +@Component({ + selector: 'ds-create-community-parent-selector', + styleUrls: ['./create-community-parent-selector.component.scss'], + templateUrl: './create-community-parent-selector.component.html', +}) +export class CreateCommunityParentSelectorComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html new file mode 100644 index 0000000000..664aef95c0 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -0,0 +1,15 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts new file mode 100644 index 0000000000..e04874c801 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; +import { + CreateItemParentSelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; + +@Component({ + selector: 'ulib-create-item-parent-selector', + templateUrl: './create-item-parent-selector.component.html', +}) +export class CreateItemParentSelectorComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html new file mode 100644 index 0000000000..85d8797e66 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts new file mode 100644 index 0000000000..6a51908dbc --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { + EditCollectionSelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component' + +@Component({ + selector: 'ds-edit-collection-selector', + templateUrl: '../dso-selector-modal-wrapper.component.html', +}) +export class EditCollectionSelectorComponent extends BaseComponent { +} \ No newline at end of file diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts new file mode 100644 index 0000000000..3a20cff266 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { + EditCommunitySelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; + +@Component({ + selector: 'ds-edit-item-selector', + templateUrl: '../dso-selector-modal-wrapper.component.html', +}) +export class EditCommunitySelectorComponent extends BaseComponent { +} \ No newline at end of file diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts new file mode 100644 index 0000000000..ddd6cae7b6 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { + EditItemSelectorComponent as BaseComponent +} from 'src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; + +@Component({ + selector: 'ds-edit-item-selector', + templateUrl: '../dso-selector-modal-wrapper.component.html', +}) +export class EditItemSelectorComponent extends BaseComponent { +} \ No newline at end of file diff --git a/src/themes/custom/eager-theme.module.ts b/src/themes/custom/eager-theme.module.ts index 5256d2fd7c..4c75f63cc9 100644 --- a/src/themes/custom/eager-theme.module.ts +++ b/src/themes/custom/eager-theme.module.ts @@ -21,6 +21,24 @@ import { } from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component'; import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component'; import { ItemSharedModule } from '../../app/item-page/item-shared.module'; +import { + CreateCollectionParentSelectorComponent +} from './app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + CreateCommunityParentSelectorComponent +} from './app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { + CreateItemParentSelectorComponent +} from './app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + EditCollectionSelectorComponent +} from './app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + EditCommunitySelectorComponent +} from './app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + EditItemSelectorComponent +} from './app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; /** * Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS. @@ -41,6 +59,12 @@ const DECLARATIONS = [ HeaderNavbarWrapperComponent, NavbarComponent, FooterComponent, + CreateCollectionParentSelectorComponent, + CreateCommunityParentSelectorComponent, + CreateItemParentSelectorComponent, + EditCollectionSelectorComponent, + EditCommunitySelectorComponent, + EditItemSelectorComponent, ]; @NgModule({ From 1f1b04e88f56ab2e8a70cf2a66e4706f9c592c54 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 2 Sep 2022 12:04:01 -0400 Subject: [PATCH 14/15] Fix lint issues, incorrect selector copied from local code. --- .../create-item-parent-selector.component.ts | 2 +- .../edit-collection-selector.component.ts | 4 ++-- .../edit-community-selector.component.ts | 2 +- .../edit-item-selector/edit-item-selector.component.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index e04874c801..44eb6edd18 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -4,7 +4,7 @@ import { } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; @Component({ - selector: 'ulib-create-item-parent-selector', + selector: 'ds-create-item-parent-selector', templateUrl: './create-item-parent-selector.component.html', }) export class CreateItemParentSelectorComponent extends BaseComponent { diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts index 6a51908dbc..3b463f2851 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -1,11 +1,11 @@ import { Component } from '@angular/core'; import { EditCollectionSelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component' +} from 'src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; @Component({ selector: 'ds-edit-collection-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class EditCollectionSelectorComponent extends BaseComponent { -} \ No newline at end of file +} diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts index 3a20cff266..cd9ea222f4 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -8,4 +8,4 @@ import { templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class EditCommunitySelectorComponent extends BaseComponent { -} \ No newline at end of file +} diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts index ddd6cae7b6..efff58d2a3 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -8,4 +8,4 @@ import { templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class EditItemSelectorComponent extends BaseComponent { -} \ No newline at end of file +} From ba2f173199ea750706479ffd3fcc0abe23358b62 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 6 Sep 2022 13:56:04 -0400 Subject: [PATCH 15/15] Address review: add missing templates, commented references to them, reference original templates by default. --- .../create-collection-parent-selector.component.html} | 0 .../create-collection-parent-selector.component.scss | 0 .../create-collection-parent-selector.component.ts | 6 ++++-- .../create-community-parent-selector.component.ts | 8 +++++--- .../create-item-parent-selector.component.scss | 0 .../create-item-parent-selector.component.ts | 6 ++++-- .../edit-collection-selector.component.html | 11 +++++++++++ .../edit-collection-selector.component.scss | 0 .../edit-collection-selector.component.ts | 6 ++++-- .../edit-community-selector.component.html | 11 +++++++++++ .../edit-community-selector.component.scss | 0 .../edit-community-selector.component.ts | 6 ++++-- .../edit-item-selector.component.html | 11 +++++++++++ .../edit-item-selector.component.scss | 0 .../edit-item-selector.component.ts | 4 +++- 15 files changed, 57 insertions(+), 12 deletions(-) rename src/themes/custom/app/shared/dso-selector/modal-wrappers/{dso-selector-modal-wrapper.component.html => create-collection-parent-selector/create-collection-parent-selector.component.html} (100%) create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.scss create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.scss create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.scss create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.scss create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html create mode 100644 src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.scss diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html similarity index 100% rename from src/themes/custom/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html rename to src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts index 6df8807cae..22d40ff539 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts @@ -1,11 +1,13 @@ import { Component } from '@angular/core'; import { CreateCollectionParentSelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; @Component({ selector: 'ds-create-collection-parent-selector', - templateUrl: '../dso-selector-modal-wrapper.component.html', + // styleUrls: ['./create-collection-parent-selector.component.scss'], + // templateUrl: './create-collection-parent-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html', }) export class CreateCollectionParentSelectorComponent extends BaseComponent { } diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts index bb2785eab9..8b28ee1bb8 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -1,12 +1,14 @@ import { Component } from '@angular/core'; import { CreateCommunityParentSelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; @Component({ selector: 'ds-create-community-parent-selector', - styleUrls: ['./create-community-parent-selector.component.scss'], - templateUrl: './create-community-parent-selector.component.html', + // styleUrls: ['./create-community-parent-selector.component.scss'], + styleUrls: ['../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss'], + // templateUrl: './create-community-parent-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html', }) export class CreateCommunityParentSelectorComponent extends BaseComponent { } diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 44eb6edd18..f8e3401454 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -1,11 +1,13 @@ import {Component} from '@angular/core'; import { CreateItemParentSelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; @Component({ selector: 'ds-create-item-parent-selector', - templateUrl: './create-item-parent-selector.component.html', + // styleUrls: ['./create-item-parent-selector.component.scss'], + // templateUrl: './create-item-parent-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html', }) export class CreateItemParentSelectorComponent extends BaseComponent { } diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.html new file mode 100644 index 0000000000..85d8797e66 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts index 3b463f2851..8f4a8dd5cd 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -1,11 +1,13 @@ import { Component } from '@angular/core'; import { EditCollectionSelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; @Component({ selector: 'ds-edit-collection-selector', - templateUrl: '../dso-selector-modal-wrapper.component.html', + // styleUrls: ['./edit-collection-selector.component.scss'], + // templateUrl: './edit-collection-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html', }) export class EditCollectionSelectorComponent extends BaseComponent { } diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.html new file mode 100644 index 0000000000..85d8797e66 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts index cd9ea222f4..79d52fc350 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -1,11 +1,13 @@ import { Component } from '@angular/core'; import { EditCommunitySelectorComponent as BaseComponent -} from 'src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; @Component({ selector: 'ds-edit-item-selector', - templateUrl: '../dso-selector-modal-wrapper.component.html', + // styleUrls: ['./edit-community-selector.component.scss'], + // templateUrl: './edit-community-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html', }) export class EditCommunitySelectorComponent extends BaseComponent { } diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html new file mode 100644 index 0000000000..85d8797e66 --- /dev/null +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.scss b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts index efff58d2a3..398dbc933c 100644 --- a/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts +++ b/src/themes/custom/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -5,7 +5,9 @@ import { @Component({ selector: 'ds-edit-item-selector', - templateUrl: '../dso-selector-modal-wrapper.component.html', + // styleUrls: ['./edit-item-selector.component.scss'], + // templateUrl: './edit-item-selector.component.html', + templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html', }) export class EditItemSelectorComponent extends BaseComponent { }