diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts deleted file mode 100644 index fa8156c..0000000 --- a/src/lib/common-ui.module.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { inject, ModuleWithProviders, NgModule, Optional, Provider, SkipSelf } from '@angular/core'; -import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; -import { MatIconRegistry } from '@angular/material/icon'; -import { DomSanitizer } from '@angular/platform-browser'; -import { ApiPathInterceptor, DefaultUserPreferenceService, IqserConfigService, IqserUserPreferenceService } from './services'; -import { CommonUiOptions, IqserAppConfig, ModuleOptions } from './utils'; -import { ICONS } from './utils/constants'; - -@NgModule({ - providers: [ - { - provide: HTTP_INTERCEPTORS, - multi: true, - useClass: ApiPathInterceptor, - }, - { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, - ], -}) -export class CommonUiModule { - constructor(@Optional() @SkipSelf() parentModule?: CommonUiModule) { - if (parentModule) { - throw new Error('CommonUiModule is already loaded. Import it in the AppModule only!'); - } - - const iconRegistry = inject(MatIconRegistry); - const sanitizer = inject(DomSanitizer); - - ICONS.forEach(icon => { - const url = sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`); - iconRegistry.addSvgIconInNamespace('iqser', icon, url); - }); - } - - static forRoot< - UserPreference extends IqserUserPreferenceService, - Config extends IqserConfigService, - AppConfig extends IqserAppConfig = IqserAppConfig, - >(options: CommonUiOptions): ModuleWithProviders { - const userPreferenceService = ModuleOptions.getService( - IqserUserPreferenceService, - DefaultUserPreferenceService, - options.existingUserPreferenceService, - ); - - const configServiceProviders = this._getConfigServiceProviders(options.configServiceFactory, options.configService); - - return { - ngModule: CommonUiModule, - providers: [userPreferenceService, ...configServiceProviders], - }; - } - - private static _getConfigServiceProviders(configServiceFactory: () => unknown, configService?: unknown): Provider[] { - if (configService) { - return [ - { - provide: configService, - useFactory: configServiceFactory, - }, - { - provide: IqserConfigService, - useExisting: configService, - }, - ]; - } - - return [ - { - provide: IqserConfigService, - useFactory: configServiceFactory, - }, - ]; - } -} diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts index 565d5cf..1b9083b 100644 --- a/src/lib/error/server-error-interceptor.ts +++ b/src/lib/error/server-error-interceptor.ts @@ -10,10 +10,11 @@ import { import { Inject, Injectable, Optional } from '@angular/core'; import { finalize, MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { MAX_RETRIES_ON_SERVER_ERROR, SERVER_ERROR_SKIP_PATHS } from './tokens'; -import { ErrorService } from './error.service'; -import { KeycloakStatusService } from '../tenants'; import { LoadingService } from '../loading'; +import { getConfig } from '../services'; +import { KeycloakStatusService } from '../tenants'; +import { ErrorService } from './error.service'; +import { SERVER_ERROR_SKIP_PATHS } from './tokens'; function updateSeconds(seconds: number) { if (seconds === 0 || seconds === 1) { @@ -52,12 +53,12 @@ function backoffOnServerError(maxRetries = 3, skippedPaths: string[] = []): Mono @Injectable() export class ServerErrorInterceptor implements HttpInterceptor { private readonly _urlsWithError = new Set(); + readonly #config = getConfig(); constructor( private readonly _errorService: ErrorService, private readonly _loadingService: LoadingService, private readonly _keycloakStatusService: KeycloakStatusService, - @Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number, @Optional() @Inject(SERVER_ERROR_SKIP_PATHS) private readonly _skippedPaths: string[], ) {} @@ -88,7 +89,7 @@ export class ServerErrorInterceptor implements HttpInterceptor { }), ); }), - backoffOnServerError(this._maxRetries, this._skippedPaths), + backoffOnServerError(this.#config.MAX_RETRIES_ON_SERVER_ERROR, this._skippedPaths), ); } } diff --git a/src/lib/error/tokens.ts b/src/lib/error/tokens.ts index af359ca..57e6c48 100644 --- a/src/lib/error/tokens.ts +++ b/src/lib/error/tokens.ts @@ -1,4 +1,3 @@ import { InjectionToken } from '@angular/core'; -export const MAX_RETRIES_ON_SERVER_ERROR = new InjectionToken('Number of retries before giving up'); export const SERVER_ERROR_SKIP_PATHS = new InjectionToken('A list of paths to skip when handling server errors'); diff --git a/src/lib/help-mode/help-mode.service.ts b/src/lib/help-mode/help-mode.service.ts index 38cc90c..62f697d 100644 --- a/src/lib/help-mode/help-mode.service.ts +++ b/src/lib/help-mode/help-mode.service.ts @@ -1,11 +1,11 @@ import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; -import { getConfig } from '../services'; -import { IqserUserPreferenceService } from '../services'; +import { getConfig, IqserUserPreferenceService } from '../services'; import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component'; -import { HELP_MODE_KEYS, MANUAL_BASE_URL } from './tokens'; +import { HELP_MODE_KEYS } from './tokens'; import { HelpModeKey } from './types'; import { DOCUMINE_THEME_CLASS, @@ -20,7 +20,6 @@ import { ScrollableParentViews, WEB_VIEWER_ELEMENTS, } from './utils/constants'; -import { toSignal } from '@angular/core/rxjs-interop'; export interface Helper { readonly element: HTMLElement; @@ -39,13 +38,13 @@ export class HelpModeService { readonly #isDocumine = getConfig().IS_DOCUMINE; #helpers: Record = {}; #dialogMode = false; + readonly #config = getConfig(); readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable(); readonly isHelpModeActive = toSignal(this.isHelpModeActive$, { initialValue: false }); readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable(); constructor( @Inject(HELP_MODE_KEYS) private readonly _keys: HelpModeKey[], - @Inject(MANUAL_BASE_URL) private readonly _manualBaseURL: string, private readonly _dialog: MatDialog, private readonly _rendererFactory: RendererFactory2, private readonly _translateService: TranslateService, @@ -161,7 +160,7 @@ export class HelpModeService { #generateDocsLink(key: string) { const currentLang = this._translateService.currentLang; - return `${this._manualBaseURL}/${currentLang}/index-${currentLang}.html?contextId=${key}`; + return `${this.#config.MANUAL_BASE_URL}/${currentLang}/index-${currentLang}.html?contextId=${key}`; } #isElementVisible(helper: Helper): boolean { diff --git a/src/lib/help-mode/tokens.ts b/src/lib/help-mode/tokens.ts index 46546ea..f224817 100644 --- a/src/lib/help-mode/tokens.ts +++ b/src/lib/help-mode/tokens.ts @@ -1,8 +1,4 @@ -import { inject, InjectionToken } from '@angular/core'; -import { IqserConfigService } from '../services/iqser-config.service'; +import { InjectionToken } from '@angular/core'; import { HelpModeKey } from './types'; export const HELP_MODE_KEYS = new InjectionToken('Help mode keys'); -export const MANUAL_BASE_URL = new InjectionToken('Base manual URL', { - factory: () => inject(IqserConfigService).values.MANUAL_BASE_URL, -}); diff --git a/src/lib/help-mode/utils/help-mode.provider.ts b/src/lib/help-mode/utils/help-mode.provider.ts index cd1804b..14d6a33 100644 --- a/src/lib/help-mode/utils/help-mode.provider.ts +++ b/src/lib/help-mode/utils/help-mode.provider.ts @@ -1,7 +1,8 @@ +import { makeEnvironmentProviders } from '@angular/core'; import { HelpModeService } from '../help-mode.service'; import { HELP_MODE_KEYS } from '../tokens'; import { HelpModeKey } from '../types'; export function provideHelpMode(helpModeKeys: HelpModeKey[]) { - return [{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService]; + return makeEnvironmentProviders([{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService]); } diff --git a/src/lib/interceptors/api-path.interceptor.ts b/src/lib/interceptors/api-path.interceptor.ts new file mode 100644 index 0000000..8aa2846 --- /dev/null +++ b/src/lib/interceptors/api-path.interceptor.ts @@ -0,0 +1,17 @@ +import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { getConfig } from '../services/iqser-config.service'; +import { UI_ROOT_PATH_FN } from '../utils/tokens'; + +export const apiPathInterceptorFn: HttpInterceptorFn = (req: HttpRequest, next: HttpHandlerFn) => { + const config = getConfig(); + const convertPath = inject(UI_ROOT_PATH_FN); + + if (!req.url.startsWith('/assets')) { + const apiUrl = `${config.API_URL}${req.url}`; + return next(req.clone({ url: apiUrl })); + } + + const url = convertPath(req.url); + return next(req.clone({ url })); +}; diff --git a/src/lib/provide-common-ui.ts b/src/lib/provide-common-ui.ts new file mode 100644 index 0000000..8508a54 --- /dev/null +++ b/src/lib/provide-common-ui.ts @@ -0,0 +1,49 @@ +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { EnvironmentProviders, inject, makeEnvironmentProviders, provideAppInitializer, Provider, Type } from '@angular/core'; +import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { apiPathInterceptorFn } from './interceptors/api-path.interceptor'; +import { CONFIG_SERVICE, DefaultUserPreferenceService, IqserConfigService, IqserUserPreferenceService } from './services'; +import { ICONS } from './utils/constants'; + +type ProvideCommonUiOptions = { + configService: Type; + existingUserPreferenceService: Type; +}; + +export function provideCommonUi(options: ProvideCommonUiOptions): EnvironmentProviders { + return makeEnvironmentProviders([ + provideCommonIcons(), + { + provide: CONFIG_SERVICE, + useExisting: options.configService, + }, + provideHttpClient(withInterceptors([apiPathInterceptorFn])), + { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }, + getService(IqserUserPreferenceService, DefaultUserPreferenceService, options.existingUserPreferenceService), + ]); +} + +export function getService(base: B, _default: Type, existing?: E): Provider { + if (existing) { + return { + provide: base, + useExisting: existing, + }; + } + + return { provide: base, useClass: _default }; +} + +function provideCommonIcons() { + return provideAppInitializer(() => { + const iconRegistry = inject(MatIconRegistry); + const sanitizer = inject(DomSanitizer); + + ICONS.forEach(icon => { + const url = sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/${icon}.svg`); + iconRegistry.addSvgIconInNamespace('iqser', icon, url); + }); + }); +} diff --git a/src/lib/services/api-path.interceptor.ts b/src/lib/services/api-path.interceptor.ts deleted file mode 100644 index 0eb3852..0000000 --- a/src/lib/services/api-path.interceptor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { UI_ROOT_PATH_FN } from '../utils/tokens'; -import { getConfig } from './iqser-config.service'; - -@Injectable() -export class ApiPathInterceptor implements HttpInterceptor { - readonly #config = getConfig(); - readonly #convertPath = inject(UI_ROOT_PATH_FN); - - intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (!req.url.startsWith('/assets')) { - const apiUrl = `${this.#config.API_URL}${req.url}`; - return next.handle(req.clone({ url: apiUrl })); - } - - const url = this.#convertPath(req.url); - return next.handle(req.clone({ url })); - } -} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index fd5d01e..c77f4bd 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -1,5 +1,3 @@ -import exp from 'constants'; - export * from './toaster.service'; export * from './error-message.service'; export * from './generic.service'; @@ -9,5 +7,4 @@ export * from './entities-map.service'; export * from './iqser-user-preference.service'; export * from './language.service'; export * from './iqser-config.service'; -export * from './api-path.interceptor'; export * from './skeleton.service'; diff --git a/src/lib/services/iqser-config.service.ts b/src/lib/services/iqser-config.service.ts index a4f2725..da92ffc 100644 --- a/src/lib/services/iqser-config.service.ts +++ b/src/lib/services/iqser-config.service.ts @@ -1,18 +1,19 @@ -import { Inject, inject, Injectable } from '@angular/core'; +import { inject, InjectionToken } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { CacheApiService } from '../caching/cache-api.service'; import { wipeAllCaches } from '../caching/cache-utils'; -import { IqserAppConfig } from '../utils/iqser-app-config'; -import { LANDING_PAGE_THEMES, MANUAL_BASE_URL, THEME_DIRECTORIES } from '../utils/constants'; import { IStoredTenantId, TenantsService } from '../tenants'; +import { LANDING_PAGE_THEMES, MANUAL_BASE_URL, THEME_DIRECTORIES } from '../utils/constants'; +import { IqserAppConfig } from '../utils/iqser-app-config'; -@Injectable() -export class IqserConfigService { +export const CONFIG_SERVICE = new InjectionToken('IqserConfigService'); + +export abstract class IqserConfigService { protected readonly _cacheApiService = inject(CacheApiService); protected readonly _titleService = inject(Title); protected readonly _tenantsService = inject(TenantsService); - constructor(@Inject('Doesnt matter') protected _values: T) { + protected constructor(protected _values: T) { this._checkFrontendVersion(); this.#updateAppType(); this._titleService.setTitle(this._values.APP_NAME); @@ -73,5 +74,5 @@ export class IqserConfigService { } export function getConfig() { - return inject>(IqserConfigService).values; + return inject>(CONFIG_SERVICE).values; } diff --git a/src/lib/translations/iqser-translate-module-options.ts b/src/lib/translations/iqser-translate-module-options.ts index 6e63989..aad0f7e 100644 --- a/src/lib/translations/iqser-translate-module-options.ts +++ b/src/lib/translations/iqser-translate-module-options.ts @@ -1,3 +1,4 @@ export interface IqserTranslateModuleOptions { readonly pathPrefix?: string; + readonly pathPrefixFactory?: () => string; } diff --git a/src/lib/translations/iqser-translate.module.ts b/src/lib/translations/iqser-translate.module.ts index 23429b0..303350c 100644 --- a/src/lib/translations/iqser-translate.module.ts +++ b/src/lib/translations/iqser-translate.module.ts @@ -45,7 +45,10 @@ export class IqserTranslateModule { providers: [ { provide: translateLoaderToken, - useFactory: () => pruningTranslationLoaderFactory(pathPrefix), + useFactory: () => { + const prefix = options?.pathPrefixFactory !== undefined ? options.pathPrefixFactory() : pathPrefix; + return pruningTranslationLoaderFactory(prefix); + }, }, { provide: MissingTranslationHandler, diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index d6e3597..fe23032 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -17,4 +17,3 @@ export * from './context.component'; export * from './tokens'; export * from './module-options'; export * from './iqser-app-config'; -export * from './types/common-ui-options'; diff --git a/src/lib/utils/iqser-app-config.ts b/src/lib/utils/iqser-app-config.ts index 59cc363..e23daa4 100644 --- a/src/lib/utils/iqser-app-config.ts +++ b/src/lib/utils/iqser-app-config.ts @@ -9,4 +9,5 @@ export interface IqserAppConfig { readonly MANUAL_BASE_URL: string; readonly BASE_TRANSLATIONS_DIRECTORY?: string; readonly LANDING_PAGE_THEME: 'redactmanager' | 'documine' | 'mixed'; + readonly MAX_RETRIES_ON_SERVER_ERROR?: number; } diff --git a/src/lib/utils/types/common-ui-options.ts b/src/lib/utils/types/common-ui-options.ts deleted file mode 100644 index d5fe61b..0000000 --- a/src/lib/utils/types/common-ui-options.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Type } from '@angular/core'; -import { IqserConfigService } from '../../services/iqser-config.service'; -import { IqserUserPreferenceService } from '../../services/iqser-user-preference.service'; -import { IqserAppConfig } from '../iqser-app-config'; - -export interface CommonUiOptions< - UserPreference extends IqserUserPreferenceService, - Config extends IqserConfigService, - AppConfig extends IqserAppConfig, -> { - existingUserPreferenceService?: Type; - configService?: Type; - configServiceFactory: () => Config; -}