Merge branch 'master' into VM/RED-2853

# Conflicts:
#	apps/red-ui/src/app/modules/admin/admin.module.ts
#	apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts
This commit is contained in:
Valentin Mihai 2022-01-28 11:26:01 +02:00
commit 83c698b2da
320 changed files with 19735 additions and 5964 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

@ -1,6 +1,6 @@
{
"cli": {
"analytics": "4b8eed12-a1e6-4b7a-9ea2-925b27941271"
"analytics": false
},
"version": 1,
"projects": {

View File

@ -1,3 +1,4 @@
<router-outlet></router-outlet>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error>

View File

@ -36,9 +36,10 @@ import { KeycloakService } from 'keycloak-angular';
import { GeneralSettingsService } from '@services/general-settings.service';
import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
export function httpLoaderFactory(httpClient: HttpClient): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', '.json');
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
}
function cleanupBaseUrl(baseUrl: string) {
@ -76,7 +77,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient],
deps: [HttpClient, ConfigService],
},
compiler: {
provide: TranslateCompiler,
@ -114,7 +115,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
provide: APP_INITIALIZER,
multi: true,
useFactory: configurationInitializer,
deps: [KeycloakService, Title, ConfigService, GeneralSettingsService, LanguageService, UserPreferenceService],
deps: [KeycloakService, Title, ConfigService, GeneralSettingsService, LanguageService, UserService, UserPreferenceService],
},
{
provide: MissingTranslationHandler,
@ -136,6 +137,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
export class AppModule {
constructor(private readonly _router: Router, private readonly _route: ActivatedRoute) {
this._configureKeyCloakRouteHandling();
// this._test();
}
private _configureKeyCloakRouteHandling() {
@ -152,4 +154,60 @@ export class AppModule {
}
});
}
// private _test(){
//
// const flatGerman = flatten(german);
//
//
// const flatEnglish = flatten(english);
//
// const tmfc = new TranslateMessageFormatCompiler();
//
//
//
// for (const key of Object.keys(flatGerman)) {
// try {
// const result = tmfc.compile(flatGerman[key], 'de');
// //console.log(result);
// } catch (e) {
// console.error('ERROR AT: ', flatGerman[key]);
// }
// }
//
// for (const key of Object.keys(flatEnglish)) {
// try {
// const result = tmfc.compile(flatEnglish[key], 'de');
// //console.log(result);
// } catch (e) {
// console.error('ERROR AT: ', flatEnglish[key]);
// }
// }
// console.log('done');
// }
}
//
// function flatten(data: any) {
// const result: any = {};
//
// function recurse(cur: any, prop: any) {
// if (Object(cur) !== cur) {
// result[prop] = cur;
// } else if (Array.isArray(cur)) {
// let l = 0;
// for (let i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + '[' + i + ']');
// if (l === 0) result[prop] = [];
// } else {
// let isEmpty = true;
// for (const p in cur) {
// isEmpty = false;
// recurse(cur[p], prop ? prop + '.' + p : p);
// }
// if (isEmpty && prop) result[prop] = {};
// }
// }
//
// recurse(data, '');
// return result;
// }

View File

