Merge branch 'VM/RED-10301' into 'unprotected'

RED-10301 - Use RM/DM UI depending on application type of tenant

See merge request fforesight/shared-ui-libraries/common-ui!14
This commit is contained in:
Dan Percic 2024-12-03 13:16:28 +01:00
commit b929f1d136
11 changed files with 106 additions and 15 deletions

View File

@ -18,9 +18,7 @@ export interface QueryParam {
*/
export abstract class GenericService<I> {
protected readonly _http = inject(HttpClient);
protected readonly _lastCheckedForChanges = new Map<string, string>([
[ROOT_CHANGES_KEY, new Date(Date.now()).toISOString()],
]);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, new Date(Date.now()).toISOString()]]);
protected abstract readonly _defaultModelPath: string;
protected readonly _serviceName: string = 'redaction-gateway-v1';
@ -40,7 +38,7 @@ export abstract class GenericService<I> {
headers: HeadersConfiguration.getHeaders({ contentType: false }),
observe: 'body',
params: this._queryParams(queryParams),
})
});
}
getFor<R = I[]>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {

View File

@ -3,6 +3,8 @@ 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 { THEME_DIRECTORIES } from '../utils/constants';
import { IStoredTenantId } from '../tenants';
@Injectable()
export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
@ -23,6 +25,15 @@ export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
this._titleService.setTitle(this._values.APP_NAME);
}
updateIsDocumine(tenant: IStoredTenantId): void {
const isDocumine = tenant.documine;
this._values = {
...this._values,
IS_DOCUMINE: isDocumine,
THEME: !isDocumine ? THEME_DIRECTORIES.REDACT : THEME_DIRECTORIES.SCM,
};
}
protected _checkFrontendVersion(): void {
this._cacheApiService.getCachedValue('FRONTEND_APP_VERSION').then(async lastVersion => {
const version = this._values.FRONTEND_APP_VERSION;

View File

@ -1,9 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { NgClass } from '@angular/common';
@Component({
selector: 'iqser-logo',
template: ` <mat-icon [svgIcon]="icon"></mat-icon>`,
template: `<mat-icon [svgIcon]="icon()"></mat-icon>`,
styles: [
`
:host {
@ -18,8 +19,8 @@ import { MatIconModule } from '@angular/material/icon';
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule],
imports: [MatIconModule, NgClass],
})
export class LogoComponent {
@Input({ required: true }) icon!: string;
icon = input.required<string>();
}

View File

@ -1,14 +1,18 @@
import { inject, Injectable, signal } from '@angular/core';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
import { Observable } from 'rxjs';
import { firstValueFrom, Observable, take } from 'rxjs';
import { GenericService } from '../../services';
import { List } from '../../utils';
import { Tenant, TenantDetails } from '../types';
import { APPLICATION_TYPES } from '../../utils/constants';
import { toObservable } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs/operators';
export interface IStoredTenantId {
readonly tenantId: string;
readonly created: string;
readonly documine: boolean;
}
export type StoredTenantIds = List<IStoredTenantId>;
@ -27,6 +31,8 @@ export class TenantsService extends GenericService<Tenant> {
key: localStorage.key.bind(localStorage),
};
readonly #activeTenantId = signal('');
readonly #tenantSet = signal(false);
readonly tenantSet$ = toObservable(this.#tenantSet);
protected readonly _defaultModelPath = 'tenants';
protected override readonly _serviceName: string = 'tenant-user-management';
@ -34,18 +40,26 @@ export class TenantsService extends GenericService<Tenant> {
return this.#activeTenantId();
}
get activeTenant() {
return this.getStoredTenants().find(t => t.tenantId === this.activeTenantId);
}
async selectTenant(tenantId: string): Promise<boolean> {
this.#mutateStorage(tenantId);
this.#setActiveTenantId(tenantId);
return true;
}
storeTenant() {
async storeTenant() {
const storedTenants = this.getStoredTenants();
const activeTenantId = this.#activeTenantId();
const existing = storedTenants.find(s => s.tenantId === activeTenantId);
const tenant = await firstValueFrom(this.getActiveTenant());
if (existing) {
this.#logger.info('[TENANTS] Stored tenant exists: ', storedTenants);
this.#tenantSet.set(true);
return;
}
@ -54,8 +68,13 @@ export class TenantsService extends GenericService<Tenant> {
return;
}
storedTenants.push({ tenantId: activeTenantId, created: new Date().toISOString() });
storedTenants.push({
tenantId: activeTenantId,
created: new Date().toISOString(),
documine: tenant.applicationType === APPLICATION_TYPES.DOCUMINE,
});
this.#storageReference.setItem(STORED_TENANTS_KEY, JSON.stringify(storedTenants));
this.#tenantSet.set(true);
this.#logger.info('[TENANTS] Stored tenants: ', storedTenants);
}
@ -104,6 +123,13 @@ export class TenantsService extends GenericService<Tenant> {
return this._getOne([this.activeTenantId]);
}
waitForSettingTenant(): Observable<boolean> {
return this.tenantSet$.pipe(
filter(tenantSet => tenantSet),
take(1),
);
}
#setActiveTenantId(tenantId: string) {
this.#logger.info('[TENANTS] Set current tenant id: ', tenantId);
this.#activeTenantId.set(tenantId);

View File

@ -21,7 +21,7 @@
<div>
@for (stored of storedTenants; track stored) {
<div (click)="select(stored.tenantId)" class="d-flex pointer mat-elevation-z2 card stored-tenant-card mt-10">
<iqser-logo class="card-icon" icon="iqser:logo" mat-card-image></iqser-logo>
<iqser-logo class="card-icon" [icon]="tenantIcon(stored)" mat-card-image></iqser-logo>
<div class="card-content flex-column">
<span class="heading">{{ stored.tenantId }}</span>
</div>

View File

@ -38,6 +38,10 @@ export class TenantSelectComponent {
this.#loadStoredTenants();
}
protected tenantIcon(tenant: IStoredTenantId) {
return `red:${tenant.documine ? 'documine' : 'redaction'}-logo`;
}
updateTenantSelection() {
const tenantId = this.form.controls.tenantId.value;
if (!tenantId) {

View File

@ -1,7 +1,10 @@
import { ApplicationType } from '../../utils/constants';
export type TenantDetails = Record<string, unknown>;
export interface Tenant<TD extends TenantDetails = TenantDetails> {
tenantId: string;
displayName: string;
applicationType: ApplicationType;
details: TD;
}

View File

@ -2,11 +2,13 @@ import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { getConfig } from '../services';
import { PruningTranslationLoader } from '../utils';
import { TenantsService } from '../tenants';
export function pruningTranslationLoaderFactory(pathPrefix: string): PruningTranslationLoader {
const httpClient = inject(HttpClient);
const tenantService = inject(TenantsService);
const config = getConfig();
const version = config.FRONTEND_APP_VERSION;
return new PruningTranslationLoader(httpClient, pathPrefix, `.json?version=${version}`);
return new PruningTranslationLoader(httpClient, tenantService, pathPrefix, `.json?version=${version}`);
}

View File

@ -1,7 +1,11 @@
import { HttpClient } from '@angular/common/http';
import { TranslateLoader } from '@ngx-translate/core';
import { map } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { TenantsService } from '../tenants';
import { APP_TYPE_PATHS } from '../utils/constants';
import { inject } from '@angular/core';
import { GET_TENANT_FROM_PATH_FN } from '../utils';
interface T {
[key: string]: string | T;
@ -10,12 +14,30 @@ interface T {
export class PruningTranslationLoader implements TranslateLoader {
constructor(
private readonly _http: HttpClient,
private readonly _tenantService: TenantsService,
private readonly _prefix: string,
private readonly _suffix: string,
) {}
getTranslation(lang: string): Observable<T> {
return this._http.get(`${this._prefix}${lang}${this._suffix}`).pipe(map(result => this._process(result as T)));
const tenant = inject(GET_TENANT_FROM_PATH_FN)();
if (tenant) {
return this._tenantService.waitForSettingTenant().pipe(
switchMap(() => {
const tenant = this._tenantService.activeTenant;
const translationPath = tenant?.documine ? APP_TYPE_PATHS.SCM : APP_TYPE_PATHS.REDACT;
return this._http
.get(`${this._prefix}${translationPath}/${lang}${this._suffix}`)
.pipe(map(result => this._process(result as T)));
}),
);
}
return this._http
.get(`${this._prefix}${APP_TYPE_PATHS.REDACT}/${lang}${this._suffix}`)
.pipe(map(result => this._process(result as T)));
}
private _process(object: T): T {

View File

@ -51,3 +51,26 @@ export const ICONS = new Set([
'visibility',
'visibility-off',
]);
export const LANDING_PAGE_THEMES = {
REDACT_MANAGER: 'redactmanager',
DOCUMINE: 'documine',
MIXED: 'mixed',
} as const;
export const THEME_DIRECTORIES = {
REDACT: 'redact',
SCM: 'scm',
} as const;
export const APP_TYPE_PATHS = {
REDACT: 'redact',
SCM: 'scm',
} as const;
export const APPLICATION_TYPES = {
REDACT_MANAGER: 'RedactManager',
DOCUMINE: 'DocuMine',
} as const;
export type ApplicationType = (typeof APPLICATION_TYPES)[keyof typeof APPLICATION_TYPES];

View File

@ -8,4 +8,5 @@ export interface IqserAppConfig {
readonly OAUTH_URL: string;
readonly MANUAL_BASE_URL: string;
readonly BASE_TRANSLATIONS_DIRECTORY?: string;
readonly LANDING_PAGE_THEME: 'redactmanager' | 'documine' | 'mixed';
}