From c651a6d863b4f2c55f6c00a66d696bab1c73bd40 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 25 Jul 2022 17:20:05 +0300 Subject: [PATCH] add user preference service --- .eslintrc.json | 1 + src/lib/common-ui.module.ts | 26 ++++++- .../services/base-user-preference.service.ts | 70 +++++++++++++++++++ .../default-user-preference.service.ts | 9 +++ src/lib/services/index.ts | 1 + src/lib/utils/index.ts | 1 + src/lib/utils/tokens.ts | 34 +++++++++ src/lib/utils/types/common-ui-options.ts | 6 ++ 8 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/lib/services/base-user-preference.service.ts create mode 100644 src/lib/services/default-user-preference.service.ts create mode 100644 src/lib/utils/tokens.ts create mode 100644 src/lib/utils/types/common-ui-options.ts diff --git a/.eslintrc.json b/.eslintrc.json index 6d61317..415447a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -58,6 +58,7 @@ "no-param-reassign": "error", "no-dupe-class-members": "off", "no-redeclare": "off", + "no-unused-vars": "off", "consistent-return": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/lines-between-class-members": "off" diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts index d6b3e6a..d44854a 100644 --- a/src/lib/common-ui.module.ts +++ b/src/lib/common-ui.module.ts @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule, Provider } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; import { TranslateModule } from '@ngx-translate/core'; @@ -36,6 +36,9 @@ import { DragDropFileUploadDirective } from './upload-file/drag-drop-file-upload import { MatProgressBarModule } from '@angular/material/progress-bar'; import { ConfirmationDialogComponent } from './dialog'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { DefaultUserPreferenceService } from './services/default-user-preference.service'; +import { CommonUiOptions } from './utils/types/common-ui-options'; +import { BaseUserPreferenceService } from './services'; const matModules = [MatIconModule, MatProgressSpinnerModule, MatButtonModule, MatDialogModule, MatCheckboxModule, MatTooltipModule]; const modules = [ @@ -73,4 +76,23 @@ const pipes = [SortByPipe, HumanizePipe, CapitalizePipe, LogPipe]; imports: [CommonModule, ...matModules, ...modules, FormsModule, ReactiveFormsModule, KeycloakAngularModule, MatProgressBarModule], exports: [...components, ...pipes, ...modules, LogPipe], }) -export class CommonUiModule {} +export class CommonUiModule { + static forRoot(options?: CommonUiOptions): ModuleWithProviders { + const userPreferenceService = this._getUserPreferenceService(options); + + return { + ngModule: CommonUiModule, + providers: [userPreferenceService], + }; + } + + private static _getUserPreferenceService(options?: CommonUiOptions): Provider { + if (options?.existingUserPreferenceService) { + return { + provide: BaseUserPreferenceService, + useExisting: options.existingUserPreferenceService, + }; + } + return { provide: BaseUserPreferenceService, useClass: DefaultUserPreferenceService }; + } +} diff --git a/src/lib/services/base-user-preference.service.ts b/src/lib/services/base-user-preference.service.ts new file mode 100644 index 0000000..41dc97f --- /dev/null +++ b/src/lib/services/base-user-preference.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { List, RequiredParam, Validate } from '../utils'; +import { GenericService } from './generic.service'; + +export type UserAttributes = Record; + +const KEYS = { + language: 'Language', + theme: 'Theme', +} as const; + +@Injectable() +export abstract class BaseUserPreferenceService extends GenericService { + protected abstract readonly _devFeaturesEnabledKey: string; + #userAttributes: UserAttributes = {}; + + get userAttributes(): UserAttributes { + return this.#userAttributes; + } + + get areDevFeaturesEnabled(): boolean { + const value = sessionStorage.getItem(this._devFeaturesEnabledKey); + return value === 'true' ?? false; + } + + getTheme(): string { + return this._getAttribute(KEYS.theme, 'light'); + } + + async saveTheme(theme: 'light' | 'dark'): Promise { + await this._save(KEYS.theme, theme); + window.location.reload(); + } + + getLanguage(): string { + return this._getAttribute(KEYS.language); + } + + async saveLanguage(language: string): Promise { + await this._save(KEYS.language, language); + } + + toggleDevFeatures(): void { + sessionStorage.setItem(this._devFeaturesEnabledKey, String(!this.areDevFeaturesEnabled)); + window.location.reload(); + } + + async reload(): Promise { + const attributes = await firstValueFrom(this.getAll()); + this.#userAttributes = attributes ?? {}; + } + + @Validate() + savePreferences(@RequiredParam() body: List, @RequiredParam() key: string) { + return this._put(body, `${this._defaultModelPath}/${key}`); + } + + private async _save(key: string, value: string): Promise { + this.userAttributes[key] = [value]; + await firstValueFrom(this.savePreferences([value], key)); + } + + private _getAttribute(key: string, defaultValue = ''): string { + if (this.userAttributes[key]?.length > 0) { + return this.userAttributes[key][0]; + } + return defaultValue; + } +} diff --git a/src/lib/services/default-user-preference.service.ts b/src/lib/services/default-user-preference.service.ts new file mode 100644 index 0000000..e63310f --- /dev/null +++ b/src/lib/services/default-user-preference.service.ts @@ -0,0 +1,9 @@ +import { inject, Injectable } from '@angular/core'; +import { BASE_HREF } from '../utils'; +import { BaseUserPreferenceService } from './base-user-preference.service'; + +@Injectable() +export class DefaultUserPreferenceService extends BaseUserPreferenceService { + protected readonly _defaultModelPath = 'attributes'; + protected readonly _devFeaturesEnabledKey = inject(BASE_HREF) + '.enable-dev-features'; +} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 365ad26..c4f13ef 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -5,3 +5,4 @@ export * from './generic.service'; export * from './composite-route.guard'; export * from './stats.service'; export * from './entities-map.service'; +export * from './base-user-preference.service'; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 6fc3c99..356df34 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -16,3 +16,4 @@ export * from './pruning-translation-loader'; export * from './custom-route-reuse.strategy'; export * from './headers-configuration'; export * from './context.component'; +export * from './tokens'; diff --git a/src/lib/utils/tokens.ts b/src/lib/utils/tokens.ts new file mode 100644 index 0000000..dfe414c --- /dev/null +++ b/src/lib/utils/tokens.ts @@ -0,0 +1,34 @@ +import { inject, InjectionToken } from '@angular/core'; +import { PlatformLocation } from '@angular/common'; + +export const BASE_HREF = new InjectionToken('BASE_HREF', { + factory: () => { + const baseUrl = inject(PlatformLocation).getBaseHrefFromDOM(); + if (!baseUrl) { + return ''; + } + + if (baseUrl[baseUrl.length - 1] === '/') { + return baseUrl.substring(0, baseUrl.length - 1); + } + + return baseUrl; + }, +}); + +export type BaseHrefFn = (path: string) => string; + +export const BASE_HREF_FN = new InjectionToken('Convert path function', { + factory: () => (path: string) => { + const baseUrl = inject(BASE_HREF); + if (!baseUrl) { + throw new Error('BASE_HREF is not defined'); + } + + if (path[0] === '/') { + return baseUrl + path; + } + + return baseUrl + '/' + path; + }, +}); diff --git a/src/lib/utils/types/common-ui-options.ts b/src/lib/utils/types/common-ui-options.ts new file mode 100644 index 0000000..0807715 --- /dev/null +++ b/src/lib/utils/types/common-ui-options.ts @@ -0,0 +1,6 @@ +import { Type } from '@angular/core'; +import { BaseUserPreferenceService } from '../../services'; + +export interface CommonUiOptions { + existingUserPreferenceService: Type; +}