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,
CustomRouteReuseStrategy,
DEFAULT_REDIRECT_KEY,
ifLoggedIn,
ifNotLoggedIn,
IqserAuthGuard,
IqserPermissionsGuard,
IqserRoutes,
@ -24,6 +26,7 @@ import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIE
import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { WebViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard';
import { ROLES } from '@users/roles';
import { mainResolver } from '@utils/main.resolver';
const dossierTemplateIdRoutes: IqserRoutes = [
{
@ -94,127 +97,135 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
];
const mainRoutes: IqserRoutes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
},
{
path: 'account',
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
},
{
path: 'admin',
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
canActivate: [RedRoleGuard],
},
{
path: 'dashboard',
loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, IqserPermissionsGuard, DossierTemplatesGuard, DashboardGuard],
permissions: {
allow: [
ROLES.any,
ROLES.templates.read,
ROLES.fileAttributes.readConfig,
ROLES.watermarks.read,
ROLES.dictionaryTypes.read,
ROLES.colors.read,
ROLES.states.read,
ROLES.notifications.read,
'RED_USER',
],
redirectTo: {
RED_USER: '/main/admin',
[ROLES.templates.read]: '/main/admin',
[DEFAULT_REDIRECT_KEY]: '/auth-error',
},
},
skeleton: 'dashboard',
},
},
{
path: 'downloads',
// TODO: transform into a lazy loaded module
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: ROLES.readDownloadStatus,
redirectTo: '/auth-error',
},
},
},
{
path: 'search',
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossiersGuard],
permissions: {
allow: [ROLES.search],
redirectTo: '/auth-error',
},
},
},
{
path: 'trash',
loadChildren: () => import('./modules/trash/trash.module').then(m => m.TrashModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossiersGuard, TrashGuard],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
permissions: {
allow: [ROLES.dossiers.read, ROLES.files.readStatus],
redirectTo: '/auth-error',
},
},
},
{
path: `:${DOSSIER_TEMPLATE_ID}`,
children: dossierTemplateIdRoutes,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossierTemplatesGuard, DashboardGuard, DossierTemplateExistsGuard],
permissions: {
allow: [
ROLES.any,
ROLES.templates.read,
ROLES.fileAttributes.readConfig,
ROLES.watermarks.read,
ROLES.dictionaryTypes.read,
ROLES.colors.read,
ROLES.states.read,
ROLES.notifications.read,
ROLES.dossiers.read,
'RED_USER',
],
redirectTo: {
[ROLES.any]: '/auth-error',
RED_USER: '/main/admin',
[DEFAULT_REDIRECT_KEY]: '/',
},
},
},
},
];
const routes: IqserRoutes = [
{
path: '',
pathMatch: 'full',
canActivate: [ifNotLoggedIn],
component: TenantResolveComponent,
},
{
path: ':tenant',
component: TenantResolveComponent,
redirectTo: ':tenant/main',
pathMatch: 'full',
},
{
path: ':tenant/main',
canActivate: [ifLoggedIn],
component: BaseScreenComponent,
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
},
{
path: 'account',
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
},
{
path: 'admin',
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
canActivate: [RedRoleGuard],
},
{
path: 'dashboard',
loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, IqserPermissionsGuard, DossierTemplatesGuard, DashboardGuard],
permissions: {
allow: [
ROLES.any,
ROLES.templates.read,
ROLES.fileAttributes.readConfig,
ROLES.watermarks.read,
ROLES.dictionaryTypes.read,
ROLES.colors.read,
ROLES.states.read,
ROLES.notifications.read,
'RED_USER',
],
redirectTo: {
RED_USER: '/main/admin',
[ROLES.templates.read]: '/main/admin',
[DEFAULT_REDIRECT_KEY]: '/auth-error',
},
},
skeleton: 'dashboard',
},
},
{
path: 'downloads',
// TODO: transform into a lazy loaded module
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: ROLES.readDownloadStatus,
redirectTo: '/auth-error',
},
},
},
{
path: 'search',
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossiersGuard],
permissions: {
allow: [ROLES.search],
redirectTo: '/auth-error',
},
},
},
{
path: 'trash',
loadChildren: () => import('./modules/trash/trash.module').then(m => m.TrashModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossiersGuard, TrashGuard],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
permissions: {
allow: [ROLES.dossiers.read, ROLES.files.readStatus],
redirectTo: '/auth-error',
},
},
},
{
path: `:${DOSSIER_TEMPLATE_ID}`,
children: dossierTemplateIdRoutes,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossierTemplatesGuard, DashboardGuard, DossierTemplateExistsGuard],
permissions: {
allow: [
ROLES.any,
ROLES.templates.read,
ROLES.fileAttributes.readConfig,
ROLES.watermarks.read,
ROLES.dictionaryTypes.read,
ROLES.colors.read,
ROLES.states.read,
ROLES.notifications.read,
ROLES.dossiers.read,
'RED_USER',
],
redirectTo: {
[ROLES.any]: '/auth-error',
RED_USER: '/main/admin',
[DEFAULT_REDIRECT_KEY]: '/',
},
},
},
},
],
resolve: {
whateverThisMainRouteNeeds: mainResolver,
},
children: mainRoutes,
},
{
path: ':tenant/auth-error',

View File

@ -1,17 +1,5 @@
<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-connection-status></iqser-connection-status>
<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 { 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 { UserPreferenceService } from '@users/user-preference.service';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
import { ROLES } from '@users/roles';
import { getConfig } from '@iqser/common-ui';
import { AppConfig } from '@red/domain';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
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 */
private readonly _routerHistoryService: RouterHistoryService,
userPreferenceService: UserPreferenceService,
readonly documentViewer: REDDocumentViewer,
dossierChangesService: DossiersChangesService,
@Inject(DOCUMENT) document: Document,
renderer: Renderer2,
permissionsService: IqserPermissionsService,
private readonly _router: Router,
route: ActivatedRoute,
) {
renderer.addClass(document.body, userPreferenceService.getTheme());
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));
this.#subscription.add(sub);
}