@ -27,7 +27,6 @@
</div>
<redaction-user-button
[matMenuTriggerFor]="userMenu"
[showDot]="fileDownloadService.hasPendingDownloads"
[userId]="currentUser.id"
iqserHelpMode="open-usermenu"
></redaction-user-button>
@ -36,7 +35,6 @@
<ng-container *ngFor="let item of userMenuItems; trackBy: trackByName">
<button (click)="(item.action)" *ngIf="item.show" [routerLink]="item.routerLink" mat-menu-item translate>
{{ item.name }}
<span *ngIf="item.showDot()" class="dot"></span>
</button>
</ng-container>

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';
@ -36,26 +35,22 @@ export class BaseScreenComponent {
routerLink: '/main/account',
show: true,
action: this.appStateService.reset,
showDot: () => false,
},
{
name: _('top-bar.navigation-items.my-account.children.admin'),
routerLink: '/main/admin',
show: this.currentUser.isManager || this.currentUser.isUserAdmin,
action: this.appStateService.reset,
showDot: () => false,
},
{
name: _('top-bar.navigation-items.my-account.children.downloads'),
routerLink: '/main/downloads',
show: this.currentUser.isUser,
showDot: () => this.fileDownloadService.hasPendingDownloads,
},
{
name: _('top-bar.navigation-items.my-account.children.trash'),
routerLink: '/main/admin/trash',
show: this.currentUser.isManager,
showDot: () => false,
},
];
readonly searchActions: readonly SpotlightSearchAction[] = [
@ -81,11 +76,9 @@ export class BaseScreenComponent {
constructor(
readonly appStateService: AppStateService,
readonly dossiersService: DossiersService,
readonly userService: UserService,
readonly userPreferenceService: UserPreferenceService,
readonly titleService: Title,
readonly fileDownloadService: FileDownloadService,
private readonly _router: Router,
private readonly _translateService: TranslateService,
readonly breadcrumbsService: BreadcrumbsService,

View File

@ -9,7 +9,7 @@
<mat-icon *ngIf="!first" svgIcon="iqser:arrow-right"></mat-icon>
<a
[routerLinkActiveOptions]="breadcrumb.routerLinkActiveOptions ?? { exact: false }"
[routerLinkActiveOptions]="breadcrumb.routerLinkActiveOptions || { exact: false }"
[routerLink]="breadcrumb.routerLink"
class="breadcrumb"
routerLinkActive="active"

View File

@ -11,6 +11,8 @@ import {
} from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { firstValueFrom, interval } from 'rxjs';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'redaction-downloads-list-screen',
@ -43,6 +45,9 @@ export class DownloadsListScreenComponent extends ListingComponent<DownloadStatu
) {
super(_injector);
this._loadingService.loadWhile(this._loadData());
this.addSubscription = interval(5000)
.pipe(switchMap(() => this._loadData()))
.subscribe();
}
downloadItem(download: DownloadStatus) {
@ -55,11 +60,11 @@ export class DownloadsListScreenComponent extends ListingComponent<DownloadStatu
private async _deleteItems(downloads?: DownloadStatus[]) {
const storageIds = (downloads || this.listingService.selected).map(d => d.storageId);
await this.fileDownloadService.delete({ storageIds }).toPromise();
await firstValueFrom(this.fileDownloadService.delete({ storageIds }));
await this._loadData();
}
private async _loadData() {
await this.fileDownloadService.loadAll().toPromise();
await firstValueFrom(this.fileDownloadService.loadAll());
}
}

View File

@ -1,9 +1,5 @@
@use 'variables';
.mt-2 {
margin-top: 2px;
}
::ng-deep .notifications-backdrop + .cdk-overlay-connected-position-bounding-box {
right: 0 !important;
@ -20,6 +16,7 @@
.view-all {
cursor: pointer;
&:hover {
color: var(--iqser-primary);
}

View File

@ -6,7 +6,7 @@ import { DossiersService } from '@services/entity-services/dossiers.service';
import { NotificationsService } from '@services/notifications.service';
import { Notification } from '@red/domain';
import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, timer } from 'rxjs';
import { BehaviorSubject, firstValueFrom, Observable, timer } from 'rxjs';
import { AutoUnsubscribe, List, shareLast } from '@iqser/common-ui';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
@ -63,12 +63,12 @@ export class NotificationsComponent extends AutoUnsubscribe implements OnInit {
async markRead($event, notifications: List<string> = this._notifications$.getValue().map(n => n.id), isRead = true): Promise<void> {
$event.stopPropagation();
await this._notificationsService.toggleNotificationRead(notifications, isRead).toPromise();
await firstValueFrom(this._notificationsService.toggleNotificationRead(notifications, isRead));
await this._loadData();
}
private async _loadData(): Promise<void> {
const notifications = await this._notificationsService.getNotifications(INCLUDE_SEEN).toPromise();
const notifications = await firstValueFrom(this._notificationsService.getNotifications(INCLUDE_SEEN));
this._notifications$.next(notifications);
}

View File

@ -1,30 +1,29 @@
<form (submit)="executeCurrentAction()">
<iqser-input-with-action
(click)="openMenuIfValue()"
(valueChange)="valueChanges$.next($event)"
[placeholder]="placeholder"
></iqser-input-with-action>
<iqser-input-with-action
(action)="executeCurrentAction()"
(click)="openMenuIfValue()"
(valueChange)="valueChanges$.next($event)"
[placeholder]="placeholder"
></iqser-input-with-action>
<mat-menu #menu="matMenu" class="search-menu" xPosition="after">
<ng-template matMenuContent>
<div class="wrapper">
<button
(click)="item.action(valueChanges$.getValue())"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon"></mat-icon>
<span>{{ item.text }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<mat-menu #menu="matMenu" class="search-menu" xPosition="after">
<ng-template matMenuContent>
<div class="wrapper">
<button
(click)="item.action(valueChanges$.getValue())"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon"></mat-icon>
<span>{{ item.text }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<!-- https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically -->
<!-- To toggle menu programmatically a matMenuTriggerFor directive is needed -->
<div [matMenuTriggerFor]="menu"></div>
<!-- https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically -->
<!-- To toggle menu programmatically a matMenuTriggerFor directive is needed -->
<div [matMenuTriggerFor]="menu"></div>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="showActions$ | async"></ng-container>
</form>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="showActions$ | async"></ng-container>

View File

@ -1,40 +1,34 @@
import { CanDeactivate } from '@angular/router';
import { Directive, HostListener, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { ConfirmationDialogService, ConfirmOptions } from '@iqser/common-ui';
export interface ComponentCanDeactivate {
hasChanges: boolean;
}
@Directive()
export abstract class ComponentHasChanges implements ComponentCanDeactivate {
abstract hasChanges: boolean;
protected constructor(protected _translateService: TranslateService) {}
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
if (this.hasChanges) {
// This message will be displayed in IE/Edge
$event.returnValue = this._translateService.instant('pending-changes-guard');
}
}
changed: boolean;
valid?: boolean;
isLeavingPage?: boolean;
save: () => Promise<void>;
}
@Injectable({ providedIn: 'root' })
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
constructor(private readonly _translateService: TranslateService) {}
constructor(private _dialogService: ConfirmationDialogService) {}
canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
// if there are no pending changes, just allow deactivation; else confirm first
return !component.hasChanges
? true
: // NOTE: this warning message will only be shown when navigating elsewhere
// within your angular app;
// when navigating away from your angular app,
// the browser will show a generic warning message
// see http://stackoverflow.com/a/42207299/7307355
confirm(this._translateService.instant('pending-changes-guard'));
if (component.changed) {
component.isLeavingPage = true;
const dialogRef = this._dialogService.openDialog({ disableConfirm: component.valid === false });
return dialogRef.afterClosed().pipe(
map(result => {
if (result === ConfirmOptions.CONFIRM) {
component.save();
}
component.isLeavingPage = false;
return !!result;
}),
);
}
return true;
}
}

View File

@ -3,6 +3,7 @@ import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilesService } from '@services/entity-services/files.service';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DossierFilesGuard implements CanActivate {
@ -22,7 +23,7 @@ export class DossierFilesGuard implements CanActivate {
}
if (!this._filesMapService.has(dossierId)) {
await this._filesService.loadAll(dossierId).toPromise();
await firstValueFrom(this._filesService.loadAll(dossierId));
}
return true;
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { TranslateService } from '@ngx-translate/core';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DossiersGuard implements CanActivate {
@ -12,7 +13,7 @@ export class DossiersGuard implements CanActivate {
) {}
async canActivate(): Promise<boolean> {
await this._dossiersService.loadAll().toPromise();
await firstValueFrom(this._dossiersService.loadAll());
return true;
}
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { UserPreferenceService } from '@services/user-preference.service';
import { firstValueFrom } from 'rxjs';
@Injectable({
providedIn: 'root',
@ -8,6 +9,7 @@ import { UserPreferenceService } from '@services/user-preference.service';
export class LanguageService {
constructor(private readonly _translateService: TranslateService, private readonly _userPreferenceService: UserPreferenceService) {
_translateService.addLangs(['en', 'de']);
_translateService.setDefaultLang('en');
}
get currentLanguage() {
@ -26,12 +28,12 @@ export class LanguageService {
}
document.documentElement.lang = defaultLang;
this._translateService.setDefaultLang(defaultLang);
this._translateService.use(defaultLang).toPromise().then();
firstValueFrom(this._translateService.use(defaultLang)).then();
}
async changeLanguage(language: string) {
await this._userPreferenceService.saveLanguage(language);
document.documentElement.lang = language;
await this._translateService.use(language).toPromise();
await firstValueFrom(this._translateService.use(language));
}
}

View File

@ -1,6 +1,6 @@
import { AnnotationWrapper } from './annotation.wrapper';
import { isArray } from 'rxjs/internal-compatibility';
import { User } from '@red/domain';
import { isArray } from 'lodash';
export class AnnotationPermissions {
canUndo = true;
@ -14,6 +14,7 @@ export class AnnotationPermissions {
canChangeLegalBasis = true;
canResizeAnnotation = true;
canRecategorizeImage = true;
canForceHint = true;
static forUser(isApprover: boolean, user: User, annotations: AnnotationWrapper | AnnotationWrapper[]) {
if (!isArray(annotations)) {
@ -29,12 +30,13 @@ export class AnnotationPermissions {
permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
permissions.canRejectSuggestion = isApprover && annotation.isSuggestion;
permissions.canForceHint = annotation.isIgnoredHint;
permissions.canForceRedaction = annotation.isSkipped && !annotation.isFalsePositive;
permissions.canAcceptRecommendation = annotation.isRecommendation;
permissions.canMarkAsFalsePositive = annotation.canBeMarkedAsFalsePositive;
permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted;
permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted || annotation.isHint;
permissions.canRemoveOrSuggestToRemoveFromDictionary =
annotation.isModifyDictionary && (annotation.isRedacted || annotation.isSkipped || annotation.isHint);
@ -42,7 +44,9 @@ export class AnnotationPermissions {
permissions.canRecategorizeImage = (annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage;
permissions.canResizeAnnotation =
((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) || annotation.isSuggestionResize;
((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) ||
annotation.isSuggestionResize ||
annotation.isRecommendation;
summedPermissions._merge(permissions);
}

View File

@ -12,6 +12,7 @@ export type AnnotationSuperType =
| 'suggestion-remove-dictionary'
| 'suggestion-add'
| 'suggestion-remove'
| 'ignored-hint'
| 'skipped'
| 'redaction'
| 'manual-redaction'
@ -43,7 +44,9 @@ export class AnnotationWrapper {
legalBasisChangeValue?: string;
resizing?: boolean;
rectangle?: boolean;
hintDictionary?: boolean;
section?: string;
reference: Array<string>;
manual?: boolean;
@ -87,7 +90,7 @@ export class AnnotationWrapper {
}
get isSuperTypeBasedColor() {
return this.isSkipped || this.isSuggestion || this.isDeclinedSuggestion;
return this.isSkipped || this.isSuggestion || this.isDeclinedSuggestion || this.isIgnoredHint;
}
get isSkipped() {
@ -106,6 +109,20 @@ export class AnnotationWrapper {
return this.recategorizationType || this.typeValue;
}
get topLevelFilter() {
return (
this.superType !== 'hint' &&
this.superType !== 'redaction' &&
this.superType !== 'recommendation' &&
this.superType !== 'ignored-hint' &&
this.superType !== 'skipped'
);
}
get filterKey() {
return this.topLevelFilter ? this.superType : this.superType + this.type;
}
get isManuallySkipped() {
return this.isSkipped && this.manual;
}
@ -113,7 +130,10 @@ export class AnnotationWrapper {
get isFalsePositive() {
return (
this.type?.toLowerCase() === 'false_positive' &&
(this.superType === 'skipped' || this.superType === 'hint' || this.superType === 'redaction')
(this.superType === 'skipped' ||
this.superType === 'hint' ||
this.superType === 'ignored-hint' ||
this.superType === 'redaction')
);
}
@ -133,6 +153,10 @@ export class AnnotationWrapper {
return this.superType === 'hint';
}
get isIgnoredHint() {
return this.superType === 'ignored-hint';
}
get isRedacted() {
return this.superType === 'redaction' || this.superType === 'manual-redaction';
}
@ -236,12 +260,14 @@ export class AnnotationWrapper {
annotationWrapper.manual = redactionLogEntry.manual;
annotationWrapper.engines = redactionLogEntry.engines;
annotationWrapper.section = redactionLogEntry.section;
annotationWrapper.reference = redactionLogEntry.reference || [];
annotationWrapper.rectangle = redactionLogEntry.rectangle;
annotationWrapper.hasBeenResized = redactionLogEntry.hasBeenResized;
annotationWrapper.hasBeenRecategorized = redactionLogEntry.hasBeenRecategorized;
annotationWrapper.hasLegalBasisChanged = redactionLogEntry.hasLegalBasisChanged;
annotationWrapper.hasBeenForced = redactionLogEntry.hasBeenForced;
annotationWrapper.hasBeenRemovedByManualOverride = redactionLogEntry.hasBeenRemovedByManualOverride;
annotationWrapper.hintDictionary = redactionLogEntry.hintDictionary;
this._createContent(annotationWrapper, redactionLogEntry);
this._setSuperType(annotationWrapper, redactionLogEntry);
@ -259,11 +285,7 @@ export class AnnotationWrapper {
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) {
if (redactionLogEntryWrapper.recommendation) {
if (redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = 'recommendation';
} else {
annotationWrapper.superType = 'skipped';
}
annotationWrapper.superType = 'recommendation';
return;
}
@ -280,7 +302,7 @@ export class AnnotationWrapper {
if (redactionLogEntryWrapper.status === 'REQUESTED') {
annotationWrapper.superType = 'suggestion-force-redaction';
} else if (redactionLogEntryWrapper.status === 'APPROVED') {
annotationWrapper.superType = 'redaction';
annotationWrapper.superType = redactionLogEntryWrapper.hint ? 'hint' : 'redaction';
} else {
annotationWrapper.superType = 'skipped';
}
@ -408,6 +430,11 @@ export class AnnotationWrapper {
if (!annotationWrapper.superType) {
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped';
}
if (annotationWrapper.superType === 'skipped') {
if (redactionLogEntryWrapper.hintDictionary) {
annotationWrapper.superType = 'ignored-hint';
}
}
}
private static _createContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntryWrapper) {

View File

@ -1,36 +1,48 @@
import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, User, ViewMode } from '@red/domain';
import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ViewMode } from '@red/domain';
import { AnnotationWrapper } from './annotation.wrapper';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
import * as moment from 'moment';
export class AnnotationData {
visibleAnnotations: AnnotationWrapper[];
allAnnotations: AnnotationWrapper[];
}
import { BehaviorSubject } from 'rxjs';
export class FileDataModel {
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
allAnnotations: AnnotationWrapper[];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly blob$ = new BehaviorSubject<Blob>(undefined);
readonly file$ = new BehaviorSubject<File>(undefined);
hasChangeLog: boolean;
constructor(
private readonly _file: File,
private readonly _blob: Blob,
private _redactionLog: IRedactionLog,
public viewedPages?: IViewedPage[],
private _dictionaryData?: { [p: string]: Dictionary },
private _areDevFeaturesEnabled?: boolean,
) {
this.file$.next(_file);
this.blob$.next(_blob);
this._buildAllAnnotations();
}
constructor(public file: File, public fileData: Blob, public redactionLog: IRedactionLog, public viewedPages?: IViewedPage[]) {}
get file(): File {
return this.file$.value;
}
getAnnotations(
dictionaryData: { [p: string]: Dictionary },
currentUser: User,
viewMode: ViewMode,
areDevFeaturesEnabled: boolean,
): AnnotationData {
const entries: RedactionLogEntryWrapper[] = this._convertData();
let allAnnotations = entries
.map(entry => AnnotationWrapper.fromData(entry))
.filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber));
set file(file: File) {
this.file$.next(file);
}
if (!areDevFeaturesEnabled) {
allAnnotations = allAnnotations.filter(annotation => !annotation.isFalsePositive);
}
get redactionLog(): IRedactionLog {
return this._redactionLog;
}
const visibleAnnotations = allAnnotations.filter(annotation => {
set redactionLog(redactionLog: IRedactionLog) {
this._redactionLog = redactionLog;
this._buildAllAnnotations();
}
getVisibleAnnotations(viewMode: ViewMode) {
return this.allAnnotations.filter(annotation => {
if (viewMode === 'STANDARD') {
return !annotation.isChangeLogRemoved;
} else if (viewMode === 'DELTA') {
@ -39,11 +51,30 @@ export class FileDataModel {
return annotation.isRedacted;
}
});
}
return {
visibleAnnotations: visibleAnnotations,
allAnnotations: allAnnotations,
};
private _buildAllAnnotations() {
const entries: RedactionLogEntryWrapper[] = this._convertData();
const previousAnnotations = this.allAnnotations || [];
this.allAnnotations = entries
.map(entry => AnnotationWrapper.fromData(entry))
.filter(ann => ann.manual || !this._file.excludedPages.includes(ann.pageNumber));
if (!this._areDevFeaturesEnabled) {
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
}
this._setHiddenPropertyToNewAnnotations(this.allAnnotations, previousAnnotations);
}
private _setHiddenPropertyToNewAnnotations(newAnnotations: AnnotationWrapper[], oldAnnotations: AnnotationWrapper[]) {
newAnnotations.forEach(newAnnotation => {
const oldAnnotation = oldAnnotations.find(a => a.annotationId === newAnnotation.annotationId);
if (oldAnnotation) {
newAnnotation.hidden = oldAnnotation.hidden;
}
});
}
private _convertData(): RedactionLogEntryWrapper[] {
@ -55,6 +86,7 @@ export class FileDataModel {
const redactionLogEntryWrapper: RedactionLogEntryWrapper = {};
Object.assign(redactionLogEntryWrapper, redactionLogEntry);
redactionLogEntryWrapper.type = redactionLogEntry.type;
redactionLogEntryWrapper.hintDictionary = this._dictionaryData[redactionLogEntry.type].hint;
this._isChangeLogEntry(redactionLogEntry, redactionLogEntryWrapper);
@ -101,11 +133,11 @@ export class FileDataModel {
}
private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) {
if (this.file.numberOfAnalyses > 1) {
redactionLogEntry.changes.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
if (this._file.numberOfAnalyses > 1) {
const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
const lastChange =
redactionLogEntry.changes.length >= 1 ? redactionLogEntry.changes[redactionLogEntry.changes.length - 1] : undefined;
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
const page = redactionLogEntry.positions?.[0].page;
const viewedPage = this.viewedPages.filter(p => p.page === page).pop();
@ -114,14 +146,14 @@ export class FileDataModel {
if (viewedPage) {
const viewTime = moment(viewedPage.viewedTime).valueOf() - FileDataModel.DELTA_VIEW_TIME;
// these are all unseen changes
const relevantChanges = redactionLogEntry.changes.filter(change => moment(change.dateTime).valueOf() > viewTime);
const relevantChanges = viableChanges.filter(change => moment(change.dateTime).valueOf() > viewTime);
// at least one unseen change
if (relevantChanges.length > 0) {
// at least 1 relevant change
wrapper.changeLogType = relevantChanges[relevantChanges.length - 1].type;
wrapper.isChangeLogEntry = true;
viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf();
this.hasChangeLog = true;
this.hasChangeLog$.next(true);
} else {
// no relevant changes - hide removed anyway
wrapper.isChangeLogEntry = false;

View File

@ -11,6 +11,7 @@ export interface RedactionLogEntryWrapper {
startOffset?: number;
type?: string;
rectangle?: boolean;
hintDictionary?: boolean;
color?: Array<number>;
dictionaryEntry?: boolean;

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

@ -1,43 +0,0 @@
export const NotificationCategories = {
inAppNotifications: 'inAppNotifications',
emailNotifications: 'emailNotifications',
} as const;
export const NotificationCategoriesValues = Object.values(NotificationCategories);
export const DossierNotificationsTypes = {
dossierOwnerSet: 'DOSSIER_OWNER_SET',
dossierOwnerRemoved: 'DOSSIER_OWNER_REMOVED',
userBecomseDossierMember: 'USER_BECOMES_DOSSIER_MEMBER',
userRemovedAsDossierMember: 'USER_REMOVED_AS_DOSSIER_MEMBER',
userPromotedToApprover: 'USER_PROMOTED_TO_APPROVER',
userDegradedToReviewer: 'USER_DEGRADED_TO_REVIEWER',
dossierOwnerDeleted: 'DOSSIER_OWNER_DELETED',
dossierDeleted: 'DOSSIER_DELETED',
} as const;
export const DossierNotificationsTypesValues = Object.values(DossierNotificationsTypes);
export const DocumentNotificationsTypes = {
assignReviewer: 'ASSIGN_REVIEWER',
assignApprover: 'ASSIGN_APPROVER',
unassignedFromFile: 'UNASSIGNED_FROM_FILE',
// documentUnderReview: 'DOCUMENT_UNDER_REVIEW',
// documentUnderApproval: 'DOCUMENT_UNDER_APPROVAL',
documentApproved: 'DOCUMENT_APPROVED',
} as const;
export const DocumentNotificationsTypesValues = Object.values(DocumentNotificationsTypes);
export const OtherNotificationsTypes = {
downloadReady: 'DOWNLOAD_READY',
} as const;
export const OtherNotificationsTypesValues = Object.values(OtherNotificationsTypes);
export const NotificationGroupsKeys = ['dossier', 'document', 'other'] as const;
export const NotificationGroupsValues = [
DossierNotificationsTypesValues,
DocumentNotificationsTypesValues,
OtherNotificationsTypesValues,
] as const;

View File

@ -1,4 +1,4 @@
<form (submit)="save()" [formGroup]="formGroup">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-content">
<div *ngFor="let category of notificationCategories">
<div class="iqser-input-group header w-full">
@ -7,25 +7,25 @@
}}</mat-slide-toggle>
</div>
<div class="options-content" *ngIf="isCategoryActive(category)">
<div class="radio-container" *ngIf="category === 'emailNotifications'">
<div class="radio-button" *ngFor="let type of emailNotificationScheduleTypes">
<iqser-round-checkbox [active]="getEmailNotificationType() === type" (click)="setEmailNotificationType(type)">
</iqser-round-checkbox>
<span> {{ translations[type.toLocaleLowerCase()] | translate }} </span>
</div>
</div>
<div *ngIf="isCategoryActive(category)" class="options-content">
<!-- <div class="radio-container" *ngIf="category === 'emailNotifications'">-->
<!-- <div class="radio-button" *ngFor="let type of emailNotificationScheduleTypes">-->
<!-- <iqser-round-checkbox [active]="getEmailNotificationType() === type" (click)="setEmailNotificationType(type)">-->
<!-- </iqser-round-checkbox>-->
<!-- <span> {{ translations[type.toLocaleLowerCase()] | translate }} </span>-->
<!-- </div>-->
<!-- </div>-->
<div class="statement" translate="notifications-screen.options-title"></div>
<div class="group" *ngFor="let key of notificationGroupsKeys; let i = index">
<div class="group-title" [translate]="translations[key]"></div>
<div *ngFor="let key of notificationGroupsKeys; let i = index" class="group">
<div [translate]="translations[key]" class="group-title"></div>
<div class="iqser-input-group">
<mat-checkbox
*ngFor="let preference of notificationGroupsValues[i]"
color="primary"
[checked]="isPreferenceChecked(category, preference)"
(change)="addRemovePreference($event.checked, category, preference)"
*ngFor="let preference of notificationGroupsValues[i]"
[checked]="isPreferenceChecked(category, preference)"
color="primary"
>
{{ translations[preference] | translate }}
</mat-checkbox>
@ -36,7 +36,7 @@
</div>
<div class="dialog-actions">
<button [disabled]="formGroup.invalid" color="primary" mat-flat-button type="submit">
<button [disabled]="form.invalid" color="primary" mat-flat-button type="submit">
{{ 'user-profile-screen.actions.save' | translate }}
</button>
</div>

View File

@ -2,10 +2,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { notificationsTranslations } from '../../../translations/notifications-translations';
import { NotificationPreferencesService } from '../../../services/notification-preferences.service';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { NotificationCategoriesValues, NotificationGroupsKeys, NotificationGroupsValues } from '../constants';
import { EmailNotificationScheduleTypesValues } from '@red/domain';
import {
EmailNotificationScheduleTypesValues,
NotificationCategoriesValues,
NotificationGroupsKeys,
NotificationGroupsValues,
} from '@red/domain';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-notifications-screen',
@ -13,26 +18,63 @@ import { EmailNotificationScheduleTypesValues } from '@red/domain';
styleUrls: ['./notifications-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotificationsScreenComponent implements OnInit {
export class NotificationsScreenComponent extends BaseFormComponent implements OnInit {
readonly emailNotificationScheduleTypes = EmailNotificationScheduleTypesValues;
readonly notificationCategories = NotificationCategoriesValues;
readonly notificationGroupsKeys = NotificationGroupsKeys;
readonly notificationGroupsValues = NotificationGroupsValues;
readonly translations = notificationsTranslations;
readonly formGroup: FormGroup = this._getForm();
constructor(
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _notificationPreferencesService: NotificationPreferencesService,
) {}
) {
super();
}
async ngOnInit(): Promise<void> {
await this._initializeForm();
}
isCategoryActive(category: string) {
return this.form.get(`${category}Enabled`).value;
}
setEmailNotificationType(type: string) {
this.form.get('emailNotificationType').setValue(type);
}
getEmailNotificationType() {
return this.form.get('emailNotificationType').value;
}
isPreferenceChecked(category: string, preference: string) {
return this.form.get(category).value.includes(preference);
}
addRemovePreference(checked: boolean, category: string, preference: string) {
const preferences = this.form.get(category).value;
if (checked) {
preferences.push(preference);
} else {
const indexOfPreference = preferences.indexOf(preference);
preferences.splice(indexOfPreference, 1);
}
this.form.get(category).setValue(preferences);
}
async save() {
this._loadingService.start();
try {
await firstValueFrom(this._notificationPreferencesService.update(this.form.value));
} catch (e) {
this._toaster.error(_('notifications-screen.error.generic'));
}
this._loadingService.stop();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
inAppNotificationsEnabled: [undefined],
@ -43,48 +85,13 @@ export class NotificationsScreenComponent implements OnInit {
});
}
isCategoryActive(category: string) {
return this.formGroup.get(`${category}Enabled`).value;
}
setEmailNotificationType(type: string) {
this.formGroup.get('emailNotificationType').setValue(type);
}
getEmailNotificationType() {
return this.formGroup.get('emailNotificationType').value;
}
isPreferenceChecked(category: string, preference: string) {
return this.formGroup.get(category).value.includes(preference);
}
addRemovePreference(checked: boolean, category: string, preference: string) {
const preferences = this.formGroup.get(category).value;
if (checked) {
preferences.push(preference);
} else {
const indexOfPreference = preferences.indexOf(preference);
preferences.splice(indexOfPreference, 1);
}
this.formGroup.get(category).setValue(preferences);
}
async save() {
this._loadingService.start();
try {
await this._notificationPreferencesService.update(this.formGroup.value).toPromise();
} catch (e) {
this._toaster.error(_('notifications-screen.error.generic'));
}
this._loadingService.stop();
}
private async _initializeForm() {
this._loadingService.start();
const notificationPreferences = await this._notificationPreferencesService.get().toPromise();
this.formGroup.patchValue(notificationPreferences);
this.form = this._getForm();
const notificationPreferences = await firstValueFrom(this._notificationPreferencesService.get());
this.form.patchValue(notificationPreferences);
this.initialFormValue = JSON.parse(JSON.stringify(this.form.getRawValue()));
this._loadingService.stop();
}

View File

@ -3,8 +3,9 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
const routes = [{ path: '', component: NotificationsScreenComponent }];
const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [NotificationsScreenComponent],

View File

@ -2,13 +2,14 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { LoadingService } from '@iqser/common-ui';
import { BaseFormComponent, 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 { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-user-profile-screen',
@ -16,8 +17,7 @@ import { LanguageService } from '../../../../../i18n/language.service';
styleUrls: ['./user-profile-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileScreenComponent implements OnInit {
readonly form: FormGroup = this._getForm();
export class UserProfileScreenComponent extends BaseFormComponent implements OnInit {
changePasswordUrl: SafeResourceUrl;
translations = languagesTranslations;
@ -30,25 +30,16 @@ export class UserProfileScreenComponent implements OnInit {
private readonly _configService: ConfigService,
private readonly _languageService: LanguageService,
private readonly _domSanitizer: DomSanitizer,
private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService,
protected readonly _translateService: TranslateService,
) {
super();
this._loadingService.start();
this.changePasswordUrl = this._domSanitizer.bypassSecurityTrustResourceUrl(
`${this._configService.values.OAUTH_URL}/account/password`,
);
}
private _getForm(): FormGroup {
return this._formBuilder.group({
email: [undefined, [Validators.required, Validators.email]],
firstName: [undefined],
lastName: [undefined],
language: [undefined],
});
}
get languageChanged(): boolean {
return this._profileModel['language'] !== this.form.get('language').value;
}
@ -82,24 +73,34 @@ export class UserProfileScreenComponent implements OnInit {
}
if (this.profileChanged) {
const value = this.form.value as IProfile;
const value = this.form.getRawValue() as IProfile;
delete value.language;
await this._userService
.updateMyProfile({
await firstValueFrom(
this._userService.updateMyProfile({
...value,
})
.toPromise();
}),
);
await this._userService.loadCurrentUser();
await this._userService.loadAll().toPromise();
await firstValueFrom(this._userService.loadAll());
}
this._initializeForm();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
email: [undefined, [Validators.required, Validators.email]],
firstName: [undefined],
lastName: [undefined],
language: [undefined],
});
}
private _initializeForm(): void {
try {
this.form = this._getForm();
this._profileModel = {
email: this._userService.currentUser.email,
firstName: this._userService.currentUser.firstName,
@ -111,6 +112,7 @@ export class UserProfileScreenComponent implements OnInit {
this.form.get('email').disable();
}
this.form.patchValue(this._profileModel, { emitEvent: false });
this.initialFormValue = this.form.getRawValue();
} catch (e) {
} finally {
this._loadingService.stop();

View File

@ -3,8 +3,9 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
const routes = [{ path: '', component: UserProfileScreenComponent }];
const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [UserProfileScreenComponent],

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' },
],
},
],
@ -164,6 +174,7 @@ const routes: Routes = [
path: 'general-config',
component: GeneralConfigScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
requiredRoles: ['RED_ADMIN'],

View File

@ -28,7 +28,7 @@ export class AdminSideNavComponent implements OnInit {
settings: [
{
screen: 'dossier-templates',
label: _('dossier-templates'),
label: _('dossier-templates.label'),
hideIf: !this.currentUser.isManager && !this.currentUser.isAdmin,
},
{
@ -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,13 +12,12 @@ 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';
import { AddEditDictionaryDialogComponent } from './dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
import { ConfirmDeleteFileAttributeDialogComponent } from './dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component';
import { ConfirmDeleteAttributeDialogComponent } from './dialogs/confirm-delete-attribute-dialog/confirm-delete-attribute-dialog.component';
import { EditColorDialogComponent } from './dialogs/edit-color-dialog/edit-color-dialog.component';
import { ComboChartComponent, ComboSeriesVerticalComponent } from './components/combo-chart';
import { NgxChartsModule } from '@swimlane/ngx-charts';
@ -49,12 +47,14 @@ import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dia
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 { FileAttributesConfigurationsDialogComponent } from './dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component';
import { SharedAdminModule } from './shared/shared-admin.module';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
const dialogs = [
AddEditDossierTemplateDialogComponent,
AddEditDictionaryDialogComponent,
AddEditFileAttributeDialogComponent,
ConfirmDeleteFileAttributeDialogComponent,
ConfirmDeleteAttributeDialogComponent,
EditColorDialogComponent,
SmtpAuthDialogComponent,
AddEditUserDialogComponent,
@ -66,7 +66,6 @@ const dialogs = [
];
const screens = [
DossierTemplatesListingScreenComponent,
RulesScreenComponent,
AuditScreenComponent,
DefaultColorsScreenComponent,
@ -84,8 +83,7 @@ const screens = [
];
const components = [
AdminBreadcrumbsComponent,
DossierTemplateActionsComponent,
DossierTemplateBreadcrumbsComponent,
ComboChartComponent,
ComboSeriesVerticalComponent,
UsersStatsComponent,
@ -94,14 +92,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

@ -12,7 +12,18 @@ import {
import { curveLinear } from 'd3-shape';
import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { BaseChartComponent, calculateViewDimensions, ColorHelper, LineSeriesComponent, ViewDimensions } from '@swimlane/ngx-charts';
import {
BaseChartComponent,
calculateViewDimensions,
Color,
ColorHelper,
LegendPosition,
LineSeriesComponent,
Orientation,
ScaleType,
ViewDimensions,
} from '@swimlane/ngx-charts';
import { ILineChartSeries } from './models';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@ -25,7 +36,7 @@ export class ComboChartComponent extends BaseChartComponent {
@Input() curve: any = curveLinear;
@Input() legend = false;
@Input() legendTitle = 'Legend';
@Input() legendPosition = 'right';
@Input() legendPosition: LegendPosition = LegendPosition.Right;
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@ -38,33 +49,33 @@ export class ComboChartComponent extends BaseChartComponent {
@Input() gradient: boolean;
@Input() showGridLines = true;
@Input() activeEntries: any[] = [];
@Input() schemeType: string;
@Input() schemeType: ScaleType;
@Input() xAxisTickFormatting: any;
@Input() yAxisTickFormatting: any;
@Input() yRightAxisTickFormatting: any;
@Input() roundDomains = false;
@Input() colorSchemeLine: any;
@Input() colorSchemeLine: Color;
@Input() autoScale;
@Input() lineChart: any;
@Input() lineChart: ILineChartSeries[];
@Input() yLeftAxisScaleFactor: any;
@Input() yRightAxisScaleFactor: any;
@Input() rangeFillOpacity: number;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate: EventEmitter<any> = new EventEmitter();
@Output() deactivate: EventEmitter<any> = new EventEmitter();
@Output() activate = new EventEmitter<{ value; entries: unknown[] }>();
@Output() deactivate = new EventEmitter<{ value; entries: unknown[] }>();
@ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef<any>;
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<any>;
@ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef<unknown>;
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<unknown>;
@ViewChild(LineSeriesComponent) lineSeriesComponent: LineSeriesComponent;
dims: ViewDimensions;
xScale: any;
yScale: any;
xDomain: any;
yDomain: any;
xDomain: string[] | number[];
yDomain: string[] | number[];
transform: string;
colors: ColorHelper;
colorsLine: ColorHelper;
@ -72,19 +83,19 @@ export class ComboChartComponent extends BaseChartComponent {
xAxisHeight = 0;
yAxisWidth = 0;
legendOptions: any;
scaleType = 'linear';
scaleType: ScaleType = ScaleType.Linear;
xScaleLine;
yScaleLine;
xDomainLine;
yDomainLine;
seriesDomain;
scaledAxis;
combinedSeries;
combinedSeries: ILineChartSeries[];
xSet;
filteredDomain;
hoveredVertical;
yOrientLeft = 'left';
yOrientRight = 'right';
yOrientLeft: Orientation = Orientation.Left;
yOrientRight: Orientation = Orientation.Right;
legendSpacing = 0;
bandwidth: number;
barPadding = 8;
@ -176,15 +187,11 @@ export class ComboChartComponent extends BaseChartComponent {
return this.combinedSeries.map(d => d.name);
}
isDate(value): boolean {
if (value instanceof Date) {
return true;
}
return false;
isDate(value): value is Date {
return value instanceof Date;
}
getScaleType(values): string {
getScaleType(values): ScaleType {
let date = true;
let num = true;
@ -199,16 +206,16 @@ export class ComboChartComponent extends BaseChartComponent {
}
if (date) {
return 'time';
return ScaleType.Time;
}
if (num) {
return 'linear';
return ScaleType.Linear;
}
return 'ordinal';
return ScaleType.Ordinal;
}
getXDomainLine(): any[] {
let values = [];
let values: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
@ -239,7 +246,7 @@ export class ComboChartComponent extends BaseChartComponent {
}
getYDomainLine(): any[] {
const domain = [];
const domain: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
@ -263,7 +270,7 @@ export class ComboChartComponent extends BaseChartComponent {
const max = Math.max(...domain);
if (this.yRightAxisScaleFactor) {
const minMax = this.yRightAxisScaleFactor(min, max);
return [Math.min(0, minMax.min), minMax.max];
return [Math.min(0, minMax.min as number), minMax.max];
} else {
min = Math.min(0, min);
return [min, max];
@ -317,12 +324,12 @@ export class ComboChartComponent extends BaseChartComponent {
}
getYDomain() {
const values = this.results.map(d => d.value);
const values: number[] = this.results.map(d => d.value);
const min = Math.min(0, ...values);
const max = Math.max(...values);
if (this.yLeftAxisScaleFactor) {
const minMax = this.yLeftAxisScaleFactor(min, max);
return [Math.min(0, minMax.min), minMax.max];
return [Math.min(0, minMax.min as number), minMax.max];
} else {
return [min, max];
}
@ -333,7 +340,7 @@ export class ComboChartComponent extends BaseChartComponent {
}
setColors(): void {
let domain;
let domain: number[] | string[];
if (this.schemeType === 'ordinal') {
domain = this.xDomain;
} else {

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations';
import { formatLabel } from '@swimlane/ngx-charts';
import { Bar, BarOrientation, formatLabel, PlacementTypes, StyleTypes } from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@ -17,7 +17,7 @@ import { formatLabel } from '@swimlane/ngx-charts';
[fill]="bar.color"
[stops]="bar.gradientStops"
[data]="bar.data"
[orientation]="'vertical'"
[orientation]="orientations.Vertical"
[roundEdges]="bar.roundEdges"
[gradient]="gradient"
[isActive]="isActive(bar.data)"
@ -27,8 +27,8 @@ import { formatLabel } from '@swimlane/ngx-charts';
(deactivate)="deactivate.emit($event)"
ngx-tooltip
[tooltipDisabled]="tooltipDisabled"
[tooltipPlacement]="0"
[tooltipType]="1"
[tooltipPlacement]="tooltipPlacements.Top"
[tooltipType]="tooltipTypes.tooltip"
[tooltipTitle]="bar.tooltipText"
></svg:g>
`,
@ -67,6 +67,9 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bars: any;
x: any;
y: any;
readonly tooltipTypes = StyleTypes;
readonly tooltipPlacements = PlacementTypes;
readonly orientations = BarOrientation;
ngOnChanges(): void {
this.update();
@ -91,7 +94,7 @@ export class ComboSeriesVerticalComponent implements OnChanges {
const formattedLabel = formatLabel(label);
const roundEdges = this.type === 'standard';
const bar: any = {
const bar: Bar = {
value,
label,
roundEdges,
@ -101,8 +104,15 @@ export class ComboSeriesVerticalComponent implements OnChanges {
height: 0,
x: 0,
y: 0,
ariaLabel: label,
tooltipText: label,
color: undefined,
gradientStops: undefined,
};
let offset0 = d0;
let offset1 = offset0 + value;
if (this.type === 'standard') {
bar.height = Math.abs(this.yScale(value) - this.yScale(0));
bar.x = this.xScale(label);
@ -113,18 +123,14 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.y = this.yScale(value);
}
} else if (this.type === 'stacked') {
const offset0 = d0;
const offset1 = offset0 + value;
d0 += value;
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
bar.offset0 = offset0;
bar.offset1 = offset1;
// bar.offset0 = offset0;
// bar.offset1 = offset1;
} else if (this.type === 'normalized') {
let offset0 = d0;
let offset1 = offset0 + value;
d0 += value;
if (total > 0) {
@ -138,8 +144,8 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
bar.offset0 = offset0;
bar.offset1 = offset1;
// bar.offset0 = offset0;
// bar.offset1 = offset1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = (offset1 - offset0).toFixed(2) + '%';
@ -152,8 +158,8 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.color = this.colors.getColor(value);
bar.gradientStops = this.colors.getLinearGradientStops(value);
} else {
bar.color = this.colors.getColor(bar.offset1);
bar.gradientStops = this.colors.getLinearGradientStops(bar.offset1, bar.offset0);
bar.color = this.colors.getColor(offset1);
bar.gradientStops = this.colors.getLinearGradientStops(offset1, offset0);
}
}

View File

@ -0,0 +1,11 @@
export interface ISeries {
name: number;
value: number;
min: number;
max: number;
}
export interface ILineChartSeries {
name: string;
series: ISeries[];
}

View File

@ -3,13 +3,14 @@
*ngIf="root || !!dossierTemplatesService.activeDossierTemplate"
[routerLink]="'/main/admin/dossier-templates'"
class="breadcrumb"
translate="dossier-templates"
translate="dossier-templates.label"
></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

@ -13,7 +13,3 @@
left: 270px;
}
}
.mt-44 {
margin-top: 44px;
}

View File

@ -95,11 +95,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" 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" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -10,10 +10,6 @@
}
}
.mb-14 {
margin-bottom: 14px;
}
.technical-name {
font-weight: 600;
}

View File

@ -1,12 +1,12 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Observable } from 'rxjs';
import { firstValueFrom, Observable } from 'rxjs';
import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { AppStateService } from '@state/app-state.service';
import { toKebabCase } from '@utils/functions';
import { toSnakeCase } from '@utils/functions';
import { DictionaryService } from '@shared/services/dictionary.service';
import { Dictionary, IDictionary } from '@red/domain';
import { UserService } from '@services/user.service';
@ -21,7 +21,6 @@ import { HttpStatusCode } from '@angular/common/http';
})
export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
readonly dictionary = this._data.dictionary;
readonly form: FormGroup = this._getForm(this.dictionary);
readonly canEditLabel$ = this._canEditLabel$;
readonly technicalName$: Observable<string>;
readonly dialogHeader = this._translateService.instant('add-edit-dictionary.title', {
@ -29,7 +28,6 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
name: this._data.dictionary?.label,
});
readonly hasColor$: Observable<boolean>;
readonly disabled = false;
private readonly _dossierTemplateId = this._data.dossierTemplateId;
constructor(
@ -39,37 +37,18 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
private readonly _appStateService: AppStateService,
private readonly _translateService: TranslateService,
private readonly _dictionaryService: DictionaryService,
private readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
@Inject(MAT_DIALOG_DATA)
private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string },
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm(this.dictionary);
this.initialFormValue = this.form.getRawValue();
this.hasColor$ = this._colorEmpty$;
this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value)));
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.dictionary) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (key === 'caseSensitive') {
if (this.getDictCaseSensitive(this.dictionary) !== this.form.get(key).value) {
return true;
}
} else if (this.dictionary[key] !== this.form.get(key).value) {
return true;
}
}
return false;
}
private get _canEditLabel$() {
return this.userService.currentUser$.pipe(
map(user => user.isAdmin || !this._data.dictionary),
@ -98,8 +77,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId });
}
return observable
.toPromise()
return firstValueFrom(observable)
.then(() => this._dialogRef.close(true))
.catch(error => {
if (error.status === HttpStatusCode.Conflict) {
@ -126,11 +104,11 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
private _toTechnicalName(value: string) {
const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]);
const baseTechnicalName = toKebabCase(value.trim());
const baseTechnicalName = toSnakeCase(value.trim());
let technicalName = baseTechnicalName;
let suffix = 1;
while (existingTechnicalNames.includes(technicalName)) {
technicalName = [baseTechnicalName, suffix++].join('-');
technicalName = [baseTechnicalName, suffix++].join('_');
}
return technicalName;
}

View File

@ -35,11 +35,11 @@
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="form.invalid || !changed" 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" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,8 +1,8 @@
import { Component, HostListener, Inject, OnDestroy } from '@angular/core';
import { Component, HostListener, Inject, Injector, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DossierAttributeConfigTypes, FileAttributeConfigTypes, IDossierAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AutoUnsubscribe, IqserEventTarget, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseDialogComponent, IqserEventTarget, LoadingService, Toaster } from '@iqser/common-ui';
import { HttpErrorResponse } from '@angular/common/http';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { dossierAttributeTypesTranslations } from '../../translations/dossier-attribute-types-translations';
@ -12,9 +12,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
templateUrl: './add-edit-dossier-attribute-dialog.component.html',
styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss'],
})
export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe implements OnDestroy {
export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent implements OnDestroy {
dossierAttribute: IDossierAttributeConfig = this.data.dossierAttribute;
readonly form: FormGroup = this._getForm(this.dossierAttribute);
readonly translations = dossierAttributeTypesTranslations;
readonly typeOptions = Object.keys(DossierAttributeConfigTypes);
@ -23,11 +22,14 @@ export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe impl
private readonly _loadingService: LoadingService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _toaster: Toaster,
readonly dialogRef: MatDialogRef<AddEditDossierAttributeDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: { readonly dossierAttribute: IDossierAttributeConfig },
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm(this.dossierAttribute);
this.initialFormValue = this.form.getRawValue();
}
get changed(): boolean {
@ -55,7 +57,7 @@ export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe impl
this._dossierAttributesService.createOrUpdate(attribute).subscribe(
() => {
this.dialogRef.close(true);
this._dialogRef.close(true);
},
(error: HttpErrorResponse) => {
this._loadingService.stop();

View File

@ -33,16 +33,11 @@
<div class="validity">
<div>
<mat-checkbox
(change)="hasValidFrom = !hasValidFrom"
[checked]="hasValidFrom"
class="filter-menu-checkbox"
color="primary"
>
<mat-checkbox (change)="toggleHasValid('from')" [checked]="hasValidFrom" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-from' | translate }}
</mat-checkbox>
<mat-checkbox (change)="hasValidTo = !hasValidTo" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
<mat-checkbox (change)="toggleHasValid('to')" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-to' | translate }}
</mat-checkbox>
</div>
@ -87,11 +82,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" 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" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,23 +1,23 @@
import { Component, Inject } from '@angular/core';
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';
import { firstValueFrom } from 'rxjs';
@Component({
templateUrl: './add-edit-dossier-template-dialog.component.html',
styleUrls: ['./add-edit-dossier-template-dialog.component.scss'],
})
export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
readonly form: FormGroup = this._getForm();
hasValidFrom: boolean;
hasValidTo: boolean;
downloadTypesEnum: DownloadFileType[] = ['ORIGINAL', 'PREVIEW', 'REDACTED'];
@ -25,79 +25,64 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
key: type,
label: downloadTypesTranslations[type],
}));
readonly disabled = false;
private _previousValidFrom: Moment;
private _previousValidTo: Moment;
private _lastValidFrom: Moment;
private _lastValidTo: Moment;
constructor(
private readonly _appStateService: AppStateService,
private readonly _toaster: Toaster,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
private readonly _loadingService: LoadingService,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplate: IDossierTemplate,
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
this.hasValidFrom = !!this.dossierTemplate?.validFrom;
this.hasValidTo = !!this.dossierTemplate?.validTo;
this._previousValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this.form.get('validTo').value;
this._previousValidFrom = this._lastValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this._lastValidTo = this.form.get('validTo').value;
this.form.valueChanges.subscribe(value => {
this.addSubscription = this.form.valueChanges.subscribe(value => {
this._applyValidityIntervalConstraints(value);
});
this.addSubscription = this.form.controls['validFrom'].valueChanges.subscribe(value => {
this._lastValidFrom = value ? value : this._lastValidFrom;
});
this.addSubscription = this.form.controls['validTo'].valueChanges.subscribe(value => {
this._lastValidFrom = value ? value : this._lastValidFrom;
});
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.dossierTemplate) {
return true;
toggleHasValid(extremity: string) {
if (extremity === 'from') {
this.hasValidFrom = !this.hasValidFrom;
this.form.controls['validFrom'].setValue(this.hasValidFrom ? this._lastValidFrom : null);
} else {
this.hasValidTo = !this.hasValidTo;
this.form.controls['validTo'].setValue(this.hasValidTo ? this._lastValidTo : null);
}
for (const key of Object.keys(this.form.getRawValue())) {
const formValue = this.form.get(key).value;
const objectValue = this.dossierTemplate[key];
if (key === 'validFrom') {
if (this.hasValidFrom !== !!objectValue || (this.hasValidFrom && !moment(objectValue).isSame(moment(formValue)))) {
return true;
}
} else if (key === 'validTo') {
if (this.hasValidTo !== !!objectValue || (this.hasValidTo && !moment(objectValue).isSame(moment(formValue)))) {
return true;
}
} else if (formValue instanceof Array) {
if (objectValue.length !== formValue.length) {
return true;
}
for (const item of objectValue) {
if (!formValue.includes(item)) {
return true;
}
}
} else if (objectValue !== formValue) {
return true;
}
}
return false;
}
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);
this._dialogRef.close(true);
} catch (error: any) {
const message =
error.status === HttpStatusCode.Conflict
@ -105,6 +90,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
: _('add-edit-dossier-template.error.generic');
this._toaster.error(message, { error });
}
this._loadingService.stop();
}
private _getForm(): FormGroup {
@ -134,7 +120,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]="!valid || !changed" 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" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { fileAttributeTypesTranslations } from '../../translations/file-attribute-types-translations';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { BaseDialogComponent } from '@iqser/common-ui';
import { BaseDialogComponent } from '../../../../../../../../libs/common-ui/src';
@Component({
selector: 'redaction-add-edit-file-attribute-dialog',
@ -13,12 +13,10 @@ import { BaseDialogComponent } from '@iqser/common-ui';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
readonly disabled = false;
DISPLAYED_FILTERABLE_LIMIT = 3;
translations = fileAttributeTypesTranslations;
fileAttribute: IFileAttributeConfig = this.data.fileAttribute;
dossierTemplateId: string = this.data.dossierTemplateId;
readonly form!: FormGroup;
readonly typeOptions = Object.keys(FileAttributeConfigTypes);
readonly canSetDisplayed!: boolean;
readonly canSetFilterable!: boolean;
@ -26,7 +24,8 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesService,
public dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: {
fileAttribute: IFileAttributeConfig;
@ -35,32 +34,11 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
numberOfFilterableAttrs: number;
},
) {
super();
super(_injector, _dialogRef);
this.canSetDisplayed = data.numberOfDisplayedAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.displayedInFileList;
this.canSetFilterable = data.numberOfFilterableAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.filterable;
this.form = this._getForm(this.fileAttribute);
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.fileAttribute) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (key === 'readonly') {
if (this.fileAttribute.editable === this.form.get(key).value) {
return true;
}
} else if (this.fileAttribute[key] !== this.form.get(key).value) {
return true;
}
}
return false;
this.initialFormValue = this.form.getRawValue();
}
save() {
@ -69,7 +47,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
editable: !this.form.get('readonly').value,
...this.form.getRawValue(),
};
this.dialogRef.close(fileAttribute);
this._dialogRef.close(fileAttribute);
}
private _getForm(fileAttribute: IFileAttributeConfig): FormGroup {

View File

@ -1,17 +1,18 @@
<section class="dialog">
<redaction-user-details
(closeDialog)="dialogRef.close($event)"
(closeDialog)="closeDialog($event)"
(cancel)="close()"
(toggleResetPassword)="toggleResetPassword()"
*ngIf="!resettingPassword"
[hidden]="resettingPassword"
[user]="user"
></redaction-user-details>
<redaction-reset-password
(close)="dialogRef.close($event)"
(close)="closeDialog($event)"
(toggleResetPassword)="toggleResetPassword()"
*ngIf="resettingPassword"
[hidden]="!resettingPassword"
[user]="user"
></redaction-reset-password>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,18 +1,44 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { User } from '@red/domain';
import { UserDetailsComponent } from './user-details/user-details.component';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
selector: 'redaction-add-edit-user-dialog',
templateUrl: './add-edit-user-dialog.component.html',
styleUrls: ['./add-edit-user-dialog.component.scss'],
})
export class AddEditUserDialogComponent {
export class AddEditUserDialogComponent extends BaseDialogComponent {
@ViewChild(UserDetailsComponent) private readonly _userDetailsComponent: UserDetailsComponent;
resettingPassword = false;
constructor(readonly dialogRef: MatDialogRef<AddEditUserDialogComponent>, @Inject(MAT_DIALOG_DATA) readonly user: User) {}
constructor(
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditUserDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly user: User,
) {
super(_injector, _dialogRef);
}
toggleResetPassword() {
this.resettingPassword = !this.resettingPassword;
}
async save(): Promise<void> {
await this._userDetailsComponent.save();
}
get changed(): boolean {
return this._userDetailsComponent.changed;
}
get valid(): boolean {
return this._userDetailsComponent.valid;
}
closeDialog(event) {
this._dialogRef.close(event);
}
}

View File

@ -4,6 +4,7 @@ import { UserService } from '@services/user.service';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { User } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-reset-password',
@ -25,15 +26,15 @@ export class ResetPasswordComponent {
async save() {
this._loadingService.start();
try {
await this._userService
.resetPassword(
await firstValueFrom(
this._userService.resetPassword(
{
password: this.form.get('temporaryPassword').value,
temporary: true,
},
this.user.id,
)
.toPromise();
),
);
this.toggleResetPassword.emit();
} catch (error) {
this._toaster.error(_('reset-password-dialog.error.password-policy'));

View File

@ -53,6 +53,6 @@
icon="iqser:trash"
></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="add-edit-user.actions.cancel"></div>
<div class="all-caps-label cancel" translate="add-edit-user.actions.cancel" (click)="cancel.emit()"></div>
</div>
</form>

View File

@ -1,28 +1,29 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { AutoUnsubscribe, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { rolesTranslations } from '../../../../../translations/roles-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { User } from '@red/domain';
import { UserService } from '@services/user.service';
import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.scss'],
})
export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges, OnDestroy {
export class UserDetailsComponent extends BaseFormComponent implements OnChanges, OnDestroy {
readonly iconButtonTypes = IconButtonTypes;
@Input() user: User;
@Output() readonly toggleResetPassword = new EventEmitter();
@Output() readonly closeDialog = new EventEmitter();
@Output() readonly cancel = new EventEmitter();
readonly ROLES = ['RED_USER', 'RED_MANAGER', 'RED_USER_ADMIN', 'RED_ADMIN'];
readonly translations = rolesTranslations;
form: FormGroup;
private readonly _ROLE_REQUIREMENTS = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
constructor(
@ -35,29 +36,6 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
super();
}
get changed(): boolean {
if (!this.user) {
return true;
}
if (this.user.roles.length !== this.activeRoles.length) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
const keyValue = this.form.get(key).value;
if (key.startsWith('RED_')) {
if (this.user.roles.includes(key) !== keyValue) {
return true;
}
} else if (this.user[key] !== keyValue) {
return true;
}
}
return false;
}
get activeRoles(): string[] {
return this.ROLES.reduce((acc, role) => {
if (this.form.get(role).value) {
@ -85,6 +63,7 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
ngOnChanges() {
this.form = this._getForm();
this._setRolesRequirements();
this.initialFormValue = this.form.getRawValue();
}
shouldBeDisabled(role: string): boolean {
@ -107,9 +86,7 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
const userData = { ...this.form.getRawValue(), roles: this.activeRoles };
if (!this.user) {
await this.userService
.create(userData)
.toPromise()
await firstValueFrom(this.userService.create(userData))
.then(() => {
this.closeDialog.emit(true);
})
@ -122,7 +99,7 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
this._loadingService.stop();
});
} else {
await this.userService.updateProfile(userData, this.user.id).toPromise();
await firstValueFrom(this.userService.updateProfile(userData, this.user.id));
this.closeDialog.emit(true);
}
}
@ -140,7 +117,7 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
email: [
{
value: this.user?.email,
disabled: !!this.user,
disabled: !!this.user?.email,
},
[Validators.required, Validators.email],
],

View File

@ -1,13 +1,6 @@
<section class="dialog">
<div class="dialog-header heading-l">
{{
'confirm-delete-file-attribute.title'
| translate
: {
type: type,
name: fileAttribute?.label
}
}}
{{ 'confirm-delete-file-attribute.title' | translate: translateArgs }}
</div>
<div *ngIf="showToast" class="inline-dialog-toast toast-error">
@ -20,14 +13,11 @@
<div class="dialog-content">
<div class="heading" translate="confirm-delete-file-attribute.warning"></div>
<mat-checkbox
*ngFor="let checkbox of checkboxes; let idx = index"
[(ngModel)]="checkbox.value"
[class.error]="!checkbox.value && showToast"
color="primary"
>
{{ checkbox.label | translate: { type: type } }}
</mat-checkbox>
<ng-container *ngFor="let checkbox of checkboxes; let idx = index">
<mat-checkbox [(ngModel)]="checkbox.value" [class.error]="!checkbox.value && showToast" color="primary">
{{ checkbox.label }}
</mat-checkbox>
</ng-container>
</div>
<div class="dialog-actions">
@ -35,7 +25,7 @@
{{ 'confirm-delete-file-attribute.delete' | translate: { type: type } }}
</button>
<div
(click)="cancel()"
(click)="this.dialogRef.close()"
[translateParams]="{ type: type }"
[translate]="'confirm-delete-file-attribute.cancel'"
class="all-caps-label cancel"

View File

@ -0,0 +1,87 @@
import { Component, Inject } from '@angular/core';
import { DossierAttributeConfig, FileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
const isFileAttributeConfig = (value: DossierAttributeConfig | FileAttributeConfig): value is FileAttributeConfig =>
value instanceof FileAttributeConfig;
interface CheckBox {
value: boolean;
label: string;
}
interface DialogData {
attribute: FileAttributeConfig | DossierAttributeConfig;
count: number;
}
@Component({
selector: 'redaction-confirm-delete-attribute-dialog',
templateUrl: './confirm-delete-attribute-dialog.component.html',
styleUrls: ['./confirm-delete-attribute-dialog.component.scss'],
})
export class ConfirmDeleteAttributeDialogComponent {
checkboxes: CheckBox[];
showToast = false;
constructor(
private readonly _translateService: TranslateService,
readonly dialogRef: MatDialogRef<ConfirmDeleteAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
) {
this.checkboxes = this.checkBoxConfig;
}
get checkBoxConfig(): CheckBox[] {
const checkBoxes = isFileAttributeConfig(this.data.attribute) ? this._fileAttributeCheckboxes : this._dossierAttributeCheckboxes;
if (this.data.count !== 0) {
checkBoxes.push({
value: false,
label: this._translateService.instant('confirm-delete-file-attribute.impacted-report', { count: this.data.count }),
});
}
return checkBoxes;
}
get valid() {
return this.checkboxes.reduce((acc, currentValue) => acc && currentValue.value, true);
}
get type(): 'bulk' | 'single' {
return this.data.attribute ? 'single' : 'bulk';
}
get translateArgs() {
return {
type: this.type,
name: this.data.attribute?.label,
};
}
private get _fileAttributeCheckboxes(): CheckBox[] {
return [
{
value: false,
label: this._translateService.instant('confirm-delete-file-attribute.file-impacted-documents', { type: this.type }),
},
{ value: false, label: this._translateService.instant('confirm-delete-file-attribute.file-lost-details') },
];
}
private get _dossierAttributeCheckboxes(): CheckBox[] {
return [
{ value: false, label: this._translateService.instant('confirm-delete-file-attribute.dossier-impacted-documents') },
{ value: false, label: this._translateService.instant('confirm-delete-file-attribute.dossier-lost-details') },
];
}
deleteFileAttribute() {
if (this.valid) {
this.dialogRef.close(true);
} else {
this.showToast = true;
}
}
}

View File

@ -1,45 +0,0 @@
import { Component, Inject } from '@angular/core';
import { IFileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'redaction-confirm-delete-file-attribute-dialog',
templateUrl: './confirm-delete-file-attribute-dialog.component.html',
styleUrls: ['./confirm-delete-file-attribute-dialog.component.scss'],
})
export class ConfirmDeleteFileAttributeDialogComponent {
fileAttribute: IFileAttributeConfig;
checkboxes = [
{ value: false, label: _('confirm-delete-file-attribute.impacted-documents') },
{ value: false, label: _('confirm-delete-file-attribute.lost-details') },
];
showToast = false;
constructor(
public dialogRef: MatDialogRef<ConfirmDeleteFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: IFileAttributeConfig,
) {
this.fileAttribute = data;
}
get valid() {
return this.checkboxes[0].value && this.checkboxes[1].value;
}
get type(): 'bulk' | 'single' {
return this.fileAttribute ? 'single' : 'bulk';
}
deleteFileAttribute() {
if (this.valid) {
this.dialogRef.close(true);
} else {
this.showToast = true;
}
}
cancel() {
this.dialogRef.close();
}
}

View File

@ -4,6 +4,7 @@ import { List, LoadingService } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { UserService } from '@services/user.service';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-confirm-delete-users-dialog',
@ -40,7 +41,7 @@ export class ConfirmDeleteUsersDialogComponent {
async deleteUser() {
if (this.valid) {
this._loadingService.start();
await this._userService.delete(this.userIds).toPromise();
await firstValueFrom(this._userService.delete(this.userIds));
this.dialogRef.close(true);
} else {
this.showToast = true;

View File

@ -28,11 +28,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" 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" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DefaultColorType, IColors } from '@red/domain';
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
@ -7,6 +7,7 @@ import { TranslateService } from '@ngx-translate/core';
import { defaultColorsTranslations } from '../../translations/default-colors-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@shared/services/dictionary.service';
import { firstValueFrom } from 'rxjs';
interface IEditColorData {
colors: IColors;
@ -19,10 +20,7 @@ interface IEditColorData {
styleUrls: ['./edit-color-dialog.component.scss'],
})
export class EditColorDialogComponent extends BaseDialogComponent {
readonly form: FormGroup;
translations = defaultColorsTranslations;
readonly disabled = false;
private readonly _initialColor: string;
private readonly _dossierTemplateId: string;
constructor(
@ -30,23 +28,16 @@ export class EditColorDialogComponent extends BaseDialogComponent {
private readonly _dictionaryService: DictionaryService,
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
private readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: IEditColorData,
) {
super();
super(_injector, _dialogRef);
this._dossierTemplateId = data.dossierTemplateId;
this._initialColor = data.colors[data.colorKey];
this.form = this._getForm();
}
get changed(): boolean {
return this.form.get('color').value !== this._initialColor;
}
get valid(): boolean {
return this.form.valid;
this.initialFormValue = this.form.getRawValue();
}
async save() {
@ -56,7 +47,7 @@ export class EditColorDialogComponent extends BaseDialogComponent {
};
try {
await this._dictionaryService.setColors(colors, this._dossierTemplateId).toPromise();
await firstValueFrom(this._dictionaryService.setColors(colors, this._dossierTemplateId));
this._dialogRef.close(true);
const color = this._translateService.instant(defaultColorsTranslations[this.data.colorKey]);
this._toaster.info(_('edit-color-dialog.success'), { params: { color: color } });

View File

@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/c
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse';
import { Observable } from 'rxjs';
import { firstValueFrom, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { DefaultListingServices, ListingComponent, TableColumnConfig, Toaster, trackBy } from '@iqser/common-ui';
import { DefaultListingServices, ListingComponent, TableColumnConfig, Toaster, trackByFactory } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileAttributeConfig, FileAttributeConfigTypes, IField, IFileAttributesConfig } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@ -34,7 +34,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<IFi
columnSample = [];
initialParseConfig: { delimiter?: string; encoding?: string } = {};
readonly tableHeaderLabel = '';
readonly trackBy = trackBy();
readonly trackBy = trackByFactory();
constructor(
private readonly _toaster: Toaster,
@ -56,14 +56,6 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<IFi
);
}
private _getForm(): FormGroup {
return this._formBuilder.group({
filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]],
delimiter: [undefined, Validators.required],
encoding: ['UTF-8', Validators.required],
});
}
readFile() {
const reader = new FileReader();
reader.addEventListener('load', event => {
@ -183,7 +175,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<IFi
};
try {
await this._fileAttributesService.setFileAttributeConfig(fileAttributes, this.data.dossierTemplateId).toPromise();
await firstValueFrom(this._fileAttributesService.setFileAttributeConfig(fileAttributes, this.data.dossierTemplateId));
this._toaster.success(_('file-attributes-csv-import.save.success'), { params: { count: this.activeFields.length } });
} catch (e) {
this._toaster.error(_('file-attributes-csv-import.save.error'));
@ -207,6 +199,14 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<IFi
}, 0);
}
private _getForm(): FormGroup {
return this._formBuilder.group({
filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]],
delimiter: [undefined, Validators.required],
encoding: ['UTF-8', Validators.required],
});
}
private _autocompleteStringValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
if ((this.parseResult?.meta?.fields || []).indexOf(control.value) !== -1) {

View File

@ -27,5 +27,5 @@
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,23 +1,27 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '@services/user.service';
import { ISmtpConfiguration } from '@red/domain';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
selector: 'redaction-smtp-auth-dialog',
templateUrl: './smtp-auth-dialog.component.html',
styleUrls: ['./smtp-auth-dialog.component.scss'],
})
export class SmtpAuthDialogComponent {
readonly form: FormGroup = this._getForm();
export class SmtpAuthDialogComponent extends BaseDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _userService: UserService,
public dialogRef: MatDialogRef<SmtpAuthDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<SmtpAuthDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ISmtpConfiguration,
) {}
) {
super(_injector, _dialogRef);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
@ -27,6 +31,6 @@ export class SmtpAuthDialogComponent {
}
save() {
this.dialogRef.close(this.form.getRawValue());
this._dialogRef.close(this.form.getRawValue());
}
}

View File

@ -60,7 +60,6 @@
*ngIf="form.get('userId').value !== ALL_USERS"
[user]="form.get('userId').value"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="form.get('userId').value === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-select-trigger>
@ -69,7 +68,6 @@
*ngIf="userId !== ALL_USERS"
[user]="userId"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="userId === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-option>
@ -109,7 +107,7 @@
</div>
<div class="user-column cell">
<redaction-initials-avatar [user]="log.userId" [withName]="true" size="small"></redaction-initials-avatar>
<redaction-initials-avatar [user]="log.userId" [withName]="true"></redaction-initials-avatar>
</div>
<div [translate]="translations[log.category]" class="cell"></div>

View File

@ -17,11 +17,3 @@ form {
font-size: 16px;
opacity: 0.7;
}
.mr-0 {
margin-right: 0;
}
.mr-20 {
margin-right: 20px;
}

View File

@ -8,6 +8,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { Audit, IAudit, IAuditResponse, IAuditSearchRequest } from '@red/domain';
import { AuditService } from '../../services/audit.service';
import { firstValueFrom } from 'rxjs';
const PAGE_SIZE = 50;
@ -52,15 +53,6 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnD
});
}
private _getForm(): FormGroup {
return this._formBuilder.group({
category: [this.ALL_CATEGORIES],
userId: [this.ALL_USERS],
from: [],
to: [],
});
}
get totalPages(): number {
if (!this.logs) {
return 0;
@ -76,6 +68,15 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnD
await this._fetchData();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
category: [this.ALL_CATEGORIES],
userId: [this.ALL_USERS],
from: [],
to: [],
});
}
private _updateDateFilters(value): boolean {
if (applyIntervalConstraints(value, this._previousFrom, this._previousTo, this.form, 'from', 'to')) {
return true;
@ -105,8 +106,8 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnD
to,
};
promises.push(this._auditService.getCategories().toPromise());
promises.push(this._auditService.searchAuditLog(logsRequestBody).toPromise());
promises.push(firstValueFrom(this._auditService.getCategories()));
promises.push(firstValueFrom(this._auditService.searchAuditLog(logsRequestBody)));
const data = await Promise.all(promises);
this.categories = data[0].map(c => c.category);

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

@ -15,6 +15,7 @@ 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 { firstValueFrom } from 'rxjs';
interface ListItem extends IListable {
readonly key: string;
@ -72,7 +73,7 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> imp
private async _loadColors() {
this._loadingService.start();
const data = await this._appStateService.loadColors(this._dossierTemplatesService.activeDossierTemplateId).toPromise();
const data = await firstValueFrom(this._appStateService.loadColors(this._dossierTemplatesService.activeDossierTemplateId));
this._colorsObj = data;
const entities = Object.keys(data)
.map(key => ({

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 { firstValueFrom, 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,27 +49,30 @@ 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) {
this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
await this._dictionaryService
.deleteDictionaries(
await firstValueFrom(
this._dictionaryService.deleteDictionaries(
types.map(t => t.type),
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
@ -55,8 +55,9 @@
<redaction-dictionary-manager
#dictionaryManager
(saveDictionary)="saveEntries($event)"
(saveDictionary)="save()"
[canEdit]="currentUser.isAdmin"
[isLeavingPage]="isLeavingPage"
[filterByDossierTemplate]="true"
[initialEntries]="initialEntries"
></redaction-dictionary-manager>

View File

@ -3,7 +3,6 @@ import { AppStateService } from '@state/app-state.service';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionaryService } from '@shared/services/dictionary.service';
@ -11,17 +10,19 @@ import { CircleButtonTypes, LoadingService } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { Dictionary } from '@red/domain';
import { firstValueFrom } from 'rxjs';
@Component({
templateUrl: './dictionary-overview-screen.component.html',
styleUrls: ['./dictionary-overview-screen.component.scss'],
})
export class DictionaryOverviewScreenComponent extends ComponentHasChanges implements OnInit, OnDestroy {
export class DictionaryOverviewScreenComponent implements OnInit, OnDestroy {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
initialEntries: string[] = [];
dictionary: Dictionary;
isLeavingPage = false;
@ViewChild('dictionaryManager', { static: false })
private readonly _dictionaryManager: DictionaryManagerComponent;
@ -37,11 +38,9 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
private readonly _dialogService: AdminDialogService,
protected readonly _translateService: TranslateService,
private readonly _dictionaryService: DictionaryService,
) {
super(_translateService);
}
) {}
get hasChanges() {
get changed() {
return this._dictionaryManager.editor.hasChanges;
}
@ -72,7 +71,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
$event?.stopPropagation();
this._dialogService.openDialog('confirm', $event, null, async () => {
await this._dictionaryService.deleteDictionaries([this.dictionary.type], this.dictionary.dossierTemplateId).toPromise();
await firstValueFrom(this._dictionaryService.deleteDictionaries([this.dictionary.type], this.dictionary.dossierTemplateId));
await this._appStateService.loadDictionaryData();
await this._router.navigate([
'/main',
@ -116,7 +115,9 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
}
}
saveEntries(entries: string[]) {
save() {
const entries = this._dictionaryManager.editor?.currentEntries;
this._loadingService.start();
this._dictionaryService
.saveEntries(entries, this.initialEntries, this.dictionary.dossierTemplateId, this.dictionary.type, null)
@ -149,9 +150,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
private async _loadEntries() {
this._loadingService.start();
await this._dictionaryService
.getForType(this.dictionary.dossierTemplateId, this.dictionary.type)
.toPromise()
await firstValueFrom(this._dictionaryService.getForType(this.dictionary.dossierTemplateId, this.dictionary.type))
.then(
data => {
this._loadingService.stop();

View File

@ -6,7 +6,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
import { DigitalSignatureService } from '../../services/digital-signature.service';
import { IDigitalSignature } from '@red/domain';
import { IDigitalSignature, IDigitalSignatureRequest } from '@red/domain';
import { HttpStatusCode } from '@angular/common/http';
@Component({
@ -40,29 +40,30 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements
}
saveDigitalSignature() {
const digitalSignature = {
...this.form.getRawValue(),
const formValue = this.form.getRawValue();
const digitalSignature: IDigitalSignature = {
...formValue,
};
//adjusted for chrome auto-complete / password manager
digitalSignature.password = digitalSignature.keySecret;
digitalSignature.password = formValue.keySecret;
const observable = this.digitalSignatureExists
? this._digitalSignatureService.update(digitalSignature)
: this._digitalSignatureService.save(digitalSignature);
this.addSubscription = observable.subscribe(
() => {
this.addSubscription = observable.subscribe({
next: () => {
this.loadDigitalSignatureAndInitializeForm();
this._toaster.success(_('digital-signature-screen.action.save-success'));
},
error => {
error: error => {
if (error.status === HttpStatusCode.BadRequest) {
this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error'));
} else {
this._toaster.error(_('digital-signature-screen.action.save-error'));
}
},
);
});
}
removeDigitalSignature() {
@ -85,23 +86,23 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements
this.form.get('certificateName').setValue(file.name);
input.value = null;
};
fileReader.readAsDataURL(file);
fileReader.readAsDataURL(file as Blob);
}
loadDigitalSignatureAndInitializeForm() {
this._loadingService.start();
this.addSubscription = this._digitalSignatureService
this._digitalSignatureService
.getSignature()
.subscribe(
digitalSignature => {
.subscribe({
next: digitalSignature => {
this.digitalSignatureExists = true;
this.digitalSignature = digitalSignature;
},
() => {
error: () => {
this.digitalSignatureExists = false;
this.digitalSignature = {};
},
)
})
.add(() => {
this.form = this._getForm();
this._loadingService.stop();

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

@ -15,6 +15,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { DossierAttributeConfig, IDossierAttributeConfig } from '@red/domain';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { firstValueFrom } from 'rxjs';
import { ReportTemplateService } from '../../../../services/report-template.service';
@Component({
templateUrl: './dossier-attributes-listing-screen.component.html',
@ -44,6 +46,7 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
private readonly _loadingService: LoadingService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _userService: UserService,
private readonly _reportTemplateService: ReportTemplateService,
) {
super(_injector);
}
@ -52,11 +55,16 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
await this._loadData();
}
openConfirmDeleteAttributeDialog($event: MouseEvent, dossierAttribute?: IDossierAttributeConfig) {
this._dialogService.openDialog('confirm', $event, null, async () => {
async openConfirmDeleteAttributeDialog($event: MouseEvent, dossierAttribute?: DossierAttributeConfig) {
const dossierTemplateId = this._dossierTemplatesService.activeDossierTemplateId;
const resp = await firstValueFrom(
this._reportTemplateService.getTemplatesByPlaceholder(dossierTemplateId, dossierAttribute.placeholder),
);
this._dialogService.openDialog('deleteAttribute', $event, { attribute: dossierAttribute, count: resp.length }, async () => {
this._loadingService.start();
const ids = dossierAttribute ? [dossierAttribute.id] : this.listingService.selected.map(item => item.id);
await this._dossierAttributesService.delete(ids).toPromise();
await firstValueFrom(this._dossierAttributesService.delete(ids));
await this._loadData();
});
}
@ -71,7 +79,7 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
private async _loadData() {
this._loadingService.start();
await this._dossierAttributesService.loadAll().toPromise();
await firstValueFrom(this._dossierAttributesService.loadAll());
this._loadingService.stop();
}
}

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,
@ -18,6 +18,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Component({
templateUrl: './dossier-templates-listing-screen.component.html',
@ -29,7 +30,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;
@ -39,6 +40,7 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
{ label: _('dossier-templates-listing.table-col-names.created-by'), class: 'user-column' },
{ label: _('dossier-templates-listing.table-col-names.created-on'), sortByKey: 'dateAdded' },
{ label: _('dossier-templates-listing.table-col-names.modified-on'), sortByKey: 'dateModified' },
{ label: _('dossier-templates-listing.table-col-names.status'), sortByKey: 'dossierTemplateStatus' },
];
constructor(
@ -55,10 +57,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,38 +64,17 @@ 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)) {
await this._dossierTemplatesService
.delete(templateIds)
.toPromise()
.catch(error => {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('dossier-templates-listing.error.conflict'));
} else {
this._toaster.error(_('dossier-templates-listing.error.generic'));
}
});
await this._dossierTemplatesService.loadAll().toPromise();
await firstValueFrom(this._dossierTemplatesService.delete(templateIds)).catch(error => {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('dossier-templates-listing.error.conflict'));
} else {
this._toaster.error(_('dossier-templates-listing.error.generic'));
}
});
await this._appStateService.loadDictionaryData();
this.loadDossierTemplateStats();
}
}

View File

@ -0,0 +1,15 @@
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],
})
export class DossierTemplatesListingModule {}

View File

@ -0,0 +1,41 @@
<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>
</div>
<div class="cell">
<div class="small-label">
{{ translations[dossierTemplate.dossierTemplateStatus] | translate }}
</div>
<redaction-dossier-template-actions
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"
></redaction-dossier-template-actions>
</div>

View File

@ -0,0 +1,30 @@
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';
import { dossierTemplateStatusTranslations } from '../../../translations/dossier-template-status-translations';
@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 translations = dossierTemplateStatusTranslations;
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

@ -17,6 +17,8 @@ import { FileAttributeConfig, IFileAttributeConfig, IFileAttributesConfig } from
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { ReportTemplateService } from '../../../../services/report-template.service';
@Component({
templateUrl: './file-attributes-listing-screen.component.html',
@ -59,6 +61,7 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialogService: AdminDialogService,
private readonly _fileAttributesService: FileAttributesService,
private readonly _reportTemplateService: ReportTemplateService,
) {
super(_injector);
}
@ -87,23 +90,23 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
});
}
openConfigurationsDialog($event: MouseEvent) {
this._dialogService.openDialog('fileAttributesConfigurations', $event, this._existingConfiguration);
}
async openConfirmDeleteAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
const dossierTemplateId = this._dossierTemplatesService.activeDossierTemplateId;
const resp = await firstValueFrom(
this._reportTemplateService.getTemplatesByPlaceholder(dossierTemplateId, fileAttribute.placeholder),
);
openConfirmDeleteAttributeDialog($event: MouseEvent, fileAttribute?: IFileAttributeConfig) {
this._dialogService.openDialog('deleteFileAttribute', $event, fileAttribute, async () => {
this._dialogService.openDialog('deleteAttribute', $event, { attribute: fileAttribute, count: resp.length }, async () => {
this._loadingService.start();
const dossierTemplateId = this._dossierTemplatesService.activeDossierTemplateId;
if (fileAttribute) {
await this._fileAttributesService.deleteFileAttributes([fileAttribute.id], dossierTemplateId).toPromise();
await firstValueFrom(this._fileAttributesService.deleteFileAttributes([fileAttribute.id], dossierTemplateId));
} else {
await this._fileAttributesService
.deleteFileAttributes(
await firstValueFrom(
this._fileAttributesService.deleteFileAttributes(
this.listingService.selected.map(f => f.id),
dossierTemplateId,
)
.toPromise();
),
);
}
await this._appStateService.refreshDossierTemplate(dossierTemplateId);
await this._loadData();
@ -126,18 +129,21 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
);
}
openConfigurationsDialog($event: MouseEvent) {
this._dialogService.openDialog('fileAttributesConfigurations', $event, this._existingConfiguration);
}
private async _createNewFileAttributeAndRefreshView(newValue: IFileAttributeConfig): Promise<void> {
await this._fileAttributesService
.setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId)
.toPromise()
.catch(error => {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('file-attributes-listing.error.conflict'));
} else {
this._toaster.error(_('file-attributes-listing.error.generic'));
}
this._loadingService.stop();
});
await firstValueFrom(
this._fileAttributesService.setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId),
).catch(error => {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('file-attributes-listing.error.conflict'));
} else {
this._toaster.error(_('file-attributes-listing.error.generic'));
}
this._loadingService.stop();
});
await this._appStateService.refreshDossierTemplate(this._dossierTemplatesService.activeDossierTemplateId);
await this._loadData();
}
@ -146,9 +152,9 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
this._loadingService.start();
try {
const response = await this._fileAttributesService
.getFileAttributesConfig(this._dossierTemplatesService.activeDossierTemplateId)
.toPromise();
const response = await firstValueFrom(
this._fileAttributesService.getFileAttributesConfig(this._dossierTemplatesService.activeDossierTemplateId),
);
this._existingConfiguration = response;
const fileAttributeConfig = response?.fileAttributeConfigs.map(item => new FileAttributeConfig(item)) || [];
this.entitiesService.setEntities(fileAttributeConfig);

View File

@ -2,7 +2,7 @@
<div class="heading-l" translate="general-config-screen.general.title"></div>
<div translate="general-config-screen.general.subtitle"></div>
</div>
<form (submit)="saveGeneralConfig()" [formGroup]="form">
<form (submit)="save()" [formGroup]="form" *ngIf="form">
<div class="dialog-content">
<div class="dialog-content-left">
<div class="iqser-input-group">
@ -23,7 +23,7 @@
</div>
</div>
<div class="dialog-actions">
<button [disabled]="form.invalid || !generalConfigurationChanged" color="primary" mat-flat-button type="submit">
<button [disabled]="form?.invalid || !changed" color="primary" mat-flat-button type="submit">
{{ 'general-config-screen.actions.save' | translate }}
</button>
</div>

View File

@ -1,18 +1,18 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AutoUnsubscribe, LoadingService } from '@iqser/common-ui';
import { BaseFormComponent, LoadingService } from '@iqser/common-ui';
import { GeneralSettingsService } from '@services/general-settings.service';
import { IGeneralConfiguration } from '@red/domain';
import { ConfigService } from '@services/config.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-general-config-form',
templateUrl: './general-config-form.component.html',
styleUrls: ['./general-config-form.component.scss'],
})
export class GeneralConfigFormComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
export class GeneralConfigFormComponent extends BaseFormComponent implements OnInit, OnDestroy {
private _initialConfiguration: IGeneralConfiguration;
readonly form: FormGroup = this._getForm();
constructor(
private readonly _loadingService: LoadingService,
@ -21,28 +21,7 @@ export class GeneralConfigFormComponent extends AutoUnsubscribe implements OnIni
private readonly _formBuilder: FormBuilder,
) {
super();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
forgotPasswordFunctionEnabled: [false],
auxiliaryName: [undefined],
});
}
async ngOnInit(): Promise<void> {
await this._loadData();
}
async saveGeneralConfig() {
this._loadingService.start();
const configFormValues = this.form.getRawValue();
await this._generalSettingsService.updateGeneralConfigurations(configFormValues).toPromise();
this._initialConfiguration = await this._generalSettingsService.getGeneralConfigurations().toPromise();
this._configService.updateDisplayName(this._initialConfiguration.displayName);
this._loadingService.stop();
this.form = this._getForm();
}
get generalConfigurationChanged(): boolean {
@ -59,11 +38,35 @@ export class GeneralConfigFormComponent extends AutoUnsubscribe implements OnIni
return false;
}
async ngOnInit(): Promise<void> {
await this._loadData();
}
async save() {
this._loadingService.start();
const configFormValues = this.form.getRawValue();
await firstValueFrom(this._generalSettingsService.updateGeneralConfigurations(configFormValues));
this._initialConfiguration = await firstValueFrom(this._generalSettingsService.getGeneralConfigurations());
this._configService.updateDisplayName(this._initialConfiguration.displayName);
this._loadingService.stop();
await this._loadData();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
forgotPasswordFunctionEnabled: [false],
auxiliaryName: [undefined],
});
}
private async _loadData() {
this._loadingService.start();
try {
this._initialConfiguration = await this._generalSettingsService.getGeneralConfigurations().toPromise();
this._initialConfiguration = await firstValueFrom(this._generalSettingsService.getGeneralConfigurations());
this.form.patchValue(this._initialConfiguration, { emitEvent: false });
this.initialFormValue = this.form.getRawValue();
} catch (e) {}
this._loadingService.stop();

View File

@ -1,13 +1,52 @@
import { Component } from '@angular/core';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { UserService } from '@services/user.service';
import { GeneralConfigFormComponent } from './general-config-form/general-config-form.component';
import { SmtpFormComponent } from './smtp-form/smtp-form.component';
import { BaseFormComponent } from '@iqser/common-ui';
@Component({
selector: 'redaction-general-config-screen',
templateUrl: './general-config-screen.component.html',
styleUrls: ['./general-config-screen.component.scss'],
})
export class GeneralConfigScreenComponent {
export class GeneralConfigScreenComponent extends BaseFormComponent implements AfterViewInit {
readonly currentUser = this._userService.currentUser;
constructor(private readonly _userService: UserService) {}
@ViewChild(GeneralConfigFormComponent) generalConfigFormComponent: GeneralConfigFormComponent;
@ViewChild(SmtpFormComponent) smtpFormComponent: SmtpFormComponent;
children: BaseFormComponent[];
constructor(private readonly _userService: UserService) {
super();
}
ngAfterViewInit() {
this.children = [this.generalConfigFormComponent, this.smtpFormComponent];
}
get changed(): boolean {
for (const child of this.children) {
if (child.changed) {
return true;
}
}
return false;
}
get valid() {
for (const child of this.children) {
if (!child.valid) {
return false;
}
}
return true;
}
async save(): Promise<void> {
for (const child of this.children) {
if (child.changed) {
await child.save();
}
}
}
}

View File

@ -94,7 +94,7 @@
</div>
</div>
<div class="dialog-actions">
<button [disabled]="form.invalid || !smtpConfigurationChanged" color="primary" mat-flat-button type="submit">
<button [disabled]="form.invalid || !changed" color="primary" mat-flat-button type="submit">
{{ 'general-config-screen.actions.save' | translate }}
</button>

View File

@ -1,20 +1,20 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ISmtpConfiguration } from '@red/domain';
import { AutoUnsubscribe, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { SmtpConfigService } from '../../../services/smtp-config.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-smtp-form',
templateUrl: './smtp-form.component.html',
styleUrls: ['./smtp-form.component.scss'],
})
export class SmtpFormComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
export class SmtpFormComponent extends BaseFormComponent implements OnInit, OnDestroy {
readonly iconButtonTypes = IconButtonTypes;
private _initialConfiguration: ISmtpConfiguration;
readonly form: FormGroup = this._getForm();
constructor(
private readonly _formBuilder: FormBuilder,
@ -24,6 +24,7 @@ export class SmtpFormComponent extends AutoUnsubscribe implements OnInit, OnDest
private readonly _toaster: Toaster,
) {
super();
this.form = this._getForm();
this.addSubscription = this.form.controls.auth.valueChanges.subscribe(auth => {
if (auth) {
this.openAuthConfigDialog();
@ -35,6 +36,36 @@ export class SmtpFormComponent extends AutoUnsubscribe implements OnInit, OnDest
await this._loadData();
}
openAuthConfigDialog(skipDisableOnCancel?: boolean) {
this._dialogService.openDialog('smtpAuthConfig', null, this.form.getRawValue(), null, authConfig => {
if (authConfig) {
this.form.patchValue(authConfig);
} else if (!skipDisableOnCancel) {
this.form.patchValue({ auth: false }, { emitEvent: false });
}
});
}
async save() {
this._loadingService.start();
await firstValueFrom(this._smtpConfigService.updateSMTPConfiguration(this.form.getRawValue()));
this._initialConfiguration = this.form.getRawValue();
this._loadingService.stop();
this._loadData();
}
async testConnection() {
this._loadingService.start();
try {
await firstValueFrom(this._smtpConfigService.testSMTPConfiguration(this.form.getRawValue()));
this._toaster.success(_('general-config-screen.test.success'));
} catch (e) {
this._toaster.error(_('general-config-screen.test.error'));
} finally {
this._loadingService.stop();
}
}
private _getForm(): FormGroup {
return this._formBuilder.group({
host: [undefined, Validators.required],
@ -52,57 +83,15 @@ export class SmtpFormComponent extends AutoUnsubscribe implements OnInit, OnDest
});
}
openAuthConfigDialog(skipDisableOnCancel?: boolean) {
this._dialogService.openDialog('smtpAuthConfig', null, this.form.getRawValue(), null, authConfig => {
if (authConfig) {
this.form.patchValue(authConfig);
} else if (!skipDisableOnCancel) {
this.form.patchValue({ auth: false }, { emitEvent: false });
}
});
}
async save() {
this._loadingService.start();
await this._smtpConfigService.updateSMTPConfiguration(this.form.getRawValue()).toPromise();
this._initialConfiguration = this.form.getRawValue();
this._loadingService.stop();
}
get smtpConfigurationChanged(): boolean {
if (!this._initialConfiguration) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (this._initialConfiguration[key] !== this.form.get(key).value) {
return true;
}
}
return false;
}
async testConnection() {
this._loadingService.start();
try {
await this._smtpConfigService.testSMTPConfiguration(this.form.getRawValue()).toPromise();
this._toaster.success(_('general-config-screen.test.success'));
} catch (e) {
this._toaster.error(_('general-config-screen.test.error'));
} finally {
this._loadingService.stop();
}
}
private async _loadData() {
this._loadingService.start();
try {
this._initialConfiguration = await this._smtpConfigService.getCurrentSMTPConfiguration().toPromise();
this._initialConfiguration = await firstValueFrom(this._smtpConfigService.getCurrentSMTPConfiguration());
this.form.patchValue(this._initialConfiguration, { emitEvent: false });
} catch (e) {}
this.initialFormValue = this.form.getRawValue();
this._loadingService.stop();
}
}

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,56 @@
<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 *ngIf="dossierTemplate.validTo && dossierTemplate.validFrom">
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ 'dossier-template-info-screen.valid-from' | translate: { date: dossierTemplate.validFrom | date: 'd MMM. yyyy' } }}
</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.validTo && dossierTemplate.validFrom">
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ 'dossier-template-info-screen.valid-to' | translate: { date: dossierTemplate.validTo | date: 'd MMM. yyyy' } }}
</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(3, 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 {
readonly dossierTemplate$: Observable<DossierTemplate>;
readonly 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: MouseEvent, dossierTemplate: DossierTemplate) {
this._dialogService.openDialog('addEditDossierTemplate', $event, dossierTemplate);
}
}

View File

@ -43,13 +43,13 @@
</div>
</div>
<div class="dialog-actions">
<button [disabled]="form.invalid || !changed" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'add-edit-justification.actions.save' | translate }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="add-edit-justification.actions.cancel"></div>
<div class="all-caps-label cancel" translate="add-edit-justification.actions.cancel" (click)="close()"></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,10 +1,11 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Justification } from '@red/domain';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { LoadingService } from '@iqser/common-ui';
import { BaseDialogComponent, LoadingService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-add-edit-justification-dialog',
@ -12,33 +13,31 @@ import { LoadingService } from '@iqser/common-ui';
styleUrls: ['./add-edit-justification-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddEditJustificationDialogComponent {
readonly form: FormGroup = this._getForm();
export class AddEditJustificationDialogComponent extends BaseDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _justificationService: JustificationsService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _loadingService: LoadingService,
public dialogRef: MatDialogRef<AddEditJustificationDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditJustificationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public justification: Justification,
) {}
) {
super(_injector, _dialogRef);
get changed(): boolean {
return (
!this.justification ||
Object.keys(this.form.getRawValue()).reduce((prev, key) => prev || this.justification[key] !== this.form.get(key).value, false)
);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
async save() {
const dossierTemplateId = this._dossierTemplatesService.activeDossierTemplateId;
this._loadingService.start();
await this._justificationService.createOrUpdate(this.form.getRawValue(), dossierTemplateId).toPromise();
await this._justificationService.loadAll(dossierTemplateId).toPromise();
await firstValueFrom(this._justificationService.createOrUpdate(this.form.getRawValue(), dossierTemplateId));
await firstValueFrom(this._justificationService.loadAll(dossierTemplateId));
this._loadingService.stop();
this.dialogRef.close(true);
this._dialogRef.close(true);
}
private _getForm(): FormGroup {

Some files were not shown because too many files have changed in this diff Show More