RED-6523: simplify tenants handling

This commit is contained in:
Dan Percic 2023-04-08 20:44:00 +03:00
parent b0afe65bbe
commit 069a345aa3
18 changed files with 281 additions and 308 deletions

View File

@ -3,6 +3,8 @@ import {
CompositeRouteGuard, CompositeRouteGuard,
CustomRouteReuseStrategy, CustomRouteReuseStrategy,
DEFAULT_REDIRECT_KEY, DEFAULT_REDIRECT_KEY,
ifLoggedIn,
ifNotLoggedIn,
IqserAuthGuard, IqserAuthGuard,
IqserPermissionsGuard, IqserPermissionsGuard,
IqserRoutes, IqserRoutes,
@ -24,6 +26,7 @@ import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIE
import { DossierFilesGuard } from '@guards/dossier-files-guard'; import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { WebViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard'; import { WebViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
import { mainResolver } from '@utils/main.resolver';
const dossierTemplateIdRoutes: IqserRoutes = [ const dossierTemplateIdRoutes: IqserRoutes = [
{ {
@ -94,20 +97,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [
}, },
]; ];
const routes: IqserRoutes = [ const mainRoutes: IqserRoutes = [
{
path: '',
pathMatch: 'full',
component: TenantResolveComponent,
},
{
path: ':tenant',
component: TenantResolveComponent,
},
{
path: ':tenant/main',
component: BaseScreenComponent,
children: [
{ {
path: '', path: '',
redirectTo: 'dashboard', redirectTo: 'dashboard',
@ -214,7 +204,28 @@ const routes: IqserRoutes = [
}, },
}, },
}, },
], ];
const routes: IqserRoutes = [
{
path: '',
pathMatch: 'full',
canActivate: [ifNotLoggedIn],
component: TenantResolveComponent,
},
{
path: ':tenant',
redirectTo: ':tenant/main',
pathMatch: 'full',
},
{
path: ':tenant/main',
canActivate: [ifLoggedIn],
component: BaseScreenComponent,
resolve: {
whateverThisMainRouteNeeds: mainResolver,
},
children: mainRoutes,
}, },
{ {
path: ':tenant/auth-error', path: ':tenant/auth-error',

View File

@ -1,17 +1,5 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<redaction-pdf-viewer [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator> <iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status> <iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error> <iqser-full-page-error></iqser-full-page-error>
<iqser-skeleton [templates]="{ dashboard: dashboardSkeleton, dossier: dossierSkeleton }"></iqser-skeleton>
<ng-template #dashboardSkeleton>
<redaction-dashboard-skeleton></redaction-dashboard-skeleton>
</ng-template>
<ng-template #dossierSkeleton>
<redaction-dossier-skeleton></redaction-dossier-skeleton>
</ng-template>

View File

@ -1,11 +1,8 @@
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service'; import { RouterHistoryService } from '@services/router-history.service';
import { REDDocumentViewer } from './modules/pdf-viewer/services/document-viewer.service';
import { DossiersChangesService } from '@services/dossiers/dossier-changes.service';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { UserPreferenceService } from '@users/user-preference.service'; import { UserPreferenceService } from '@users/user-preference.service';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui'; import { getConfig } from '@iqser/common-ui';
import { ROLES } from '@users/roles';
import { AppConfig } from '@red/domain'; import { AppConfig } from '@red/domain';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -38,22 +35,13 @@ export class AppComponent implements OnDestroy {
/** RouterHistoryService needs to be injected for last dossiers screen to be updated on first app load */ /** RouterHistoryService needs to be injected for last dossiers screen to be updated on first app load */
private readonly _routerHistoryService: RouterHistoryService, private readonly _routerHistoryService: RouterHistoryService,
userPreferenceService: UserPreferenceService, userPreferenceService: UserPreferenceService,
readonly documentViewer: REDDocumentViewer,
dossierChangesService: DossiersChangesService,
@Inject(DOCUMENT) document: Document, @Inject(DOCUMENT) document: Document,
renderer: Renderer2, renderer: Renderer2,
permissionsService: IqserPermissionsService,
private readonly _router: Router, private readonly _router: Router,
route: ActivatedRoute, route: ActivatedRoute,
) { ) {
renderer.addClass(document.body, userPreferenceService.getTheme()); renderer.addClass(document.body, userPreferenceService.getTheme());
loadCustomTheme(); loadCustomTheme();
// TODO: Find a better place to initialize dossiers refresh
if (permissionsService.has(ROLES.dossiers.read)) {
const refreshSub = dossierChangesService.initializeRefresh().subscribe();
this.#subscription.add(refreshSub);
}
const sub = route.queryParamMap.subscribe(queryParams => this.#navigate(queryParams)); const sub = route.queryParamMap.subscribe(queryParams => this.#navigate(queryParams));
this.#subscription.add(sub); this.#subscription.add(sub);
} }

View File

@ -1,12 +1,11 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; import { ENVIRONMENT_INITIALIZER, ErrorHandler, inject, NgModule } from '@angular/core';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component'; import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { MissingTranslationHandler } from '@ngx-translate/core'; import { MissingTranslationHandler } from '@ngx-translate/core';
import { import {
BASE_HREF,
CachingModule, CachingModule,
CircleButtonComponent, CircleButtonComponent,
CommonUiModule, CommonUiModule,
@ -19,10 +18,8 @@ import {
IqserHelpModeModule, IqserHelpModeModule,
IqserListingModule, IqserListingModule,
IqserLoadingModule, IqserLoadingModule,
IqserPermissionsService,
IqserTranslateModule, IqserTranslateModule,
IqserUsersModule, IqserUsersModule,
KeycloakStatusService,
LanguageService, LanguageService,
LogoComponent, LogoComponent,
MAX_RETRIES_ON_SERVER_ERROR, MAX_RETRIES_ON_SERVER_ERROR,
@ -48,37 +45,31 @@ import { FileUploadDownloadModule } from '@upload-download/file-upload-download.
import { DatePipe as BaseDatePipe } from '@angular/common'; import { DatePipe as BaseDatePipe } from '@angular/common';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens'; import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { GlobalErrorHandler } from '@utils/global-error-handler.service'; import { GlobalErrorHandler } from '@services/global-error-handler.service';
import { REDMissingTranslationHandler } from '@utils/missing-translations-handler'; import { REDMissingTranslationHandler } from '@utils/missing-translations-handler';
import { configurationInitializer } from '@utils/configuration.initializer';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { SpotlightSearchComponent } from '@components/spotlight-search/spotlight-search.component'; import { SpotlightSearchComponent } from '@components/spotlight-search/spotlight-search.component';
import { DatePipe } from '@shared/pipes/date.pipe'; import { DatePipe } from '@shared/pipes/date.pipe';
import * as links from '../assets/help-mode/links.json'; import * as links from '../assets/help-mode/links.json';
import { KeycloakService } from 'keycloak-angular';
import { GeneralSettingsService } from '@services/general-settings.service';
import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component'; import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component';
import { UserPreferenceService } from '@users/user-preference.service'; import { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service'; import { UserService } from '@users/user.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service'; import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { FeaturesService } from '@services/features.service';
import { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip'; import { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip';
import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger'; import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger';
import { LoggerRulesService } from '@services/logger-rules.service'; import { LoggerRulesService } from '@services/logger-rules.service';
import { AppConfig, ILoggerConfig } from '@red/domain'; import { AppConfig, ILoggerConfig } from '@red/domain';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module'; import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { LicenseService } from '@services/license.service';
import { UI_CACHES } from '@utils/constants'; import { UI_CACHES } from '@utils/constants';
import { RedRoleGuard } from '@users/red-role.guard'; import { RedRoleGuard } from '@users/red-role.guard';
import { SkeletonTopBarComponent } from '@components/skeleton/skeleton-top-bar/skeleton-top-bar.component'; import { SkeletonTopBarComponent } from '@components/skeleton/skeleton-top-bar/skeleton-top-bar.component';
import { DossierSkeletonComponent } from '@components/skeleton/dossier-skeleton/dossier-skeleton.component'; import { DossierSkeletonComponent } from '@components/skeleton/dossier-skeleton/dossier-skeleton.component';
import { SkeletonStatsComponent } from '@components/skeleton/skeleton-stats/skeleton-stats.component'; import { SkeletonStatsComponent } from '@components/skeleton/skeleton-stats/skeleton-stats.component';
const screens = [BaseScreenComponent, DownloadsListScreenComponent]; export const appModuleFactory = (config: AppConfig) => {
@NgModule({
const components = [ declarations: [
AppComponent, AppComponent,
AuthErrorComponent, AuthErrorComponent,
NotificationsComponent, NotificationsComponent,
@ -88,13 +79,9 @@ const components = [
DossierSkeletonComponent, DossierSkeletonComponent,
SkeletonTopBarComponent, SkeletonTopBarComponent,
SkeletonStatsComponent, SkeletonStatsComponent,
BaseScreenComponent,
...screens, DownloadsListScreenComponent,
]; ],
export const appModuleFactory = (config: AppConfig) => {
@NgModule({
declarations: [...components],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
@ -187,23 +174,12 @@ export const appModuleFactory = (config: AppConfig) => {
useClass: GlobalErrorHandler, useClass: GlobalErrorHandler,
}, },
{ {
provide: APP_INITIALIZER, provide: ENVIRONMENT_INITIALIZER,
multi: true, multi: true,
useFactory: configurationInitializer, useValue: async () => {
deps: [ const languageService = inject(LanguageService);
BASE_HREF, return languageService.setInitialLanguage();
KeycloakStatusService, },
KeycloakService,
ConfigService,
SystemPreferencesService,
FeaturesService,
GeneralSettingsService,
LanguageService,
UserService,
UserPreferenceService,
LicenseService,
IqserPermissionsService,
],
}, },
{ {
provide: MissingTranslationHandler, provide: MissingTranslationHandler,

View File

@ -43,10 +43,6 @@
</a> </a>
</ng-container> </ng-container>
<button (click)="selectTenant()" *ngIf="tenantContext.hasMultipleTenants" id="select-tenant" mat-menu-item>
<span translate="top-bar.navigation-items.my-account.children.select-tenant"> </span>
</button>
<button (click)="userService.logout()" id="logout" mat-menu-item> <button (click)="userService.logout()" id="logout" mat-menu-item>
<mat-icon svgIcon="iqser:logout"></mat-icon> <mat-icon svgIcon="iqser:logout"></mat-icon>
<span translate="top-bar.navigation-items.my-account.children.logout"> </span> <span translate="top-bar.navigation-items.my-account.children.logout"> </span>
@ -59,3 +55,15 @@
<div *ngIf="userPreferenceService.areDevFeaturesEnabled" class="dev-mode" translate="dev-mode"></div> <div *ngIf="userPreferenceService.areDevFeaturesEnabled" class="dev-mode" translate="dev-mode"></div>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<redaction-pdf-viewer [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<iqser-skeleton [templates]="{ dashboard: dashboardSkeleton, dossier: dossierSkeleton }"></iqser-skeleton>
<ng-template #dashboardSkeleton>
<redaction-dashboard-skeleton></redaction-dashboard-skeleton>
</ng-template>
<ng-template #dossierSkeleton>
<redaction-dossier-skeleton></redaction-dossier-skeleton>
</ng-template>

View File

@ -7,11 +7,12 @@ import { TranslateService } from '@ngx-translate/core';
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action'; import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { filter, map, startWith } from 'rxjs/operators'; import { filter, map, startWith } from 'rxjs/operators';
import { BASE_HREF, IqserPermissionsService, shareDistinctLast, TenantContext, TenantContextHolder } from '@iqser/common-ui'; import { BASE_HREF, IqserPermissionsService, shareDistinctLast, TenantContextHolder, TenantsService } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service'; import { BreadcrumbsService } from '@services/breadcrumbs.service';
import { FeaturesService } from '@services/features.service'; import { FeaturesService } from '@services/features.service';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain'; import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
import { REDDocumentViewer } from '../../modules/pdf-viewer/services/document-viewer.service';
interface MenuItem { interface MenuItem {
readonly id: string; readonly id: string;
@ -31,8 +32,8 @@ const isSearchScreen: (url: string) => boolean = url => url.includes('/search');
}) })
export class BaseScreenComponent { export class BaseScreenComponent {
readonly roles = ROLES; readonly roles = ROLES;
private readonly _baseHref = inject(BASE_HREF); readonly tenantContext = inject(TenantsService);
readonly tenantContext = inject(TenantContext); readonly documentViewer = inject(REDDocumentViewer);
readonly currentUser = this.userService.currentUser; readonly currentUser = this.userService.currentUser;
readonly userMenuItems: readonly MenuItem[] = [ readonly userMenuItems: readonly MenuItem[] = [
{ {
@ -59,6 +60,12 @@ export class BaseScreenComponent {
routerLink: '/main/trash', routerLink: '/main/trash',
show: this.currentUser.isUser && this.permissionsService.has([ROLES.dossiers.read, ROLES.files.readStatus]), show: this.currentUser.isUser && this.permissionsService.has([ROLES.dossiers.read, ROLES.files.readStatus]),
}, },
{
id: 'select-tenant',
name: _('top-bar.navigation-items.my-account.children.select-tenant'),
action: () => this.selectTenant(),
show: this.tenantContext.hasMultipleTenants,
},
]; ];
readonly searchActions: readonly SpotlightSearchAction[] = [ readonly searchActions: readonly SpotlightSearchAction[] = [
{ {
@ -79,6 +86,7 @@ export class BaseScreenComponent {
action: (query): void => this._search(query, []), action: (query): void => this._search(query, []),
}, },
]; ];
private readonly _baseHref = inject(BASE_HREF);
private readonly _navigationStart$ = this._router.events.pipe( private readonly _navigationStart$ = this._router.events.pipe(
filter(isNavigationStart), filter(isNavigationStart),
map((event: NavigationStart) => event.url), map((event: NavigationStart) => event.url),
@ -113,6 +121,10 @@ export class BaseScreenComponent {
return item.name; return item.name;
} }
selectTenant() {
window.open(window.location.origin + this._baseHref, '_blank');
}
private _search(query: string, dossierIds: string[], onlyActive = false) { private _search(query: string, dossierIds: string[], onlyActive = false) {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive }; const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
this._router.navigate([`/${this._tenantContextHolder.currentTenant}/main/search`], { queryParams }).then(); this._router.navigate([`/${this._tenantContextHolder.currentTenant}/main/search`], { queryParams }).then();
@ -126,8 +138,4 @@ export class BaseScreenComponent {
const dossierId = routerLink[2]; const dossierId = routerLink[2];
return this._search(query, [dossierId]); return this._search(query, [dossierId]);
} }
selectTenant() {
window.open(window.location.origin + this._baseHref, '_blank');
}
} }

View File

@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { SystemPreferences } from '@red/domain'; import { SystemPreferences } from '@red/domain';
import { BaseFormComponent, IqserPermissionsService, KeysOf, LoadingService } from '@iqser/common-ui'; import { BaseFormComponent, IqserPermissionsService, KeysOf, LoadingService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { SystemPreferencesService } from '@services/system-preferences.service'; import { SystemPreferencesService } from '@services/system-preferences.service';
import { systemPreferencesTranslations } from '@translations/system-preferences-translations'; import { systemPreferencesTranslations } from '@translations/system-preferences-translations';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
@ -38,7 +37,7 @@ export class SystemPreferencesFormComponent extends BaseFormComponent {
async save(): Promise<void> { async save(): Promise<void> {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom(this._systemPreferencesService.update(this.form.getRawValue())); await this._systemPreferencesService.update(this.form.getRawValue());
this._loadData(); this._loadData();
this._loadingService.stop(); this._loadingService.stop();
} }

View File

@ -1,18 +1,19 @@
import { GenericService, LAST_CHECKED_OFFSET, List, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui'; import { GenericService, LAST_CHECKED_OFFSET, List, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossierChanges } from '@red/domain'; import { Dossier, DossierStats, IDossierChanges } from '@red/domain';
import { forkJoin, Observable, of, throwError, timer } from 'rxjs'; import { forkJoin, Observable, of, Subscription, throwError, timer } from 'rxjs';
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { ActiveDossiersService } from './active-dossiers.service'; import { ActiveDossiersService } from './active-dossiers.service';
import { ArchivedDossiersService } from './archived-dossiers.service'; import { ArchivedDossiersService } from './archived-dossiers.service';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, OnDestroy } from '@angular/core';
import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service'; import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class DossiersChangesService extends GenericService<Dossier> { export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy {
protected readonly _defaultModelPath = 'dossier'; protected readonly _defaultModelPath = 'dossier';
readonly #subscription = new Subscription();
readonly #activeDossiersService = inject(ActiveDossiersService); readonly #activeDossiersService = inject(ActiveDossiersService);
readonly #archivedDossiersService = inject(ArchivedDossiersService); readonly #archivedDossiersService = inject(ArchivedDossiersService);
@ -31,7 +32,7 @@ export class DossiersChangesService extends GenericService<Dossier> {
}); });
const load = (changes: IDossierChanges) => const load = (changes: IDossierChanges) =>
changes.map(change => this._load(change.dossierId).pipe(removeIfNotFound(change.dossierId))); changes.map(change => this.#load(change.dossierId).pipe(removeIfNotFound(change.dossierId)));
return this.hasChangesDetails$().pipe( return this.hasChangesDetails$().pipe(
tap(changes => this.#logger.info('[CHANGES] ', changes)), tap(changes => this.#logger.info('[CHANGES] ', changes)),
@ -50,17 +51,23 @@ export class DossiersChangesService extends GenericService<Dossier> {
); );
} }
initializeRefresh() { initialize() {
return timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL).pipe( this.#logger.info('[DOSSIERS_CHANGES] Initialize timer');
const subscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL).pipe(
switchMap(() => this.loadOnlyChanged()), switchMap(() => this.loadOnlyChanged()),
tap(changes => { tap(changes => {
this.#activeDossiersService.emitFileChanges(changes); this.#activeDossiersService.emitFileChanges(changes);
this.#archivedDossiersService.emitFileChanges(changes); this.#archivedDossiersService.emitFileChanges(changes);
}), }),
); );
this.#subscription.add(subscription.subscribe());
} }
private _load(id: string): Observable<DossierStats[]> { ngOnDestroy() {
this.#subscription.unsubscribe();
}
#load(id: string): Observable<DossierStats[]> {
const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }]; const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe( return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity)), map(entity => new Dossier(entity)),

View File

@ -1,6 +1,8 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import featuresJson from '../../assets/features/features.json'; import featuresJson from '../../assets/features/features.json';
import { getConfig } from '@iqser/common-ui';
import { AppConfig } from '@red/domain';
import { NGXLogger } from 'ngx-logger';
function transform(version: string): number { function transform(version: string): number {
const [major, minor, patch, release] = version.split(/[.-]/).map(x => parseInt(x, 10)); const [major, minor, patch, release] = version.split(/[.-]/).map(x => parseInt(x, 10));
@ -11,19 +13,20 @@ function transform(version: string): number {
providedIn: 'root', providedIn: 'root',
}) })
export class FeaturesService { export class FeaturesService {
private _features = new Map<string, boolean>(); readonly #features = new Map<string, boolean>();
readonly #config = getConfig<AppConfig>();
constructor(private readonly _configService: ConfigService) {} readonly #logger = inject(NGXLogger);
isEnabled(feature: string): boolean { isEnabled(feature: string): boolean {
// If feature is not defined in config object return true // If feature is not defined in config object return true
return this._features.get(feature) !== false; return this.#features.get(feature) !== false;
} }
loadConfig() { loadConfig() {
const BACKEND_APP_VERSION = transform(this._configService.values.BACKEND_APP_VERSION as string); const BACKEND_APP_VERSION = transform(this.#config.BACKEND_APP_VERSION as string);
featuresJson.features.forEach(feature => { featuresJson.features.forEach(feature => {
this._features.set(feature.name, transform(feature.minVersion) <= BACKEND_APP_VERSION); this.#features.set(feature.name, transform(feature.minVersion) <= BACKEND_APP_VERSION);
}); });
this.#logger.info('[INFO] Features loaded: ', this.#features);
} }
} }

View File

@ -129,7 +129,7 @@ export class LicenseService extends GenericService<ILicenseReport> {
} }
@Validate() @Validate()
getReport$(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) { getReport(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = []; const queryParams: QueryParam[] = [];
if (limit) { if (limit) {
queryParams.push({ key: 'limit', value: limit }); queryParams.push({ key: 'limit', value: limit });
@ -139,19 +139,14 @@ export class LicenseService extends GenericService<ILicenseReport> {
queryParams.push({ key: 'offset', value: offset }); queryParams.push({ key: 'offset', value: offset });
} }
return this._post(body, `${this._defaultModelPath}/license`, queryParams); return firstValueFrom(this._post(body, `${this._defaultModelPath}/license`, queryParams));
} }
getReport(body: ILicenseReportRequest, limit?: number, offset?: number) { async loadLicenses() {
return firstValueFrom(this.getReport$(body, limit, offset)); const licenses$ = this._http.get<ILicenses>('/license').pipe(catchError(() => of(defaultOnError)));
} const licenses = await firstValueFrom(licenses$);
this.#licenseData$.next(licenses);
loadLicense() { this.setDefaultSelectedLicense();
return this._http.get<ILicenses>('/license').pipe(
catchError(() => of(defaultOnError)),
tap(licenses => this.#licenseData$.next(licenses)),
tap(() => this.setDefaultSelectedLicense()),
);
} }
setSelectedLicense($event: ILicense) { setSelectedLicense($event: ILicense) {

View File

@ -1,26 +1,25 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { GenericService } from '@iqser/common-ui'; import { GenericService } from '@iqser/common-ui';
import { SystemPreferences } from '@red/domain'; import { SystemPreferences } from '@red/domain';
import { Observable, switchMap } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { tap } from 'rxjs/operators'; import { NGXLogger } from 'ngx-logger';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class SystemPreferencesService extends GenericService<SystemPreferences> { export class SystemPreferencesService extends GenericService<SystemPreferences> {
protected readonly _defaultModelPath = 'app-config';
values: SystemPreferences; values: SystemPreferences;
protected readonly _defaultModelPath = 'app-config';
readonly #logger = inject(NGXLogger);
loadPreferences(): Observable<SystemPreferences> { async loadPreferences() {
return this.get<SystemPreferences>().pipe( this.values = await firstValueFrom(this.get<SystemPreferences>());
tap((config: SystemPreferences) => { this.#logger.info('[REDACTION] Loaded config: ', this.values);
console.log('[REDACTION] Loaded config: ', config); return this.values;
this.values = config;
}),
);
} }
update(value: SystemPreferences): Observable<SystemPreferences> { async update(value: SystemPreferences) {
return this._post(value).pipe(switchMap(() => this.loadPreferences())); await firstValueFrom(this._post(value));
return await this.loadPreferences();
} }
} }

View File

@ -1,63 +0,0 @@
import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '@services/config.service';
import { firstValueFrom, map, of, throwError } from 'rxjs';
import { KeycloakService } from 'keycloak-angular';
import { GeneralSettingsService } from '@services/general-settings.service';
import { IqserPermissionsService, KeycloakStatus, KeycloakStatusService, LanguageService } from '@iqser/common-ui';
import { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service';
import { FeaturesService } from '@services/features.service';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { LicenseService } from '@services/license.service';
import { ROLES } from '@users/roles';
function lastDossierTemplateRedirect(baseHref: string, userPreferenceService: UserPreferenceService) {
const url = window.location.href.split('/').filter(s => s.length > 0);
const lastDossierTemplate = userPreferenceService.getLastDossierTemplate();
if (lastDossierTemplate && [baseHref, 'main'].includes(url[url.length - 1])) {
window.location.href = `/${baseHref}/main/${lastDossierTemplate}`;
}
}
export function configurationInitializer(
baseHref: string,
keycloakStatusService: KeycloakStatusService,
keycloakService: KeycloakService,
configService: ConfigService,
systemPreferencesService: SystemPreferencesService,
featuresService: FeaturesService,
generalSettingsService: GeneralSettingsService,
languageService: LanguageService,
userService: UserService,
userPreferenceService: UserPreferenceService,
licenseService: LicenseService,
permissionsService: IqserPermissionsService,
) {
console.log('BASE HREF: ', baseHref);
const setup = keycloakStatusService.keycloakStatus$.pipe(
filter(event => event === KeycloakStatus.READY || event === KeycloakStatus.NOT_ACTIVE),
map(() => featuresService.loadConfig()),
switchMap(() => keycloakService.isLoggedIn()),
switchMap(loggedIn => (!loggedIn ? throwError(() => 'Not Logged In') : of({}))),
switchMap(() => userService.initialize()),
switchMap(() => (!permissionsService.has(ROLES.any) ? throwError(() => 'User has no red roles') : of({}))),
switchMap(() => generalSettingsService.getGeneralConfigurations()),
tap(configuration => configService.updateDisplayName(configuration.displayName)),
switchMap(() => systemPreferencesService.loadPreferences()),
switchMap(() => userPreferenceService.reload()),
catchError((e: unknown) => {
console.log('[Redaction] Initialization error:', e);
if (keycloakStatusService.keycloakStatus$.value === KeycloakStatus.READY) {
return of(userService.redirectToLogin());
}
return of({});
}),
tap(() => {
lastDossierTemplateRedirect(baseHref.replace('/', ''), userPreferenceService);
}),
switchMap(() => languageService.setInitialLanguage()),
switchMap(() => licenseService.loadLicense()),
);
return () => firstValueFrom(setup);
}

View File

@ -1,13 +1,11 @@
export * from './sorters/redaction-filter-sorter'; export * from './sorters/redaction-filter-sorter';
export * from './sorters/super-type-sorter'; export * from './sorters/super-type-sorter';
export * from './configuration.initializer';
export * from './date-inputs-utils'; export * from './date-inputs-utils';
export * from './file-download-utils'; export * from './file-download-utils';
export * from './file-drop-utils'; export * from './file-drop-utils';
export * from './filter-utils'; export * from './filter-utils';
export * from './functions'; export * from './functions';
export * from './global-error-handler.service';
export * from './missing-translations-handler'; export * from './missing-translations-handler';
export * from './page-stamper'; export * from './page-stamper';
export * from './router-links'; export * from './router-links';

View File

@ -0,0 +1,57 @@
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { ConfigService } from '@services/config.service';
import { UserService } from '@users/user.service';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { LicenseService } from '@services/license.service';
import { BASE_HREF, IqserPermissionsService, LoadingService } from '@iqser/common-ui';
import { FeaturesService } from '@services/features.service';
import { GeneralSettingsService } from '@services/general-settings.service';
import { tap } from 'rxjs/operators';
import { firstValueFrom } from 'rxjs';
import { ROLES } from '@users/roles';
import { DossiersChangesService } from '@services/dossiers/dossier-changes.service';
import { ResolveFn } from '@angular/router';
function lastDossierTemplateRedirect(baseHref: string, userPreferenceService: UserPreferenceService) {
const url = window.location.href.split('/').filter(s => s.length > 0);
const lastDossierTemplate = userPreferenceService.getLastDossierTemplate();
if (lastDossierTemplate && [baseHref, 'main'].includes(url[url.length - 1])) {
window.location.href = `/${baseHref}/main/${lastDossierTemplate}`;
}
}
export const mainResolver: ResolveFn<void> = async () => {
const logger = inject(NGXLogger);
logger.info('[ROUTES] Main resolver started...');
inject(FeaturesService).loadConfig();
if (inject(IqserPermissionsService).has(ROLES.dossiers.read)) {
inject(DossiersChangesService).initialize();
}
const configService = inject(ConfigService);
const userService = inject(UserService);
const systemPreferencesService = inject(SystemPreferencesService);
const userPreferenceService = inject(UserPreferenceService);
const licenseService = inject(LicenseService);
const loadingService = inject(LoadingService);
const baseHref = inject(BASE_HREF);
const generalConfig$ = inject(GeneralSettingsService).getGeneralConfigurations();
const updatedDisplayName$ = generalConfig$.pipe(tap(config => configService.updateDisplayName(config.displayName)));
await Promise.all([
userService.initialize(),
systemPreferencesService.loadPreferences(),
userPreferenceService.reload(),
firstValueFrom(updatedDisplayName$),
licenseService.loadLicenses(),
]);
lastDossierTemplateRedirect(baseHref, userPreferenceService);
loadingService.stop();
logger.info('[ROUTES] Main resolver finished!');
};

View File

@ -1,7 +1,7 @@
{ {
"ADMIN_CONTACT_NAME": null, "ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null, "ADMIN_CONTACT_URL": null,
"API_URL": "https://dom2.iqser.cloud/redaction-gateway-v1", "API_URL": "https://dan.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager", "APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3, "AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40", "BACKEND_APP_VERSION": "4.4.40",
@ -11,7 +11,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3, "MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction", "OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null, "OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dom2.iqser.cloud/auth", "OAUTH_URL": "https://dan.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24, "RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural", "SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview", "MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",

View File

@ -17,15 +17,14 @@ async function bootstrap(appConfig: AppConfig) {
console.log('Started with local config: ', config); console.log('Started with local config: ', config);
const appModule = appModuleFactory(config); const appModule = appModuleFactory(config);
const ngModuleRef = platformBrowserDynamic().bootstrapModule(appModule, { ngZoneEventCoalescing: true }); const ngModuleRef = await platformBrowserDynamic().bootstrapModule(appModule, { ngZoneEventCoalescing: true });
return ngModuleRef.then(moduleRef => {
if (!environment.production) { if (!environment.production) {
const applicationRef = moduleRef.injector.get(ApplicationRef); const applicationRef = ngModuleRef.injector.get(ApplicationRef);
const componentRef = applicationRef.components[0]; const componentRef = applicationRef.components[0];
// allows to run `ng.profiler.timeChangeDetection();` // allows to run `ng.profiler.timeChangeDetection();`
enableDebugTools(componentRef); enableDebugTools(componentRef);
} }
});
} }
const configPromise = fetch('/ui/assets/config/config.json').then(resp => resp.json()); const configPromise = fetch('/ui/assets/config/config.json').then(resp => resp.json());

@ -1 +1 @@
Subproject commit 6c63f851cf09496d425e9af75f18902557021c8e Subproject commit 03084293b2f5e3b96fe44f75252ae1f75ce7039f