File

src/app/core/site-settings/site-settings.service.ts

Description

Access to site settings stored in the database, like styling, site name and logo.

Extends

LatestEntityLoader

Index

Properties
Methods

Constructor

constructor()

Methods

getPropertyObservable
getPropertyObservable(property: P)
Type parameters :
  • P
Parameters :
Name Type Optional
property P No
Returns : Observable<unknown>
init
init()
Returns : void
Async loadOnce
loadOnce()
Inherited from LatestEntityLoader

Do an initial load of the entity to be available through the entityUpdated property (without watching for continuous updates).

Returns undefined if the entity does not exist (HTTP 404). Other errors are propagated and should be handled by the subclass with a domain-specific error so that monitoring tools (e.g. Sentry) can group them properly.

Returns : Promise<T | undefined>
Protected onInit
onInit()
Inherited from LatestEntityLoader

Override this to trigger actions upon initialization of the service

Returns : void
Async startLoading
startLoading()
Inherited from LatestEntityLoader

Initialize the loader to make the entity available and emit continuous updates through the entityUpdated property

Returns : unknown

Properties

dateFormat
Type : unknown
Default value : this.getPropertyObservable("dateFormat")
Readonly DEFAULT_FAVICON
Type : string
Default value : "favicon.ico"
defaultLanguage
Type : unknown
Default value : this.getPropertyObservable("defaultLanguage")
displayLanguageSelect
Type : unknown
Default value : this.getPropertyObservable("displayLanguageSelect")
Readonly SITE_SETTINGS_LOCAL_STORAGE_KEY
Type : unknown
Default value : Entity.createPrefixedId( SiteSettings.ENTITY_TYPE, SiteSettings.ENTITY_ID, )
siteName
Type : unknown
Default value : this.getPropertyObservable("siteName")
siteSettings
Type : unknown
Default value : this.entityUpdated.pipe(shareReplay(1))
entityUpdated
Type : unknown
Default value : new Subject<T>()
Inherited from LatestEntityLoader

subscribe to this and execute any actions required when the entity changes

import { inject, Injectable } from "@angular/core";
import { SiteSettings } from "./site-settings";
import { Observable, skipWhile } from "rxjs";
import { distinctUntilChanged, map, shareReplay, take } from "rxjs/operators";
import { Title } from "@angular/platform-browser";
import materialColours from "@aytek/material-color-picker";
import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service";
import { LatestEntityLoader } from "../entity/latest-entity-loader";
import { Logging } from "../logging/logging.service";
import { Entity } from "../entity/model/entity";
import { EntitySchemaService } from "../entity/schema/entity-schema.service";
import { availableLocales } from "../language/languages";
import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service";
import { EntityConfigReadyService } from "../entity/entity-config-ready.service";

/**
 * Access to site settings stored in the database, like styling, site name and logo.
 */
@Injectable({
  providedIn: "root",
})
export class SiteSettingsService extends LatestEntityLoader<SiteSettings> {
  private title = inject(Title);
  private schemaService = inject(EntitySchemaService);
  private enumService = inject(ConfigurableEnumService);

  readonly DEFAULT_FAVICON = "favicon.ico";
  readonly SITE_SETTINGS_LOCAL_STORAGE_KEY = Entity.createPrefixedId(
    SiteSettings.ENTITY_TYPE,
    SiteSettings.ENTITY_ID,
  );

  siteSettings = this.entityUpdated.pipe(shareReplay(1));

  siteName = this.getPropertyObservable("siteName");
  defaultLanguage = this.getPropertyObservable("defaultLanguage");
  displayLanguageSelect = this.getPropertyObservable("displayLanguageSelect");
  dateFormat = this.getPropertyObservable("dateFormat");

  private entityConfigReady = inject(EntityConfigReadyService);

  constructor() {
    const entityMapper = inject(EntityMapperService);

    super(SiteSettings, SiteSettings.ENTITY_ID, entityMapper);

    this.init();

    // Wait for dynamic entity schemas to be applied before loading from DB,
    // so config-defined SiteSettings fields are transformed correctly.
    this.entityConfigReady.setupCompleted$.pipe(take(1)).subscribe(() => {
      super.startLoading().catch((err) => {
        const error = new Error("Failed to load site settings", { cause: err });
        error.name = "SiteSettingsLoadError";
        Logging.error(error);
      });
    });
  }

  init() {
    this.initAvailableLocales();

    this.siteName.subscribe((name) => this.title.setTitle(name));
    this.subscribeFontChanges();
    this.subscribeColorChanges("primary");
    this.subscribeColorChanges("secondary");
    this.subscribeColorChanges("error");
    this.subscribeDateFormatChanges();

    // Apply cached branding early so login/setup screens use the latest known theme.
    this.initFromLocalStorage();
    this.cacheInLocalStorage();
  }

  /**
   * Making locales enum available at runtime
   * so that UI can show dropdown options
   * @private
   */
  private initAvailableLocales() {
    this.enumService["cacheEnum"](availableLocales);
  }

  /**
   * Do an initial loading of settings from localStorage, if available.
   * @private
   */
  private initFromLocalStorage() {
    let localStorageSettings: SiteSettings;

    try {
      const stored = localStorage.getItem(this.SITE_SETTINGS_LOCAL_STORAGE_KEY);
      if (stored) {
        localStorageSettings = this.schemaService.loadDataIntoEntity(
          new SiteSettings(),
          JSON.parse(stored),
        );
      }
    } catch (e) {
      Logging.debug(
        "SiteSettingsService: could not parse settings from localStorage: " + e,
      );
    }

    if (localStorageSettings) {
      this.entityUpdated.next(localStorageSettings);
    }
  }

  /**
   * Store the latest SiteSettings in localStorage to be available before login also.
   * @private
   */
  private cacheInLocalStorage() {
    this.entityUpdated.subscribe((settings) => {
      const dbFormat =
        this.schemaService.transformEntityToDatabaseFormat(settings);
      localStorage.setItem(
        this.SITE_SETTINGS_LOCAL_STORAGE_KEY,
        JSON.stringify(dbFormat),
      );
    });
  }

  private subscribeFontChanges() {
    this.getPropertyObservable("font").subscribe((font) =>
      document.documentElement.style.setProperty("--font-family", font),
    );
  }

  private subscribeColorChanges(property: "primary" | "secondary" | "error") {
    this.getPropertyObservable(property).subscribe((color) => {
      if (color) {
        try {
          const palette = materialColours(color);
          palette["A100"] = palette["200"];
          palette["A200"] = palette["300"];
          palette["A400"] = palette["500"];
          palette["A700"] = palette["800"];
          Object.entries(palette).forEach(([key, value]) =>
            document.documentElement.style.setProperty(
              `--${property}-${key}`,
              `#${value}`,
            ),
          );
        } catch (e) {
          Logging.warn(
            `SiteSettingsService: invalid color value for "${property}": ${color}`,
            e,
          );
        }
      }
    });
  }

  private subscribeDateFormatChanges() {
    import("../basic-datatypes/date/date.static").then((dateModule) => {
      this.dateFormat.subscribe((format) => {
        // Fall back to default when stored value is blank or corrupt
        const formatToApply = format?.trim() || dateModule.defaultDateFormat();
        dateModule.setGlobalDateFormat(formatToApply);
      });
    });
  }

  getPropertyObservable<P extends keyof SiteSettings>(
    property: P,
  ): Observable<SiteSettings[P]> {
    return this.siteSettings.pipe(
      skipWhile((v) => !v[property]),
      map((s) => s[property]),
      distinctUntilChanged(),
    );
  }
}

results matching ""

    No results matching ""