Merge branch 'master' into VM/RED-2614

* master:
  chore(release)
  OneTime Token for downloads
  chore(release)
  update readme for redirect_uri errors
  chore(release)
  Update common
  Update backend url
  Fix config
  Fix rebase: common
  Improvement
  Edit dossier template from info
  Dossier template info

# Conflicts:
#	libs/common-ui
This commit is contained in:
Valentin Mihai 2022-01-25 14:26:11 +02:00
commit ecc56b875b
65 changed files with 649 additions and 456 deletions

View File

@ -1,20 +1,5 @@
# Redaction
## Swagger Generated Code
To re-generate http rune swagger
YOu need swagger-codegen installed `brew install swagger-codegen`
```
BASE=https://dev-06.iqser.cloud/
URL="$BASE"redaction-gateway-v1/v2/api-docs?group=redaction-gateway-v1
rm -Rf /tmp/swagger
mkdir -p /tmp/swagger
swagger-codegen generate -i "$URL" -l typescript-angular -o /tmp/swagger
cd /tmp/swagger
```
## To Create a new Stack in rancher
Goto rancher.iqser.com: Select Cluster `Development`, go to apps, click launch and select `Redaction` from the `dev`
@ -25,17 +10,25 @@ Add cloudflare certificate and specify a hostname to use `timo-redaction-dev.iqs
## Keycloak Staging Config
keycloak:
authServerUrl: 'https://redkc-staging.iqser.cloud/auth'
client:
secret: 'a4e8aa56-03b0-4e6b-b822-8ac1f41280c4'
- keycloak:
- authServerUrl: 'https://redkc-staging.iqser.cloud/auth'
- client:
- secret: 'a4e8aa56-03b0-4e6b-b822-8ac1f41280c4'
## Default Testing URL
`https://timo-redaction-dev.iqser.cloud/`
Hostname:
`https://dev-04.iqser.cloud/`
timo-redaction-dev.iqser.cloud
## Known errors
- In case of CORS or redirect_uri errors follow these steps:
- Go to `<HOST>.iqser.cloud/auth/admin/master/console`
- Login with `admin` and `admin1234`
- In the left menu go to `Clients`
- In the table click `redaction`
- Find `Valid Redirect URIs` input
- Under `/ui/*` add new value `http://localhost:4200/*`
- **Save**
## Test Users

View File

@ -9,7 +9,6 @@ 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 { DossiersService } from '@services/entity-services/dossiers.service';
import { shareDistinctLast } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service';
@ -77,7 +76,6 @@ export class BaseScreenComponent {
constructor(
readonly appStateService: AppStateService,
readonly dossiersService: DossiersService,
readonly userService: UserService,
readonly userPreferenceService: UserPreferenceService,
readonly titleService: Title,

View File

@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { CompositeRouteGuard } from '@iqser/common-ui';
import { AuthGuard } from '../auth/auth.guard';
import { RedRoleGuard } from '../auth/red-role.guard';
import { AppStateGuard } from '../../state/app-state.guard';
import { AppStateGuard } from '@state/app-state.guard';
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
const routes = [

View File

@ -5,8 +5,8 @@ import { TranslateService } from '@ngx-translate/core';
import { LoadingService } from '@iqser/common-ui';
import { IProfile } from '@red/domain';
import { languagesTranslations } from '../../../translations/languages-translations';
import { PermissionsService } from '../../../../../services/permissions.service';
import { UserService } from '../../../../../services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';
import { ConfigService } from '../../../../../services/config.service';
import { LanguageService } from '../../../../../i18n/language.service';
import { ConfirmationDialogService } from '../../../../../../../../../libs/common-ui/src/lib/dialog/confirmation-dialog.service';

View File

@ -1,7 +1,7 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService } from '@iqser/common-ui';
import { Observable, of } from 'rxjs';
import { UserService } from '../../../services/user.service';
import { UserService } from '@services/user.service';
import { EmailNotificationScheduleTypes, INotificationPreferences } from '@red/domain';
import { catchError } from 'rxjs/operators';
@ -11,14 +11,6 @@ export class NotificationPreferencesService extends GenericService<INotification
super(_injector, 'notification-preferences');
}
get(): Observable<INotificationPreferences> {
return super.get<INotificationPreferences>().pipe(catchError(() => of(this._defaultPreferences)));
}
update(notificationPreferences: INotificationPreferences): Observable<void> {
return super._post(notificationPreferences);
}
private get _defaultPreferences(): INotificationPreferences {
return {
emailNotificationType: EmailNotificationScheduleTypes.INSTANT,
@ -28,4 +20,12 @@ export class NotificationPreferencesService extends GenericService<INotification
inAppNotificationsEnabled: true,
};
}
get(): Observable<INotificationPreferences> {
return super.get<INotificationPreferences>().pipe(catchError(() => of(this._defaultPreferences)));
}
update(notificationPreferences: INotificationPreferences): Observable<void> {
return super._post(notificationPreferences);
}
}

View File

