Merge branch 'master' into VM/RED-2539

This commit is contained in:
Valentin 2021-11-20 22:32:55 +02:00
commit 7ef6718df3
275 changed files with 7429 additions and 5714 deletions

View File

@ -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
View File

@ -45,3 +45,4 @@ version.properties
paligo-styles/style.css*
migrations.json
*.iml

View File

@ -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": []
}
}
}

View File

@ -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',

View File

@ -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],

View File

@ -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">

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,10 @@
:host {
display: flex;
align-items: center;
overflow: hidden;
height: 100%;
> *:not(:last-child) {
margin-right: 6px;
}
}

View File

@ -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) {}
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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>

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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,
) {}

View File

@ -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;

View File

@ -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 {}

View File

@ -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>

View File

@ -0,0 +1,7 @@
:host {
height: calc(100vh - 61px);
&.dossier-templates {
height: calc(100vh - 111px);
}
}

View File

@ -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'),
},
];
}

View 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 {}

View File

@ -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>

View File

@ -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;
}

View File

@ -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%');
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;

View File

@ -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 {}

View File

@ -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,
};
}
}

View File

@ -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;

View File

@ -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',

View File

@ -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 {}

View File

@ -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'));

View File

@ -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>

View File

@ -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,
};
}
}

View File

@ -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 });

View File

@ -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'));

View File

@ -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();
}

View File

@ -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">

View File

@ -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 {

View File

@ -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>

View File

@ -0,0 +1,11 @@
.dialog-header {
color: var(--iqser-primary);
}
.dialog-actions > *:not(:last-child) {
margin-right: 16px;
}
.cancel {
margin-left: 8px;
}

View File

@ -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 });
}
}

View File

@ -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,

View File

@ -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 = [];

View File

@ -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">

View File

@ -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 = [];
});
}
}

View File

@ -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';
}

View File

@ -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'));

View File

@ -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,

View File

@ -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">

View File

@ -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'));

View File

@ -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();

View File

@ -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,

View File

@ -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);
}
}

View File

@ -26,6 +26,6 @@ export class TableItemComponent {
}
openConfirmDeleteDialog() {
this._dialogService.confirmDelete([this.justification], this._listingService);
this._dialogService.confirmDelete([this.justification]);
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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,
) {

View File

@ -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) {

View File

@ -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,

View File

@ -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>

View File

@ -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,
);

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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>;

View File

@ -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>

View File

@ -167,3 +167,11 @@
}
}
}
.padding-left-0 {
padding-left: 0 !important;
}
::ng-deep .page-separator iqser-circle-button mat-icon {
color: var(--iqser-primary);
}

View File

@ -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[],

View File

@ -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>

View File

@ -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('-');

View File

@ -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>

View File

@ -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,
);
}
}

View File

@ -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();
});
}
}

View File

@ -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"

View File

@ -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[]) {

View File

@ -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();
}
}

View File

@ -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">

View File

@ -2,6 +2,10 @@
margin-top: 24px;
}
.watermark-preview {
margin-top: 8px;
}
.due-date {
margin-top: 8px;
min-height: 34px;

View File

@ -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,
};
}
}

View File

@ -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>

View File

@ -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],
});
}
}

View File

@ -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),
});
}

View File

@ -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"

View 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) {

View File

@ -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();
}

View File

@ -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