Merge branch 'master' into release/4.839.x
This commit is contained in:
commit
8f541081ae
@ -4,38 +4,40 @@ variables:
|
||||
PROJECT: red-ui
|
||||
DOCKERFILELOCATION: 'docker/$PROJECT/Dockerfile'
|
||||
|
||||
|
||||
include:
|
||||
- project: 'gitlab/gitlab'
|
||||
ref: 'main'
|
||||
file: 'ci-templates/docker_build_nexus_v2.yml'
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE != "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE != "schedule"
|
||||
|
||||
localazy update:
|
||||
image: node:20.5
|
||||
cache:
|
||||
- key:
|
||||
files:
|
||||
- yarn.lock
|
||||
paths:
|
||||
- .yarn-cache/
|
||||
script:
|
||||
- git config user.email "${CI_EMAIL}"
|
||||
- git config user.name "${CI_USERNAME}"
|
||||
- git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
|
||||
- cd tools/localazy
|
||||
- yarn install --cache-folder .yarn-cache
|
||||
- yarn start
|
||||
- cd ../..
|
||||
- git add .
|
||||
- |-
|
||||
CHANGES=$(git status --porcelain | wc -l)
|
||||
if [ "$CHANGES" -gt "0" ]
|
||||
then
|
||||
git status
|
||||
git commit -m "push back localazy update"
|
||||
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME}
|
||||
fi
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
image: node:20.5
|
||||
cache:
|
||||
- key:
|
||||
files:
|
||||
- yarn.lock
|
||||
paths:
|
||||
- .yarn-cache/
|
||||
script:
|
||||
# - git config user.email "${CI_EMAIL}"
|
||||
# - git config user.name "${CI_USERNAME}"
|
||||
# - git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
|
||||
- git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
|
||||
- cd tools/localazy
|
||||
- yarn install --cache-folder .yarn-cache
|
||||
- yarn start
|
||||
- cd ../..
|
||||
- git add .
|
||||
- |-
|
||||
CHANGES=$(git status --porcelain | wc -l)
|
||||
if [ "$CHANGES" -gt "0" ]
|
||||
then
|
||||
git status
|
||||
git commit -m "push back localazy update"
|
||||
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME}
|
||||
# git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
|
||||
# git push
|
||||
fi
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.mat-mdc-menu-item.notification {
|
||||
padding: 8px 26px 10px 8px;
|
||||
padding: 8px 26px 10px 8px !important;
|
||||
margin: 2px 0 0 0;
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
.container {
|
||||
padding: 32px;
|
||||
width: 900px;
|
||||
width: 1000px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction
|
||||
(annotation.isRedacted || (annotation.isHint && !annotation.isImage));
|
||||
|
||||
export const canRemoveFromDictionary = (annotation: AnnotationWrapper, autoAnalysisDisabled: boolean) =>
|
||||
annotation.isModifyDictionary &&
|
||||
(annotation.isModifyDictionary || annotation.engines.includes('DICTIONARY') || annotation.engines.includes('DOSSIER_DICTIONARY')) &&
|
||||
(annotation.isRedacted || annotation.isSkipped || annotation.isHint || (annotation.isIgnoredHint && !annotation.isRuleBased)) &&
|
||||
(autoAnalysisDisabled || !annotation.pending) &&
|
||||
annotation.isDictBased;
|
||||
|
||||
@ -26,6 +26,12 @@ import {
|
||||
} from '@red/domain';
|
||||
import { annotationTypesTranslations } from '@translations/annotation-types-translations';
|
||||
|
||||
interface AnnotationContent {
|
||||
translation: string;
|
||||
params: { [key: string]: string };
|
||||
untranslatedContent: string;
|
||||
}
|
||||
|
||||
export class AnnotationWrapper implements IListable {
|
||||
id: string;
|
||||
superType: SuperType;
|
||||
@ -36,7 +42,7 @@ export class AnnotationWrapper implements IListable {
|
||||
numberOfComments = 0;
|
||||
firstTopLeftPoint: IPoint;
|
||||
shortContent: string;
|
||||
content: string;
|
||||
content: AnnotationContent;
|
||||
value: string;
|
||||
pageNumber: number;
|
||||
dictionaryOperation = false;
|
||||
@ -279,7 +285,7 @@ export class AnnotationWrapper implements IListable {
|
||||
);
|
||||
|
||||
const content = this.#createContent(annotationWrapper, logEntry, isDocumine);
|
||||
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper, legalBasisList) || content;
|
||||
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper, legalBasisList) || content.untranslatedContent;
|
||||
annotationWrapper.content = content;
|
||||
|
||||
const lastRelevantManualChange = logEntry.manualChanges?.at(-1);
|
||||
@ -311,39 +317,57 @@ export class AnnotationWrapper implements IListable {
|
||||
}
|
||||
|
||||
static #createContent(annotationWrapper: AnnotationWrapper, logEntry: IEntityLogEntry, isDocumine: boolean) {
|
||||
let content = '';
|
||||
let untranslatedContent = '';
|
||||
const params: { [key: string]: string } = {};
|
||||
if (logEntry.matchedRule) {
|
||||
content += `Rule ${logEntry.matchedRule} matched${isDocumine ? ':' : ''} \n\n`;
|
||||
params['hasRule'] = 'true';
|
||||
params['matchedRule'] = logEntry.matchedRule.replace(/(^[, ]*)|([, ]*$)/g, '');
|
||||
params['ruleSymbol'] = isDocumine ? ':' : '';
|
||||
|
||||
untranslatedContent += `Rule ${logEntry.matchedRule} matched${isDocumine ? ':' : ''} \n\n`;
|
||||
}
|
||||
|
||||
if (logEntry.reason) {
|
||||
params['hasReason'] = 'true';
|
||||
if (isDocumine && logEntry.reason.slice(-1) === '.') {
|
||||
logEntry.reason = logEntry.reason.slice(0, -1);
|
||||
}
|
||||
|
||||
content += logEntry.reason + '\n\n';
|
||||
if (!params['hasRule']) {
|
||||
params['reason'] = logEntry.reason.substring(0, 1).toUpperCase() + logEntry.reason.substring(1);
|
||||
} else {
|
||||
params['reason'] = logEntry.reason;
|
||||
}
|
||||
params['reason'] = params['reason'].replace(/(^[, ]*)|([, ]*$)/g, '');
|
||||
untranslatedContent += logEntry.reason + '\n\n';
|
||||
//remove leading and trailing commas and whitespaces
|
||||
content = content.replace(/(^[, ]*)|([, ]*$)/g, '');
|
||||
content = content.substring(0, 1).toUpperCase() + content.substring(1);
|
||||
untranslatedContent = untranslatedContent.replace(/(^[, ]*)|([, ]*$)/g, '');
|
||||
untranslatedContent = untranslatedContent.substring(0, 1).toUpperCase() + untranslatedContent.substring(1);
|
||||
}
|
||||
|
||||
if (annotationWrapper.legalBasis && !isDocumine) {
|
||||
content += 'Legal basis: ' + annotationWrapper.legalBasis + '\n\n';
|
||||
params['hasLb'] = 'true';
|
||||
params['legalBasis'] = annotationWrapper.legalBasis;
|
||||
untranslatedContent += 'Legal basis: ' + annotationWrapper.legalBasis + '\n\n';
|
||||
}
|
||||
|
||||
if (annotationWrapper.hasBeenRemovedByManualOverride) {
|
||||
content += 'Removed by manual override';
|
||||
params['hasOverride'] = 'true';
|
||||
untranslatedContent += 'Removed by manual override';
|
||||
}
|
||||
|
||||
if (logEntry.section) {
|
||||
params['hasSection'] = 'true';
|
||||
params['sectionSymbol'] = isDocumine ? '' : ':';
|
||||
params['shouldLower'] = untranslatedContent.length.toString();
|
||||
params['section'] = logEntry.section;
|
||||
let prefix = `In section${isDocumine ? '' : ':'} `;
|
||||
if (content.length) {
|
||||
if (untranslatedContent.length) {
|
||||
prefix = ` ${prefix.toLowerCase()}`;
|
||||
}
|
||||
content += `${prefix} "${logEntry.section}"`;
|
||||
untranslatedContent += `${prefix} "${logEntry.section}"`;
|
||||
}
|
||||
|
||||
return content;
|
||||
return { translation: _('annotation-content'), params: params, untranslatedContent: untranslatedContent };
|
||||
}
|
||||
|
||||
static #getShortContent(annotationWrapper: AnnotationWrapper, legalBasisList: ILegalBasis[]) {
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label>
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="language">
|
||||
<mat-select-trigger>{{ languageSelectLabel() | translate }}</mat-select-trigger>
|
||||
<mat-option *ngFor="let language of languages" [value]="language">
|
||||
{{ translations[language] | translate }}
|
||||
</mat-option>
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
[disabled]="form.invalid || !(profileChanged || languageChanged || themeChanged)"
|
||||
[disabled]="disabled"
|
||||
[label]="'user-profile-screen.actions.save' | translate"
|
||||
[submit]="true"
|
||||
[type]="iconButtonTypes.primary"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed } from '@angular/core';
|
||||
import { FormGroup, ReactiveFormsModule, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import {
|
||||
BaseFormComponent,
|
||||
@ -19,21 +19,49 @@ import { firstValueFrom } from 'rxjs';
|
||||
import { UserProfileDialogService } from '../services/user-profile-dialog.service';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatOption, MatSelect } from '@angular/material/select';
|
||||
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { PdfViewer } from '../../../../pdf-viewer/services/pdf-viewer.service';
|
||||
import { formControlToSignal } from '@utils/functions';
|
||||
import { AsControl } from '@common-ui/utils';
|
||||
|
||||
interface UserProfileForm {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
language: string;
|
||||
darkTheme: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './user-profile-screen.component.html',
|
||||
styleUrls: ['./user-profile-screen.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, MatFormField, MatSelect, MatOption, NgForOf, TranslateModule, MatSlideToggle, IconButtonComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
MatFormField,
|
||||
MatSelect,
|
||||
MatOption,
|
||||
NgForOf,
|
||||
TranslateModule,
|
||||
MatSlideToggle,
|
||||
IconButtonComponent,
|
||||
MatSelectTrigger,
|
||||
],
|
||||
})
|
||||
export class UserProfileScreenComponent extends BaseFormComponent implements OnInit {
|
||||
#profileModel: IProfile;
|
||||
export class UserProfileScreenComponent extends BaseFormComponent {
|
||||
readonly form: FormGroup<AsControl<UserProfileForm>> = this.#getForm();
|
||||
initialFormValue = this.form.getRawValue();
|
||||
readonly translations = languagesTranslations;
|
||||
readonly devMode = this._userPreferenceService.isIqserDevMode;
|
||||
|
||||
readonly profileKeys = ['email', 'firstName', 'lastName'];
|
||||
readonly languages = this._translateService.langs;
|
||||
readonly language = formControlToSignal<UserProfileForm['language']>(this.form.controls.language);
|
||||
readonly languageSelectLabel = computed(() => this.translations[this.language()]);
|
||||
|
||||
constructor(
|
||||
private readonly _userService: UserService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
@ -45,43 +73,29 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
|
||||
protected readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _changeRef: ChangeDetectorRef,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _pdfViewer: PdfViewer,
|
||||
) {
|
||||
super();
|
||||
this._loadingService.start();
|
||||
if (!this._permissionsService.has(Roles.updateMyProfile)) {
|
||||
this.form.disable();
|
||||
}
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
get languageChanged(): boolean {
|
||||
return this.#profileModel['language'] !== this.form.get('language').value;
|
||||
return this.initialFormValue['language'] !== this.form.controls.language.value;
|
||||
}
|
||||
|
||||
get themeChanged(): boolean {
|
||||
return this.#profileModel['darkTheme'] !== this.form.get('darkTheme').value;
|
||||
return this.initialFormValue['darkTheme'] !== this.form.controls.darkTheme.value;
|
||||
}
|
||||
|
||||
get emailChanged(): boolean {
|
||||
return this.#profileModel['email'] !== this.form.get('email').value;
|
||||
return this.initialFormValue['email'] !== this.form.controls.email.value;
|
||||
}
|
||||
|
||||
get profileChanged(): boolean {
|
||||
const keys = Object.keys(this.form.getRawValue());
|
||||
keys.splice(keys.indexOf('language'), 1);
|
||||
keys.splice(keys.indexOf('darkTheme'), 1);
|
||||
|
||||
for (const key of keys) {
|
||||
if (this.#profileModel[key] !== this.form.get(key).value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get languages(): string[] {
|
||||
return this._translateService.langs;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._initializeForm();
|
||||
return this.profileKeys.some(key => this.initialFormValue[key] !== this.form.get(key).value);
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
@ -106,15 +120,17 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
|
||||
}
|
||||
|
||||
if (this.languageChanged) {
|
||||
await this._languageService.change(this.form.get('language').value);
|
||||
await this._languageService.change(this.form.controls.language.value);
|
||||
await this._pdfViewer.instance?.UI.setLanguage(this._languageService.currentLanguage);
|
||||
}
|
||||
|
||||
if (this.themeChanged) {
|
||||
await this._userPreferenceService.saveTheme(this.form.get('darkTheme').value ? 'dark' : 'light');
|
||||
await this._userPreferenceService.saveTheme(this.form.controls.darkTheme.value ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
this._initializeForm();
|
||||
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
this._changeRef.markForCheck();
|
||||
this._loadingService.stop();
|
||||
this._toaster.success(_('user-profile-screen.update.success'));
|
||||
} catch (e) {
|
||||
this._loadingService.stop();
|
||||
@ -125,35 +141,13 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
|
||||
await this._userService.createResetPasswordAction();
|
||||
}
|
||||
|
||||
private _getForm(): UntypedFormGroup {
|
||||
#getForm() {
|
||||
return this._formBuilder.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
firstName: [''],
|
||||
lastName: [''],
|
||||
language: [''],
|
||||
darkTheme: [false],
|
||||
email: [this._userService.currentUser.email ?? '', [Validators.required, Validators.email]],
|
||||
firstName: [this._userService.currentUser.firstName ?? ''],
|
||||
lastName: [this._userService.currentUser.lastName ?? ''],
|
||||
language: [this._userPreferenceService.getLanguage()],
|
||||
darkTheme: [this._userPreferenceService.getTheme() === 'dark'],
|
||||
});
|
||||
}
|
||||
|
||||
private _initializeForm(): void {
|
||||
try {
|
||||
this.form = this._getForm();
|
||||
if (!this._permissionsService.has(Roles.updateMyProfile)) {
|
||||
this.form.disable();
|
||||
}
|
||||
this.#profileModel = {
|
||||
email: this._userService.currentUser.email ?? '',
|
||||
firstName: this._userService.currentUser.firstName ?? '',
|
||||
lastName: this._userService.currentUser.lastName ?? '',
|
||||
language: this._languageService.currentLanguage ?? '',
|
||||
darkTheme: this._userPreferenceService.getTheme() === 'dark',
|
||||
};
|
||||
this.form.patchValue(this.#profileModel, { emitEvent: false });
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
this._loadingService.stop();
|
||||
this._changeRef.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,18 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { addHintTranslations } from '@translations/add-hint-translations';
|
||||
import { redactTextTranslations } from '@translations/redact-text-translations';
|
||||
import { removeRedactionTranslations } from '@translations/remove-redaction-translations';
|
||||
import { RedactOrHintOptions, RemoveRedactionOptions } from '../../file-preview/utils/dialog-types';
|
||||
import {
|
||||
ForceAnnotationOptions,
|
||||
RectangleRedactOptions,
|
||||
RedactOrHintOptions,
|
||||
RemoveRedactionOptions,
|
||||
} from '../../file-preview/utils/dialog-types';
|
||||
|
||||
export const SystemDefaults = {
|
||||
RECTANGLE_REDACT_DEFAULT: RectangleRedactOptions.ONLY_THIS_PAGE,
|
||||
ADD_REDACTION_DEFAULT: RedactOrHintOptions.IN_DOSSIER,
|
||||
ADD_HINT_DEFAULT: RedactOrHintOptions.IN_DOSSIER,
|
||||
FORCE_REDACTION_DEFAULT: ForceAnnotationOptions.ONLY_HERE,
|
||||
REMOVE_REDACTION_DEFAULT: RemoveRedactionOptions.ONLY_HERE,
|
||||
REMOVE_HINT_DEFAULT: RemoveRedactionOptions.ONLY_HERE,
|
||||
REMOVE_RECOMMENDATION_DEFAULT: RemoveRedactionOptions.DO_NOT_RECOMMEND,
|
||||
@ -28,6 +35,10 @@ export const redactionAddOptions = [
|
||||
label: redactTextTranslations.onlyHere.label,
|
||||
value: RedactOrHintOptions.ONLY_HERE,
|
||||
},
|
||||
{
|
||||
label: redactTextTranslations.inDocument.label,
|
||||
value: RedactOrHintOptions.IN_DOCUMENT,
|
||||
},
|
||||
{
|
||||
label: redactTextTranslations.inDossier.label,
|
||||
value: RedactOrHintOptions.IN_DOSSIER,
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
<div class="mt-44">
|
||||
<redaction-donut-chart
|
||||
[config]="chartConfig"
|
||||
[config]="chartConfig()"
|
||||
[radius]="63"
|
||||
[strokeWidth]="15"
|
||||
[subtitles]="['user-stats.chart.users' | translate]"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, input, Input, output, Output } from '@angular/core';
|
||||
import { DonutChartConfig } from '@red/domain';
|
||||
import { CircleButtonComponent } from '@iqser/common-ui';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@ -12,6 +12,6 @@ import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.
|
||||
imports: [CircleButtonComponent, TranslateModule, DonutChartComponent],
|
||||
})
|
||||
export class UsersStatsComponent {
|
||||
@Output() toggleCollapse = new EventEmitter();
|
||||
@Input() chartConfig: DonutChartConfig[];
|
||||
readonly chartConfig = input.required<DonutChartConfig[]>();
|
||||
readonly toggleCollapse = output();
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div
|
||||
[translateParams]="{
|
||||
type: user ? 'edit' : 'create'
|
||||
type: !!user() ? 'edit' : 'create',
|
||||
}"
|
||||
[translate]="'add-edit-user.title'"
|
||||
class="dialog-header heading-l"
|
||||
@ -37,9 +37,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!user()) {
|
||||
<div class="iqser-input-group">
|
||||
<label [translate]="'add-edit-user.form.account-setup'"></label>
|
||||
<mat-checkbox formControlName="sendSetPasswordMail">{{ 'add-edit-user.form.send-email' | translate }}</mat-checkbox>
|
||||
<span [translate]="'add-edit-user.form.send-email-explanation'" class="hint"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
(click)="toggleResetPassword.emit()"
|
||||
*ngIf="!!user"
|
||||
*ngIf="!!user()"
|
||||
[translate]="'add-edit-user.form.reset-password'"
|
||||
class="mt-24 fit-content link-action"
|
||||
></div>
|
||||
@ -48,14 +56,14 @@
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
[disabled]="form.invalid || !changed"
|
||||
[label]="(user ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save') | translate"
|
||||
[label]="(user() ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save') | translate"
|
||||
[submit]="true"
|
||||
[type]="iconButtonTypes.primary"
|
||||
></iqser-icon-button>
|
||||
|
||||
<iqser-icon-button
|
||||
(action)="delete()"
|
||||
*ngIf="user && !disabledDelete(user)"
|
||||
*ngIf="user() && !disabledDelete(user())"
|
||||
[label]="'add-edit-user.actions.delete' | translate"
|
||||
[type]="iconButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
|
||||
@ -5,3 +5,7 @@
|
||||
margin-top: 8px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-left: 23px;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import { Component, input, OnInit, output } from '@angular/core';
|
||||
import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { AdminDialogService } from '../../../services/admin-dialog.service';
|
||||
import { BaseFormComponent, IconButtonComponent, LoadingService, Toaster } from '@iqser/common-ui';
|
||||
@ -20,15 +20,15 @@ import { NgForOf, NgIf } from '@angular/common';
|
||||
standalone: true,
|
||||
imports: [TranslateModule, ReactiveFormsModule, MatCheckbox, NgForOf, IconButtonComponent, NgIf],
|
||||
})
|
||||
export class UserDetailsComponent extends BaseFormComponent implements OnChanges {
|
||||
/** e.g. a RED_ADMIN is automatically a RED_USER_ADMIN => can't disable RED_USER_ADMIN as long as RED_ADMIN is checked */
|
||||
private readonly _ROLE_REQUIREMENTS = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
|
||||
@Input() user: User;
|
||||
@Output() readonly toggleResetPassword = new EventEmitter();
|
||||
@Output() readonly closeDialog = new EventEmitter();
|
||||
@Output() readonly cancel = new EventEmitter();
|
||||
export class UserDetailsComponent extends BaseFormComponent implements OnInit {
|
||||
user = input<User>();
|
||||
readonly toggleResetPassword = output();
|
||||
readonly closeDialog = output<boolean>();
|
||||
readonly cancel = output();
|
||||
readonly ROLES = ['RED_USER', 'RED_MANAGER', 'RED_USER_ADMIN', 'RED_ADMIN'];
|
||||
readonly translations = rolesTranslations;
|
||||
/** e.g. a RED_ADMIN is automatically a RED_USER_ADMIN => can't disable RED_USER_ADMIN as long as RED_ADMIN is checked */
|
||||
readonly #ROLE_REQUIREMENTS = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
|
||||
|
||||
constructor(
|
||||
private readonly _formBuilder: UntypedFormBuilder,
|
||||
@ -49,13 +49,17 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
|
||||
}, []);
|
||||
}
|
||||
|
||||
private get _rolesControls(): any {
|
||||
get sendSetPasswordMail() {
|
||||
return !this.form.controls.sendSetPasswordMail.value;
|
||||
}
|
||||
|
||||
get #rolesControls() {
|
||||
return this.ROLES.reduce(
|
||||
(prev, role) => ({
|
||||
...prev,
|
||||
[role]: [
|
||||
{
|
||||
value: this.user && this.user.has(role),
|
||||
value: this.user() && this.user().has(role),
|
||||
disabled: this.shouldBeDisabled(role),
|
||||
},
|
||||
],
|
||||
@ -64,20 +68,20 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.form = this._getForm();
|
||||
ngOnInit() {
|
||||
this.form = this.#getForm();
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
}
|
||||
|
||||
shouldBeDisabled(role: string): boolean {
|
||||
const isCurrentAdminUser = this.user && this.user.isAdmin && this.user.id === this._userService.currentUser.id;
|
||||
const isCurrentAdminUser = this.user() && this.user().isAdmin && this.user().id === this._userService.currentUser.id;
|
||||
return (
|
||||
// RED_ADMIN can't remove own RED_ADMIN role
|
||||
(role === 'RED_ADMIN' && isCurrentAdminUser) ||
|
||||
// only RED_ADMINs can edit RED_ADMIN roles
|
||||
(role === 'RED_ADMIN' && !this._userService.currentUser.isAdmin) ||
|
||||
Object.keys(this._ROLE_REQUIREMENTS).reduce(
|
||||
(value, key) => value || (role === this._ROLE_REQUIREMENTS[key] && this.user?.roles.includes(key)),
|
||||
Object.keys(this.#ROLE_REQUIREMENTS).reduce(
|
||||
(value, key) => value || (role === this.#ROLE_REQUIREMENTS[key] && this.user()?.roles.includes(key)),
|
||||
false,
|
||||
)
|
||||
);
|
||||
@ -85,38 +89,38 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
|
||||
|
||||
async save() {
|
||||
this._loadingService.start();
|
||||
const userData: IProfileUpdateRequest = { ...this.form.getRawValue(), roles: this.activeRoles };
|
||||
const userData: IProfileUpdateRequest = {
|
||||
...this.form.getRawValue(),
|
||||
roles: this.activeRoles,
|
||||
sendSetPasswordMail: this.sendSetPasswordMail,
|
||||
};
|
||||
|
||||
if (!this.user) {
|
||||
if (!this.user()) {
|
||||
await firstValueFrom(this._userService.create(userData))
|
||||
.then(() => {
|
||||
this.closeDialog.emit(true);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === HttpStatusCode.Conflict) {
|
||||
this._toaster.error(_('add-edit-user.error.email-already-used'));
|
||||
} else {
|
||||
this._toaster.error(_('add-edit-user.error.generic'));
|
||||
}
|
||||
this._toaster.error(null, { error });
|
||||
this._loadingService.stop();
|
||||
});
|
||||
} else {
|
||||
await firstValueFrom(this._userService.updateProfile(userData, this.user.id));
|
||||
await firstValueFrom(this._userService.updateProfile(userData, this.user().id));
|
||||
this.closeDialog.emit(true);
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this._dialogService.deleteUsers([this.user.id], () => this.closeDialog.emit(true));
|
||||
this._dialogService.deleteUsers([this.user().id], () => this.closeDialog.emit(true));
|
||||
}
|
||||
|
||||
setRolesRequirements(checked: boolean, role: string): void {
|
||||
if (Object.keys(this._ROLE_REQUIREMENTS).includes(role)) {
|
||||
if (Object.keys(this.#ROLE_REQUIREMENTS).includes(role)) {
|
||||
if (checked) {
|
||||
this.form.patchValue({ [this._ROLE_REQUIREMENTS[role]]: true });
|
||||
this.form.controls[this._ROLE_REQUIREMENTS[role]].disable();
|
||||
this.form.patchValue({ [this.#ROLE_REQUIREMENTS[role]]: true });
|
||||
this.form.controls[this.#ROLE_REQUIREMENTS[role]].disable();
|
||||
} else {
|
||||
this.form.controls[this._ROLE_REQUIREMENTS[role]].enable();
|
||||
this.form.controls[this.#ROLE_REQUIREMENTS[role]].enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,18 +131,19 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
|
||||
return user.id === this._userService.currentUser.id || (userAdmin && !currentUserAdmin);
|
||||
}
|
||||
|
||||
private _getForm(): UntypedFormGroup {
|
||||
#getForm(): UntypedFormGroup {
|
||||
return this._formBuilder.group({
|
||||
firstName: [this.user?.firstName, Validators.required],
|
||||
lastName: [this.user?.lastName, Validators.required],
|
||||
firstName: [this.user()?.firstName, Validators.required],
|
||||
lastName: [this.user()?.lastName, Validators.required],
|
||||
email: [
|
||||
{
|
||||
value: this.user?.email,
|
||||
disabled: !!this.user?.email,
|
||||
value: this.user()?.email,
|
||||
disabled: !!this.user()?.email,
|
||||
},
|
||||
[Validators.required, Validators.email],
|
||||
],
|
||||
...this._rolesControls,
|
||||
...this.#rolesControls,
|
||||
sendSetPasswordMail: [false],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { auditCategoriesTranslations } from '@translations/audit-categories-translations';
|
||||
import { Roles } from '@users/roles';
|
||||
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AdminDialogService } from '../../services/admin-dialog.service';
|
||||
import { AuditService } from '../../services/audit.service';
|
||||
@ -139,16 +139,9 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnI
|
||||
const promises = [];
|
||||
const category = this.form.get('category').value;
|
||||
const userId = this.form.get('userId').value;
|
||||
const from = this.form.get('from').value;
|
||||
let to = this.form.get('to').value;
|
||||
if (to) {
|
||||
const hoursLeft = new Date(to).getHours();
|
||||
const minutesLeft = new Date(to).getMinutes();
|
||||
to = to
|
||||
.clone()
|
||||
.add(24 - hoursLeft - 1, 'h')
|
||||
.add(60 - minutesLeft - 1);
|
||||
}
|
||||
const from = this.form.get('from').value ? dayjs(this.form.get('from').value).startOf('day').toISOString() : null;
|
||||
const to = this.form.get('to').value ? dayjs(this.form.get('to').value).endOf('day').toISOString() : null;
|
||||
|
||||
const logsRequestBody: IAuditSearchRequest = {
|
||||
pageSize: PAGE_SIZE,
|
||||
page: page,
|
||||
|
||||
@ -42,6 +42,15 @@
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div class="iqser-input-group required w-150">
|
||||
<label translate="add-edit-component-mapping.form.quote-char"></label>
|
||||
<input
|
||||
[placeholder]="'add-edit-component-mapping.form.quote-char-placeholder' | translate"
|
||||
formControlName="quoteChar"
|
||||
name="quoteChar"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group required w-150">
|
||||
<label translate="add-edit-component-mapping.form.encoding-type"></label>
|
||||
|
||||
@ -17,12 +17,14 @@ interface DialogData {
|
||||
dossierTemplateId: string;
|
||||
mapping: IComponentMapping;
|
||||
}
|
||||
|
||||
interface DialogResult {
|
||||
id: string;
|
||||
name: string;
|
||||
file: Blob;
|
||||
encoding: string;
|
||||
delimiter: string;
|
||||
quoteChar: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
@ -72,14 +74,14 @@ export class AddEditComponentMappingDialogComponent
|
||||
const file = new Blob([fileContent.body as Blob], { type: 'text/csv' });
|
||||
this.form.get('file').setValue(file);
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
this.#disableEncodingAndDelimiter();
|
||||
this.#disableEncodingAndQuoteCharAndDelimiter();
|
||||
}
|
||||
}
|
||||
|
||||
changeFile(file: File) {
|
||||
this.form.get('file').setValue(file);
|
||||
this.form.get('fileName').setValue(file?.name);
|
||||
this.#enableEncodingAndDelimiter();
|
||||
this.#enableEncodingAndQuoteCharAndDelimiter();
|
||||
}
|
||||
|
||||
save() {
|
||||
@ -93,16 +95,19 @@ export class AddEditComponentMappingDialogComponent
|
||||
fileName: [this.data?.mapping?.fileName, Validators.required],
|
||||
encoding: this.encodingTypeOptions.find(e => e === this.data?.mapping?.encoding) ?? this.encodingTypeOptions[0],
|
||||
delimiter: [this.data?.mapping?.delimiter ?? ',', Validators.required],
|
||||
quoteChar: [this.data?.mapping?.quoteChar ?? '"', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
#disableEncodingAndDelimiter() {
|
||||
#disableEncodingAndQuoteCharAndDelimiter() {
|
||||
this.form.get('encoding').disable();
|
||||
this.form.get('delimiter').disable();
|
||||
this.form.get('quoteChar').disable();
|
||||
}
|
||||
|
||||
#enableEncodingAndDelimiter() {
|
||||
#enableEncodingAndQuoteCharAndDelimiter() {
|
||||
this.form.get('encoding').enable();
|
||||
this.form.get('delimiter').enable();
|
||||
this.form.get('quoteChar').enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,8 +99,8 @@ export default class ComponentMappingsScreenComponent extends ListingComponent<C
|
||||
const result = await dialog.result();
|
||||
if (result) {
|
||||
this._loadingService.start();
|
||||
const { id, name, encoding, delimiter, fileName } = result;
|
||||
const newMapping = { id, name, encoding, delimiter, fileName };
|
||||
const { id, name, encoding, delimiter, fileName, quoteChar } = result;
|
||||
const newMapping = { id, name, encoding, delimiter, fileName, quoteChar };
|
||||
await firstValueFrom(
|
||||
this._componentMappingService.createUpdateComponentMapping(this.#dossierTemplateId, newMapping, result.file),
|
||||
);
|
||||
|
||||
@ -91,7 +91,7 @@ export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent
|
||||
const createOrUpdate = this._dossierAttributesService.createOrUpdate(attribute, this.data.dossierTemplateId);
|
||||
const result = await createOrUpdate.catch((error: HttpErrorResponse) => {
|
||||
this._loadingService.stop();
|
||||
this._toaster.error(_('add-edit-dossier-attribute.error.generic'), { error });
|
||||
this._toaster.rawError(error.error.message);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
(action)="save()"
|
||||
[buttonId]="'save-dossier-state'"
|
||||
[disabled]="disabled"
|
||||
[label]="'add-edit-dossier-state.save' | translate"
|
||||
[submit]="true"
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div [innerHTML]="'confirm-delete-dossier-state.warning' | translate : translateArgs" class="heading"></div>
|
||||
<div [innerHTML]="'confirm-delete-dossier-state.warning' | translate: translateArgs" class="heading"></div>
|
||||
|
||||
<form *ngIf="data.dossierCount !== 0 && data.otherStates.length > 0" [formGroup]="form" class="mt-16">
|
||||
<div class="iqser-input-group">
|
||||
<mat-checkbox color="primary" formControlName="replace">
|
||||
{{ 'confirm-delete-dossier-state.question' | translate : { count: data.dossierCount } }}
|
||||
{{ 'confirm-delete-dossier-state.question' | translate: { count: data.dossierCount } }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@ -30,7 +30,12 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button (action)="save()" [label]="label | translate" [type]="iconButtonTypes.primary"></iqser-icon-button>
|
||||
<iqser-icon-button
|
||||
(action)="save()"
|
||||
[buttonId]="'confirm-delete-dossier-state'"
|
||||
[label]="label | translate"
|
||||
[type]="iconButtonTypes.primary"
|
||||
></iqser-icon-button>
|
||||
<div [translate]="'confirm-delete-dossier-state.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -13,18 +13,20 @@
|
||||
<span class="small-label">{{ state.dossierCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<div [id]="'dossier_' + (state.name | snakeCase)" class="cell">
|
||||
<div *ngIf="permissionsService.canPerformDossierStatesActions()" class="action-buttons">
|
||||
<div [attr.help-mode-key]="'edit_delete_dossier_state'">
|
||||
<iqser-circle-button
|
||||
(action)="openEditStateDialog(state)"
|
||||
[tooltip]="'dossier-states-listing.action.edit' | translate"
|
||||
[buttonId]="'dossier-state-edit-button'"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="openConfirmDeleteStateDialog(state)"
|
||||
[tooltip]="'dossier-states-listing.action.delete' | translate"
|
||||
[buttonId]="'dossier-state-delete-button'"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
@ -14,13 +14,14 @@ import {
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { SnakeCasePipe } from '@common-ui/pipes/snake-case.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-states-table-item',
|
||||
templateUrl: './dossier-states-table-item.component.html',
|
||||
styleUrls: ['./dossier-states-table-item.component.scss'],
|
||||
standalone: true,
|
||||
imports: [MatTooltip, CircleButtonComponent, TranslateModule, NgIf],
|
||||
imports: [MatTooltip, CircleButtonComponent, TranslateModule, NgIf, SnakeCasePipe],
|
||||
})
|
||||
export class DossierStatesTableItemComponent {
|
||||
readonly #dialog = inject(MatDialog);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<div class="dialog-header">
|
||||
<div class="heading-l" translate="general-config-screen.general.title"></div>
|
||||
<div translate="general-config-screen.general.subtitle"></div>
|
||||
</div>
|
||||
<form (submit)="save()" *ngIf="form" [formGroup]="form">
|
||||
<div class="dialog-content">
|
||||
|
||||
@ -11,7 +11,12 @@
|
||||
<div class="dialog mt-24 mb-0">
|
||||
<redaction-system-preferences-form></redaction-system-preferences-form>
|
||||
</div>
|
||||
<div class="dialog mt-24">
|
||||
<redaction-smtp-form></redaction-smtp-form>
|
||||
</div>
|
||||
|
||||
@if (smtpLicenseFeatureEnabled) {
|
||||
<div class="dialog mt-24">
|
||||
<redaction-smtp-form></redaction-smtp-form>
|
||||
</div>
|
||||
} @else {
|
||||
<div style="visibility: hidden" class="dialog mt-24"></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,8 @@ import { BaseFormComponent, IqserListingModule } from '@iqser/common-ui';
|
||||
import { SystemPreferencesFormComponent } from './system-preferences-form/system-preferences-form.component';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LicenseService } from '@services/license.service';
|
||||
import { ILicenseFeature } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-general-config-screen',
|
||||
@ -17,15 +19,17 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
export class GeneralConfigScreenComponent extends BaseFormComponent implements AfterViewInit {
|
||||
readonly currentUser = inject(UserService).currentUser;
|
||||
readonly routerHistoryService = inject(RouterHistoryService);
|
||||
readonly licenseService = inject(LicenseService);
|
||||
|
||||
@ViewChild(GeneralConfigFormComponent) generalConfigFormComponent: GeneralConfigFormComponent;
|
||||
@ViewChild(SystemPreferencesFormComponent) systemPreferencesFormComponent: SystemPreferencesFormComponent;
|
||||
@ViewChild(SmtpFormComponent) smtpFormComponent: SmtpFormComponent;
|
||||
children: BaseFormComponent[];
|
||||
smtpLicenseFeatureEnabled: boolean;
|
||||
|
||||
get changed(): boolean {
|
||||
for (const child of this.children) {
|
||||
if (child.changed) {
|
||||
if (child?.changed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -43,6 +47,8 @@ export class GeneralConfigScreenComponent extends BaseFormComponent implements A
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.children = [this.generalConfigFormComponent, this.systemPreferencesFormComponent, this.smtpFormComponent];
|
||||
let licenseFeature: ILicenseFeature = this.licenseService.getFeature('configurableSMTPServer');
|
||||
this.smtpLicenseFeatureEnabled = licenseFeature != null && licenseFeature.value === true;
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
|
||||
@ -28,7 +28,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { SelectComponent } from '@shared/components/select/select.component';
|
||||
import { MatSuffix } from '@angular/material/form-field';
|
||||
|
||||
const downloadTypes = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(type => ({
|
||||
const downloadTypes = ['ORIGINAL', 'PREVIEW', 'OPTIMIZED_PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(type => ({
|
||||
key: type,
|
||||
label: downloadTypesTranslations[type],
|
||||
}));
|
||||
|
||||
@ -17,8 +17,17 @@
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div class="iqser-input-group">
|
||||
<label translate="add-edit-entity.form.technical-name"></label>
|
||||
<div class="technical-name">{{ this.technicalName() || '-' }}</div>
|
||||
<span
|
||||
[translateParams]="{ type: data.justification ? 'edit' : 'create' }"
|
||||
[translate]="'add-edit-entity.form.technical-name-hint'"
|
||||
class="hint"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group required w-400">
|
||||
<div class="iqser-input-group w-400">
|
||||
<label translate="add-edit-justification.form.reason"></label>
|
||||
<input
|
||||
[placeholder]="'add-edit-justification.form.reason-placeholder' | translate"
|
||||
@ -28,7 +37,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group required w-400">
|
||||
<div class="iqser-input-group w-400">
|
||||
<label translate="add-edit-justification.form.description"></label>
|
||||
<textarea
|
||||
[placeholder]="'add-edit-justification.form.description-placeholder' | translate"
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, Inject, untracked } from '@angular/core';
|
||||
import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Justification } from '@red/domain';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
|
||||
import { BaseDialogComponent, CircleButtonComponent, HasScrollbarDirective, IconButtonComponent } from '@iqser/common-ui';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { formControlToSignal } from '@utils/functions';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
interface DialogData {
|
||||
justification?: Justification;
|
||||
@ -16,9 +18,29 @@ interface DialogData {
|
||||
templateUrl: './add-edit-justification-dialog.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TranslateModule, ReactiveFormsModule, IconButtonComponent, CircleButtonComponent],
|
||||
imports: [TranslateModule, ReactiveFormsModule, IconButtonComponent, CircleButtonComponent, HasScrollbarDirective],
|
||||
})
|
||||
export class AddEditJustificationDialogComponent extends BaseDialogComponent {
|
||||
readonly form = this.#getForm();
|
||||
readonly name = formControlToSignal(this.form.controls['name']);
|
||||
readonly allJustifications = toSignal(this._justificationService.all$);
|
||||
readonly technicalName = computed(() => {
|
||||
if (this.data.justification) {
|
||||
return this.data.justification.technicalName;
|
||||
}
|
||||
if (!this.name()) {
|
||||
return null;
|
||||
}
|
||||
let currentTechnicalName = Justification.toTechnicalName(this.name());
|
||||
const existingTechnicalNames = untracked(this.allJustifications).map(justification => justification.technicalName);
|
||||
let suffix = 1;
|
||||
while (existingTechnicalNames.includes(currentTechnicalName)) {
|
||||
currentTechnicalName =
|
||||
currentTechnicalName === '_' ? `${currentTechnicalName}${suffix++}` : [currentTechnicalName, suffix++].join('_');
|
||||
}
|
||||
return currentTechnicalName;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly _justificationService: JustificationsService,
|
||||
protected readonly _dialogRef: MatDialogRef<AddEditJustificationDialogComponent>,
|
||||
@ -26,7 +48,6 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
|
||||
) {
|
||||
super(_dialogRef, !!data.justification);
|
||||
|
||||
this.form = this._getForm();
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
}
|
||||
|
||||
@ -34,7 +55,8 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
|
||||
const dossierTemplateId = this.data.dossierTemplateId;
|
||||
this._loadingService.start();
|
||||
try {
|
||||
await firstValueFrom(this._justificationService.createOrUpdate(this.form.getRawValue() as Justification, dossierTemplateId));
|
||||
const formValue = { ...this.form.getRawValue(), technicalName: this.technicalName() };
|
||||
await firstValueFrom(this._justificationService.createOrUpdate(formValue as Justification, dossierTemplateId));
|
||||
await firstValueFrom(this._justificationService.loadAll(dossierTemplateId));
|
||||
this._dialogRef.close(true);
|
||||
} catch (error) {
|
||||
@ -43,11 +65,12 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
private _getForm(): UntypedFormGroup {
|
||||
#getForm(): UntypedFormGroup {
|
||||
return this._formBuilder.group({
|
||||
name: [{ value: this.data.justification?.name, disabled: !!this.data.justification }, Validators.required],
|
||||
reason: [this.data.justification?.reason, Validators.required],
|
||||
description: [this.data.justification?.description, Validators.required],
|
||||
reason: [this.data.justification?.reason],
|
||||
description: [this.data.justification?.description],
|
||||
technicalName: [this.data.justification?.technicalName ?? null],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ChartDataset } from 'chart.js';
|
||||
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
|
||||
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
|
||||
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonth } from '../../utils/functions';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { size } from '@iqser/common-ui/lib/utils';
|
||||
@ -43,7 +43,7 @@ export class LicenseAnalysisCapacityUsageComponent {
|
||||
#getCapacityDatasets(): ChartDataset[] {
|
||||
const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
|
||||
const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
|
||||
if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
|
||||
if (monthlyData.length === 1 || isCurrentMonth(monthlyData[0].startDate)) {
|
||||
const empty = { analysedFilesBytes: null } as ILicenseData;
|
||||
dataUntilCurrentMonth.splice(0, 0, empty);
|
||||
monthlyData.splice(0, 0, empty);
|
||||
@ -60,11 +60,8 @@ export class LicenseAnalysisCapacityUsageComponent {
|
||||
},
|
||||
|
||||
{
|
||||
data: dataUntilCurrentMonth.map((month, monthIndex) =>
|
||||
month.analysedFilesBytes
|
||||
? month.analysedFilesBytes +
|
||||
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.analysedFilesBytes ?? 0), 0)
|
||||
: 0,
|
||||
data: dataUntilCurrentMonth.map((_, monthIndex) =>
|
||||
monthlyData.slice(0, monthIndex + 1).reduce((acc, curr) => acc + (curr.analysedFilesBytes ?? 0), 0),
|
||||
),
|
||||
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-cumulative'),
|
||||
yAxisID: 'y1',
|
||||
|
||||
@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ChartDataset } from 'chart.js';
|
||||
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
|
||||
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
|
||||
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonth } from '../../utils/functions';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ILicenseData } from '@red/domain';
|
||||
@ -40,7 +40,7 @@ export class LicensePageUsageComponent {
|
||||
#getPagesDatasets(): ChartDataset[] {
|
||||
const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
|
||||
const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
|
||||
if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
|
||||
if (monthlyData.length === 1 || isCurrentMonth(monthlyData[0].startDate)) {
|
||||
const empty = { numberOfAnalyzedPages: null } as ILicenseData;
|
||||
dataUntilCurrentMonth.splice(0, 0, empty);
|
||||
monthlyData.splice(0, 0, empty);
|
||||
@ -63,11 +63,8 @@ export class LicensePageUsageComponent {
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
data: dataUntilCurrentMonth.map((month, monthIndex) =>
|
||||
month.numberOfAnalyzedPages
|
||||
? month.numberOfAnalyzedPages +
|
||||
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.numberOfAnalyzedPages ?? 0), 0)
|
||||
: 0,
|
||||
data: dataUntilCurrentMonth.map((_, monthIndex) =>
|
||||
monthlyData.slice(0, monthIndex + 1).reduce((acc, curr) => acc + (curr.numberOfAnalyzedPages ?? 0), 0),
|
||||
),
|
||||
label: this._translateService.instant('license-info-screen.page-usage.cumulative-pages'),
|
||||
yAxisID: 'y1',
|
||||
|
||||
@ -78,9 +78,9 @@ export class LicenseRetentionCapacityComponent {
|
||||
|
||||
return [
|
||||
{
|
||||
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes),
|
||||
label: this._translateService.instant('license-info-screen.retention-capacity-usage.active-documents'),
|
||||
...getLineConfig(ChartGreen, false, 'origin'),
|
||||
data: monthlyData.flatMap(d => d.trashFilesUploadedBytes),
|
||||
label: this._translateService.instant('license-info-screen.retention-capacity-usage.trash-documents'),
|
||||
...getLineConfig(ChartRed, false, 'origin'),
|
||||
stack: 'storage',
|
||||
},
|
||||
{
|
||||
@ -90,9 +90,9 @@ export class LicenseRetentionCapacityComponent {
|
||||
stack: 'storage',
|
||||
},
|
||||
{
|
||||
data: monthlyData.flatMap(d => d.trashFilesUploadedBytes),
|
||||
label: this._translateService.instant('license-info-screen.retention-capacity-usage.trash-documents'),
|
||||
...getLineConfig(ChartRed, false, '-1'),
|
||||
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes),
|
||||
label: this._translateService.instant('license-info-screen.retention-capacity-usage.active-documents'),
|
||||
...getLineConfig(ChartGreen, false, 'origin'),
|
||||
stack: 'storage',
|
||||
},
|
||||
{
|
||||
|
||||
@ -6,7 +6,6 @@ import { ComplexFillTarget } from 'chart.js/dist/types';
|
||||
|
||||
const monthNames = dayjs.monthsShort();
|
||||
const currentMonth = dayjs(Date.now()).month();
|
||||
const currentYear = dayjs(Date.now()).year();
|
||||
|
||||
export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
|
||||
|
||||
@ -45,7 +44,7 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
|
||||
monthIterator = monthIterator.add(1, 'month');
|
||||
}
|
||||
|
||||
if (startMonth.month() === endMonth.month() || startMonth.month() === currentMonth) {
|
||||
if (startMonth.isSame(endMonth, 'month') || isCurrentMonth(startMonth.toDate())) {
|
||||
result.splice(0, 0, '');
|
||||
}
|
||||
|
||||
@ -53,9 +52,9 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
|
||||
};
|
||||
|
||||
export const getDataUntilCurrentMonth = (monthlyData: ILicenseData[]) => {
|
||||
return monthlyData.filter(data => dayjs(data.startDate).month() <= currentMonth && dayjs(data.startDate).year() <= currentYear);
|
||||
return monthlyData.filter(data => dayjs(data.startDate).isSameOrBefore(dayjs(Date.now()), 'month'));
|
||||
};
|
||||
|
||||
export const isCurrentMonthAndYear = (date: Date | string) => {
|
||||
return dayjs(date).month() === currentMonth && dayjs(date).year() === currentYear;
|
||||
export const isCurrentMonth = (date: Date | string) => {
|
||||
return dayjs(date).isSame(dayjs(Date.now()), 'month');
|
||||
};
|
||||
|
||||
@ -2,64 +2,74 @@
|
||||
<div [translate]="'reports-screen.title'" class="heading-xl"></div>
|
||||
|
||||
<div [translate]="'reports-screen.setup'" class="description"></div>
|
||||
<div *ngIf="!isDocumine" [translate]="'reports-screen.description'" class="description"></div>
|
||||
|
||||
<div *ngIf="!isDocumine && placeholders$ | async as placeholders" class="placeholders">
|
||||
<div [translate]="'reports-screen.table-header.placeholders'" class="all-caps-label"></div>
|
||||
<div [translate]="'reports-screen.table-header.description'" class="all-caps-label"></div>
|
||||
<ng-container *ngFor="let placeholder of placeholders">
|
||||
<div class="placeholder">{{ placeholder.placeholder }}</div>
|
||||
<div
|
||||
[innerHTML]="placeholder.descriptionTranslation | translate: { attribute: placeholder.attributeName }"
|
||||
class="description"
|
||||
></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@if (!isDocumine) {
|
||||
<div [translate]="'reports-screen.description'" class="description"></div>
|
||||
}
|
||||
@if (!isDocumine && placeholders$ | async; as placeholders) {
|
||||
<div class="placeholders">
|
||||
<div [translate]="'reports-screen.table-header.placeholders'" class="all-caps-label"></div>
|
||||
<div [translate]="'reports-screen.table-header.description'" class="all-caps-label"></div>
|
||||
@for (placeholder of placeholders; track placeholder.placeholder) {
|
||||
<div class="placeholder">{{ placeholder.placeholder }}</div>
|
||||
<div
|
||||
[innerHTML]="placeholder.descriptionTranslation | translate: { attribute: placeholder.attributeName }"
|
||||
class="description"
|
||||
></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div *ngIf="availableTemplates$ | async as availableTemplates" class="right-container" iqserHasScrollbar>
|
||||
<div class="header">
|
||||
<div [translate]="'reports-screen.report-documents'" class="heading"></div>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="fileInput.click()"
|
||||
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin"
|
||||
[tooltip]="'reports-screen.upload-document' | translate"
|
||||
[attr.help-mode-key]="'upload_report'"
|
||||
icon="iqser:upload"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
(click)="fileInput.click()"
|
||||
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin && !availableTemplates?.length"
|
||||
[translate]="'reports-screen.upload-document'"
|
||||
class="template upload-button"
|
||||
></div>
|
||||
|
||||
<div *ngFor="let template of availableTemplates" class="template">
|
||||
<div class="name">
|
||||
{{ template.fileName }} {{ template.multiFileReport ? ('reports-screen.multi-file-report' | translate) : '' }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<iqser-circle-button
|
||||
(action)="download(template)"
|
||||
*allow="roles.reportTemplates.download"
|
||||
[iconSize]="12"
|
||||
[size]="18"
|
||||
icon="iqser:download"
|
||||
></iqser-circle-button>
|
||||
@if (availableTemplates$ | async; as availableTemplates) {
|
||||
<div class="right-container" iqserHasScrollbar>
|
||||
<div class="header">
|
||||
<div [translate]="'reports-screen.report-documents'" class="heading"></div>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="deleteTemplate(template)"
|
||||
*allow="roles.reportTemplates.delete; if: currentUser.isAdmin"
|
||||
[iconSize]="12"
|
||||
[size]="18"
|
||||
icon="iqser:trash"
|
||||
(action)="fileInput.click()"
|
||||
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin"
|
||||
[tooltip]="'reports-screen.upload-document' | translate"
|
||||
[attr.help-mode-key]="'upload_report'"
|
||||
[buttonId]="'upload_report'"
|
||||
icon="iqser:upload"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
(click)="fileInput.click()"
|
||||
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin && !availableTemplates?.length"
|
||||
[translate]="'reports-screen.upload-document'"
|
||||
class="template upload-button"
|
||||
></div>
|
||||
|
||||
@for (template of availableTemplates; track template.templateId) {
|
||||
<div [id]="template.fileName | snakeCase" class="template">
|
||||
<div class="name">
|
||||
{{ template.fileName }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<iqser-circle-button
|
||||
(action)="download(template)"
|
||||
*allow="roles.reportTemplates.download"
|
||||
[buttonId]="(template.fileName | snakeCase) + '-download-button'"
|
||||
[iconSize]="12"
|
||||
[size]="18"
|
||||
icon="iqser:download"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="deleteTemplate(template)"
|
||||
*allow="roles.reportTemplates.delete; if: currentUser.isAdmin"
|
||||
[buttonId]="(template.fileName | snakeCase) + '-delete-button'"
|
||||
[iconSize]="12"
|
||||
[size]="18"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<input #fileInput (change)="uploadTemplate($event)" hidden type="file" />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, viewChild } from '@angular/core';
|
||||
import { DOSSIER_TEMPLATE_ID, IPlaceholdersResponse, IReportTemplate, User } from '@red/domain';
|
||||
import { download } from '@utils/file-download-utils';
|
||||
import {
|
||||
@ -23,8 +23,9 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
import { Roles } from '@users/roles';
|
||||
import { getCurrentUser } from '@iqser/common-ui/lib/users';
|
||||
import { getParam } from '@iqser/common-ui/lib/utils';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { SnakeCasePipe } from '@common-ui/pipes/snake-case.pipe';
|
||||
|
||||
interface Placeholder {
|
||||
placeholder: string;
|
||||
@ -41,15 +42,16 @@ const placeholderTypes: PlaceholderType[] = ['generalPlaceholders', 'fileAttribu
|
||||
styleUrls: ['./reports-screen.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [HasScrollbarDirective, NgIf, NgForOf, AsyncPipe, TranslateModule, CircleButtonComponent, IqserAllowDirective],
|
||||
imports: [HasScrollbarDirective, AsyncPipe, TranslateModule, CircleButtonComponent, IqserAllowDirective, SnakeCasePipe],
|
||||
})
|
||||
export default class ReportsScreenComponent implements OnInit {
|
||||
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
|
||||
readonly placeholders$ = new BehaviorSubject<Placeholder[]>([]);
|
||||
readonly availableTemplates$ = new BehaviorSubject<IReportTemplate[]>([]);
|
||||
readonly currentUser = getCurrentUser<User>();
|
||||
readonly roles = Roles;
|
||||
readonly isDocumine = getConfig().IS_DOCUMINE;
|
||||
readonly #translateService = inject(TranslateService);
|
||||
private readonly _fileInput = viewChild<ElementRef>('fileInput');
|
||||
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
|
||||
|
||||
constructor(
|
||||
@ -62,8 +64,8 @@ export default class ReportsScreenComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
this._loadingService.start();
|
||||
await this._loadReportTemplates();
|
||||
await this._loadPlaceholders();
|
||||
await this.#loadReportTemplates();
|
||||
await this.#loadPlaceholders();
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
@ -83,14 +85,14 @@ export default class ReportsScreenComponent implements OnInit {
|
||||
|
||||
deleteTemplate(template: IReportTemplate) {
|
||||
this._dialogService.openDialog('confirm', null, () => {
|
||||
this._loadingService.loadWhile(this._deleteTemplate(template));
|
||||
this._loadingService.loadWhile(this.#deleteTemplate(template));
|
||||
});
|
||||
}
|
||||
|
||||
uploadTemplate($event) {
|
||||
const file: File = $event.target.files[0];
|
||||
|
||||
if (!this._isValidFile(file)) {
|
||||
if (!this.#isValidFile(file)) {
|
||||
this._toaster.error(_('reports-screen.invalid-upload'));
|
||||
return;
|
||||
}
|
||||
@ -114,27 +116,39 @@ export default class ReportsScreenComponent implements OnInit {
|
||||
template => template.fileName === file.name && template.multiFileReport === multiFileReport,
|
||||
)
|
||||
) {
|
||||
await this._openOverwriteConfirmationDialog(file, multiFileReport);
|
||||
await this.#openOverwriteConfirmationDialog(file, multiFileReport);
|
||||
} else {
|
||||
await this._uploadTemplateForm(file, multiFileReport);
|
||||
await this.#uploadTemplateForm(file, multiFileReport);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._fileInput.nativeElement.value = null;
|
||||
this._fileInput().nativeElement.value = null;
|
||||
}
|
||||
|
||||
private _getAttributeName(placeholder: string): string {
|
||||
#getAttributeName(placeholder: string): string {
|
||||
return removeBraces(placeholder).split('.').pop();
|
||||
}
|
||||
|
||||
private _getPlaceholderDescriptionTranslation(type: PlaceholderType, placeholder: string): string {
|
||||
#getPlaceholderDescriptionTranslation(type: PlaceholderType, placeholder: string): string {
|
||||
return type === 'generalPlaceholders'
|
||||
? generalPlaceholdersDescriptionsTranslations[removeBraces(placeholder)]
|
||||
: placeholdersDescriptionsTranslations[type];
|
||||
}
|
||||
|
||||
private async _openOverwriteConfirmationDialog(file: File, multiFileReport: boolean): Promise<void> {
|
||||
#getTemplateFilename(template: IReportTemplate): string {
|
||||
const extensionIndex = template.fileName.lastIndexOf('.');
|
||||
const hasExtension = extensionIndex !== -1;
|
||||
|
||||
const baseName = hasExtension ? template.fileName.substring(0, extensionIndex) : template.fileName;
|
||||
const extension = hasExtension ? template.fileName.substring(extensionIndex) : '';
|
||||
|
||||
const multiFileSuffix = template.multiFileReport ? ` ${this.#translateService.instant(_('reports-screen.multi-file-report'))}` : '';
|
||||
|
||||
return `${baseName}${multiFileSuffix}${extension}`.trim();
|
||||
}
|
||||
|
||||
async #openOverwriteConfirmationDialog(file: File, multiFileReport: boolean): Promise<void> {
|
||||
const data: IConfirmationDialogData = {
|
||||
title: _('confirmation-dialog.report-template-same-name.title'),
|
||||
question: _('confirmation-dialog.report-template-same-name.question'),
|
||||
@ -147,29 +161,34 @@ export default class ReportsScreenComponent implements OnInit {
|
||||
|
||||
this._dialogService.openDialog('confirm', data, null, async result => {
|
||||
if (result) {
|
||||
await this._uploadTemplateForm(file, multiFileReport);
|
||||
await this.#uploadTemplateForm(file, multiFileReport);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _uploadTemplateForm(file: File, multiFileReport: boolean): Promise<void> {
|
||||
async #uploadTemplateForm(file: File, multiFileReport: boolean): Promise<void> {
|
||||
this._loadingService.start();
|
||||
await firstValueFrom(this._reportTemplateService.uploadTemplateForm(this.#dossierTemplateId, multiFileReport, file));
|
||||
await this._loadReportTemplates();
|
||||
await this.#loadReportTemplates();
|
||||
this._loadingService.stop();
|
||||
}
|
||||
|
||||
private async _deleteTemplate(template: IReportTemplate) {
|
||||
async #deleteTemplate(template: IReportTemplate) {
|
||||
await firstValueFrom(this._reportTemplateService.delete(template.dossierTemplateId, template.templateId));
|
||||
await this._loadReportTemplates();
|
||||
await this.#loadReportTemplates();
|
||||
}
|
||||
|
||||
private async _loadReportTemplates() {
|
||||
async #loadReportTemplates() {
|
||||
const reportTemplates = await this._reportTemplateService.getAvailableReportTemplates(this.#dossierTemplateId);
|
||||
this.availableTemplates$.next(reportTemplates);
|
||||
this.availableTemplates$.next(
|
||||
reportTemplates.map(template => ({
|
||||
...template,
|
||||
fileName: this.#getTemplateFilename(template),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadPlaceholders() {
|
||||
async #loadPlaceholders() {
|
||||
const placeholdersResponse: IPlaceholdersResponse = await firstValueFrom(
|
||||
this._reportTemplateService.getAvailablePlaceholders(this.#dossierTemplateId),
|
||||
);
|
||||
@ -177,25 +196,25 @@ export default class ReportsScreenComponent implements OnInit {
|
||||
placeholderTypes.flatMap(type =>
|
||||
placeholdersResponse[type].map(placeholder => ({
|
||||
placeholder,
|
||||
descriptionTranslation: this._getPlaceholderDescriptionTranslation(type, placeholder),
|
||||
attributeName: this._getAttributeName(placeholder),
|
||||
descriptionTranslation: this.#getPlaceholderDescriptionTranslation(type, placeholder),
|
||||
attributeName: this.#getAttributeName(placeholder),
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _isValidFile(file: File): boolean {
|
||||
return this._isExcelFile(file) || this._isWordFile(file);
|
||||
#isValidFile(file: File): boolean {
|
||||
return this.#isExcelFile(file) || this.#isWordFile(file);
|
||||
}
|
||||
|
||||
private _isExcelFile(file: File): boolean {
|
||||
#isExcelFile(file: File): boolean {
|
||||
return (
|
||||
file.type?.toLowerCase() === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.name.toLowerCase().endsWith('.xlsx')
|
||||
);
|
||||
}
|
||||
|
||||
private _isWordFile(file: File): boolean {
|
||||
#isWordFile(file: File): boolean {
|
||||
return (
|
||||
file.type?.toLowerCase() === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
file.name.toLowerCase().endsWith('.docx')
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<div class="pagination noselect">
|
||||
<div (click)="changePage.emit(1)" class="page-button" id="portraitPage">
|
||||
<mat-icon class="chevron-icon" svgIcon="iqser:nav-prev"></mat-icon>
|
||||
Portrait
|
||||
{{ 'watermark-screen.pagination.portrait' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="separator">/</div>
|
||||
|
||||
<div (click)="changePage.emit(2)" class="page-button" id="landscapePage">
|
||||
Landscape
|
||||
{{ 'watermark-screen.pagination.landscape' | translate }}
|
||||
<mat-icon class="chevron-icon" svgIcon="iqser:nav-next"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-paginator',
|
||||
templateUrl: './paginator.component.html',
|
||||
styleUrls: ['./paginator.component.scss'],
|
||||
standalone: true,
|
||||
imports: [MatIcon],
|
||||
imports: [MatIcon, TranslateModule],
|
||||
})
|
||||
export class PaginatorComponent {
|
||||
@Output() readonly changePage = new EventEmitter<number>();
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
.container {
|
||||
padding: 32px;
|
||||
width: 1000px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<mat-icon *ngIf="!fileAttribute.editable" [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon>
|
||||
<span
|
||||
*ngIf="!isDate; else date"
|
||||
[style.max-width]="attributeValueWidth"
|
||||
[style.max-width]="attributeValueWidth()"
|
||||
[matTooltip]="fileAttributeValue"
|
||||
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
|
||||
>
|
||||
@ -56,13 +56,12 @@
|
||||
class="edit-input"
|
||||
iqserStopPropagation
|
||||
>
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" [style.width]="inputFormWidth()">
|
||||
<iqser-dynamic-input
|
||||
(closedDatepicker)="closedDatepicker = $event"
|
||||
(keyup.enter)="form.valid && save()"
|
||||
(keydown.escape)="close()"
|
||||
[style.max-width]="editFieldWidth"
|
||||
[style.min-width]="editFieldWidth"
|
||||
[style.width]="inputFieldWidth()"
|
||||
[formControlName]="fileAttribute.id"
|
||||
[id]="fileAttribute.id"
|
||||
[ngClass]="{ 'workflow-input': mode === 'workflow' || fileNameColumn, 'file-name-input': fileNameColumn }"
|
||||
|
||||
@ -97,7 +97,7 @@
|
||||
iqser-circle-button {
|
||||
margin-left: 15px;
|
||||
|
||||
@media screen and (max-width: 1395px) {
|
||||
@media screen and (max-width: 1745px) {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, computed, effect, HostListener, Input, OnDestroy } from '@angular/core';
|
||||
import { Component, computed, effect, HostListener, input, Input, OnDestroy } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormsModule, ReactiveFormsModule, UntypedFormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
@ -14,7 +14,7 @@ import {
|
||||
StopPropagationDirective,
|
||||
Toaster,
|
||||
} from '@iqser/common-ui';
|
||||
import { Debounce, log } from '@iqser/common-ui/lib/utils';
|
||||
import { Debounce } from '@iqser/common-ui/lib/utils';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
@ -49,7 +49,6 @@ import { ConfigService } from '../../config.service';
|
||||
})
|
||||
export class FileAttributeComponent extends BaseFormComponent implements OnDestroy {
|
||||
readonly #subscriptions = new Subscription();
|
||||
#widthFactor = window.innerWidth >= 1800 ? 0.85 : 0.7;
|
||||
isInEditMode = false;
|
||||
closedDatepicker = true;
|
||||
@Input({ required: true }) fileAttribute!: IFileAttributeConfig;
|
||||
@ -66,7 +65,10 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
|
||||
@Input({ required: true }) dossier!: Dossier;
|
||||
@Input() fileNameColumn = false;
|
||||
readonlyAttrs: string[] = [];
|
||||
@Input() width?: number;
|
||||
readonly width = input<number>();
|
||||
readonly inputFormWidth = computed(() => (this.width() ? this.width() + 'px' : 'unset'));
|
||||
readonly inputFieldWidth = computed(() => (this.width() ? this.width() - 50 + 'px' : 'unset'));
|
||||
readonly attributeValueWidth = computed(() => (this.width() ? `${this.width() * 0.9}px` : 'unset'));
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
@ -99,14 +101,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
|
||||
);
|
||||
}
|
||||
|
||||
get editFieldWidth(): string {
|
||||
return this.width ? `${this.width * this.#widthFactor}px` : 'unset';
|
||||
}
|
||||
|
||||
get attributeValueWidth(): string {
|
||||
return this.width ? `${this.width * 0.9}px` : 'unset';
|
||||
}
|
||||
|
||||
get isDate(): boolean {
|
||||
return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
|
||||
}
|
||||
@ -123,15 +117,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
|
||||
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
if (window.innerWidth >= 1800) {
|
||||
this.#widthFactor = 0.85;
|
||||
} else {
|
||||
this.#widthFactor = 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@Debounce(60)
|
||||
@HostListener('document:click', ['$event'])
|
||||
clickOutside($event: MouseEvent) {
|
||||
@ -147,6 +132,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
|
||||
|
||||
handleClick($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -154,12 +140,14 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
|
||||
}
|
||||
|
||||
handleFieldClick($event: MouseEvent) {
|
||||
$event.preventDefault();
|
||||
if (!this.fileNameColumn) {
|
||||
this.editFileAttribute($event);
|
||||
}
|
||||
}
|
||||
|
||||
editFileAttribute($event: MouseEvent) {
|
||||
$event.preventDefault();
|
||||
if (
|
||||
!this.file.isInitialProcessing &&
|
||||
this.permissionsService.canEditFileAttributes(this.file, this.dossier) &&
|
||||
|
||||
@ -2,18 +2,33 @@
|
||||
<redaction-annotation-icon
|
||||
*ngIf="file.analysisRequired"
|
||||
[color]="analysisColor$ | async"
|
||||
label="A"
|
||||
[label]="(workloadTranslations['analysis'] | translate)[0]"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="updated"
|
||||
[color]="updatedColor$ | async"
|
||||
[label]="(workloadTranslations['updated'] | translate)[0]"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="updated" [color]="updatedColor$ | async" label="U" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="file.hasRedactions"
|
||||
[color]="redactionColor$ | async"
|
||||
[label]="'redaction-abbreviation' | translate"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hasImages" [color]="imageColor$ | async" label="I" type="square"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="file.hintsOnly" [color]="hintColor$ | async" label="H" type="circle"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="file.hasImages"
|
||||
[color]="imageColor$ | async"
|
||||
[label]="(workloadTranslations['image'] | translate)[0]"
|
||||
type="square"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="file.hintsOnly"
|
||||
[color]="hintColor$ | async"
|
||||
[label]="(workloadTranslations['hint'] | translate)[0]"
|
||||
type="circle"
|
||||
></redaction-annotation-icon>
|
||||
<mat-icon *ngIf="file.hasAnnotationComments" svgIcon="red:comment"></mat-icon>
|
||||
<ng-container *ngIf="noWorkloadItems"> -</ng-container>
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,7 @@ import { AnnotationIconComponent } from '@shared/components/annotation-icon/anno
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { workloadTranslations } from '@translations/workload-translations';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-workload',
|
||||
@ -27,6 +28,7 @@ export class FileWorkloadComponent implements OnInit {
|
||||
analysisColor$: Observable<string>;
|
||||
hintColor$: Observable<string>;
|
||||
redactionColor$: Observable<string>;
|
||||
readonly workloadTranslations = workloadTranslations;
|
||||
|
||||
constructor(
|
||||
private readonly _userService: UserService,
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
</iqser-workflow>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dossierAttributes$ | async" [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
|
||||
<div *ngIf="dossierAttributes$ | async" [class.collapsed]="collapsedDetails" class="right-container">
|
||||
<redaction-dossier-details
|
||||
(toggleCollapse)="collapsedDetails = !collapsedDetails"
|
||||
[dossierAttributes]="dossierAttributes"
|
||||
|
||||
@ -25,10 +25,7 @@
|
||||
width: 375px;
|
||||
min-width: 375px;
|
||||
padding: 16px 24px 16px 24px;
|
||||
|
||||
&.has-scrollbar:hover {
|
||||
padding-right: 13px;
|
||||
}
|
||||
overflow-y: auto;
|
||||
|
||||
redaction-dossier-details {
|
||||
width: 100%;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { IConfirmationDialogData, IqserDialog, LoadingService } from '@iqser/common-ui';
|
||||
import { Dossier, File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
|
||||
import { ApproveResponse, Dossier, File, User, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { FileManagementService } from '@services/files/file-management.service';
|
||||
import { FilesService } from '@services/files/files.service';
|
||||
@ -11,6 +11,7 @@ import { AssignReviewerApproverDialogComponent } from '../../shared-dossiers/dia
|
||||
import { DossiersDialogService } from '../../shared-dossiers/services/dossiers-dialog.service';
|
||||
import { FileAssignService } from '../../shared-dossiers/services/file-assign.service';
|
||||
import { getCurrentUser } from '@common-ui/users';
|
||||
import { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component';
|
||||
|
||||
@Injectable()
|
||||
export class BulkActionsService {
|
||||
@ -110,34 +111,35 @@ export class BulkActionsService {
|
||||
}
|
||||
|
||||
async approve(files: File[]): Promise<void> {
|
||||
const foundAnalysisRequiredFile = files.find(file => file.analysisRequired);
|
||||
const foundUpdatedFile = files.find(file => file.hasUpdates);
|
||||
if (foundAnalysisRequiredFile || foundUpdatedFile) {
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
{
|
||||
title: foundAnalysisRequiredFile
|
||||
? _('confirmation-dialog.approve-multiple-files-without-analysis.title')
|
||||
: _('confirmation-dialog.approve-multiple-files.title'),
|
||||
question: foundAnalysisRequiredFile
|
||||
? _('confirmation-dialog.approve-multiple-files-without-analysis.question')
|
||||
: _('confirmation-dialog.approve-multiple-files.question'),
|
||||
confirmationText: foundAnalysisRequiredFile
|
||||
? _('confirmation-dialog.approve-multiple-files-without-analysis.confirmationText')
|
||||
: null,
|
||||
denyText: foundAnalysisRequiredFile ? _('confirmation-dialog.approve-multiple-files-without-analysis.denyText') : null,
|
||||
} as IConfirmationDialogData,
|
||||
async () => {
|
||||
this._loadingService.start();
|
||||
await this._filesService.setApproved(files);
|
||||
this._loadingService.stop();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this._loadingService.start();
|
||||
await this._filesService.setApproved(files);
|
||||
this._loadingService.stop();
|
||||
this._loadingService.start();
|
||||
const approvalResponse: ApproveResponse[] = await this._filesService.getApproveWarnings(files);
|
||||
this._loadingService.stop();
|
||||
const hasWarnings = approvalResponse.some(response => response.hasWarnings);
|
||||
if (!hasWarnings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileWarnings = approvalResponse
|
||||
.filter(response => response.hasWarnings)
|
||||
.map(response => ({ fileId: response.fileId, fileWarnings: response.fileWarnings }));
|
||||
this._dialogService.openDialog(
|
||||
'confirm',
|
||||
{
|
||||
title: _('confirmation-dialog.approve-file.title'),
|
||||
question: _('confirmation-dialog.approve-file.question'),
|
||||
translateParams: { questionLength: files.length },
|
||||
confirmationText: _('confirmation-dialog.approve-file.confirmationText'),
|
||||
denyText: _('confirmation-dialog.approve-file.denyText'),
|
||||
component: ApproveWarningDetailsComponent,
|
||||
componentInputs: { data: signal(fileWarnings), files: signal(files) },
|
||||
cancelButtonPrimary: true,
|
||||
} as IConfirmationDialogData,
|
||||
async () => {
|
||||
this._loadingService.start();
|
||||
await this._filesService.setApproved(files);
|
||||
this._loadingService.stop();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
assign(files: File[]): void {
|
||||
|
||||
@ -1,161 +1,165 @@
|
||||
<div
|
||||
*ngIf="canPerformAnnotationActions && annotationPermissions"
|
||||
[class.always-visible]="alwaysVisible || (helpModeService.isHelpModeActive$ | async)"
|
||||
class="annotation-actions"
|
||||
>
|
||||
<!-- Resize Mode for annotation -> only resize accept and deny actions are available-->
|
||||
<ng-container *ngIf="resizing && annotationPermissions.canResizeAnnotation">
|
||||
<iqser-circle-button
|
||||
(action)="acceptResize()"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-accept_resize' : 'annotations-accept_resize'"
|
||||
[class.disabled]="!resized"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="resized ? ('annotation-actions.resize-accept.label' | translate) : ''"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
@if (canPerformAnnotationActions && annotationPermissions()) {
|
||||
<div [class.always-visible]="alwaysVisible || (helpModeService.isHelpModeActive$ | async)" class="annotation-actions">
|
||||
<!-- Resize Mode for annotation -> only resize accept and deny actions are available-->
|
||||
<ng-container *ngIf="resizing() && annotationPermissions().canResizeAnnotation">
|
||||
<iqser-circle-button
|
||||
(action)="acceptResize()"
|
||||
[buttonId]="
|
||||
annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-accept_resize' : 'annotations-accept_resize'
|
||||
"
|
||||
[class.disabled]="!resized"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="resized ? ('annotation-actions.resize-accept.label' | translate) : ''"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="cancelResize()"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-cancel_resize' : 'annotations-cancel_resize'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize-cancel.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
<iqser-circle-button
|
||||
(action)="cancelResize()"
|
||||
[buttonId]="
|
||||
annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-cancel_resize' : 'annotations-cancel_resize'
|
||||
"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize-cancel.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Not resizing - standard actions -->
|
||||
<ng-container *ngIf="!resizing">
|
||||
<iqser-circle-button
|
||||
(action)="resize()"
|
||||
*ngIf="canResize"
|
||||
[attr.help-mode-key]="helpModeKey('resize')"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-resize' : 'annotations-resize'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:resize"
|
||||
></iqser-circle-button>
|
||||
<!-- Not resizing - standard actions -->
|
||||
<ng-container *ngIf="!resizing()">
|
||||
<iqser-circle-button
|
||||
(action)="resize()"
|
||||
*ngIf="canResize()"
|
||||
[attr.help-mode-key]="helpModeKey('resize')"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-resize' : 'annotations-resize'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.resize.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:resize"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.editRedaction(annotations)"
|
||||
*ngIf="canEdit"
|
||||
[attr.help-mode-key]="helpModeKey('edit')"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-edit' : 'annotations-edit'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.edit-redaction.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.editRedaction(annotations())"
|
||||
*ngIf="canEdit()"
|
||||
[attr.help-mode-key]="helpModeKey('edit')"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-edit' : 'annotations-edit'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.edit-redaction.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="acceptRecommendation()"
|
||||
*ngIf="canAcceptRecommendation"
|
||||
[attr.help-mode-key]="helpModeKey('accept')"
|
||||
[buttonId]="
|
||||
annotations.length === 1
|
||||
? 'annotation-' + annotations[0].id + '-accept_recommendation'
|
||||
: 'annotations-accept_recommendation'
|
||||
"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-recommendation.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="acceptRecommendation()"
|
||||
*ngIf="canAcceptRecommendation()"
|
||||
[attr.help-mode-key]="helpModeKey('accept')"
|
||||
[buttonId]="
|
||||
annotations().length === 1
|
||||
? 'annotation-' + annotations()[0].id + '-accept_recommendation'
|
||||
: 'annotations-accept_recommendation'
|
||||
"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.accept-recommendation.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.convertHighlights(annotations)"
|
||||
*ngIf="viewModeService.isEarmarks()"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.convert-highlights.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:convert"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.convertHighlights(annotations())"
|
||||
*ngIf="viewModeService.isEarmarks()"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.convert-highlights.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:convert"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.removeHighlights(annotations)"
|
||||
*ngIf="viewModeService.isEarmarks()"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-highlights.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.removeHighlights(annotations())"
|
||||
*ngIf="viewModeService.isEarmarks()"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-highlights.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.undoDirectAction(annotations)"
|
||||
*allow="roles.redactions.deleteManual; if: annotationPermissions.canUndo"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.undo' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.undoDirectAction(annotations())"
|
||||
*allow="roles.redactions.deleteManual; if: annotationPermissions().canUndo"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.undo' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:undo"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationReferencesService.show(annotations[0])"
|
||||
*ngIf="multiSelectService.inactive() && annotations[0].reference.length"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.see-references.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:reference"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationReferencesService.show(annotations()[0])"
|
||||
*ngIf="multiSelectService.inactive() && annotations()[0].reference.length"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.see-references.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="red:reference"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceAnnotation(annotations)"
|
||||
*ngIf="canForceRedaction"
|
||||
[attr.help-mode-key]="isImageHint ? helpModeKey('redact') : helpModeKey('force')"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-force_redaction' : 'annotations-force_redaction'"
|
||||
[icon]="isImageHint ? 'red:pdftron-action-add-redaction' : 'iqser:thumb-up'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="
|
||||
isImageHint
|
||||
? ('annotation-actions.force-redaction.label-image-hint' | translate)
|
||||
: ('annotation-actions.force-redaction.label' | translate)
|
||||
"
|
||||
[type]="buttonType"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceAnnotation(annotations())"
|
||||
*ngIf="canForceRedaction()"
|
||||
[attr.help-mode-key]="isImageHint() ? helpModeKey('redact') : helpModeKey('force')"
|
||||
[buttonId]="
|
||||
annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-force_redaction' : 'annotations-force_redaction'
|
||||
"
|
||||
[icon]="isImageHint() ? 'red:pdftron-action-add-redaction' : 'iqser:thumb-up'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="
|
||||
isImageHint()
|
||||
? ('annotation-actions.force-redaction.label-image-hint' | translate)
|
||||
: ('annotation-actions.force-redaction.label' | translate)
|
||||
"
|
||||
[type]="buttonType"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceAnnotation(annotations, true)"
|
||||
*ngIf="canForceHint"
|
||||
[attr.help-mode-key]="actionsHelpModeKey"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-force_hint' : 'annotations-force_hint'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.force-hint.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:thumb-up"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="annotationActionsService.forceAnnotation(annotations(), true)"
|
||||
*ngIf="canForceHint()"
|
||||
[attr.help-mode-key]="actionsHelpModeKey"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-force_hint' : 'annotations-force_hint'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.force-hint.label' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:thumb-up"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="hideAnnotation()"
|
||||
*ngIf="isImage && isVisible() && !hideSkipped"
|
||||
[attr.help-mode-key]="helpModeKey('hide')"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-hide' : 'annotations-hide'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.hide' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:visibility-off"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="hideAnnotation()"
|
||||
*ngIf="isImage() && isVisible() && !hideSkipped()"
|
||||
[attr.help-mode-key]="helpModeKey('hide')"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-hide' : 'annotations-hide'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.hide' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:visibility-off"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="showAnnotation()"
|
||||
*ngIf="isImage && !isVisible() && !hideSkipped"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-show' : 'annotations-show'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.show' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:visibility"
|
||||
></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="showAnnotation()"
|
||||
*ngIf="isImage() && !isVisible() && !hideSkipped()"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-show' : 'annotations-show'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.show' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:visibility"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="removeRedaction()"
|
||||
*ngIf="canRemoveRedaction"
|
||||
[attr.help-mode-key]="helpModeKey('remove')"
|
||||
[buttonId]="annotations.length === 1 ? 'annotation-' + annotations[0].id + '-remove' : 'annotations-remove'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.remove-redaction' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<iqser-circle-button
|
||||
(action)="removeRedaction()"
|
||||
*ngIf="canRemoveRedaction()"
|
||||
[attr.help-mode-key]="helpModeKey('remove')"
|
||||
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-remove' : 'annotations-remove'"
|
||||
[tooltipPosition]="tooltipPosition"
|
||||
[tooltip]="'annotation-actions.remove-annotation.remove-redaction' | translate"
|
||||
[type]="buttonType"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { Component, computed, Input, OnChanges } from '@angular/core';
|
||||
import { CircleButtonComponent, getConfig, HelpModeService, IqserAllowDirective, IqserPermissionsService } from '@iqser/common-ui';
|
||||
import { Component, computed, input, Input, untracked } from '@angular/core';
|
||||
import { CircleButtonComponent, getConfig, HelpModeService, IqserPermissionsService } from '@iqser/common-ui';
|
||||
import { AnnotationPermissions } from '@models/file/annotation.permissions';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@ -26,23 +26,79 @@ export type AnnotationButtonType = keyof typeof AnnotationButtonTypes;
|
||||
templateUrl: './annotation-actions.component.html',
|
||||
styleUrls: ['./annotation-actions.component.scss'],
|
||||
standalone: true,
|
||||
imports: [CircleButtonComponent, NgIf, TranslateModule, AsyncPipe, IqserAllowDirective],
|
||||
imports: [CircleButtonComponent, TranslateModule, AsyncPipe, NgIf],
|
||||
})
|
||||
export class AnnotationActionsComponent implements OnChanges {
|
||||
#annotations: AnnotationWrapper[] = [];
|
||||
export class AnnotationActionsComponent {
|
||||
readonly #isDocumine = getConfig().IS_DOCUMINE;
|
||||
protected _annotationId = '';
|
||||
@Input() buttonType: AnnotationButtonType = AnnotationButtonTypes.default;
|
||||
@Input() tooltipPosition: 'before' | 'above' = 'before';
|
||||
@Input() canPerformAnnotationActions: boolean;
|
||||
@Input() alwaysVisible: boolean;
|
||||
@Input() actionsHelpModeKey: string;
|
||||
readonly roles = Roles;
|
||||
annotationPermissions: AnnotationPermissions;
|
||||
isImage = true;
|
||||
readonly annotations = input.required<AnnotationWrapper[], (AnnotationWrapper | undefined)[]>({
|
||||
transform: value => value.filter(a => a !== undefined),
|
||||
});
|
||||
readonly isVisible = computed(() => {
|
||||
const hidden = this._annotationManager.hidden();
|
||||
return this.#annotations.reduce((acc, annotation) => !hidden.has(annotation.id) && acc, true);
|
||||
return this.annotations().reduce((acc, annotation) => !hidden.has(annotation.id) && acc, true);
|
||||
});
|
||||
readonly somePending = computed(() => {
|
||||
return this.annotations().some(a => a.pending);
|
||||
});
|
||||
readonly sameType = computed(() => {
|
||||
const annotations = this.annotations();
|
||||
const type = annotations[0].superType;
|
||||
return annotations.every(a => a.superType === type);
|
||||
});
|
||||
readonly resizing = computed(() => {
|
||||
return this.annotations().length === 1 && this.annotations()[0].id === this._annotationManager.resizingAnnotationId;
|
||||
});
|
||||
readonly viewerAnnotations = computed(() => {
|
||||
return this._annotationManager.get(this.annotations());
|
||||
});
|
||||
readonly annotationPermissions = computed(() =>
|
||||
AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(this._state.dossier()),
|
||||
this.annotations(),
|
||||
this._state.dictionaries,
|
||||
this._iqserPermissionsService,
|
||||
this._state.file().excludedFromAutomaticAnalysis,
|
||||
),
|
||||
);
|
||||
readonly hideSkipped = computed(() => this.skippedService.hideSkipped() && this.annotations().some(a => a.isSkipped));
|
||||
readonly isImageHint = computed(() => this.annotations().every(a => a.IMAGE_HINT));
|
||||
readonly isImage = computed(() => this.annotations().reduce((acc, a) => acc && a.isImage, true));
|
||||
readonly annotationChangesAllowed = computed(
|
||||
() => (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.somePending(),
|
||||
);
|
||||
readonly canRemoveRedaction = computed(
|
||||
() => this.annotationChangesAllowed() && this.annotationPermissions().canRemoveRedaction && this.sameType(),
|
||||
);
|
||||
readonly canForceRedaction = computed(() => this.annotationChangesAllowed() && this.annotationPermissions().canForceRedaction);
|
||||
readonly canForceHint = computed(() => this.annotationChangesAllowed() && this.annotationPermissions().canForceHint);
|
||||
readonly canAcceptRecommendation = computed(
|
||||
() => this.annotationChangesAllowed() && this.annotationPermissions().canAcceptRecommendation,
|
||||
);
|
||||
readonly canResize = computed(
|
||||
() => this.annotationChangesAllowed() && this.annotationPermissions().canResizeAnnotation && this.annotations().length === 1,
|
||||
);
|
||||
readonly canEdit = computed(() => {
|
||||
const canEditRedactions =
|
||||
this.annotationPermissions().canChangeLegalBasis ||
|
||||
this.annotationPermissions().canRecategorizeAnnotation ||
|
||||
this.annotationPermissions().canForceHint ||
|
||||
this.annotationPermissions().canForceRedaction;
|
||||
return (
|
||||
this.annotationChangesAllowed() &&
|
||||
(this.annotations().length > 1
|
||||
? this.#isDocumine
|
||||
? this.annotationPermissions().canEditAnnotations
|
||||
: this.annotationPermissions().canEditHints ||
|
||||
this.annotationPermissions().canEditImages ||
|
||||
this.annotationPermissions().canEditAnnotations
|
||||
: canEditRedactions)
|
||||
);
|
||||
});
|
||||
|
||||
constructor(
|
||||
@ -58,137 +114,54 @@ export class AnnotationActionsComponent implements OnChanges {
|
||||
readonly annotationReferencesService: AnnotationReferencesService,
|
||||
) {}
|
||||
|
||||
get annotations(): AnnotationWrapper[] {
|
||||
return this.#annotations;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set annotations(annotations: AnnotationWrapper[]) {
|
||||
this.#annotations = annotations.filter(a => a !== undefined);
|
||||
this.isImage = this.#annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true);
|
||||
this._annotationId = this.#annotations[0]?.id;
|
||||
}
|
||||
|
||||
get isImageHint(): boolean {
|
||||
return this.annotations.every(annotation => annotation.IMAGE_HINT);
|
||||
}
|
||||
|
||||
get canEdit(): boolean {
|
||||
const canEditRedactions =
|
||||
this.annotationPermissions.canChangeLegalBasis ||
|
||||
this.annotationPermissions.canRecategorizeAnnotation ||
|
||||
this.annotationPermissions.canForceHint ||
|
||||
this.annotationPermissions.canForceRedaction;
|
||||
return (
|
||||
this.#annotationChangesAllowed &&
|
||||
(this.annotations.length > 1
|
||||
? this.#isDocumine
|
||||
? this.annotationPermissions.canEditAnnotations
|
||||
: this.annotationPermissions.canEditHints ||
|
||||
this.annotationPermissions.canEditImages ||
|
||||
this.annotationPermissions.canEditAnnotations
|
||||
: canEditRedactions)
|
||||
);
|
||||
}
|
||||
|
||||
get canResize(): boolean {
|
||||
return this.#annotationChangesAllowed && this.annotationPermissions.canResizeAnnotation && this.annotations.length === 1;
|
||||
}
|
||||
|
||||
get canRemoveRedaction(): boolean {
|
||||
return this.#annotationChangesAllowed && this.annotationPermissions.canRemoveRedaction && this.#sameType;
|
||||
}
|
||||
|
||||
get canForceRedaction() {
|
||||
return this.#annotationChangesAllowed && this.annotationPermissions.canForceRedaction;
|
||||
}
|
||||
|
||||
get canForceHint() {
|
||||
return this.#annotationChangesAllowed && this.annotationPermissions.canForceHint;
|
||||
}
|
||||
|
||||
get canAcceptRecommendation() {
|
||||
return this.#annotationChangesAllowed && this.annotationPermissions.canAcceptRecommendation;
|
||||
}
|
||||
|
||||
get viewerAnnotations() {
|
||||
return this._annotationManager.get(this.#annotations);
|
||||
}
|
||||
|
||||
get resizing() {
|
||||
return this.#annotations?.length === 1 && this.#annotations?.[0].id === this._annotationManager.resizingAnnotationId;
|
||||
}
|
||||
|
||||
get resized() {
|
||||
get resized(): boolean {
|
||||
return this._annotationManager.annotationHasBeenResized;
|
||||
}
|
||||
|
||||
get hideSkipped() {
|
||||
return this.skippedService.hideSkipped() && this.annotations.some(a => a.isSkipped);
|
||||
async removeRedaction(): Promise<void> {
|
||||
const annotations = untracked(this.annotations);
|
||||
const permissions = untracked(this.annotationPermissions);
|
||||
await this.annotationActionsService.removeRedaction(annotations, permissions);
|
||||
}
|
||||
|
||||
get #sameType() {
|
||||
const type = this.annotations[0].superType;
|
||||
return this.annotations.every(a => a.superType === type);
|
||||
}
|
||||
|
||||
get #annotationChangesAllowed() {
|
||||
return (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.#somePending;
|
||||
}
|
||||
|
||||
get #somePending() {
|
||||
return this.#annotations.some(a => a.pending);
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.#setPermissions();
|
||||
}
|
||||
|
||||
removeRedaction() {
|
||||
this.annotationActionsService.removeRedaction(this.annotations, this.annotationPermissions);
|
||||
}
|
||||
|
||||
acceptRecommendation() {
|
||||
return this.annotationActionsService.convertRecommendationToAnnotation(this.annotations);
|
||||
async acceptRecommendation(): Promise<void> {
|
||||
const annotations = untracked(this.annotations);
|
||||
await this.annotationActionsService.convertRecommendationToAnnotation(annotations);
|
||||
}
|
||||
|
||||
hideAnnotation() {
|
||||
this._annotationManager.hide(this.viewerAnnotations);
|
||||
const viewerAnnotations = untracked(this.viewerAnnotations);
|
||||
this._annotationManager.hide(viewerAnnotations);
|
||||
this._annotationManager.deselect();
|
||||
this._annotationManager.addToHidden(this.viewerAnnotations[0].Id);
|
||||
this._annotationManager.addToHidden(viewerAnnotations[0].Id);
|
||||
}
|
||||
|
||||
showAnnotation() {
|
||||
this._annotationManager.show(this.viewerAnnotations);
|
||||
const viewerAnnotations = untracked(this.viewerAnnotations);
|
||||
this._annotationManager.show(viewerAnnotations);
|
||||
this._annotationManager.deselect();
|
||||
this._annotationManager.removeFromHidden(this.viewerAnnotations[0].Id);
|
||||
this._annotationManager.removeFromHidden(viewerAnnotations[0].Id);
|
||||
}
|
||||
|
||||
resize() {
|
||||
return this.annotationActionsService.resize(this.#annotations[0]);
|
||||
const annotations = untracked(this.annotations);
|
||||
return this.annotationActionsService.resize(annotations[0]);
|
||||
}
|
||||
|
||||
acceptResize() {
|
||||
if (this.resized) {
|
||||
return this.annotationActionsService.acceptResize(this.#annotations[0], this.annotationPermissions);
|
||||
const annotations = untracked(this.annotations);
|
||||
const permissions = untracked(this.annotationPermissions);
|
||||
return this.annotationActionsService.acceptResize(annotations[0], permissions);
|
||||
}
|
||||
}
|
||||
|
||||
cancelResize() {
|
||||
return this.annotationActionsService.cancelResize(this.#annotations[0]);
|
||||
const annotations = untracked(this.annotations);
|
||||
return this.annotationActionsService.cancelResize(annotations[0]);
|
||||
}
|
||||
|
||||
helpModeKey(action: string) {
|
||||
return this.#isDocumine ? `${action}_annotation` : `${this.actionsHelpModeKey}_${action}`;
|
||||
}
|
||||
|
||||
#setPermissions() {
|
||||
this.annotationPermissions = AnnotationPermissions.forUser(
|
||||
this._permissionsService.isApprover(this._state.dossier()),
|
||||
this.#annotations,
|
||||
this._state.dictionaries,
|
||||
this._iqserPermissionsService,
|
||||
this._state.file().excludedFromAutomaticAnalysis,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +1,31 @@
|
||||
<div
|
||||
*ngIf="noSelection && changesTooltip"
|
||||
[matTooltip]="changesTooltip"
|
||||
class="chip"
|
||||
matTooltipClass="multiline"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon [svgIcon]="'red:redaction-changes'"></mat-icon>
|
||||
</div>
|
||||
@if (_noSelection() && _changesTooltip()) {
|
||||
<div [matTooltip]="_changesTooltip()" class="chip" matTooltipClass="multiline" matTooltipPosition="above">
|
||||
<mat-icon [svgIcon]="'red:redaction-changes'"></mat-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="noSelection && engines">
|
||||
<div #trigger="cdkOverlayOrigin" (mouseout)="isPopoverOpen = false" (mouseover)="isPopoverOpen = true" cdkOverlayOrigin class="chip">
|
||||
<mat-icon *ngFor="let engine of engines" [svgIcon]="engine.icon"></mat-icon>
|
||||
@if (_noSelection() && _engines()) {
|
||||
<div
|
||||
#trigger="cdkOverlayOrigin"
|
||||
(mouseout)="_isPopoverOpen.set(false)"
|
||||
(mouseover)="_isPopoverOpen.set(true)"
|
||||
cdkOverlayOrigin
|
||||
class="chip"
|
||||
>
|
||||
<mat-icon *ngFor="let engine of _engines()" [svgIcon]="engine.icon"></mat-icon>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlayOffsetY]="-8"
|
||||
[cdkConnectedOverlayOpen]="isPopoverOpen"
|
||||
[cdkConnectedOverlayOpen]="_isPopoverOpen()"
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
cdkConnectedOverlay
|
||||
>
|
||||
<div class="popover">
|
||||
<div *ngFor="let engine of engines" class="flex-align-items-center">
|
||||
<div *ngFor="let engine of _engines()" class="flex-align-items-center">
|
||||
<mat-icon [svgIcon]="engine.icon"></mat-icon>
|
||||
<span [innerHTML]="engine.description | translate : engine.translateParams"></span>
|
||||
<span [innerHTML]="engine.description | translate: engine.translateParams"></span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 19px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, inject, Input, OnChanges } from '@angular/core';
|
||||
import { Component, computed, inject, input, signal } from '@angular/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { KeysOf } from '@iqser/common-ui/lib/utils';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
@ -33,30 +33,27 @@ const changesProperties: KeysOf<AnnotationWrapper>[] = [
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotation-details [annotation]',
|
||||
selector: 'redaction-annotation-details',
|
||||
templateUrl: './annotation-details.component.html',
|
||||
styleUrls: ['./annotation-details.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, MatTooltip, MatIcon, CdkOverlayOrigin, NgForOf, CdkConnectedOverlay, TranslateModule],
|
||||
})
|
||||
export class AnnotationDetailsComponent implements OnChanges {
|
||||
@Input() annotation: ListItem<AnnotationWrapper>;
|
||||
isPopoverOpen = false;
|
||||
engines: Engine[];
|
||||
changesTooltip: string;
|
||||
noSelection: boolean;
|
||||
export class AnnotationDetailsComponent {
|
||||
readonly annotation = input.required<ListItem<AnnotationWrapper>>();
|
||||
protected readonly _isPopoverOpen = signal(false);
|
||||
protected readonly _engines = computed(() => this.#extractEngines(this.annotation().item).filter(engine => engine.show));
|
||||
private readonly _translateService = inject(TranslateService);
|
||||
private readonly _multiSelectService = inject(MultiSelectService);
|
||||
protected readonly _changesTooltip = computed(() => {
|
||||
const annotation = this.annotation().item;
|
||||
const changes = changesProperties.filter(key => annotation[key]);
|
||||
|
||||
getChangesTooltip(): string | undefined {
|
||||
const changes = changesProperties.filter(key => this.annotation.item[key]);
|
||||
|
||||
if (!changes.length && !this.annotation.item.engines?.includes(LogEntryEngines.MANUAL)) {
|
||||
if (!changes.length && !annotation.engines?.includes(LogEntryEngines.MANUAL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const details = [];
|
||||
if (this.annotation.item.engines?.includes(LogEntryEngines.MANUAL)) {
|
||||
if (annotation.engines?.includes(LogEntryEngines.MANUAL)) {
|
||||
details.push(this._translateService.instant(_('annotation-changes.added-locally')));
|
||||
}
|
||||
|
||||
@ -66,13 +63,9 @@ export class AnnotationDetailsComponent implements OnChanges {
|
||||
|
||||
const header = this._translateService.instant(_('annotation-changes.header'));
|
||||
return [header, ...details.map(change => `• ${change}`)].join('\n');
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.engines = this.#extractEngines(this.annotation.item).filter(engine => engine.show);
|
||||
this.changesTooltip = this.getChangesTooltip();
|
||||
this.noSelection = !this.annotation.isSelected || this._multiSelectService.inactive();
|
||||
}
|
||||
});
|
||||
private readonly _multiSelectService = inject(MultiSelectService);
|
||||
protected readonly _noSelection = computed(() => !this.annotation().isSelected || this._multiSelectService.inactive());
|
||||
|
||||
#extractEngines(annotation: AnnotationWrapper): Engine[] {
|
||||
return [
|
||||
|
||||
@ -1,37 +1,44 @@
|
||||
<div class="active-bar-marker"></div>
|
||||
|
||||
<div [class.removed]="annotation.item.isRemoved" class="annotation">
|
||||
<div #annotationDiv [class.removed]="annotation().item.isRemoved" class="annotation">
|
||||
<redaction-annotation-card
|
||||
[annotation]="annotation.item"
|
||||
[isSelected]="annotation.isSelected"
|
||||
[matTooltip]="annotation.item.content"
|
||||
[annotation]="annotation().item"
|
||||
[isSelected]="annotation().isSelected"
|
||||
[matTooltip]="annotation().item.content.translation | translate: annotation().item.content.params | replaceNbsp"
|
||||
matTooltipPosition="above"
|
||||
></redaction-annotation-card>
|
||||
|
||||
<div *ngIf="!annotation.item.isEarmark" class="actions-wrapper">
|
||||
<div
|
||||
*ngIf="!annotation.item.pending"
|
||||
(click)="showComments = !showComments"
|
||||
[matTooltip]="'comments.comments' | translate: { count: annotation.item.numberOfComments }"
|
||||
class="comments-counter"
|
||||
iqserStopPropagation
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon svgIcon="red:comment"></mat-icon>
|
||||
{{ annotation.item.numberOfComments }}
|
||||
</div>
|
||||
@if (!annotation().item.isEarmark) {
|
||||
<div class="actions-wrapper">
|
||||
@if (!annotation().item.pending) {
|
||||
<div
|
||||
(click)="showComments = !showComments"
|
||||
[matTooltip]="'comments.comments' | translate: { count: annotation().item.numberOfComments }"
|
||||
class="comments-counter"
|
||||
iqserStopPropagation
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon svgIcon="red:comment"></mat-icon>
|
||||
{{ annotation().item.numberOfComments }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="multiSelectService.inactive()" class="actions">
|
||||
<redaction-annotation-actions
|
||||
[annotations]="[annotation.item]"
|
||||
[actionsHelpModeKey]="actionsHelpModeKey"
|
||||
[canPerformAnnotationActions]="pdfProxyService.canPerformActions()"
|
||||
></redaction-annotation-actions>
|
||||
@if (multiSelectService.inactive()) {
|
||||
@defer (on hover(annotationDiv)) {
|
||||
<div class="actions">
|
||||
<redaction-annotation-actions
|
||||
[actionsHelpModeKey]="actionsHelpModeKey()"
|
||||
[annotations]="[annotation().item]"
|
||||
[canPerformAnnotationActions]="pdfProxyService.canPerformActions()"
|
||||
></redaction-annotation-actions>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="showComments">
|
||||
<redaction-comments [annotation]="annotation.item"></redaction-comments>
|
||||
@if (showComments) {
|
||||
<redaction-comments [annotation]="annotation().item"></redaction-comments>
|
||||
|
||||
<div
|
||||
(click)="showComments = false"
|
||||
@ -39,7 +46,7 @@
|
||||
iqserStopPropagation
|
||||
translate="comments.hide-comments"
|
||||
></div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
<redaction-annotation-details [annotation]="annotation"></redaction-annotation-details>
|
||||
<redaction-annotation-details [annotation]="annotation()"></redaction-annotation-details>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, HostBinding, inject, Input, OnChanges } from '@angular/core';
|
||||
import { Component, computed, effect, HostBinding, inject, input } from '@angular/core';
|
||||
import { getConfig, StopPropagationDirective } from '@iqser/common-ui';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { ListItem } from '@models/file/list-item';
|
||||
@ -6,13 +6,13 @@ import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { PdfProxyService } from '../../services/pdf-proxy.service';
|
||||
import { ActionsHelpModeKeys } from '../../utils/constants';
|
||||
import { AnnotationCardComponent } from '../annotation-card/annotation-card.component';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { AnnotationActionsComponent } from '../annotation-actions/annotation-actions.component';
|
||||
import { CommentsComponent } from '../comments/comments.component';
|
||||
import { AnnotationDetailsComponent } from '../annotation-details/annotation-details.component';
|
||||
import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotation-wrapper',
|
||||
@ -21,7 +21,6 @@ import { AnnotationDetailsComponent } from '../annotation-details/annotation-det
|
||||
standalone: true,
|
||||
imports: [
|
||||
AnnotationCardComponent,
|
||||
NgIf,
|
||||
MatTooltip,
|
||||
TranslateModule,
|
||||
StopPropagationDirective,
|
||||
@ -29,28 +28,31 @@ import { AnnotationDetailsComponent } from '../annotation-details/annotation-det
|
||||
AnnotationActionsComponent,
|
||||
CommentsComponent,
|
||||
AnnotationDetailsComponent,
|
||||
ReplaceNbspPipe,
|
||||
],
|
||||
})
|
||||
export class AnnotationWrapperComponent implements OnChanges {
|
||||
readonly #isDocumine = getConfig().IS_DOCUMINE;
|
||||
export class AnnotationWrapperComponent {
|
||||
readonly annotation = input.required<ListItem<AnnotationWrapper>>();
|
||||
@HostBinding('attr.annotation-id') annotationId: string;
|
||||
@HostBinding('class.active') active: boolean;
|
||||
|
||||
readonly actionsHelpModeKey = computed(() => this.#getActionsHelpModeKey(this.annotation().item));
|
||||
showComments = false;
|
||||
protected readonly pdfProxyService = inject(PdfProxyService);
|
||||
protected readonly multiSelectService = inject(MultiSelectService);
|
||||
@Input({ required: true }) annotation!: ListItem<AnnotationWrapper>;
|
||||
@HostBinding('attr.annotation-id') annotationId: string;
|
||||
@HostBinding('class.active') active = false;
|
||||
actionsHelpModeKey: string;
|
||||
showComments = false;
|
||||
readonly #isDocumine = getConfig().IS_DOCUMINE;
|
||||
|
||||
ngOnChanges() {
|
||||
this.annotationId = this.annotation.item.id;
|
||||
this.active = this.annotation.isSelected;
|
||||
this.actionsHelpModeKey = this.#getActionsHelpModeKey();
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.active = this.annotation().isSelected;
|
||||
this.annotationId = this.annotation().item.id;
|
||||
});
|
||||
}
|
||||
|
||||
#getActionsHelpModeKey(): string {
|
||||
#getActionsHelpModeKey(item: AnnotationWrapper): string {
|
||||
if (!this.#isDocumine) {
|
||||
const superType = this.annotation.item.superTypeLabel?.split('.')[1];
|
||||
const type = this.annotation.item.type;
|
||||
const superType = item.superTypeLabel?.split('.')[1];
|
||||
const type = item.type;
|
||||
if (superType === 'hint' && (type === 'ocr' || type === 'formula' || type === 'image')) {
|
||||
return ActionsHelpModeKeys[`${superType}-${type}`];
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<ng-container *ngFor="let annotation of annotations; let idx = index; trackBy: _trackBy">
|
||||
<div *ngIf="showHighlightGroup(idx) as highlightGroup" class="workload-separator">
|
||||
@for (annotation of annotations(); track annotation.item.id) {
|
||||
@if (displayedHighlightGroups()[$index]; as highlightGroup) {
|
||||
<redaction-highlights-separator [annotation]="annotation.item" [highlightGroup]="highlightGroup"></redaction-highlights-separator>
|
||||
</div>
|
||||
}
|
||||
|
||||
<redaction-annotation-wrapper
|
||||
*ngIf="!annotation.item.hiddenInWorkload"
|
||||
(click)="annotationClicked(annotation.item, $event)"
|
||||
*ngIf="!annotation.item.hiddenInWorkload"
|
||||
[annotation]="annotation"
|
||||
[id]="'annotation-' + annotation.item.id"
|
||||
[class.documine-wrapper]="isDocumine"
|
||||
[id]="'annotation-' + annotation.item.id"
|
||||
></redaction-annotation-wrapper>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<redaction-annotation-references-list
|
||||
(referenceClicked)="referenceClicked($event)"
|
||||
|
||||
@ -3,21 +3,13 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
@include common-mixins.scroll-bar;
|
||||
}
|
||||
overflow-y: auto;
|
||||
@include common-mixins.scroll-bar;
|
||||
|
||||
&.has-scrollbar:hover redaction-annotation-wrapper::ng-deep,
|
||||
&::ng-deep.documine-wrapper {
|
||||
.annotation {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
redaction-annotation-details {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { Component, computed, ElementRef, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, computed, ElementRef, input, output } from '@angular/core';
|
||||
import { getConfig, HasScrollbarDirective } from '@iqser/common-ui';
|
||||
import { FilterService } from '@iqser/common-ui/lib/filtering';
|
||||
import { IqserEventTarget } from '@iqser/common-ui/lib/utils';
|
||||
@ -21,18 +22,29 @@ import { HighlightsSeparatorComponent } from '../highlights-separator/highlights
|
||||
templateUrl: './annotations-list.component.html',
|
||||
styleUrls: ['./annotations-list.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgForOf, NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent],
|
||||
imports: [NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent],
|
||||
})
|
||||
export class AnnotationsListComponent extends HasScrollbarDirective {
|
||||
readonly #earmarkGroups = computed(() => {
|
||||
if (this._viewModeService.isEarmarks()) {
|
||||
return this.#getEarmarksGroups();
|
||||
}
|
||||
return [] as EarmarkGroup[];
|
||||
});
|
||||
protected readonly isDocumine = getConfig().IS_DOCUMINE;
|
||||
@Input({ required: true }) annotations: ListItem<AnnotationWrapper>[];
|
||||
@Output() readonly pagesPanelActive = new EventEmitter<boolean>();
|
||||
readonly annotations = input.required<ListItem<AnnotationWrapper>[]>();
|
||||
readonly pagesPanelActive = output<boolean>();
|
||||
readonly displayedHighlightGroups = computed(() => {
|
||||
const isEarMarks = this._viewModeService.isEarmarks();
|
||||
let result: Record<number, EarmarkGroup> = {};
|
||||
|
||||
if (isEarMarks) {
|
||||
const annotations = this.annotations();
|
||||
const earmarkGroups = this.#getEarmarksGroups(annotations);
|
||||
for (let idx = 0; idx < annotations.length; ++idx) {
|
||||
const group = earmarkGroups.find(h => h.startIdx === idx);
|
||||
if (group) {
|
||||
result[idx] = group;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected readonly _elementRef: ElementRef,
|
||||
@ -43,11 +55,15 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
|
||||
private readonly _annotationManager: REDAnnotationManager,
|
||||
private readonly _listingService: AnnotationsListingService,
|
||||
readonly annotationReferencesService: AnnotationReferencesService,
|
||||
private readonly clipboard: Clipboard,
|
||||
) {
|
||||
super(_elementRef);
|
||||
}
|
||||
|
||||
annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void {
|
||||
if ($event.ctrlKey && $event.altKey) {
|
||||
this.clipboard.copy(annotation.id);
|
||||
}
|
||||
if (this._userPreferenceService.isIqserDevMode) {
|
||||
console.log('Selected Annotation:', annotation);
|
||||
}
|
||||
@ -68,6 +84,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
|
||||
this._multiSelectService.activate();
|
||||
}
|
||||
this._listingService.selectAnnotations(annotation);
|
||||
this._annotationManager.setSelectedFromWorkload();
|
||||
}
|
||||
|
||||
referenceClicked(annotation: AnnotationWrapper): void {
|
||||
@ -77,26 +94,20 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
|
||||
}
|
||||
}
|
||||
|
||||
showHighlightGroup(idx: number): EarmarkGroup {
|
||||
return this._viewModeService.isEarmarks() && this.#earmarkGroups().find(h => h.startIdx === idx);
|
||||
}
|
||||
|
||||
protected readonly _trackBy = (index: number, listItem: ListItem<AnnotationWrapper>) => listItem.item.id;
|
||||
|
||||
#getEarmarksGroups() {
|
||||
#getEarmarksGroups(annotations: ListItem<AnnotationWrapper>[]) {
|
||||
const earmarksGroups: EarmarkGroup[] = [];
|
||||
|
||||
if (!this.annotations?.length) {
|
||||
if (!annotations.length) {
|
||||
return earmarksGroups;
|
||||
}
|
||||
|
||||
let lastGroup: EarmarkGroup;
|
||||
for (let idx = 0; idx < this.annotations.length; ++idx) {
|
||||
if (idx === 0 || this.annotations[idx].item.color !== this.annotations[idx - 1].item.color) {
|
||||
for (let idx = 0; idx < annotations.length; ++idx) {
|
||||
if (idx === 0 || annotations[idx].item.color !== annotations[idx - 1].item.color) {
|
||||
if (lastGroup) {
|
||||
earmarksGroups.push(lastGroup);
|
||||
}
|
||||
lastGroup = { startIdx: idx, length: 1, color: this.annotations[idx].item.color };
|
||||
lastGroup = { startIdx: idx, length: 1, color: annotations[idx].item.color };
|
||||
} else {
|
||||
lastGroup.length += 1;
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
<div cdkDrag class="editing-value">
|
||||
<mat-icon
|
||||
[class.hidden-button]="currentEntry().componentValues.length === 1"
|
||||
[attr.help-mode-key]="'change_component_order'"
|
||||
[attr.help-mode-key]="currentEntry().componentValues.length > 1 ? 'change_component_order' : null"
|
||||
cdkDragHandle
|
||||
class="draggable"
|
||||
svgIcon="red:draggable-dots"
|
||||
@ -44,7 +44,7 @@
|
||||
(action)="removeValue($index)"
|
||||
[tooltip]="'component-management.actions.delete' | translate"
|
||||
[class.hidden-button]="currentEntry().componentValues.length === 1"
|
||||
[attr.help-mode-key]="'remove_component_value'"
|
||||
[attr.help-mode-key]="currentEntry().componentValues.length > 1 ? 'remove_component_value' : null"
|
||||
class="remove-value"
|
||||
icon="iqser:trash"
|
||||
></iqser-circle-button>
|
||||
|
||||
@ -2,12 +2,13 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 0 10px 0;
|
||||
margin-left: 26px;
|
||||
margin-right: 26px;
|
||||
margin-left: 14px;
|
||||
margin-right: 20px;
|
||||
position: relative;
|
||||
|
||||
.component {
|
||||
width: 40%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value {
|
||||
@ -72,13 +73,13 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-right: 26px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.component {
|
||||
margin-left: 26px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
@ -94,7 +95,7 @@
|
||||
border-left: 4px solid var(--iqser-primary);
|
||||
|
||||
.component {
|
||||
margin-left: 22px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
|
||||
@ -32,11 +32,11 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@if (displayedAnnotations$ | async; as annotations) {
|
||||
@if (filteredAnnotations$ | async; as annotations) {
|
||||
<div class="right-content">
|
||||
<ng-container *ngIf="!isDocumine">
|
||||
<redaction-readonly-banner
|
||||
*ngIf="showAnalysisDisabledBanner; else readOnlyBanner"
|
||||
*ngIf="showAnalysisDisabledBanner(); else readOnlyBanner"
|
||||
[customTranslation]="translations.analysisDisabled"
|
||||
></redaction-readonly-banner>
|
||||
<ng-template #readOnlyBanner>
|
||||
@ -112,7 +112,7 @@
|
||||
></iqser-circle-button>
|
||||
|
||||
<span
|
||||
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations.length }"
|
||||
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations().length }"
|
||||
[translate]="'page'"
|
||||
class="all-caps-label"
|
||||
></span>
|
||||
@ -200,7 +200,7 @@
|
||||
|
||||
<redaction-annotations-list
|
||||
(pagesPanelActive)="pagesPanelActive = $event"
|
||||
[annotations]="annotations.get(pdf.currentPage())"
|
||||
[annotations]="annotationsList$ | async"
|
||||
></redaction-annotations-list>
|
||||
</div>
|
||||
}
|
||||
@ -224,7 +224,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #documineHeader>
|
||||
<span [translate]="'annotations'" [attr.help-mode-key]="'annotations_list'"></span>
|
||||
<span [attr.help-mode-key]="'annotations_list'" [translate]="'annotations'"></span>
|
||||
<ng-container *ngTemplateOutlet="annotationsFilter"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
flex-direction: column;
|
||||
|
||||
&.documine-width {
|
||||
width: calc(var(--documine-workload-content-width) - 50px);
|
||||
width: calc(var(--documine-workload-content-width) - 55px);
|
||||
border-right: 1px solid var(--iqser-separator);
|
||||
z-index: 1;
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
TemplateRef,
|
||||
untracked,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@ -36,7 +37,7 @@ import { workloadTranslations } from '@translations/workload-translations';
|
||||
import { UserPreferenceService } from '@users/user-preference.service';
|
||||
import { getLocalStorageDataByFileId } from '@utils/local-storage';
|
||||
import { combineLatest, delay, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { filter, map, tap } from 'rxjs/operators';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
|
||||
import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service';
|
||||
@ -57,6 +58,7 @@ import { PageExclusionComponent } from '../page-exclusion/page-exclusion.compone
|
||||
import { PagesComponent } from '../pages/pages.component';
|
||||
import { ReadonlyBannerComponent } from '../readonly-banner/readonly-banner.component';
|
||||
import { DocumentInfoComponent } from '../document-info/document-info.component';
|
||||
import { getLast } from '@utils/functions';
|
||||
|
||||
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
|
||||
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
@ -89,23 +91,30 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
],
|
||||
})
|
||||
export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
|
||||
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
|
||||
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
|
||||
readonly multiSelectTemplate = viewChild<TemplateRef<any>>('multiSelect');
|
||||
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
|
||||
readonly annotationsList$: Observable<ListItem<AnnotationWrapper>[]>;
|
||||
readonly allPages = computed(() => Array.from({ length: this.state.file()?.numberOfPages }, (_x, i) => i + 1));
|
||||
protected readonly iconButtonTypes = IconButtonTypes;
|
||||
protected readonly circleButtonTypes = CircleButtonTypes;
|
||||
protected readonly displayedAnnotations$: Observable<Map<number, ListItem<AnnotationWrapper>[]>>;
|
||||
protected readonly filteredAnnotations$: Observable<Map<number, ListItem<AnnotationWrapper>[]>>;
|
||||
protected readonly title = computed(() =>
|
||||
this.viewModeService.isEarmarks() ? _('file-preview.tabs.highlights.label') : _('file-preview.tabs.annotations.label'),
|
||||
);
|
||||
protected readonly currentPageIsExcluded = computed(() => this.state.file().excludedPages.includes(this.pdf.currentPage()));
|
||||
protected readonly translations = workloadTranslations;
|
||||
protected readonly isDocumine = getConfig().IS_DOCUMINE;
|
||||
readonly showAnalysisDisabledBanner = computed(() => {
|
||||
const file = this.state.file();
|
||||
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
|
||||
});
|
||||
protected displayedAnnotations = new Map<number, AnnotationWrapper[]>();
|
||||
readonly activeAnnotations = computed(() => this.displayedAnnotations.get(this.pdf.currentPage()) || []);
|
||||
protected displayedPages: number[] = [];
|
||||
protected pagesPanelActive = true;
|
||||
protected enabledFilters = [];
|
||||
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
|
||||
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
|
||||
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
|
||||
#displayedPagesChanged = false;
|
||||
|
||||
constructor(
|
||||
@ -129,10 +138,9 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
) {
|
||||
super();
|
||||
|
||||
// TODO: ngOnDetach is not called here, so we need to unsubscribe manually
|
||||
this.addActiveScreenSubscription = this.pdf.currentPage$.subscribe(pageNumber => {
|
||||
effect(() => {
|
||||
this._scrollViews();
|
||||
this.scrollAnnotationsToPage(pageNumber, 'always');
|
||||
this.scrollAnnotationsToPage(this.pdf.currentPage(), 'always');
|
||||
});
|
||||
|
||||
this.addActiveScreenSubscription = this.listingService.selected$.subscribe(annotationIds => {
|
||||
@ -146,7 +154,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
this.handleKeyEvent($event);
|
||||
});
|
||||
|
||||
this.displayedAnnotations$ = this._displayedAnnotations$;
|
||||
this.filteredAnnotations$ = this._filteredAnnotations$;
|
||||
|
||||
this.annotationsList$ = combineLatest([this.filteredAnnotations$, this.pdf.currentPage$]).pipe(
|
||||
map(([annotations, page]) => annotations.get(page)),
|
||||
);
|
||||
|
||||
effect(() => {
|
||||
if (this.multiSelectService.inactive()) {
|
||||
@ -169,24 +181,16 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
);
|
||||
}
|
||||
|
||||
get activeAnnotations(): AnnotationWrapper[] {
|
||||
return this.displayedAnnotations.get(this.pdf.currentPage()) || [];
|
||||
}
|
||||
|
||||
get showAnalysisDisabledBanner() {
|
||||
const file = this.state.file();
|
||||
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
|
||||
}
|
||||
|
||||
private get _firstSelectedAnnotation() {
|
||||
return this.listingService.selected.length ? this.listingService.selected[0] : null;
|
||||
}
|
||||
|
||||
private get _displayedAnnotations$(): Observable<Map<number, ListItem<AnnotationWrapper>[]>> {
|
||||
private get _filteredAnnotations$(): Observable<Map<number, ListItem<AnnotationWrapper>[]>> {
|
||||
const primary$ = this.filterService.getFilterModels$('primaryFilters');
|
||||
const secondary$ = this.filterService.getFilterModels$('secondaryFilters');
|
||||
|
||||
return combineLatest([
|
||||
this._documentViewer.loaded$,
|
||||
this.fileDataService.all$,
|
||||
primary$,
|
||||
secondary$,
|
||||
@ -195,7 +199,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
this._pageRotationService.rotations$,
|
||||
]).pipe(
|
||||
delay(0),
|
||||
map(([annotations, primary, secondary, componentReferenceIds]) =>
|
||||
filter(([loaded]) => loaded),
|
||||
map(([, annotations, primary, secondary, componentReferenceIds]) =>
|
||||
this.#filterAnnotations(annotations, primary, secondary, componentReferenceIds),
|
||||
),
|
||||
map(annotations => this.#mapListItemsFromAnnotationWrapperArray(annotations)),
|
||||
@ -203,10 +208,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
);
|
||||
}
|
||||
|
||||
get #allPages() {
|
||||
return Array.from({ length: this.state.file()?.numberOfPages }, (_x, i) => i + 1);
|
||||
}
|
||||
|
||||
private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
|
||||
if (elements.length > 0) {
|
||||
scrollIntoView(elements[0], {
|
||||
@ -220,12 +221,13 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
|
||||
ngOnInit(): void {
|
||||
setTimeout(() => {
|
||||
const showExcludePages = getLocalStorageDataByFileId(this.state.file()?.id, 'show-exclude-pages') ?? false;
|
||||
const file = untracked(this.state.file);
|
||||
const showExcludePages = getLocalStorageDataByFileId(file?.id, 'show-exclude-pages') ?? false;
|
||||
if (showExcludePages) {
|
||||
this.excludedPagesService.show();
|
||||
}
|
||||
|
||||
const showDocumentInfo = getLocalStorageDataByFileId(this.state.file()?.id, 'show-document-info') ?? false;
|
||||
const showDocumentInfo = getLocalStorageDataByFileId(file?.id, 'show-document-info') ?? false;
|
||||
if (showDocumentInfo) {
|
||||
this.documentInfoService.show();
|
||||
}
|
||||
@ -233,16 +235,20 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
selectAllOnActivePage() {
|
||||
this.listingService.selectAnnotations(this.activeAnnotations);
|
||||
const activeAnnotations = untracked(this.activeAnnotations);
|
||||
this.listingService.selectAnnotations(activeAnnotations);
|
||||
}
|
||||
|
||||
deselectAllOnActivePage(): void {
|
||||
this.listingService.deselect(this.activeAnnotations);
|
||||
this.annotationManager.deselect(this.activeAnnotations);
|
||||
const activeAnnotations = untracked(this.activeAnnotations);
|
||||
this.listingService.deselect(activeAnnotations);
|
||||
this.annotationManager.deselect(activeAnnotations);
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyEvent($event: KeyboardEvent): void {
|
||||
const multiSelectServiceInactive = untracked(this.multiSelectService.inactive);
|
||||
|
||||
if (
|
||||
!ALL_HOTKEY_ARRAY.includes($event.key) ||
|
||||
this._dialog.openDialogs.length ||
|
||||
@ -262,7 +268,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
// if we activated annotationsPanel -
|
||||
// select first annotation from this page in case there is no
|
||||
// selected annotation on this page and not in multi select mode
|
||||
if (!this.pagesPanelActive && this.multiSelectService.inactive()) {
|
||||
if (!this.pagesPanelActive && multiSelectServiceInactive) {
|
||||
this._documentViewer.clearSelection();
|
||||
this.#selectFirstAnnotationOnCurrentPageIfNecessary();
|
||||
}
|
||||
@ -273,7 +279,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
if (!this.pagesPanelActive) {
|
||||
// Disable annotation navigation in multi select mode
|
||||
// => TODO: maybe implement selection on enter?
|
||||
if (this.multiSelectService.inactive()) {
|
||||
if (multiSelectServiceInactive) {
|
||||
this.navigateAnnotations($event);
|
||||
}
|
||||
} else {
|
||||
@ -284,7 +290,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
scrollAnnotations(): void {
|
||||
const currentPage = this.pdf.currentPage();
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
if (this._firstSelectedAnnotation?.pageNumber === currentPage) {
|
||||
return;
|
||||
}
|
||||
@ -292,27 +298,27 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
|
||||
if (this._annotationsElement()) {
|
||||
const elements: HTMLElement[] = this._annotationsElement().nativeElement.querySelectorAll(
|
||||
`div[anotation-page-header="${page}"]`,
|
||||
);
|
||||
const annotationsElement = untracked(this._annotationsElement);
|
||||
if (annotationsElement) {
|
||||
const elements: HTMLElement[] = annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements, mode);
|
||||
}
|
||||
}
|
||||
|
||||
@Debounce()
|
||||
scrollToSelectedAnnotation(): void {
|
||||
if (this.listingService.selected.length === 0 || !this._annotationsElement()) {
|
||||
const annotationsElement = untracked(this._annotationsElement);
|
||||
if (this.listingService.selected.length === 0 || !annotationsElement) {
|
||||
return;
|
||||
}
|
||||
const elements: HTMLElement[] = this._annotationsElement().nativeElement.querySelectorAll(
|
||||
const elements: HTMLElement[] = annotationsElement.nativeElement.querySelectorAll(
|
||||
`[annotation-id="${this._firstSelectedAnnotation?.id}"]`,
|
||||
);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
scrollQuickNavigation(): void {
|
||||
const currentPage = this.pdf.currentPage();
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
let quickNavPageIndex = this.displayedPages.findIndex(p => p >= currentPage);
|
||||
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== currentPage) {
|
||||
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
|
||||
@ -325,7 +331,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
scrollQuickNavLast() {
|
||||
this.pdf.navigateTo(this.state.file().numberOfPages);
|
||||
const file = untracked(this.state.file);
|
||||
this.pdf.navigateTo(file.numberOfPages);
|
||||
}
|
||||
|
||||
preventKeyDefault($event: KeyboardEvent): void {
|
||||
@ -342,12 +349,14 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
this.pdf.navigateTo(this.#nextPageWithAnnotations());
|
||||
}
|
||||
|
||||
@Debounce(15)
|
||||
navigateAnnotations($event: KeyboardEvent) {
|
||||
const currentPage = this.pdf.currentPage();
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
const activeAnnotations = untracked(this.activeAnnotations);
|
||||
if (!this._firstSelectedAnnotation || currentPage !== this._firstSelectedAnnotation.pageNumber) {
|
||||
if (this.displayedPages.indexOf(currentPage) !== -1) {
|
||||
// Displayed page has annotations
|
||||
return this.listingService.selectAnnotations(this.activeAnnotations ? this.activeAnnotations[0] : null);
|
||||
return this.listingService.selectAnnotations(activeAnnotations ? activeAnnotations[0] : null);
|
||||
}
|
||||
// Displayed page doesn't have annotations
|
||||
if ($event.key === 'ArrowDown') {
|
||||
@ -359,7 +368,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
const prevPage = this.#prevPageWithAnnotations();
|
||||
const prevPageAnnotations = this.displayedAnnotations.get(prevPage);
|
||||
|
||||
return this.listingService.selectAnnotations(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
return this.listingService.selectAnnotations(getLast(prevPageAnnotations));
|
||||
}
|
||||
|
||||
const page = this._firstSelectedAnnotation.pageNumber;
|
||||
@ -396,7 +405,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
for (let i = previousPageIdx; i >= 0; i--) {
|
||||
const prevPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]);
|
||||
if (prevPageAnnotations) {
|
||||
this.listingService.selectAnnotations(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
this.listingService.selectAnnotations(getLast(prevPageAnnotations));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -420,14 +429,16 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
secondary: INestedFilter[] = [],
|
||||
componentReferenceIds: string[],
|
||||
): Map<number, AnnotationWrapper[]> {
|
||||
const onlyPageWithAnnotations = this.viewModeService.onlyPagesWithAnnotations();
|
||||
const onlyPageWithAnnotations = untracked(this.viewModeService.onlyPagesWithAnnotations);
|
||||
const isRedacted = untracked(this.viewModeService.isRedacted);
|
||||
const allPages = untracked(this.allPages);
|
||||
if (!primary || primary.length === 0) {
|
||||
const pages = onlyPageWithAnnotations ? [] : this.#allPages;
|
||||
const pages = onlyPageWithAnnotations ? [] : allPages;
|
||||
this.#setDisplayedPages(pages);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewModeService.isRedacted()) {
|
||||
if (isRedacted) {
|
||||
annotations = annotations.filter(a => !a.isRemoved);
|
||||
}
|
||||
|
||||
@ -445,7 +456,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
this.enabledFilters = this.filterService.enabledFlatFilters;
|
||||
if (this.enabledFilters.some(f => f.id === 'pages-without-annotations')) {
|
||||
if (this.enabledFilters.length === 1 && !onlyPageWithAnnotations) {
|
||||
const pages = this.#allPages.filter(page => !pagesThatDisplayAnnotations.includes(page));
|
||||
const pages = allPages.filter(page => !pagesThatDisplayAnnotations.includes(page));
|
||||
this.#setDisplayedPages(pages);
|
||||
} else {
|
||||
this.#setDisplayedPages([]);
|
||||
@ -454,7 +465,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
} else if (this.enabledFilters.length || onlyPageWithAnnotations || componentReferenceIds) {
|
||||
this.#setDisplayedPages(pagesThatDisplayAnnotations);
|
||||
} else {
|
||||
this.#setDisplayedPages(this.#allPages);
|
||||
this.#setDisplayedPages(allPages);
|
||||
}
|
||||
this.displayedPages.sort((a, b) => a - b);
|
||||
|
||||
@ -462,18 +473,20 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
#selectFirstAnnotationOnCurrentPageIfNecessary() {
|
||||
const currentPage = this.pdf.currentPage();
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
const activeAnnotations = untracked(this.activeAnnotations);
|
||||
if (
|
||||
(!this._firstSelectedAnnotation || currentPage !== this._firstSelectedAnnotation.pageNumber) &&
|
||||
this.displayedPages.indexOf(currentPage) >= 0 &&
|
||||
this.activeAnnotations.length > 0
|
||||
activeAnnotations.length > 0
|
||||
) {
|
||||
this.listingService.selectAnnotations(this.activeAnnotations[0]);
|
||||
this.listingService.selectAnnotations(activeAnnotations[0]);
|
||||
}
|
||||
}
|
||||
|
||||
#navigatePages($event: KeyboardEvent) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.pdf.currentPage());
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
const pageIdx = this.displayedPages.indexOf(currentPage);
|
||||
|
||||
if ($event.key !== 'ArrowDown') {
|
||||
if (pageIdx === -1) {
|
||||
@ -505,9 +518,10 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
#nextPageWithAnnotations() {
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
let idx = 0;
|
||||
for (const page of this.displayedPages) {
|
||||
if (page > this.pdf.currentPage() && this.displayedAnnotations.get(page)) {
|
||||
if (page > currentPage && this.displayedAnnotations.get(page)) {
|
||||
break;
|
||||
}
|
||||
++idx;
|
||||
@ -516,10 +530,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
#prevPageWithAnnotations() {
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
let idx = this.displayedPages.length - 1;
|
||||
const reverseDisplayedPages = [...this.displayedPages].reverse();
|
||||
for (const page of reverseDisplayedPages) {
|
||||
if (page < this.pdf.currentPage() && this.displayedAnnotations.get(page)) {
|
||||
if (page < currentPage && this.displayedAnnotations.get(page)) {
|
||||
break;
|
||||
}
|
||||
--idx;
|
||||
@ -528,8 +543,9 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
#scrollQuickNavigationToPage(page: number) {
|
||||
if (this._quickNavigationElement()) {
|
||||
const elements: HTMLElement[] = this._quickNavigationElement().nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
|
||||
const quickNavigationElement = untracked(this._quickNavigationElement);
|
||||
if (quickNavigationElement) {
|
||||
const elements: HTMLElement[] = quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements);
|
||||
}
|
||||
}
|
||||
@ -550,7 +566,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
}
|
||||
|
||||
#scrollToFirstAnnotationPage(annotations: Map<number, ListItem<AnnotationWrapper>[]>) {
|
||||
if (this.isDocumine && annotations.size && this.#displayedPagesChanged && !this.displayedPages.includes(this.pdf.currentPage())) {
|
||||
const currentPage = untracked(this.pdf.currentPage);
|
||||
if (this.isDocumine && annotations.size && this.#displayedPagesChanged && !this.displayedPages.includes(currentPage)) {
|
||||
const page = annotations.keys().next().value;
|
||||
this.pdf.navigateTo(page);
|
||||
}
|
||||
@ -571,4 +588,16 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
|
||||
this.displayedPages.every((value, index) => value === newDisplayedPages[index])
|
||||
);
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
clickInsideWorkloadView($event: MouseEvent) {
|
||||
$event?.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('document: click')
|
||||
clickOutsideWorkloadView() {
|
||||
if (this.multiSelectService.active() && !this._dialog.openDialogs.length) {
|
||||
this.multiSelectService.deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th *ngFor="let column of columns" [ngClass]="{ hide: !column.show, 'w-50': staticColumns }">
|
||||
<label>{{ column.label }}</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody [ngStyle]="{ height: redactedTextsAreaHeight + 'px' }">
|
||||
<tr *ngFor="let row of data">
|
||||
<td *ngFor="let cell of row" [ngClass]="{ hide: !cell.show, bold: cell.bold, 'w-50': staticColumns }">
|
||||
<div [ngStyle]="gridConfig()" class="table">
|
||||
@for (column of _columns(); track column.label) {
|
||||
<div [ngClass]="{ hide: !!column.hide }" class="col cell">
|
||||
<label>{{ column.label }}</label>
|
||||
</div>
|
||||
}
|
||||
@for (row of _data(); track $index) {
|
||||
@for (cell of row; track cell.label) {
|
||||
<div [ngClass]="{ background: _data().indexOf(row) % 2 === 0, hide: !!cell.hide, bold: cell.bold }" class="cell">
|
||||
{{ cell.label }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,74 +1,44 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
table {
|
||||
.table {
|
||||
padding: 0 13px;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
border-spacing: 0;
|
||||
display: grid;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
@include common-mixins.scroll-bar;
|
||||
|
||||
tbody {
|
||||
padding-top: 2px;
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
@include common-mixins.scroll-bar;
|
||||
.col {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
|
||||
label {
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
display: table;
|
||||
.cell {
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
list-style-position: inside;
|
||||
overflow: hidden;
|
||||
|
||||
th {
|
||||
label {
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
&:not(.w-50) {
|
||||
width: 25%;
|
||||
}
|
||||
max-width: 0;
|
||||
text-align: start;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
list-style-position: inside;
|
||||
overflow: hidden;
|
||||
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
max-width: 0;
|
||||
width: 50%;
|
||||
padding-right: 0;
|
||||
}
|
||||
padding-right: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
td {
|
||||
background-color: var(--iqser-alt-background);
|
||||
}
|
||||
.background {
|
||||
background-color: var(--iqser-alt-background);
|
||||
}
|
||||
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.w-50 {
|
||||
max-width: 0;
|
||||
min-width: 50%;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { NgClass, NgForOf, NgStyle } from '@angular/common';
|
||||
|
||||
export interface ValueColumn {
|
||||
label: string;
|
||||
show: boolean;
|
||||
hide?: boolean;
|
||||
bold?: boolean;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const TABLE_ROW_SIZE = 18;
|
||||
const TABLE_ROW_SIZE = 20;
|
||||
const MAX_ITEMS_DISPLAY = 10;
|
||||
|
||||
@Component({
|
||||
@ -18,11 +19,33 @@ const MAX_ITEMS_DISPLAY = 10;
|
||||
styleUrl: './selected-annotations-table.component.scss',
|
||||
})
|
||||
export class SelectedAnnotationsTableComponent {
|
||||
@Input({ required: true }) columns: ValueColumn[];
|
||||
@Input({ required: true }) data: ValueColumn[][];
|
||||
@Input() staticColumns = false;
|
||||
readonly defaultColumnWidth = input(false);
|
||||
|
||||
get redactedTextsAreaHeight() {
|
||||
return this.data.length <= MAX_ITEMS_DISPLAY ? TABLE_ROW_SIZE * this.data.length : TABLE_ROW_SIZE * MAX_ITEMS_DISPLAY;
|
||||
readonly columns = input.required<ValueColumn[]>();
|
||||
readonly _columns = computed(() => this.columns().filter(item => !this.defaultColumnWidth() || !item.hide));
|
||||
|
||||
readonly data = input.required<ValueColumn[][]>();
|
||||
readonly _data = computed(() => this.data().map(item => item.filter(cell => !this.defaultColumnWidth() || !cell.hide)));
|
||||
|
||||
readonly redactedTextsAreaHeight = computed(
|
||||
() =>
|
||||
(this._data().length <= MAX_ITEMS_DISPLAY ? TABLE_ROW_SIZE * this._data().length : TABLE_ROW_SIZE * MAX_ITEMS_DISPLAY) +
|
||||
TABLE_ROW_SIZE,
|
||||
);
|
||||
|
||||
readonly gridConfig = computed(() => ({
|
||||
height: `${this.redactedTextsAreaHeight()}px`,
|
||||
'grid-template-columns': !this.defaultColumnWidth() ? this.#computeCustomColumnWidths() : this.#getDefaultColumnWidth(),
|
||||
'grid-template-rows': `repeat(${this._data().length + 1}, ${TABLE_ROW_SIZE}px)`,
|
||||
}));
|
||||
|
||||
#computeCustomColumnWidths() {
|
||||
return this._columns()
|
||||
.map(column => column.width ?? `auto`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
#getDefaultColumnWidth() {
|
||||
return `repeat(${this._columns().length}, 1fr)`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
.components-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -24,18 +26,19 @@ mat-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
height: calc(100% - 40px);
|
||||
@include common-mixins.scroll-bar;
|
||||
|
||||
.component-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 13px;
|
||||
margin-right: 13px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 10px 26px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
|
||||
:first-child {
|
||||
@ -47,7 +50,9 @@ mat-icon {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--iqser-separator);
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--iqser-separator);
|
||||
margin-right: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit, signal, ViewChildren } from '@angular/core';
|
||||
import { Component, effect, Input, OnInit, signal, ViewChildren } from '@angular/core';
|
||||
import { List } from '@common-ui/utils';
|
||||
import { IconButtonTypes, LoadingService } from '@iqser/common-ui';
|
||||
import { ComponentLogEntry, Dictionary, File, IComponentLogEntry, WorkflowFileStatuses } from '@red/domain';
|
||||
@ -11,6 +11,7 @@ import { map } from 'rxjs/operators';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-structured-component-management',
|
||||
@ -33,7 +34,13 @@ export class StructuredComponentManagementComponent implements OnInit {
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _componentLogFilterService: ComponentLogFilterService,
|
||||
private readonly _filterService: FilterService,
|
||||
) {}
|
||||
private readonly _state: FilePreviewStateService,
|
||||
) {
|
||||
effect(async () => {
|
||||
this._state.file();
|
||||
await this.#loadData();
|
||||
});
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return this.file.workflowStatus !== WorkflowFileStatuses.APPROVED;
|
||||
@ -75,7 +82,6 @@ export class StructuredComponentManagementComponent implements OnInit {
|
||||
}
|
||||
|
||||
async #loadData(): Promise<void> {
|
||||
this._loadingService.start();
|
||||
const componentLogData = await firstValueFrom(
|
||||
this._componentLogService.getComponentLogData(
|
||||
this.file.dossierTemplateId,
|
||||
|
||||
@ -27,7 +27,7 @@ export class ViewSwitchComponent {
|
||||
});
|
||||
protected readonly canSwitchToRedactedView = computed(() => {
|
||||
const file = this._state.file();
|
||||
return !file.analysisRequired && !file.excluded && !file.isError;
|
||||
return !file.excluded && !file.isError;
|
||||
});
|
||||
protected readonly canSwitchToEarmarksView = computed(() => {
|
||||
const file = this._state.file();
|
||||
|
||||
@ -3,9 +3,56 @@
|
||||
<div [translate]="'add-hint.dialog.title'" class="dialog-header heading-l"></div>
|
||||
|
||||
<div class="dialog-content redaction">
|
||||
<div class="iqser-input-group w-450">
|
||||
<label class="selected-text" [translate]="'add-hint.dialog.content.selected-text'"></label>
|
||||
{{ form.get('selectedText').value }}
|
||||
<div class="iqser-input-group w-full selected-text-group">
|
||||
<div
|
||||
[class.fixed-height-36]="dictionaryRequest"
|
||||
[ngClass]="isEditingSelectedText ? 'flex relative' : 'flex-align-items-center'"
|
||||
>
|
||||
<div class="table">
|
||||
<label> {{ 'add-hint.dialog.content.value' | translate }} </label>
|
||||
<div class="row">
|
||||
<span
|
||||
*ngIf="!isEditingSelectedText"
|
||||
[innerHTML]="form.controls.selectedText.value"
|
||||
[ngStyle]="{
|
||||
'min-width': textWidth > maximumSelectedTextWidth ? '95%' : 'unset',
|
||||
'max-width': textWidth > maximumSelectedTextWidth ? 0 : 'unset',
|
||||
}"
|
||||
></span>
|
||||
<textarea
|
||||
*ngIf="isEditingSelectedText"
|
||||
[rows]="selectedTextRows"
|
||||
[ngStyle]="{ width: maximumTextAreaWidth + 'px' }"
|
||||
formControlName="selectedText"
|
||||
iqserHasScrollbar
|
||||
name="value"
|
||||
type="text"
|
||||
></textarea>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="toggleEditingSelectedText()"
|
||||
*ngIf="dictionaryRequest && !isEditingSelectedText"
|
||||
[tooltip]="'redact-text.dialog.content.edit-text' | translate"
|
||||
[size]="20"
|
||||
[iconSize]="13"
|
||||
icon="iqser:edit"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="undoTextChange(); toggleEditingSelectedText()"
|
||||
*ngIf="isEditingSelectedText"
|
||||
[showDot]="initialText !== form.get('selectedText').value"
|
||||
[tooltip]="'redact-text.dialog.content.revert-text' | translate"
|
||||
[size]="20"
|
||||
[iconSize]="13"
|
||||
class="undo-button"
|
||||
icon="red:undo"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iqser-details-radio
|
||||
|
||||
@ -1,3 +1,67 @@
|
||||
.dialog-content {
|
||||
height: 400px;
|
||||
height: 493px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selected-text-group > div {
|
||||
gap: 0.5rem;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
iqser-circle-button {
|
||||
padding-left: 10px;
|
||||
|
||||
&.undo-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
::ng-deep mat-icon {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fixed-height-36 {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
textarea[name='value'] {
|
||||
margin-top: 0;
|
||||
min-height: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: calc(100% - 26px);
|
||||
padding: 0 13px;
|
||||
|
||||
label {
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--iqser-alt-background);
|
||||
min-width: 100%;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
list-style-position: inside;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { NgClass, NgForOf, NgIf, NgStyle } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormBuilder, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
|
||||
@ -23,12 +23,14 @@ import { ActiveDossiersService } from '@services/dossiers/active-dossiers.servic
|
||||
import { DictionaryService } from '@services/entity-services/dictionary.service';
|
||||
import { Roles } from '@users/roles';
|
||||
import { UserPreferenceService } from '@users/user-preference.service';
|
||||
import { stringToBoolean } from '@utils/functions';
|
||||
import { calcTextWidthInPixels, stringToBoolean } from '@utils/functions';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { SystemDefaultOption, SystemDefaults } from '../../../account/utils/dialog-defaults';
|
||||
import { getRedactOrHintOptions } from '../../utils/dialog-options';
|
||||
import { AddHintData, AddHintResult, RedactOrHintOption, RedactOrHintOptions } from '../../utils/dialog-types';
|
||||
|
||||
const MAXIMUM_TEXT_AREA_WIDTH = 421;
|
||||
|
||||
@Component({
|
||||
templateUrl: './add-hint-dialog.component.html',
|
||||
styleUrls: ['./add-hint-dialog.component.scss'],
|
||||
@ -49,6 +51,8 @@ import { AddHintData, AddHintResult, RedactOrHintOption, RedactOrHintOptions } f
|
||||
CircleButtonComponent,
|
||||
MatDialogClose,
|
||||
NgForOf,
|
||||
NgClass,
|
||||
NgStyle,
|
||||
],
|
||||
})
|
||||
export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogComponent, AddHintData, AddHintResult> implements OnInit {
|
||||
@ -58,9 +62,15 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
readonly roles = Roles;
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
readonly options: DetailsRadioOption<RedactOrHintOption>[];
|
||||
readonly initialText = this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value;
|
||||
readonly maximumTextAreaWidth = MAXIMUM_TEXT_AREA_WIDTH;
|
||||
readonly maximumSelectedTextWidth = 567;
|
||||
dictionaryRequest = false;
|
||||
dictionaries: Dictionary[] = [];
|
||||
form!: UntypedFormGroup;
|
||||
isEditingSelectedText = false;
|
||||
selectedTextRows = 1;
|
||||
textWidth: number;
|
||||
|
||||
constructor(
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
@ -83,6 +93,7 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
);
|
||||
|
||||
this.form = this.#getForm();
|
||||
this.textWidth = calcTextWidthInPixels(this.form.controls.selectedText.value);
|
||||
|
||||
this.form
|
||||
.get('option')
|
||||
@ -97,6 +108,18 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
toggleEditingSelectedText() {
|
||||
this.isEditingSelectedText = !this.isEditingSelectedText;
|
||||
if (this.isEditingSelectedText) {
|
||||
const width = calcTextWidthInPixels(this.form.controls.selectedText.value);
|
||||
this.selectedTextRows = Math.ceil(width / MAXIMUM_TEXT_AREA_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
undoTextChange() {
|
||||
this.form.patchValue({ selectedText: this.initialText });
|
||||
}
|
||||
|
||||
get displayedDictionaryLabel() {
|
||||
const dictType = this.form.get('dictionary').value;
|
||||
if (dictType) {
|
||||
@ -137,7 +160,7 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
}
|
||||
|
||||
extraOptionChanged(option: DetailsRadioOption<RedactOrHintOption>): void {
|
||||
this.#applyToAllDossiers = option.extraOption.checked;
|
||||
this.#applyToAllDossiers = option.additionalCheck.checked;
|
||||
|
||||
this.#setDictionaries();
|
||||
if (this.#applyToAllDossiers && this.form.get('dictionary').value) {
|
||||
@ -153,7 +176,7 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
if (!this.#applyToAllDossiers) {
|
||||
const selectedDictionaryType = this.form.get('dictionary').value;
|
||||
const selectedDictionary = this.dictionaries.find(d => d.type === selectedDictionaryType);
|
||||
this.options[1].extraOption.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
this.options[1].additionalCheck.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +223,7 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
|
||||
#resetValues() {
|
||||
this.#applyToAllDossiers = this.applyToAll;
|
||||
if (!this.#isRss) {
|
||||
this.options[1].extraOption.checked = this.#applyToAllDossiers;
|
||||
this.options[1].additionalCheck.checked = this.#applyToAllDossiers;
|
||||
}
|
||||
this.form.get('dictionary').setValue(null);
|
||||
}
|
||||
|
||||
@ -11,12 +11,7 @@ import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
export interface LegalBasisOption {
|
||||
label?: string;
|
||||
legalBasis?: string;
|
||||
description?: string;
|
||||
}
|
||||
import { LegalBasisOption } from '../../utils/dialog-types';
|
||||
|
||||
@Component({
|
||||
templateUrl: './change-legal-basis-dialog.component.html',
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
IconButtonTypes,
|
||||
IqserDialogComponent,
|
||||
} from '@iqser/common-ui';
|
||||
import { Dictionary, Dossier, SuperTypes } from '@red/domain';
|
||||
import { Dictionary, Dossier } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DictionaryService } from '@services/entity-services/dictionary.service';
|
||||
import { Roles } from '@users/roles';
|
||||
@ -47,12 +47,12 @@ export class EditAnnotationDialogComponent
|
||||
extends IqserDialogComponent<EditAnnotationDialogComponent, EditRedactionData, EditRedactResult>
|
||||
implements OnInit
|
||||
{
|
||||
readonly #dossier: Dossier;
|
||||
readonly roles = Roles;
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
readonly redactedTexts: string[];
|
||||
dictionaries: Dictionary[] = [];
|
||||
form: UntypedFormGroup;
|
||||
readonly #dossier: Dossier;
|
||||
|
||||
constructor(
|
||||
private readonly _activeDossiersService: ActiveDossiersService,
|
||||
@ -60,7 +60,7 @@ export class EditAnnotationDialogComponent
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
) {
|
||||
super();
|
||||
this.#dossier = _activeDossiersService.find(this.data.dossierId);
|
||||
this.#dossier = this._activeDossiersService.find(this.data.dossierId);
|
||||
const annotations = this.data.annotations;
|
||||
this.redactedTexts = annotations.map(annotation => annotation.value);
|
||||
this.form = this.#getForm();
|
||||
@ -83,10 +83,6 @@ export class EditAnnotationDialogComponent
|
||||
this.#setTypes();
|
||||
}
|
||||
|
||||
reasonChanged() {
|
||||
this.form.patchValue({ reason: this.dictionaries.find(d => d.type === SuperTypes.ManualRedaction) });
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const value = this.form.value;
|
||||
this.dialogRef.close({
|
||||
@ -106,8 +102,4 @@ export class EditAnnotationDialogComponent
|
||||
type: [sameType ? this.data.annotations[0].type : null],
|
||||
});
|
||||
}
|
||||
|
||||
#allRectangles() {
|
||||
return this.data.annotations.reduce((acc, a) => acc && a.AREA, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,16 +6,23 @@
|
||||
class="dialog-header heading-l"
|
||||
></div>
|
||||
|
||||
<div class="dialog-content redaction" [class.fixed-height]="isRedacted && isImage">
|
||||
<div class="iqser-input-group" *ngIf="!isImage && redactedTexts.length && !allRectangles">
|
||||
<div [class.image-dialog]="isRedacted && isImage" [class.rectangle-dialog]="allRectangles" class="dialog-content redaction">
|
||||
<div *ngIf="!isImage && redactedTexts.length && !allRectangles" class="iqser-input-group">
|
||||
<redaction-selected-annotations-table
|
||||
[columns]="tableColumns"
|
||||
[data]="tableData"
|
||||
[staticColumns]="true"
|
||||
[defaultColumnWidth]="true"
|
||||
></redaction-selected-annotations-table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isManualRedaction" class="iqser-input-group w-450" [class.required]="!form.controls.type.disabled">
|
||||
<iqser-details-radio
|
||||
*ngIf="!isImage && annotations.length === 1"
|
||||
[options]="options"
|
||||
(extraOptionChanged)="extraOptionChanged($event)"
|
||||
formControlName="option"
|
||||
></iqser-details-radio>
|
||||
|
||||
<div *ngIf="!isManualRedaction" [class.required]="!form.controls.type.disabled" class="iqser-input-group w-450">
|
||||
<label [translate]="'edit-redaction.dialog.content.type'"></label>
|
||||
|
||||
<mat-form-field>
|
||||
@ -83,7 +90,7 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="allRectangles" class="iqser-input-group w-400">
|
||||
<div *ngIf="allRectangles" class="iqser-input-group w-450">
|
||||
<label [translate]="'change-legal-basis-dialog.content.classification'"></label>
|
||||
<input
|
||||
[placeholder]="'edit-redaction.dialog.content.unchanged' | translate"
|
||||
@ -100,7 +107,7 @@
|
||||
formControlName="comment"
|
||||
iqserHasScrollbar
|
||||
name="comment"
|
||||
rows="4"
|
||||
rows="3"
|
||||
type="text"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
.dialog-content {
|
||||
padding-top: 8px;
|
||||
|
||||
&.fixed-height {
|
||||
height: 386px;
|
||||
&.rectangle-dialog {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
&.image-dialog {
|
||||
height: 346px;
|
||||
}
|
||||
|
||||
.rectangle-dialog,
|
||||
.image-dialog {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { MatDialogClose } from '@angular/material/dialog';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
|
||||
import {
|
||||
CircleButtonComponent,
|
||||
HasScrollbarDirective,
|
||||
@ -26,10 +25,20 @@ import {
|
||||
SelectedAnnotationsTableComponent,
|
||||
ValueColumn,
|
||||
} from '../../components/selected-annotations-table/selected-annotations-table.component';
|
||||
import { DialogHelpModeKeys } from '../../utils/constants';
|
||||
import { getEditRedactionOptions } from '../../utils/dialog-options';
|
||||
import { EditRedactionData, EditRedactResult, RedactOrHintOption } from '../../utils/dialog-types';
|
||||
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
|
||||
import { getEditRedactionOptions, getRectangleRedactOptions } from '../../utils/dialog-options';
|
||||
import {
|
||||
EditRedactionData,
|
||||
EditRedactionOption,
|
||||
EditRedactResult,
|
||||
LegalBasisOption,
|
||||
RectangleRedactOption,
|
||||
RectangleRedactOptions,
|
||||
RedactOrHintOptions,
|
||||
} from '../../utils/dialog-types';
|
||||
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
|
||||
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
|
||||
import { validatePageRange } from '../../utils/form-validators';
|
||||
import { parseRectanglePosition, parseSelectedPageNumbers, prefillPageRange } from '../../utils/enhance-manual-redaction-request.utils';
|
||||
|
||||
interface TypeSelectOptions {
|
||||
type: string;
|
||||
@ -58,18 +67,16 @@ interface TypeSelectOptions {
|
||||
HelpButtonComponent,
|
||||
MatDialogClose,
|
||||
HasScrollbarDirective,
|
||||
DetailsRadioComponent,
|
||||
],
|
||||
})
|
||||
export class EditRedactionDialogComponent
|
||||
extends IqserDialogComponent<EditRedactionDialogComponent, EditRedactionData, EditRedactResult>
|
||||
implements OnInit
|
||||
{
|
||||
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
|
||||
readonly #applyToAllDossiers = this.data.applyToAllDossiers;
|
||||
protected readonly roles = Roles;
|
||||
readonly ignoredKeys = ['option', 'comment'];
|
||||
readonly annotations = this.data.annotations;
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
readonly isModifyDictionary = this.annotations.every(annotation => annotation.isModifyDictionary);
|
||||
readonly isImage = this.annotations.reduce((acc, next) => acc && next.isImage, true);
|
||||
readonly redactedTexts = !this.isImage ? this.annotations.map(annotation => annotation.value).filter(value => !!value) : null;
|
||||
readonly isManualRedaction = this.annotations.some(annotation => annotation.type === SuperTypes.ManualRedaction);
|
||||
@ -77,33 +84,34 @@ export class EditRedactionDialogComponent
|
||||
readonly isRedacted = this.annotations.every(annotation => annotation.isRedacted);
|
||||
readonly isImported: boolean = this.annotations.every(annotation => annotation.imported);
|
||||
readonly allRectangles = this.annotations.reduce((acc, a) => acc && a.AREA, true);
|
||||
readonly tableColumns = [
|
||||
{
|
||||
label: 'Value',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }];
|
||||
readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [
|
||||
{ label: redaction.value, show: true, bold: true },
|
||||
{ label: redaction.typeLabel, show: true },
|
||||
{ label: redaction.value, bold: true },
|
||||
{ label: redaction.typeLabel },
|
||||
]);
|
||||
options: DetailsRadioOption<RedactOrHintOption>[] | undefined;
|
||||
options = this.allRectangles ? getRectangleRedactOptions('edit') : getEditRedactionOptions(this.isHint);
|
||||
legalOptions: LegalBasisOption[] = [];
|
||||
dictionaries: Dictionary[] = [];
|
||||
typeSelectOptions: TypeSelectOptions[] = [];
|
||||
readonly form = this.#getForm();
|
||||
hasTypeChanged = false;
|
||||
initialReasonDisabled = this.someSkipped;
|
||||
protected readonly roles = Roles;
|
||||
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
|
||||
|
||||
constructor(
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
) {
|
||||
super();
|
||||
|
||||
if (this.allRectangles) {
|
||||
prefillPageRange(
|
||||
this.data.annotations[0],
|
||||
this.data.allFileAnnotations,
|
||||
this.options as DetailsRadioOption<RectangleRedactOption>[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get displayedDictionaryLabel() {
|
||||
@ -153,20 +161,18 @@ export class EditRedactionDialogComponent
|
||||
return this.annotations.length > 1;
|
||||
}
|
||||
|
||||
get helpButtonKey() {
|
||||
if (this.isHint) {
|
||||
return DialogHelpModeKeys.HINT_EDIT;
|
||||
}
|
||||
if (this.someSkipped) {
|
||||
return DialogHelpModeKeys.SKIPPED_EDIT;
|
||||
}
|
||||
return DialogHelpModeKeys.REDACTION_EDIT;
|
||||
}
|
||||
|
||||
get sameType() {
|
||||
return this.annotations.every(annotation => annotation.type === this.annotations[0].type);
|
||||
}
|
||||
|
||||
extraOptionChanged(option: DetailsRadioOption<EditRedactionOption | RectangleRedactOption>): void {
|
||||
if (option.value === RectangleRedactOptions.MULTIPLE_PAGES) {
|
||||
setTimeout(() => {
|
||||
this.form.get('option')?.updateValueAndValidity();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.#setTypes();
|
||||
const data = await firstValueFrom(this._justificationsService.loadAll(this.#dossier.dossierTemplateId));
|
||||
@ -188,7 +194,6 @@ export class EditRedactionDialogComponent
|
||||
|
||||
typeChanged() {
|
||||
const selectedDictionaryType = this.form.controls.type.value;
|
||||
this.#setOptions(selectedDictionaryType);
|
||||
|
||||
const initialReason = this.form.get('type').value === this.initialFormValue.type && !this.initialReasonDisabled;
|
||||
if (this.redactBasedTypes.includes(selectedDictionaryType) || initialReason) {
|
||||
@ -202,19 +207,29 @@ export class EditRedactionDialogComponent
|
||||
} else {
|
||||
this.form.controls.reason.disable();
|
||||
}
|
||||
this.form.patchValue({ reason: null, option: null });
|
||||
this.form.patchValue({ reason: null });
|
||||
}
|
||||
|
||||
save() {
|
||||
const value = this.form.value;
|
||||
const initialReason: LegalBasisOption = this.initialFormValue.reason;
|
||||
const initialLegalBasis = initialReason?.legalBasis ?? '';
|
||||
const pageNumbers = parseSelectedPageNumbers(
|
||||
this.form.get('option').value?.additionalInput?.value,
|
||||
this.data.file,
|
||||
this.data.annotations[0],
|
||||
);
|
||||
const position = parseRectanglePosition(this.annotations[0]);
|
||||
|
||||
this.close({
|
||||
legalBasis: value.reason?.legalBasis ?? (this.isImage ? initialLegalBasis : ''),
|
||||
section: value.section,
|
||||
comment: value.comment,
|
||||
type: value.type,
|
||||
value: this.allRectangles ? value.value : null,
|
||||
option: value.option?.value ?? RedactOrHintOptions.ONLY_HERE,
|
||||
position,
|
||||
pageNumbers,
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,22 +254,6 @@ export class EditRedactionDialogComponent
|
||||
}
|
||||
}
|
||||
|
||||
#setOptions(type: string, reasonChanged = false) {
|
||||
const selectedDictionary = this.dictionaries.find(d => d.type === type);
|
||||
this.options = getEditRedactionOptions(
|
||||
this.#dossier.dossierName,
|
||||
this.#applyToAllDossiers,
|
||||
!!selectedDictionary?.dossierDictionaryOnly,
|
||||
this.isModifyDictionary,
|
||||
);
|
||||
this.form.patchValue(
|
||||
{
|
||||
option: !this.isModifyDictionary || reasonChanged ? this.options[0] : this.options[1],
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
}
|
||||
|
||||
#getForm() {
|
||||
const sameSection = this.annotations.every(annotation => annotation.section === this.annotations[0].section);
|
||||
return new FormGroup({
|
||||
@ -265,7 +264,10 @@ export class EditRedactionDialogComponent
|
||||
disabled: this.isImported,
|
||||
}),
|
||||
section: new FormControl<string>({ value: sameSection ? this.annotations[0].section : null, disabled: this.isImported }),
|
||||
option: new FormControl<LegalBasisOption>(null),
|
||||
option: new FormControl<DetailsRadioOption<EditRedactionOption | RectangleRedactOption>>(
|
||||
this.options[0],
|
||||
validatePageRange(this.data.file.numberOfPages),
|
||||
),
|
||||
value: new FormControl<string>(this.allRectangles ? this.annotations[0].value : null),
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,32 +2,40 @@
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div class="dialog-header heading-l" [translate]="dialogTitle"></div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<redaction-selected-annotations-table
|
||||
[columns]="tableColumns"
|
||||
[data]="tableData"
|
||||
[staticColumns]="true"
|
||||
*ngIf="!isImageHint"
|
||||
></redaction-selected-annotations-table>
|
||||
<div *ngIf="!isHintDialog && !isDocumine" class="iqser-input-group required w-400">
|
||||
<label [translate]="'manual-annotation.dialog.content.reason'"></label>
|
||||
<mat-form-field>
|
||||
<mat-select
|
||||
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
|
||||
class="full-width"
|
||||
formControlName="reason"
|
||||
>
|
||||
<mat-option *ngFor="let option of legalOptions" [matTooltip]="option.description" [value]="option">
|
||||
{{ option.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="dialog-content force-annotation">
|
||||
@if (!isImageHint) {
|
||||
<redaction-selected-annotations-table
|
||||
[columns]="tableColumns"
|
||||
[data]="tableData"
|
||||
[defaultColumnWidth]="true"
|
||||
></redaction-selected-annotations-table>
|
||||
}
|
||||
|
||||
<div *ngIf="!isHintDialog && !isDocumine" class="iqser-input-group w-400">
|
||||
<label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
|
||||
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
|
||||
</div>
|
||||
@if (!isHintDialog && !isDocumine) {
|
||||
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>
|
||||
|
||||
<div class="iqser-input-group required w-400">
|
||||
<label [translate]="'manual-annotation.dialog.content.reason'"></label>
|
||||
<mat-form-field>
|
||||
<mat-select
|
||||
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
|
||||
class="full-width"
|
||||
formControlName="reason"
|
||||
>
|
||||
@for (option of legalOptions; track option) {
|
||||
<mat-option [matTooltip]="option.description" [value]="option">
|
||||
{{ option.label }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group w-400">
|
||||
<label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
|
||||
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="iqser-input-group w-300">
|
||||
<label [translate]="'manual-annotation.dialog.content.comment'"></label>
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
BaseDialogComponent,
|
||||
CircleButtonComponent,
|
||||
getConfig,
|
||||
HasScrollbarDirective,
|
||||
HelpButtonComponent,
|
||||
IconButtonComponent,
|
||||
IqserDenyDirective,
|
||||
IqserDialogComponent,
|
||||
} from '@iqser/common-ui';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
import { Dossier, ILegalBasisChangeRequest } from '@red/domain';
|
||||
@ -21,21 +20,19 @@ import {
|
||||
ValueColumn,
|
||||
} from '../../components/selected-annotations-table/selected-annotations-table.component';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
|
||||
import { ForceAnnotationData, ForceAnnotationOption, ForceAnnotationResult, LegalBasisOption } from '../../utils/dialog-types';
|
||||
import { getForceAnnotationOptions } from '../../utils/dialog-options';
|
||||
import { SystemDefaults } from '../../../account/utils/dialog-defaults';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
export interface LegalBasisOption {
|
||||
label?: string;
|
||||
legalBasis?: string;
|
||||
description?: string;
|
||||
}
|
||||
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
|
||||
|
||||
const DOCUMINE_LEGAL_BASIS = 'n-a.';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-force-annotation-dialog',
|
||||
templateUrl: './force-annotation-dialog.component.html',
|
||||
styleUrls: ['./force-annotation-dialog.component.scss'],
|
||||
standalone: true,
|
||||
@ -55,24 +52,21 @@ const DOCUMINE_LEGAL_BASIS = 'n-a.';
|
||||
CircleButtonComponent,
|
||||
NgForOf,
|
||||
HelpButtonComponent,
|
||||
DetailsRadioComponent,
|
||||
],
|
||||
})
|
||||
export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
|
||||
export class ForceAnnotationDialogComponent
|
||||
extends IqserDialogComponent<ForceAnnotationDialogComponent, ForceAnnotationData, ForceAnnotationResult>
|
||||
implements OnInit
|
||||
{
|
||||
readonly isDocumine = getConfig().IS_DOCUMINE;
|
||||
readonly options: DetailsRadioOption<ForceAnnotationOption>[];
|
||||
|
||||
readonly tableColumns = [
|
||||
{
|
||||
label: 'Value',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
readonly tableData: ValueColumn[][] = this._data.annotations.map(redaction => [
|
||||
{ label: redaction.value, show: true, bold: true },
|
||||
{ label: redaction.typeLabel, show: true },
|
||||
readonly form: FormGroup;
|
||||
readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }];
|
||||
readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [
|
||||
{ label: redaction.value, bold: true },
|
||||
{ label: redaction.typeLabel },
|
||||
]);
|
||||
|
||||
legalOptions: LegalBasisOption[] = [];
|
||||
@ -80,20 +74,23 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
|
||||
|
||||
constructor(
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
protected readonly _dialogRef: MatDialogRef<ForceAnnotationDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] },
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
) {
|
||||
super(_dialogRef);
|
||||
super();
|
||||
this.options = getForceAnnotationOptions(this.isDocumine, this.isHintDialog, this.isImageDialog);
|
||||
this.form = this.#getForm();
|
||||
}
|
||||
|
||||
get isImageHint() {
|
||||
return this._data.annotations.every(annotation => annotation.IMAGE_HINT);
|
||||
return this.data.annotations.every(annotation => annotation.IMAGE_HINT);
|
||||
}
|
||||
|
||||
get isHintDialog() {
|
||||
return this._data.hint;
|
||||
return this.data.hint;
|
||||
}
|
||||
|
||||
get isImageDialog() {
|
||||
return this.data.image;
|
||||
}
|
||||
|
||||
get disabled(): boolean {
|
||||
@ -110,7 +107,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.isDocumine) {
|
||||
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId));
|
||||
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.data.dossier.dossierTemplateId));
|
||||
|
||||
this.legalOptions = data.map(lbm => ({
|
||||
legalBasis: lbm.reason,
|
||||
@ -121,8 +118,8 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
|
||||
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
// Set pre-existing reason if it exists
|
||||
const existingReason = this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis);
|
||||
if (!this._data.hint && existingReason) {
|
||||
const existingReason = this.legalOptions.find(option => option.legalBasis === this.data.annotations[0].legalBasis);
|
||||
if (!this.data.hint && existingReason) {
|
||||
this.form.patchValue({ reason: existingReason }, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
@ -130,13 +127,14 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
|
||||
}
|
||||
|
||||
save() {
|
||||
this._dialogRef.close(this.#createForceRedactionRequest());
|
||||
this.close(this.#createForceRedactionRequest());
|
||||
}
|
||||
|
||||
#getForm(): UntypedFormGroup {
|
||||
return this._formBuilder.group({
|
||||
reason: this._data.hint ? ['Forced Hint'] : [null, !this.isDocumine ? Validators.required : null],
|
||||
reason: this.data.hint ? ['Forced Hint'] : [null, !this.isDocumine ? Validators.required : null],
|
||||
comment: [null],
|
||||
option: this.options.find(o => o.value === SystemDefaults.FORCE_REDACTION_DEFAULT),
|
||||
});
|
||||
}
|
||||
|
||||
@ -145,6 +143,8 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
|
||||
|
||||
request.legalBasis = !this.isDocumine ? this.form.get('reason').value.legalBasis : DOCUMINE_LEGAL_BASIS;
|
||||
request.comment = this.form.get('comment').value;
|
||||
request.reason = this.form.get('reason').value.description;
|
||||
request.option = this.form.get('option').value?.value;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
<section class="dialog">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div [translate]="title" class="dialog-header heading-l"></div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div *ngIf="!isRectangle" class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.text'"></label>
|
||||
<div *ngIf="!isEditingSelectedText" class="flex-align-items-center">
|
||||
{{ form.get('selectedText').value }}
|
||||
<iqser-circle-button
|
||||
(action)="isEditingSelectedText = true"
|
||||
*ngIf="isDictionaryRequest"
|
||||
[tooltip]="'manual-annotation.dialog.content.edit-selected-text' | translate"
|
||||
icon="iqser:edit"
|
||||
tooltipPosition="below"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
*ngIf="isEditingSelectedText"
|
||||
formControlName="selectedText"
|
||||
iqserHasScrollbar
|
||||
name="comment"
|
||||
rows="4"
|
||||
type="text"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRectangle" class="iqser-input-group">
|
||||
<label [translate]="'manual-annotation.dialog.content.rectangle'"></label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!isFalsePositiveRequest && (isDictionaryRequest || !manualRedactionTypeExists)"
|
||||
class="iqser-input-group required w-450"
|
||||
>
|
||||
<label *ngIf="isDictionaryRequest" [translate]="'manual-annotation.dialog.content.dictionary'"></label>
|
||||
<label *ngIf="!isDictionaryRequest" [translate]="'manual-annotation.dialog.content.type'"></label>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="dictionary">
|
||||
<mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let dictionary of possibleDictionaries"
|
||||
[matTooltip]="dictionary.description"
|
||||
[value]="dictionary.type"
|
||||
matTooltipPosition="after"
|
||||
>
|
||||
<span> {{ dictionary.label }} </span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group required w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.reason'"></label>
|
||||
<mat-form-field>
|
||||
<mat-select
|
||||
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
|
||||
class="full-width"
|
||||
formControlName="reason"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let option of legalOptions"
|
||||
[matTooltip]="option.description"
|
||||
[value]="option"
|
||||
matTooltipPosition="after"
|
||||
>
|
||||
{{ option.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
|
||||
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRectangle" class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.section'"></label>
|
||||
<input formControlName="section" name="section" type="text" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRectangle" class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.classification'"></label>
|
||||
<input formControlName="classification" name="classification" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.comment'"></label>
|
||||
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRectangle" class="apply-on-multiple-pages iqser-input-group w-450">
|
||||
<mat-checkbox
|
||||
(change)="applyOnMultiplePages = !applyOnMultiplePages"
|
||||
[checked]="applyOnMultiplePages"
|
||||
class="mb-15"
|
||||
color="primary"
|
||||
>
|
||||
{{ 'manual-annotation.dialog.content.apply-on-multiple-pages' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<div *ngIf="applyOnMultiplePages">
|
||||
<input
|
||||
[placeholder]="'manual-annotation.dialog.content.apply-on-multiple-pages-placeholder' | translate"
|
||||
class="full-width"
|
||||
formControlName="multiplePages"
|
||||
/>
|
||||
|
||||
<span class="hint">{{ 'manual-annotation.dialog.content.apply-on-multiple-pages-hint' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
[disabled]="disabled"
|
||||
[label]="'manual-annotation.dialog.actions.save' | translate"
|
||||
[submit]="true"
|
||||
[type]="iconButtonTypes.primary"
|
||||
>
|
||||
</iqser-icon-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
|
||||
</section>
|
||||
@ -1,235 +0,0 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import {
|
||||
BaseDialogComponent,
|
||||
CircleButtonComponent,
|
||||
HasScrollbarDirective,
|
||||
IconButtonComponent,
|
||||
IqserDenyDirective,
|
||||
IqserPermissionsService,
|
||||
} from '@iqser/common-ui';
|
||||
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
||||
import { Dictionary, Dossier, File, IAddRedactionRequest, SuperTypes } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DictionaryService } from '@services/entity-services/dictionary.service';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
import { Roles } from '@users/roles';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ManualRedactionService } from '../../services/manual-redaction.service';
|
||||
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
|
||||
export interface LegalBasisOption {
|
||||
label?: string;
|
||||
legalBasis?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './manual-annotation-dialog.component.html',
|
||||
styleUrls: ['./manual-annotation-dialog.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
CircleButtonComponent,
|
||||
TranslateModule,
|
||||
HasScrollbarDirective,
|
||||
MatFormField,
|
||||
MatSelectTrigger,
|
||||
MatSelect,
|
||||
MatOption,
|
||||
NgForOf,
|
||||
MatTooltip,
|
||||
IqserDenyDirective,
|
||||
MatCheckbox,
|
||||
IconButtonComponent,
|
||||
],
|
||||
providers: [ManualRedactionService],
|
||||
})
|
||||
export class ManualAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
|
||||
readonly #dossier: Dossier;
|
||||
readonly roles = Roles;
|
||||
isDictionaryRequest: boolean;
|
||||
isFalsePositiveRequest: boolean;
|
||||
isEditingSelectedText = false;
|
||||
applyOnMultiplePages = false;
|
||||
manualRedactionTypeExists = true;
|
||||
possibleDictionaries: Dictionary[] = [];
|
||||
legalOptions: LegalBasisOption[] = [];
|
||||
|
||||
constructor(
|
||||
readonly iqserPermissionsService: IqserPermissionsService,
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
private readonly _manualRedactionService: ManualRedactionService,
|
||||
activeDossiersService: ActiveDossiersService,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
protected readonly _dialogRef: MatDialogRef<ManualAnnotationDialogComponent>,
|
||||
private readonly _annotationManager: REDAnnotationManager,
|
||||
@Inject(MAT_DIALOG_DATA) readonly data: { manualRedactionEntryWrapper: ManualRedactionEntryWrapper; dossierId: string; file: File },
|
||||
) {
|
||||
super(_dialogRef);
|
||||
this.#dossier = activeDossiersService.find(this.data.dossierId);
|
||||
|
||||
this.isFalsePositiveRequest = this.data.manualRedactionEntryWrapper.type === 'FALSE_POSITIVE';
|
||||
this.isDictionaryRequest = this.data.manualRedactionEntryWrapper.type === 'DICTIONARY' || this.isFalsePositiveRequest;
|
||||
|
||||
this.manualRedactionTypeExists = this._dictionaryService.hasManualType(this.#dossier.dossierTemplateId);
|
||||
|
||||
this.form = this.#getForm();
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this._manualRedactionService.getTitle(this.data.manualRedactionEntryWrapper.type);
|
||||
}
|
||||
|
||||
get isRectangle() {
|
||||
return !!this.data.manualRedactionEntryWrapper.manualRedactionEntry.rectangle;
|
||||
}
|
||||
|
||||
get displayedDictionaryLabel() {
|
||||
const dictType = this.form.get('dictionary').value;
|
||||
if (dictType) {
|
||||
return this.possibleDictionaries.find(d => d.type === dictType).label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return this.form.invalid || (this.applyOnMultiplePages && !this.form.get('multiplePages')?.value);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.possibleDictionaries = this.isDictionaryRequest
|
||||
? this._dictionaryService.getDictionariesOptions(this.#dossier.dossierTemplateId)
|
||||
: this._dictionaryService.getRedactionTypes(this.#dossier.dossierTemplateId);
|
||||
|
||||
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.#dossier.dossierTemplateId));
|
||||
this.legalOptions = data.map(lbm => ({
|
||||
legalBasis: lbm.reason,
|
||||
description: lbm.description,
|
||||
label: lbm.name,
|
||||
}));
|
||||
|
||||
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
this.#selectReason();
|
||||
|
||||
if (!this.isRectangle) {
|
||||
this.#formatSelectedTextValue();
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
this.#enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry);
|
||||
try {
|
||||
const annotations =
|
||||
this.isRectangle && !!this.form.get('multiplePages').value
|
||||
? this.#getRectangles()
|
||||
: [this.data.manualRedactionEntryWrapper];
|
||||
this._dialogRef.close({
|
||||
annotations,
|
||||
dictionary: this.possibleDictionaries.find(d => d.type === this.form.get('dictionary').value),
|
||||
});
|
||||
} catch (e) {
|
||||
this._toaster.error(_('manual-annotation.dialog.error'));
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
super.close();
|
||||
if (this.isRectangle) {
|
||||
this._annotationManager.delete(this._annotationManager.selected[0].Id);
|
||||
}
|
||||
}
|
||||
|
||||
#getRectangles() {
|
||||
const quads = this.data.manualRedactionEntryWrapper.manualRedactionEntry.positions.find(a => !!a);
|
||||
const value: string = this.form.get('multiplePages').value.replace(/[^0-9-,]/g, '');
|
||||
const entry = { ...this.data.manualRedactionEntryWrapper.manualRedactionEntry };
|
||||
const wrapper = { ...this.data.manualRedactionEntryWrapper };
|
||||
const wrappers: ManualRedactionEntryWrapper[] = [wrapper];
|
||||
|
||||
value.split(',').forEach(range => {
|
||||
const splitted = range.split('-');
|
||||
const startPage = parseInt(splitted[0], 10);
|
||||
const endPage = splitted.length > 1 ? parseInt(splitted[1], 10) : startPage;
|
||||
if (!startPage || !endPage || startPage > this.data.file.numberOfPages || endPage > this.data.file.numberOfPages) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
if (page === wrapper.manualRedactionEntry.positions[0].page) {
|
||||
continue;
|
||||
}
|
||||
const manualRedactionEntry = { ...entry, positions: [{ ...quads, page }] };
|
||||
wrappers.push({ ...wrapper, manualRedactionEntry });
|
||||
}
|
||||
});
|
||||
|
||||
return wrappers;
|
||||
}
|
||||
|
||||
#formatSelectedTextValue() {
|
||||
this.data.manualRedactionEntryWrapper.manualRedactionEntry.value =
|
||||
this.data.manualRedactionEntryWrapper.manualRedactionEntry.value.replace(
|
||||
// eslint-disable-next-line no-control-regex,max-len
|
||||
/([^\s\d-]{2,})[-\u00AD]\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]/gi,
|
||||
'$1',
|
||||
);
|
||||
}
|
||||
|
||||
#getForm() {
|
||||
return this._formBuilder.group({
|
||||
selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value,
|
||||
section: [null],
|
||||
reason: this.isDictionaryRequest ? [null] : [null, Validators.required],
|
||||
dictionary: this.isDictionaryRequest
|
||||
? [this.isFalsePositiveRequest ? 'false_positive' : null, Validators.required]
|
||||
: [this.manualRedactionTypeExists ? SuperTypes.ManualRedaction : null, Validators.required],
|
||||
comment: [null],
|
||||
classification: ['non-readable content'],
|
||||
multiplePages: '',
|
||||
});
|
||||
}
|
||||
|
||||
#enhanceManualRedaction(addRedactionRequest: IAddRedactionRequest) {
|
||||
const legalOption: LegalBasisOption = this.form.get('reason').value;
|
||||
addRedactionRequest.type = this.form.get('dictionary').value;
|
||||
if (legalOption) {
|
||||
addRedactionRequest.reason = legalOption.description;
|
||||
addRedactionRequest.legalBasis = legalOption.legalBasis;
|
||||
}
|
||||
|
||||
if (this.iqserPermissionsService.has(Roles.getRss)) {
|
||||
const selectedType = this.possibleDictionaries.find(d => d.type === addRedactionRequest.type);
|
||||
addRedactionRequest.addToDictionary = selectedType.hasDictionary;
|
||||
} else {
|
||||
addRedactionRequest.addToDictionary = this.isDictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
|
||||
}
|
||||
|
||||
if (!addRedactionRequest.reason) {
|
||||
addRedactionRequest.reason = 'Dictionary Request';
|
||||
}
|
||||
const commentValue = this.form.get('comment').value;
|
||||
addRedactionRequest.comment = commentValue ? { text: commentValue } : null;
|
||||
addRedactionRequest.section = this.form.get('section').value;
|
||||
addRedactionRequest.value = addRedactionRequest.rectangle
|
||||
? this.form.get('classification').value
|
||||
: this.form.get('selectedText').value;
|
||||
}
|
||||
|
||||
#selectReason() {
|
||||
if (this.legalOptions.length === 1) {
|
||||
this.form.get('reason').setValue(this.legalOptions[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
<section class="dialog">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div [translate]="'manual-annotation.dialog.header.redaction'" class="dialog-header heading-l"></div>
|
||||
|
||||
<div class="dialog-content redaction">
|
||||
<iqser-details-radio
|
||||
[options]="options"
|
||||
(extraOptionChanged)="extraOptionChanged($event)"
|
||||
formControlName="option"
|
||||
></iqser-details-radio>
|
||||
|
||||
<div *deny="roles.getRss" class="iqser-input-group required w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.reason'"></label>
|
||||
<mat-form-field>
|
||||
<mat-select
|
||||
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
|
||||
class="full-width"
|
||||
formControlName="reason"
|
||||
>
|
||||
@for (option of legalOptions; track option) {
|
||||
<mat-option [matTooltip]="option.description" [value]="option" matTooltipPosition="after">
|
||||
{{ option.label }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *deny="roles.getRss" class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
|
||||
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.section'"></label>
|
||||
<input formControlName="section" name="section" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.classification'"></label>
|
||||
<input formControlName="classification" name="classification" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group w-450">
|
||||
<label [translate]="'manual-annotation.dialog.content.comment'"></label>
|
||||
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="3" type="text"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
[disabled]="disabled"
|
||||
[label]="'manual-annotation.dialog.actions.save' | translate"
|
||||
[submit]="true"
|
||||
[type]="iconButtonTypes.primary"
|
||||
>
|
||||
</iqser-icon-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
|
||||
</section>
|
||||
@ -2,6 +2,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
height: 600px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.apply-on-multiple-pages {
|
||||
min-height: 55px;
|
||||
display: flex;
|
||||
@ -0,0 +1,185 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import {
|
||||
CircleButtonComponent,
|
||||
HasScrollbarDirective,
|
||||
IconButtonComponent,
|
||||
IqserDenyDirective,
|
||||
IqserDialogComponent,
|
||||
Toaster,
|
||||
} from '@iqser/common-ui';
|
||||
import { Dossier, IAddRedactionRequest, SuperTypes } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
import { Roles } from '@users/roles';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ManualRedactionService } from '../../services/manual-redaction.service';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
|
||||
import {
|
||||
LegalBasisOption,
|
||||
RectangleDialogData,
|
||||
RectangleDialogResult,
|
||||
RectangleRedactOption,
|
||||
RectangleRedactOptions,
|
||||
} from '../../utils/dialog-types';
|
||||
import { getRectangleRedactOptions } from '../../utils/dialog-options';
|
||||
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
|
||||
import { SystemDefaults } from '../../../account/utils/dialog-defaults';
|
||||
import { validatePageRange } from '../../utils/form-validators';
|
||||
|
||||
export const NON_READABLE_CONTENT = 'non-readable content';
|
||||
|
||||
@Component({
|
||||
templateUrl: './rectangle-annotation-dialog.component.html',
|
||||
styleUrls: ['./rectangle-annotation-dialog.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
CircleButtonComponent,
|
||||
TranslateModule,
|
||||
HasScrollbarDirective,
|
||||
MatFormField,
|
||||
MatSelectTrigger,
|
||||
MatSelect,
|
||||
MatOption,
|
||||
NgForOf,
|
||||
MatTooltip,
|
||||
IqserDenyDirective,
|
||||
MatCheckbox,
|
||||
IconButtonComponent,
|
||||
DetailsRadioComponent,
|
||||
],
|
||||
providers: [ManualRedactionService],
|
||||
})
|
||||
export class RectangleAnnotationDialog
|
||||
extends IqserDialogComponent<RectangleAnnotationDialog, RectangleDialogData, RectangleDialogResult>
|
||||
implements OnInit
|
||||
{
|
||||
readonly #dossier: Dossier;
|
||||
protected readonly roles = Roles;
|
||||
protected readonly options: DetailsRadioOption<RectangleRedactOption>[];
|
||||
protected legalOptions: LegalBasisOption[] = [];
|
||||
|
||||
readonly form: UntypedFormGroup;
|
||||
|
||||
constructor(
|
||||
private readonly activeDossiersService: ActiveDossiersService,
|
||||
private readonly _justificationsService: JustificationsService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _toaster: Toaster,
|
||||
) {
|
||||
super();
|
||||
this.#dossier = this.activeDossiersService.find(this.data.dossierId);
|
||||
|
||||
this.options = getRectangleRedactOptions();
|
||||
|
||||
this.form = this.#getForm();
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
}
|
||||
|
||||
get #isMultiplePages() {
|
||||
return this.form.get('option').value.value === RectangleRedactOptions.MULTIPLE_PAGES;
|
||||
}
|
||||
|
||||
extraOptionChanged(option: DetailsRadioOption<RectangleRedactOption>): void {
|
||||
if (option.value === RectangleRedactOptions.MULTIPLE_PAGES) {
|
||||
setTimeout(() => {
|
||||
this.form.get('option')?.updateValueAndValidity();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.#dossier.dossierTemplateId));
|
||||
this.legalOptions = data.map(lbm => ({
|
||||
legalBasis: lbm.reason,
|
||||
description: lbm.description,
|
||||
label: lbm.name,
|
||||
}));
|
||||
|
||||
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
this.#selectReason();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.#enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry);
|
||||
try {
|
||||
const annotation = (this.#isMultiplePages ? this.#multiplePagesRectangle : this.data.manualRedactionEntryWrapper)
|
||||
.manualRedactionEntry;
|
||||
super.close({
|
||||
annotation,
|
||||
});
|
||||
} catch (e) {
|
||||
this._toaster.error(_('manual-annotation.dialog.error'));
|
||||
}
|
||||
}
|
||||
|
||||
#getForm() {
|
||||
return this._formBuilder.group({
|
||||
selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value,
|
||||
section: [null],
|
||||
reason: [null, Validators.required],
|
||||
comment: [null],
|
||||
classification: [NON_READABLE_CONTENT],
|
||||
option: [this.#getOption(SystemDefaults.RECTANGLE_REDACT_DEFAULT), validatePageRange(this.data.file.numberOfPages)],
|
||||
});
|
||||
}
|
||||
|
||||
#enhanceManualRedaction(addRedactionRequest: IAddRedactionRequest) {
|
||||
const legalOption: LegalBasisOption = this.form.get('reason').value;
|
||||
addRedactionRequest.type = SuperTypes.ManualRedaction;
|
||||
if (legalOption) {
|
||||
addRedactionRequest.reason = legalOption.description;
|
||||
addRedactionRequest.legalBasis = legalOption.legalBasis;
|
||||
}
|
||||
|
||||
addRedactionRequest.addToDictionary = false;
|
||||
const commentValue = this.form.get('comment').value;
|
||||
addRedactionRequest.comment = commentValue ? (this.#isMultiplePages ? commentValue : { text: commentValue }) : null;
|
||||
addRedactionRequest.section = this.form.get('section').value;
|
||||
addRedactionRequest.value = this.form.get('classification').value;
|
||||
}
|
||||
|
||||
#getOption(option: RectangleRedactOption): DetailsRadioOption<RectangleRedactOption> {
|
||||
return this.options.find(o => o.value === option);
|
||||
}
|
||||
|
||||
get #multiplePagesRectangle() {
|
||||
const value: string = this.form.get('option').value.additionalInput.value.replace(/[^0-9-,]/g, '');
|
||||
const entry = { ...this.data.manualRedactionEntryWrapper.manualRedactionEntry, pageNumbers: [] };
|
||||
const wrapper = {
|
||||
...this.data.manualRedactionEntryWrapper,
|
||||
manualRedactionEntry: entry,
|
||||
};
|
||||
|
||||
value.split(',').forEach(range => {
|
||||
const splitted = range.split('-');
|
||||
const startPage = parseInt(splitted[0], 10);
|
||||
const endPage = splitted.length > 1 ? parseInt(splitted[1], 10) : startPage;
|
||||
if (!startPage || !endPage || startPage > this.data.file.numberOfPages || endPage > this.data.file.numberOfPages) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
wrapper.manualRedactionEntry.pageNumbers.push(page);
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
#selectReason() {
|
||||
if (this.legalOptions.length === 1) {
|
||||
this.form.get('reason').setValue(this.legalOptions[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
<redaction-selected-annotations-table
|
||||
[columns]="tableColumns"
|
||||
[data]="tableData"
|
||||
[staticColumns]="true"
|
||||
[defaultColumnWidth]="true"
|
||||
></redaction-selected-annotations-table>
|
||||
</div>
|
||||
|
||||
|
||||
@ -22,8 +22,14 @@ import {
|
||||
ValueColumn,
|
||||
} from '../../components/selected-annotations-table/selected-annotations-table.component';
|
||||
import { getRedactOrHintOptions } from '../../utils/dialog-options';
|
||||
import { RedactOrHintOption, RedactOrHintOptions, RedactRecommendationData, RedactRecommendationResult } from '../../utils/dialog-types';
|
||||
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
|
||||
import {
|
||||
LegalBasisOption,
|
||||
RedactOrHintOption,
|
||||
RedactOrHintOptions,
|
||||
RedactRecommendationData,
|
||||
RedactRecommendationResult,
|
||||
ResizeOptions,
|
||||
} from '../../utils/dialog-types';
|
||||
|
||||
@Component({
|
||||
templateUrl: './redact-recommendation-dialog.component.html',
|
||||
@ -69,19 +75,10 @@ export class RedactRecommendationDialogComponent
|
||||
reason: [null],
|
||||
});
|
||||
|
||||
readonly tableColumns = [
|
||||
{
|
||||
label: 'Value',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }];
|
||||
readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [
|
||||
{ label: redaction.value, show: true, bold: true },
|
||||
{ label: redaction.typeLabel, show: true },
|
||||
{ label: redaction.value, bold: true },
|
||||
{ label: redaction.typeLabel },
|
||||
]);
|
||||
|
||||
constructor(
|
||||
@ -105,6 +102,10 @@ export class RedactRecommendationDialogComponent
|
||||
this.form.controls.option.setValue(this.options[0]);
|
||||
}
|
||||
|
||||
get isBulkLocal(): boolean {
|
||||
return this.form.controls.option.value.value === ResizeOptions.IN_DOCUMENT;
|
||||
}
|
||||
|
||||
get displayedDictionaryLabel() {
|
||||
const dictType = this.form.controls.dictionary.value;
|
||||
if (dictType) {
|
||||
@ -131,7 +132,7 @@ export class RedactRecommendationDialogComponent
|
||||
}
|
||||
|
||||
extraOptionChanged(option: DetailsRadioOption<RedactOrHintOption>): void {
|
||||
this.#applyToAllDossiers = option.extraOption.checked;
|
||||
this.#applyToAllDossiers = option.additionalCheck.checked;
|
||||
|
||||
this.#setDictionaries();
|
||||
if (this.#applyToAllDossiers && this.form.controls.dictionary.value) {
|
||||
@ -147,7 +148,7 @@ export class RedactRecommendationDialogComponent
|
||||
if (!this.#applyToAllDossiers) {
|
||||
const selectedDictionaryType = this.form.controls.dictionary.value;
|
||||
const selectedDictionary = this.dictionaries.find(d => d.type === selectedDictionaryType);
|
||||
this.options[0].extraOption.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
this.options[0].additionalCheck.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,11 +157,16 @@ export class RedactRecommendationDialogComponent
|
||||
this.close({
|
||||
redaction,
|
||||
isMulti: this.isMulti,
|
||||
bulkLocal: this.isBulkLocal,
|
||||
});
|
||||
}
|
||||
|
||||
#setDictionaries() {
|
||||
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers);
|
||||
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(
|
||||
this.#dossier.dossierId,
|
||||
!this.#applyToAllDossiers,
|
||||
this.#dossier.dossierTemplateId,
|
||||
);
|
||||
}
|
||||
|
||||
#selectReason() {
|
||||
@ -183,7 +189,7 @@ export class RedactRecommendationDialogComponent
|
||||
}
|
||||
|
||||
const commentValue = this.form.controls.comment.value;
|
||||
addRedactionRequest.comment = commentValue ? { text: commentValue } : null;
|
||||
addRedactionRequest.comment = commentValue ? (this.isBulkLocal ? commentValue : { text: commentValue }) : null;
|
||||
addRedactionRequest.addToAllDossiers = this.data.isApprover && this.dictionaryRequest && this.#applyToAllDossiers;
|
||||
return addRedactionRequest;
|
||||
}
|
||||
|
||||
@ -9,14 +9,14 @@
|
||||
[ngClass]="isEditingSelectedText ? 'flex relative' : 'flex-align-items-center'"
|
||||
>
|
||||
<div class="table">
|
||||
<label>Value</label>
|
||||
<label> {{ 'redact-text.dialog.content.value' | translate }} </label>
|
||||
<div class="row">
|
||||
<span
|
||||
*ngIf="!isEditingSelectedText"
|
||||
[innerHTML]="form.controls.selectedText.value"
|
||||
[ngStyle]="{
|
||||
'min-width': textWidth > maximumSelectedTextWidth ? '95%' : 'unset',
|
||||
'max-width': textWidth > maximumSelectedTextWidth ? 0 : 'unset'
|
||||
'max-width': textWidth > maximumSelectedTextWidth ? 0 : 'unset',
|
||||
}"
|
||||
></span>
|
||||
<textarea
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.dialog-content {
|
||||
height: 493px;
|
||||
height: 540px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radi
|
||||
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
|
||||
import { CircleButtonComponent, HasScrollbarDirective, IconButtonComponent, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Dictionary, IAddRedactionRequest, SuperTypes } from '@red/domain';
|
||||
import { Dictionary, SuperTypes } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DictionaryService } from '@services/entity-services/dictionary.service';
|
||||
import { JustificationsService } from '@services/entity-services/justifications.service';
|
||||
@ -21,8 +21,15 @@ import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { SystemDefaultOption, SystemDefaults } from '../../../account/utils/dialog-defaults';
|
||||
import { getRedactOrHintOptions } from '../../utils/dialog-options';
|
||||
import { RedactOrHintOption, RedactOrHintOptions, RedactTextData, RedactTextResult } from '../../utils/dialog-types';
|
||||
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
|
||||
import {
|
||||
LegalBasisOption,
|
||||
RedactOrHintOption,
|
||||
RedactOrHintOptions,
|
||||
RedactTextData,
|
||||
RedactTextResult,
|
||||
ResizeOptions,
|
||||
} from '../../utils/dialog-types';
|
||||
import { enhanceManualRedactionRequest, EnhanceRequestData } from '../../utils/enhance-manual-redaction-request.utils';
|
||||
|
||||
const MAXIMUM_TEXT_AREA_WIDTH = 421;
|
||||
|
||||
@ -109,6 +116,10 @@ export class RedactTextDialogComponent
|
||||
);
|
||||
}
|
||||
|
||||
get isBulkLocal() {
|
||||
return this.form.controls.option.value.value === ResizeOptions.IN_DOCUMENT;
|
||||
}
|
||||
|
||||
get isSystemDefault(): boolean {
|
||||
return this._userPreferences.getAddRedactionDefaultOption() === SystemDefaultOption.SYSTEM_DEFAULT;
|
||||
}
|
||||
@ -149,7 +160,7 @@ export class RedactTextDialogComponent
|
||||
}
|
||||
|
||||
extraOptionChanged(option: DetailsRadioOption<RedactOrHintOption>): void {
|
||||
this.#applyToAllDossiers = option.extraOption.checked;
|
||||
this.#applyToAllDossiers = option.additionalCheck.checked;
|
||||
|
||||
this.#setDictionaries();
|
||||
if (this.#applyToAllDossiers && this.form.controls.dictionary.value) {
|
||||
@ -165,16 +176,17 @@ export class RedactTextDialogComponent
|
||||
if (!this.#applyToAllDossiers) {
|
||||
const selectedDictionaryType = this.form.controls.dictionary.value;
|
||||
const selectedDictionary = this.dictionaries.find(d => d.type === selectedDictionaryType);
|
||||
this.options[1].extraOption.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
this.options[2].additionalCheck.disabled = selectedDictionary.dossierDictionaryOnly;
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.#enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry);
|
||||
const redaction = this.data.manualRedactionEntryWrapper.manualRedactionEntry;
|
||||
enhanceManualRedactionRequest(redaction, this.#enhanceRequestData);
|
||||
this.close({
|
||||
redaction,
|
||||
dictionary: this.dictionaries.find(d => d.type === this.form.controls.dictionary.value),
|
||||
bulkLocal: this.isBulkLocal,
|
||||
});
|
||||
}
|
||||
|
||||
@ -188,14 +200,18 @@ export class RedactTextDialogComponent
|
||||
|
||||
#setupValidators(option: RedactOrHintOption) {
|
||||
switch (option) {
|
||||
case RedactOrHintOptions.IN_DOSSIER:
|
||||
this.form.controls.reason.clearValidators();
|
||||
this.form.controls.dictionary.addValidators(Validators.required);
|
||||
break;
|
||||
case RedactOrHintOptions.ONLY_HERE:
|
||||
this.form.controls.dictionary.clearValidators();
|
||||
this.form.controls.reason.addValidators(Validators.required);
|
||||
break;
|
||||
case RedactOrHintOptions.IN_DOCUMENT:
|
||||
this.form.controls.dictionary.clearValidators();
|
||||
this.form.controls.reason.addValidators(Validators.required);
|
||||
break;
|
||||
case RedactOrHintOptions.IN_DOSSIER:
|
||||
this.form.controls.reason.clearValidators();
|
||||
this.form.controls.dictionary.addValidators(Validators.required);
|
||||
break;
|
||||
}
|
||||
|
||||
this.form.controls.reason.updateValueAndValidity();
|
||||
@ -203,7 +219,11 @@ export class RedactTextDialogComponent
|
||||
}
|
||||
|
||||
#setDictionaries() {
|
||||
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers);
|
||||
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(
|
||||
this.#dossier.dossierId,
|
||||
!this.#applyToAllDossiers,
|
||||
this.#dossier.dossierTemplateId,
|
||||
);
|
||||
}
|
||||
|
||||
#getForm(): FormGroup {
|
||||
@ -222,36 +242,9 @@ export class RedactTextDialogComponent
|
||||
}
|
||||
}
|
||||
|
||||
#enhanceManualRedaction(addRedactionRequest: IAddRedactionRequest) {
|
||||
addRedactionRequest.type = this.form.controls.dictionary.value;
|
||||
addRedactionRequest.section = null;
|
||||
addRedactionRequest.value = this.form.controls.selectedText.value;
|
||||
|
||||
const legalOption: LegalBasisOption = this.form.controls.reason.value;
|
||||
if (legalOption) {
|
||||
addRedactionRequest.reason = legalOption.description;
|
||||
addRedactionRequest.legalBasis = legalOption.legalBasis;
|
||||
}
|
||||
|
||||
const selectedType = this.dictionaries.find(d => d.type === addRedactionRequest.type);
|
||||
|
||||
if (selectedType) {
|
||||
addRedactionRequest.addToDictionary = selectedType.hasDictionary;
|
||||
} else {
|
||||
addRedactionRequest.addToDictionary = this.dictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
|
||||
}
|
||||
|
||||
if (!addRedactionRequest.reason) {
|
||||
addRedactionRequest.reason = 'Dictionary Request';
|
||||
}
|
||||
const commentValue = this.form.controls.comment.value;
|
||||
addRedactionRequest.comment = commentValue ? { text: commentValue } : null;
|
||||
addRedactionRequest.addToAllDossiers = this.data.isApprover && this.dictionaryRequest && this.#applyToAllDossiers;
|
||||
}
|
||||
|
||||
#resetValues() {
|
||||
this.#applyToAllDossiers = this.applyToAll;
|
||||
this.options[1].extraOption.checked = this.#applyToAllDossiers;
|
||||
this.options[2].additionalCheck.checked = this.#applyToAllDossiers;
|
||||
if (this.dictionaryRequest) {
|
||||
this.form.controls.reason.setValue(null);
|
||||
this.form.controls.dictionary.setValue(null);
|
||||
@ -263,4 +256,18 @@ export class RedactTextDialogComponent
|
||||
#getOption(option: RedactOrHintOption): DetailsRadioOption<RedactOrHintOption> {
|
||||
return this.options.find(o => o.value === option);
|
||||
}
|
||||
|
||||
get #enhanceRequestData(): EnhanceRequestData {
|
||||
return {
|
||||
type: this.form.controls.dictionary.value,
|
||||
selectedText: this.form.controls.selectedText.value,
|
||||
reason: this.form.controls.reason.value,
|
||||
dictionaries: this.dictionaries,
|
||||
dictionaryRequest: this.dictionaryRequest,
|
||||
comment: this.form.controls.comment.value,
|
||||
isApprover: this.data.isApprover,
|
||||
applyToAllDossiers: this.#applyToAllDossiers,
|
||||
bulkLocal: this.isBulkLocal,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,15 @@
|
||||
<redaction-selected-annotations-table
|
||||
[columns]="tableColumns()"
|
||||
[data]="tableData()"
|
||||
[staticColumns]="!hasFalsePositiveOption"
|
||||
[defaultColumnWidth]="!hasFalsePositiveOption"
|
||||
></redaction-selected-annotations-table>
|
||||
</div>
|
||||
|
||||
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>
|
||||
<iqser-details-radio
|
||||
(extraOptionChanged)="extraOptionChanged($event)"
|
||||
[options]="options"
|
||||
formControlName="option"
|
||||
></iqser-details-radio>
|
||||
|
||||
<div class="iqser-input-group w-450">
|
||||
<label [translate]="'remove-redaction.dialog.content.comment'"></label>
|
||||
@ -31,6 +35,7 @@
|
||||
|
||||
<div class="dialog-actions">
|
||||
<iqser-icon-button
|
||||
[disabled]="disabled"
|
||||
[label]="'remove-redaction.dialog.actions.save' | translate"
|
||||
[submit]="true"
|
||||
[type]="iconButtonTypes.primary"
|
||||
|
||||
@ -24,9 +24,19 @@ import {
|
||||
SelectedAnnotationsTableComponent,
|
||||
ValueColumn,
|
||||
} from '../../components/selected-annotations-table/selected-annotations-table.component';
|
||||
import { DialogHelpModeKeys } from '../../utils/constants';
|
||||
import { getRemoveRedactionOptions } from '../../utils/dialog-options';
|
||||
import { RemoveRedactionData, RemoveRedactionOption, RemoveRedactionOptions, RemoveRedactionResult } from '../../utils/dialog-types';
|
||||
import { getRectangleRedactOptions, getRemoveRedactionOptions } from '../../utils/dialog-options';
|
||||
import {
|
||||
RectangleRedactOption,
|
||||
RectangleRedactOptions,
|
||||
RemoveRedactionData,
|
||||
RemoveRedactionOption,
|
||||
RemoveRedactionOptions,
|
||||
RemoveRedactionResult,
|
||||
ResizeOptions,
|
||||
} from '../../utils/dialog-types';
|
||||
import { isJustOne } from '@common-ui/utils';
|
||||
import { validatePageRange } from '../../utils/form-validators';
|
||||
import { parseRectanglePosition, parseSelectedPageNumbers, prefillPageRange } from '../../utils/enhance-manual-redaction-request.utils';
|
||||
|
||||
@Component({
|
||||
templateUrl: './remove-redaction-dialog.component.html',
|
||||
@ -84,6 +94,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
|
||||
extra: false,
|
||||
},
|
||||
};
|
||||
readonly #allRectangles = this.data.redactions.reduce((acc, a) => acc && a.AREA, true);
|
||||
readonly #applyToAllDossiers = this.systemDefaultByType[this.annotationsType].extra;
|
||||
readonly isSystemDefault = this.optionByType[this.annotationsType].main === SystemDefaultOption.SYSTEM_DEFAULT;
|
||||
readonly isExtraOptionSystemDefault = this.optionByType[this.annotationsType].extra === 'undefined';
|
||||
@ -91,39 +102,36 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
|
||||
? this.systemDefaultByType[this.annotationsType].main
|
||||
: this.optionByType[this.annotationsType].main;
|
||||
readonly extraOptionPreference = stringToBoolean(this.optionByType[this.annotationsType].extra);
|
||||
readonly options: DetailsRadioOption<RemoveRedactionOption>[] = getRemoveRedactionOptions(
|
||||
this.data,
|
||||
this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference,
|
||||
);
|
||||
readonly options: DetailsRadioOption<RectangleRedactOption | RemoveRedactionOption>[] = this.#allRectangles
|
||||
? getRectangleRedactOptions('remove')
|
||||
: getRemoveRedactionOptions(
|
||||
this.data,
|
||||
this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference,
|
||||
);
|
||||
readonly skipped = this.data.redactions.some(annotation => annotation.isSkipped);
|
||||
readonly redactedTexts = this.data.redactions.map(annotation => annotation.value);
|
||||
form: UntypedFormGroup = this._formBuilder.group({
|
||||
comment: [null],
|
||||
option: [this.defaultOption],
|
||||
option: [this.defaultOption, validatePageRange(this.data.file.numberOfPages, true)],
|
||||
});
|
||||
|
||||
readonly selectedOption = toSignal(this.form.get('option').valueChanges.pipe(map(value => value.value)));
|
||||
readonly isFalsePositive = computed(() => this.selectedOption() === RemoveRedactionOptions.FALSE_POSITIVE);
|
||||
readonly tableColumns = computed<ValueColumn[]>(() => [
|
||||
{
|
||||
label: 'Value',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
show: true,
|
||||
},
|
||||
{ label: 'Value', width: '25%' },
|
||||
{ label: 'Type', width: '25%' },
|
||||
{
|
||||
label: 'Context',
|
||||
show: this.isFalsePositive(),
|
||||
hide: !this.isFalsePositive(),
|
||||
width: '50%',
|
||||
},
|
||||
]);
|
||||
|
||||
readonly tableData = computed<ValueColumn[][]>(() =>
|
||||
this.data.redactions.map((redaction, index) => [
|
||||
{ label: redaction.value, show: true, bold: true },
|
||||
{ label: redaction.typeLabel, show: true },
|
||||
{ label: this.data.falsePositiveContext[index], show: this.isFalsePositive() },
|
||||
{ label: redaction.value, bold: true },
|
||||
{ label: redaction.typeLabel },
|
||||
{ label: this.data.falsePositiveContext[index], hide: !this.isFalsePositive() },
|
||||
]),
|
||||
);
|
||||
|
||||
@ -132,19 +140,14 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
|
||||
private readonly _userPreferences: UserPreferenceService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get helpButtonKey() {
|
||||
if (this.hint) {
|
||||
return DialogHelpModeKeys.HINT_REMOVE;
|
||||
if (this.#allRectangles) {
|
||||
prefillPageRange(
|
||||
this.data.redactions[0],
|
||||
this.data.allFileRedactions,
|
||||
this.options as DetailsRadioOption<RectangleRedactOption>[],
|
||||
);
|
||||
}
|
||||
if (this.recommendation) {
|
||||
return DialogHelpModeKeys.RECOMMENDATION_REMOVE;
|
||||
}
|
||||
if (this.skipped) {
|
||||
return DialogHelpModeKeys.SKIPPED_REMOVE;
|
||||
}
|
||||
return DialogHelpModeKeys.REDACTION_REMOVE;
|
||||
}
|
||||
|
||||
get hasFalsePositiveOption() {
|
||||
@ -162,7 +165,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
|
||||
}
|
||||
|
||||
get isBulk() {
|
||||
return this.data.redactions.length > 1;
|
||||
return !isJustOne(this.data.redactions);
|
||||
}
|
||||
|
||||
get redactedTextsAreaHeight() {
|
||||
@ -173,11 +176,29 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
|
||||
return this.options.length * 75 + 230;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.close(this.form.getRawValue());
|
||||
extraOptionChanged(option: DetailsRadioOption<RemoveRedactionOption | RectangleRedactOption>): void {
|
||||
if (option.value === RectangleRedactOptions.MULTIPLE_PAGES) {
|
||||
setTimeout(() => {
|
||||
this.form.get('option')?.updateValueAndValidity();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#getOption(option: RemoveRedactionOption): DetailsRadioOption<RemoveRedactionOption> {
|
||||
save(): void {
|
||||
const optionValue = this.form.controls.option?.value?.value;
|
||||
const optionInputValue = this.form.controls.option?.value?.additionalInput?.value;
|
||||
const pageNumbers = parseSelectedPageNumbers(optionInputValue, this.data.file, this.data.redactions[0]);
|
||||
const position = parseRectanglePosition(this.data.redactions[0]);
|
||||
|
||||
this.close({
|
||||
...this.form.getRawValue(),
|
||||
bulkLocal: optionValue === ResizeOptions.IN_DOCUMENT || optionValue === RectangleRedactOptions.MULTIPLE_PAGES,
|
||||
pageNumbers,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
#getOption(option: RemoveRedactionOption): DetailsRadioOption<RectangleRedactOption | RemoveRedactionOption> {
|
||||
return this.options.find(o => o.value === option);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,12 +77,12 @@ export class ResizeRedactionDialogComponent extends IqserDialogComponent<
|
||||
|
||||
save() {
|
||||
const formValue = this.form.getRawValue();
|
||||
const updateDictionary = formValue.option.value === ResizeOptions.IN_DOSSIER;
|
||||
const updateDictionary = formValue.option?.value === ResizeOptions.IN_DOSSIER;
|
||||
|
||||
super.close({
|
||||
comment: formValue.comment,
|
||||
updateDictionary,
|
||||
addToAllDossiers: !!formValue.option?.extraOption?.checked,
|
||||
addToAllDossiers: !!formValue.option?.additionalCheck?.checked,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
&.documine-container {
|
||||
width: 60%;
|
||||
width: calc(100% - var(--structured-component-management-width));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router';
|
||||
import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { InitialsAvatarComponent } from '@common-ui/users';
|
||||
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
|
||||
import {
|
||||
CircleButtonComponent,
|
||||
CircleButtonTypes,
|
||||
ConfirmOption,
|
||||
ConfirmOptions,
|
||||
@ -14,7 +12,6 @@ import {
|
||||
ErrorService,
|
||||
getConfig,
|
||||
IConfirmationDialogData,
|
||||
IqserAllowDirective,
|
||||
IqserDialog,
|
||||
LoadingService,
|
||||
Toaster,
|
||||
@ -24,7 +21,7 @@ import { AutoUnsubscribe, Bind, bool, List, log, OnAttach, OnDetach } from '@iqs
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AnalyseStatuses, AnalysisEvent, Dictionary, File, ViewModes, WsTopics } from '@red/domain';
|
||||
import { AnalyseStatuses, AnalysisEvent, File, ViewModes, WsTopics } from '@red/domain';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
|
||||
import { DossiersService } from '@services/dossiers/dossiers.service';
|
||||
@ -33,7 +30,6 @@ import { FilesService } from '@services/files/files.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { ReanalysisService } from '@services/reanalysis.service';
|
||||
import { WebSocketService } from '@services/web-socket.service';
|
||||
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
|
||||
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
|
||||
import { Roles } from '@users/roles';
|
||||
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
|
||||
@ -49,14 +45,12 @@ import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service';
|
||||
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
|
||||
import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service';
|
||||
import { ROTATION_ACTION_BUTTONS, ViewerEvents } from '../pdf-viewer/utils/constants';
|
||||
import { FileActionsComponent } from '../shared-dossiers/components/file-actions/file-actions.component';
|
||||
import { FileHeaderComponent } from './components/file-header/file-header.component';
|
||||
import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component';
|
||||
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
|
||||
import { UserManagementComponent } from './components/user-management/user-management.component';
|
||||
import { ViewSwitchComponent } from './components/view-switch/view-switch.component';
|
||||
import { AddHintDialogComponent } from './dialogs/add-hint-dialog/add-hint-dialog.component';
|
||||
import { AddAnnotationDialogComponent } from './dialogs/docu-mine/add-annotation-dialog/add-annotation-dialog.component';
|
||||
import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
|
||||
import { RedactTextDialogComponent } from './dialogs/redact-text-dialog/redact-text-dialog.component';
|
||||
import { filePreviewScreenProviders } from './file-preview-providers';
|
||||
import { AnnotationProcessingService } from './services/annotation-processing.service';
|
||||
@ -80,16 +74,8 @@ import { RedactTextData } from './utils/dialog-types';
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
ViewSwitchComponent,
|
||||
ProcessingIndicatorComponent,
|
||||
UserManagementComponent,
|
||||
TranslateModule,
|
||||
InitialsAvatarComponent,
|
||||
CircleButtonComponent,
|
||||
IqserAllowDirective,
|
||||
FileActionsComponent,
|
||||
DisableStopPropagationDirective,
|
||||
RouterLink,
|
||||
FilePreviewRightContainerComponent,
|
||||
TypeFilterComponent,
|
||||
FileHeaderComponent,
|
||||
@ -157,6 +143,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
// this._fileDataService.loadAnnotations(file).then();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const file = this.state.file();
|
||||
if (file.analysisRequired && !file.excludedFromAutomaticAnalysis) {
|
||||
this._reanalysisService.reanalyzeFilesForDossier([file], file.dossierId, { force: true }).then();
|
||||
}
|
||||
});
|
||||
|
||||
this.#wsConnection$ = this._webSocketService.listen<AnalysisEvent>(WsTopics.ANALYSIS).pipe(
|
||||
log('[WS] Analysis events'),
|
||||
filter(event => event.analyseStatus === AnalyseStatuses.FINISHED),
|
||||
@ -193,7 +186,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
|
||||
effect(() => {
|
||||
this._viewModeService.viewMode();
|
||||
this.updateViewMode().then();
|
||||
this.#updateViewMode().then();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
@ -221,7 +214,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
const earmarks$ = isEarmarksViewMode$.pipe(
|
||||
tap(() => this._loadingService.start()),
|
||||
switchMap(() => this._fileDataService.loadEarmarks()),
|
||||
tap(() => this.updateViewMode().then(() => this._loadingService.stop())),
|
||||
tap(() => this.#updateViewMode().then(() => this._loadingService.stop())),
|
||||
);
|
||||
|
||||
const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
|
||||
@ -247,7 +240,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
|
||||
return isChangingFromEarmarksViewMode$.pipe(
|
||||
map(() => this._fileDataService.earmarks().get(this.pdf.currentPage()) ?? []),
|
||||
map(earmarks => this.deleteAnnotations(earmarks, [])),
|
||||
map(earmarks => this.#deleteAnnotations(earmarks, [])),
|
||||
);
|
||||
}
|
||||
|
||||
@ -256,15 +249,188 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
|
||||
}
|
||||
|
||||
async updateViewMode(): Promise<void> {
|
||||
this._logger.info(`[PDF] Update ${this._viewModeService.viewMode()} view mode`);
|
||||
ngOnDetach() {
|
||||
this._viewerHeaderService.resetCompareButtons();
|
||||
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
|
||||
super.ngOnDetach();
|
||||
this.pdf.instance.UI.hotkeys.off('esc');
|
||||
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
|
||||
this._changeRef.markForCheck();
|
||||
this.#wsConnectionSub.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.pdf.instance.UI.hotkeys.off('esc');
|
||||
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
|
||||
super.ngOnDestroy();
|
||||
this.#wsConnectionSub.unsubscribe();
|
||||
}
|
||||
|
||||
@Bind()
|
||||
handleEscInsideViewer($event: KeyboardEvent) {
|
||||
$event.preventDefault();
|
||||
if (!!this._annotationManager.selected[0]) {
|
||||
const doesHaveWrapper = this._fileDataService.find(this._annotationManager.selected[0]?.Id);
|
||||
if (!doesHaveWrapper) {
|
||||
this._annotationManager.delete(this._annotationManager.selected[0]?.Id);
|
||||
} else {
|
||||
this._annotationManager.deselect(this._annotationManager.selected[0]?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._annotationManager.selected.length) {
|
||||
this._annotationManager.deselectAll();
|
||||
}
|
||||
|
||||
if (this._multiSelectService.active()) {
|
||||
this._multiSelectService.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
@Bind()
|
||||
handleViewerClick(event: MouseEvent) {
|
||||
this._ngZone.run(() => {
|
||||
if (event.isTrusted) {
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
const editingAnnotation =
|
||||
(clickedElement as HTMLImageElement).src?.includes('edit.svg') || clickedElement.getAttribute('aria-label') === 'Edit';
|
||||
if (this._multiSelectService.active() && !editingAnnotation) {
|
||||
if (
|
||||
clickedElement.querySelector('#selectionrect') ||
|
||||
clickedElement.id === `pageWidgetContainer${this.pdf.currentPage()}`
|
||||
) {
|
||||
if (!this._annotationManager.selected.length) {
|
||||
this._multiSelectService.deactivate();
|
||||
}
|
||||
} else {
|
||||
this._multiSelectService.deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
|
||||
if (!this.state.file().canBeOpened) {
|
||||
return this.#navigateToDossier();
|
||||
}
|
||||
|
||||
this._viewModeService.switchToStandard();
|
||||
|
||||
await this.ngOnInit();
|
||||
this._viewerHeaderService.updateElements();
|
||||
const page = previousRoute.queryParams.page ?? '1';
|
||||
await this.#updateQueryParamsPage(Number(page));
|
||||
await this.viewerReady(page);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.#wsConnectionSub = this.#wsConnection$.subscribe();
|
||||
this.#updateViewerPosition();
|
||||
const file = this.state.file();
|
||||
|
||||
if (!file) {
|
||||
return this.#handleDeletedFile();
|
||||
}
|
||||
|
||||
this._loadingService.start();
|
||||
this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId).then();
|
||||
this.#subscribeToFileUpdates();
|
||||
|
||||
this.pdfProxyService.configureElements();
|
||||
this.#restoreOldFilters();
|
||||
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
|
||||
this._viewerHeaderService.resetLayers();
|
||||
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
|
||||
this.pdf.instance.UI.iframeWindow.document.addEventListener('click', this.handleViewerClick);
|
||||
}
|
||||
|
||||
async openRectangleAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
|
||||
const file = this.state.file();
|
||||
|
||||
const data = { manualRedactionEntryWrapper, file, dossierId: this.dossierId };
|
||||
const result = await this._iqserDialog.openDefault(RectangleAnnotationDialog, { data }).result();
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnnotations = this._annotationManager.selected;
|
||||
if (selectedAnnotations.length > 0) {
|
||||
this._annotationManager.delete([selectedAnnotations[0].Id]);
|
||||
}
|
||||
|
||||
const add$ = this._manualRedactionService.addAnnotation([result.annotation], this.dossierId, this.fileId);
|
||||
|
||||
const addAndReload$ = add$.pipe(switchMap(() => this._filesService.reload(this.dossierId, file)));
|
||||
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
|
||||
}
|
||||
|
||||
async viewerReady(pageNumber?: string) {
|
||||
if (pageNumber) {
|
||||
const file = this.state.file();
|
||||
let page = parseInt(pageNumber, 10);
|
||||
|
||||
if (page < 1 || Number.isNaN(page)) {
|
||||
page = 1;
|
||||
await this.#updateQueryParamsPage(page);
|
||||
} else if (page > file.numberOfPages) {
|
||||
page = file.numberOfPages;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.pdf.navigateTo(page);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
this._loadingService.stop();
|
||||
this._changeRef.markForCheck();
|
||||
}
|
||||
|
||||
loadAnnotations$() {
|
||||
const annotations$ = this._fileDataService.annotations$.pipe(
|
||||
startWith([] as AnnotationWrapper[]),
|
||||
pairwise(),
|
||||
tap(annotations => this.#deleteAnnotations(...annotations)),
|
||||
tap(() => this.#updateFiltersAfterAnnotationsChanged()),
|
||||
tap(() => this.#updateViewMode()),
|
||||
);
|
||||
|
||||
const currentPageIfNotHighlightsView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
|
||||
filter(([, viewMode]) => viewMode !== ViewModes.TEXT_HIGHLIGHTS),
|
||||
map(([page]) => page),
|
||||
);
|
||||
|
||||
const currentPageAnnotations$ = combineLatest([currentPageIfNotHighlightsView$, annotations$]).pipe(
|
||||
map(([page, [oldAnnotations, newAnnotations]]) =>
|
||||
this.#loadAllAnnotationsEnabled
|
||||
? ([oldAnnotations, newAnnotations] as const)
|
||||
: ([oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const),
|
||||
),
|
||||
);
|
||||
|
||||
return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe(
|
||||
filter(([, loaded]) => loaded),
|
||||
map(([annotations]) => annotations),
|
||||
switchMap(async ([oldAnnotations, newAnnotations]) => {
|
||||
await this.#drawChangedAnnotations(oldAnnotations, newAnnotations);
|
||||
return newAnnotations;
|
||||
}),
|
||||
tap(newAnnotations => this.#highlightSelectedAnnotations(newAnnotations)),
|
||||
);
|
||||
}
|
||||
|
||||
async #updateViewMode(): Promise<void> {
|
||||
const viewMode = untracked(this._viewModeService.viewMode);
|
||||
this._logger.info(`[PDF] Update ${viewMode} view mode`);
|
||||
|
||||
const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager')));
|
||||
const redactions = annotations.filter(a => bool(a.getCustomData('redaction')));
|
||||
|
||||
switch (this._viewModeService.viewMode()) {
|
||||
switch (viewMode) {
|
||||
case ViewModes.STANDARD: {
|
||||
const wrappers = this._fileDataService.annotations();
|
||||
// TODO: const wrappers = untracked(this._fileDataService.annotations);
|
||||
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
|
||||
const standardEntries = annotations
|
||||
.filter(a => !bool(a.getCustomData('changeLogRemoved')) && !this._annotationManager.isHidden(a.Id))
|
||||
@ -273,7 +439,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
a =>
|
||||
bool(a.getCustomData('changeLogRemoved')) ||
|
||||
this._annotationManager.isHidden(a.Id) ||
|
||||
(this._skippedService.hideSkipped() && bool(a.getCustomData('skipped'))),
|
||||
(untracked(this._skippedService.hideSkipped) && bool(a.getCustomData('skipped'))),
|
||||
);
|
||||
this._readableRedactionsService.setAnnotationsColor(standardEntries, 'annotationColor');
|
||||
this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true);
|
||||
@ -311,154 +477,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._logger.info('[PDF] Update done');
|
||||
}
|
||||
|
||||
ngOnDetach() {
|
||||
this._viewerHeaderService.resetCompareButtons();
|
||||
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
|
||||
super.ngOnDetach();
|
||||
this.pdf.instance.UI.hotkeys.off('esc');
|
||||
this._changeRef.markForCheck();
|
||||
this.#wsConnectionSub.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.pdf.instance.UI.hotkeys.off('esc');
|
||||
super.ngOnDestroy();
|
||||
this.#wsConnectionSub.unsubscribe();
|
||||
}
|
||||
|
||||
@Bind()
|
||||
handleEscInsideViewer($event: KeyboardEvent) {
|
||||
$event.preventDefault();
|
||||
if (!!this._annotationManager.selected[0]) {
|
||||
const doesHaveWrapper = this._fileDataService.find(this._annotationManager.selected[0]?.Id);
|
||||
if (!doesHaveWrapper) {
|
||||
this._annotationManager.delete(this._annotationManager.selected[0]?.Id);
|
||||
} else {
|
||||
this._annotationManager.deselect(this._annotationManager.selected[0]?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._annotationManager.selected.length) {
|
||||
this._annotationManager.deselectAll();
|
||||
}
|
||||
|
||||
if (this._multiSelectService.active()) {
|
||||
this._multiSelectService.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
|
||||
if (!this.state.file().canBeOpened) {
|
||||
return this.#navigateToDossier();
|
||||
}
|
||||
|
||||
this._viewModeService.switchToStandard();
|
||||
|
||||
await this.ngOnInit();
|
||||
this._viewerHeaderService.updateElements();
|
||||
const page = previousRoute.queryParams.page ?? '1';
|
||||
await this.#updateQueryParamsPage(Number(page));
|
||||
await this.viewerReady(page);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.#wsConnectionSub = this.#wsConnection$.subscribe();
|
||||
this.#updateViewerPosition();
|
||||
const file = this.state.file();
|
||||
|
||||
if (!file) {
|
||||
return this.#handleDeletedFile();
|
||||
}
|
||||
|
||||
this._loadingService.start();
|
||||
this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId).then();
|
||||
this.#subscribeToFileUpdates();
|
||||
|
||||
if (file?.analysisRequired && !file.excludedFromAutomaticAnalysis) {
|
||||
await this._reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true });
|
||||
}
|
||||
|
||||
this.pdfProxyService.configureElements();
|
||||
this.#restoreOldFilters();
|
||||
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
|
||||
this._viewerHeaderService.resetLayers();
|
||||
}
|
||||
|
||||
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
|
||||
const file = this.state.file();
|
||||
|
||||
this._dialogService.openDialog(
|
||||
'manualAnnotation',
|
||||
{ manualRedactionEntryWrapper, dossierId: this.dossierId, file },
|
||||
(result: { annotations: ManualRedactionEntryWrapper[]; dictionary?: Dictionary }) => {
|
||||
const selectedAnnotations = this._annotationManager.selected;
|
||||
if (selectedAnnotations.length > 0) {
|
||||
this._annotationManager.delete([selectedAnnotations[0].Id]);
|
||||
}
|
||||
|
||||
const add$ = this._manualRedactionService.addAnnotation(
|
||||
result.annotations.map(w => w.manualRedactionEntry).filter(e => e.positions[0].page <= file.numberOfPages),
|
||||
this.dossierId,
|
||||
this.fileId,
|
||||
{ dictionaryLabel: result.dictionary?.label },
|
||||
);
|
||||
|
||||
const addAndReload$ = add$.pipe(switchMap(() => this._filesService.reload(this.dossierId, file)));
|
||||
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async viewerReady(pageNumber?: string) {
|
||||
if (pageNumber) {
|
||||
const file = this.state.file();
|
||||
let page = parseInt(pageNumber, 10);
|
||||
|
||||
if (page < 1 || Number.isNaN(page)) {
|
||||
page = 1;
|
||||
await this.#updateQueryParamsPage(page);
|
||||
} else if (page > file.numberOfPages) {
|
||||
page = file.numberOfPages;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.pdf.navigateTo(page);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
this._loadingService.stop();
|
||||
this._changeRef.markForCheck();
|
||||
}
|
||||
|
||||
loadAnnotations$() {
|
||||
const annotations$ = this._fileDataService.annotations$.pipe(
|
||||
startWith([] as AnnotationWrapper[]),
|
||||
pairwise(),
|
||||
tap(annotations => this.deleteAnnotations(...annotations)),
|
||||
);
|
||||
|
||||
const currentPageIfNotHighlightsView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
|
||||
filter(([, viewMode]) => viewMode !== ViewModes.TEXT_HIGHLIGHTS),
|
||||
map(([page]) => page),
|
||||
);
|
||||
|
||||
const currentPageAnnotations$ = combineLatest([currentPageIfNotHighlightsView$, annotations$]).pipe(
|
||||
map(([page, [oldAnnotations, newAnnotations]]) =>
|
||||
this.#loadAllAnnotationsEnabled
|
||||
? ([oldAnnotations, newAnnotations] as const)
|
||||
: ([oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const),
|
||||
),
|
||||
);
|
||||
|
||||
return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe(
|
||||
filter(([, loaded]) => loaded),
|
||||
map(([annotations]) => annotations),
|
||||
tap(([oldA, newA]) => this.drawChangedAnnotations(oldA, newA)?.then(() => this.updateViewMode())),
|
||||
tap(([, newAnnotations]) => this.#highlightSelectedAnnotations(newAnnotations)),
|
||||
);
|
||||
}
|
||||
|
||||
deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
|
||||
#deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
|
||||
const annotationsToDelete = oldAnnotations.filter(oldAnnotation => {
|
||||
const newAnnotation = newAnnotations.find(byId(oldAnnotation.id));
|
||||
return newAnnotation ? hasChanges(oldAnnotation, newAnnotation) : true;
|
||||
@ -468,11 +487,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._annotationManager.delete(annotationsToDelete);
|
||||
}
|
||||
|
||||
drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
|
||||
async #drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]): Promise<void> {
|
||||
const annotationsToDraw = this.#getAnnotationsToDraw(oldAnnotations, newAnnotations);
|
||||
this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw);
|
||||
this._annotationManager.delete(annotationsToDraw);
|
||||
return this.#cleanupAndRedrawAnnotations(annotationsToDraw);
|
||||
await this.#cleanupAndRedrawAnnotations(annotationsToDraw);
|
||||
}
|
||||
|
||||
async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
|
||||
@ -489,6 +508,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, {
|
||||
hint,
|
||||
dictionaryLabel: result.dictionary?.label,
|
||||
bulkLocal: result.bulkLocal,
|
||||
});
|
||||
|
||||
const addAndReload$ = add$.pipe(
|
||||
@ -642,7 +662,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = this.pdfProxyService.manualAnnotationRequested$.subscribe($event => {
|
||||
this.openManualAnnotationDialog($event);
|
||||
this.openRectangleAnnotationDialog($event).then();
|
||||
});
|
||||
|
||||
this.addActiveScreenSubscription = this.pdfProxyService.redactTextRequested$.subscribe($event => {
|
||||
@ -662,7 +682,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
filter(event => event.type === ViewerEvents.LOAD_ALL_ANNOTATIONS),
|
||||
switchMap(() => {
|
||||
// TODO: this switchMap is ugly, to be refactored
|
||||
const annotations = this._fileDataService.annotations();
|
||||
const annotations = untracked(this._fileDataService.annotations);
|
||||
const showWarning = !this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning);
|
||||
const annotationsExceedThreshold = annotations.length >= this.configService.values.ANNOTATIONS_THRESHOLD;
|
||||
|
||||
@ -675,7 +695,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
filter(([confirmed]) => confirmed),
|
||||
map(([, annotations]) => {
|
||||
this.#loadAllAnnotationsEnabled = true;
|
||||
this.drawChangedAnnotations([], annotations).then(() => {
|
||||
this.#drawChangedAnnotations([], annotations).then(() => {
|
||||
this._toaster.success(_('load-all-annotations-success'));
|
||||
this._viewerHeaderService.disableLoadAllAnnotations();
|
||||
});
|
||||
@ -683,7 +703,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = this._readableRedactionsService.active$.pipe(map(() => this.updateViewMode())).subscribe();
|
||||
this.addActiveScreenSubscription = this._readableRedactionsService.active$
|
||||
.pipe(switchMap(() => this.#updateViewMode()))
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$, this._documentViewer.loaded$])
|
||||
.pipe(
|
||||
@ -722,11 +744,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._errorService.set(error);
|
||||
}
|
||||
|
||||
#cleanupAndRedrawAnnotations(newAnnotations: List<AnnotationWrapper>) {
|
||||
async #cleanupAndRedrawAnnotations(newAnnotations: List<AnnotationWrapper>): Promise<void> {
|
||||
if (!newAnnotations.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped(), this.state.dossierTemplateId);
|
||||
}
|
||||
|
||||
#updateFiltersAfterAnnotationsChanged(): void {
|
||||
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
|
||||
this.#rebuildFilters();
|
||||
|
||||
@ -735,8 +761,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this.#handleDeltaAnnotationFilters(currentFilters);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped(), this.state.dossierTemplateId);
|
||||
}
|
||||
|
||||
#handleDeltaAnnotationFilters(currentFilters: NestedFilter[]) {
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service';
|
||||
import { getConfig, Toaster } from '@iqser/common-ui';
|
||||
import { getConfig } from '@iqser/common-ui';
|
||||
import { List, log } from '@iqser/common-ui/lib/utils';
|
||||
import { AnnotationPermissions } from '@models/file/annotation.permissions';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { Core } from '@pdftron/webviewer';
|
||||
import {
|
||||
DictionaryEntryTypes,
|
||||
DownloadFileTypes,
|
||||
EarmarkOperation,
|
||||
type IBulkLocalRemoveRequest,
|
||||
IBulkRecategorizationRequest,
|
||||
ILegalBasisChangeRequest,
|
||||
IRecategorizationRequest,
|
||||
IRectangle,
|
||||
type IRemoveRedactionRequest,
|
||||
IResizeRequest,
|
||||
} from '@red/domain';
|
||||
import { CommentsApiService } from '@services/comments-api.service';
|
||||
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
@ -32,6 +35,10 @@ import { ResizeRedactionDialogComponent } from '../dialogs/resize-redaction-dial
|
||||
import {
|
||||
EditRedactionData,
|
||||
EditRedactResult,
|
||||
ForceAnnotationOptions,
|
||||
RectangleRedactOptions,
|
||||
RedactOrHintOptions,
|
||||
RedactRecommendationData,
|
||||
RemoveRedactionData,
|
||||
RemoveRedactionOptions,
|
||||
RemoveRedactionPermissions,
|
||||
@ -44,11 +51,12 @@ import { FilePreviewDialogService } from './file-preview-dialog.service';
|
||||
import { FilePreviewStateService } from './file-preview-state.service';
|
||||
import { ManualRedactionService } from './manual-redaction.service';
|
||||
import { SkippedService } from './skipped.service';
|
||||
import { NON_READABLE_CONTENT } from '../dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
|
||||
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class AnnotationActionsService {
|
||||
readonly #isDocumine = getConfig().IS_DOCUMINE;
|
||||
readonly #commentsApiService = inject(CommentsApiService);
|
||||
|
||||
constructor(
|
||||
private readonly _manualRedactionService: ManualRedactionService,
|
||||
@ -63,7 +71,6 @@ export class AnnotationActionsService {
|
||||
private readonly _skippedService: SkippedService,
|
||||
private readonly _dossierTemplatesService: DossierTemplatesService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _toaster: Toaster,
|
||||
) {}
|
||||
|
||||
removeHighlights(highlights: AnnotationWrapper[]): void {
|
||||
@ -76,29 +83,51 @@ export class AnnotationActionsService {
|
||||
this._dialogService.openDialog('highlightAction', data);
|
||||
}
|
||||
|
||||
forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) {
|
||||
async forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) {
|
||||
const { dossierId, fileId } = this._state;
|
||||
const data = { dossier: this._state.dossier(), annotations, hint };
|
||||
this._dialogService.openDialog('forceAnnotation', data, (request: ILegalBasisChangeRequest) => {
|
||||
this.#processObsAndEmit(
|
||||
this._manualRedactionService.bulkForce(
|
||||
annotations.map(a => ({ ...request, annotationId: a.id })),
|
||||
dossierId,
|
||||
fileId,
|
||||
annotations[0].isIgnoredHint,
|
||||
),
|
||||
).then();
|
||||
});
|
||||
const image = annotations.every(a => a.isImage);
|
||||
const data = { dossier: this._state.dossier(), annotations, hint, image };
|
||||
|
||||
const dialogRef = this._iqserDialog.openDefault(ForceAnnotationDialogComponent, { data });
|
||||
const result = await dialogRef.result();
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
let obs$: Observable<unknown>;
|
||||
if (result.option === ForceAnnotationOptions.ONLY_HERE || hint || image) {
|
||||
obs$ = this._manualRedactionService.bulkForce(
|
||||
annotations.map(a => ({ ...result, annotationId: a.id })),
|
||||
dossierId,
|
||||
fileId,
|
||||
annotations[0].isIgnoredHint,
|
||||
);
|
||||
} else {
|
||||
const addAnnotationRequest = annotations.map(a => ({
|
||||
comment: result.comment,
|
||||
legalBasis: result.legalBasis,
|
||||
reason: result.reason,
|
||||
positions: a.positions,
|
||||
type: a.type,
|
||||
value: a.value,
|
||||
}));
|
||||
obs$ = this._manualRedactionService.addAnnotation(addAnnotationRequest, dossierId, fileId, {
|
||||
hint,
|
||||
bulkLocal: true,
|
||||
});
|
||||
}
|
||||
this.#processObsAndEmit(obs$).then();
|
||||
}
|
||||
|
||||
async editRedaction(annotations: AnnotationWrapper[]) {
|
||||
const { dossierId, dossierTemplateId, fileId, file } = this._state;
|
||||
const includeUnprocessed = annotations.every(annotation => this.#includeUnprocessed(annotation, true));
|
||||
const dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
|
||||
const { dossierId, file } = this._state;
|
||||
const allFileAnnotations = this._fileDataService.annotations();
|
||||
const data = {
|
||||
annotations,
|
||||
allFileAnnotations,
|
||||
dossierId,
|
||||
applyToAllDossiers: dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
|
||||
file: file(),
|
||||
};
|
||||
|
||||
const result = await this.#getEditRedactionDialog(data).result();
|
||||
@ -106,40 +135,49 @@ export class AnnotationActionsService {
|
||||
return;
|
||||
}
|
||||
|
||||
const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => {
|
||||
const body = { annotationId: annotation.id, type: result.type ?? annotation.type };
|
||||
if (!this.#isDocumine) {
|
||||
return {
|
||||
...body,
|
||||
legalBasis: result.legalBasis,
|
||||
section: result.section ?? annotation.section,
|
||||
value: result.value ?? annotation.value,
|
||||
let recategorizeBody: List<IRecategorizationRequest> | IBulkRecategorizationRequest;
|
||||
|
||||
if (result.option === RedactOrHintOptions.ONLY_HERE || result.option === RectangleRedactOptions.ONLY_THIS_PAGE) {
|
||||
recategorizeBody = annotations.map(annotation => {
|
||||
const body: IRecategorizationRequest = {
|
||||
annotationId: annotation.id,
|
||||
type: result.type ?? annotation.type,
|
||||
comment: result.comment,
|
||||
};
|
||||
}
|
||||
return body;
|
||||
});
|
||||
if (!this.#isDocumine) {
|
||||
return {
|
||||
...body,
|
||||
legalBasis: result.legalBasis,
|
||||
section: result.section ?? annotation.section,
|
||||
value: result.value ?? annotation.value,
|
||||
};
|
||||
}
|
||||
return body;
|
||||
});
|
||||
} else {
|
||||
recategorizeBody = {
|
||||
value: annotations[0].value,
|
||||
type: result.type,
|
||||
legalBasis: result.legalBasis,
|
||||
section: result.section,
|
||||
rectangle: annotations[0].AREA,
|
||||
pageNumbers: result.pageNumbers,
|
||||
position: result.position,
|
||||
comment: result.comment,
|
||||
};
|
||||
}
|
||||
|
||||
await this.#processObsAndEmit(
|
||||
this._manualRedactionService
|
||||
.recategorizeRedactions(
|
||||
recategorizeBody,
|
||||
dossierId,
|
||||
fileId,
|
||||
file().id,
|
||||
this.#getChangedFields(annotations, result),
|
||||
includeUnprocessed,
|
||||
result.option === RedactOrHintOptions.IN_DOCUMENT || !!result.pageNumbers.length,
|
||||
)
|
||||
.pipe(log()),
|
||||
);
|
||||
|
||||
if (result.comment) {
|
||||
try {
|
||||
for (const a of annotations) {
|
||||
await this.#commentsApiService.add(result.comment, a.id, dossierId, fileId);
|
||||
}
|
||||
} catch (error) {
|
||||
this._toaster.rawError(error.error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeRedaction(redactions: AnnotationWrapper[], permissions: AnnotationPermissions) {
|
||||
@ -150,9 +188,13 @@ export class AnnotationActionsService {
|
||||
};
|
||||
const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
|
||||
const isApprover = this._permissionsService.isApprover(this._state.dossier());
|
||||
const { file } = this._state;
|
||||
const allFileRedactions = this._fileDataService.annotations();
|
||||
|
||||
const data = {
|
||||
redactions,
|
||||
allFileRedactions,
|
||||
file: file(),
|
||||
dossier: this._state.dossier(),
|
||||
falsePositiveContext: redactions.map(r => this.#getFalsePositiveText(r)),
|
||||
permissions: removePermissions,
|
||||
@ -167,8 +209,8 @@ export class AnnotationActionsService {
|
||||
}
|
||||
|
||||
if (
|
||||
result.option.value === RemoveRedactionOptions.FALSE_POSITIVE ||
|
||||
result.option.value === RemoveRedactionOptions.DO_NOT_RECOMMEND
|
||||
result.option?.value === RemoveRedactionOptions.FALSE_POSITIVE ||
|
||||
result.option?.value === RemoveRedactionOptions.DO_NOT_RECOMMEND
|
||||
) {
|
||||
this.#setAsFalsePositive(redactions, result);
|
||||
} else {
|
||||
@ -190,7 +232,7 @@ export class AnnotationActionsService {
|
||||
|
||||
async convertRecommendationToAnnotation(recommendations: AnnotationWrapper[]) {
|
||||
const { dossierId, fileId } = this._state;
|
||||
const data = this.#getRedactRecommendationDialogData(recommendations);
|
||||
const data = this.#getRedactRecommendationDialogData(recommendations) as RedactRecommendationData;
|
||||
const dialog = this._iqserDialog.openDefault(RedactRecommendationDialogComponent, { data });
|
||||
const result = await dialog.result();
|
||||
if (!result) {
|
||||
@ -201,7 +243,13 @@ export class AnnotationActionsService {
|
||||
this.cancelResize(recommendations[0]).then();
|
||||
}
|
||||
|
||||
const request$ = this._manualRedactionService.addRecommendation(recommendations, result.redaction, dossierId, fileId);
|
||||
const request$ = this._manualRedactionService.addRecommendation(
|
||||
recommendations,
|
||||
result.redaction,
|
||||
dossierId,
|
||||
fileId,
|
||||
result.bulkLocal,
|
||||
);
|
||||
return this.#processObsAndEmit(request$);
|
||||
}
|
||||
|
||||
@ -224,7 +272,6 @@ export class AnnotationActionsService {
|
||||
|
||||
async acceptResize(annotation: AnnotationWrapper, permissions: AnnotationPermissions): Promise<void> {
|
||||
const textAndPositions = await this.#extractTextAndPositions(annotation.id);
|
||||
const includeUnprocessed = this.#includeUnprocessed(annotation);
|
||||
if (annotation.isRecommendation) {
|
||||
const recommendation = {
|
||||
...annotation,
|
||||
@ -275,16 +322,16 @@ export class AnnotationActionsService {
|
||||
await this.cancelResize(annotation);
|
||||
|
||||
const { fileId, dossierId } = this._state;
|
||||
const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId, includeUnprocessed);
|
||||
const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId);
|
||||
return this.#processObsAndEmit(request);
|
||||
}
|
||||
|
||||
async cancelResize(annotationWrapper: AnnotationWrapper) {
|
||||
this._annotationManager.resizingAnnotationId = undefined;
|
||||
this._annotationManager.annotationHasBeenResized = false;
|
||||
this._annotationManager.deselect();
|
||||
this._annotationManager.delete(annotationWrapper);
|
||||
await this._annotationDrawService.draw([annotationWrapper], this._skippedService.hideSkipped(), this._state.dossierTemplateId);
|
||||
this._annotationManager.deselect();
|
||||
}
|
||||
|
||||
#generateRectangle(annotationWrapper: AnnotationWrapper) {
|
||||
@ -418,7 +465,7 @@ export class AnnotationActionsService {
|
||||
type: redaction.type,
|
||||
positions: redaction.positions,
|
||||
addToDictionary: true,
|
||||
addToAllDossiers: !!dialogResult.option.extraOption?.checked || !!dialogResult.applyToAllDossiers,
|
||||
addToAllDossiers: !!dialogResult.option.additionalCheck?.checked || !!dialogResult.applyToAllDossiers,
|
||||
reason: 'False Positive',
|
||||
dictionaryEntryType: redaction.isRecommendation
|
||||
? DictionaryEntryTypes.FALSE_RECOMMENDATION
|
||||
@ -431,26 +478,52 @@ export class AnnotationActionsService {
|
||||
}
|
||||
|
||||
#removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) {
|
||||
const removeFromDictionary = dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER;
|
||||
const includeUnprocessed = redactions.every(redaction => this.#includeUnprocessed(redaction, true));
|
||||
const body = redactions.map(redaction => ({
|
||||
annotationId: redaction.id,
|
||||
comment: dialogResult.comment,
|
||||
removeFromDictionary,
|
||||
removeFromAllDossiers: !!dialogResult.option.extraOption?.checked || !!dialogResult.applyToAllDossiers,
|
||||
}));
|
||||
const removeFromDictionary = dialogResult.option?.value === RemoveRedactionOptions.IN_DOSSIER;
|
||||
const body = this.#getRemoveRedactionBody(redactions, dialogResult);
|
||||
// todo: might not be correct, probably shouldn't get to this point if they are not all the same
|
||||
const isHint = redactions.every(r => r.isHint);
|
||||
const { dossierId, fileId } = this._state;
|
||||
const req$ = this._manualRedactionService.removeRedaction(
|
||||
body,
|
||||
dossierId,
|
||||
fileId,
|
||||
removeFromDictionary,
|
||||
isHint,
|
||||
includeUnprocessed,
|
||||
);
|
||||
this.#processObsAndEmit(req$).then();
|
||||
const maximumNumberEntries = 100;
|
||||
if (removeFromDictionary && (body as List<IRemoveRedactionRequest>).length > maximumNumberEntries) {
|
||||
const requests = body as List<IRemoveRedactionRequest>;
|
||||
const splitNumber = Math.floor(requests.length / maximumNumberEntries);
|
||||
const remainder = requests.length % maximumNumberEntries;
|
||||
const splitRequests = [];
|
||||
for (let i = 0; i < splitNumber; i++) {
|
||||
splitRequests.push(requests.slice(i * maximumNumberEntries, (i + 1) * maximumNumberEntries));
|
||||
}
|
||||
if (remainder) {
|
||||
splitRequests.push(requests.slice(splitNumber * maximumNumberEntries, splitNumber * maximumNumberEntries + remainder));
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
for (const split of splitRequests) {
|
||||
promises.push(
|
||||
firstValueFrom(
|
||||
this._manualRedactionService.removeRedaction(
|
||||
split,
|
||||
dossierId,
|
||||
fileId,
|
||||
removeFromDictionary,
|
||||
isHint,
|
||||
dialogResult.bulkLocal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Promise.all(promises).finally(() => this._fileDataService.annotationsChanged());
|
||||
return;
|
||||
}
|
||||
this.#processObsAndEmit(
|
||||
this._manualRedactionService.removeRedaction(
|
||||
body,
|
||||
dossierId,
|
||||
fileId,
|
||||
removeFromDictionary,
|
||||
isHint,
|
||||
dialogResult.bulkLocal || !!dialogResult.pageNumbers.length,
|
||||
),
|
||||
).then();
|
||||
}
|
||||
|
||||
#getRemoveRedactionDialog(data: RemoveRedactionData) {
|
||||
@ -506,17 +579,27 @@ export class AnnotationActionsService {
|
||||
return { changes: changedFields.join(', ') };
|
||||
}
|
||||
|
||||
//TODO this is temporary, based on RED-8950. Should be removed when a better solution will be found
|
||||
#includeUnprocessed(annotation: AnnotationWrapper, isRemoveOrRecategorize = false) {
|
||||
const processed = annotation.entry.manualChanges.at(-1)?.processed;
|
||||
if (!processed) {
|
||||
const autoAnalysisDisabled = this._state.file().excludedFromAutomaticAnalysis;
|
||||
const addedLocallyWhileDisabled = annotation.manual;
|
||||
if (autoAnalysisDisabled) {
|
||||
return addedLocallyWhileDisabled;
|
||||
}
|
||||
return isRemoveOrRecategorize && addedLocallyWhileDisabled;
|
||||
#getRemoveRedactionBody(
|
||||
redactions: AnnotationWrapper[],
|
||||
dialogResult: RemoveRedactionResult,
|
||||
): List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest {
|
||||
if (dialogResult.bulkLocal || !!dialogResult.pageNumbers.length) {
|
||||
const redaction = redactions[0];
|
||||
return {
|
||||
value: redaction.value,
|
||||
rectangle: redaction.value === NON_READABLE_CONTENT,
|
||||
pageNumbers: dialogResult.pageNumbers,
|
||||
position: dialogResult.position,
|
||||
comment: dialogResult.comment,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
|
||||
return redactions.map(redaction => ({
|
||||
annotationId: redaction.id,
|
||||
value: redaction.value,
|
||||
comment: dialogResult.comment,
|
||||
removeFromDictionary: dialogResult.option?.value === RemoveRedactionOptions.IN_DOSSIER,
|
||||
removeFromAllDossiers: !!dialogResult.option?.additionalCheck?.checked || !!dialogResult.applyToAllDossiers,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { computed, Injectable, Signal, signal } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { FileDataService } from './file-data.service';
|
||||
|
||||
@ -7,7 +6,6 @@ import { FileDataService } from './file-data.service';
|
||||
export class AnnotationReferencesService {
|
||||
readonly references: Signal<AnnotationWrapper[]>;
|
||||
readonly annotation: Signal<AnnotationWrapper | undefined>;
|
||||
private readonly _annotation$ = new BehaviorSubject<AnnotationWrapper | undefined>(undefined);
|
||||
readonly #annotation = signal<AnnotationWrapper | undefined>(undefined);
|
||||
|
||||
constructor(private readonly _fileDataService: FileDataService) {
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { effect, Injectable, untracked } from '@angular/core';
|
||||
import { EntitiesService, ListingService, SearchService } from '@iqser/common-ui';
|
||||
import { filter, tap } from 'rxjs/operators';
|
||||
import { MultiSelectService } from './multi-select.service';
|
||||
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
|
||||
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterService } from '@iqser/common-ui/lib/filtering';
|
||||
import { SortingService } from '@iqser/common-ui/lib/sorting';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable()
|
||||
export class AnnotationsListingService extends ListingService<AnnotationWrapper> implements OnDestroy {
|
||||
readonly #subscriptions: Subscription;
|
||||
export class AnnotationsListingService extends ListingService<AnnotationWrapper> {
|
||||
readonly selectedLength = toSignal(this.selectedLength$);
|
||||
|
||||
constructor(
|
||||
protected readonly _filterService: FilterService,
|
||||
@ -24,23 +23,22 @@ export class AnnotationsListingService extends ListingService<AnnotationWrapper>
|
||||
) {
|
||||
super(_filterService, _searchService, _entitiesService, _sortingService);
|
||||
|
||||
this.#subscriptions = this.selectedLength$
|
||||
.pipe(
|
||||
filter(length => length > 1),
|
||||
tap(() => this._multiSelectService.activate()),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.#subscriptions.unsubscribe();
|
||||
effect(
|
||||
() => {
|
||||
if (this.selectedLength() > 1) {
|
||||
this._multiSelectService.activate();
|
||||
}
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
}
|
||||
|
||||
selectAnnotations(annotations: AnnotationWrapper[] | AnnotationWrapper) {
|
||||
annotations = Array.isArray(annotations) ? annotations : [annotations];
|
||||
const pageNumber = annotations[annotations.length - 1].pageNumber;
|
||||
|
||||
const annotationsToSelect = this._multiSelectService.active() ? [...this.selected, ...annotations] : annotations;
|
||||
const multiSelectActive = untracked(this._multiSelectService.active);
|
||||
const annotationsToSelect = multiSelectActive ? [...this.selected, ...annotations] : annotations;
|
||||
this.#selectAnnotations(annotationsToSelect, pageNumber);
|
||||
}
|
||||
|
||||
@ -49,16 +47,18 @@ export class AnnotationsListingService extends ListingService<AnnotationWrapper>
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._multiSelectService.inactive()) {
|
||||
const multiSelectInactive = untracked(this._multiSelectService.inactive);
|
||||
if (multiSelectInactive) {
|
||||
this._annotationManager.deselect();
|
||||
}
|
||||
|
||||
if (pageNumber === this._pdf.currentPage()) {
|
||||
const currentPage = untracked(this._pdf.currentPage);
|
||||
if (pageNumber === currentPage) {
|
||||
return this._annotationManager.jumpAndSelect(annotations);
|
||||
}
|
||||
|
||||
this._pdf.navigateTo(pageNumber);
|
||||
// wait for page to be loaded and to draw annotations
|
||||
setTimeout(() => this._annotationManager.jumpAndSelect(annotations), 300);
|
||||
setTimeout(() => this._annotationManager.jumpAndSelect(annotations), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,9 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser/common-ui';
|
||||
import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
|
||||
import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/document-info-dialog.component';
|
||||
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
|
||||
import { HighlightActionDialogComponent } from '../dialogs/highlight-action-dialog/highlight-action-dialog.component';
|
||||
import { ManualAnnotationDialogComponent } from '../dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
|
||||
|
||||
type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'forceAnnotation' | 'manualAnnotation' | 'highlightAction';
|
||||
type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'highlightAction';
|
||||
|
||||
@Injectable()
|
||||
export class FilePreviewDialogService extends DialogService<DialogType> {
|
||||
@ -23,13 +21,6 @@ export class FilePreviewDialogService extends DialogService<DialogType> {
|
||||
changeLegalBasis: {
|
||||
component: ChangeLegalBasisDialogComponent,
|
||||
},
|
||||
forceAnnotation: {
|
||||
component: ForceAnnotationDialogComponent,
|
||||
},
|
||||
manualAnnotation: {
|
||||
component: ManualAnnotationDialogComponent,
|
||||
dialogConfig: { autoFocus: true },
|
||||
},
|
||||
highlightAction: {
|
||||
component: HighlightActionDialogComponent,
|
||||
},
|
||||
|
||||
@ -8,6 +8,8 @@ import { type ManualRedactionEntryType } from '@models/file/manual-redaction-ent
|
||||
import type {
|
||||
DictionaryActions,
|
||||
IAddRedactionRequest,
|
||||
IBulkLocalRemoveRequest,
|
||||
IBulkRecategorizationRequest,
|
||||
ILegalBasisChangeRequest,
|
||||
IManualAddResponse,
|
||||
IRecategorizationRequest,
|
||||
@ -39,6 +41,7 @@ function getMessage(action: ManualRedactionActions, isDictionary = false, error
|
||||
export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
protected readonly _defaultModelPath = 'manualRedaction';
|
||||
readonly #bulkRedaction = `${this._defaultModelPath}/bulk/redaction`;
|
||||
readonly #bulkLocal = `${this._defaultModelPath}/bulk-local`;
|
||||
|
||||
constructor(
|
||||
private readonly _toaster: Toaster,
|
||||
@ -48,7 +51,13 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
super();
|
||||
}
|
||||
|
||||
addRecommendation(annotations: AnnotationWrapper[], redaction: IAddRedactionRequest, dossierId: string, fileId: string) {
|
||||
addRecommendation(
|
||||
annotations: AnnotationWrapper[],
|
||||
redaction: IAddRedactionRequest,
|
||||
dossierId: string,
|
||||
fileId: string,
|
||||
bulkLocal: boolean = false,
|
||||
) {
|
||||
const recommendations: List<IAddRedactionRequest> = annotations.map(annotation => ({
|
||||
addToDictionary: redaction.addToDictionary,
|
||||
addToAllDossiers: redaction.addToAllDossiers,
|
||||
@ -59,19 +68,17 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
type: redaction.type ?? annotation.type,
|
||||
comment: redaction.comment,
|
||||
}));
|
||||
return this.addAnnotation(recommendations, dossierId, fileId);
|
||||
return this.addAnnotation(recommendations, dossierId, fileId, { bulkLocal });
|
||||
}
|
||||
|
||||
recategorizeRedactions(
|
||||
body: List<IRecategorizationRequest>,
|
||||
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
|
||||
dossierId: string,
|
||||
fileId: string,
|
||||
successMessageParameters?: {
|
||||
[key: string]: string;
|
||||
},
|
||||
includeUnprocessed = false,
|
||||
successMessageParameters?: { [p: string]: string },
|
||||
bulkLocal = false,
|
||||
) {
|
||||
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(
|
||||
return this.#recategorize(body, dossierId, fileId, bulkLocal).pipe(
|
||||
this.#showToast('recategorize-annotation', false, successMessageParameters),
|
||||
);
|
||||
}
|
||||
@ -80,14 +87,14 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
requests: List<IAddRedactionRequest>,
|
||||
dossierId: string,
|
||||
fileId: string,
|
||||
options?: { hint?: boolean; dictionaryLabel?: string },
|
||||
options?: { hint?: boolean; dictionaryLabel?: string; bulkLocal?: boolean },
|
||||
) {
|
||||
const toast = requests[0].addToDictionary
|
||||
? this.#showAddToDictionaryToast(requests, options?.dictionaryLabel)
|
||||
: this.#showToast(options?.hint ? 'force-hint' : 'add');
|
||||
const canAddRedaction = this._iqserPermissionsService.has(Roles.redactions.write);
|
||||
if (canAddRedaction) {
|
||||
return this.add(requests, dossierId, fileId).pipe(toast);
|
||||
return this.add(requests, dossierId, fileId, options?.bulkLocal).pipe(toast);
|
||||
}
|
||||
|
||||
return of(undefined);
|
||||
@ -102,14 +109,14 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
}
|
||||
|
||||
removeRedaction(
|
||||
body: List<IRemoveRedactionRequest>,
|
||||
body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest,
|
||||
dossierId: string,
|
||||
fileId: string,
|
||||
removeFromDictionary = false,
|
||||
isHint = false,
|
||||
includeUnprocessed = false,
|
||||
bulkLocal = false,
|
||||
) {
|
||||
return this.remove(body, dossierId, fileId, includeUnprocessed).pipe(
|
||||
return this.#remove(body, dossierId, fileId, bulkLocal).pipe(
|
||||
this.#showToast(!isHint ? 'remove' : 'remove-hint', removeFromDictionary),
|
||||
);
|
||||
}
|
||||
@ -127,14 +134,11 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
add(body: List<IAddRedactionRequest>, dossierId: string, fileId: string) {
|
||||
return this._post(body, `${this.#bulkRedaction}/add/${dossierId}/${fileId}`).pipe(this.#log('Add', body));
|
||||
}
|
||||
add(body: List<IAddRedactionRequest>, dossierId: string, fileId: string, bulkLocal = false) {
|
||||
bulkLocal = bulkLocal || !!body[0].pageNumbers?.length;
|
||||
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
|
||||
|
||||
recategorize(body: List<IRecategorizationRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
|
||||
return this._post(body, `${this.#bulkRedaction}/recategorize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
|
||||
this.#log('Recategorize', body),
|
||||
);
|
||||
return this._post(bulkLocal ? body[0] : body, `${bulkPath}/add/${dossierId}/${fileId}`).pipe(this.#log('Add', body));
|
||||
}
|
||||
|
||||
undo(annotationIds: List, dossierId: string, fileId: string) {
|
||||
@ -142,20 +146,27 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
return super.delete(annotationIds, url).pipe(this.#log('Undo', annotationIds));
|
||||
}
|
||||
|
||||
remove(body: List<IRemoveRedactionRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
|
||||
return this._post(body, `${this.#bulkRedaction}/remove/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
|
||||
this.#log('Remove', body),
|
||||
);
|
||||
}
|
||||
|
||||
forceRedaction(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string) {
|
||||
return this._post(body, `${this.#bulkRedaction}/force/${dossierId}/${fileId}`).pipe(this.#log('Force redaction', body));
|
||||
}
|
||||
|
||||
resize(body: List<IResizeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
|
||||
return this._post(body, `${this.#bulkRedaction}/resize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
|
||||
this.#log('Resize', body),
|
||||
);
|
||||
resize(body: List<IResizeRequest>, dossierId: string, fileId: string) {
|
||||
return this._post(body, `${this.#bulkRedaction}/resize/${dossierId}/${fileId}`).pipe(this.#log('Resize', body));
|
||||
}
|
||||
|
||||
#recategorize(
|
||||
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
|
||||
dossierId: string,
|
||||
fileId: string,
|
||||
bulkLocal = false,
|
||||
) {
|
||||
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
|
||||
return this._post(body, `${bulkPath}/recategorize/${dossierId}/${fileId}`).pipe(this.#log('Recategorize', body));
|
||||
}
|
||||
|
||||
#remove(body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest, dossierId: string, fileId: string, bulkLocal = false) {
|
||||
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
|
||||
return this._post(body, `${bulkPath}/remove/${dossierId}/${fileId}`).pipe(this.#log('Remove', body));
|
||||
}
|
||||
|
||||
#log(action: string, body: unknown) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { computed, Injectable, Signal, signal } from '@angular/core';
|
||||
import { computed, Injectable, Signal, signal, untracked } from '@angular/core';
|
||||
import { ViewModeService } from './view-mode.service';
|
||||
import { FilePreviewStateService } from './file-preview-state.service';
|
||||
import { ViewMode, ViewModes } from '@red/domain';
|
||||
@ -13,13 +13,17 @@ export class MultiSelectService {
|
||||
|
||||
readonly #active = signal(false);
|
||||
|
||||
constructor(protected readonly _viewModeService: ViewModeService, protected readonly _state: FilePreviewStateService) {
|
||||
constructor(
|
||||
protected readonly _viewModeService: ViewModeService,
|
||||
protected readonly _state: FilePreviewStateService,
|
||||
) {
|
||||
this.active = this.#active.asReadonly();
|
||||
this.inactive = computed(() => !this.#active());
|
||||
}
|
||||
|
||||
activate() {
|
||||
if (this.enabled()) {
|
||||
const enabled = untracked(this.enabled);
|
||||
if (enabled) {
|
||||
this.#active.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export class PdfAnnotationActionsService {
|
||||
get(annotations: AnnotationWrapper[], annotationChangesAllowed: boolean): IHeaderElement[] {
|
||||
const availableActions: IHeaderElement[] = [];
|
||||
const permissions = this.#getAnnotationsPermissions(annotations);
|
||||
const sameType = annotations.every(a => a.type === annotations[0].type);
|
||||
const sameType = annotations.every(a => a.superType === annotations[0].superType);
|
||||
|
||||
// you can only resize one annotation at a time
|
||||
if (permissions.canResizeAnnotation && annotationChangesAllowed) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { computed, effect, inject, Injectable, NgZone } from '@angular/core';
|
||||
import { computed, effect, inject, Injectable, NgZone, untracked } from '@angular/core';
|
||||
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
|
||||
import { getCurrentUser } from '@iqser/common-ui/lib/users';
|
||||
import { isJustOne, shareDistinctLast, UI_ROOT_PATH_FN } from '@iqser/common-ui/lib/utils';
|
||||
@ -49,7 +49,7 @@ export class PdfProxyService {
|
||||
readonly redactTextRequested$ = new Subject<ManualRedactionEntryWrapper>();
|
||||
readonly currentUser = getCurrentUser<User>();
|
||||
readonly pageChanged$ = this._pdf.pageChanged$.pipe(
|
||||
tap(() => this.#handleExcludedPageActions()),
|
||||
tap(pageNumber => this.#handleExcludedPageActions(pageNumber)),
|
||||
tap(() => {
|
||||
if (this._multiSelectService.inactive()) {
|
||||
this._annotationManager.deselect();
|
||||
@ -98,7 +98,7 @@ export class PdfProxyService {
|
||||
effect(
|
||||
() => {
|
||||
const canPerformActions = this.canPerformActions();
|
||||
this._pdf.isCompareMode();
|
||||
const isCompareMode = this._pdf.isCompareMode();
|
||||
|
||||
this.#configureTextPopup();
|
||||
|
||||
@ -109,7 +109,8 @@ export class PdfProxyService {
|
||||
this.#deactivateMultiSelect();
|
||||
}
|
||||
|
||||
this.#handleExcludedPageActions();
|
||||
const currentPage = untracked(this._pdf.currentPage);
|
||||
this.#handleExcludedPageActions(currentPage);
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
@ -227,8 +228,9 @@ export class PdfProxyService {
|
||||
this.redactTextRequested$.next({ manualRedactionEntry, type });
|
||||
}
|
||||
|
||||
#handleExcludedPageActions() {
|
||||
const isCurrentPageExcluded = this._state.file().isPageExcluded(this._pdf.currentPage());
|
||||
#handleExcludedPageActions(currentPage: number) {
|
||||
const isCurrentPageExcluded = this._state.file().isPageExcluded(currentPage);
|
||||
|
||||
if (!isCurrentPageExcluded) {
|
||||
return;
|
||||
}
|
||||
@ -402,11 +404,14 @@ export class PdfProxyService {
|
||||
|
||||
const annotationChangesAllowed = !this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis;
|
||||
const somePending = annotationWrappers.some(a => a.pending);
|
||||
const selectedFromWorkload = untracked(this._annotationManager.selectedFromWorkload);
|
||||
|
||||
actions =
|
||||
this._multiSelectService.inactive() && !this._documentViewer.selectedText.length && !somePending
|
||||
(this._multiSelectService.inactive() || !selectedFromWorkload) && !this._documentViewer.selectedText.length && !somePending
|
||||
? [...actions, ...this._pdfAnnotationActionsService.get(annotationWrappers, annotationChangesAllowed)]
|
||||
: [];
|
||||
this._pdf.instance.UI.annotationPopup.update(actions);
|
||||
this._annotationManager.resetSelectedFromWorkload();
|
||||
}
|
||||
|
||||
#getTitle(type: ManualRedactionEntryType) {
|
||||
|
||||
@ -19,16 +19,6 @@ export const ActionsHelpModeKeys = {
|
||||
'hint-image': 'hint',
|
||||
} as const;
|
||||
|
||||
export const DialogHelpModeKeys = {
|
||||
REDACTION_EDIT: 'redaction_edit',
|
||||
REDACTION_REMOVE: 'redaction_remove',
|
||||
SKIPPED_EDIT: 'skipped_edit',
|
||||
SKIPPED_REMOVE: 'skipped_remove',
|
||||
RECOMMENDATION_REMOVE: 'recommendation_remove',
|
||||
HINT_EDIT: 'hint_edit',
|
||||
HINT_REMOVE: 'hint_remove',
|
||||
} as const;
|
||||
|
||||
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown', 'H', 'h'] as const;
|
||||
|
||||
export const HeaderElements = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user