@ -3,7 +3,6 @@ import { AuthGuard } from '../auth/auth.guard';
import { CompositeRouteGuard } from '@iqser/common-ui';
import { RedRoleGuard } from '../auth/red-role.guard';
import { AppStateGuard } from '@state/app-state.guard';
import { DossierTemplatesListingScreenComponent } from './screens/dossier-template-listing/dossier-templates-listing-screen.component';
import { DictionaryListingScreenComponent } from './screens/dictionary-listing/dictionary-listing-screen.component';
import { DictionaryOverviewScreenComponent } from './screens/dictionary-overview/dictionary-overview-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
@ -21,6 +20,7 @@ import { DossierAttributesListingScreenComponent } from './screens/dossier-attri
import { TrashScreenComponent } from './screens/trash/trash-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
const routes: Routes = [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
@ -29,15 +29,25 @@ const routes: Routes = [
children: [
{
path: '',
component: DossierTemplatesListingScreenComponent,
component: BaseAdminScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
},
loadChildren: () =>
import('./screens/dossier-templates-listing/dossier-templates-listing.module').then(
m => m.DossierTemplatesListingModule,
),
},
{
path: ':dossierTemplateId',
children: [
{
path: 'info',
canActivate: [CompositeRouteGuard],
component: BaseDossierTemplateScreenComponent,
loadChildren: () => import('./screens/info/dossier-template-info.module').then(m => m.DossierTemplateInfoModule),
},
{
path: 'dictionaries',
children: [
@ -111,11 +121,11 @@ const routes: Routes = [
},
{
path: 'justifications',
component: BaseAdminScreenComponent,
component: BaseDossierTemplateScreenComponent,
canActivate: [CompositeRouteGuard],
loadChildren: () => import('./screens/justifications/justifications.module').then(m => m.JustificationsModule),
},
{ path: '', redirectTo: 'dictionaries', pathMatch: 'full' },
{ path: '', redirectTo: 'info', pathMatch: 'full' },
],
},
],

View File

@ -50,6 +50,7 @@ export class AdminSideNavComponent implements OnInit {
},
],
dossierTemplates: [
{ screen: 'info', label: _('dossier-template-info') },
{ screen: 'dictionaries', label: _('dictionaries') },
{
screen: 'rules',

View File

@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { RulesScreenComponent } from './screens/rules/rules-screen.component';
import { SharedModule } from '@shared/shared.module';
import { DossierTemplatesListingScreenComponent } from './screens/dossier-template-listing/dossier-templates-listing-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { DictionaryListingScreenComponent } from './screens/dictionary-listing/dictionary-listing-screen.component';
@ -13,8 +12,7 @@ import { FileAttributesListingScreenComponent } from './screens/file-attributes-
import { LicenseInformationScreenComponent } from './screens/license-information/license-information-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { WatermarkScreenComponent } from './screens/watermark/watermark-screen.component';
import { AdminBreadcrumbsComponent } from './components/breadcrumbs/admin-breadcrumbs.component';
import { DossierTemplateActionsComponent } from './components/dossier-template-actions/dossier-template-actions.component';
import { DossierTemplateBreadcrumbsComponent } from './components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { ColorPickerModule } from 'ngx-color-picker';
import { AddEditFileAttributeDialogComponent } from './dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { AddEditDossierTemplateDialogComponent } from './dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component';
@ -48,6 +46,8 @@ import { SmtpConfigService } from './services/smtp-config.service';
import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
import { GeneralConfigFormComponent } from './screens/general-config/general-config-form/general-config-form.component';
import { SmtpFormComponent } from './screens/general-config/smtp-form/smtp-form.component';
import { SharedAdminModule } from './shared/shared-admin.module';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
const dialogs = [
AddEditDossierTemplateDialogComponent,
@ -64,7 +64,6 @@ const dialogs = [
];
const screens = [
DossierTemplatesListingScreenComponent,
RulesScreenComponent,
AuditScreenComponent,
DefaultColorsScreenComponent,
@ -82,8 +81,7 @@ const screens = [
];
const components = [
AdminBreadcrumbsComponent,
DossierTemplateActionsComponent,
DossierTemplateBreadcrumbsComponent,
ComboChartComponent,
ComboSeriesVerticalComponent,
UsersStatsComponent,
@ -92,14 +90,17 @@ const components = [
ResetPasswordComponent,
UserDetailsComponent,
BaseAdminScreenComponent,
BaseDossierTemplateScreenComponent,
GeneralConfigFormComponent,
SmtpFormComponent,
...dialogs,
...screens,
];
@NgModule({
declarations: [...components, GeneralConfigFormComponent, SmtpFormComponent],
declarations: [...components],
providers: [AdminDialogService, AuditService, DigitalSignatureService, LicenseReportService, RulesService, SmtpConfigService],
imports: [CommonModule, SharedModule, AdminRoutingModule, NgxChartsModule, ColorPickerModule, MonacoEditorModule],
imports: [CommonModule, SharedModule, AdminRoutingModule, SharedAdminModule, NgxChartsModule, ColorPickerModule, MonacoEditorModule],
})
export class AdminModule {}

View File

@ -1,26 +1,5 @@
<!--TODO: This is only used for justifications for now, should be used for all admin screens-->
<div class="overlay-shadow"></div>
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-admin-side-nav type="settings"></redaction-admin-side-nav>
<div class="flex-1 actions">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<router-outlet></router-outlet>
</div>
</section>
<router-outlet></router-outlet>

View File

@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'redaction-base-admin-screen',
templateUrl: './base-admin-screen.component.html',
styleUrls: ['./base-admin-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -0,0 +1,26 @@
<!--TODO: Use this for all dossier template screens -->
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="flex-1 actions">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<router-outlet></router-outlet>
</div>
</section>

View File

@ -0,0 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
templateUrl: './base-dossier-template-screen.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BaseDossierTemplateScreenComponent {}

View File

@ -1,21 +0,0 @@
import { Component, Input } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { PermissionsService } from '@services/permissions.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
@Component({
selector: 'redaction-admin-breadcrumbs',
templateUrl: './admin-breadcrumbs.component.html',
styleUrls: ['./admin-breadcrumbs.component.scss'],
})
export class AdminBreadcrumbsComponent {
@Input() root = false;
constructor(
readonly userPreferenceService: UserPreferenceService,
readonly permissionService: PermissionsService,
readonly appStateService: AppStateService,
readonly dossierTemplatesService: DossierTemplatesService,
) {}
}

View File

@ -6,10 +6,11 @@
translate="dossier-templates"
></a>
<ng-container *ngIf="dossierTemplatesService.activeDossierTemplate$ | async as activeDossierTemplate">
<mat-icon svgIcon="iqser:arrow-right"></mat-icon>
<a [class.active]="!appStateService.activeDictionaryType" [routerLink]="activeDossierTemplate.routerLink" class="breadcrumb ml-0">
{{ activeDossierTemplate.name }}
<mat-icon svgIcon="iqser:arrow-right"></mat-icon>
<ng-container *ngIf="dossierTemplate$ | async as dossierTemplate">
<a [class.active]="!appStateService.activeDictionaryType" [routerLink]="dossierTemplate.routerLink" class="breadcrumb ml-0">
{{ dossierTemplate.name }}
</a>
<ng-container *ngIf="appStateService.activeDictionary">
@ -17,7 +18,7 @@
<a
[routerLink]="
'/main/admin/dossier-templates/' +
activeDossierTemplate.dossierTemplateId +
dossierTemplate.dossierTemplateId +
'/dictionaries/' +
appStateService.activeDictionaryType
"

View File

@ -0,0 +1,32 @@
import { Component, Input } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { PermissionsService } from '@services/permissions.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { DossierTemplate } from '@red/domain';
@Component({
selector: 'redaction-dossier-template-breadcrumbs',
templateUrl: './dossier-template-breadcrumbs.component.html',
styleUrls: ['./dossier-template-breadcrumbs.component.scss'],
})
export class DossierTemplateBreadcrumbsComponent {
@Input() root = false;
readonly dossierTemplate$: Observable<DossierTemplate>;
constructor(
readonly userPreferenceService: UserPreferenceService,
readonly permissionService: PermissionsService,
readonly appStateService: AppStateService,
readonly dossierTemplatesService: DossierTemplatesService,
private readonly _route: ActivatedRoute,
) {
this.dossierTemplate$ = _route.paramMap.pipe(
map(params => params.get('dossierTemplateId')),
switchMap((dossierTemplateId: string) => this.dossierTemplatesService.getEntityChanged$(dossierTemplateId)),
);
}
}

View File

@ -95,11 +95,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dictionary.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -35,11 +35,11 @@
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dossier-attribute.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -82,11 +82,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dossier-template.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,13 +1,13 @@
import { Component, Inject, Injector } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as moment from 'moment';
import { Moment } from 'moment';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { downloadTypesTranslations } from '../../../../translations/download-types-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
import { BaseDialogComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DownloadFileType, IDossierTemplate } from '@red/domain';
import { HttpStatusCode } from '@angular/common/http';
@ -36,6 +36,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
private readonly _formBuilder: FormBuilder,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
private readonly _loadingService: LoadingService,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplate: IDossierTemplate,
) {
super(_injector, _dialogRef);
@ -70,15 +71,15 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
}
async save() {
this._loadingService.start();
try {
const dossierTemplate = {
dossierTemplateId: this.dossierTemplate?.dossierTemplateId,
...this.form.getRawValue(),
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
};
} as IDossierTemplate;
await this._dossierTemplatesService.createOrUpdate(dossierTemplate).toPromise();
await this._dossierTemplatesService.loadAll().toPromise();
await this._appStateService.loadDictionaryData();
this._dialogRef.close(true);
} catch (error: any) {
@ -88,6 +89,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
: _('add-edit-dossier-template.error.generic');
this._toaster.error(message, { error });
}
this._loadingService.stop();
}
private _getForm(): FormGroup {
@ -117,7 +119,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
}
private _requiredIfValidator(predicate) {
return formControl => {
return (formControl: AbstractControl) => {
if (!formControl.parent) {
return null;
}

View File

@ -84,11 +84,11 @@
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-file-attribute.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -28,11 +28,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'edit-color-dialog.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="flex-1 actions">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="flex-1 actions">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
@ -78,51 +78,53 @@
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as dict">
<div class="cell">
<div [ngStyle]="{ 'background-color': dict.hexColor }" class="color-square"></div>
<div class="dict-name">
<div class="table-item-title heading">
{{ dict.label }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ dict.entries?.length }}
<ng-container *ngIf="templateStats$ | async as templateStats">
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as dict">
<div class="cell">
<div [ngStyle]="{ 'background-color': dict.hexColor }" class="color-square"></div>
<div class="dict-name">
<div class="table-item-title heading">
{{ dict.label }}
</div>
<div *ngIf="!dict.caseInsensitive">
<mat-icon svgIcon="red:case-sensitive"></mat-icon>
{{ 'dictionary-listing.case-sensitive' | translate }}
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ templateStats.dictionarySummary(dict.type)?.entriesCount || 0 }}
</div>
<div *ngIf="!dict.caseInsensitive">
<mat-icon svgIcon="red:case-sensitive"></mat-icon>
{{ 'dictionary-listing.case-sensitive' | translate }}
</div>
</div>
</div>
</div>
</div>
<div class="cell center small-label">
{{ dict.rank }}
</div>
<div class="cell center small-label">
{{ dict.rank }}
</div>
<div class="cell center">
<redaction-annotation-icon [dictionary]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon>
</div>
<div class="cell center">
<redaction-annotation-icon [dictionary]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon>
</div>
<div class="cell">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openDeleteDictionariesDialog($event, [dict])"
[tooltip]="'dictionary-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
<div class="cell">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openDeleteDictionariesDialog($event, [dict])"
[tooltip]="'dictionary-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="openAddEditDictionaryDialog($event, dict)"
[tooltip]="'dictionary-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openAddEditDictionaryDialog($event, dict)"
[tooltip]="'dictionary-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
</div>
</div>
</div>
</div>
</ng-template>
</ng-template>
</ng-container>

View File

@ -1,8 +1,6 @@
import { Component, forwardRef, Injector, OnInit } from '@angular/core';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { AppStateService } from '@state/app-state.service';
import { catchError, defaultIfEmpty, tap } from 'rxjs/operators';
import { forkJoin, of } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import {
CircleButtonTypes,
@ -17,14 +15,11 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { DictionaryService } from '@shared/services/dictionary.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { Dictionary } from '@red/domain';
const toChartConfig = (dict: Dictionary): DoughnutChartConfig => ({
value: dict.entries?.length ?? 0,
color: dict.hexColor,
label: dict.label,
key: dict.type,
});
import { Dictionary, DossierTemplateStats } from '@red/domain';
import { Observable } from 'rxjs';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
import { ActivatedRoute } from '@angular/router';
import { tap } from 'rxjs/operators';
@Component({
templateUrl: './dictionary-listing-screen.component.html',
@ -42,6 +37,8 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
{ label: _('dictionary-listing.table-col-names.hint-redaction'), class: 'flex-center' },
];
chartData: DoughnutChartConfig[] = [];
readonly templateStats$: Observable<DossierTemplateStats>;
templateStats: DossierTemplateStats;
constructor(
protected readonly _injector: Injector,
@ -52,13 +49,18 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
private readonly _dialogService: AdminDialogService,
private readonly _translateService: TranslateService,
private readonly _dictionaryService: DictionaryService,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
private readonly _route: ActivatedRoute,
) {
super(_injector);
_loadingService.start();
const dossierTemplateId = _route.snapshot.paramMap.get('dossierTemplateId');
this.templateStats = this._dossierTemplateStatsService.get(dossierTemplateId);
this.templateStats$ = this._dossierTemplateStatsService.watch$(dossierTemplateId).pipe(tap(stats => (this.templateStats = stats)));
}
ngOnInit(): void {
this._loadDictionaryData();
async ngOnInit(): Promise<void> {
await this._loadDictionaryData(false);
}
openDeleteDictionariesDialog($event?: MouseEvent, types = this.listingService.selected) {
@ -70,9 +72,7 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
this._dossierTemplatesService.activeDossierTemplateId,
)
.toPromise();
await this._appStateService.loadDictionaryData();
this._loadDictionaryData(false);
this._calculateData();
await this._loadDictionaryData();
this._loadingService.stop();
});
}
@ -87,50 +87,31 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
},
async () => {
this._loadingService.start();
await this._appStateService.loadDictionaryData();
this._loadDictionaryData(false);
this._calculateData();
await this._loadDictionaryData();
this._loadingService.stop();
},
);
}
private _loadDictionaryData(loadEntries = true): void {
private async _loadDictionaryData(refresh = true): Promise<void> {
if (refresh) {
await this._appStateService.loadDictionaryData();
}
const appStateDictionaryData = this._appStateService.dictionaryData[this._dossierTemplatesService.activeDossierTemplateId];
const entities = Object.values(appStateDictionaryData).filter(d => !d.virtual);
this.entitiesService.setEntities(entities);
if (!loadEntries) {
this.entitiesService.setEntities(
entities.map(dict => {
dict.entries = this.allEntities.find(d => d.type === dict.type)?.entries || [];
return dict;
}),
);
} else {
this.entitiesService.setEntities(entities);
}
if (!loadEntries) {
return;
}
const dataObs = this.allEntities.map(dict =>
this._dictionaryService.getForType(this._dossierTemplatesService.activeDossierTemplateId, dict.type).pipe(
tap(values => (dict.entries = [...values.entries] ?? [])),
catchError(() => {
dict.entries = [];
return of({});
}),
),
);
forkJoin(dataObs)
.pipe(defaultIfEmpty(null))
.subscribe(() => this._calculateData());
this._calculateData();
}
private _calculateData(): void {
this.chartData = this.allEntities.map(dict => toChartConfig(dict));
this.chartData = this.allEntities.map(dict => ({
value: this.templateStats.dictionarySummary(dict.type).entriesCount ?? 0,
color: dict.hexColor,
label: dict.label,
key: dict.type,
}));
this.chartData.sort((a, b) => (a.label < b.label ? -1 : 1));
this._loadingService.stop();
}

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs></redaction-dossier-template-breadcrumbs>
<div class="actions">
<iqser-circle-button

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -1,100 +0,0 @@
<section class="settings">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="settings"></redaction-admin-side-nav>
<div>
<iqser-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[pageLabel]="'dossier-templates' | translate"
[showCloseButton]="currentUser.isUser"
></iqser-page-header>
<div class="content-inner">
<div class="content-container">
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-templates-listing.no-data.title' | translate"
[noMatchText]="'dossier-templates-listing.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
noDataIcon="red:template"
></iqser-table>
</div>
</div>
</div>
</section>
<ng-template #bulkActions>
<iqser-circle-button
(action)="openBulkDeleteTemplatesDialog($event)"
*ngIf="currentUser.isAdmin && (listingService.areSomeSelected$ | async)"
[tooltip]="'dossier-templates-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
</ng-template>
<ng-template #actionsTemplate let-dossierTemplate="entity">
<redaction-dossier-template-actions
(loadDossierTemplatesData)="loadDossierTemplateStats()"
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"
></redaction-dossier-template-actions>
</ng-template>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddDossierTemplateDialog()"
*ngIf="currentUser.isAdmin && userPreferenceService.areDevFeaturesEnabled"
[label]="'dossier-templates-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as dossierTemplate">
<div class="cell">
<div class="table-item-title heading">
{{ dossierTemplate.name }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'dossier-templates-listing.dictionaries' | translate: { length: dossierTemplate.dictionariesCount } }}
</div>
</div>
</div>
<div class="cell user-column">
<redaction-initials-avatar
[defaultValue]="'unknown' | translate"
[user]="dossierTemplate.createdBy || 'system'"
[withName]="true"
></redaction-initials-avatar>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
</div>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateModified | date: 'd MMM. yyyy' }}
</div>
<ng-container *ngTemplateOutlet="actionsTemplate; context: { entity: dossierTemplate }"></ng-container>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,47 @@
<iqser-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[pageLabel]="'dossier-templates' | translate"
[showCloseButton]="currentUser.isUser"
></iqser-page-header>
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-templates-listing.no-data.title' | translate"
[noMatchText]="'dossier-templates-listing.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
noDataIcon="red:template"
></iqser-table>
<ng-template #bulkActions>
<iqser-circle-button
(action)="openBulkDeleteTemplatesDialog($event)"
*ngIf="currentUser.isAdmin && (listingService.areSomeSelected$ | async)"
[tooltip]="'dossier-templates-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
</ng-template>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddDossierTemplateDialog()"
*ngIf="currentUser.isAdmin && userPreferenceService.areDevFeaturesEnabled"
[label]="'dossier-templates-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<redaction-table-item [dossierTemplate]="entity"></redaction-table-item>
</ng-template>

View File

@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { DossierTemplate } from '@red/domain';
import {
CircleButtonTypes,
@ -29,7 +29,7 @@ import { HttpStatusCode } from '@angular/common/http';
{ provide: ListingComponent, useExisting: forwardRef(() => DossierTemplatesListingScreenComponent) },
],
})
export class DossierTemplatesListingScreenComponent extends ListingComponent<DossierTemplate> implements OnInit {
export class DossierTemplatesListingScreenComponent extends ListingComponent<DossierTemplate> {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@ -55,10 +55,6 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
super(_injector);
}
ngOnInit(): void {
this.loadDossierTemplateStats();
}
openBulkDeleteTemplatesDialog($event?: MouseEvent) {
return this._dialogService.openDialog('confirm', $event, null, () => {
this._loadingService.loadWhile(this._deleteTemplates());
@ -66,23 +62,7 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
}
openAddDossierTemplateDialog() {
this._dialogService.openDialog('addEditDossierTemplate', null, null, () => {
this.loadDossierTemplateStats();
});
}
loadDossierTemplateStats() {
this.entitiesService.all.forEach(rs => {
const dictionaries = this._appStateService.dictionaryData[rs.dossierTemplateId];
if (dictionaries) {
rs.dictionariesCount = Object.keys(dictionaries)
.map(key => dictionaries[key])
.filter(d => !d.virtual || d.type === 'false_positive').length;
} else {
rs.dictionariesCount = 0;
rs.totalDictionaryEntries = 0;
}
});
this._dialogService.openDialog('addEditDossierTemplate', null, null);
}
private async _deleteTemplates(templateIds = this.listingService.selected.map(d => d.dossierTemplateId)) {
@ -96,8 +76,6 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
this._toaster.error(_('dossier-templates-listing.error.generic'));
}
});
await this._dossierTemplatesService.loadAll().toPromise();
await this._appStateService.loadDictionaryData();
this.loadDossierTemplateStats();
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { TableItemComponent } from './table-item/table-item.component';
import { DossierTemplatesListingScreenComponent } from './dossier-templates-listing-screen/dossier-templates-listing-screen.component';
import { SharedAdminModule } from '../../shared/shared-admin.module';
const routes = [{ path: '', component: DossierTemplatesListingScreenComponent }];
@NgModule({
declarations: [TableItemComponent, DossierTemplatesListingScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedAdminModule],
providers: [],
})
export class DossierTemplatesListingModule {}

View File

@ -0,0 +1,35 @@
<div class="cell">
<div class="table-item-title heading">
{{ dossierTemplate.name }}
</div>
<div class="small-label stats-subtitle">
<div *ngIf="stats$ | async as stats">
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'dossier-templates-listing.dictionaries' | translate: { length: stats.numberOfDictionaries } }}
</div>
</div>
</div>
<div class="cell user-column">
<redaction-initials-avatar
[defaultValue]="'unknown' | translate"
[user]="dossierTemplate.createdBy || 'system'"
[withName]="true"
></redaction-initials-avatar>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
</div>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateModified | date: 'd MMM. yyyy' }}
</div>
<redaction-dossier-template-actions
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"
></redaction-dossier-template-actions>
</div>

