Merge branch 'master' into VM/RED-2539
This commit is contained in:
commit
7ef6718df3
@ -17,6 +17,7 @@ trim_trailing_whitespace = false
|
||||
[*.ts]
|
||||
ij_typescript_use_double_quotes = false
|
||||
ij_typescript_enforce_trailing_comma = keep
|
||||
ij_typescript_spaces_within_imports = true
|
||||
|
||||
[{*.json, .prettierrc, .eslintrc}]
|
||||
indent_size = 2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -45,3 +45,4 @@ version.properties
|
||||
paligo-styles/style.css*
|
||||
|
||||
migrations.json
|
||||
*.iml
|
||||
|
||||
81
angular.json
81
angular.json
@ -1,23 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"defaultCollection": "@nrwl/angular",
|
||||
"analytics": false,
|
||||
"packageManager": "yarn"
|
||||
},
|
||||
"defaultProject": "red-ui",
|
||||
"schematics": {
|
||||
"@nrwl/angular:application": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest",
|
||||
"e2eTestRunner": "cypress"
|
||||
},
|
||||
"@nrwl/angular:library": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest"
|
||||
},
|
||||
"@nrwl/angular:component": {}
|
||||
"analytics": "d22ff5ae-c863-4253-83e3-0a969e4bb5fe"
|
||||
},
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"common-ui": {
|
||||
"projectType": "library",
|
||||
@ -40,30 +25,8 @@
|
||||
},
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"red-domain": {
|
||||
"projectType": "library",
|
||||
"root": "libs/red-domain",
|
||||
"sourceRoot": "libs/red-domain/src",
|
||||
"prefix": "red",
|
||||
"architect": {
|
||||
"test": {
|
||||
"builder": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/red-domain"],
|
||||
"options": {
|
||||
"jestConfig": "libs/red-domain/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@nrwl/linter:eslint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/red-domain/src/**/*.ts", "libs/red-domain/src/**/*.html"]
|
||||
},
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"red-cache": {
|
||||
"projectType": "library",
|
||||
@ -91,6 +54,30 @@
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"red-domain": {
|
||||
"projectType": "library",
|
||||
"root": "libs/red-domain",
|
||||
"sourceRoot": "libs/red-domain/src",
|
||||
"prefix": "red",
|
||||
"architect": {
|
||||
"test": {
|
||||
"builder": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/red-domain"],
|
||||
"options": {
|
||||
"jestConfig": "libs/red-domain/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@nrwl/linter:eslint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/red-domain/src/**/*.ts", "libs/red-domain/src/**/*.html"]
|
||||
},
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"red-ui": {
|
||||
@ -152,7 +139,14 @@
|
||||
"with": "apps/red-ui/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"optimization": {
|
||||
"scripts": true,
|
||||
"styles": {
|
||||
"minify": true,
|
||||
"inlineCritical": false
|
||||
},
|
||||
"fonts": true
|
||||
},
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
@ -209,7 +203,8 @@
|
||||
},
|
||||
"outputs": ["coverage/apps/red-ui"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@ import { AuthGuard } from './modules/auth/auth.guard';
|
||||
import { CompositeRouteGuard, CustomRouteReuseStrategy } from '@iqser/common-ui';
|
||||
import { RedRoleGuard } from './modules/auth/red-role.guard';
|
||||
import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
|
||||
import { RouteReuseStrategy, RouterModule } from '@angular/router';
|
||||
import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
|
||||
import { AppStateGuard } from '@state/app-state.guard';
|
||||
import { UserProfileScreenComponent } from '@components/user-profile/user-profile-screen.component';
|
||||
import { DossiersGuard } from '@guards/dossiers.guard';
|
||||
|
||||
const routes = [
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'main/dossiers',
|
||||
@ -21,18 +21,9 @@ const routes = [
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'main/my-profile',
|
||||
path: 'main/account',
|
||||
component: BaseScreenComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: UserProfileScreenComponent,
|
||||
},
|
||||
],
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
|
||||
},
|
||||
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
|
||||
},
|
||||
{
|
||||
path: 'main/admin',
|
||||
@ -43,6 +34,12 @@ const routes = [
|
||||
path: 'main/dossiers',
|
||||
component: BaseScreenComponent,
|
||||
loadChildren: () => import('./modules/dossier/dossiers.module').then(m => m.DossiersModule),
|
||||
canActivate: [CompositeRouteGuard],
|
||||
canDeactivate: [DossiersGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard, DossiersGuard],
|
||||
requiredRoles: ['RED_USER', 'RED_MANAGER'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'main/downloads',
|
||||
|
||||
@ -20,7 +20,6 @@ import { DownloadsListScreenComponent } from '@components/downloads-list-screen/
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
|
||||
import { UserProfileScreenComponent } from '@components/user-profile/user-profile-screen.component';
|
||||
import { PlatformLocation } from '@angular/common';
|
||||
import { BASE_HREF } from './tokens';
|
||||
import { MONACO_PATH, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
|
||||
@ -36,6 +35,7 @@ import * as links from '../assets/help-mode/links.json';
|
||||
import { HELP_DOCS, IqserHelpModeModule, MAX_RETRIES_ON_SERVER_ERROR, ServerErrorInterceptor, ToastComponent } from '@iqser/common-ui';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { GeneralSettingsService } from '@services/general-settings.service';
|
||||
import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component';
|
||||
|
||||
export function httpLoaderFactory(httpClient: HttpClient): PruningTranslationLoader {
|
||||
return new PruningTranslationLoader(httpClient, '/assets/i18n/', '.json');
|
||||
@ -51,9 +51,9 @@ function cleanupBaseUrl(baseUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const screens = [BaseScreenComponent, DownloadsListScreenComponent, UserProfileScreenComponent];
|
||||
const screens = [BaseScreenComponent, DownloadsListScreenComponent];
|
||||
|
||||
const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, ...screens];
|
||||
const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, BreadcrumbsComponent, ...screens];
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
|
||||
@ -4,41 +4,7 @@
|
||||
<div *ngIf="!currentUser.isUser" class="menu-placeholder"></div>
|
||||
|
||||
<div *ngIf="currentUser.isUser" class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a *ngIf="(isDossiersView$ | async) === false" class="breadcrumb back" redactionNavigateLastDossiersScreen>
|
||||
<mat-icon svgIcon="iqser:expand"></mat-icon>
|
||||
{{ 'top-bar.navigation-items.back' | translate }}
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="isDossiersView$ | async">
|
||||
<a
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="breadcrumb"
|
||||
routerLink="/main/dossiers"
|
||||
routerLinkActive="active"
|
||||
translate="top-bar.navigation-items.dossiers"
|
||||
></a>
|
||||
|
||||
<ng-container *ngIf="dossiersService.activeDossier$ | async as dossier">
|
||||
<mat-icon svgIcon="iqser:arrow-right"></mat-icon>
|
||||
|
||||
<a
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="dossier.routerLink"
|
||||
class="breadcrumb"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
{{ dossier.dossierName }}
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="appStateService.activeFile as activeFile">
|
||||
<mat-icon svgIcon="iqser:arrow-right"></mat-icon>
|
||||
|
||||
<a [routerLink]="activeFile.routerLink" class="breadcrumb" routerLinkActive="active">
|
||||
{{ activeFile.filename }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<redaction-breadcrumbs></redaction-breadcrumbs>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
@ -54,16 +20,16 @@
|
||||
*ngIf="(isSearchScreen$ | async) === false && (currentUser.isUser || currentUser.isManager)"
|
||||
[actions]="searchActions"
|
||||
[placeholder]="'search.placeholder' | translate"
|
||||
iqserHelpMode="search"
|
||||
iqserHelpMode="search-in-entire-application"
|
||||
></redaction-spotlight-search>
|
||||
|
||||
<redaction-notifications iqserHelpMode="notifications"></redaction-notifications>
|
||||
<redaction-notifications iqserHelpMode="open-notifications"></redaction-notifications>
|
||||
</div>
|
||||
<redaction-user-button
|
||||
[matMenuTriggerFor]="userMenu"
|
||||
[showDot]="fileDownloadService.hasPendingDownloads"
|
||||
[userId]="currentUser.id"
|
||||
iqserHelpMode="user-menu"
|
||||
iqserHelpMode="open-usermenu"
|
||||
></redaction-user-button>
|
||||
|
||||
<mat-menu #userMenu="matMenu" xPosition="before">
|
||||
|
||||
@ -8,8 +8,10 @@ import { FileDownloadService } from '@upload-download/services/file-download.ser
|
||||
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 { distinctUntilChanged, filter, map, startWith } from 'rxjs/operators';
|
||||
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';
|
||||
|
||||
interface MenuItem {
|
||||
readonly name: string;
|
||||
@ -20,7 +22,6 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
const isNavigationStart = event => event instanceof NavigationStart;
|
||||
const isDossiersView = url => url.includes('/main/dossiers') && !url.includes('/search');
|
||||
const isSearchScreen = url => url.includes('/main/dossiers') && url.includes('/search');
|
||||
|
||||
@Component({
|
||||
@ -31,9 +32,10 @@ export class BaseScreenComponent {
|
||||
readonly currentUser = this.userService.currentUser;
|
||||
readonly userMenuItems: readonly MenuItem[] = [
|
||||
{
|
||||
name: _('top-bar.navigation-items.my-account.children.my-profile'),
|
||||
routerLink: '/main/my-profile',
|
||||
show: true,
|
||||
name: _('top-bar.navigation-items.my-account.children.account'),
|
||||
routerLink: '/main/account',
|
||||
show: this.currentUser.isUser,
|
||||
action: this.appStateService.reset,
|
||||
showDot: () => false,
|
||||
},
|
||||
{
|
||||
@ -60,8 +62,8 @@ export class BaseScreenComponent {
|
||||
{
|
||||
text: this._translateService.instant('search.this-dossier'),
|
||||
icon: 'red:enter',
|
||||
hide: (): boolean => !this.dossiersService.activeDossier,
|
||||
action: (query): void => this._search(query, this.dossiersService.activeDossierId),
|
||||
hide: (): boolean => this._hideSearchThisDossier,
|
||||
action: (query): void => this._searchThisDossier(query),
|
||||
},
|
||||
{
|
||||
text: this._translateService.instant('search.entire-platform'),
|
||||
@ -73,9 +75,8 @@ export class BaseScreenComponent {
|
||||
filter(isNavigationStart),
|
||||
map((event: NavigationStart) => event.url),
|
||||
startWith(this._router.url),
|
||||
distinctUntilChanged(),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
readonly isDossiersView$ = this._navigationStart$.pipe(map(isDossiersView));
|
||||
readonly isSearchScreen$ = this._navigationStart$.pipe(map(isSearchScreen));
|
||||
|
||||
constructor(
|
||||
@ -87,8 +88,19 @@ export class BaseScreenComponent {
|
||||
readonly fileDownloadService: FileDownloadService,
|
||||
private readonly _router: Router,
|
||||
private readonly _translateService: TranslateService,
|
||||
readonly breadcrumbsService: BreadcrumbsService,
|
||||
) {}
|
||||
|
||||
private get _hideSearchThisDossier() {
|
||||
const routerLink = this.breadcrumbsService.breadcrumbs[1]?.routerLink;
|
||||
if (!routerLink) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isDossierOverview = routerLink.includes('dossiers') && routerLink.length === 3;
|
||||
return !isDossierOverview;
|
||||
}
|
||||
|
||||
trackByName(index: number, item: MenuItem) {
|
||||
return item.name;
|
||||
}
|
||||
@ -97,4 +109,13 @@ export class BaseScreenComponent {
|
||||
const queryParams = { query, dossierId };
|
||||
this._router.navigate(['main/dossiers/search'], { queryParams }).then();
|
||||
}
|
||||
|
||||
private _searchThisDossier(query: string) {
|
||||
const routerLink = this.breadcrumbsService.breadcrumbs[1]?.routerLink;
|
||||
if (!routerLink) {
|
||||
return this._search(query);
|
||||
}
|
||||
const dossierId = routerLink[2];
|
||||
return this._search(query, dossierId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
<ng-container *ngIf="breadcrumbsService.breadcrumbs$ | async as breadcrumbs">
|
||||
<a
|
||||
*ngIf="breadcrumbs.length === 0 || (breadcrumbsService.showGoBack$ | async) === true; else items"
|
||||
class="breadcrumb back"
|
||||
redactionNavigateLastDossiersScreen
|
||||
>
|
||||
<mat-icon svgIcon="iqser:expand"></mat-icon>
|
||||
{{ 'top-bar.navigation-items.back' | translate }}
|
||||
</a>
|
||||
|
||||
<ng-template #items>
|
||||
<ng-container *ngFor="let breadcrumb of breadcrumbs; first as first">
|
||||
<mat-icon *ngIf="!first" svgIcon="iqser:arrow-right"></mat-icon>
|
||||
|
||||
<a
|
||||
[routerLinkActiveOptions]="breadcrumb.routerLinkActiveOptions ?? { exact: false }"
|
||||
[routerLink]="breadcrumb.routerLink"
|
||||
class="breadcrumb"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
{{ breadcrumb.name$ | async }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@ -0,0 +1,10 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { BreadcrumbsService } from '@services/breadcrumbs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-breadcrumbs',
|
||||
templateUrl: './breadcrumbs.component.html',
|
||||
styleUrls: ['./breadcrumbs.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BreadcrumbsComponent {
|
||||
constructor(readonly breadcrumbsService: BreadcrumbsService) {}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
<iqser-empty-state
|
||||
*ngIf="!groups.length"
|
||||
[horizontalPadding]="40"
|
||||
[text]="'notifications.no-data' | translate"
|
||||
[text]="'notification.no-data' | translate"
|
||||
[verticalPadding]="0"
|
||||
></iqser-empty-state>
|
||||
|
||||
|
||||
@ -6,9 +6,11 @@ import { UserService } from '@services/user.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { NotificationsService } from '@services/notifications.service';
|
||||
import { Notification } from '@red/domain';
|
||||
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { List } from '@iqser/common-ui';
|
||||
import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, timer } from 'rxjs';
|
||||
import { AutoUnsubscribe, CHANGED_CHECK_INTERVAL, List, shareLast } from '@iqser/common-ui';
|
||||
|
||||
const INCLUDE_SEEN = false;
|
||||
|
||||
interface NotificationsGroup {
|
||||
date: string;
|
||||
@ -21,11 +23,11 @@ interface NotificationsGroup {
|
||||
styleUrls: ['./notifications.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NotificationsComponent implements OnInit {
|
||||
export class NotificationsComponent extends AutoUnsubscribe implements OnInit {
|
||||
notifications$: Observable<Notification[]>;
|
||||
hasUnreadNotifications$: Observable<boolean>;
|
||||
groupedNotifications$: Observable<NotificationsGroup[]>;
|
||||
private _notifications$ = new BehaviorSubject([]);
|
||||
private _notifications$ = new BehaviorSubject<Notification[]>([]);
|
||||
|
||||
constructor(
|
||||
private readonly _translateService: TranslateService,
|
||||
@ -35,17 +37,25 @@ export class NotificationsComponent implements OnInit {
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _datePipe: DatePipe,
|
||||
) {
|
||||
this.notifications$ = this._notifications$.asObservable();
|
||||
super();
|
||||
this.notifications$ = this._notifications$.asObservable().pipe(shareLast());
|
||||
this.groupedNotifications$ = this.notifications$.pipe(map(notifications => this._groupNotifications(notifications)));
|
||||
this.hasUnreadNotifications$ = this.notifications$.pipe(
|
||||
map(notifications => notifications.filter(n => !n.readDate).length > 0),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1),
|
||||
shareLast(),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this._loadData();
|
||||
|
||||
this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
|
||||
.pipe(
|
||||
switchMap(() => this._notificationsService.getNotificationsIfChanged(INCLUDE_SEEN)),
|
||||
tap(notifications => this._notifications$.next(notifications)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async markRead($event, notifications: List<string> = this._notifications$.getValue().map(n => n.id), isRead = true): Promise<void> {
|
||||
@ -55,7 +65,7 @@ export class NotificationsComponent implements OnInit {
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const notifications = await this._notificationsService.getNotifications(false).toPromise();
|
||||
const notifications = await this._notificationsService.getNotifications(INCLUDE_SEEN).toPromise();
|
||||
this._notifications$.next(notifications);
|
||||
}
|
||||
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
<section class="content-inner">
|
||||
<div class="content-container full-height">
|
||||
<div class="overlay-shadow"></div>
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<div class="heading-l" translate="user-profile.title"></div>
|
||||
</div>
|
||||
<form (submit)="save()" [formGroup]="formGroup">
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-content-left">
|
||||
<div class="iqser-input-group required">
|
||||
<label translate="user-profile.form.email"></label>
|
||||
<input formControlName="email" name="email" type="email" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group">
|
||||
<label translate="user-profile.form.first-name"></label>
|
||||
<input formControlName="firstName" name="firstName" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group">
|
||||
<label translate="user-profile.form.last-name"></label>
|
||||
<input formControlName="lastName" name="lastName" type="text" />
|
||||
</div>
|
||||
<div class="iqser-input-group">
|
||||
<label translate="top-bar.navigation-items.my-account.children.language.label"></label>
|
||||
<mat-select formControlName="language">
|
||||
<mat-option *ngFor="let language of languages" [value]="language">
|
||||
{{ translations[language] | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
[disabled]="formGroup.invalid || !(profileChanged || languageChanged)"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
>
|
||||
{{ 'user-profile.actions.save' | translate }}
|
||||
</button>
|
||||
<a [href]="changePasswordUrl" target="_blank"> {{ 'user-profile.actions.change-password' | translate }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
55
apps/red-ui/src/app/guards/dossier-files-guard.ts
Normal file
55
apps/red-ui/src/app/guards/dossier-files-guard.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { DossiersService } from '../services/entity-services/dossiers.service';
|
||||
import { BreadcrumbsService } from '../services/breadcrumbs.service';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
import { AppStateService } from '../state/app-state.service';
|
||||
import { FilesMapService } from '../services/entity-services/files-map.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DossierFilesGuard implements CanActivate, CanDeactivate<unknown> {
|
||||
constructor(
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _breadcrumbsService: BreadcrumbsService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _router: Router,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const dossierId = route.paramMap.get('dossierId');
|
||||
const dossier = this._dossiersService.find(dossierId);
|
||||
|
||||
if (!dossier) {
|
||||
await this._router.navigate(['/main', 'dossiers']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._filesMapService.has(dossierId)) {
|
||||
await this._appStateService.getFiles(dossier);
|
||||
}
|
||||
|
||||
this._breadcrumbsService.append({
|
||||
name$: this._dossiersService.getEntityChanged$(dossierId).pipe(pluck('dossierName')),
|
||||
routerLink: ['/main', 'dossiers', dossierId],
|
||||
routerLinkActiveOptions: { exact: true },
|
||||
});
|
||||
|
||||
this._breadcrumbsService.hideGoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
canDeactivate(
|
||||
component: unknown,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState?: RouterStateSnapshot,
|
||||
) {
|
||||
const dossierId = currentRoute.paramMap.get('dossierId');
|
||||
this._breadcrumbsService.remove(['/main', 'dossiers', dossierId]);
|
||||
if (!nextState.url.startsWith('/main/dossiers')) {
|
||||
this._breadcrumbsService.showGoBack();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
33
apps/red-ui/src/app/guards/dossiers.guard.ts
Normal file
33
apps/red-ui/src/app/guards/dossiers.guard.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { BreadcrumbsService } from '@services/breadcrumbs.service';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DossiersGuard implements CanActivate, CanDeactivate<unknown> {
|
||||
constructor(
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _breadcrumbsService: BreadcrumbsService,
|
||||
private readonly _router: Router,
|
||||
) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
await this._dossiersService.loadAll().toPromise();
|
||||
this._breadcrumbsService.hideGoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
canDeactivate(
|
||||
component: unknown,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState?: RouterStateSnapshot,
|
||||
) {
|
||||
if (!nextState.url.startsWith('/main/dossiers')) {
|
||||
this._breadcrumbsService.showGoBack();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
54
apps/red-ui/src/app/guards/file-preview.guard.ts
Normal file
54
apps/red-ui/src/app/guards/file-preview.guard.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { FilesMapService } from '../services/entity-services/files-map.service';
|
||||
import { AppStateService } from '../state/app-state.service';
|
||||
import { DossiersService } from '../services/entity-services/dossiers.service';
|
||||
import { BreadcrumbsService } from '../services/breadcrumbs.service';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FilePreviewGuard implements CanActivate, CanDeactivate<unknown> {
|
||||
constructor(
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _breadcrumbsService: BreadcrumbsService,
|
||||
private readonly _router: Router,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const dossierId = route.paramMap.get('dossierId');
|
||||
const fileId = route.paramMap.get('fileId');
|
||||
|
||||
const dossier = this._dossiersService.find(dossierId);
|
||||
|
||||
if (!this._filesMapService.get(dossierId, fileId)) {
|
||||
await this._router.navigate([dossier.routerLink]);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._breadcrumbsService.append({
|
||||
name$: this._filesMapService.watch$(dossierId, fileId).pipe(pluck('filename')),
|
||||
routerLink: ['/main', 'dossiers', dossierId, 'file', fileId],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
canDeactivate(
|
||||
component: unknown,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState?: RouterStateSnapshot,
|
||||
) {
|
||||
const dossierId = currentRoute.paramMap.get('dossierId');
|
||||
const fileId = currentRoute.paramMap.get('fileId');
|
||||
this._breadcrumbsService.remove(['/main', 'dossiers', dossierId, 'file', fileId]);
|
||||
|
||||
if (!nextState.url.startsWith('/main/dossiers')) {
|
||||
this._breadcrumbsService.showGoBack();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
18
apps/red-ui/src/app/guards/go-back-guard.service.ts
Normal file
18
apps/red-ui/src/app/guards/go-back-guard.service.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, CanDeactivate } from '@angular/router';
|
||||
import { BreadcrumbsService } from '@services/breadcrumbs.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GoBackGuard implements CanActivate, CanDeactivate<unknown> {
|
||||
constructor(private readonly _breadcrumbsService: BreadcrumbsService) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
this._breadcrumbsService.showGoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
canDeactivate() {
|
||||
this._breadcrumbsService.hideGoBack();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ export class AnnotationPermissions {
|
||||
canRejectSuggestion = true;
|
||||
canForceRedaction = true;
|
||||
canChangeLegalBasis = true;
|
||||
canResizeAnnotation = true;
|
||||
canRecategorizeImage = true;
|
||||
|
||||
static forUser(isApprover: boolean, user: User, annotations: AnnotationWrapper | AnnotationWrapper[]) {
|
||||
@ -23,7 +24,10 @@ export class AnnotationPermissions {
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const permissions: AnnotationPermissions = new AnnotationPermissions();
|
||||
|
||||
permissions.canUndo = !isApprover && annotation.isSuggestion;
|
||||
permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
|
||||
permissions.canRejectSuggestion = isApprover && annotation.isSuggestion;
|
||||
|
||||
permissions.canForceRedaction = annotation.isSkipped && !annotation.isFalsePositive;
|
||||
permissions.canAcceptRecommendation = annotation.isRecommendation;
|
||||
@ -34,12 +38,11 @@ export class AnnotationPermissions {
|
||||
permissions.canRemoveOrSuggestToRemoveFromDictionary =
|
||||
annotation.isModifyDictionary && (annotation.isRedacted || annotation.isSkipped || annotation.isHint);
|
||||
|
||||
permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
|
||||
permissions.canRejectSuggestion = isApprover && annotation.isSuggestion;
|
||||
|
||||
permissions.canChangeLegalBasis = annotation.isRedacted;
|
||||
|
||||
permissions.canRecategorizeImage = annotation.isImage;
|
||||
permissions.canRecategorizeImage = (annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage;
|
||||
permissions.canResizeAnnotation =
|
||||
((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) || annotation.isSuggestionResize;
|
||||
|
||||
summedPermissions._merge(permissions);
|
||||
}
|
||||
|
||||
@ -4,14 +4,11 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { IComment, IPoint, IRectangle } from '@red/domain';
|
||||
|
||||
export type AnnotationSuperType =
|
||||
| 'add-dictionary'
|
||||
| 'remove-dictionary'
|
||||
| 'remove-only-here'
|
||||
| 'change-legal-basis'
|
||||
| 'suggestion-change-legal-basis'
|
||||
| 'suggestion-recategorize-image'
|
||||
| 'suggestion-add-dictionary'
|
||||
| 'suggestion-force-redaction'
|
||||
| 'suggestion-resize'
|
||||
| 'suggestion-remove-dictionary'
|
||||
| 'suggestion-add'
|
||||
| 'suggestion-remove'
|
||||
@ -20,7 +17,6 @@ export type AnnotationSuperType =
|
||||
| 'manual-redaction'
|
||||
| 'recommendation'
|
||||
| 'hint'
|
||||
| 'pending-analysis'
|
||||
| 'declined-suggestion';
|
||||
|
||||
export class AnnotationWrapper {
|
||||
@ -45,6 +41,7 @@ export class AnnotationWrapper {
|
||||
recommendationType: string;
|
||||
legalBasisValue: string;
|
||||
legalBasisChangeValue?: string;
|
||||
resizing?: boolean;
|
||||
|
||||
manual?: boolean;
|
||||
|
||||
@ -82,7 +79,7 @@ export class AnnotationWrapper {
|
||||
}
|
||||
|
||||
get isSuperTypeBasedColor() {
|
||||
return this.isSkipped || this.isSuggestion || this.isReadyForAnalysis || this.isDeclinedSuggestion;
|
||||
return this.isSkipped || this.isSuggestion || this.isDeclinedSuggestion;
|
||||
}
|
||||
|
||||
get isSkipped() {
|
||||
@ -120,16 +117,6 @@ export class AnnotationWrapper {
|
||||
return this.superType === 'declined-suggestion';
|
||||
}
|
||||
|
||||
get isReadyForAnalysis() {
|
||||
return (
|
||||
this.superType === 'add-dictionary' ||
|
||||
this.superType === 'remove-dictionary' ||
|
||||
this.superType === 'remove-only-here' ||
|
||||
this.superType === 'pending-analysis' ||
|
||||
this.superType === 'change-legal-basis'
|
||||
);
|
||||
}
|
||||
|
||||
get isApproved() {
|
||||
return this.status === 'APPROVED';
|
||||
}
|
||||
@ -143,7 +130,17 @@ export class AnnotationWrapper {
|
||||
}
|
||||
|
||||
get isSuggestion() {
|
||||
return this.isSuggestionAdd || this.isSuggestionRemove || this.isSuggestionChangeLegalBasis || this.isSuggestionRecategorizeImage;
|
||||
return (
|
||||
this.isSuggestionAdd ||
|
||||
this.isSuggestionRemove ||
|
||||
this.isSuggestionChangeLegalBasis ||
|
||||
this.isSuggestionRecategorizeImage ||
|
||||
this.isSuggestionResize
|
||||
);
|
||||
}
|
||||
|
||||
get isSuggestionResize() {
|
||||
return this.superType === 'suggestion-resize';
|
||||
}
|
||||
|
||||
get isSuggestionRecategorizeImage() {
|
||||
@ -171,7 +168,7 @@ export class AnnotationWrapper {
|
||||
}
|
||||
|
||||
get isConvertedRecommendation() {
|
||||
return this.isRecommendation && (this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary');
|
||||
return this.isRecommendation && this.superType === 'suggestion-add-dictionary';
|
||||
}
|
||||
|
||||
get isRecommendation() {
|
||||
@ -245,6 +242,13 @@ export class AnnotationWrapper {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redactionLogEntryWrapper.manualRedactionType === 'RESIZE') {
|
||||
if (redactionLogEntryWrapper.status === 'REQUESTED') {
|
||||
annotationWrapper.superType = 'suggestion-resize';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (redactionLogEntryWrapper.manualRedactionType === 'FORCE_REDACT') {
|
||||
annotationWrapper.force = true;
|
||||
|
||||
@ -263,10 +267,6 @@ export class AnnotationWrapper {
|
||||
annotationWrapper.superType = 'suggestion-add-dictionary';
|
||||
return;
|
||||
}
|
||||
if (redactionLogEntryWrapper.status === 'APPROVED') {
|
||||
annotationWrapper.superType = 'add-dictionary';
|
||||
return;
|
||||
}
|
||||
if (redactionLogEntryWrapper.status === 'DECLINED') {
|
||||
annotationWrapper.superType = 'declined-suggestion';
|
||||
return;
|
||||
@ -318,10 +318,6 @@ export class AnnotationWrapper {
|
||||
annotationWrapper.superType = 'suggestion-add-dictionary';
|
||||
return;
|
||||
}
|
||||
if (redactionLogEntryWrapper.status === 'APPROVED') {
|
||||
annotationWrapper.superType = 'add-dictionary';
|
||||
return;
|
||||
}
|
||||
if (redactionLogEntryWrapper.status === 'DECLINED') {
|
||||
annotationWrapper.superType = 'declined-suggestion';
|
||||
return;
|
||||
@ -369,11 +365,16 @@ export class AnnotationWrapper {
|
||||
|
||||
if (redactionLogEntryWrapper.status === 'APPROVED') {
|
||||
if (redactionLogEntryWrapper.dictionaryEntry) {
|
||||
annotationWrapper.superType =
|
||||
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary';
|
||||
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped';
|
||||
} else {
|
||||
annotationWrapper.superType =
|
||||
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'manual-redaction' : 'remove-only-here';
|
||||
redactionLogEntryWrapper.manualRedactionType === 'ADD'
|
||||
? 'manual-redaction'
|
||||
: annotationWrapper.redaction
|
||||
? 'redaction'
|
||||
: annotationWrapper.hint
|
||||
? 'hint'
|
||||
: 'skipped';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ export class FileDataModel {
|
||||
const entries: RedactionLogEntryWrapper[] = this._convertData();
|
||||
let allAnnotations = entries
|
||||
.map(entry => AnnotationWrapper.fromData(entry))
|
||||
.filter(ann => !this.file.excludedPages.includes(ann.pageNumber));
|
||||
.filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber));
|
||||
|
||||
if (!areDevFeaturesEnabled) {
|
||||
allAnnotations = allAnnotations.filter(annotation => !annotation.isFalsePositive);
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
import { IManualRedactionEntry } from '@red/domain';
|
||||
|
||||
export const ManualRedactionEntryTypes = {
|
||||
DICTIONARY: 'DICTIONARY',
|
||||
REDACTION: 'REDACTION',
|
||||
FALSE_POSITIVE: 'FALSE_POSITIVE',
|
||||
} as const;
|
||||
|
||||
export type ManualRedactionEntryType = keyof typeof ManualRedactionEntryTypes;
|
||||
|
||||
export class ManualRedactionEntryWrapper {
|
||||
constructor(
|
||||
readonly quads: any,
|
||||
readonly manualRedactionEntry: IManualRedactionEntry,
|
||||
readonly type: 'DICTIONARY' | 'REDACTION' | 'FALSE_POSITIVE',
|
||||
readonly type: ManualRedactionEntryType,
|
||||
readonly annotationType: 'TEXT' | 'RECTANGLE' = 'TEXT',
|
||||
readonly rectId?: string,
|
||||
) {}
|
||||
|
||||
@ -17,7 +17,7 @@ export interface RedactionLogEntryWrapper {
|
||||
id?: string;
|
||||
legalBasis?: string;
|
||||
manual?: boolean;
|
||||
manualRedactionType?: 'ADD' | 'REMOVE' | 'UNDO' | 'LEGAL_BASIS_CHANGE' | 'FORCE_REDACT' | 'RECATEGORIZE';
|
||||
manualRedactionType?: 'ADD' | 'REMOVE' | 'LEGAL_BASIS_CHANGE' | 'FORCE_REDACT' | 'RECATEGORIZE' | 'RESIZE';
|
||||
matchedRule?: number;
|
||||
positions?: Array<IRectangle>;
|
||||
reason?: string;
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
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 { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
|
||||
|
||||
const routes = [
|
||||
{ path: '', redirectTo: 'user-profile', pathMatch: 'full' },
|
||||
{
|
||||
path: 'user-profile',
|
||||
component: BaseAccountScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
|
||||
requiredRoles: ['RED_USER'],
|
||||
},
|
||||
loadChildren: () => import('./screens/user-profile/user-profile.module').then(m => m.UserProfileModule),
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
component: BaseAccountScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
|
||||
},
|
||||
loadChildren: () => import('./screens/notifications/notifications.module').then(m => m.NotificationsModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AccountRoutingModule {}
|
||||
@ -0,0 +1,7 @@
|
||||
<iqser-side-nav [title]="'account-settings' | translate">
|
||||
<ng-container *ngFor="let item of items">
|
||||
<div [routerLinkActiveOptions]="{ exact: false }" [routerLink]="'../' + item.screen" class="item" routerLinkActive="active">
|
||||
{{ item.label | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</iqser-side-nav>
|
||||
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
height: calc(100vh - 61px);
|
||||
|
||||
&.dossier-templates {
|
||||
height: calc(100vh - 111px);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
interface NavItem {
|
||||
readonly label: string;
|
||||
readonly screen: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-account-side-nav',
|
||||
templateUrl: './account-side-nav.component.html',
|
||||
styleUrls: ['./account-side-nav.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccountSideNavComponent {
|
||||
readonly items: NavItem[] = [
|
||||
{
|
||||
screen: 'user-profile',
|
||||
label: _('user-profile'),
|
||||
},
|
||||
{
|
||||
screen: 'notifications',
|
||||
label: _('notifications'),
|
||||
},
|
||||
];
|
||||
}
|
||||
14
apps/red-ui/src/app/modules/account/account.module.ts
Normal file
14
apps/red-ui/src/app/modules/account/account.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AccountRoutingModule } from './account-routing.module';
|
||||
import { AccountSideNavComponent } from './account-side-nav/account-side-nav.component';
|
||||
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
|
||||
import { NotificationPreferencesService } from './services/notification-preferences.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountSideNavComponent, BaseAccountScreenComponent],
|
||||
imports: [CommonModule, SharedModule, AccountRoutingModule],
|
||||
providers: [NotificationPreferencesService],
|
||||
})
|
||||
export class AccountModule {}
|
||||
@ -0,0 +1,20 @@
|
||||
<section class="settings">
|
||||
<div class="overlay-shadow"></div>
|
||||
|
||||
<redaction-account-side-nav></redaction-account-side-nav>
|
||||
|
||||
<div>
|
||||
<div class="content-inner">
|
||||
<div class="content-container full-height">
|
||||
<div class="overlay-shadow"></div>
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<div class="heading-l" [translate]="translations[path]"></div>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,11 +1,16 @@
|
||||
@use 'variables';
|
||||
@use 'common-mixins';
|
||||
@use 'apps/red-ui/src/assets/styles/variables';
|
||||
@use 'libs/common-ui/src/assets/styles/common-mixins';
|
||||
|
||||
.content-container {
|
||||
background-color: variables.$grey-2;
|
||||
justify-content: center;
|
||||
@include common-mixins.scroll-bar;
|
||||
overflow: auto;
|
||||
|
||||
.dialog {
|
||||
width: var(--width);
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.full-height {
|
||||
@ -17,11 +22,3 @@
|
||||
height: calc(100% + 50px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: white;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, ViewContainerRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { notificationsTranslations } from '../translations/notifications-translations';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-base-account-screen',
|
||||
templateUrl: './base-account-screen-component.html',
|
||||
styleUrls: ['./base-account-screen-component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseAccountScreenComponent implements OnInit {
|
||||
readonly translations = notificationsTranslations;
|
||||
path: string;
|
||||
|
||||
constructor(private readonly _router: Router, private readonly _hostRef: ViewContainerRef) {
|
||||
this.path = this._router.url.split('/').pop();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._setDialogWidth();
|
||||
}
|
||||
|
||||
private _setDialogWidth() {
|
||||
const element = this._hostRef.element.nativeElement as HTMLElement;
|
||||
element.style.setProperty('--width', this.path === 'user-profile' ? 'unset' : '100%');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
export const NotificationCategories = {
|
||||
inAppNotifications: 'inAppNotifications',
|
||||
emailNotifications: 'emailNotifications',
|
||||
} as const;
|
||||
|
||||
export const NotificationCategoriesValues = Object.values(NotificationCategories);
|
||||
|
||||
export const OwnDossiersNotificationsTypes = {
|
||||
dossierStatusChanges: 'dossierStatusChanges',
|
||||
requestToJoinTheDossier: 'requestToJoinTheDossier',
|
||||
documentStatusChanges: 'documentStatusChanges',
|
||||
documentIsSentForApproval: 'documentIsSentForApproval',
|
||||
} as const;
|
||||
|
||||
export const OwnDossiersNotificationsTypesValues = Object.values(OwnDossiersNotificationsTypes);
|
||||
|
||||
export const ReviewerOnDossiersNotificationsTypes = {
|
||||
whenIAmAssignedOnADocument: 'whenIAmAssignedOnADocument',
|
||||
whenIAmUnassignedFromADocument: 'whenIAmUnassignedFromADocument',
|
||||
whenADocumentIsApproved: 'whenADocumentIsApproved',
|
||||
} as const;
|
||||
|
||||
export const ReviewerOnDossiersNotificationsTypesValues = Object.values(ReviewerOnDossiersNotificationsTypes);
|
||||
|
||||
export const ApproverOnDossiersNotificationsTypes = {
|
||||
whenADocumentIsSentForApproval: 'whenADocumentIsSentForApproval',
|
||||
whenADocumentIsAssignedToAReviewer: 'whenADocumentIsAssignedToAReviewer',
|
||||
whenAReviewerIsUnassignedFromADocument: 'whenAReviewerIsUnassignedFromADocument',
|
||||
} as const;
|
||||
|
||||
export const ApproverOnDossiersNotificationsTypesValues = Object.values(ApproverOnDossiersNotificationsTypes);
|
||||
|
||||
export const NotificationGroupsKeys = ['own', 'reviewer', 'approver'] as const;
|
||||
export const NotificationGroupsValues = [
|
||||
OwnDossiersNotificationsTypesValues,
|
||||
ReviewerOnDossiersNotificationsTypesValues,
|
||||
ApproverOnDossiersNotificationsTypesValues,
|
||||
] as const;
|
||||
@ -0,0 +1,43 @@
|
||||
<form (submit)="save()" [formGroup]="formGroup">
|
||||
<div class="dialog-content">
|
||||
<div *ngFor="let category of notificationCategories">
|
||||
<div class="iqser-input-group header w-full">
|
||||
<mat-slide-toggle color="primary" formControlName="{{ category }}Enabled">{{
|
||||
translations[category] | translate
|
||||
}}</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 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 class="iqser-input-group">
|
||||
<mat-checkbox
|
||||
*ngFor="let preference of notificationGroupsValues[i]"
|
||||
color="primary"
|
||||
[checked]="isPreferenceChecked(category, preference)"
|
||||
(change)="addRemovePreference($event.checked, category, preference)"
|
||||
>
|
||||
{{ translations[preference] | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="formGroup.invalid" color="primary" mat-flat-button type="submit">
|
||||
{{ 'user-profile-screen.actions.save' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,51 @@
|
||||
@use 'variables';
|
||||
@use 'libs/common-ui/src/assets/styles/common-mixins';
|
||||
|
||||
.dialog-content {
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
grid-column: span 2;
|
||||
padding: 10px 10px;
|
||||
margin-bottom: -1px;
|
||||
border-top: 1px solid variables.$separator;
|
||||
border-bottom: 1px solid variables.$separator;
|
||||
}
|
||||
|
||||
.options-content {
|
||||
padding: 10px 48px;
|
||||
|
||||
.statement {
|
||||
opacity: 0.7;
|
||||
color: variables.$grey-1;
|
||||
font-weight: 500;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.radio-container {
|
||||
display: flex;
|
||||
padding: 10px 0 10px;
|
||||
.radio-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 30px;
|
||||
iqser-round-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
padding: 10px 0;
|
||||
|
||||
.group-title {
|
||||
color: variables.$grey-1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.iqser-input-group {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
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 { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { NotificationCategoriesValues, NotificationGroupsKeys, NotificationGroupsValues } from '../constants';
|
||||
import { EmailNotificationScheduleTypesValues } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-notifications-screen',
|
||||
templateUrl: './notifications-screen.component.html',
|
||||
styleUrls: ['./notifications-screen.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NotificationsScreenComponent implements OnInit {
|
||||
readonly emailNotificationScheduleTypes = EmailNotificationScheduleTypesValues;
|
||||
readonly notificationCategories = NotificationCategoriesValues;
|
||||
readonly notificationGroupsKeys = NotificationGroupsKeys;
|
||||
readonly notificationGroupsValues = NotificationGroupsValues;
|
||||
readonly translations = notificationsTranslations;
|
||||
|
||||
formGroup: FormGroup;
|
||||
|
||||
constructor(
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _notificationPreferencesService: NotificationPreferencesService,
|
||||
) {
|
||||
this.formGroup = this._formBuilder.group({
|
||||
inAppNotificationsEnabled: [undefined],
|
||||
emailNotificationsEnabled: [undefined],
|
||||
emailNotificationType: [undefined],
|
||||
emailNotifications: [undefined],
|
||||
inAppNotifications: [undefined],
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this._initializeForm();
|
||||
}
|
||||
|
||||
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._loadingService.stop();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
|
||||
|
||||
const routes = [{ path: '', component: NotificationsScreenComponent }];
|
||||
|
||||
@NgModule({
|
||||
declarations: [NotificationsScreenComponent],
|
||||
imports: [RouterModule.forChild(routes), CommonModule, SharedModule],
|
||||
})
|
||||
export class NotificationsModule {}
|
||||
@ -0,0 +1,37 @@
|
||||
<form (submit)="save()" [formGroup]="formGroup">
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-content-left">
|
||||
<div class="iqser-input-group required">
|
||||
<label translate="user-profile-screen.form.email"></label>
|
||||
<input formControlName="email" name="email" type="email" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group">
|
||||
<label translate="user-profile-screen.form.first-name"></label>
|
||||
<input formControlName="firstName" name="firstName" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group">
|
||||
<label translate="user-profile-screen.form.last-name"></label>
|
||||
<input formControlName="lastName" name="lastName" type="text" />
|
||||
</div>
|
||||
<div class="iqser-input-group">
|
||||
<label translate="top-bar.navigation-items.my-account.children.language.label"></label>
|
||||
<mat-select formControlName="language">
|
||||
<mat-option *ngFor="let language of languages" [value]="language">
|
||||
{{ translations[language] | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<div class="iqser-input-group">
|
||||
<a [href]="changePasswordUrl" target="_blank"> {{ 'user-profile-screen.actions.change-password' | translate }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="formGroup.invalid || !(profileChanged || languageChanged)" color="primary" mat-flat-button type="submit">
|
||||
{{ 'user-profile-screen.actions.save' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,3 @@
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
@ -1,19 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { LanguageService } from '@i18n/language.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { languagesTranslations } from '../../translations/languages-translations';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { LoadingService } from '@iqser/common-ui';
|
||||
import { IProfile } from '@red/domain';
|
||||
import { languagesTranslations } from '../../../translations/languages-translations';
|
||||
import { PermissionsService } from '../../../../../services/permissions.service';
|
||||
import { UserService } from '../../../../../services/user.service';
|
||||
import { ConfigService } from '../../../../../services/config.service';
|
||||
import { LanguageService } from '../../../../../i18n/language.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-user-profile-screen',
|
||||
templateUrl: './user-profile-screen.component.html',
|
||||
styleUrls: ['./user-profile-screen.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserProfileScreenComponent implements OnInit {
|
||||
formGroup: FormGroup;
|
||||
@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
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';
|
||||
|
||||
const routes = [{ path: '', component: UserProfileScreenComponent }];
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserProfileScreenComponent],
|
||||
imports: [RouterModule.forChild(routes), CommonModule, SharedModule],
|
||||
})
|
||||
export class UserProfileModule {}
|
||||
@ -0,0 +1,31 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { GenericService } from '@iqser/common-ui';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { EmailNotificationScheduleTypes, INotificationPreferences } from '@red/domain';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationPreferencesService extends GenericService<INotificationPreferences> {
|
||||
constructor(protected readonly _injector: Injector, private readonly _userService: UserService) {
|
||||
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,
|
||||
emailNotifications: [],
|
||||
emailNotificationsEnabled: false,
|
||||
inAppNotifications: [],
|
||||
inAppNotificationsEnabled: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
export const notificationsTranslations: { [key: string]: string } = {
|
||||
daily: _('notifications-screen.schedule.instant'),
|
||||
daily_summary: _('notifications-screen.schedule.daily'),
|
||||
weekly_summary: _('notifications-screen.schedule.weekly'),
|
||||
inAppNotifications: _('notifications-screen.category.in-app-notifications'),
|
||||
emailNotifications: _('notifications-screen.category.email-notifications'),
|
||||
documentIsSentForApproval: _('notifications-screen.options.document-is-sent-for-approval'),
|
||||
documentStatusChanges: _('notifications-screen.options.document-status-changes'),
|
||||
dossierStatusChanges: _('notifications-screen.options.dossier-status-changes'),
|
||||
requestToJoinTheDossier: _('notifications-screen.options.request-to-join-the-dossier'),
|
||||
whenADocumentIsApproved: _('notifications-screen.options.when-a-document-is-approved'),
|
||||
whenADocumentIsAssignedToAReviewer: _('notifications-screen.options.when-a-document-is-assigned-to-a-reviewer'),
|
||||
whenADocumentIsSentForApproval: _('notifications-screen.options.when-a-document-is-sent-for-approval'),
|
||||
whenAReviewerIsUnassignedFromADocument: _('notifications-screen.options.when-a-reviewer-is-unassigned-from-a-document'),
|
||||
whenIAmAssignedOnADocument: _('notifications-screen.options.when-i-am-assigned-on-a-document'),
|
||||
whenIAmUnassignedFromADocument: _('notifications-screen.options.when-i-am-unassigned-from-a-document'),
|
||||
approver: _('notifications-screen.groups.approver'),
|
||||
own: _('notifications-screen.groups.own'),
|
||||
reviewer: _('notifications-screen.groups.reviewer'),
|
||||
notifications: _('notifications-screen.title'),
|
||||
'user-profile': _('user-profile-screen.title'),
|
||||
} as const;
|
||||
@ -15,14 +15,14 @@ import { UserListingScreenComponent } from './screens/user-listing/user-listing-
|
||||
import { LicenseInformationScreenComponent } from './screens/license-information/license-information-screen.component';
|
||||
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
|
||||
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ReportsScreenComponent } from './screens/reports/reports-screen.component';
|
||||
import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component';
|
||||
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';
|
||||
|
||||
const routes = [
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
|
||||
{
|
||||
path: 'dossier-templates',
|
||||
|
||||
@ -45,7 +45,7 @@ import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.
|
||||
import { LicenseReportService } from './services/licence-report.service';
|
||||
import { RulesService } from './services/rules.service';
|
||||
import { SmtpConfigService } from './services/smtp-config.service';
|
||||
import { WatermarkService } from './services/watermark.service';
|
||||
import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
|
||||
|
||||
const dialogs = [
|
||||
AddEditDossierTemplateDialogComponent,
|
||||
@ -58,6 +58,7 @@ const dialogs = [
|
||||
ConfirmDeleteUsersDialogComponent,
|
||||
FileAttributesCsvImportDialogComponent,
|
||||
AddEditDossierAttributeDialogComponent,
|
||||
UploadDictionaryDialogComponent,
|
||||
];
|
||||
|
||||
const screens = [
|
||||
@ -96,15 +97,7 @@ const components = [
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
providers: [
|
||||
AdminDialogService,
|
||||
AuditService,
|
||||
DigitalSignatureService,
|
||||
LicenseReportService,
|
||||
RulesService,
|
||||
SmtpConfigService,
|
||||
WatermarkService,
|
||||
],
|
||||
providers: [AdminDialogService, AuditService, DigitalSignatureService, LicenseReportService, RulesService, SmtpConfigService],
|
||||
imports: [CommonModule, SharedModule, AdminRoutingModule, NgxChartsModule, ColorPickerModule, MonacoEditorModule],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@ -6,6 +6,7 @@ import { CircleButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { HttpStatusCode } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-template-actions',
|
||||
@ -57,7 +58,7 @@ export class DossierTemplateActionsComponent implements OnInit {
|
||||
await this._router.navigate(['main', 'admin']);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 409) {
|
||||
if (error.status === HttpStatusCode.Conflict) {
|
||||
this._toaster.error(_('dossier-templates-listing.error.conflict'));
|
||||
} else {
|
||||
this._toaster.error(_('dossier-templates-listing.error.generic'));
|
||||
|
||||
@ -7,16 +7,16 @@
|
||||
<div class="dialog-content">
|
||||
<div class="iqser-input-group mb-14">
|
||||
<label translate="add-edit-dictionary.form.technical-name"></label>
|
||||
<div class="technical-name">{{ dictionary?.type || technicalName || '-' }}</div>
|
||||
<div class="technical-name">{{ dictionary?.type || (technicalName$ | async) || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!dictionary" class="iqser-input-group mb-14">
|
||||
<div *ngIf="(canEditLabel$ | async) === false" class="iqser-input-group mb-14">
|
||||
<label translate="add-edit-dictionary.form.name"></label>
|
||||
{{ dictionary.label }}
|
||||
</div>
|
||||
|
||||
<div class="first-row">
|
||||
<div *ngIf="!dictionary" class="iqser-input-group required">
|
||||
<div *ngIf="canEditLabel$ | async" class="iqser-input-group required">
|
||||
<label translate="add-edit-dictionary.form.name"></label>
|
||||
<input
|
||||
[placeholder]="'add-edit-dictionary.form.name-placeholder' | translate"
|
||||
@ -53,7 +53,7 @@
|
||||
[style.background]="form.get('hexColor').value"
|
||||
class="input-icon"
|
||||
>
|
||||
<mat-icon *ngIf="hasColor" svgIcon="red:color-picker"></mat-icon>
|
||||
<mat-icon *ngIf="hasColor$ | async" svgIcon="red:color-picker"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,39 +1,48 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
|
||||
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 { DictionaryService } from '@shared/services/dictionary.service';
|
||||
import { Dictionary, IDictionary } from '@red/domain';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { HttpStatusCode } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-add-edit-dictionary-dialog',
|
||||
templateUrl: './add-edit-dictionary-dialog.component.html',
|
||||
styleUrls: ['./add-edit-dictionary-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
form: FormGroup;
|
||||
readonly dictionary: Dictionary;
|
||||
technicalName = '';
|
||||
private readonly _dossierTemplateId: string;
|
||||
readonly form: FormGroup;
|
||||
readonly dictionary = this._data.dictionary;
|
||||
readonly canEditLabel$ = this._canEditLabel$;
|
||||
readonly technicalName$: Observable<string>;
|
||||
readonly dialogHeader = this._translateService.instant('add-edit-dictionary.title', {
|
||||
type: this._data.dictionary ? 'edit' : 'create',
|
||||
name: this._data.dictionary?.label,
|
||||
});
|
||||
readonly hasColor$: Observable<boolean>;
|
||||
private readonly _dossierTemplateId = this._data.dossierTemplateId;
|
||||
|
||||
constructor(
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
readonly userService: UserService,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
private readonly _data: { dictionary: Dictionary; dossierTemplateId: string },
|
||||
private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string },
|
||||
) {
|
||||
super();
|
||||
this.dictionary = _data.dictionary;
|
||||
this._dossierTemplateId = _data.dossierTemplateId;
|
||||
this.form = _formBuilder.group({
|
||||
label: [this.dictionary?.label, [Validators.required, Validators.minLength(3)]],
|
||||
description: [this.dictionary?.description],
|
||||
@ -43,21 +52,8 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
addToDictionaryAction: [!!this.dictionary?.addToDictionaryAction],
|
||||
caseSensitive: [this.dictCaseSensitive],
|
||||
});
|
||||
this.form.get('label').valueChanges.subscribe(() => {
|
||||
this._updateTechnicalName();
|
||||
});
|
||||
}
|
||||
|
||||
get dialogHeader(): string {
|
||||
return this._translateService.instant('add-edit-dictionary.title', {
|
||||
type: this.dictionary ? 'edit' : 'create',
|
||||
name: this.dictionary?.label,
|
||||
});
|
||||
}
|
||||
|
||||
get hasColor(): boolean {
|
||||
const hexColorValue = this.form.get('hexColor').value;
|
||||
return !hexColorValue || hexColorValue?.length === 0;
|
||||
this.hasColor$ = this._colorEmpty$;
|
||||
this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value)));
|
||||
}
|
||||
|
||||
get dictCaseSensitive(): boolean {
|
||||
@ -82,47 +78,58 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
private get _canEditLabel$() {
|
||||
return this.userService.currentUser$.pipe(
|
||||
map(user => user.isAdmin || !this._data.dictionary),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
}
|
||||
|
||||
private get _colorEmpty$() {
|
||||
return this.form.get('hexColor').valueChanges.pipe(map((value: string) => !value || value?.length === 0));
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const dictionary = this._formToObject();
|
||||
let observable: Observable<unknown>;
|
||||
const dossierTemplateId = this._data.dossierTemplateId;
|
||||
|
||||
if (this.dictionary) {
|
||||
// edit mode
|
||||
observable = this._dictionaryService.updateDictionary(dictionary, this._dossierTemplateId, dictionary.type);
|
||||
observable = this._dictionaryService.updateDictionary(dictionary, dossierTemplateId, dictionary.type);
|
||||
} else {
|
||||
// create mode
|
||||
observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId: this._dossierTemplateId });
|
||||
observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId });
|
||||
}
|
||||
|
||||
observable.subscribe(
|
||||
() => this._dialogRef.close(true),
|
||||
error => {
|
||||
if (error.status === 409) {
|
||||
return observable
|
||||
.toPromise()
|
||||
.then(() => this._dialogRef.close(true))
|
||||
.catch(error => {
|
||||
if (error.status === HttpStatusCode.Conflict) {
|
||||
this._toaster.error(_('add-edit-dictionary.error.dictionary-already-exists'));
|
||||
} else if (error.status === 400) {
|
||||
} else if (error.status === HttpStatusCode.BadRequest) {
|
||||
this._toaster.error(_('add-edit-dictionary.error.invalid-color-or-rank'));
|
||||
} else {
|
||||
this._toaster.error(_('add-edit-dictionary.error.generic'));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateTechnicalName() {
|
||||
const displayName = this.form.get('label').value.trim();
|
||||
private _toTechnicalName(value: string) {
|
||||
const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]);
|
||||
const baseTechnicalName: string = toKebabCase(displayName);
|
||||
const baseTechnicalName = toKebabCase(value.trim());
|
||||
let technicalName = baseTechnicalName;
|
||||
let suffix = 1;
|
||||
while (existingTechnicalNames.includes(technicalName)) {
|
||||
technicalName = [baseTechnicalName, suffix++].join('-');
|
||||
}
|
||||
this.technicalName = technicalName;
|
||||
return technicalName;
|
||||
}
|
||||
|
||||
private _formToObject(): IDictionary {
|
||||
return {
|
||||
type: this.dictionary?.type || this.technicalName,
|
||||
type: this.dictionary?.type || this._toTechnicalName(this.form.get('label').value),
|
||||
label: this.form.get('label').value,
|
||||
caseInsensitive: !this.form.get('caseSensitive').value,
|
||||
description: this.form.get('description').value,
|
||||
@ -130,7 +137,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
hint: this.form.get('hint').value,
|
||||
rank: this.form.get('rank').value,
|
||||
addToDictionaryAction: this.form.get('addToDictionaryAction').value,
|
||||
dossierTemplateId: this._dossierTemplateId,
|
||||
dossierTemplateId: this._data.dossierTemplateId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,10 @@ 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, CONFLICT_ERROR_CODE, Toaster } from '@iqser/common-ui';
|
||||
import { BaseDialogComponent, 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';
|
||||
|
||||
@Component({
|
||||
templateUrl: './add-edit-dossier-template-dialog.component.html',
|
||||
@ -108,7 +109,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
|
||||
this.dialogRef.close(true);
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error.status === CONFLICT_ERROR_CODE
|
||||
error.status === HttpStatusCode.Conflict
|
||||
? _('add-edit-dossier-template.error.conflict')
|
||||
: _('add-edit-dossier-template.error.generic');
|
||||
this._toaster.error(message, { error });
|
||||
|
||||
@ -6,6 +6,7 @@ import { rolesTranslations } from '../../../../../translations/roles-translation
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-user-details',
|
||||
@ -119,7 +120,7 @@ export class UserDetailsComponent implements OnInit {
|
||||
this.closeDialog.emit(true);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 409) {
|
||||
if (error.status === HttpStatusCode.Conflict) {
|
||||
this._toaster.error(_('add-edit-user.error.email-already-used'));
|
||||
} else {
|
||||
this._toaster.error(_('add-edit-user.error.generic'));
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { Component, EventEmitter, forwardRef, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { Field } from '../file-attributes-csv-import-dialog.component';
|
||||
import { CircleButtonTypes, DefaultListingServices, ListingComponent, TableColumnConfig } from '@iqser/common-ui';
|
||||
import { fileAttributeTypesTranslations } from '../../../translations/file-attribute-types-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileAttributeConfigTypes } from '@red/domain';
|
||||
import { FileAttributeConfigTypes, IField } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-active-fields-listing',
|
||||
@ -11,11 +10,11 @@ import { FileAttributeConfigTypes } from '@red/domain';
|
||||
styleUrls: ['./active-fields-listing.component.scss'],
|
||||
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => ActiveFieldsListingComponent) }],
|
||||
})
|
||||
export class ActiveFieldsListingComponent extends ListingComponent<Field> implements OnChanges {
|
||||
export class ActiveFieldsListingComponent extends ListingComponent<IField> implements OnChanges {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly translations = fileAttributeTypesTranslations;
|
||||
readonly tableHeaderLabel = _('file-attributes-csv-import.table-header.title');
|
||||
readonly tableColumnConfigs: TableColumnConfig<Field>[] = [
|
||||
readonly tableColumnConfigs: TableColumnConfig<IField>[] = [
|
||||
{
|
||||
label: _('file-attributes-csv-import.table-col-names.name'),
|
||||
class: 'name',
|
||||
@ -40,10 +39,10 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
},
|
||||
];
|
||||
readonly typeOptions = Object.keys(FileAttributeConfigTypes);
|
||||
@Input() entities: Field[];
|
||||
@Output() readonly entitiesChange = new EventEmitter<Field[]>();
|
||||
@Input() entities: IField[];
|
||||
@Output() readonly entitiesChange = new EventEmitter<IField[]>();
|
||||
@Output() readonly setHoveredColumn = new EventEmitter<string>();
|
||||
@Output() readonly toggleFieldActive = new EventEmitter<Field>();
|
||||
@Output() readonly toggleFieldActive = new EventEmitter<IField>();
|
||||
|
||||
constructor(protected readonly _injector: Injector) {
|
||||
super(_injector);
|
||||
@ -56,8 +55,8 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
}
|
||||
|
||||
deactivateSelection() {
|
||||
this.allEntities.filter(field => this.isSelected(field)).forEach(field => (field.primaryAttribute = false));
|
||||
this.entitiesService.setEntities(this.allEntities.filter(field => !this.isSelected(field)));
|
||||
this.allEntities.filter(field => this.listingService.isSelected(field)).forEach(field => (field.primaryAttribute = false));
|
||||
this.entitiesService.setEntities(this.allEntities.filter(field => !this.listingService.isSelected(field)));
|
||||
this.entitiesChange.emit(this.allEntities);
|
||||
this.listingService.setSelected([]);
|
||||
}
|
||||
@ -68,7 +67,7 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
}
|
||||
}
|
||||
|
||||
togglePrimary(field: Field) {
|
||||
togglePrimary(field: IField) {
|
||||
if (field.primaryAttribute) {
|
||||
field.primaryAttribute = false;
|
||||
return;
|
||||
@ -80,6 +79,6 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
field.primaryAttribute = true;
|
||||
}
|
||||
|
||||
itemMouseEnterFn = (field: Field) => this.setHoveredColumn.emit(field.csvColumn);
|
||||
itemMouseEnterFn = (field: IField) => this.setHoveredColumn.emit(field.csvColumn);
|
||||
itemMouseLeaveFn = () => this.setHoveredColumn.emit();
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
(click)="toggleFieldActive(field)"
|
||||
(mouseenter)="setHoveredColumn(field.csvColumn)"
|
||||
(mouseleave)="setHoveredColumn()"
|
||||
*ngFor="let field of fields"
|
||||
*ngFor="let field of fields; trackBy: trackBy"
|
||||
class="csv-header-pill-wrapper"
|
||||
>
|
||||
<div [class.selected]="isActive(field)" class="csv-header-pill">
|
||||
|
||||
@ -1,33 +1,25 @@
|
||||
import { Component, Inject, Injector } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
|
||||
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 { map, startWith } from 'rxjs/operators';
|
||||
import { DefaultListingServices, IListable, ListingComponent, TableColumnConfig, Toaster } from '@iqser/common-ui';
|
||||
import { DefaultListingServices, ListingComponent, TableColumnConfig, Toaster, trackBy } from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileAttributeConfig, FileAttributeConfigType, FileAttributeConfigTypes, IFileAttributesConfig } from '@red/domain';
|
||||
import { FileAttributeConfig, FileAttributeConfigTypes, IField, IFileAttributesConfig } from '@red/domain';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
|
||||
export interface Field extends IListable {
|
||||
id: string;
|
||||
csvColumn: string;
|
||||
name: string;
|
||||
type: FileAttributeConfigType;
|
||||
readonly: boolean;
|
||||
primaryAttribute: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './file-attributes-csv-import-dialog.component.html',
|
||||
styleUrls: ['./file-attributes-csv-import-dialog.component.scss'],
|
||||
providers: [...DefaultListingServices],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileAttributesCsvImportDialogComponent extends ListingComponent<Field> {
|
||||
readonly tableColumnConfigs: TableColumnConfig<Field>[] = [];
|
||||
parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
|
||||
export class FileAttributesCsvImportDialogComponent extends ListingComponent<IField> {
|
||||
readonly tableColumnConfigs: TableColumnConfig<IField>[] = [];
|
||||
parseResult: { data: any[]; errors: any[]; meta: any; fields: IField[] };
|
||||
hoveredColumn: string;
|
||||
activeFields: Field[] = [];
|
||||
activeFields: IField[] = [];
|
||||
readonly baseConfigForm: FormGroup;
|
||||
isSearchOpen = false;
|
||||
previewExpanded = true;
|
||||
@ -36,6 +28,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
columnSample = [];
|
||||
initialParseConfig: { delimiter?: string; encoding?: string } = {};
|
||||
readonly tableHeaderLabel = '';
|
||||
readonly trackBy = trackBy();
|
||||
|
||||
constructor(
|
||||
private readonly _toaster: Toaster,
|
||||
@ -87,7 +80,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
this.activeFields = [];
|
||||
|
||||
for (const entity of this.allEntities) {
|
||||
const existing = this.data.existingConfiguration.fileAttributeConfigs.find(a => a.csvColumnHeader === entity.csvColumn);
|
||||
const existing = this.data.existingConfiguration.fileAttributeConfigs?.find(a => a.csvColumnHeader === entity.csvColumn);
|
||||
if (existing) {
|
||||
entity.id = existing.id;
|
||||
entity.name = existing.label;
|
||||
@ -142,21 +135,20 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
return 0;
|
||||
}
|
||||
|
||||
isActive(field: Field): boolean {
|
||||
isActive(field: IField): boolean {
|
||||
return !!this.activeFields.find(f => f.id === field.id);
|
||||
}
|
||||
|
||||
toggleFieldActive(field: Field) {
|
||||
toggleFieldActive(field: IField) {
|
||||
if (!this.isActive(field)) {
|
||||
this.activeFields = [...this.activeFields, { ...field, searchKey: field.csvColumn }];
|
||||
} else {
|
||||
this.activeFields.splice(this.activeFields.indexOf(field), 1);
|
||||
this.activeFields = [...this.activeFields];
|
||||
this.activeFields = this.activeFields.filter(f => f.id !== field.id);
|
||||
}
|
||||
}
|
||||
|
||||
activateAll() {
|
||||
this.activeFields = [...this.allEntities.map(item => ({ ...item, searchKey: item.csvColumn }))];
|
||||
this.activeFields = this.allEntities.map(item => ({ ...item, searchKey: item.csvColumn }));
|
||||
}
|
||||
|
||||
deactivateAll() {
|
||||
@ -165,12 +157,12 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
|
||||
async save() {
|
||||
const newPrimary = !!this.activeFields.find(attr => attr.primaryAttribute);
|
||||
let fileAttributeConfigs = this.data.existingConfiguration.fileAttributeConfigs;
|
||||
let fileAttributeConfigs = this.data.existingConfiguration.fileAttributeConfigs ?? [];
|
||||
if (newPrimary) {
|
||||
fileAttributeConfigs = fileAttributeConfigs.map(attr => new FileAttributeConfig({ ...attr, primaryAttribute: false }));
|
||||
}
|
||||
|
||||
const fileAttributes = {
|
||||
const fileAttributes: IFileAttributesConfig = {
|
||||
...this.baseConfigForm.getRawValue(),
|
||||
fileAttributeConfigs: [
|
||||
...fileAttributeConfigs.filter(a => !this.allEntities.find(entity => entity.csvColumn === a.csvColumnHeader)),
|
||||
@ -181,12 +173,14 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
label: field.name,
|
||||
type: field.type,
|
||||
primaryAttribute: field.primaryAttribute,
|
||||
displayedInFileList: null,
|
||||
filterable: null,
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await this._fileAttributesService.setFileAttributesConfig(fileAttributes, this.data.dossierTemplateId).toPromise();
|
||||
await this._fileAttributesService.setFileAttributeConfig(fileAttributes, this.data.dossierTemplateId).toPromise();
|
||||
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'));
|
||||
@ -219,7 +213,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
};
|
||||
}
|
||||
|
||||
private _buildAttribute(csvColumn: string): Field {
|
||||
private _buildAttribute(csvColumn: string): IField {
|
||||
const sample = this.getSample(csvColumn);
|
||||
const isNumber = sample && !isNaN(sample);
|
||||
return {
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<section class="dialog">
|
||||
<div class="dialog-header heading-l" translate="upload-dictionary-dialog.title"></div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<p translate="upload-dictionary-dialog.question"></p>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
(click)="selectOption('overwrite')"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
translate="upload-dictionary-dialog.options.overwrite"
|
||||
></button>
|
||||
<iqser-icon-button
|
||||
(action)="selectOption('merge')"
|
||||
[label]="'upload-dictionary-dialog.options.merge' | translate"
|
||||
[type]="iconButtonTypes.dark"
|
||||
></iqser-icon-button>
|
||||
<div (click)="cancel()" class="all-caps-label cancel" translate="upload-dictionary-dialog.options.cancel"></div>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,11 @@
|
||||
.dialog-header {
|
||||
color: var(--iqser-primary);
|
||||
}
|
||||
|
||||
.dialog-actions > *:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { IconButtonTypes } from '@iqser/common-ui';
|
||||
|
||||
@Component({
|
||||
templateUrl: './upload-dictionary-dialog.component.html',
|
||||
styleUrls: ['./upload-dictionary-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UploadDictionaryDialogComponent {
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
|
||||
constructor(private readonly _translateService: TranslateService, public dialogRef: MatDialogRef<UploadDictionaryDialogComponent>) {}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
selectOption(option: 'overwrite' | 'merge') {
|
||||
this.dialogRef.close({ option });
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { DefaultColorType, IColors } from '@red/domain';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import {
|
||||
CircleButtonTypes,
|
||||
@ -43,7 +42,6 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> imp
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _dialogService: AdminDialogService,
|
||||
|
||||
@ -3,7 +3,6 @@ import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/si
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { catchError, defaultIfEmpty, tap } from 'rxjs/operators';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
CircleButtonTypes,
|
||||
@ -47,7 +46,6 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
@ -117,7 +115,7 @@ export class DictionaryListingScreenComponent extends ListingComponent<Dictionar
|
||||
}
|
||||
|
||||
const dataObs = this.allEntities.map(dict =>
|
||||
this._dictionaryService.getFor(this._dossierTemplatesService.activeDossierTemplateId, dict.type).pipe(
|
||||
this._dictionaryService.getForType(this._dossierTemplatesService.activeDossierTemplateId, dict.type).pipe(
|
||||
tap(values => (dict.entries = [...values.entries] ?? [])),
|
||||
catchError(() => {
|
||||
dict.entries = [];
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
(saveDictionary)="saveEntries($event)"
|
||||
[canEdit]="currentUser.isAdmin"
|
||||
[filterByDossierTemplate]="true"
|
||||
[initialEntries]="entries"
|
||||
[initialEntries]="initialEntries"
|
||||
></redaction-dictionary-manager>
|
||||
|
||||
<div *ngIf="!!dictionary" class="right-container">
|
||||
|
||||
@ -20,7 +20,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly currentUser = this._userService.currentUser;
|
||||
|
||||
entries: string[] = [];
|
||||
initialEntries: string[] = [];
|
||||
dictionary: Dictionary;
|
||||
|
||||
@ViewChild('dictionaryManager', { static: false })
|
||||
@ -98,7 +98,18 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
|
||||
|
||||
if (file) {
|
||||
fileReader.onload = () => {
|
||||
this._dictionaryManager.editor.value = fileReader.result as string;
|
||||
const fileContent = fileReader.result as string;
|
||||
if (this._dictionaryManager.editor.value) {
|
||||
this._dialogService.openDialog('uploadDictionary', null, null, ({ option }) => {
|
||||
if (option === 'overwrite') {
|
||||
this._overwrite(fileContent);
|
||||
} else if (option === 'merge') {
|
||||
this._merge(fileContent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._overwrite(fileContent);
|
||||
}
|
||||
this._fileInput.nativeElement.value = null;
|
||||
};
|
||||
fileReader.readAsText(file);
|
||||
@ -107,38 +118,55 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
|
||||
|
||||
saveEntries(entries: string[]) {
|
||||
this._loadingService.start();
|
||||
this._dictionaryService.saveEntries(entries, this.entries, this.dictionary.dossierTemplateId, this.dictionary.type, null).subscribe(
|
||||
async () => {
|
||||
await this._loadEntries();
|
||||
},
|
||||
() => {
|
||||
this._loadingService.stop();
|
||||
},
|
||||
);
|
||||
this._dictionaryService
|
||||
.saveEntries(entries, this.initialEntries, this.dictionary.dossierTemplateId, this.dictionary.type, null)
|
||||
.subscribe(
|
||||
async () => {
|
||||
await this._loadEntries();
|
||||
},
|
||||
() => {
|
||||
this._loadingService.stop();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._appStateService.reset();
|
||||
}
|
||||
|
||||
private _overwrite(fileContent: string): void {
|
||||
this._dictionaryManager.editor.value = fileContent;
|
||||
}
|
||||
|
||||
private _merge(fileContent: string): void {
|
||||
const currentEntries = this._dictionaryManager.editor.value.split('\n');
|
||||
fileContent
|
||||
.split('\n')
|
||||
.filter(entry => !currentEntries.includes(entry))
|
||||
.forEach(entry => currentEntries.push(entry));
|
||||
this._dictionaryManager.editor.value = currentEntries.join('\n');
|
||||
}
|
||||
|
||||
private async _loadEntries() {
|
||||
this._loadingService.start();
|
||||
await this._dictionaryService
|
||||
.getFor(this.dictionary.dossierTemplateId, this.dictionary.type)
|
||||
.getForType(this.dictionary.dossierTemplateId, this.dictionary.type)
|
||||
.toPromise()
|
||||
.then(
|
||||
data => {
|
||||
this._loadingService.stop();
|
||||
this.entries = [...data.entries].sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' }));
|
||||
this.initialEntries = [...data.entries].sort((str1, str2) =>
|
||||
str1.localeCompare(str2, undefined, { sensitivity: 'accent' }),
|
||||
);
|
||||
},
|
||||
() => {
|
||||
this._loadingService.stop();
|
||||
this.entries = [];
|
||||
this.initialEntries = [];
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
this._loadingService.stop();
|
||||
this.entries = [];
|
||||
this.initialEntries = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,6 @@ form {
|
||||
|
||||
/* target the input field inexplicably to throw Chrome's AI off.
|
||||
* feel free to use a more complicated selector */
|
||||
input[name='keySecret'] {
|
||||
input[name='keySecret']:not(:placeholder-shown) {
|
||||
font-family: 'secret';
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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 { HttpStatusCode } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-digital-signature-screen',
|
||||
@ -55,7 +56,7 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements
|
||||
this._toaster.success(_('digital-signature-screen.action.save-success'));
|
||||
},
|
||||
error => {
|
||||
if (error.status === 400) {
|
||||
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'));
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
TableColumnConfig,
|
||||
} from '@iqser/common-ui';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
|
||||
import { dossierAttributeTypesTranslations } from '../../translations/dossier-attribute-types-translations';
|
||||
@ -43,7 +42,6 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dialogService: AdminDialogService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _dossierAttributesService: DossierAttributesService,
|
||||
|
||||
@ -78,13 +78,15 @@
|
||||
<div class="cell user-column">
|
||||
<redaction-initials-avatar
|
||||
[defaultValue]="'unknown' | translate"
|
||||
[user]="dossierTemplate.createdBy"
|
||||
[user]="dossierTemplate.createdBy || 'system'"
|
||||
[withName]="true"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cell small-label">
|
||||
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
|
||||
<div class="cell">
|
||||
<div class="small-label">
|
||||
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
|
||||
@ -17,6 +17,7 @@ import { UserService } from '@services/user.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
templateUrl: './dossier-templates-listing-screen.component.html',
|
||||
@ -89,7 +90,7 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
|
||||
.delete(templateIds)
|
||||
.toPromise()
|
||||
.catch(error => {
|
||||
if (error.status === 409) {
|
||||
if (error.status === HttpStatusCode.Conflict) {
|
||||
this._toaster.error(_('dossier-templates-listing.error.conflict'));
|
||||
} else {
|
||||
this._toaster.error(_('dossier-templates-listing.error.generic'));
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import {
|
||||
CircleButtonTypes,
|
||||
@ -9,6 +8,7 @@ import {
|
||||
ListingComponent,
|
||||
LoadingService,
|
||||
TableColumnConfig,
|
||||
Toaster,
|
||||
} from '@iqser/common-ui';
|
||||
import { fileAttributeTypesTranslations } from '../../translations/file-attribute-types-translations';
|
||||
import { UserService } from '@services/user.service';
|
||||
@ -16,6 +16,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileAttributeConfig, IFileAttributeConfig, IFileAttributesConfig } from '@red/domain';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { HttpStatusCode } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
templateUrl: './file-attributes-listing-screen.component.html',
|
||||
@ -51,8 +52,8 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
|
||||
|
||||
constructor(
|
||||
protected readonly _injector: Injector,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
@ -71,13 +72,8 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
|
||||
'addEditFileAttribute',
|
||||
$event,
|
||||
{ fileAttribute, dossierTemplateId: this._dossierTemplatesService.activeDossierTemplateId },
|
||||
async (newValue: IFileAttributeConfig) => {
|
||||
this._loadingService.start();
|
||||
await this._fileAttributesService
|
||||
.setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId)
|
||||
.toPromise();
|
||||
await this._appStateService.refreshDossierTemplate(this._dossierTemplatesService.activeDossierTemplateId);
|
||||
await this._loadData();
|
||||
(newValue: IFileAttributeConfig) => {
|
||||
this._loadingService.loadWhile(this._createNewFileAttributeAndRefreshView(newValue));
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -117,6 +113,23 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
|
||||
);
|
||||
}
|
||||
|
||||
private async _createNewFileAttributeAndRefreshView(newValue: IFileAttributeConfig): Promise<void> {
|
||||
await this._fileAttributesService
|
||||
.setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId)
|
||||
.toPromise()
|
||||
.catch(error => {
|
||||
console.log('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();
|
||||
}
|
||||
|
||||
private async _loadData() {
|
||||
this._loadingService.start();
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
ConfirmationDialogInput,
|
||||
DialogConfig,
|
||||
DialogService,
|
||||
ListingService,
|
||||
LoadingService,
|
||||
TitleColors,
|
||||
} from '@iqser/common-ui';
|
||||
@ -38,7 +37,7 @@ export class JustificationsDialogService extends DialogService<DialogType> {
|
||||
super(_dialog);
|
||||
}
|
||||
|
||||
confirmDelete(justifications: Justification[], listingService: ListingService<Justification>) {
|
||||
confirmDelete(justifications: Justification[]) {
|
||||
const data = new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.delete-justification.title'),
|
||||
titleColor: TitleColors.WARN,
|
||||
|
||||
@ -59,6 +59,6 @@ export class JustificationsScreenComponent extends ListingComponent<Justificatio
|
||||
}
|
||||
|
||||
openConfirmDeleteDialog() {
|
||||
this._dialogService.confirmDelete(this.listingService.selected, this.listingService);
|
||||
this._dialogService.confirmDelete(this.listingService.selected);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,6 @@ export class TableItemComponent {
|
||||
}
|
||||
|
||||
openConfirmDeleteDialog() {
|
||||
this._dialogService.confirmDelete([this.justification], this._listingService);
|
||||
this._dialogService.confirmDelete([this.justification]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { IPlaceholdersResponse, IReportTemplate } from '@red/domain';
|
||||
import { download } from '@utils/file-download-utils';
|
||||
@ -36,7 +35,6 @@ export class ReportsScreenComponent implements OnInit {
|
||||
@ViewChild('fileInput') private _fileInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _reportTemplateService: ReportTemplateService,
|
||||
|
||||
@ -4,7 +4,6 @@ import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/commo
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
@ -44,7 +43,6 @@ export class RulesScreenComponent extends ComponentHasChanges implements OnInit
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _toaster: Toaster,
|
||||
protected readonly _translateService: TranslateService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _loadingService: LoadingService,
|
||||
) {
|
||||
super(_translateService);
|
||||
|
||||
@ -6,12 +6,11 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
|
||||
import { IWatermark, WatermarkOrientation, WatermarkOrientations } from '@red/domain';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BASE_HREF } from '../../../../tokens';
|
||||
import { stampPDFPage } from '@utils/page-stamper';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { WatermarkService } from '../../services/watermark.service';
|
||||
import { WatermarkService } from '../../../shared/services/watermark.service';
|
||||
|
||||
export const DEFAULT_WATERMARK: IWatermark = {
|
||||
text: null,
|
||||
@ -43,7 +42,6 @@ export class WatermarkScreenComponent implements OnInit {
|
||||
private readonly _http: HttpClient,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
) {
|
||||
|
||||
@ -12,6 +12,7 @@ import { FileAttributesCsvImportDialogComponent } from '../dialogs/file-attribut
|
||||
import { AddEditDossierAttributeDialogComponent } from '../dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component';
|
||||
import { ConfirmationDialogComponent, DialogConfig, DialogService, largeDialogConfig } from '@iqser/common-ui';
|
||||
import { AddEditJustificationDialogComponent } from '../screens/justifications/add-edit-justification-dialog/add-edit-justification-dialog.component';
|
||||
import { UploadDictionaryDialogComponent } from '../dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
|
||||
|
||||
type DialogType =
|
||||
| 'confirm'
|
||||
@ -25,7 +26,8 @@ type DialogType =
|
||||
| 'smtpAuthConfig'
|
||||
| 'addEditDossierTemplate'
|
||||
| 'addEditDossierAttribute'
|
||||
| 'addEditJustification';
|
||||
| 'addEditJustification'
|
||||
| 'uploadDictionary';
|
||||
|
||||
@Injectable()
|
||||
export class AdminDialogService extends DialogService<DialogType> {
|
||||
@ -76,6 +78,9 @@ export class AdminDialogService extends DialogService<DialogType> {
|
||||
component: AddEditJustificationDialogComponent,
|
||||
dialogConfig: { autoFocus: true },
|
||||
},
|
||||
uploadDictionary: {
|
||||
component: UploadDictionaryDialogComponent,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(protected readonly _dialog: MatDialog) {
|
||||
|
||||
@ -5,7 +5,6 @@ import { HttpClientModule } from '@angular/common/http';
|
||||
import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { BASE_HREF } from '../../tokens';
|
||||
import { environment } from '@environments/environment';
|
||||
|
||||
function getKeycloakOptions(configService: ConfigService, baseUrl: string) {
|
||||
let url: string = configService.values.OAUTH_URL;
|
||||
@ -21,9 +20,7 @@ function getKeycloakOptions(configService: ConfigService, baseUrl: string) {
|
||||
initOptions: {
|
||||
checkLoginIframe: false,
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: environment.production
|
||||
? window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html'
|
||||
: null,
|
||||
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
|
||||
flow: 'standard',
|
||||
},
|
||||
enableBearerInterceptor: true,
|
||||
|
||||
@ -1,109 +1,142 @@
|
||||
<div *ngIf="canPerformAnnotationActions" [class.always-visible]="alwaysVisible" class="annotation-actions">
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.changeLegalBasis($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canChangeLegalBasis"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.edit-reason.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
<!-- Resize Mode for annotation -> only resize accept and deny actions are available-->
|
||||
<ng-container *ngIf="resizing">
|
||||
<iqser-circle-button
|
||||
(action)="acceptResize($event)"
|
||||
*ngIf="annotationPermissions.canResizeAnnotation && annotations.length === 1 && annotations[0].resizing"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize-accept.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.convertRecommendationToAnnotation($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canAcceptRecommendation"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-recommendation.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="cancelResize($event)"
|
||||
*ngIf="annotationPermissions.canResizeAnnotation && annotations.length === 1 && annotations[0].resizing"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize-cancel.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.acceptSuggestion($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canAcceptSuggestion"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-suggestion.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
<!-- Not resizing - standard actions -->
|
||||
<ng-container *ngIf="!resizing">
|
||||
<iqser-circle-button
|
||||
(action)="resize($event)"
|
||||
*ngIf="annotationPermissions.canResizeAnnotation && annotations.length === 1"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:resize"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.undoDirectAction($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canUndo"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.undo' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.changeLegalBasis($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canChangeLegalBasis"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.edit-reason.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.rejectSuggestion($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canRejectSuggestion"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.reject-suggestion' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.convertRecommendationToAnnotation($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canAcceptRecommendation"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-recommendation.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.recategorizeImages($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canRecategorizeImage"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.recategorize-image' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-down"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.acceptSuggestion($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canAcceptSuggestion"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-suggestion.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceRedaction($event, annotations, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canForceRedaction"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.force-redaction.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-up"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.undoDirectAction($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canUndo"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.undo' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hideAnnotation($event)"
|
||||
*ngIf="isImage && isVisible"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.hide' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:visibility-off"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.rejectSuggestion($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canRejectSuggestion"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.reject-suggestion' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="showAnnotation($event)"
|
||||
*ngIf="isImage && !isVisible"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.show' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:visibility"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.recategorizeImages($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canRecategorizeImage"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.recategorize-image' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-down"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="suggestRemoveAnnotations($event, true)"
|
||||
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveFromDictionary"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.remove-from-dict' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:remove-from-dict"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceRedaction($event, annotations, dossier, annotationsChanged)"
|
||||
*ngIf="annotationPermissions.canForceRedaction"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.force-redaction.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-up"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="markAsFalsePositive($event)"
|
||||
*ngIf="annotationPermissions.canMarkAsFalsePositive"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.false-positive' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-down"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="hideAnnotation($event)"
|
||||
*ngIf="isImage && isVisible"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.hide' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:visibility-off"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="suggestRemoveAnnotations($event, false)"
|
||||
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.only-here' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="showAnnotation($event)"
|
||||
*ngIf="isImage && !isVisible"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.show' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:visibility"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="suggestRemoveAnnotations($event, true)"
|
||||
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveFromDictionary"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.remove-from-dict' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:remove-from-dict"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="markAsFalsePositive($event)"
|
||||
*ngIf="annotationPermissions.canMarkAsFalsePositive"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.false-positive' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:thumb-down"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="suggestRemoveAnnotations($event, false)"
|
||||
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.only-here' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { AnnotationPermissions } from '@models/file/annotation.permissions';
|
||||
import { AnnotationActionsService } from '../../services/annotation-actions.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { Dossier } from '@red/domain';
|
||||
import { Required } from '@iqser/common-ui';
|
||||
|
||||
export const AnnotationButtonTypes = {
|
||||
dark: 'dark',
|
||||
@ -19,17 +20,17 @@ export type AnnotationButtonType = keyof typeof AnnotationButtonTypes;
|
||||
templateUrl: './annotation-actions.component.html',
|
||||
styleUrls: ['./annotation-actions.component.scss'],
|
||||
})
|
||||
export class AnnotationActionsComponent implements OnInit {
|
||||
export class AnnotationActionsComponent implements OnChanges {
|
||||
@Input() buttonType: AnnotationButtonType = AnnotationButtonTypes.dark;
|
||||
@Input() tooltipPosition: 'before' | 'above' = 'before';
|
||||
@Input() canPerformAnnotationActions: boolean;
|
||||
@Input() viewer: WebViewerInstance;
|
||||
@Input() alwaysVisible: boolean;
|
||||
@Input() @Required() dossier!: Dossier;
|
||||
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
|
||||
annotationPermissions: AnnotationPermissions;
|
||||
|
||||
constructor(
|
||||
readonly appStateService: AppStateService,
|
||||
readonly annotationActionsService: AnnotationActionsService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
@ -44,7 +45,6 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
@Input()
|
||||
set annotations(value: AnnotationWrapper[]) {
|
||||
this._annotations = value.filter(a => a !== undefined);
|
||||
this._setPermissions();
|
||||
}
|
||||
|
||||
get viewerAnnotations() {
|
||||
@ -63,17 +63,27 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
return this.annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
get resizing() {
|
||||
return this.annotations?.length === 1 && this.annotations?.[0].resizing;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this._setPermissions();
|
||||
}
|
||||
|
||||
suggestRemoveAnnotations($event, removeFromDict: boolean) {
|
||||
$event.stopPropagation();
|
||||
this.annotationActionsService.suggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged);
|
||||
this.annotationActionsService.suggestRemoveAnnotation(
|
||||
$event,
|
||||
this.annotations,
|
||||
this.dossier,
|
||||
removeFromDict,
|
||||
this.annotationsChanged,
|
||||
);
|
||||
}
|
||||
|
||||
markAsFalsePositive($event) {
|
||||
this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged);
|
||||
this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.dossier, this.annotationsChanged);
|
||||
}
|
||||
|
||||
hideAnnotation($event: MouseEvent) {
|
||||
@ -90,9 +100,21 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, false);
|
||||
}
|
||||
|
||||
resize($event: MouseEvent) {
|
||||
this.annotationActionsService.resize($event, this.viewer, this.annotations[0]);
|
||||
}
|
||||
|
||||
acceptResize($event: MouseEvent) {
|
||||
this.annotationActionsService.acceptResize($event, this.viewer, this.dossier, this.annotations[0], this.annotationsChanged);
|
||||
}
|
||||
|
||||
cancelResize($event: MouseEvent) {
|
||||
this.annotationActionsService.cancelResize($event, this.viewer, this.annotations[0], this.annotationsChanged);
|
||||
}
|
||||
|
||||
private _setPermissions() {
|
||||
this.annotationPermissions = AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(),
|
||||
this._permissionsService.isApprover(this.dossier),
|
||||
this._userService.currentUser,
|
||||
this.annotations,
|
||||
);
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
<div *ngFor="let comment of annotation.comments; trackBy: trackBy" class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
<strong> {{ comment.user | name }} </strong>
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
<ng-container *ngIf="file$ | async as file">
|
||||
<div *ngFor="let comment of annotation.comments; trackBy: trackBy" class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
<strong> {{ comment.user | name }} </strong>
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<iqser-circle-button
|
||||
(action)="deleteComment(comment)"
|
||||
*ngIf="permissionsService.canDeleteComment(comment, file)"
|
||||
[iconSize]="10"
|
||||
[size]="20"
|
||||
class="pointer"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<iqser-circle-button
|
||||
(action)="deleteComment(comment)"
|
||||
*ngIf="permissionsService.canDeleteComment(comment)"
|
||||
[iconSize]="10"
|
||||
[size]="20"
|
||||
class="pointer"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
<div>{{ comment.text }}</div>
|
||||
</div>
|
||||
|
||||
<div>{{ comment.text }}</div>
|
||||
</div>
|
||||
|
||||
<iqser-input-with-action
|
||||
(action)="addComment($event)"
|
||||
*ngIf="permissionsService.canAddComment()"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
<iqser-input-with-action
|
||||
(action)="addComment($event)"
|
||||
*ngIf="permissionsService.canAddComment(file)"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
</ng-container>
|
||||
|
||||
<div (click)="toggleExpandComments($event)" class="all-caps-label pointer hide-comments" translate="comments.hide-comments"></div>
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, ViewChild } from '@angular/core';
|
||||
import { IComment } from '@red/domain';
|
||||
import { File, IComment } from '@red/domain';
|
||||
import { ManualAnnotationService } from '../../services/manual-annotation.service';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { InputWithActionComponent, trackBy } from '@iqser/common-ui';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-comments',
|
||||
@ -15,27 +18,37 @@ import { InputWithActionComponent, trackBy } from '@iqser/common-ui';
|
||||
export class CommentsComponent {
|
||||
@Input() annotation: AnnotationWrapper;
|
||||
readonly trackBy = trackBy();
|
||||
readonly file$: Observable<File>;
|
||||
@HostBinding('class.hidden') private _hidden = true;
|
||||
@ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent;
|
||||
private readonly _fileId: string;
|
||||
private readonly _dossierId: string;
|
||||
|
||||
constructor(
|
||||
readonly permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
) {}
|
||||
readonly filesMapService: FilesMapService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this._fileId = activatedRoute.snapshot.paramMap.get('fileId');
|
||||
this._dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.file$ = filesMapService.watch$(this._dossierId, this._fileId);
|
||||
}
|
||||
|
||||
addComment(value: string): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
this._manualAnnotationService
|
||||
.addComment(value, this.annotation.id)
|
||||
.addComment(value, this.annotation.id, this._dossierId, this._fileId)
|
||||
.toPromise()
|
||||
.then(commentId => {
|
||||
this.annotation.comments.push({
|
||||
text: value,
|
||||
id: commentId,
|
||||
annotationId: this.annotation.id,
|
||||
user: this._userService.currentUser.id,
|
||||
});
|
||||
this._input.reset();
|
||||
@ -50,7 +63,7 @@ export class CommentsComponent {
|
||||
|
||||
deleteComment(comment: IComment): void {
|
||||
this._manualAnnotationService
|
||||
.deleteComment(comment.id, this.annotation.id)
|
||||
.deleteComment(comment.id, this.annotation.id, this._dossierId, this._fileId)
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
|
||||
<div [matTooltip]="annotation.content" class="details" matTooltipPosition="above">
|
||||
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
|
||||
<redaction-type-annotation-icon [annotation]="annotation" [file]="file"></redaction-type-annotation-icon>
|
||||
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { IqserEventTarget } from '@iqser/common-ui';
|
||||
import { File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotations-list',
|
||||
@ -9,6 +10,7 @@ import { IqserEventTarget } from '@iqser/common-ui';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AnnotationsListComponent {
|
||||
@Input() file: File;
|
||||
@Input() annotations: AnnotationWrapper[];
|
||||
@Input() selectedAnnotations: AnnotationWrapper[];
|
||||
@Input() annotationActionsTemplate: TemplateRef<unknown>;
|
||||
|
||||
@ -5,11 +5,13 @@
|
||||
*ngIf="!multiSelectActive && !isReadOnly"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="file-preview.tabs.annotations.select"
|
||||
iqserHelpMode="bulk-select-annotations"
|
||||
></div>
|
||||
<iqser-popup-filter
|
||||
[actionsTemplate]="annotationFilterActionTemplate"
|
||||
[primaryFiltersSlug]="'primaryFilters'"
|
||||
[secondaryFiltersSlug]="'secondaryFilters'"
|
||||
iqserHelpMode="workload-filter"
|
||||
></iqser-popup-filter>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,17 +28,20 @@
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
<div *ngIf="isReadOnly" [class.justify-center]="!isProcessing" class="read-only d-flex">
|
||||
<div *ngIf="isProcessing" class="flex-align-items-center">
|
||||
<span [translate]="'file-status.processing'" class="read-only-text"></span>
|
||||
<mat-progress-bar [mode]="'indeterminate'" class="w-100"></mat-progress-bar>
|
||||
</div>
|
||||
<div *ngIf="isReadOnly" class="justify-center read-only d-flex">
|
||||
<div class="flex-center">
|
||||
<mat-icon class="primary-white" svgIcon="red:read-only"></mat-icon>
|
||||
<span class="read-only-text" translate="readonly"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isReadOnly && file.isProcessing" class="justify-center read-only d-flex">
|
||||
<div class="flex-align-items-center">
|
||||
<span [translate]="'file-status.processing'" class="read-only-text"></span>
|
||||
<mat-progress-bar [mode]="'indeterminate'" class="w-100"></mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="multiSelectActive" class="multi-select">
|
||||
<div class="selected-wrapper">
|
||||
<iqser-round-checkbox
|
||||
@ -51,6 +56,7 @@
|
||||
[alwaysVisible]="true"
|
||||
[annotations]="selectedAnnotations"
|
||||
[canPerformAnnotationActions]="!isReadOnly"
|
||||
[dossier]="dossier"
|
||||
[viewer]="viewer"
|
||||
buttonType="primary"
|
||||
tooltipPosition="above"
|
||||
@ -87,13 +93,15 @@
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[activeSelection]="pageHasSelection(pageNumber)"
|
||||
[active]="pageNumber === activeViewerPage"
|
||||
[file]="file"
|
||||
[number]="pageNumber"
|
||||
[viewedPages]="fileData?.viewedPages"
|
||||
[showDottedIcon]="hasOnlyManualRedactionsAndNotExcluded(pageNumber)"
|
||||
[viewedPages]="viewedPages"
|
||||
></redaction-page-indicator>
|
||||
</div>
|
||||
<div
|
||||
(click)="scrollQuickNavLast()"
|
||||
[class.disabled]="activeViewerPage === fileData?.file?.numberOfPages"
|
||||
[class.disabled]="activeViewerPage === file?.numberOfPages"
|
||||
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
|
||||
class="jump"
|
||||
matTooltipPosition="above"
|
||||
@ -104,11 +112,21 @@
|
||||
|
||||
<div style="overflow: hidden; width: 100%">
|
||||
<ng-container *ngIf="!excludePages">
|
||||
<div [attr.anotation-page-header]="activeViewerPage" class="page-separator">
|
||||
<span *ngIf="!!activeViewerPage" class="all-caps-label">
|
||||
<span translate="page"></span> {{ activeViewerPage }} -
|
||||
{{ activeAnnotations?.length || 0 }}
|
||||
<span [translate]="activeAnnotations?.length === 1 ? 'annotation' : 'annotations'"></span>
|
||||
<div [attr.anotation-page-header]="activeViewerPage" [class.padding-left-0]="currentPageIsExcluded" class="page-separator">
|
||||
<span *ngIf="!!activeViewerPage" class="flex-align-items-center">
|
||||
<iqser-circle-button
|
||||
(action)="viewExcludePages()"
|
||||
*ngIf="currentPageIsExcluded"
|
||||
[tooltip]="'file-preview.excluded-from-redaction' | translate | capitalize"
|
||||
icon="red:exclude-pages"
|
||||
tooltipPosition="above"
|
||||
></iqser-circle-button>
|
||||
|
||||
<span class="all-caps-label">
|
||||
<span translate="page"></span> {{ activeViewerPage }} -
|
||||
{{ activeAnnotations?.length || 0 }}
|
||||
<span [translate]="activeAnnotations?.length === 1 ? 'annotation' : 'annotations'"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div *ngIf="multiSelectActive">
|
||||
@ -141,13 +159,9 @@
|
||||
[verticalPadding]="40"
|
||||
icon="iqser:document"
|
||||
>
|
||||
<ng-container *ngIf="fileData?.file?.excludedPages?.includes(activeViewerPage)">
|
||||
<ng-container *ngIf="currentPageIsExcluded">
|
||||
{{ 'file-preview.tabs.annotations.page-is' | translate }}
|
||||
<a
|
||||
(click)="actionPerformed.emit('view-exclude-pages')"
|
||||
class="with-underline"
|
||||
translate="file-preview.excluded-from-redaction"
|
||||
></a
|
||||
<a (click)="viewExcludePages()" class="with-underline" translate="file-preview.excluded-from-redaction"></a
|
||||
>.
|
||||
</ng-container>
|
||||
</iqser-empty-state>
|
||||
@ -173,13 +187,15 @@
|
||||
<redaction-annotations-list
|
||||
(deselectAnnotations)="deselectAnnotations.emit($event)"
|
||||
(pagesPanelActive)="pagesPanelActive = $event"
|
||||
(selectAnnotations)="selectAnnotations.emit($event)"
|
||||
[(multiSelectActive)]="multiSelectActive"
|
||||
[activeViewerPage]="activeViewerPage"
|
||||
[annotationActionsTemplate]="annotationActionsTemplate"
|
||||
[annotations]="(displayedAnnotations$ | async)?.get(activeViewerPage)"
|
||||
(selectAnnotations)="selectAnnotations.emit($event)"
|
||||
[canMultiSelect]="!isReadOnly"
|
||||
[file]="file"
|
||||
[selectedAnnotations]="selectedAnnotations"
|
||||
iqserHelpMode="workload-annotations-list"
|
||||
></redaction-annotations-list>
|
||||
</div>
|
||||
</ng-container>
|
||||
@ -187,7 +203,7 @@
|
||||
<redaction-page-exclusion
|
||||
(actionPerformed)="actionPerformed.emit($event)"
|
||||
*ngIf="excludePages"
|
||||
[file]="fileData.file"
|
||||
[file]="file"
|
||||
></redaction-page-exclusion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -167,3 +167,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padding-left-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .page-separator iqser-circle-button mat-icon {
|
||||
color: var(--iqser-primary);
|
||||
}
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget } from '@iqser/common-ui';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget, Required } from '@iqser/common-ui';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Dossier, File, IViewedPage } from '@red/domain';
|
||||
|
||||
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
|
||||
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
@ -17,6 +28,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
selector: 'redaction-file-workload',
|
||||
templateUrl: './file-workload.component.html',
|
||||
styleUrls: ['./file-workload.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileWorkloadComponent {
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
@ -27,7 +39,9 @@ export class FileWorkloadComponent {
|
||||
@Input() activeViewerPage: number;
|
||||
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
|
||||
@Input() dialogRef: MatDialogRef<unknown>;
|
||||
@Input() fileData: FileDataModel;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@Input() @Required() file!: File;
|
||||
@Input() @Required() dossier!: Dossier;
|
||||
@Input() hideSkipped: boolean;
|
||||
@Input() excludePages: boolean;
|
||||
@Input() annotationActionsTemplate: TemplateRef<unknown>;
|
||||
@ -78,16 +92,16 @@ export class FileWorkloadComponent {
|
||||
}
|
||||
}
|
||||
|
||||
get isProcessing(): boolean {
|
||||
return this.fileData?.file?.isProcessing;
|
||||
}
|
||||
|
||||
get activeAnnotations(): AnnotationWrapper[] | undefined {
|
||||
return this.displayedAnnotations.get(this.activeViewerPage);
|
||||
}
|
||||
|
||||
get isReadOnly(): boolean {
|
||||
return !this._permissionsService.canPerformAnnotationActions();
|
||||
return !this._permissionsService.canPerformAnnotationActions(this.file);
|
||||
}
|
||||
|
||||
get currentPageIsExcluded(): boolean {
|
||||
return this.file?.excludedPages?.includes(this.activeViewerPage);
|
||||
}
|
||||
|
||||
private get _firstSelectedAnnotation() {
|
||||
@ -114,6 +128,11 @@ export class FileWorkloadComponent {
|
||||
}
|
||||
}
|
||||
|
||||
hasOnlyManualRedactionsAndNotExcluded(pageNumber: number): boolean {
|
||||
const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber).every(annotation => annotation.manual);
|
||||
return hasOnlyManualRedactions && this.file.excludedPages.includes(pageNumber);
|
||||
}
|
||||
|
||||
pageHasSelection(page: number) {
|
||||
return this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page);
|
||||
}
|
||||
@ -162,7 +181,7 @@ export class FileWorkloadComponent {
|
||||
this._navigatePages($event);
|
||||
}
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
scrollAnnotations(): void {
|
||||
@ -203,7 +222,7 @@ export class FileWorkloadComponent {
|
||||
}
|
||||
|
||||
scrollQuickNavLast(): void {
|
||||
this.selectPage.emit(this.fileData.file.numberOfPages);
|
||||
this.selectPage.emit(this.file.numberOfPages);
|
||||
}
|
||||
|
||||
pageSelectedByClick($event: number): void {
|
||||
@ -225,6 +244,10 @@ export class FileWorkloadComponent {
|
||||
this.selectPage.emit(this._nextPageWithAnnotations());
|
||||
}
|
||||
|
||||
viewExcludePages(): void {
|
||||
this.actionPerformed.emit('view-exclude-pages');
|
||||
}
|
||||
|
||||
private _filterAnnotations(
|
||||
annotations: AnnotationWrapper[],
|
||||
primary: INestedFilter[],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div *ngIf="permissionsService.canExcludePages()" class="exclude-pages-input-container">
|
||||
<div *ngIf="permissionsService.canExcludePages(file)" class="exclude-pages-input-container">
|
||||
<iqser-input-with-action
|
||||
(action)="excludePagesRange($event)"
|
||||
[hint]="'file-preview.tabs.exclude-pages.hint' | translate"
|
||||
@ -21,7 +21,7 @@
|
||||
<ng-container *ngIf="range.startPage !== range.endPage"> {{ range.startPage }} -{{ range.endPage }} </ng-container>
|
||||
<iqser-circle-button
|
||||
(action)="includePagesRange(range)"
|
||||
*ngIf="permissionsService.canExcludePages()"
|
||||
*ngIf="permissionsService.canExcludePages(file)"
|
||||
[tooltip]="'file-preview.tabs.exclude-pages.put-back' | translate"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!permissionsService.canExcludePages() && !excludedPagesRanges.length"
|
||||
*ngIf="!permissionsService.canExcludePages(file) && !excludedPagesRanges.length"
|
||||
class="no-excluded heading-l"
|
||||
translate="file-preview.tabs.exclude-pages.no-excluded"
|
||||
></div>
|
||||
|
||||
@ -40,8 +40,9 @@ export class PageExclusionComponent implements OnChanges {
|
||||
}, []);
|
||||
}
|
||||
|
||||
async excludePagesRange(value: string): Promise<void> {
|
||||
async excludePagesRange(inputValue: string): Promise<void> {
|
||||
this._loadingService.start();
|
||||
const value = inputValue.replace(/[^0-9-,]/g, '');
|
||||
try {
|
||||
const pageRanges = value.split(',').map(range => {
|
||||
const splitted = range.split('-');
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
[id]="'quick-nav-page-' + number"
|
||||
class="page-wrapper"
|
||||
>
|
||||
<mat-icon svgIcon="red:page"></mat-icon>
|
||||
<mat-icon [svgIcon]="showDottedIcon ? 'red:excluded-page' : 'red:page'"></mat-icon>
|
||||
<div class="text">
|
||||
{{ number }}
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,26 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { ViewedPagesService } from '../../shared/services/viewed-pages.service';
|
||||
import { IViewedPage } from '@red/domain';
|
||||
import { File, IViewedPage } from '@red/domain';
|
||||
import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-page-indicator',
|
||||
templateUrl: './page-indicator.component.html',
|
||||
styleUrls: ['./page-indicator.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
|
||||
@Input() active: boolean;
|
||||
export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy, OnChanges {
|
||||
@Input()
|
||||
file: File;
|
||||
|
||||
@Input()
|
||||
active = false;
|
||||
|
||||
@Input() showDottedIcon = false;
|
||||
@Input() number: number;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@Input() activeSelection = false;
|
||||
@ -21,16 +28,16 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
|
||||
@Output() readonly pageSelected = new EventEmitter<number>();
|
||||
|
||||
pageReadTimeout: number = null;
|
||||
canMarkPagesAsViewed: boolean;
|
||||
private _subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly _viewedPagesService: ViewedPagesService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _filesMapService: FilesMapService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _permissionService: PermissionsService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
return this.viewedPages?.find(p => p.page === this.number);
|
||||
@ -45,49 +52,35 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._subscription = this._appStateService.fileChanged$.subscribe(() => {
|
||||
if (this.canMarkPagesAsViewed !== this._permissionService.canMarkPagesAsViewed()) {
|
||||
this.canMarkPagesAsViewed = this._permissionService.canMarkPagesAsViewed();
|
||||
this._handlePageRead();
|
||||
}
|
||||
});
|
||||
ngOnChanges() {
|
||||
this.handlePageRead();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.active) {
|
||||
this._handlePageRead();
|
||||
}
|
||||
}
|
||||
|
||||
toggleReadState() {
|
||||
if (this.canMarkPagesAsViewed) {
|
||||
async toggleReadState() {
|
||||
if (this._permissionService.canMarkPagesAsViewed(this.file)) {
|
||||
if (this.read) {
|
||||
this._markPageUnread();
|
||||
await this._markPageUnread();
|
||||
} else {
|
||||
this._markPageRead();
|
||||
await this._markPageRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
handlePageRead() {
|
||||
if (!this._permissionService.canMarkPagesAsViewed(this.file)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePageRead() {
|
||||
if (this.canMarkPagesAsViewed) {
|
||||
if (this.pageReadTimeout) {
|
||||
clearTimeout(this.pageReadTimeout);
|
||||
}
|
||||
if (this.active && !this.read) {
|
||||
this.pageReadTimeout = window.setTimeout(() => {
|
||||
if (this.active && !this.read) {
|
||||
this._markPageRead();
|
||||
}
|
||||
}, this._configService.values.AUTO_READ_TIME * 1000);
|
||||
}
|
||||
if (this.pageReadTimeout) {
|
||||
clearTimeout(this.pageReadTimeout);
|
||||
}
|
||||
|
||||
if (this.active && !this.read) {
|
||||
this.pageReadTimeout = window.setTimeout(async () => {
|
||||
if (this.active && !this.read) {
|
||||
await this._markPageRead();
|
||||
}
|
||||
}, this._configService.values.AUTO_READ_TIME * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,26 +101,20 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
|
||||
// }
|
||||
// }
|
||||
|
||||
private _markPageRead() {
|
||||
this._viewedPagesService
|
||||
.addPage({ page: this.number }, this._dossiersService.activeDossierId, this._appStateService.activeFileId)
|
||||
.subscribe(() => {
|
||||
if (this.activePage) {
|
||||
this.activePage.hasChanges = false;
|
||||
} else {
|
||||
this.viewedPages?.push({ page: this.number, fileId: this._appStateService.activeFileId });
|
||||
}
|
||||
});
|
||||
private async _markPageRead() {
|
||||
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
|
||||
if (this.activePage) {
|
||||
this.activePage.hasChanges = false;
|
||||
} else {
|
||||
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId });
|
||||
}
|
||||
}
|
||||
|
||||
private _markPageUnread() {
|
||||
this._viewedPagesService
|
||||
.removePage(this._dossiersService.activeDossierId, this._appStateService.activeFileId, this.number)
|
||||
.subscribe(() => {
|
||||
this.viewedPages?.splice(
|
||||
this.viewedPages?.findIndex(p => p.page === this.number),
|
||||
1,
|
||||
);
|
||||
});
|
||||
private async _markPageUnread() {
|
||||
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
|
||||
this.viewedPages?.splice(
|
||||
this.viewedPages?.findIndex(p => p.page === this.number),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,14 @@ import {
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { File, IManualRedactionEntry, ViewMode } from '@red/domain';
|
||||
import { Dossier, File, IManualRedactionEntry, ViewMode } from '@red/domain';
|
||||
import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
||||
import {
|
||||
ManualRedactionEntryType,
|
||||
ManualRedactionEntryTypes,
|
||||
ManualRedactionEntryWrapper,
|
||||
} from '@models/file/manual-redaction-entry.wrapper';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { ManualAnnotationService } from '../../services/manual-annotation.service';
|
||||
import { environment } from '@environments/environment';
|
||||
@ -29,10 +33,26 @@ import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils';
|
||||
import { PdfViewerUtils } from '../../utils/pdf-viewer.utils';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { toPosition } from '../../utils/pdf-calculation.utils';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import Tools = Core.Tools;
|
||||
import TextTool = Tools.TextTool;
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
|
||||
const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const;
|
||||
const dataElements = {
|
||||
ADD_REDACTION: 'add-redaction',
|
||||
ADD_DICTIONARY: 'add-dictionary',
|
||||
ADD_RECTANGLE: 'add-rectangle',
|
||||
ADD_FALSE_POSITIVE: 'add-false-positive',
|
||||
SHAPE_TOOL_GROUP_BUTTON: 'shapeToolGroupButton',
|
||||
RECTANGLE_TOOL_DIVIDER: 'rectangleToolDivider',
|
||||
ANNOTATION_POPUP: 'annotationPopup',
|
||||
COMPARE_BUTTON: 'compareButton',
|
||||
CLOSE_COMPARE_BUTTON: 'closeCompareButton',
|
||||
COMPARE_TOOL_DIVIDER: 'compareToolDivider',
|
||||
} as const;
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-pdf-viewer',
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
@ -55,10 +75,11 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
@ViewChild('viewer', { static: true }) viewer: ElementRef;
|
||||
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
|
||||
instance: WebViewerInstance;
|
||||
documentViewer: Core.DocumentViewer;
|
||||
annotationManager: Core.AnnotationManager;
|
||||
utils: PdfViewerUtils;
|
||||
private _selectedText = '';
|
||||
private _firstPageChange = true;
|
||||
private readonly _allowedKeyboardShortcuts = ['+', '-', 'p', 'r', 'Escape'];
|
||||
|
||||
constructor(
|
||||
@Inject(BASE_HREF) private readonly _baseHref: string,
|
||||
@ -72,6 +93,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
private readonly _annotationActionsService: AnnotationActionsService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {}
|
||||
|
||||
private _viewMode: ViewMode = 'STANDARD';
|
||||
@ -85,97 +107,95 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.utils.viewMode = value;
|
||||
}
|
||||
|
||||
private get _dossier(): Dossier {
|
||||
return this._dossiersService.find(this.file.dossierId);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this._documentLoaded = this._documentLoaded.bind(this);
|
||||
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
|
||||
await this._loadViewer();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.instance) {
|
||||
if (changes.fileData) {
|
||||
this._loadDocument();
|
||||
}
|
||||
if (changes.canPerformActions) {
|
||||
this._handleCustomActions();
|
||||
}
|
||||
if (changes.multiSelectActive) {
|
||||
this.utils.multiSelectActive = this.multiSelectActive;
|
||||
}
|
||||
if (!this.instance) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setInitialViewerState() {
|
||||
// viewer init
|
||||
this.instance.UI.setFitMode('FitPage');
|
||||
if (changes.fileData) {
|
||||
this._loadDocument();
|
||||
}
|
||||
|
||||
const instanceDisplayMode = this.instance.Core.documentViewer.getDisplayModeManager().getDisplayMode();
|
||||
instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing';
|
||||
this.instance.Core.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
|
||||
if (changes.canPerformActions) {
|
||||
this._handleCustomActions();
|
||||
}
|
||||
|
||||
if (changes.multiSelectActive) {
|
||||
this.utils.multiSelectActive = this.multiSelectActive;
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile(files: any) {
|
||||
const fileToCompare = files[0];
|
||||
this.compareFileInput.nativeElement.value = null;
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
if (fileToCompare) {
|
||||
fileReader.onload = async () => {
|
||||
const pdfData = fileReader.result;
|
||||
const pdfNet = this.instance.Core.PDFNet;
|
||||
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
|
||||
const mergedDocument = await pdfNet.PDFDoc.create();
|
||||
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(<any>pdfData);
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
|
||||
|
||||
const currentDocumentPageCount = await currentDocument.getPageCount();
|
||||
const compareDocumentPageCount = await compareDocument.getPageCount();
|
||||
|
||||
const loadCompareDocument = async () => {
|
||||
this._loadingService.start();
|
||||
this.utils.ready = false;
|
||||
await loadCompareDocumentWrapper(
|
||||
currentDocumentPageCount,
|
||||
compareDocumentPageCount,
|
||||
currentDocument,
|
||||
compareDocument,
|
||||
mergedDocument,
|
||||
this.instance,
|
||||
this.file,
|
||||
() => {
|
||||
this.viewMode = 'COMPARE';
|
||||
},
|
||||
() => {
|
||||
this.utils.navigateToPage(1);
|
||||
},
|
||||
this.instance.Core.PDFNet,
|
||||
);
|
||||
this._loadingService.stop();
|
||||
};
|
||||
|
||||
if (currentDocumentPageCount !== compareDocumentPageCount) {
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
null,
|
||||
new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.compare-file.title'),
|
||||
question: _('confirmation-dialog.compare-file.question'),
|
||||
translateParams: {
|
||||
fileName: fileToCompare.name,
|
||||
currentDocumentPageCount,
|
||||
compareDocumentPageCount,
|
||||
},
|
||||
}),
|
||||
loadCompareDocument,
|
||||
);
|
||||
} else {
|
||||
await loadCompareDocument();
|
||||
}
|
||||
};
|
||||
if (!fileToCompare) {
|
||||
console.error('No file to compare!');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = async () => {
|
||||
const pdfNet = this.instance.Core.PDFNet;
|
||||
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
|
||||
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
|
||||
|
||||
const loadCompareDocument = async () => {
|
||||
this._loadingService.start();
|
||||
this.utils.ready = false;
|
||||
const mergedDocument = await pdfNet.PDFDoc.create();
|
||||
await loadCompareDocumentWrapper(
|
||||
currentDocument,
|
||||
compareDocument,
|
||||
mergedDocument,
|
||||
this.instance,
|
||||
this.file,
|
||||
() => {
|
||||
this.viewMode = 'COMPARE';
|
||||
},
|
||||
() => {
|
||||
this.utils.navigateToPage(1);
|
||||
},
|
||||
this.instance.Core.PDFNet,
|
||||
);
|
||||
this._loadingService.stop();
|
||||
};
|
||||
|
||||
const currentDocumentPageCount = await currentDocument.getPageCount();
|
||||
const compareDocumentPageCount = await compareDocument.getPageCount();
|
||||
|
||||
if (currentDocumentPageCount !== compareDocumentPageCount) {
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
null,
|
||||
new ConfirmationDialogInput({
|
||||
title: _('confirmation-dialog.compare-file.title'),
|
||||
question: _('confirmation-dialog.compare-file.question'),
|
||||
translateParams: {
|
||||
fileName: fileToCompare.name,
|
||||
currentDocumentPageCount,
|
||||
compareDocumentPageCount,
|
||||
},
|
||||
}),
|
||||
loadCompareDocument,
|
||||
);
|
||||
} else {
|
||||
await loadCompareDocument();
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(fileToCompare);
|
||||
}
|
||||
|
||||
@ -187,11 +207,18 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.instance.UI.loadDocument(currentDocument, {
|
||||
filename: this.file ? this.file.filename : 'document.pdf',
|
||||
});
|
||||
this.instance.UI.disableElements(['closeCompareButton']);
|
||||
this.instance.UI.enableElements(['compareButton']);
|
||||
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
|
||||
this.instance.UI.enableElements([dataElements.COMPARE_BUTTON]);
|
||||
this.utils.navigateToPage(1);
|
||||
}
|
||||
|
||||
private _setInitialDisplayMode() {
|
||||
this.instance.UI.setFitMode('FitPage');
|
||||
const instanceDisplayMode = this.documentViewer.getDisplayModeManager().getDisplayMode();
|
||||
instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing';
|
||||
this.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
|
||||
}
|
||||
|
||||
private _convertPath(path: string): string {
|
||||
return this._baseHref + path;
|
||||
}
|
||||
@ -208,6 +235,8 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.viewer.nativeElement,
|
||||
);
|
||||
|
||||
this.documentViewer = this.instance.Core.documentViewer;
|
||||
this.annotationManager = this.instance.Core.annotationManager;
|
||||
this.utils = new PdfViewerUtils(this.instance, this.viewMode, this.multiSelectActive);
|
||||
|
||||
this._setSelectionMode();
|
||||
@ -215,8 +244,8 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.utils.disableHotkeys();
|
||||
this._configureTextPopup();
|
||||
|
||||
this.instance.Core.annotationManager.on('annotationSelected', (annotations, action) => {
|
||||
this.annotationSelected.emit(this.instance.Core.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
|
||||
this.annotationManager.on('annotationSelected', (annotations, action) => {
|
||||
this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
|
||||
if (action === 'deselected') {
|
||||
this._toggleRectangleAnnotationAction(true);
|
||||
} else {
|
||||
@ -225,16 +254,17 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.Core.annotationManager.on('annotationChanged', annotations => {
|
||||
this.annotationManager.on('annotationChanged', annotations => {
|
||||
// when a rectangle is drawn,
|
||||
// it returns one annotation with tool name 'AnnotationCreateRectangle;
|
||||
// this will auto select rectangle after drawing
|
||||
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
|
||||
this.instance.Core.annotationManager.selectAnnotations(annotations);
|
||||
this.annotationManager.selectAnnotations(annotations);
|
||||
annotations[0].setRotationControlEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.Core.documentViewer.on('pageNumberUpdated', pageNumber => {
|
||||
this.documentViewer.on('pageNumberUpdated', pageNumber => {
|
||||
if (this.shouldDeselectAnnotationsOnPageChange) {
|
||||
this.utils.deselectAllAnnotations();
|
||||
}
|
||||
@ -250,9 +280,9 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this._handleCustomActions();
|
||||
});
|
||||
|
||||
this.instance.Core.documentViewer.on('documentLoaded', this._documentLoaded);
|
||||
this.documentViewer.on('documentLoaded', this._setReadyAndInitialState);
|
||||
|
||||
this.instance.Core.documentViewer.on('keyUp', $event => {
|
||||
this.documentViewer.on('keyUp', $event => {
|
||||
// arrows and full-screen
|
||||
if ($event.target?.tagName?.toLowerCase() !== 'input') {
|
||||
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
|
||||
@ -264,18 +294,20 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._allowedKeyboardShortcuts.indexOf($event.key) < 0) {
|
||||
if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.Core.documentViewer.on('textSelected', (quads, selectedText) => {
|
||||
this.documentViewer.on('textSelected', (quads, selectedText) => {
|
||||
this._selectedText = selectedText;
|
||||
if (selectedText.length > 2 && this.canPerformActions) {
|
||||
this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']);
|
||||
const textActions = [dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE];
|
||||
|
||||
if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded) {
|
||||
this.instance.UI.enableElements(textActions);
|
||||
} else {
|
||||
this.instance.UI.disableElements(['add-dictionary', 'add-false-positive']);
|
||||
this.instance.UI.disableElements(textActions);
|
||||
}
|
||||
});
|
||||
|
||||
@ -286,7 +318,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
inputElement.value = '';
|
||||
}, 0);
|
||||
if (!event.detail.isVisible) {
|
||||
this.instance.Core.documentViewer.clearSearchResults();
|
||||
this.documentViewer.clearSearchResults();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -295,15 +327,15 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private _setSelectionMode(): void {
|
||||
const textTool = (<unknown> this.instance.Core.Tools.TextTool) as TextTool;
|
||||
const textTool = this.instance.Core.Tools.TextTool as unknown as TextTool;
|
||||
textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE;
|
||||
}
|
||||
|
||||
private _toggleRectangleAnnotationAction(readonly: boolean) {
|
||||
private _toggleRectangleAnnotationAction(readonly = false) {
|
||||
if (!readonly) {
|
||||
this.instance.UI.enableElements(['add-rectangle']);
|
||||
this.instance.UI.enableElements([dataElements.ADD_RECTANGLE]);
|
||||
} else {
|
||||
this.instance.UI.disableElements(['add-rectangle']);
|
||||
this.instance.UI.disableElements([dataElements.ADD_RECTANGLE]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,53 +363,64 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
'annotationGroupButton',
|
||||
]);
|
||||
|
||||
this.instance.UI.setHeaderItems(header => {
|
||||
const originalHeaderItems = header.getItems();
|
||||
originalHeaderItems.splice(8, 0, {
|
||||
const headerItems = [
|
||||
{
|
||||
type: 'divider',
|
||||
dataElement: 'rectangleToolDivider',
|
||||
});
|
||||
originalHeaderItems.splice(9, 0, {
|
||||
dataElement: dataElements.RECTANGLE_TOOL_DIVIDER,
|
||||
},
|
||||
{
|
||||
type: 'toolGroupButton',
|
||||
toolGroup: 'rectangleTools',
|
||||
dataElement: 'shapeToolGroupButton',
|
||||
dataElement: dataElements.SHAPE_TOOL_GROUP_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rectangle.svg'),
|
||||
title: 'annotation.rectangle',
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
this.instance.UI.setHeaderItems(header => {
|
||||
const originalHeaderItems = header.getItems();
|
||||
originalHeaderItems.splice(8, 0, ...headerItems);
|
||||
|
||||
if (this._userPreferenceService.areDevFeaturesEnabled) {
|
||||
originalHeaderItems.splice(11, 0, {
|
||||
type: 'actionButton',
|
||||
element: 'compare',
|
||||
dataElement: 'compareButton',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
|
||||
title: 'Compare',
|
||||
onClick: () => {
|
||||
this.compareFileInput.nativeElement.click();
|
||||
const devHeaderItems = [
|
||||
{
|
||||
type: 'actionButton',
|
||||
element: 'compare',
|
||||
dataElement: dataElements.COMPARE_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
|
||||
title: 'Compare',
|
||||
onClick: () => {
|
||||
this.compareFileInput.nativeElement.click();
|
||||
},
|
||||
},
|
||||
});
|
||||
originalHeaderItems.splice(11, 0, {
|
||||
type: 'actionButton',
|
||||
element: 'closeCompare',
|
||||
dataElement: 'closeCompareButton',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
|
||||
title: 'Leave Compare Mode',
|
||||
onClick: () => {
|
||||
this.closeCompareMode();
|
||||
{
|
||||
type: 'actionButton',
|
||||
element: 'closeCompare',
|
||||
dataElement: dataElements.CLOSE_COMPARE_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
|
||||
title: 'Leave Compare Mode',
|
||||
onClick: async () => {
|
||||
await this.closeCompareMode();
|
||||
},
|
||||
},
|
||||
});
|
||||
originalHeaderItems.splice(13, 0, {
|
||||
type: 'divider',
|
||||
dataElement: 'compareToolDivider',
|
||||
});
|
||||
{
|
||||
type: 'divider',
|
||||
dataElement: dataElements.COMPARE_TOOL_DIVIDER,
|
||||
},
|
||||
];
|
||||
|
||||
originalHeaderItems.splice(11, 0, ...devHeaderItems);
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.UI.disableElements(['closeCompareButton']);
|
||||
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
|
||||
|
||||
this.instance.Core.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({
|
||||
const dossierTemplateId = this._dossier.dossierTemplateId;
|
||||
|
||||
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({
|
||||
StrokeThickness: 2,
|
||||
StrokeColor: this._annotationDrawService.getColor(this.instance, 'manual'),
|
||||
FillColor: this._annotationDrawService.getColor(this.instance, 'manual'),
|
||||
StrokeColor: this._annotationDrawService.getColor(this.instance, dossierTemplateId, 'manual'),
|
||||
FillColor: this._annotationDrawService.getColor(this.instance, dossierTemplateId, 'manual'),
|
||||
Opacity: 0.6,
|
||||
}));
|
||||
}
|
||||
@ -410,11 +453,11 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
onClick: () => {
|
||||
this._ngZone.run(() => {
|
||||
if (allAreVisible) {
|
||||
this.instance.Core.annotationManager.hideAnnotations(viewerAnnotations);
|
||||
this.annotationManager.hideAnnotations(viewerAnnotations);
|
||||
} else {
|
||||
this.instance.Core.annotationManager.showAnnotations(viewerAnnotations);
|
||||
this.annotationManager.showAnnotations(viewerAnnotations);
|
||||
}
|
||||
this.instance.Core.annotationManager.deselectAllAnnotations();
|
||||
this.annotationManager.deselectAllAnnotations();
|
||||
this._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible);
|
||||
});
|
||||
},
|
||||
@ -423,43 +466,53 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
this.instance.UI.annotationPopup.add(
|
||||
this._annotationActionsService.getViewerAvailableActions(annotationWrappers, this.annotationsChanged),
|
||||
this._annotationActionsService.getViewerAvailableActions(
|
||||
this.instance,
|
||||
this._dossier,
|
||||
annotationWrappers,
|
||||
this.annotationsChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _configureRectangleAnnotationPopup() {
|
||||
this.instance.UI.annotationPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
dataElement: 'add-rectangle',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
||||
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')),
|
||||
onClick: () => {
|
||||
const selectedAnnotations = this.instance.Core.annotationManager.getSelectedAnnotations();
|
||||
const activeAnnotation = selectedAnnotations[0];
|
||||
const activePage = selectedAnnotations[0].getPageNumber();
|
||||
const quad = this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance);
|
||||
const quadsObject = {};
|
||||
quadsObject[activePage] = [quad];
|
||||
const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle');
|
||||
// cleanup selection and button state
|
||||
this.utils.deselectAllAnnotations();
|
||||
this.instance.UI.disableElements(['shapeToolGroupButton', 'rectangleToolDivider']);
|
||||
this.instance.UI.enableElements(['shapeToolGroupButton', 'rectangleToolDivider']);
|
||||
// dispatch event
|
||||
this.manualAnnotationRequested.emit(
|
||||
new ManualRedactionEntryWrapper([quad], mre, 'REDACTION', 'RECTANGLE', activeAnnotation.Id),
|
||||
);
|
||||
this.instance.UI.annotationPopup.add([
|
||||
{
|
||||
type: 'actionButton',
|
||||
dataElement: dataElements.ADD_RECTANGLE,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
||||
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION', this._dossier)),
|
||||
onClick: () => this._addRectangleManualRedaction(),
|
||||
},
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
||||
private _addRectangleManualRedaction() {
|
||||
const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0];
|
||||
const activePage = activeAnnotation.getPageNumber();
|
||||
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance)];
|
||||
const manualRedaction = this._getManualRedaction({ [activePage]: quads }, 'Rectangle');
|
||||
this._cleanUpSelectionAndButtonState();
|
||||
|
||||
this.manualAnnotationRequested.emit(
|
||||
new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', 'RECTANGLE', activeAnnotation.Id),
|
||||
);
|
||||
}
|
||||
|
||||
private _cleanUpSelectionAndButtonState() {
|
||||
const rectangleElements = [dataElements.SHAPE_TOOL_GROUP_BUTTON, dataElements.RECTANGLE_TOOL_DIVIDER];
|
||||
this.utils.deselectAllAnnotations();
|
||||
this.instance.UI.disableElements(rectangleElements);
|
||||
this.instance.UI.enableElements(rectangleElements);
|
||||
}
|
||||
|
||||
private _configureTextPopup() {
|
||||
this.instance.UI.textPopup.add(<any>{
|
||||
const searchButton = {
|
||||
type: 'actionButton',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'),
|
||||
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
|
||||
onClick: () => {
|
||||
const text = this.instance.Core.documentViewer.getSelectedText();
|
||||
const text = this.documentViewer.getSelectedText();
|
||||
const searchOptions = {
|
||||
caseSensitive: true, // match case
|
||||
wholeWord: true, // match whole words only
|
||||
@ -469,117 +522,128 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
ambientString: true, // return ambient string as part of the result
|
||||
};
|
||||
this.instance.UI.openElements(['searchPanel']);
|
||||
setTimeout(() => {
|
||||
this.instance.UI.searchTextFull(text, searchOptions);
|
||||
}, 250);
|
||||
setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
this.instance.UI.textPopup.add([searchButton]);
|
||||
|
||||
// Adding directly to the false-positive dict is only available in dev-mode
|
||||
if (this._userPreferenceService.areDevFeaturesEnabled) {
|
||||
this.instance.UI.textPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
dataElement: 'add-false-positive',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
|
||||
title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')),
|
||||
onClick: () => {
|
||||
const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads();
|
||||
const text = this.instance.Core.documentViewer.getSelectedText();
|
||||
const mre = this._getManualRedactionEntry(selectedQuads, text, true);
|
||||
this.manualAnnotationRequested.emit(
|
||||
new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE'),
|
||||
);
|
||||
this.instance.UI.textPopup.add([
|
||||
{
|
||||
type: 'actionButton',
|
||||
dataElement: 'add-false-positive',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
|
||||
title: this._translateService.instant(
|
||||
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE, this._dossier),
|
||||
),
|
||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
|
||||
},
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
||||
this.instance.UI.textPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
dataElement: 'add-dictionary',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
|
||||
title: this._translateService.instant(this._manualAnnotationService.getTitle('DICTIONARY')),
|
||||
onClick: () => {
|
||||
const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads();
|
||||
const text = this.instance.Core.documentViewer.getSelectedText();
|
||||
const mre = this._getManualRedactionEntry(selectedQuads, text, true);
|
||||
this.manualAnnotationRequested.emit(
|
||||
new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'DICTIONARY'),
|
||||
);
|
||||
this.instance.UI.textPopup.add([
|
||||
{
|
||||
type: 'actionButton',
|
||||
dataElement: dataElements.ADD_REDACTION,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
||||
title: this._translateService.instant(
|
||||
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.REDACTION, this._dossier),
|
||||
),
|
||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
|
||||
},
|
||||
});
|
||||
{
|
||||
type: 'actionButton',
|
||||
dataElement: dataElements.ADD_DICTIONARY,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
|
||||
title: this._translateService.instant(
|
||||
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.DICTIONARY, this._dossier),
|
||||
),
|
||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
|
||||
},
|
||||
]);
|
||||
|
||||
this.instance.UI.textPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
dataElement: 'add-redaction',
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
||||
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')),
|
||||
onClick: () => {
|
||||
const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads();
|
||||
const text = this.instance.Core.documentViewer.getSelectedText();
|
||||
const mre = this._getManualRedactionEntry(selectedQuads, text, true);
|
||||
this.manualAnnotationRequested.emit(
|
||||
new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'REDACTION'),
|
||||
);
|
||||
},
|
||||
});
|
||||
this._handleCustomActions();
|
||||
}
|
||||
|
||||
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
|
||||
const selectedQuads = this.documentViewer.getSelectedTextQuads();
|
||||
const text = this.documentViewer.getSelectedText();
|
||||
const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
|
||||
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
|
||||
}
|
||||
|
||||
private _handleCustomActions() {
|
||||
this.instance.UI.setToolMode('AnnotationEdit');
|
||||
const { ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON } = dataElements;
|
||||
const elements = [
|
||||
ADD_REDACTION,
|
||||
ADD_RECTANGLE,
|
||||
'add-false-positive',
|
||||
SHAPE_TOOL_GROUP_BUTTON,
|
||||
'rectangleToolDivider',
|
||||
ANNOTATION_POPUP,
|
||||
];
|
||||
|
||||
if (this.canPerformActions && !this.utils.isCurrentPageExcluded) {
|
||||
this.instance.UI.enableTools(['AnnotationCreateRectangle']);
|
||||
this.instance.UI.enableElements([
|
||||
'add-redaction',
|
||||
'add-rectangle',
|
||||
'add-false-positive',
|
||||
'shapeToolGroupButton',
|
||||
'rectangleToolDivider',
|
||||
'annotationPopup',
|
||||
]);
|
||||
this.instance.UI.enableElements(elements);
|
||||
|
||||
if (this._selectedText.length > 2) {
|
||||
this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']);
|
||||
this.instance.UI.enableElements([dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let elementsToDisable = [...elements, ADD_RECTANGLE];
|
||||
|
||||
if (this.utils.isCurrentPageExcluded) {
|
||||
const allowedActionsWhenPageExcluded: string[] = [ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON];
|
||||
elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
|
||||
} else {
|
||||
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
|
||||
this.instance.UI.disableElements([
|
||||
'add-redaction',
|
||||
'add-dictionary',
|
||||
'add-false-positive',
|
||||
'add-rectangle',
|
||||
'shapeToolGroupButton',
|
||||
'rectangleToolDivider',
|
||||
'annotationPopup',
|
||||
]);
|
||||
}
|
||||
|
||||
this.instance.UI.disableElements(elementsToDisable);
|
||||
}
|
||||
|
||||
private _getManualRedactionEntry(quads: any, text: string, convertQuads: boolean = false): IManualRedactionEntry {
|
||||
private _getManualRedaction(
|
||||
quads: Readonly<Record<string, Core.Math.Quad[]>>,
|
||||
text: string,
|
||||
convertQuads = false,
|
||||
): IManualRedactionEntry {
|
||||
const entry: IManualRedactionEntry = { positions: [] };
|
||||
|
||||
for (const key of Object.keys(quads)) {
|
||||
for (const quad of quads[key]) {
|
||||
const page = parseInt(key, 10);
|
||||
entry.positions.push(this.utils.toPosition(page, convertQuads ? this.utils.translateQuads(page, quad) : quad));
|
||||
const pageHeight = this.documentViewer.getPageHeight(page);
|
||||
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuads(page, quad) : quad));
|
||||
}
|
||||
}
|
||||
|
||||
entry.value = text;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private _loadDocument() {
|
||||
if (this.fileData) {
|
||||
this.instance.UI.loadDocument(this.fileData, {
|
||||
filename: this.file ? this.file.filename : 'document.pdf',
|
||||
});
|
||||
if (!this.fileData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance.UI.loadDocument(this.fileData, {
|
||||
filename: this.file ? this.file.filename : 'document.pdf',
|
||||
});
|
||||
}
|
||||
|
||||
private _documentLoaded(): void {
|
||||
private _setReadyAndInitialState(): void {
|
||||
this._ngZone.run(() => {
|
||||
this.utils.ready = true;
|
||||
this._firstPageChange = true;
|
||||
this.viewerReady.emit(this.instance);
|
||||
this.setInitialViewerState();
|
||||
this._setInitialDisplayMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<form (submit)="saveMembers()" [formGroup]="teamForm">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div class="iqser-input-group w-300">
|
||||
<mat-form-field floatLabel="always">
|
||||
<mat-label>{{ 'assign-dossier-owner.dialog.single-user' | translate }}</mat-label>
|
||||
@ -28,15 +28,19 @@
|
||||
[canAdd]="false"
|
||||
[canRemove]="true"
|
||||
[largeSpacing]="true"
|
||||
[memberIds]="selectedReviewersList"
|
||||
[memberIds]="selectedReviewers$ | async"
|
||||
[perLine]="13"
|
||||
[unremovableMembers]="[selectedOwnerId]"
|
||||
></redaction-team-members>
|
||||
|
||||
<pre *ngIf="selectedReviewersList.length === 0" [innerHTML]="'assign-dossier-owner.dialog.no-reviewers' | translate" class="info"></pre>
|
||||
<pre
|
||||
*ngIf="(selectedReviewers$ | async).length === 0"
|
||||
[innerHTML]="'assign-dossier-owner.dialog.no-reviewers' | translate"
|
||||
class="info"
|
||||
></pre>
|
||||
|
||||
<iqser-input-with-action
|
||||
(valueChange)="setMembersSelectOptions()"
|
||||
(valueChange)="setMembersSelectOptions($event)"
|
||||
[(value)]="searchQuery"
|
||||
[placeholder]="'assign-dossier-owner.dialog.search' | translate"
|
||||
[width]="560"
|
||||
|
||||
@ -1,55 +1,75 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { Toaster } from '@iqser/common-ui';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { Dossier, IDossier, IDossierRequest } from '@red/domain';
|
||||
import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
import { EditDossierSectionInterface } from '../../dialogs/edit-dossier-dialog/edit-dossier-section.interface';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-team-members-manager',
|
||||
templateUrl: './team-members-manager.component.html',
|
||||
styleUrls: ['./team-members-manager.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TeamMembersManagerComponent implements OnInit {
|
||||
teamForm: FormGroup;
|
||||
export class TeamMembersManagerComponent extends AutoUnsubscribe implements EditDossierSectionInterface, OnInit, OnDestroy {
|
||||
form: FormGroup;
|
||||
searchQuery = '';
|
||||
|
||||
@Input() dossier: Dossier;
|
||||
@Output() readonly save = new EventEmitter<IDossier>();
|
||||
@Output() readonly updateDossier = new EventEmitter<IDossier>();
|
||||
|
||||
readonly ownersSelectOptions = this.userService.managerUsers.map(m => m.id);
|
||||
selectedReviewersList: string[] = [];
|
||||
membersSelectOptions: string[] = [];
|
||||
changed = false;
|
||||
readonly selectedReviewers$ = new BehaviorSubject<string[]>([]);
|
||||
|
||||
constructor(
|
||||
readonly userService: UserService,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get selectedOwnerId(): string {
|
||||
return this.teamForm.get('owner').value;
|
||||
return this.form.get('owner').value;
|
||||
}
|
||||
|
||||
get selectedApproversList(): string[] {
|
||||
return this.teamForm.get('approvers').value;
|
||||
return this.form.get('approvers').value;
|
||||
}
|
||||
|
||||
get selectedMembersList(): string[] {
|
||||
return this.teamForm.get('members').value;
|
||||
return this.form.get('members').value;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return this.teamForm.valid;
|
||||
return this.form.valid;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.userService.currentUser.isManager;
|
||||
}
|
||||
|
||||
get changed() {
|
||||
if (this.dossier.ownerId !== this.selectedOwnerId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const initialMembers = [...this.dossier.memberIds].sort();
|
||||
const currentMembers = this.selectedMembersList.sort();
|
||||
|
||||
const initialApprovers = [...this.dossier.approverIds].sort();
|
||||
const currentApprovers = this.selectedApproversList.sort();
|
||||
return this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
isOwner(userId: string): boolean {
|
||||
return userId === this.selectedOwnerId;
|
||||
}
|
||||
|
||||
async saveMembers() {
|
||||
async save() {
|
||||
const dossier = {
|
||||
...this.dossier,
|
||||
memberIds: this.selectedMembersList,
|
||||
@ -59,8 +79,7 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
|
||||
const result = await this._dossiersService.createOrUpdate(dossier).toPromise();
|
||||
if (result) {
|
||||
this.save.emit(result);
|
||||
this._updateChanged();
|
||||
this.updateDossier.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,39 +131,25 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
setMembersSelectOptions(): void {
|
||||
setMembersSelectOptions(value = this.searchQuery): void {
|
||||
this.membersSelectOptions = this.userService.eligibleUsers
|
||||
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
|
||||
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(value.toLowerCase()))
|
||||
.filter(user => this.selectedOwnerId !== user.id)
|
||||
.map(user => user.id);
|
||||
}
|
||||
|
||||
private _updateChanged() {
|
||||
if (this.dossier.ownerId !== this.selectedOwnerId) {
|
||||
this.changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const initialMembers = [...this.dossier.memberIds].sort();
|
||||
const currentMembers = this.selectedMembersList.sort();
|
||||
|
||||
const initialApprovers = [...this.dossier.approverIds].sort();
|
||||
const currentApprovers = this.selectedApproversList.sort();
|
||||
|
||||
this.changed = this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
private _setSelectedReviewersList() {
|
||||
this.selectedReviewersList = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
|
||||
const selectedReviewers = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
|
||||
this.selectedReviewers$.next(selectedReviewers);
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
this.teamForm = this._formBuilder.group({
|
||||
this.form = this._formBuilder.group({
|
||||
owner: [this.dossier?.ownerId, Validators.required],
|
||||
approvers: [[...this.dossier?.approverIds]],
|
||||
members: [[...this.dossier?.memberIds]],
|
||||
});
|
||||
this.teamForm.get('owner').valueChanges.subscribe(owner => {
|
||||
this.addSubscription = this.form.get('owner').valueChanges.subscribe(owner => {
|
||||
if (!this.isApprover(owner)) {
|
||||
this.toggleApprover(owner);
|
||||
}
|
||||
@ -157,7 +162,6 @@ export class TeamMembersManagerComponent implements OnInit {
|
||||
private _updateLists() {
|
||||
this._setSelectedReviewersList();
|
||||
this.setMembersSelectOptions();
|
||||
this._updateChanged();
|
||||
}
|
||||
|
||||
private _compareLists(l1: string[], l2: string[]) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { File } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-type-annotation-icon',
|
||||
@ -9,19 +11,24 @@ import { AppStateService } from '@state/app-state.service';
|
||||
})
|
||||
export class TypeAnnotationIconComponent implements OnChanges {
|
||||
@Input() annotation: AnnotationWrapper;
|
||||
@Input() file: File;
|
||||
|
||||
label: string;
|
||||
color: string;
|
||||
type: 'square' | 'rhombus' | 'circle' | 'hexagon';
|
||||
|
||||
constructor(private _appStateService: AppStateService) {}
|
||||
constructor(private _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {}
|
||||
|
||||
private get _dossierTemplateId(): string {
|
||||
return this._dossiersService.find(this.file.dossierId).dossierTemplateId;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.annotation) {
|
||||
if (this.annotation.isSuperTypeBasedColor) {
|
||||
this.color = this._appStateService.getDictionaryColor(this.annotation.superType);
|
||||
this.color = this._appStateService.getDictionaryColor(this.annotation.superType, this._dossierTemplateId);
|
||||
} else {
|
||||
this.color = this._appStateService.getDictionaryColor(this.annotation.type);
|
||||
this.color = this._appStateService.getDictionaryColor(this.annotation.type, this._dossierTemplateId);
|
||||
}
|
||||
this.type =
|
||||
this.annotation.isSuggestion || this.annotation.isDeclinedSuggestion
|
||||
@ -36,8 +43,6 @@ export class TypeAnnotationIconComponent implements OnChanges {
|
||||
? 'S'
|
||||
: this.annotation.isSkipped
|
||||
? 'S'
|
||||
: this.annotation.isReadyForAnalysis
|
||||
? 'A'
|
||||
: this.annotation.type[0].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,9 +41,17 @@
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<mat-checkbox class="watermark" color="primary" formControlName="watermarkEnabled">
|
||||
{{ 'add-dossier-dialog.form.watermark' | translate }}
|
||||
</mat-checkbox>
|
||||
<div>
|
||||
<mat-checkbox class="watermark" color="primary" formControlName="watermarkEnabled">
|
||||
{{ 'add-dossier-dialog.form.watermark' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox class="watermark-preview" color="primary" formControlName="watermarkPreviewEnabled">
|
||||
{{ 'add-dossier-dialog.form.watermark-preview' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="due-date">
|
||||
<mat-checkbox (change)="hasDueDate = !hasDueDate" [checked]="hasDueDate" class="filter-menu-checkbox" color="primary">
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.watermark-preview {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.due-date {
|
||||
margin-top: 8px;
|
||||
min-height: 34px;
|
||||
|
||||
@ -42,6 +42,7 @@ export class AddDossierDialogComponent {
|
||||
description: [null],
|
||||
dueDate: [null],
|
||||
watermarkEnabled: [true],
|
||||
watermarkPreviewEnabled: [false],
|
||||
},
|
||||
{
|
||||
validators: control =>
|
||||
@ -123,6 +124,7 @@ export class AddDossierDialogComponent {
|
||||
downloadFileTypes: this.dossierForm.get('downloadFileTypes').value,
|
||||
reportTemplateIds: this.dossierForm.get('reportTemplateIds').value,
|
||||
watermarkEnabled: this.dossierForm.get('watermarkEnabled').value,
|
||||
watermarkPreviewEnabled: this.dossierForm.get('watermarkPreviewEnabled').value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,13 @@
|
||||
class="dialog-header heading-l"
|
||||
></div>
|
||||
|
||||
<form (submit)="save()" [formGroup]="usersForm">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div class="dialog-content">
|
||||
<div class="iqser-input-group w-300 required">
|
||||
<mat-form-field floatLabel="always">
|
||||
<mat-label>{{ 'assign-owner.dialog.label' | translate: { type: data.mode } }}</mat-label>
|
||||
<mat-select [placeholder]="'initials-avatar.unassigned' | translate" formControlName="singleUser">
|
||||
<mat-option *ngFor="let userId of singleUsersSelectOptions" [value]="userId">
|
||||
<mat-select [placeholder]="'initials-avatar.unassigned' | translate" formControlName="user">
|
||||
<mat-option *ngFor="let userId of userOptions" [value]="userId">
|
||||
{{ userId | name: { defaultValue: 'initials-avatar.unassigned' | translate } }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="!usersForm.valid || !changed" color="primary" mat-flat-button type="submit">
|
||||
<button [disabled]="!form.valid || !changed" color="primary" mat-flat-button type="submit">
|
||||
{{ 'assign-owner.dialog.save' | translate }}
|
||||
</button>
|
||||
|
||||
|
||||
@ -12,8 +12,7 @@ import { PermissionsService } from '@services/permissions.service';
|
||||
|
||||
class DialogData {
|
||||
mode: 'approver' | 'reviewer';
|
||||
dossier?: Dossier;
|
||||
files?: File[];
|
||||
files: File[];
|
||||
ignoreChanged?: boolean;
|
||||
withCurrentUserAsDefault?: boolean;
|
||||
}
|
||||
@ -23,8 +22,8 @@ class DialogData {
|
||||
styleUrls: ['./assign-reviewer-approver-dialog.component.scss'],
|
||||
})
|
||||
export class AssignReviewerApproverDialogComponent {
|
||||
usersForm: FormGroup;
|
||||
searchForm: FormGroup;
|
||||
form: FormGroup;
|
||||
dossier: Dossier;
|
||||
|
||||
constructor(
|
||||
readonly userService: UserService,
|
||||
@ -37,18 +36,19 @@ export class AssignReviewerApproverDialogComponent {
|
||||
private readonly _dialogRef: MatDialogRef<AssignReviewerApproverDialogComponent, boolean>,
|
||||
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
|
||||
) {
|
||||
this.dossier = this._dossiersService.find(this.data.files[0].dossierId);
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
get selectedSingleUser(): string {
|
||||
return this.usersForm.get('singleUser').value;
|
||||
get selectedUser(): string {
|
||||
return this.form.get('user').value;
|
||||
}
|
||||
|
||||
get singleUsersSelectOptions() {
|
||||
get userOptions() {
|
||||
const unassignUser = this._canUnassignFiles ? [undefined] : [];
|
||||
return this.data.mode === 'approver'
|
||||
? [...this._dossiersService.activeDossier.approverIds, ...unassignUser]
|
||||
: [...this._dossiersService.activeDossier.memberIds, ...unassignUser];
|
||||
? [...this.dossier.approverIds, ...unassignUser]
|
||||
: [...this.dossier.memberIds, ...unassignUser];
|
||||
}
|
||||
|
||||
get changed(): boolean {
|
||||
@ -57,7 +57,7 @@ export class AssignReviewerApproverDialogComponent {
|
||||
}
|
||||
|
||||
for (const file of this.data.files) {
|
||||
if (file.currentReviewer !== this.selectedSingleUser) {
|
||||
if (file.currentReviewer !== this.selectedUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -70,27 +70,25 @@ export class AssignReviewerApproverDialogComponent {
|
||||
}
|
||||
|
||||
isOwner(userId: string): boolean {
|
||||
return userId === this.selectedSingleUser;
|
||||
return userId === this.selectedUser;
|
||||
}
|
||||
|
||||
async save() {
|
||||
try {
|
||||
const selectedUser = this.selectedSingleUser;
|
||||
|
||||
if (this.data.mode === 'reviewer') {
|
||||
await this._filesService
|
||||
.setReviewerFor(
|
||||
this.data.files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
selectedUser,
|
||||
this.dossier.id,
|
||||
this.selectedUser,
|
||||
)
|
||||
.toPromise();
|
||||
} else {
|
||||
await this._filesService
|
||||
.setUnderApprovalFor(
|
||||
this.data.files.map(f => f.fileId),
|
||||
this._dossiersService.activeDossierId,
|
||||
selectedUser,
|
||||
this.dossier.id,
|
||||
this.selectedUser,
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
@ -112,17 +110,16 @@ export class AssignReviewerApproverDialogComponent {
|
||||
uniqueReviewers.add(file.currentReviewer);
|
||||
}
|
||||
}
|
||||
let user: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id;
|
||||
user = this.userOptions.indexOf(user) >= 0 ? user : this.userOptions[0];
|
||||
|
||||
let singleUser: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id;
|
||||
singleUser = this.singleUsersSelectOptions.indexOf(singleUser) >= 0 ? singleUser : this.singleUsersSelectOptions[0];
|
||||
|
||||
if (this.data.withCurrentUserAsDefault && this.singleUsersSelectOptions.includes(this.userService.currentUser.id)) {
|
||||
singleUser = this.userService.currentUser.id;
|
||||
if (this.data.withCurrentUserAsDefault && this.userOptions.includes(this.userService.currentUser.id)) {
|
||||
user = this.userService.currentUser.id;
|
||||
}
|
||||
|
||||
this.usersForm = this._formBuilder.group({
|
||||
this.form = this._formBuilder.group({
|
||||
// Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign
|
||||
singleUser: [singleUser, this._canUnassignFiles && !singleUser ? Validators.required : null],
|
||||
user: [user, this._canUnassignFiles && !user ? Validators.required : null],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
import { Dossier } from '@red/domain';
|
||||
|
||||
export interface LegalBasisOption {
|
||||
label?: string;
|
||||
@ -15,9 +14,7 @@ export interface LegalBasisOption {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-change-legal-basis-dialog',
|
||||
templateUrl: './change-legal-basis-dialog.component.html',
|
||||
styleUrls: ['./change-legal-basis-dialog.component.scss'],
|
||||
})
|
||||
export class ChangeLegalBasisDialogComponent implements OnInit {
|
||||
legalBasisForm: FormGroup;
|
||||
@ -25,34 +22,30 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
|
||||
legalOptions: LegalBasisOption[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _dossiersService: DossiersService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
public dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[],
|
||||
readonly dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) private readonly _data: { annotations: AnnotationWrapper[]; dossier: Dossier },
|
||||
) {}
|
||||
|
||||
get changed(): boolean {
|
||||
return this.legalBasisForm.get('reason').value.legalBasis !== this.annotations[0].legalBasis;
|
||||
return this.legalBasisForm.get('reason').value.legalBasis !== this._data.annotations[0].legalBasis;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isDocumentAdmin = this._permissionsService.isApprover();
|
||||
this.isDocumentAdmin = this._permissionsService.isApprover(this._data.dossier);
|
||||
|
||||
this.legalBasisForm = this._formBuilder.group({
|
||||
reason: [null, Validators.required],
|
||||
comment: this.isDocumentAdmin ? [null] : [null, Validators.required],
|
||||
});
|
||||
|
||||
const data = await this._justificationsService
|
||||
.getForDossierTemplate(this._dossiersService.activeDossier.dossierTemplateId)
|
||||
.toPromise();
|
||||
const data = await this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId).toPromise();
|
||||
|
||||
this.legalOptions = data
|
||||
.map(lbm => ({
|
||||
.map<LegalBasisOption>(lbm => ({
|
||||
legalBasis: lbm.reason,
|
||||
description: lbm.description,
|
||||
label: lbm.name,
|
||||
@ -60,7 +53,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
this.legalBasisForm.patchValue({
|
||||
reason: this.legalOptions.find(option => option.legalBasis === this.annotations[0].legalBasis),
|
||||
reason: this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
(change)="uploadImage($event, attr)"
|
||||
[id]="attr.id"
|
||||
[name]="attr.id"
|
||||
accept="image/*"
|
||||
accept="image/jpg, image/jpeg, image/png"
|
||||
class="file-upload-input"
|
||||
hidden
|
||||
type="file"
|
||||
|
||||
@ -62,7 +62,6 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
|
||||
}
|
||||
|
||||
async save() {
|
||||
this._loadingService.start();
|
||||
const dossierAttributeList = this.attributes.map(attr => ({
|
||||
dossierAttributeConfigId: attr.id,
|
||||
value: this.currentAttrValue(attr),
|
||||
@ -70,7 +69,6 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
|
||||
await this._dossierAttributesService.setAttributes(this.dossier, dossierAttributeList).toPromise();
|
||||
await this._loadAttributes();
|
||||
this.updateDossier.emit();
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
fileInputClick(attr: DossierAttributeWithValue) {
|
||||
|
||||
@ -115,7 +115,7 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
|
||||
const fileIds = files.map(f => f.fileId);
|
||||
await this._fileManagementService.restore(fileIds, this.dossier.id).toPromise();
|
||||
this._removeFromList(fileIds);
|
||||
await this._appStateService.reloadActiveDossierFiles();
|
||||
await this._appStateService.reloadDossierFiles(files[0].dossierId);
|
||||
this.updateDossier.emit();
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<div class="header-wrapper">
|
||||
<div class="heading">
|
||||
<div>{{ dossier.type?.label }}</div>
|
||||
<div>{{ dossierDictionary?.label }}</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:entries"></mat-icon>
|
||||
{{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossier.type?.entries || []).length } }}
|
||||
{{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossierDictionary?.entries || []).length } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -19,13 +19,13 @@
|
||||
[placeholder]="'edit-dossier-dialog.dictionary.display-name.placeholder' | translate"
|
||||
[saveTooltip]="'edit-dossier-dialog.dictionary.display-name.save' | translate"
|
||||
[showPreview]="false"
|
||||
[value]="dossier.type?.label"
|
||||
[value]="dossierDictionary?.label"
|
||||
></iqser-editable-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<redaction-dictionary-manager
|
||||
[canEdit]="canEdit"
|
||||
[initialEntries]="dossier.type?.entries || []"
|
||||
[initialEntries]="dossierDictionary?.entries || []"
|
||||
[withFloatingActions]="false"
|
||||
></redaction-dictionary-manager>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user