View File

@ -1,12 +1,11 @@
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 { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { MissingTranslationHandler } from '@ngx-translate/core';
import {
BASE_HREF,
CachingModule,
CircleButtonComponent,
CommonUiModule,
@ -19,10 +18,8 @@ import {
IqserHelpModeModule,
IqserListingModule,
IqserLoadingModule,
IqserPermissionsService,
IqserTranslateModule,
IqserUsersModule,
KeycloakStatusService,
LanguageService,
LogoComponent,
MAX_RETRIES_ON_SERVER_ERROR,
@ -48,53 +45,43 @@ import { FileUploadDownloadModule } from '@upload-download/file-upload-download.
import { DatePipe as BaseDatePipe } from '@angular/common';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
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 { configurationInitializer } from '@utils/configuration.initializer';
import { ConfigService } from '@services/config.service';
import { SpotlightSearchComponent } from '@components/spotlight-search/spotlight-search.component';
import { DatePipe } from '@shared/pipes/date.pipe';
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 { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service';
import { ActiveDossiersService } from '@services/dossiers/active-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 { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger';
import { LoggerRulesService } from '@services/logger-rules.service';
import { AppConfig, ILoggerConfig } from '@red/domain';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { LicenseService } from '@services/license.service';
import { UI_CACHES } from '@utils/constants';
import { RedRoleGuard } from '@users/red-role.guard';
import { SkeletonTopBarComponent } from '@components/skeleton/skeleton-top-bar/skeleton-top-bar.component';
import { DossierSkeletonComponent } from '@components/skeleton/dossier-skeleton/dossier-skeleton.component';
import { SkeletonStatsComponent } from '@components/skeleton/skeleton-stats/skeleton-stats.component';
const screens = [BaseScreenComponent, DownloadsListScreenComponent];
const components = [
AppComponent,
AuthErrorComponent,
NotificationsComponent,
SpotlightSearchComponent,
BreadcrumbsComponent,
DashboardSkeletonComponent,
DossierSkeletonComponent,
SkeletonTopBarComponent,
SkeletonStatsComponent,
...screens,
];
export const appModuleFactory = (config: AppConfig) => {
@NgModule({
declarations: [...components],
declarations: [
AppComponent,
AuthErrorComponent,
NotificationsComponent,
SpotlightSearchComponent,
BreadcrumbsComponent,
DashboardSkeletonComponent,
DossierSkeletonComponent,
SkeletonTopBarComponent,
SkeletonStatsComponent,
BaseScreenComponent,
DownloadsListScreenComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
@ -187,23 +174,12 @@ export const appModuleFactory = (config: AppConfig) => {
useClass: GlobalErrorHandler,
},
{
provide: APP_INITIALIZER,
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: configurationInitializer,
deps: [
BASE_HREF,
KeycloakStatusService,
KeycloakService,
ConfigService,
SystemPreferencesService,
FeaturesService,
GeneralSettingsService,
LanguageService,
UserService,
UserPreferenceService,
LicenseService,
IqserPermissionsService,
],
useValue: async () => {
const languageService = inject(LanguageService);
return languageService.setInitialLanguage();
},
},
{
provide: MissingTranslationHandler,

View File

@ -43,10 +43,6 @@
</a>
</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>
<mat-icon svgIcon="iqser:logout"></mat-icon>
<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>
<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 { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
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 { FeaturesService } from '@services/features.service';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain';
import { ROLES } from '@users/roles';
import { REDDocumentViewer } from '../../modules/pdf-viewer/services/document-viewer.service';
interface MenuItem {
readonly id: string;
@ -31,8 +32,8 @@ const isSearchScreen: (url: string) => boolean = url => url.includes('/search');
})
export class BaseScreenComponent {
readonly roles = ROLES;
private readonly _baseHref = inject(BASE_HREF);
readonly tenantContext = inject(TenantContext);
readonly tenantContext = inject(TenantsService);
readonly documentViewer = inject(REDDocumentViewer);
readonly currentUser = this.userService.currentUser;
readonly userMenuItems: readonly MenuItem[] = [
{
@ -59,6 +60,12 @@ export class BaseScreenComponent {
routerLink: '/main/trash',
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[] = [
{
@ -79,6 +86,7 @@ export class BaseScreenComponent {
action: (query): void => this._search(query, []),
},
];
private readonly _baseHref = inject(BASE_HREF);
private readonly _navigationStart$ = this._router.events.pipe(
filter(isNavigationStart),
map((event: NavigationStart) => event.url),
@ -113,6 +121,10 @@ export class BaseScreenComponent {
return item.name;
}
selectTenant() {
window.open(window.location.origin + this._baseHref, '_blank');
}
private _search(query: string, dossierIds: string[], onlyActive = false) {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
this._router.navigate([`/${this._tenantContextHolder.currentTenant}/main/search`], { queryParams }).then();
@ -126,8 +138,4 @@ export class BaseScreenComponent {
const dossierId = routerLink[2];
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 { SystemPreferences } from '@red/domain';
import { BaseFormComponent, IqserPermissionsService, KeysOf, LoadingService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { systemPreferencesTranslations } from '@translations/system-preferences-translations';
import { ROLES } from '@users/roles';
@ -38,7 +37,7 @@ export class SystemPreferencesFormComponent extends BaseFormComponent {
async save(): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._systemPreferencesService.update(this.form.getRawValue()));
await this._systemPreferencesService.update(this.form.getRawValue());
this._loadData();
this._loadingService.stop();
}

View File

@ -1,18 +1,19 @@
import { GenericService, LAST_CHECKED_OFFSET, List, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui';
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 { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { NGXLogger } from 'ngx-logger';
import { ActiveDossiersService } from './active-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 { CHANGED_CHECK_INTERVAL } from '@utils/constants';
@Injectable({ providedIn: 'root' })
export class DossiersChangesService extends GenericService<Dossier> {
export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy {
protected readonly _defaultModelPath = 'dossier';
readonly #subscription = new Subscription();
readonly #activeDossiersService = inject(ActiveDossiersService);
readonly #archivedDossiersService = inject(ArchivedDossiersService);
@ -31,7 +32,7 @@ export class DossiersChangesService extends GenericService<Dossier> {
});
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(
tap(changes => this.#logger.info('[CHANGES] ', changes)),
@ -50,17 +51,23 @@ export class DossiersChangesService extends GenericService<Dossier> {
);
}
initializeRefresh() {
return timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL).pipe(
initialize() {
this.#logger.info('[DOSSIERS_CHANGES] Initialize timer');
const subscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL).pipe(
switchMap(() => this.loadOnlyChanged()),
tap(changes => {
this.#activeDossiersService.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 }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity)),

View File

@ -1,6 +1,8 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { inject, Injectable } from '@angular/core';
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 {
const [major, minor, patch, release] = version.split(/[.-]/).map(x => parseInt(x, 10));
@ -11,19 +13,20 @@ function transform(version: string): number {
providedIn: 'root',
})
export class FeaturesService {
private _features = new Map<string, boolean>();
constructor(private readonly _configService: ConfigService) {}
readonly #features = new Map<string, boolean>();
readonly #config = getConfig<AppConfig>();
readonly #logger = inject(NGXLogger);
isEnabled(feature: string): boolean {
// If feature is not defined in config object return true
return this._features.get(feature) !== false;
return this.#features.get(feature) !== false;
}
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 => {
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()
getReport$(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) {
getReport(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = [];
if (limit) {
queryParams.push({ key: 'limit', value: limit });
@ -139,19 +139,14 @@ export class LicenseService extends GenericService<ILicenseReport> {
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) {
return firstValueFrom(this.getReport$(body, limit, offset));
}
loadLicense() {
return this._http.get<ILicenses>('/license').pipe(
catchError(() => of(defaultOnError)),
tap(licenses => this.#licenseData$.next(licenses)),
tap(() => this.setDefaultSelectedLicense()),
);
async loadLicenses() {
const licenses$ = this._http.get<ILicenses>('/license').pipe(catchError(() => of(defaultOnError)));
const licenses = await firstValueFrom(licenses$);
this.#licenseData$.next(licenses);
this.setDefaultSelectedLicense();
}
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 { SystemPreferences } from '@red/domain';
import { Observable, switchMap } from 'rxjs';
import { tap } from 'rxjs/operators';
import { firstValueFrom } from 'rxjs';
import { NGXLogger } from 'ngx-logger';
@Injectable({
providedIn: 'root',
})
export class SystemPreferencesService extends GenericService<SystemPreferences> {
protected readonly _defaultModelPath = 'app-config';
values: SystemPreferences;
protected readonly _defaultModelPath = 'app-config';
readonly #logger = inject(NGXLogger);
loadPreferences(): Observable<SystemPreferences> {
return this.get<SystemPreferences>().pipe(
tap((config: SystemPreferences) => {
console.log('[REDACTION] Loaded config: ', config);
this.values = config;
}),
);
async loadPreferences() {
this.values = await firstValueFrom(this.get<SystemPreferences>());
this.#logger.info('[REDACTION] Loaded config: ', this.values);
return this.values;
}
update(value: SystemPreferences): Observable<SystemPreferences> {
return this._post(value).pipe(switchMap(() => this.loadPreferences()));
async update(value: SystemPreferences) {
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/super-type-sorter';
export * from './configuration.initializer';
export * from './date-inputs-utils';
export * from './file-download-utils';
export * from './file-drop-utils';
export * from './filter-utils';
export * from './functions';
export * from './global-error-handler.service';
export * from './missing-translations-handler';
export * from './page-stamper';
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_URL": null,
"API_URL": "https://dom2.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://dan.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -11,7 +11,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dom2.iqser.cloud/auth",
"OAUTH_URL": "https://dan.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"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);
const appModule = appModuleFactory(config);
const ngModuleRef = platformBrowserDynamic().bootstrapModule(appModule, { ngZoneEventCoalescing: true });
return ngModuleRef.then(moduleRef => {
if (!environment.production) {
const applicationRef = moduleRef.injector.get(ApplicationRef);
const componentRef = applicationRef.components[0];
// allows to run `ng.profiler.timeChangeDetection();`
enableDebugTools(componentRef);
}
});
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(appModule, { ngZoneEventCoalescing: true });
if (!environment.production) {
const applicationRef = ngModuleRef.injector.get(ApplicationRef);
const componentRef = applicationRef.components[0];
// allows to run `ng.profiler.timeChangeDetection();`
enableDebugTools(componentRef);
}
}
const configPromise = fetch('/ui/assets/config/config.json').then(resp => resp.json());

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