View File

@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { DossierTemplate, DossierTemplateStats } from '@red/domain';
import { BehaviorSubject, Observable } from 'rxjs';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'redaction-table-item [dossierTemplate]',
templateUrl: './table-item.component.html',
styleUrls: ['./table-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableItemComponent implements OnChanges {
@Input() dossierTemplate!: DossierTemplate;
readonly stats$: Observable<DossierTemplateStats>;
private readonly _ngOnChanges$ = new BehaviorSubject<string>(undefined);
constructor(readonly dossierTemplateStatsService: DossierTemplateStatsService) {
this.stats$ = this._ngOnChanges$.pipe(switchMap(id => this.dossierTemplateStatsService.watch$(id)));
}
ngOnChanges() {
if (this.dossierTemplate) {
this._ngOnChanges$.next(this.dossierTemplate.id);
}
}
}

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DossierTemplateInfoScreenComponent } from './info-screen/dossier-template-info-screen.component';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
const routes = [{ path: '', component: DossierTemplateInfoScreenComponent }];
@NgModule({
declarations: [DossierTemplateInfoScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule],
})
export class DossierTemplateInfoModule {}

View File

@ -0,0 +1,46 @@
<div *ngIf="dossierTemplate$ | async as dossierTemplate" class="content-container" iqserHasScrollbar>
<ng-container *ngIf="dossierTemplateStats$ | async as stats">
<div class="heading-xl">{{ dossierTemplate.name }}</div>
<div class="all-caps-label mt-24 mb-8" translate="dossier-template-info-screen.created-by"></div>
<redaction-initials-avatar
[user]="dossierTemplate.createdBy || 'system'"
[withName]="true"
size="large"
></redaction-initials-avatar>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'dossier-template-info-screen.dictionaries' | translate: { count: stats.numberOfDictionaries } }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ 'dossier-template-info-screen.created-on' | translate: { date: dossierTemplate.dateAdded | date: 'd MMM. yyyy' } }}
</div>
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ 'dossier-template-info-screen.entries' | translate: { count: stats.numberOfEntries } }}
</div>
<div *ngIf="dossierTemplate.dateModified">
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ 'dossier-template-info-screen.modified-on' | translate: { date: dossierTemplate.dateModified | date: 'd MMM. yyyy' } }}
</div>
</div>
<div class="heading mt-40" translate="dossier-template-info-screen.description">
<iqser-circle-button
(action)="openEditDossierTemplateDialog($event, dossierTemplate)"
*ngIf="permissionsService.isAdmin()"
class="ml-8"
icon="iqser:edit"
></iqser-circle-button>
</div>
<div>{{ dossierTemplate.description }}</div>
</ng-container>
</div>

View File

@ -0,0 +1,30 @@
@use 'variables';
@use 'common-mixins';
:host {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.content-container {
flex: 1;
padding: 30px;
overflow: auto;
@include common-mixins.scroll-bar;
}
.heading {
display: flex;
align-items: center;
margin-top: 40px;
margin-bottom: 8px;
}
.stats-subtitle {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(2, max-content);
grid-row-gap: 8px;
grid-column-gap: 40px;
}

View File

@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { DossierTemplate, DossierTemplateStats } from '@red/domain';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { PermissionsService } from '@services/permissions.service';
@Component({
templateUrl: './dossier-template-info-screen.component.html',
styleUrls: ['./dossier-template-info-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierTemplateInfoScreenComponent {
dossierTemplate$: Observable<DossierTemplate>;
dossierTemplateStats$: Observable<DossierTemplateStats>;
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
private readonly _dialogService: AdminDialogService,
private readonly _route: ActivatedRoute,
readonly permissionsService: PermissionsService,
) {
const dossierTemplateId = _route.snapshot.paramMap.get('dossierTemplateId');
this.dossierTemplate$ = this._dossierTemplatesService.getEntityChanged$(dossierTemplateId);
this.dossierTemplateStats$ = this._dossierTemplateStatsService.watch$(dossierTemplateId);
}
openEditDossierTemplateDialog($event: any, dossierTemplate: DossierTemplate) {
this._dialogService.openDialog('addEditDossierTemplate', $event, dossierTemplate);
}
}

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="flex-1 actions">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -1,6 +1,6 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { Router } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { CircleButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -18,7 +18,6 @@ export class DossierTemplateActionsComponent implements OnInit {
readonly currentUser = this._userService.currentUser;
@Input() dossierTemplateId: string;
@Output() readonly loadDossierTemplatesData = new EventEmitter<void>();
constructor(
private readonly _router: Router,
@ -39,9 +38,7 @@ export class DossierTemplateActionsComponent implements OnInit {
}
openEditDossierTemplateDialog($event: any) {
this._dialogService.openDialog('addEditDossierTemplate', $event, this.dossierTemplate, () => {
this.loadDossierTemplatesData.emit();
});
this._dialogService.openDialog('addEditDossierTemplate', $event, this.dossierTemplate);
}
openDeleteDossierTemplateDialog($event?: MouseEvent) {
@ -54,7 +51,6 @@ export class DossierTemplateActionsComponent implements OnInit {
.then(async () => {
await this._dossierTemplatesService.loadAll().toPromise();
await this._appStateService.loadDictionaryData();
this.loadDossierTemplatesData.emit();
await this._router.navigate(['main', 'admin']);
})
.catch(error => {

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { DossierTemplateActionsComponent } from './components/dossier-template-actions/dossier-template-actions.component';
const components = [DossierTemplateActionsComponent];
@NgModule({
declarations: [...components],
exports: [...components],
providers: [],
imports: [CommonModule, SharedModule],
})
export class SharedAdminModule {}

View File

@ -49,7 +49,7 @@
</div>
<div *ngIf="showActionButtons" class="dialog-actions">
<button (click)="save()" [disabled]="disabled || !valid || !changed" color="primary" mat-flat-button>
<button (click)="save()" [disabled]="disabled || !valid || !changed" color="primary" mat-flat-button type="button">
{{ 'edit-dossier-dialog.actions.save' | translate }}
</button>
<iqser-icon-button

View File

@ -5,7 +5,7 @@ import { AutoUnsubscribe } from '@iqser/common-ui';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { Observable } from 'rxjs';
import { PermissionsService } from '../../../../../../services/permissions.service';
import { PermissionsService } from '@services/permissions.service';
@Component({
selector: 'redaction-document-info [file] [dossier]',

View File

@ -2,8 +2,9 @@ import { Injectable, Injector } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { EntitiesService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { Dictionary, IColors, IDictionary, IUpdateDictionary } from '@red/domain';
import { tap } from 'rxjs/operators';
import { mapTo, switchMap, tap } from 'rxjs/operators';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
const MIN_WORD_LENGTH = 2;
@ -11,7 +12,11 @@ const MIN_WORD_LENGTH = 2;
providedIn: 'root',
})
export class DictionaryService extends EntitiesService<Dictionary, IDictionary> {
constructor(private readonly _toaster: Toaster, protected readonly _injector: Injector) {
constructor(
private readonly _toaster: Toaster,
protected readonly _injector: Injector,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
) {
super(_injector, Dictionary, 'dictionary');
}
@ -31,7 +36,9 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
deleteDictionaries(@RequiredParam() body: List, @RequiredParam() dossierTemplateId: string, dossierId?: string) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const url = `${this._defaultModelPath}/type/${dossierTemplateId}/delete`;
return this._post<unknown>(body, url, queryParams);
return this._post<unknown>(body, url, queryParams).pipe(
switchMap(dictionaries => this._dossierTemplateStatsService.getFor([dossierTemplateId]).pipe(mapTo(dictionaries))),
);
}
/**
@ -63,7 +70,9 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
) {
const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`;
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._post(body, url, queryParams);
return this._post(body, url, queryParams).pipe(
switchMap(dictionary => this._dossierTemplateStatsService.getFor([dossierTemplateId]).pipe(mapTo(dictionary))),
);
}
/**
@ -78,43 +87,11 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
* Creates entry type with colors, hint and caseInsensitive
*/
@Validate()
addDictionary(@RequiredParam() body: IDictionary, dossierId?: string) {
addDictionary(@RequiredParam() dictionary: IDictionary, dossierId?: string) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._post(body, `${this._defaultModelPath}/type`, queryParams);
}
/**
* Add dictionary entries with entry type.
*/
@Validate()
addEntry(
@RequiredParam() body: List,
@RequiredParam() dossierTemplateId: string,
@RequiredParam() type: string,
dossierId?: string,
removeCurrent?: boolean,
) {
const queryParams: List<QueryParam> = [
{ key: 'dossierId', value: dossierId },
{ key: 'removeCurrent', value: removeCurrent },
];
const url = `${this._defaultModelPath}/${type}/${dossierTemplateId}`;
return this._post(body, url, queryParams);
}
/**
* Delete dictionary entries with entry type.
*/
@Validate()
deleteEntries(
@RequiredParam() body: List,
@RequiredParam() dossierTemplateId: string,
@RequiredParam() type: string,
@RequiredParam() dossierId?: string,
) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const url = `${this._defaultModelPath}/delete/${type}/${dossierTemplateId}`;
return this._post(body, url, queryParams);
return this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])),
);
}
saveEntries(
@ -134,14 +111,15 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
const invalidRowsExist = entriesToAdd.filter(e => e.length < MIN_WORD_LENGTH);
if (invalidRowsExist.length === 0) {
// can add at least 1 - block UI
let obs: Observable<any>;
let obs: Observable<IDictionary>;
if (entriesToAdd.length > 0) {
obs = this.addEntry(entriesToAdd, dossierTemplateId, type, dossierId, true);
obs = this._addEntry(entriesToAdd, dossierTemplateId, type, dossierId, true);
} else {
obs = this.deleteEntries(initialEntries, dossierTemplateId, type, dossierId);
obs = this._deleteEntries(initialEntries, dossierTemplateId, type, dossierId);
}
return obs.pipe(
switchMap(dictionary => this._dossierTemplateStatsService.getFor([dossierTemplateId]).pipe(mapTo(dictionary))),
tap(
() => {
if (showToast) {
@ -157,4 +135,38 @@ export class DictionaryService extends EntitiesService<Dictionary, IDictionary>
return throwError('Entries too short');
}
}
/**
* Add dictionary entries with entry type.
*/
@Validate()
private _addEntry(
@RequiredParam() body: List,
@RequiredParam() dossierTemplateId: string,
@RequiredParam() type: string,
dossierId?: string,
removeCurrent?: boolean,
) {
const queryParams: List<QueryParam> = [
{ key: 'dossierId', value: dossierId },
{ key: 'removeCurrent', value: removeCurrent },
];
const url = `${this._defaultModelPath}/${type}/${dossierTemplateId}`;
return this._post(body, url, queryParams);
}
/**
* Delete dictionary entries with entry type.
*/
@Validate()
private _deleteEntries(
@RequiredParam() body: List,
@RequiredParam() dossierTemplateId: string,
@RequiredParam() type: string,
@RequiredParam() dossierId?: string,
) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const url = `${this._defaultModelPath}/delete/${type}/${dossierTemplateId}`;
return this._post(body, url, queryParams);
}
}

View File

@ -7,7 +7,7 @@ import {
IPrepareDownloadRequest,
IRemoveDownloadRequest,
} from '@red/domain';
import { Observable } from 'rxjs';
import { firstValueFrom, Observable } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { map, switchMap, tap } from 'rxjs/operators';
import { KeycloakService } from 'keycloak-angular';
@ -44,12 +44,9 @@ export class FileDownloadService extends EntitiesService<DownloadStatus, IDownlo
}
async performDownload(status: DownloadStatus) {
const token = await firstValueFrom(this.generateToken(status.storageId));
const anchor = document.createElement('a');
anchor.href = `${this._configService.values.API_URL}/async/download?storageId=${encodeURIComponent(status.storageId)}`;
if (!this._configService.values.USE_SESSION_FOR_DOWNLOAD) {
const token = await this._keycloakService.getToken();
anchor.href = anchor.href + `&access_token=${encodeURIComponent(token)}`;
}
anchor.href = `${this._configService.values.API_URL}/async/download/${token.value}`;
anchor.download = status.filename;
anchor.target = '_blank';
@ -63,6 +60,11 @@ export class FileDownloadService extends EntitiesService<DownloadStatus, IDownlo
return this._post(body, `${this._defaultModelPath}/prepare`);
}
@Validate()
generateToken(@RequiredParam() storageId: string): Observable<{ value: string }> {
return this._post<{ value: string }>({ value: storageId }, `${this._defaultModelPath}/generate-ott`);
}
@Validate()
delete(@RequiredParam() body: IRemoveDownloadRequest): Observable<unknown> {
return super._post(body, `${this._defaultModelPath}/delete`);

View File

@ -1,52 +1,12 @@
import { Injectable } from '@angular/core';
import { HeadersConfiguration, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { Injectable, Injector } from '@angular/core';
import { StatsService } from '@iqser/common-ui';
import { DossierStats, IDossierStats } from '@red/domain';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { UserService } from '@services/user.service';
@Injectable({
providedIn: 'root',
})
export class DossierStatsService {
private readonly _map = new Map<string, BehaviorSubject<DossierStats>>();
constructor(private readonly _http: HttpClient, private readonly _userService: UserService) {}
@Validate()
getFor(@RequiredParam() dossierIds: string[]): Observable<DossierStats[]> {
if (!this._userService.currentUser.isUser) {
return of([]);
}
const request = this._http.post<IDossierStats[]>(`/${encodeURI('dossier-stats')}`, dossierIds, {
headers: HeadersConfiguration.getHeaders(),
observe: 'body',
});
return request.pipe(
mapEach(entity => new DossierStats(entity)),
tap(entities => entities.forEach(entity => this.set(entity))),
);
}
get(key: string): DossierStats {
return this._map.get(key)?.value;
}
set(stats: DossierStats): void {
if (!this._map.has(stats.dossierId)) {
this._map.set(stats.dossierId, new BehaviorSubject<DossierStats>(stats));
return;
}
const old = this.get(stats.dossierId);
if (JSON.stringify(old) !== JSON.stringify(stats)) {
this._map.get(stats.dossierId).next(stats);
}
}
watch$(key: string): Observable<DossierStats> {
return this._map.get(key)?.asObservable();
export class DossierStatsService extends StatsService<DossierStats, IDossierStats> {
constructor(protected readonly _injector: Injector) {
super(_injector, 'dossierId', DossierStats, 'dossier-stats');
}
}

View File

@ -0,0 +1,12 @@
import { Injectable, Injector } from '@angular/core';
import { StatsService } from '@iqser/common-ui';
import { DossierTemplateStats, IDossierTemplateStats } from '@red/domain';
@Injectable({
providedIn: 'root',
})
export class DossierTemplateStatsService extends StatsService<DossierTemplateStats, IDossierTemplateStats> {
constructor(protected readonly _injector: Injector) {
super(_injector, 'dossierTemplateId', DossierTemplateStats, 'dossier-template-stats');
}
}

View File

@ -1,11 +1,12 @@
import { EntitiesService, List, RequiredParam, Validate } from '@iqser/common-ui';
import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { DossierTemplate, IDossierTemplate } from '@red/domain';
import { Injectable, Injector } from '@angular/core';
import { BehaviorSubject, forkJoin, Observable } from 'rxjs';
import { FileAttributesService } from './file-attributes.service';
import { ActivationEnd, Router } from '@angular/router';
import { currentComponentRoute } from '@utils/functions';
import { map, switchMap } from 'rxjs/operators';
import { mapTo, switchMap, tap } from 'rxjs/operators';
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
@Injectable({
providedIn: 'root',
@ -18,6 +19,7 @@ export class DossierTemplatesService extends EntitiesService<DossierTemplate, ID
protected readonly _injector: Injector,
private readonly _fileAttributesService: FileAttributesService,
private readonly _router: Router,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
) {
super(_injector, DossierTemplate, 'dossier-template');
this.activeDossierTemplate$ = this._activeDossierTemplate$.asObservable();
@ -53,18 +55,25 @@ export class DossierTemplatesService extends EntitiesService<DossierTemplate, ID
loadAll(): Observable<DossierTemplate[]> {
const getAttributes = (entities: DossierTemplate[]) => entities.map(e => this._fileAttributesService.getFileAttributesConfig(e.id));
return super.loadAll().pipe(
switchMap(entities => forkJoin(getAttributes(entities))),
map(() => this.all),
const dossierTemplateIds = (templates: DossierTemplate[]) => templates.map(d => d.id);
return this.getAll().pipe(
mapEach(entity => new DossierTemplate(entity)),
/* Load stats before updating entities */
switchMap(templates =>
forkJoin([this._dossierTemplateStatsService.getFor(dossierTemplateIds(templates)), ...getAttributes(templates)]).pipe(
mapTo(templates),
),
),
tap(templates => this.setEntities(templates)),
);
}
delete(body: List): Observable<unknown> {
return super._post(body, `${this._defaultModelPath}/delete`);
return super._post(body, `${this._defaultModelPath}/delete`).pipe(switchMap(() => this.loadAll()));
}
@Validate()
createOrUpdate(@RequiredParam() body: IDossierTemplate) {
return this._post(body);
return this._post(body).pipe(switchMap(() => this.loadAll()));
}
}

View File

@ -49,7 +49,7 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
}
loadAll(): Observable<Dossier[]> {
const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.dossierId);
const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id);
return this.getAll().pipe(
mapEach(entity => new Dossier(entity)),
/* Load stats before updating entities */

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://aks-staging.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://dev-04.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -17,8 +17,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://aks-staging.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://dev-04.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"USE_SESSION_FOR_DOWNLOAD": false
"SELECTION_MODE": "structural"
}

View File

@ -789,6 +789,15 @@
"under-review": "Under Review",
"upload-files": "Drag & drop files anywhere..."
},
"dossier-template-info": "Info",
"dossier-template-info-screen": {
"created-by": "Created by",
"created-on": "Created on: {date}",
"description": "Description",
"dictionaries": "{count} {count, plural, one{dictionary} other{dictionaries}}",
"entries": "{count} {count, plural, one{entry} other{entries}}",
"modified-on": "Modified on: {date}"
},
"dossier-templates": "Dossier Templates",
"dossier-templates-listing": {
"action": {

View File

@ -24,7 +24,6 @@ OAUTH_URL="${OAUTH_URL:-/auth}"
RECENT_PERIOD_IN_HOURS="${RECENT_PERIOD_IN_HOURS:-24}"
SELECTION_MODE="${SELECTION_MODE:-structural}"
USE_SESSION_FOR_DOWNLOAD="${USE_SESSION_FOR_DOWNLOAD:-false}"
echo '{
@ -47,8 +46,7 @@ echo '{
"OAUTH_IDP_HINT":"'"$OAUTH_IDP_HINT"'",
"OAUTH_URL":"'"$OAUTH_URL"'",
"RECENT_PERIOD_IN_HOURS":'"$RECENT_PERIOD_IN_HOURS"',
"SELECTION_MODE":"'"$SELECTION_MODE"'",
"USE_SESSION_FOR_DOWNLOAD":'"$USE_SESSION_FOR_DOWNLOAD"'
"SELECTION_MODE":"'"$SELECTION_MODE"'"
}' > /usr/share/nginx/html/ui/assets/config/config.json
echo 'Env variables: '

View File

@ -0,0 +1,23 @@
import { DictionarySummary, IDossierTemplateStats } from './dossier-template-stats';
export class DossierTemplateStats implements IDossierTemplateStats {
readonly dossierTemplateId: string;
readonly dictionarySummaryList: DictionarySummary[];
readonly numberOfDictionaries: number;
readonly numberOfEntries: number;
private readonly _dictionarySummaryMap = new Map<string, DictionarySummary>();
constructor(stats: IDossierTemplateStats) {
this.dossierTemplateId = stats.dossierTemplateId;
this.dictionarySummaryList = stats.dictionarySummaryList;
this.numberOfDictionaries = stats.numberOfDictionaries;
this.numberOfEntries = this.dictionarySummaryList.reduce((counter, summary) => counter + summary.entriesCount, 0);
this.dictionarySummaryList.forEach(summary => this._dictionarySummaryMap.set(summary.type, summary));
}
dictionarySummary(type: string): DictionarySummary | undefined {
return this._dictionarySummaryMap.get(type);
}
}

View File

@ -0,0 +1,12 @@
export interface DictionarySummary {
readonly entriesCount: number;
readonly id: string;
readonly name: string;
readonly type: string;
}
export interface IDossierTemplateStats {
readonly dossierTemplateId: string;
readonly dictionarySummaryList: DictionarySummary[];
readonly numberOfDictionaries: number;
}

View File

@ -14,8 +14,6 @@ export class DossierTemplate implements IDossierTemplate, IListable {
readonly reportTemplateIds?: List;
readonly validFrom?: string;
readonly validTo?: string;
dictionariesCount = 0;
totalDictionaryEntries = 0;
constructor(dossierTemplate: IDossierTemplate) {
this.createdBy = dossierTemplate.createdBy;
@ -40,6 +38,6 @@ export class DossierTemplate implements IDossierTemplate, IListable {
}
get routerLink(): string {
return `/main/admin/dossier-templates/${this.dossierTemplateId}/dictionaries`;
return `/main/admin/dossier-templates/${this.dossierTemplateId}`;
}
}

View File

@ -1,2 +1,4 @@
export * from './dossier-template';
export * from './dossier-template.model';
export * from './dossier-template-stats';
export * from './dossier-template-stats.model';

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.176.0",
"version": "3.179.0",
"private": true,
"license": "MIT",
"scripts": {

Binary file not shown.