Compare commits

...

91 Commits

Author SHA1 Message Date
Nicoleta Panaghiu
5ffdfe1029 RED-9097: fixed workflow file attributes not updating. 2024-05-08 13:34:56 +03:00
Nicoleta Panaghiu
4ecd8f34dd RED-9083: prevent search panel from re-opening unexpectedly. 2024-04-30 12:47:58 +03:00
Nicoleta Panaghiu
478a661ca4 RED-9090: modal displays actual state & other improvements. 2024-04-29 16:29:14 +03:00
Nicoleta Panaghiu
e72ba642e2 RED-9089: fixed assign user button. 2024-04-29 14:29:09 +03:00
Nicoleta Panaghiu
d9820bd0fd RED-8904: run i18n extract. 2024-04-24 15:30:23 +03:00
Nicoleta Panaghiu
fd96d827c1 RED-8904: more improvements. 2024-04-24 15:28:34 +03:00
Marius Schummer
2b499dbf57 RED-8904 Correct label for hints in remove dialog 2024-04-24 13:36:27 +02:00
Nicoleta Panaghiu
a19692dae6 RED-8904: use table format for add dialog as well. 2024-04-24 14:06:34 +03:00
Marius Schummer
a5e41616cd RED-8904 Correct "in dossier" labels in Remove dialog 2024-04-24 11:53:09 +02:00
Nicoleta Panaghiu
23f5fbc615 RED-8904: changed from list to table display for some dialogs. 2024-04-24 11:10:29 +03:00
Nicoleta Panaghiu
b2a96433d3 Revert "RED-8945: filtering out types for edit dialog."
This reverts commit c90b362e8e9113a9a22787cfbd44c555c092bff5.
2024-04-23 15:27:02 +03:00
Nicoleta Panaghiu
c90b362e8e RED-8945: filtering out types for edit dialog. 2024-04-22 16:41:53 +03:00
Valentin Mihai
46a787ce71 RED-9020 - Use new POST endpoint to hard-delete files 2024-04-22 13:56:45 +03:00
Nicoleta Panaghiu
2f38c2b491 RED-8904: adjusted the table columns width for specific cases. 2024-04-22 12:40:27 +03:00
Valentin Mihai
392ac7c4b1 RED-9020 - Use new POST endpoint to hard-delete files 2024-04-19 16:37:50 +03:00
Nicoleta Panaghiu
907245149a RED-8945: set addToDictionaryAction flag on dossier level. 2024-04-19 15:53:55 +03:00
Valentin Mihai
91d65750c1 RED-8638 - Adapt UI to new manualChanges types 2024-04-16 16:44:59 +03:00
Valentin Mihai
d158d41a10 RED-8950 - Not possible to edit/remove manual local redaction when auto-analysis is disabled, wrong flag is used 2024-04-16 15:35:02 +03:00
Valentin Mihai
0095379584 RED-8711 - update common-ui 2024-04-15 18:49:15 +03:00
Nicoleta Panaghiu
26b0d36e7f RED-8915: fixed edge cases. 2024-04-12 15:01:58 +03:00
Dan Percic
a0bff7a4b5 update pdftron 2024-04-12 13:23:49 +03:00
Valentin Mihai
cc215aed12 RED-8638 - check if annotation has redaction changes 2024-04-12 12:34:13 +03:00
Valentin Mihai
1d2351dba1 RED-8638 - fixed build 2024-04-11 19:19:26 +03:00
Valentin Mihai
42fe183170 RED-8638 - removed “Redaction/Hint added” tooltip text 2024-04-11 19:12:45 +03:00
Nicoleta Panaghiu
ae27842ceb RED-8907: removed resize in dictionary option for forced annotations. 2024-04-11 15:34:28 +03:00
Valentin Mihai
4ef7ae71d4 RED-8638 - updated “Only annotations with manual changes” filter to include only the anotations with manual engine 2024-04-11 10:46:43 +03:00
Nicoleta Panaghiu
964227811c RED-8915: made some graphs stop at the current month. 2024-04-10 17:28:07 +03:00
Nicoleta Panaghiu
2738b94913 RED-8904: improved dialogs. 2024-04-10 16:37:00 +03:00
Nicoleta Panaghiu
ff129b64ca RED-8636: use the new recategorize endpoint & updated success message. 2024-04-10 16:12:54 +03:00
Valentin Mihai
313eeef0ae RED-8711 - check for positions property to be sure it exists and is not null 2024-04-10 12:49:34 +03:00
Valentin Mihai
9232d426a9 RED-8638 - Adapt UI to new manualChanges types 2024-04-09 19:30:09 +03:00
Nicoleta Panaghiu
8a9ab32188 RED-8907: changed dialog options permissions based on local changes. 2024-04-09 18:37:53 +03:00
Valentin Mihai
bda45f208f RED-8711 - filtered out entities with empty rectangle positions 2024-04-09 12:34:53 +03:00
Nicoleta Panaghiu
d9c607ffb3 RED-8812 & RED-8841: fixes after suggestions. 2024-04-02 16:26:09 +03:00
Nicoleta Panaghiu
400ad29bf2 RED-8765: fixed actions and edit fields for specific combinations. 2024-03-28 12:00:39 +02:00
Nicoleta Panaghiu
eaf908bc7c RED-8812: changed table styling. 2024-03-28 11:35:55 +02:00
Nicoleta Panaghiu
7fbe0f8398 RED-8870: fixed comment author displayed as unknown user. 2024-03-27 16:30:01 +02:00
Nicoleta Panaghiu
a2b8541e8e RED-8812: display selected annotations in table form. 2024-03-27 16:08:54 +02:00
Valentin Mihai
ed234ec5d6 RED-8638 - In the workload, annotations with manualChanges have the hand icon displayed 2024-03-26 18:32:12 +02:00
Valentin Mihai
9982b8c37f RED-8829 - revert changes 2024-03-26 16:51:12 +02:00
Nicoleta Panaghiu
b6d2c7878f RED-8817: removed the italic style from the extra option description. 2024-03-26 16:04:05 +02:00
Nicoleta Panaghiu
24cabac3fc RED-8850: import FALLBACK_COLOR. 2024-03-26 12:52:55 +02:00
Nicoleta Panaghiu
a957cb6226 RED-8850: show backend error message. 2024-03-26 12:41:00 +02:00
Valentin Mihai
9deaa08d5a RED-8829 - Disable option to resize a forced redaction with dictionary action 2024-03-25 17:05:16 +02:00
Valentin Mihai
9d6e0f35cf RED-8711 - Filter out entity log entries that have no positions and show a toast message in dev mode 2024-03-25 13:50:18 +02:00
Nicoleta Panaghiu
55f62e7197 RED-8812: bold only the selected value, not the entire context. 2024-03-25 11:35:21 +02:00
Nicoleta Panaghiu
f0ce6aadb1 RED-8841: show 10 items instead of 5 in bulk-delete dialog. 2024-03-25 11:14:08 +02:00
Nicoleta Panaghiu
b314d6fb17 RED-8817: update common-ui. 2024-03-25 10:50:09 +02:00
Nicoleta Panaghiu
b1615b7309 RED-8817: added fp extraOption description and increase dialog height. 2024-03-21 17:14:00 +02:00
Nicoleta Panaghiu
6da85dae52 RED-8812: added selected label and term for single redaction delete. 2024-03-21 16:26:00 +02:00
Marius Schummer
83afc3d578 RED-8734 Backport to 4.0.0 release branch 2024-03-20 18:28:15 +01:00
Nicoleta Panaghiu
ea7033b394 RED-8648: added missing method after cherry pick. 2024-03-20 16:21:11 +02:00
Nicoleta Panaghiu
be143566a6 RED-8648: added ellipsis for attribute value in workflow mode. 2024-03-20 16:17:32 +02:00
Nicoleta Panaghiu
7920d3d5fd RED-8795: changed default options for add and remove dialogs. 2024-03-20 16:08:49 +02:00
Valentin Mihai
1240a1ef99 RED-7980 - stop searching when another page has been reached 2024-03-20 15:07:11 +02:00
Valentin Mihai
456ff25508 RED-7980 - fixed "Pending annotation not displayed" 2024-03-20 14:30:55 +02:00
Nicoleta Panaghiu
be5622ce9e RED-8737: made primary attribute editable only by clicking the button. 2024-03-20 10:47:50 +02:00
Valentin Mihai
0ab411b8ae RED-7980 - build fix 2024-03-20 10:42:40 +02:00
Valentin Mihai
0b31b731f5 RED-7980 - Don’t allow any user actions at the original annotation while there is a related pending annotation 2024-03-20 10:30:43 +02:00
Valentin Mihai
f6eefde596 RED-7980 - displayed pending annotations with only a border in the analysis color 2024-03-18 15:39:52 +02:00
Nicoleta Panaghiu
bbedf1e2cb RED-8728: update common ui. 2024-03-15 15:09:09 +02:00
Nicoleta Panaghiu
aac3303101 RED-8728 & RED-8293: adjusted the styling and spacing. 2024-03-15 15:08:10 +02:00
Nicoleta Panaghiu
9c1245b7e9 RED-8763: fixed image attribute upload button. 2024-03-15 11:47:19 +02:00
Nicoleta Panaghiu
fe24b1ebdc RED-8730: fixed error when opening dictionaries in settings.. 2024-03-15 10:51:02 +02:00
Marius Schummer
f79519785c Corrected english translation (on master already corrected via localazy) 2024-03-14 17:30:01 +01:00
Nicoleta Panaghiu
1a2ddcfaaa RED-8735: add false positive option for dict-based skipped annotations. 2024-03-14 16:34:00 +02:00
Nicoleta Panaghiu
0f89b1954a RED-8753: fixed images disappearing upon aborting resize. 2024-03-14 15:04:42 +02:00
Marius Schummer
504fc8aa43 RED-8576: Change tooltip for enable/disable redaction 2024-03-13 18:58:16 +01:00
Nicoleta Panaghiu
5b4ae97503 RED-8649: made in-place editing for the primary attribute responsive. 2024-03-13 11:18:17 +02:00
Valentin Mihai
e0b9cfcb05 RED-8638 - Adapt UI to new manualChanges types 2024-03-12 20:42:30 +02:00
Valentin Mihai
791b584df9 RED-7980 - removed polyline annotation and displayed pending annotations as before, but with 50% opacity 2024-03-11 19:42:29 +02:00
Valentin Mihai
152ce8d9b2 RED-7980 - Display unprocessed annotations correctly as pending 2024-03-08 17:12:36 +02:00
Nicoleta Panaghiu
ad4c6c458d RED-8639: fixed some fields not updating for dossier_redaction. 2024-03-04 12:03:40 +02:00
Nicoleta Panaghiu
a008f8b5d1 RED-8639: fixed settings options for dossier dictionary only entities. 2024-02-26 17:49:49 +02:00
Nicoleta Panaghiu
a9719d8465 RED-8566: fixed error 400 when creating dossier by pressing enter. 2024-02-26 17:10:07 +02:00
Nicoleta Panaghiu
fada310046 RED-8578: fixed hard-delete error still appearing. 2024-02-26 15:49:07 +02:00
Nicoleta Panaghiu
12ae0f5fb2 RED-8637: fixed suffix for manual technical name. 2024-02-23 16:39:31 +02:00
Valentin Mihai
24ecfbd81f Merge branch 'VM/RED-7874-release' into release/4.630.x 2024-02-23 15:44:54 +02:00
Valentin Mihai
562ae83f73 RED-7874 - added back auth-error component 2024-02-23 15:43:13 +02:00
Valentin Mihai
3512e51341 RED-7874 - Integrate "user has no roles" message in sign-in page 2024-02-23 15:30:21 +02:00
Valentin Mihai
d1fe3b3181 RED-8463 - Date filter in audit log not filtering correctly 2024-02-21 17:30:24 +02:00
Nicoleta Panaghiu
aa8c2114bc RED-8578: removed error when hard deleting dossier. 2024-02-21 16:54:23 +02:00
Valentin Mihai
a85a7e4298 RED-8520 - Cannot compare False positives and False recommendations dictionaries in Dossier Template Entity configuration 2024-02-21 12:42:57 +02:00
Nicoleta Panaghiu
d450c5c99f RED-8565: fixed password reset redirect. 2024-02-21 10:10:21 +02:00
Valentin Mihai
856563999b RED-6960 - update common ui 2024-02-21 10:02:33 +02:00
Valentin Mihai
9b21e6872b RED-8363 - UI should reject unsupported file types directly based on license 2024-02-20 10:53:05 +02:00
Valentin Mihai
1d6d9a97d4 RED-8253- Create new help mode links 2024-02-19 13:37:06 +02:00
Nicoleta Panaghiu
7d72c78b11 RED-8563: fixed workload filter closing upon clicking on it. 2024-02-16 13:52:04 +02:00
Dan Percic
7345991f03 show dossier dictionaries even if dossier redaction is not available 2024-02-16 10:29:50 +02:00
Valentin Mihai
cff765f478 RED-7980 - Not all unprocessed annotations are displayed correctly 2024-02-15 16:31:41 +02:00
Nicoleta Panaghiu
26186e6bf7 RED-6880: fixed infinite loading. 2024-02-15 14:40:34 +02:00
119 changed files with 1661 additions and 1042 deletions

View File

@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouteReuseStrategy, RouterModule } from '@angular/router'; import { RouteReuseStrategy, RouterModule } from '@angular/router';
import { ifNotLoggedIn } from '@common-ui/tenants/guards/if-not-logged-in.guard'; import { ifNotLoggedIn } from '@common-ui/tenants/guards/if-not-logged-in.guard';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component'; import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component'; import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
import { DashboardGuard } from '@guards/dashboard-guard.service'; import { DashboardGuard } from '@guards/dashboard-guard.service';
@ -23,6 +22,7 @@ import { Roles } from '@users/roles';
import { mainGuard } from '@utils/main.guard'; import { mainGuard } from '@utils/main.guard';
import { webViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard'; import { webViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard';
import { ACTIVE_DOSSIERS_SERVICE } from './tokens'; import { ACTIVE_DOSSIERS_SERVICE } from './tokens';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
const dossierTemplateIdRoutes: IqserRoutes = [ const dossierTemplateIdRoutes: IqserRoutes = [
{ {

View File

@ -7,7 +7,6 @@ import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { GET_TENANT_FROM_PATH_FN, UI_ROOT } from '@common-ui/utils'; import { GET_TENANT_FROM_PATH_FN, UI_ROOT } from '@common-ui/utils';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component'; import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component'; import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component'; import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
@ -69,6 +68,7 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module'; import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens'; import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
export const appModuleFactory = (config: AppConfig) => { export const appModuleFactory = (config: AppConfig) => {
@NgModule({ @NgModule({

View File

@ -2,17 +2,17 @@
<p *ngIf="!adminName && !adminUrl" class="heading-xl" translate="auth-error.heading"></p> <p *ngIf="!adminName && !adminUrl" class="heading-xl" translate="auth-error.heading"></p>
<p <p
*ngIf="adminName && adminUrl" *ngIf="adminName && adminUrl"
[innerHTML]="'auth-error.heading-with-name-and-link' | translate : { adminName: adminName, adminUrl: adminUrl }" [innerHTML]="'auth-error.heading-with-name-and-link' | translate: { adminName: adminName, adminUrl: adminUrl }"
class="heading-xl" class="heading-xl"
></p> ></p>
<p <p
*ngIf="adminName && !adminUrl" *ngIf="adminName && !adminUrl"
[innerHTML]="'auth-error.heading-with-name' | translate : { adminName: adminName }" [innerHTML]="'auth-error.heading-with-name' | translate: { adminName: adminName }"
class="heading-xl" class="heading-xl"
></p> ></p>
<p <p
*ngIf="!adminName && adminUrl" *ngIf="!adminName && adminUrl"
[innerHTML]="'auth-error.heading-with-link' | translate : { adminName: adminName }" [innerHTML]="'auth-error.heading-with-link' | translate: { adminName: adminName }"
class="heading-xl" class="heading-xl"
></p> ></p>
<a (click)="userService.logout()" translate="auth-error.logout"></a> <a (click)="userService.logout()" translate="auth-error.logout"></a>

View File

@ -47,7 +47,7 @@ export class DossierFilesGuard implements CanActivate {
const promises = []; const promises = [];
if (!this._dictionaryMapService.has(dossierId) && !this.isDocumine) { if (!this._dictionaryMapService.has(dossierId) && !this.isDocumine) {
const dictionaryPromise = this._dictionaryService.loadDossierRedaction(dossierTemplateId, dossierId); const dictionaryPromise = firstValueFrom(this._dictionaryService.loadDictionaryDataForDossier(dossierTemplateId, dossierId));
promises.push(dictionaryPromise); promises.push(dictionaryPromise);
} }

View File

@ -13,7 +13,10 @@ export const canForceRedaction = (annotation: AnnotationWrapper, canAddRedaction
export const canAcceptRecommendation = (annotation: AnnotationWrapper) => annotation.isRecommendation && !annotation.pending; export const canAcceptRecommendation = (annotation: AnnotationWrapper) => annotation.isRecommendation && !annotation.pending;
export const canMarkAsFalsePositive = (annotation: AnnotationWrapper, annotationEntity: Dictionary) => export const canMarkAsFalsePositive = (annotation: AnnotationWrapper, annotationEntity: Dictionary) =>
annotation.canBeMarkedAsFalsePositive && !annotation.hasBeenForcedRedaction && annotationEntity?.hasDictionary; annotation.canBeMarkedAsFalsePositive &&
!annotation.hasBeenForcedRedaction &&
!annotation.hasBeenResizedLocally &&
annotationEntity?.hasDictionary;
export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) => export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) =>
canAddRedaction && canAddRedaction &&
@ -24,7 +27,8 @@ export const canRemoveFromDictionary = (annotation: AnnotationWrapper, autoAnaly
annotation.isModifyDictionary && annotation.isModifyDictionary &&
(annotation.isRedacted || annotation.isSkipped || annotation.isHint || (annotation.isIgnoredHint && !annotation.isRuleBased)) && (annotation.isRedacted || annotation.isSkipped || annotation.isHint || (annotation.isIgnoredHint && !annotation.isRuleBased)) &&
(autoAnalysisDisabled || !annotation.pending) && (autoAnalysisDisabled || !annotation.pending) &&
!annotation.hasBeenResized; !annotation.hasBeenResizedLocally &&
!(annotation.hasBeenForcedHint || annotation.hasBeenForcedRedaction);
export const canRemoveRedaction = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) => export const canRemoveRedaction = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
(!annotation.isIgnoredHint || !annotation.isRuleBased) && (!annotation.isIgnoredHint || !annotation.isRuleBased) &&
@ -54,8 +58,15 @@ export const canResizeAnnotation = (
(!annotation.isHint && hasDictionary && annotation.isRuleBased))) || (!annotation.isHint && hasDictionary && annotation.isRuleBased))) ||
annotation.isRecommendation; annotation.isRecommendation;
export const canResizeInDictionary = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
permissions.canResizeAnnotation &&
annotation.isModifyDictionary &&
!annotation.hasBeenResizedLocally &&
!(annotation.hasBeenForcedHint || annotation.hasBeenForcedRedaction);
export const canEditAnnotation = (annotation: AnnotationWrapper) => (annotation.isRedacted || annotation.isSkipped) && !annotation.isImage; export const canEditAnnotation = (annotation: AnnotationWrapper) => (annotation.isRedacted || annotation.isSkipped) && !annotation.isImage;
export const canEditHint = (annotation: AnnotationWrapper) => (annotation.isHint && !annotation.isRuleBased) || annotation.isIgnoredHint; export const canEditHint = (annotation: AnnotationWrapper) =>
((annotation.isHint && !annotation.isRuleBased) || annotation.isIgnoredHint) && !annotation.isImage;
export const canEditImage = (annotation: AnnotationWrapper) => annotation.isImage; export const canEditImage = (annotation: AnnotationWrapper) => annotation.isImage;

View File

@ -16,6 +16,7 @@ import {
canRemoveOnlyHere, canRemoveOnlyHere,
canRemoveRedaction, canRemoveRedaction,
canResizeAnnotation, canResizeAnnotation,
canResizeInDictionary,
canUndo, canUndo,
} from './annotation-permissions.utils'; } from './annotation-permissions.utils';
import { AnnotationWrapper } from './annotation.wrapper'; import { AnnotationWrapper } from './annotation.wrapper';
@ -30,6 +31,7 @@ export class AnnotationPermissions {
canForceRedaction = true; canForceRedaction = true;
canChangeLegalBasis = true; canChangeLegalBasis = true;
canResizeAnnotation = true; canResizeAnnotation = true;
canResizeInDictionary = true;
canRecategorizeAnnotation = true; canRecategorizeAnnotation = true;
canForceHint = true; canForceHint = true;
canEditAnnotations = true; canEditAnnotations = true;
@ -69,6 +71,7 @@ export class AnnotationPermissions {
autoAnalysisDisabled, autoAnalysisDisabled,
annotationEntity?.hasDictionary, annotationEntity?.hasDictionary,
); );
permissions.canResizeInDictionary = canResizeInDictionary(annotation, permissions);
permissions.canEditAnnotations = canEditAnnotation(annotation); permissions.canEditAnnotations = canEditAnnotation(annotation);
permissions.canEditHints = canEditHint(annotation); permissions.canEditHints = canEditHint(annotation);
permissions.canEditImages = canEditImage(annotation); permissions.canEditImages = canEditImage(annotation);
@ -80,6 +83,7 @@ export class AnnotationPermissions {
static reduce(permissions: AnnotationPermissions[]): AnnotationPermissions { static reduce(permissions: AnnotationPermissions[]): AnnotationPermissions {
const result = new AnnotationPermissions(); const result = new AnnotationPermissions();
result.canResizeAnnotation = permissions.length === 1 && permissions[0].canResizeAnnotation; result.canResizeAnnotation = permissions.length === 1 && permissions[0].canResizeAnnotation;
result.canResizeInDictionary = permissions.length === 1 && permissions[0].canResizeInDictionary;
result.canChangeLegalBasis = permissions.reduce((acc, next) => acc && next.canChangeLegalBasis, true); result.canChangeLegalBasis = permissions.reduce((acc, next) => acc && next.canChangeLegalBasis, true);
result.canRecategorizeAnnotation = permissions.reduce((acc, next) => acc && next.canRecategorizeAnnotation, true); result.canRecategorizeAnnotation = permissions.reduce((acc, next) => acc && next.canRecategorizeAnnotation, true);
result.canRemoveFromDictionary = permissions.reduce((acc, next) => acc && next.canRemoveFromDictionary, true); result.canRemoveFromDictionary = permissions.reduce((acc, next) => acc && next.canRemoveFromDictionary, true);

View File

@ -25,7 +25,6 @@ import {
SuperTypes, SuperTypes,
} from '@red/domain'; } from '@red/domain';
import { annotationTypesTranslations } from '@translations/annotation-types-translations'; import { annotationTypesTranslations } from '@translations/annotation-types-translations';
import { log } from '@common-ui/utils';
export class AnnotationWrapper implements IListable { export class AnnotationWrapper implements IListable {
id: string; id: string;
@ -58,6 +57,7 @@ export class AnnotationWrapper implements IListable {
isChangeLogEntry = false; isChangeLogEntry = false;
engines: LogEntryEngine[] = []; engines: LogEntryEngine[] = [];
hasBeenResized: boolean; hasBeenResized: boolean;
hasBeenResizedLocally: boolean;
hasBeenRecategorized: boolean; hasBeenRecategorized: boolean;
hasLegalBasisChanged: boolean; hasLegalBasisChanged: boolean;
hasBeenForcedHint: boolean; hasBeenForcedHint: boolean;
@ -65,12 +65,18 @@ export class AnnotationWrapper implements IListable {
hasBeenRemovedByManualOverride: boolean; hasBeenRemovedByManualOverride: boolean;
isRemoved = false; isRemoved = false;
isRemovedLocally = false; isRemovedLocally = false;
hiddenInWorkload = false;
lastManualChange: ManualRedactionType; lastManualChange: ManualRedactionType;
entry: IEntityLogEntry;
get isRuleBased() { get isRuleBased() {
return this.engines.includes(LogEntryEngines.RULE); return this.engines.includes(LogEntryEngines.RULE);
} }
get isDictBased() {
return this.engines.includes(LogEntryEngines.DICTIONARY);
}
get searchKey(): string { get searchKey(): string {
return this.id; return this.id;
} }
@ -89,7 +95,7 @@ export class AnnotationWrapper implements IListable {
get canBeMarkedAsFalsePositive() { get canBeMarkedAsFalsePositive() {
return ( return (
(this.isRecommendation || this.superType === SuperTypes.Redaction) && (this.isRecommendation || this.superType === SuperTypes.Redaction || (this.isSkipped && this.isDictBased)) &&
!this.isImage && !this.isImage &&
!this.imported && !this.imported &&
!this.pending && !this.pending &&
@ -218,7 +224,8 @@ export class AnnotationWrapper implements IListable {
) { ) {
const annotationWrapper = new AnnotationWrapper(); const annotationWrapper = new AnnotationWrapper();
annotationWrapper.id = logEntry.id; annotationWrapper.pending = logEntry.state === EntryStates.PENDING;
annotationWrapper.id = logEntry.id + (annotationWrapper.pending ? '-pending' : '');
annotationWrapper.isChangeLogEntry = logEntry.state === EntryStates.REMOVED || !!changeLogType; annotationWrapper.isChangeLogEntry = logEntry.state === EntryStates.REMOVED || !!changeLogType;
annotationWrapper.type = logEntry.type; annotationWrapper.type = logEntry.type;
annotationWrapper.value = logEntry.value; annotationWrapper.value = logEntry.value;
@ -247,20 +254,20 @@ export class AnnotationWrapper implements IListable {
annotationWrapper.section = logEntry.section; annotationWrapper.section = logEntry.section;
annotationWrapper.reference = logEntry.reference || []; annotationWrapper.reference = logEntry.reference || [];
annotationWrapper.hasBeenResized = !!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.RESIZE); annotationWrapper.hasBeenResized = !!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.RESIZE);
annotationWrapper.hasBeenResizedLocally =
annotationWrapper.hasBeenResized && annotationWrapper.engines.includes(LogEntryEngines.MANUAL);
annotationWrapper.hasBeenRecategorized = !!logEntry.manualChanges?.find( annotationWrapper.hasBeenRecategorized = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE, c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE,
); );
annotationWrapper.hasLegalBasisChanged = !!logEntry.manualChanges?.find( annotationWrapper.hasLegalBasisChanged = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE, c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE,
); );
annotationWrapper.hasBeenForcedHint = !!logEntry.manualChanges?.find( annotationWrapper.hasBeenForcedHint = !!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.FORCE);
c => c.manualRedactionType === ManualRedactionTypes.FORCE_HINT,
);
annotationWrapper.hasBeenForcedRedaction = !!logEntry.manualChanges?.find( annotationWrapper.hasBeenForcedRedaction = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.FORCE_REDACT, c => c.manualRedactionType === ManualRedactionTypes.FORCE,
); );
annotationWrapper.hasBeenRemovedByManualOverride = !!logEntry.manualChanges?.find( annotationWrapper.hasBeenRemovedByManualOverride = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.REMOVE_LOCALLY, c => c.manualRedactionType === ManualRedactionTypes.REMOVE,
); );
const content = this.#createContent(annotationWrapper, logEntry, isDocumine); const content = this.#createContent(annotationWrapper, logEntry, isDocumine);
@ -269,19 +276,25 @@ export class AnnotationWrapper implements IListable {
const lastRelevantManualChange = logEntry.manualChanges?.at(-1); const lastRelevantManualChange = logEntry.manualChanges?.at(-1);
annotationWrapper.lastManualChange = lastRelevantManualChange?.manualRedactionType; annotationWrapper.lastManualChange = lastRelevantManualChange?.manualRedactionType;
annotationWrapper.pending = lastRelevantManualChange && !lastRelevantManualChange.processed;
annotationWrapper.superType = SuperTypeMapper[logEntry.entryType][logEntry.state](logEntry); annotationWrapper.superType = SuperTypeMapper[logEntry.entryType][logEntry.state](logEntry);
annotationWrapper.superTypeLabel = annotationTypesTranslations[annotationWrapper.superType]; annotationWrapper.superTypeLabel = annotationTypesTranslations[annotationWrapper.superType];
annotationWrapper.isRemoved = logEntry.state === EntryStates.REMOVED; annotationWrapper.isRemoved = logEntry.state === EntryStates.REMOVED;
annotationWrapper.isRemovedLocally = lastRelevantManualChange?.manualRedactionType === ManualRedactionTypes.REMOVE_LOCALLY; annotationWrapper.isRemovedLocally =
lastRelevantManualChange?.manualRedactionType === ManualRedactionTypes.REMOVE &&
logEntry.engines.includes(LogEntryEngines.MANUAL);
annotationWrapper.typeLabel = dictionary?.virtual ? undefined : dictionary?.label; annotationWrapper.typeLabel = dictionary?.virtual ? undefined : dictionary?.label;
const colorKey = annotationEntityColorConfig[annotationWrapper.superType]; if (annotationWrapper.pending) {
const defaultColor = annotationDefaultColorConfig[annotationWrapper.superType]; annotationWrapper.color = defaultColors[annotationDefaultColorConfig.analysis] as string;
annotationWrapper.color = dictionary ? (dictionary[colorKey] as string) : (defaultColors[defaultColor] as string); } else {
annotationWrapper['entry'] = logEntry; const colorKey = annotationEntityColorConfig[annotationWrapper.superType];
const defaultColor = annotationDefaultColorConfig[annotationWrapper.superType];
annotationWrapper.color = dictionary ? (dictionary[colorKey] as string) : (defaultColors[defaultColor] as string);
}
annotationWrapper.entry = logEntry;
return annotationWrapper; return annotationWrapper;
} }

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="iqser-input-group"> <div class="iqser-input-group">
<a [href]="changePasswordUrl" target="_blank"> {{ 'user-profile-screen.actions.change-password' | translate }}</a> <a (click)="resetPassword()" target="_blank"> {{ 'user-profile-screen.actions.change-password' | translate }}</a>
</div> </div>
<div *ngIf="devMode" class="iqser-input-group"> <div *ngIf="devMode" class="iqser-input-group">

View File

@ -1,11 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DomSanitizer } from '@angular/platform-browser';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { BaseFormComponent, getConfig, IqserPermissionsService, LanguageService, LoadingService, Toaster } from '@iqser/common-ui'; import { BaseFormComponent, IqserPermissionsService, LanguageService, LoadingService, Toaster } from '@iqser/common-ui';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AppConfig, IProfile } from '@red/domain'; import { IProfile } from '@red/domain';
import { languagesTranslations } from '@translations/languages-translations'; import { languagesTranslations } from '@translations/languages-translations';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service'; import { UserPreferenceService } from '@users/user-preference.service';
@ -22,10 +20,8 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
#profileModel: IProfile; #profileModel: IProfile;
readonly translations = languagesTranslations; readonly translations = languagesTranslations;
readonly devMode = this._userPreferenceService.isIqserDevMode; readonly devMode = this._userPreferenceService.isIqserDevMode;
readonly changePasswordUrl: string;
constructor( constructor(
domSanitizer: DomSanitizer,
private readonly _userService: UserService, private readonly _userService: UserService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _dialogService: UserProfileDialogService, private readonly _dialogService: UserProfileDialogService,
@ -39,9 +35,6 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
) { ) {
super(); super();
this._loadingService.start(); this._loadingService.start();
const tenant = inject(TenantsService).activeTenantId;
const realmUrl = `${getConfig<AppConfig>().OAUTH_URL}/realms/${tenant}`;
this.changePasswordUrl = `${realmUrl}/account/password`;
} }
get languageChanged(): boolean { get languageChanged(): boolean {
@ -115,6 +108,10 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
} }
} }
async resetPassword() {
await this._userService.createResetPasswordAction();
}
private _getForm(): UntypedFormGroup { private _getForm(): UntypedFormGroup {
return this._formBuilder.group({ return this._formBuilder.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],

View File

@ -34,11 +34,7 @@ export class AddEntityDialogComponent extends BaseDialogComponent {
} }
async save(): Promise<void> { async save(): Promise<void> {
try { await this._addEditEntityComponent.save();
await this._addEditEntityComponent.save(); this._dialogRef.close(true);
this._dialogRef.close(true);
} catch (e) {
console.error(e);
}
} }
} }

View File

@ -104,7 +104,12 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnI
const from = this.form.get('from').value; const from = this.form.get('from').value;
let to = this.form.get('to').value; let to = this.form.get('to').value;
if (to) { if (to) {
to = to.clone().add(1, 'd'); 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 logsRequestBody: IAuditSearchRequest = { const logsRequestBody: IAuditSearchRequest = {
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,

View File

@ -7,5 +7,6 @@
[initialEntries]="initialEntries$ | async" [initialEntries]="initialEntries$ | async"
[isLeavingPage]="isLeavingPage" [isLeavingPage]="isLeavingPage"
[selectedDictionaryType]="entityType" [selectedDictionaryType]="entityType"
[activeEntryType]="activeEntryType"
[type]="type" [type]="type"
></redaction-dictionary-manager> ></redaction-dictionary-manager>

View File

@ -79,4 +79,8 @@ export class DictionaryScreenComponent implements OnInit {
this._loadingService.stop(); this._loadingService.stop();
} }
} }
get activeEntryType() {
return DICTIONARY_TO_ENTRY_TYPE_MAP[this.type];
}
} }

View File

@ -37,16 +37,8 @@ export class EntityInfoComponent {
return !this._addEditEntityComponent || this._addEditEntityComponent.form?.invalid || !this._addEditEntityComponent.changed; return !this._addEditEntityComponent || this._addEditEntityComponent.form?.invalid || !this._addEditEntityComponent.changed;
} }
get changed(): boolean {
return this._addEditEntityComponent.changed;
}
async save(): Promise<void> { async save(): Promise<void> {
try { await this._addEditEntityComponent.save();
await this._addEditEntityComponent.save();
} catch (e) {
console.error(e);
}
} }
revert(): void { revert(): void {

View File

@ -3,10 +3,11 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants'; import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions'; import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { size } from '@iqser/common-ui/lib/utils'; import { size } from '@iqser/common-ui/lib/utils';
import { ILicenseData } from '@red/domain';
@Component({ @Component({
selector: 'red-license-analysis-capacity-usage', selector: 'red-license-analysis-capacity-usage',
@ -35,7 +36,13 @@ export class LicenseAnalysisCapacityUsageComponent {
} }
#getCapacityDatasets(): ChartDataset[] { #getCapacityDatasets(): ChartDataset[] {
const monthlyData = this.licenseService.selectedLicenseReport.monthlyData; const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
const empty = { analysedFilesBytes: null } as ILicenseData;
dataUntilCurrentMonth.splice(0, 0, empty);
monthlyData.splice(0, 0, empty);
}
const datasets: ChartDataset[] = [ const datasets: ChartDataset[] = [
{ {
@ -48,9 +55,11 @@ export class LicenseAnalysisCapacityUsageComponent {
}, },
{ {
data: monthlyData.map( data: dataUntilCurrentMonth.map((month, monthIndex) =>
(month, monthIndex) => month.analysedFilesBytes
month.analysedFilesBytes + monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.analysedFilesBytes, 0), ? month.analysedFilesBytes +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.analysedFilesBytes ?? 0), 0)
: 0,
), ),
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-cumulative'), label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-cumulative'),
yAxisID: 'y1', yAxisID: 'y1',

View File

@ -3,10 +3,9 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants'; import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions'; import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { addPaddingToArray } from '@utils/functions';
import { ILicenseData } from '@red/domain'; import { ILicenseData } from '@red/domain';
@Component({ @Component({
@ -35,10 +34,12 @@ export class LicensePageUsageComponent {
} }
#getPagesDatasets(): ChartDataset[] { #getPagesDatasets(): ChartDataset[] {
const monthlyData = this.licenseService.selectedLicenseReport.monthlyData; const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
if (monthlyData.length === 1) { const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
const empty = monthlyData.map(() => ({ numberOfAnalyzedPages: 0 }) as ILicenseData)[0]; if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
addPaddingToArray<ILicenseData>(monthlyData, empty); const empty = { numberOfAnalyzedPages: null } as ILicenseData;
dataUntilCurrentMonth.splice(0, 0, empty);
monthlyData.splice(0, 0, empty);
} }
return [ return [
@ -58,10 +59,11 @@ export class LicensePageUsageComponent {
order: 1, order: 1,
}, },
{ {
data: monthlyData.map( data: dataUntilCurrentMonth.map((month, monthIndex) =>
(month, monthIndex) => month.numberOfAnalyzedPages
month.numberOfAnalyzedPages + ? month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.numberOfAnalyzedPages, 0), monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.numberOfAnalyzedPages ?? 0), 0)
: 0,
), ),
label: this._translateService.instant('license-info-screen.page-usage.cumulative-pages'), label: this._translateService.instant('license-info-screen.page-usage.cumulative-pages'),
yAxisID: 'y1', yAxisID: 'y1',

View File

@ -3,10 +3,10 @@ import { TranslateService } from '@ngx-translate/core';
import { size } from '@iqser/common-ui/lib/utils'; import { size } from '@iqser/common-ui/lib/utils';
import { LicenseService } from '@services/license.service'; import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import type { DonutChartConfig, ILicenseReport } from '@red/domain'; import { DonutChartConfig, ILicenseData, ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
import { ChartBlack, ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants'; import { ChartBlack, ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions'; import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({ @Component({
@ -60,7 +60,15 @@ export class LicenseRetentionCapacityComponent {
} }
#getDatasets(license: ILicenseReport): ChartDataset[] { #getDatasets(license: ILicenseReport): ChartDataset[] {
const monthlyData = license.monthlyData; const monthlyData = getDataUntilCurrentMonth(license.monthlyData);
if (monthlyData.length === 1) {
const empty = {
archivedFilesUploadedBytes: 0,
activeFilesUploadedBytes: 0,
trashFilesUploadedBytes: 0,
} as ILicenseData;
monthlyData.splice(0, 0, empty);
}
return [ return [
{ {

View File

@ -9,4 +9,5 @@ export enum LicenseFeatures {
PDFTRON = 'pdftron', PDFTRON = 'pdftron',
ANALYSIS_CAPACITY_BYTES = 'analysisCapacityBytes', ANALYSIS_CAPACITY_BYTES = 'analysisCapacityBytes',
RETENTION_CAPACITY_BYTES = 'retentionCapacityBytes', RETENTION_CAPACITY_BYTES = 'retentionCapacityBytes',
SUPPORT_MS_OFFICE_FORMATS = 'supportMSOfficeFormats',
} }

View File

@ -1,10 +1,12 @@
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { FillTarget } from 'chart.js'; import { FillTarget } from 'chart.js';
import { addPaddingToArray, hexToRgba } from '@utils/functions'; import { hexToRgba } from '@utils/functions';
import { ILicenseReport } from '@red/domain'; import { ILicenseData, ILicenseReport } from '@red/domain';
import { ComplexFillTarget } from 'chart.js/dist/types'; import { ComplexFillTarget } from 'chart.js/dist/types';
const monthNames = dayjs.monthsShort(); 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()}`; export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
@ -43,9 +45,17 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
monthIterator = monthIterator.add(1, 'month'); monthIterator = monthIterator.add(1, 'month');
} }
if (startMonth.month() === endMonth.month()) { if (startMonth.month() === endMonth.month() || startMonth.month() === currentMonth) {
addPaddingToArray<string>(result, ''); result.splice(0, 0, '');
} }
return result; return result;
}; };
export const getDataUntilCurrentMonth = (monthlyData: ILicenseData[]) => {
return monthlyData.filter(data => dayjs(data.startDate).month() <= currentMonth && dayjs(data.startDate).year() <= currentYear);
};
export const isCurrentMonthAndYear = (date: Date | string) => {
return dayjs(date).month() === currentMonth && dayjs(date).year() === currentYear;
};

View File

@ -1,26 +1,26 @@
<ng-container *ngIf="configService.listingMode$ | async as mode"> <ng-container *ngIf="configService.listingMode$ | async as mode">
<div <div
(click)="editFileAttribute($event)" (click)="handleFieldClick($event)"
(mousedown)="handleClick($event)" (mousedown)="handleClick($event)"
[ngClass]="{ 'workflow-attribute': mode === 'workflow', 'file-name-column': fileNameColumn }" [ngClass]="{ 'workflow-attribute': mode === 'workflow', 'file-name-column': fileNameColumn }"
class="file-attribute" class="file-attribute"
> >
<div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value"> <div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value" *ngIf="!isInEditMode || mode === 'workflow'">
<mat-icon *ngIf="!fileAttribute.editable" [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon> <mat-icon *ngIf="!fileAttribute.editable" [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon>
<div> <span
*ngIf="!isDate; else date"
[style.max-width]="attributeValueWidth"
[matTooltip]="fileAttributeValue"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
>
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b> <b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
<span {{ fileAttributeValue || '-' }}
*ngIf="!isDate; else date" </span>
[matTooltip]="fileAttributeValue"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
>
{{ fileAttributeValue || '-' }}</span
>
</div>
<ng-template #date> <ng-template #date>
<span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"> <span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }">
{{ fileAttributeValue ? (fileAttributeValue | date: 'd MMM yyyy') : '-' }}</span <b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
> {{ fileAttributeValue ? (fileAttributeValue | date: 'd MMM yyyy') : '-' }}
</span>
</ng-template> </ng-template>
</div> </div>
@ -37,7 +37,11 @@
*ngIf="!isInEditMode; else input" *ngIf="!isInEditMode; else input"
[attr.help-mode-key]="'edit_file_attributes'" [attr.help-mode-key]="'edit_file_attributes'"
[class.help-mode-button]="helpModeService.isHelpModeActive$ | async" [class.help-mode-button]="helpModeService.isHelpModeActive$ | async"
[ngClass]="{ 'workflow-edit-button': mode === 'workflow', 'action-buttons edit-button': !fileNameColumn }" [ngClass]="{
'workflow-edit-button': mode === 'workflow',
'action-buttons edit-button': !fileNameColumn,
'filename-edit-button': fileNameColumn
}"
> >
<div [ngClass]="{ 'workflow-edit-icon': mode === 'workflow', 'edit-icon': !fileNameColumn }"> <div [ngClass]="{ 'workflow-edit-icon': mode === 'workflow', 'edit-icon': !fileNameColumn }">
<mat-icon [svgIcon]="'iqser:edit'"></mat-icon> <mat-icon [svgIcon]="'iqser:edit'"></mat-icon>
@ -55,6 +59,8 @@
<form (ngSubmit)="form.valid && save()" [formGroup]="form"> <form (ngSubmit)="form.valid && save()" [formGroup]="form">
<iqser-dynamic-input <iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event" (closedDatepicker)="closedDatepicker = $event"
[style.max-width]="editFieldWidth"
[style.min-width]="editFieldWidth"
(keydown.escape)="close()" (keydown.escape)="close()"
[formControlName]="fileAttribute.id" [formControlName]="fileAttribute.id"
[id]="fileAttribute.id" [id]="fileAttribute.id"
@ -66,11 +72,17 @@
(action)="save()" (action)="save()"
[disabled]="disabled" [disabled]="disabled"
[icon]="'iqser:check'" [icon]="'iqser:check'"
[size]="mode === 'workflow' ? 15 : 34" [size]="mode === 'workflow' || fileNameColumn ? 15 : 34"
[ngClass]="{ 'file-name-btn': fileNameColumn }"
class="save" class="save"
></iqser-circle-button> ></iqser-circle-button>
<iqser-circle-button (action)="close()" [icon]="'iqser:close'" [size]="mode === 'workflow' ? 15 : 34"></iqser-circle-button> <iqser-circle-button
(action)="close()"
[ngClass]="{ 'file-name-btn': fileNameColumn }"
[icon]="'iqser:close'"
[size]="mode === 'workflow' || fileNameColumn ? 15 : 34"
></iqser-circle-button>
</form> </form>
</div> </div>
</ng-template> </ng-template>

View File

@ -6,7 +6,7 @@
&.file-name-column { &.file-name-column {
height: 20px; height: 20px;
width: fit-content; width: 100%;
border-radius: 4px; border-radius: 4px;
font-size: 11px; font-size: 11px;
line-height: 14px; line-height: 14px;
@ -40,7 +40,7 @@
.workflow-value { .workflow-value {
display: flex; display: flex;
width: 90%; width: 95%;
b { b {
text-transform: uppercase; text-transform: uppercase;
@ -81,6 +81,18 @@
&.file-name-column-input { &.file-name-column-input {
background: transparent; background: transparent;
border: none;
box-shadow: none;
form {
iqser-circle-button {
margin-left: 15px;
@media screen and (max-width: 1395px) {
margin-left: 6px;
}
}
}
} }
&.workflow-edit-input { &.workflow-edit-input {
@ -99,10 +111,6 @@
iqser-circle-button { iqser-circle-button {
margin: 0 5px; margin: 0 5px;
&:nth-child(2) {
padding-left: 10px;
}
&:last-child { &:last-child {
margin-right: -8px; margin-right: -8px;
} }
@ -118,7 +126,6 @@
margin-top: 0; margin-top: 0;
} }
.file-name-input,
.workflow-input { .workflow-input {
width: 100%; width: 100%;
padding-left: 2px; padding-left: 2px;
@ -143,6 +150,10 @@
} }
} }
.file-name-input {
padding-left: 5px;
}
.save { .save {
margin-left: 7px; margin-left: 7px;
} }
@ -150,6 +161,11 @@
} }
} }
.filename-edit-button:hover {
background-color: var(--iqser-grey-6);
border-radius: 50%;
}
.file-attribute:hover { .file-attribute:hover {
&.workflow-attribute { &.workflow-attribute {
background-color: var(--iqser-grey-6); background-color: var(--iqser-grey-6);
@ -158,6 +174,8 @@
} }
.workflow-value { .workflow-value {
width: 90%;
b { b {
white-space: nowrap; white-space: nowrap;
overflow: unset; overflow: unset;

View File

@ -62,6 +62,16 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
this.file.fileId !== this.fileAttributesService.fileEdit())) || this.file.fileId !== this.fileAttributesService.fileEdit())) ||
!this.fileAttributesService.openAttributeEdits().length, !this.fileAttributesService.openAttributeEdits().length,
); );
@Input() width?: number;
#widthFactor = window.innerWidth >= 1800 ? 0.85 : 0.7;
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 { get isDate(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.DATE; return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
@ -110,6 +120,15 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
); );
} }
@HostListener('window:resize')
onResize() {
if (window.innerWidth >= 1800) {
this.#widthFactor = 0.85;
} else {
this.#widthFactor = 0.7;
}
}
@Debounce(60) @Debounce(60)
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
clickOutside($event: MouseEvent) { clickOutside($event: MouseEvent) {
@ -131,7 +150,13 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
this.#subscriptions.unsubscribe(); this.#subscriptions.unsubscribe();
} }
async editFileAttribute($event: MouseEvent): Promise<void> { handleFieldClick($event: MouseEvent) {
if (!this.fileNameColumn) {
this.editFileAttribute($event);
}
}
editFileAttribute($event: MouseEvent) {
if ( if (
!this.file.isInitialProcessing && !this.file.isInitialProcessing &&
this.permissionsService.canEditFileAttributes(this.file, this.dossier) && this.permissionsService.canEditFileAttributes(this.file, this.dossier) &&
@ -143,9 +168,9 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
...value, ...value,
{ attribute: this.fileAttribute.id, file: this.file.id }, { attribute: this.fileAttribute.id, file: this.file.id },
]); ]);
this.#toggleEdit();
this.fileAttributesService.setFileEdit(this.file.fileId); this.fileAttributesService.setFileEdit(this.file.fileId);
this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute.id); this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute.id);
this.#toggleEdit();
} }
} }

View File

@ -31,9 +31,10 @@
.file-name-cell:hover { .file-name-cell:hover {
::ng-deep.file-name-column { ::ng-deep.file-name-column {
background: var(--iqser-grey-6); .filename-edit-button {
width: 24px;
height: 24px;
div {
mat-icon { mat-icon {
visibility: visible; visibility: visible;
} }

View File

@ -43,6 +43,7 @@ import { merge, Observable } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators'; import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '../config.service'; import { ConfigService } from '../config.service';
import { BulkActionsService } from '../services/bulk-actions.service'; import { BulkActionsService } from '../services/bulk-actions.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
@Component({ @Component({
templateUrl: './dossier-overview-screen.component.html', templateUrl: './dossier-overview-screen.component.html',
@ -92,6 +93,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
private readonly _fileDropOverlayService: FileDropOverlayService, private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierAttributesService: DossierAttributesService, private readonly _dossierAttributesService: DossierAttributesService,
private readonly _dossiersCacheService: DossiersCacheService,
) { ) {
super(); super();
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId).pipe(tap(dossier => (this.#dossier = dossier))); this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId).pipe(tap(dossier => (this.#dossier = dossier)));
@ -121,7 +123,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
get #dossierFilesChange$() { get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe( return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId), filter(dossierId => dossierId === this.dossierId && !!this._dossiersCacheService.get(dossierId)),
switchMap(dossierId => this._filesService.loadAll(dossierId)), switchMap(dossierId => this._filesService.loadAll(dossierId)),
); );
} }

View File

@ -148,7 +148,7 @@ export class AnnotationActionsComponent implements OnChanges {
acceptResize() { acceptResize() {
if (this.resized) { if (this.resized) {
return this.annotationActionsService.acceptResize(this.#annotations[0]); return this.annotationActionsService.acceptResize(this.#annotations[0], this.annotationPermissions);
} }
} }
@ -171,6 +171,10 @@ export class AnnotationActionsComponent implements OnChanges {
} }
get #annotationChangesAllowed() { get #annotationChangesAllowed() {
return !this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis; return (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.#somePending;
}
get #somePending() {
return this.#annotations.some(a => a.pending);
} }
} }

View File

@ -10,7 +10,7 @@
<div> <div>
<strong>{{ annotation.superTypeLabel | translate }}</strong> <strong>{{ annotation.superTypeLabel | translate }}</strong>
&nbsp; &nbsp;
<strong *ngIf="pending" class="pending-analysis"> <strong *ngIf="annotation.pending" class="pending-analysis">
{{ 'annotation.pending' | translate }} {{ 'annotation.pending' | translate }}
</strong> </strong>
</div> </div>

View File

@ -18,16 +18,4 @@ export class AnnotationCardComponent {
@Input() isSelected = false; @Input() isSelected = false;
constructor(readonly multiSelectService: MultiSelectService) {} constructor(readonly multiSelectService: MultiSelectService) {}
get pending() {
return (
this.annotation.pending &&
((this.annotation.isModifyDictionary &&
!this.annotation.isRemovedLocally &&
!this.annotation.hasBeenForcedHint &&
(this.annotation.lastManualChange !== ManualRedactionTypes.LEGAL_BASIS_CHANGE &&
this.annotation.lastManualChange !== ManualRedactionTypes.RESIZE_LOCALLY)) ||
this.annotation.type === ImageCategory.SIGNATURE)
);
}
} }

View File

@ -6,7 +6,6 @@
} }
.popover { .popover {
width: 260px;
padding: 10px; padding: 10px;
border-radius: 3px; border-radius: 3px;
background-color: var(--iqser-grey-1); background-color: var(--iqser-grey-1);

View File

@ -14,11 +14,12 @@ interface Engine {
readonly translateParams?: Record<string, any>; readonly translateParams?: Record<string, any>;
} }
const Engines = { export const Engines = {
DICTIONARY: 'DICTIONARY', DICTIONARY: 'DICTIONARY',
NER: 'NER', NER: 'NER',
RULE: 'RULE', RULE: 'RULE',
IMPORTED: 'IMPORTED', IMPORTED: 'IMPORTED',
MANUAL: 'MANUAL',
} as const; } as const;
type EngineName = keyof typeof Engines; type EngineName = keyof typeof Engines;
@ -53,7 +54,7 @@ export class AnnotationDetailsComponent implements OnChanges {
getChangesTooltip(): string | undefined { getChangesTooltip(): string | undefined {
const changes = changesProperties.filter(key => this.annotation.item[key]); const changes = changesProperties.filter(key => this.annotation.item[key]);
if (!changes.length) { if (!changes.length || !this.annotation.item.engines?.includes(Engines.MANUAL)) {
return; return;
} }

View File

@ -10,6 +10,7 @@
<div *ngIf="!annotation.item.isEarmark" class="actions-wrapper"> <div *ngIf="!annotation.item.isEarmark" class="actions-wrapper">
<div <div
*ngIf="!annotation.item.pending"
(click)="showComments = !showComments" (click)="showComments = !showComments"
[matTooltip]="'comments.comments' | translate: { count: annotation.item.numberOfComments }" [matTooltip]="'comments.comments' | translate: { count: annotation.item.numberOfComments }"
class="comments-counter" class="comments-counter"

View File

@ -4,6 +4,7 @@
</div> </div>
<redaction-annotation-wrapper <redaction-annotation-wrapper
*ngIf="!annotation.item.hiddenInWorkload"
(click)="annotationClicked(annotation.item, $event)" (click)="annotationClicked(annotation.item, $event)"
[annotation]="annotation" [annotation]="annotation"
></redaction-annotation-wrapper> ></redaction-annotation-wrapper>

View File

@ -1,7 +1,7 @@
<div *ngFor="let comment of comments(); trackBy: trackBy" class="comment"> <div *ngFor="let comment of comments(); trackBy: trackBy" class="comment">
<div class="comment-details-wrapper"> <div class="comment-details-wrapper">
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label"> <div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
<strong> {{ comment.user | name }} </strong> <strong> {{ comment.userId | name }} </strong>
{{ comment.date | date: 'sophisticatedDate' }} {{ comment.date | date: 'sophisticatedDate' }}
</div> </div>

View File

@ -55,7 +55,7 @@ export class CommentsComponent implements OnChanges {
text: value, text: value,
id: commentId, id: commentId,
annotationId: this.annotation.id, annotationId: this.annotation.id,
user: this.currentUser.id, userId: this.currentUser.id,
}, },
]); ]);
this.input.reset(); this.input.reset();

View File

@ -0,0 +1,5 @@
<cdk-virtual-scroll-viewport [itemSize]="LIST_ITEM_SIZE" [ngStyle]="{ height: redactedTextsAreaHeight + 'px' }">
<ul>
<li *cdkVirtualFor="let value of values">{{ value }}</li>
</ul>
</cdk-virtual-scroll-viewport>

View File

@ -0,0 +1,21 @@
@use 'common-mixins';
cdk-virtual-scroll-viewport {
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}
}

View File

@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { NgStyle } from '@angular/common';
const LIST_ITEM_SIZE = 16;
const MAX_ITEMS_DISPLAY = 5;
@Component({
selector: 'redaction-selected-annotations-list',
standalone: true,
imports: [CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, NgStyle],
templateUrl: './selected-annotations-list.component.html',
styleUrl: './selected-annotations-list.component.scss',
})
export class SelectedAnnotationsListComponent {
@Input({ required: true }) values: string[];
protected readonly LIST_ITEM_SIZE = LIST_ITEM_SIZE;
get redactedTextsAreaHeight() {
return this.values.length <= MAX_ITEMS_DISPLAY ? LIST_ITEM_SIZE * this.values.length : LIST_ITEM_SIZE * MAX_ITEMS_DISPLAY;
}
}

View File

@ -0,0 +1,16 @@
<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 }">
{{ cell.label }}
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,74 @@
@use 'common-mixins';
table {
padding: 0 13px;
max-width: 100%;
min-width: 100%;
border-spacing: 0;
tbody {
padding-top: 2px;
overflow-y: auto;
display: block;
@include common-mixins.scroll-bar;
}
tr {
max-width: 100%;
min-width: 100%;
display: table;
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;
}
}
}
tbody tr:nth-child(odd) {
td {
background-color: var(--iqser-alt-background);
}
}
.hide {
visibility: hidden;
}
.w-50 {
max-width: 0;
min-width: 50%;
&.hide {
display: none;
}
}
.bold {
font-weight: bold;
}

View File

@ -0,0 +1,28 @@
import { Component, Input } from '@angular/core';
import { NgClass, NgForOf, NgStyle } from '@angular/common';
export interface ValueColumn {
label: string;
show: boolean;
bold?: boolean;
}
const TABLE_ROW_SIZE = 18;
const MAX_ITEMS_DISPLAY = 10;
@Component({
selector: 'redaction-selected-annotations-table',
standalone: true,
imports: [NgForOf, NgClass, NgStyle],
templateUrl: './selected-annotations-table.component.html',
styleUrl: './selected-annotations-table.component.scss',
})
export class SelectedAnnotationsTableComponent {
@Input({ required: true }) columns: ValueColumn[];
@Input({ required: true }) data: ValueColumn[][];
@Input() staticColumns = false;
get redactedTextsAreaHeight() {
return this.data.length <= MAX_ITEMS_DISPLAY ? TABLE_ROW_SIZE * this.data.length : TABLE_ROW_SIZE * MAX_ITEMS_DISPLAY;
}
}

View File

@ -6,32 +6,33 @@
</div> </div>
<iqser-initials-avatar <iqser-initials-avatar
*ngIf="!editingReviewer" *ngIf="!state.isEditingReviewer()"
[id]="'assignee'" [id]="'assignee'"
[user]="file.assignee" [user]="file.assignee"
[withName]="!!file.assignee" [withName]="!!file.assignee"
></iqser-initials-avatar> ></iqser-initials-avatar>
<div <div
(click)="editingReviewer = true" (click)="state.isEditingReviewer.set(true)"
*ngIf="!editingReviewer && _canAssignReviewer()" *ngIf="!state.isEditingReviewer() && _canAssignReviewer()"
[id]="'assign-reviewer'" [id]="'assign-reviewer'"
[translate]="'file-preview.assign-reviewer'" [translate]="'file-preview.assign-reviewer'"
class="assign-reviewer pointer" class="assign-reviewer pointer"
iqserStopPropagation
></div> ></div>
<redaction-assign-user-dropdown <redaction-assign-user-dropdown
(cancel)="editingReviewer = false" (cancel)="state.isEditingReviewer.set(false)"
(save)="assignReviewer(file, $event)" (save)="assignReviewer(file, $event)"
*ngIf="editingReviewer" *ngIf="state.isEditingReviewer()"
[options]="_usersOptions()" [options]="_usersOptions()"
[value]="file.assignee" [value]="file.assignee"
iqserStopPropagation iqserStopPropagation
></redaction-assign-user-dropdown> ></redaction-assign-user-dropdown>
<div *ngIf="!editingReviewer && _canAssign()" class="assign-actions-wrapper"> <div *ngIf="!state.isEditingReviewer() && _canAssign()" class="assign-actions-wrapper">
<iqser-circle-button <iqser-circle-button
(action)="editingReviewer = true" (action)="state.isEditingReviewer.set(true)"
*ngIf="_canAssignOrUnassign() && !!file.assignee" *ngIf="_canAssignOrUnassign() && !!file.assignee"
[attr.help-mode-key]="'editor_assign_user'" [attr.help-mode-key]="'editor_assign_user'"
[buttonId]="'change-assignee'" [buttonId]="'change-assignee'"

View File

@ -45,7 +45,6 @@ export class UserManagementComponent implements OnInit, OnDestroy {
? _('file-preview.change-reviewer') ? _('file-preview.change-reviewer')
: _('file-preview.assign-reviewer'); : _('file-preview.assign-reviewer');
}); });
editingReviewer = false;
constructor( constructor(
readonly fileAssignService: FileAssignService, readonly fileAssignService: FileAssignService,
@ -76,7 +75,7 @@ export class UserManagementComponent implements OnInit, OnDestroy {
const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename }; const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename };
this.toaster.success(_('assignment.reviewer'), { params: translateParams }); this.toaster.success(_('assignment.reviewer'), { params: translateParams });
this.editingReviewer = false; this.state.isEditingReviewer.set(false);
} }
ngOnInit() { ngOnInit() {
@ -90,21 +89,21 @@ export class UserManagementComponent implements OnInit, OnDestroy {
@Bind() @Bind()
handleViewerClick() { handleViewerClick() {
this.ngZone.run(() => { this.ngZone.run(() => {
this.editingReviewer = false; this.state.isEditingReviewer.set(false);
}); });
} }
@HostListener('document:click') @HostListener('document:click')
clickOutside() { clickOutside() {
if (this.editingReviewer) { if (this.state.isEditingReviewer()) {
this.editingReviewer = false; this.state.isEditingReviewer.set(false);
} }
} }
@HostListener('document:keyup', ['$event']) @HostListener('document:keyup', ['$event'])
handleEsc($event: KeyboardEvent) { handleEsc($event: KeyboardEvent) {
if ($event.key === 'Escape' && this.editingReviewer) { if ($event.key === 'Escape' && this.state.isEditingReviewer()) {
this.editingReviewer = false; this.state.isEditingReviewer.set(false);
} }
} }

View File

@ -108,7 +108,7 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
#setDictionaries() { #setDictionaries() {
this.dictionaries = this._dictionaryService.getAddHintDictionaries( this.dictionaries = this._dictionaryService.getAddHintDictionaries(
this.#dossier.dossierTemplateId, this.#dossier.dossierId,
!this.#applyToAllDossiers, !this.#applyToAllDossiers,
this.dictionaryRequest, this.dictionaryRequest,
); );

View File

@ -3,16 +3,9 @@
<div [translate]="'edit-redaction.dialog.title'" class="dialog-header heading-l"></div> <div [translate]="'edit-redaction.dialog.title'" class="dialog-header heading-l"></div>
<div class="dialog-content redaction"> <div class="dialog-content redaction">
<div *ngIf="showList"> <div class="iqser-input-group" *ngIf="showList">
<label [translate]="'edit-redaction.dialog.content.redacted-text'" class="selected-text"></label> <label [translate]="'edit-redaction.dialog.content.redacted-text'" class="selected-text"></label>
<cdk-virtual-scroll-viewport <redaction-selected-annotations-list [values]="redactedTexts"></redaction-selected-annotations-list>
[itemSize]="16"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }"
>
<ul *cdkVirtualFor="let text of redactedTexts">
<li>{{ text }}</li>
</ul>
</cdk-virtual-scroll-viewport>
</div> </div>
<div class="iqser-input-group required w-450"> <div class="iqser-input-group required w-450">

View File

@ -1,22 +0,0 @@
@use 'common-mixins';
cdk-virtual-scroll-viewport {
margin-top: 8px;
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}

View File

@ -7,18 +7,12 @@
></div> ></div>
<div class="dialog-content redaction" [class.fixed-height]="isRedacted && isImage"> <div class="dialog-content redaction" [class.fixed-height]="isRedacted && isImage">
<div *ngIf="!isImage"> <div class="iqser-input-group" *ngIf="!isImage && redactedTexts.length && !allRectangles">
<label [translate]="labelTranslation"></label> <redaction-selected-annotations-table
<div *ngIf="!!redactedTexts.length && !allRectangles" class="iqser-input-group"> [columns]="tableColumns"
<cdk-virtual-scroll-viewport [data]="tableData"
[itemSize]="16" [staticColumns]="true"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }" ></redaction-selected-annotations-table>
>
<ul *cdkVirtualFor="let text of redactedTexts">
<li>{{ text }}</li>
</ul>
</cdk-virtual-scroll-viewport>
</div>
</div> </div>
<div *ngIf="!isManualRedaction" class="iqser-input-group w-450" [class.required]="!form.controls.type.disabled"> <div *ngIf="!isManualRedaction" class="iqser-input-group w-450" [class.required]="!form.controls.type.disabled">

View File

@ -1,29 +1,8 @@
@use 'common-mixins';
.dialog-content { .dialog-content {
padding-top: 8px;
&.fixed-height { &.fixed-height {
height: 386px; height: 386px;
overflow-y: auto; overflow-y: auto;
} }
} }
cdk-virtual-scroll-viewport {
margin-top: 8px;
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}

View File

@ -9,9 +9,9 @@ import { firstValueFrom } from 'rxjs';
import { getEditRedactionOptions, RedactOrHintOption } from '../../utils/dialog-options'; import { getEditRedactionOptions, RedactOrHintOption } from '../../utils/dialog-options';
import { EditRedactionData, EditRedactResult } from '../../utils/dialog-types'; import { EditRedactionData, EditRedactResult } from '../../utils/dialog-types';
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component'; import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
import { editRedactionLabelsTranslations } from '@translations/redact-text-translations';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { DialogHelpModeKeys } from '../../utils/constants'; import { DialogHelpModeKeys } from '../../utils/constants';
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
interface TypeSelectOptions { interface TypeSelectOptions {
type: string; type: string;
@ -31,15 +31,28 @@ export class EditRedactionDialogComponent
readonly #applyToAllDossiers = this.data.applyToAllDossiers; readonly #applyToAllDossiers = this.data.applyToAllDossiers;
readonly annotations = this.data.annotations; readonly annotations = this.data.annotations;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly editRedactionLabelsTranslations = editRedactionLabelsTranslations;
readonly isModifyDictionary = this.annotations.every(annotation => annotation.isModifyDictionary); readonly isModifyDictionary = this.annotations.every(annotation => annotation.isModifyDictionary);
readonly isImage = this.annotations.reduce((acc, next) => acc && next.isImage, true); 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 redactedTexts = !this.isImage ? this.annotations.map(annotation => annotation.value).filter(value => !!value) : null;
readonly isManualRedaction = this.annotations.every(annotation => annotation.type === SuperTypes.ManualRedaction); readonly isManualRedaction = this.annotations.some(annotation => annotation.type === SuperTypes.ManualRedaction);
readonly isHint = this.annotations.every(annotation => annotation.HINT || annotation.IMAGE_HINT); readonly isHint = this.annotations.every(annotation => annotation.HINT || annotation.IMAGE_HINT);
readonly isRedacted = this.annotations.every(annotation => annotation.isRedacted); readonly isRedacted = this.annotations.every(annotation => annotation.isRedacted);
readonly isImported: boolean = this.annotations.every(annotation => annotation.imported); readonly isImported: boolean = this.annotations.every(annotation => annotation.imported);
readonly allRectangles = this.annotations.reduce((acc, a) => acc && a.AREA, true); readonly allRectangles = this.annotations.reduce((acc, a) => acc && a.AREA, true);
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 },
]);
protected readonly roles = Roles; protected readonly roles = Roles;
options: DetailsRadioOption<RedactOrHintOption>[] | undefined; options: DetailsRadioOption<RedactOrHintOption>[] | undefined;
@ -81,9 +94,7 @@ export class EditRedactionDialogComponent
} }
get redactBasedTypes() { get redactBasedTypes() {
return this._dictionaryService return this._dictionaryService.getRedactionTypes(this.#dossier.dossierTemplateId).map(dictionary => dictionary.type);
.getRedactTextDictionaries(this.#dossier.dossierTemplateId, !this.#applyToAllDossiers)
.map(dictionary => dictionary.type);
} }
get isRedactBasedType() { get isRedactBasedType() {
@ -106,20 +117,12 @@ export class EditRedactionDialogComponent
return this.annotations.length > 1; return this.annotations.length > 1;
} }
get label() {
return this.allRectangles ? 'customRectangle' : this.isImported ? 'imported' : 'redactedText';
}
get labelTranslation() {
return editRedactionLabelsTranslations[this.label];
}
get helpButtonKey() { get helpButtonKey() {
if (this.isHint) { if (this.isHint) {
return DialogHelpModeKeys.HINT_EDIT return DialogHelpModeKeys.HINT_EDIT;
} }
if (this.someSkipped) { if (this.someSkipped) {
return DialogHelpModeKeys.SKIPPED_EDIT return DialogHelpModeKeys.SKIPPED_EDIT;
} }
return DialogHelpModeKeys.REDACTION_EDIT; return DialogHelpModeKeys.REDACTION_EDIT;
} }

View File

@ -4,6 +4,11 @@
<div *ngIf="isHintDialog" class="dialog-header heading-l" [translate]="'manual-annotation.dialog.header.force-hint'"></div> <div *ngIf="isHintDialog" class="dialog-header heading-l" [translate]="'manual-annotation.dialog.header.force-hint'"></div>
<div class="dialog-content"> <div class="dialog-content">
<redaction-selected-annotations-table
[columns]="tableColumns"
[data]="tableData"
[staticColumns]="true"
></redaction-selected-annotations-table>
<div *ngIf="!isHintDialog && !isDocumine" class="iqser-input-group required w-400"> <div *ngIf="!isHintDialog && !isDocumine" class="iqser-input-group required w-400">
<label [translate]="'manual-annotation.dialog.content.reason'"></label> <label [translate]="'manual-annotation.dialog.content.reason'"></label>
<mat-form-field> <mat-form-field>

View File

@ -1,3 +1,7 @@
.full-width { .full-width {
width: 100%; width: 100%;
} }
.dialog-content {
padding-top: 8px;
}

View File

@ -7,6 +7,7 @@ import { Dossier, ILegalBasisChangeRequest } from '@red/domain';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
export interface LegalBasisOption { export interface LegalBasisOption {
label?: string; label?: string;
@ -23,8 +24,24 @@ const DOCUMINE_LEGAL_BASIS = 'n-a.';
}) })
export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit { export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
readonly isDocumine = getConfig().IS_DOCUMINE; readonly isDocumine = getConfig().IS_DOCUMINE;
protected readonly roles = Roles;
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 },
]);
legalOptions: LegalBasisOption[] = []; legalOptions: LegalBasisOption[] = [];
protected readonly roles = Roles;
constructor( constructor(
private readonly _justificationsService: JustificationsService, private readonly _justificationsService: JustificationsService,
@ -33,7 +50,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] }, private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] },
) { ) {
super(_dialogRef); super(_dialogRef);
this.form = this._getForm(); this.form = this.#getForm();
} }
get isHintDialog() { get isHintDialog() {
@ -66,17 +83,17 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
} }
save() { save() {
this._dialogRef.close(this._createForceRedactionRequest()); this._dialogRef.close(this.#createForceRedactionRequest());
} }
private _getForm(): UntypedFormGroup { #getForm(): UntypedFormGroup {
return this._formBuilder.group({ 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], comment: [null],
}); });
} }
private _createForceRedactionRequest(): ILegalBasisChangeRequest { #createForceRedactionRequest(): ILegalBasisChangeRequest {
const request: ILegalBasisChangeRequest = {}; const request: ILegalBasisChangeRequest = {};
request.legalBasis = !this.isDocumine ? this.form.get('reason').value.legalBasis : DOCUMINE_LEGAL_BASIS; request.legalBasis = !this.isDocumine ? this.form.get('reason').value.legalBasis : DOCUMINE_LEGAL_BASIS;

View File

@ -78,7 +78,7 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
async ngOnInit() { async ngOnInit() {
this.possibleDictionaries = this.isDictionaryRequest this.possibleDictionaries = this.isDictionaryRequest
? await this._dictionaryService.getDictionariesOptions(this.#dossier.dossierTemplateId, this.#dossier.id) ? this._dictionaryService.getDictionariesOptions(this.#dossier.dossierTemplateId)
: this._dictionaryService.getRedactionTypes(this.#dossier.dossierTemplateId); : this._dictionaryService.getRedactionTypes(this.#dossier.dossierTemplateId);
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.#dossier.dossierTemplateId)); const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.#dossier.dossierTemplateId));

View File

@ -3,9 +3,12 @@
<div [translate]="'redact-text.dialog.title'" class="dialog-header heading-l"></div> <div [translate]="'redact-text.dialog.title'" class="dialog-header heading-l"></div>
<div class="dialog-content redaction"> <div class="dialog-content redaction">
<div *ngIf="form.controls.selectedText.value as selectedText" class="iqser-input-group w-450"> <div class="iqser-input-group">
<label [translate]="'redact-text.dialog.content.selected-text'" class="selected-text"></label> <redaction-selected-annotations-table
{{ selectedText }} [columns]="tableColumns"
[data]="tableData"
[staticColumns]="true"
></redaction-selected-annotations-table>
</div> </div>
<iqser-details-radio <iqser-details-radio
@ -18,7 +21,7 @@
<label [translate]="'redact-text.dialog.content.type'"></label> <label [translate]="'redact-text.dialog.content.type'"></label>
<mat-form-field> <mat-form-field>
<mat-select [placeholder]="'redact-text.dialog.content.type-placeholder' | translate" formControlName="dictionary"> <mat-select [placeholder]="'redact-text.dialog.content.unchanged' | translate" formControlName="dictionary">
<mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger> <mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger>
<mat-option <mat-option
(click)="typeChanged()" (click)="typeChanged()"
@ -47,7 +50,12 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<iqser-icon-button [label]="'redact-text.dialog.actions.save' | translate" [submit]="true" [type]="iconButtonTypes.primary"> <iqser-icon-button
[disabled]="disabled"
[label]="'redact-text.dialog.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
>
</iqser-icon-button> </iqser-icon-button>
<div [translate]="'redact-text.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div> <div [translate]="'redact-text.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>

View File

@ -12,9 +12,11 @@ import { tap } from 'rxjs/operators';
import { getRedactOrHintOptions, RedactOrHintOption, RedactOrHintOptions } from '../../utils/dialog-options'; import { getRedactOrHintOptions, RedactOrHintOption, RedactOrHintOptions } from '../../utils/dialog-options';
import { RedactRecommendationData, RedactRecommendationResult } from '../../utils/dialog-types'; import { RedactRecommendationData, RedactRecommendationResult } from '../../utils/dialog-types';
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component'; import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
@Component({ @Component({
templateUrl: './redact-recommendation-dialog.component.html', templateUrl: './redact-recommendation-dialog.component.html',
styleUrl: './redact-recommendation-dialog.component.scss',
}) })
export class RedactRecommendationDialogComponent export class RedactRecommendationDialogComponent
extends IqserDialogComponent<RedactRecommendationDialogComponent, RedactRecommendationData, RedactRecommendationResult> extends IqserDialogComponent<RedactRecommendationDialogComponent, RedactRecommendationData, RedactRecommendationResult>
@ -38,6 +40,21 @@ export class RedactRecommendationDialogComponent
reason: [null], reason: [null],
}); });
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 },
]);
constructor( constructor(
private readonly _justificationsService: JustificationsService, private readonly _justificationsService: JustificationsService,
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
@ -68,7 +85,7 @@ export class RedactRecommendationDialogComponent
} }
get disabled() { get disabled() {
return this.dictionaryRequest && !this.form.controls.dictionary.value; return !this.form.controls.dictionary.value;
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -114,7 +131,7 @@ export class RedactRecommendationDialogComponent
} }
#setDictionaries() { #setDictionaries() {
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierTemplateId, !this.#applyToAllDossiers); this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers);
} }
#selectReason() { #selectReason() {
@ -143,9 +160,10 @@ export class RedactRecommendationDialogComponent
} }
#resetValues() { #resetValues() {
const sameType = this.data.annotations.every(annotation => annotation.type === this.firstEntry.type);
this.#applyToAllDossiers = this.data.applyToAllDossiers ?? true; this.#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
if (this.dictionaryRequest) { if (this.dictionaryRequest) {
this.form.controls.dictionary.setValue(this.firstEntry.type); this.form.controls.dictionary.setValue(sameType ? this.firstEntry.type : null);
return; return;
} }
this.form.controls.dictionary.setValue(this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null); this.form.controls.dictionary.setValue(this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null);

View File

@ -3,53 +3,56 @@
<div [translate]="'redact-text.dialog.title'" class="dialog-header heading-l"></div> <div [translate]="'redact-text.dialog.title'" class="dialog-header heading-l"></div>
<div class="dialog-content redaction"> <div class="dialog-content redaction">
<div class="iqser-input-group w-450 selected-text-group"> <div class="iqser-input-group w-full selected-text-group">
<label
[translate]="
initialText === form.controls.selectedText.value
? 'redact-text.dialog.content.selected-text'
: 'redact-text.dialog.content.text'
"
class="selected-text"
></label>
<div <div
[class.fixed-height-36]="dictionaryRequest" [class.fixed-height-36]="dictionaryRequest"
[ngClass]="isEditingSelectedText ? 'flex relative' : 'flex-align-items-center'" [ngClass]="isEditingSelectedText ? 'flex relative' : 'flex-align-items-center'"
> >
<span *ngIf="!isEditingSelectedText" [innerHTML]="form.controls.selectedText.value"></span> <div class="table">
<label>Value</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="comment"
type="text"
></textarea>
<textarea <iqser-circle-button
*ngIf="isEditingSelectedText" (action)="toggleEditingSelectedText()"
[rows]="selectedTextRows" *ngIf="dictionaryRequest"
class="w-full" [showDot]="initialText !== form.get('selectedText').value && !isEditingSelectedText"
formControlName="selectedText" [tooltip]="'redact-text.dialog.content.edit-text' | translate"
iqserHasScrollbar [type]="isEditingSelectedText ? 'dark' : 'default'"
name="comment" [size]="18"
type="text" [iconSize]="13"
></textarea> icon="iqser:edit"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button <iqser-circle-button
(action)="toggleEditingSelectedText()" (action)="undoTextChange()"
*ngIf="dictionaryRequest" *ngIf="isEditingSelectedText"
[class.absolute]="isEditingSelectedText" [showDot]="initialText !== form.get('selectedText').value"
[showDot]="initialText !== form.get('selectedText').value && !isEditingSelectedText" [tooltip]="'redact-text.dialog.content.revert-text' | translate"
[tooltip]="'redact-text.dialog.content.edit-text' | translate" [size]="18"
[type]="isEditingSelectedText ? 'dark' : 'default'" [iconSize]="13"
class="edit-button" icon="red:undo"
icon="iqser:edit" tooltipPosition="below"
tooltipPosition="below" ></iqser-circle-button>
></iqser-circle-button> </div>
</div>
<iqser-circle-button
(action)="undoTextChange()"
*ngIf="isEditingSelectedText"
[showDot]="initialText !== form.get('selectedText').value"
[tooltip]="'redact-text.dialog.content.revert-text' | translate"
class="absolute undo-button"
icon="red:undo"
tooltipPosition="below"
></iqser-circle-button>
</div> </div>
</div> </div>

View File

@ -13,29 +13,14 @@
} }
} }
.edit-button { iqser-circle-button {
top: 0; padding-left: 8px;
right: calc((0.5rem + 34px) * -1);
position: sticky;
}
.undo-button {
top: 0;
right: calc((0.5rem + 34px) * -2);
} }
.w-full { .w-full {
width: 100%; width: 100%;
} }
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.fixed-height-36 { .fixed-height-36 {
min-height: 36px; min-height: 36px;
} }
@ -43,3 +28,30 @@
textarea { textarea {
margin-top: 0; margin-top: 0;
} }
.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;
}
}
}

View File

@ -14,7 +14,7 @@ import { getRedactOrHintOptions, RedactOrHintOption, RedactOrHintOptions } from
import { RedactTextData, RedactTextResult } from '../../utils/dialog-types'; import { RedactTextData, RedactTextResult } from '../../utils/dialog-types';
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component'; import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
const MAXIMUM_SELECTED_TEXT_WIDTH = 421; const MAXIMUM_TEXT_AREA_WIDTH = 421;
@Component({ @Component({
templateUrl: './redact-text-dialog.component.html', templateUrl: './redact-text-dialog.component.html',
@ -39,6 +39,20 @@ export class RedactTextDialogComponent
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId); readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
readonly #manualRedactionTypeExists = inject(DictionaryService).hasManualType(this.#dossier.dossierTemplateId); readonly #manualRedactionTypeExists = inject(DictionaryService).hasManualType(this.#dossier.dossierTemplateId);
#applyToAllDossiers = this.data.applyToAllDossiers ?? true; #applyToAllDossiers = this.data.applyToAllDossiers ?? true;
readonly maximumTextAreaWidth = MAXIMUM_TEXT_AREA_WIDTH;
readonly maximumSelectedTextWidth = 567;
textWidth: number;
get defaultOption() {
const inDossierOption = this.options.find(option => option.value === RedactOrHintOptions.IN_DOSSIER);
this.dictionaryRequest = !!inDossierOption && !inDossierOption.disabled;
if (this.dictionaryRequest) {
this.#setDictionaries();
return inDossierOption;
}
return this.options[0];
}
constructor( constructor(
private readonly _justificationsService: JustificationsService, private readonly _justificationsService: JustificationsService,
@ -49,7 +63,8 @@ export class RedactTextDialogComponent
this.options = getRedactOrHintOptions(this.#dossier, this.#applyToAllDossiers, this.data.isApprover, this.data.isPageExcluded); this.options = getRedactOrHintOptions(this.#dossier, this.#applyToAllDossiers, this.data.isApprover, this.data.isPageExcluded);
this.form = this.#getForm(); this.form = this.#getForm();
this.#setupValidators(this.dictionaryRequest ? RedactOrHintOptions.IN_DOSSIER : RedactOrHintOptions.ONLY_HERE);
this.textWidth = calcTextWidthInPixels(this.form.controls.selectedText.value);
this.form.controls.option.valueChanges this.form.controls.option.valueChanges
.pipe( .pipe(
tap((option: DetailsRadioOption<RedactOrHintOption>) => { tap((option: DetailsRadioOption<RedactOrHintOption>) => {
@ -125,7 +140,7 @@ export class RedactTextDialogComponent
this.isEditingSelectedText = !this.isEditingSelectedText; this.isEditingSelectedText = !this.isEditingSelectedText;
if (this.isEditingSelectedText) { if (this.isEditingSelectedText) {
const width = calcTextWidthInPixels(this.form.controls.selectedText.value); const width = calcTextWidthInPixels(this.form.controls.selectedText.value);
this.selectedTextRows = Math.ceil(width / MAXIMUM_SELECTED_TEXT_WIDTH); this.selectedTextRows = Math.ceil(width / MAXIMUM_TEXT_AREA_WIDTH);
} }
} }
@ -146,16 +161,16 @@ export class RedactTextDialogComponent
} }
#setDictionaries() { #setDictionaries() {
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierTemplateId, !this.#applyToAllDossiers); this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers);
} }
#getForm(): FormGroup { #getForm(): FormGroup {
return this._formBuilder.group({ return this._formBuilder.group({
selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value, selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value,
reason: [null as LegalBasisOption, Validators.required], reason: [null as LegalBasisOption],
comment: [null], comment: [null],
dictionary: [this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null], dictionary: [this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null],
option: this.options[0], option: this.defaultOption,
}); });
} }
@ -202,4 +217,6 @@ export class RedactTextDialogComponent
} }
this.form.controls.dictionary.setValue(this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null); this.form.controls.dictionary.setValue(this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null);
} }
protected readonly window = window;
} }

View File

@ -1,51 +1,38 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div <div
*ngIf="data.redactions.length === 1" [innerHTML]="(isBulk ? 'remove-redaction.dialog.title-bulk' : 'remove-redaction.dialog.title') | translate: typeTranslationArg"
[innerHTML]="
'remove-redaction.dialog.title' | translate: { type: hint ? 'hint' : recommendation ? 'recommendation' : 'redaction' }
"
class="dialog-header heading-l"
></div>
<div
*ngIf="data.redactions.length > 1"
[innerHTML]="
'remove-redaction.dialog.title-bulk' | translate: { type: hint ? 'hint' : recommendation ? 'recommendation' : 'redaction' }
"
class="dialog-header heading-l" class="dialog-header heading-l"
></div> ></div>
<div class="dialog-content redaction"> <div [ngStyle]="{ height: dialogContentHeight + redactedTextsAreaHeight + 'px' }" class="dialog-content redaction">
<div *ngIf="data.redactions.length > 1"> <div class="iqser-input-group">
<label <redaction-selected-annotations-table
[translateParams]="{ type: hint ? 'hints' : recommendation ? 'recommendations' : 'redactions' }" [columns]="tableColumns()"
[translate]="'remove-redaction.dialog.content.redacted-text'" [data]="tableData()"
class="selected-text" [staticColumns]="!hasFalsePositiveOption"
></label> ></redaction-selected-annotations-table>
<cdk-virtual-scroll-viewport
[itemSize]="16"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }"
>
<ul *cdkVirtualFor="let text of redactedTexts; let idx = index">
<li>
{{
(isFalsePositive
? 'remove-redaction.dialog.content.list-item-false-positive'
: 'remove-redaction.dialog.content.list-item'
)
| translate
: {
text: text,
context: data.falsePositiveContext[idx]
}
| replaceNbsp
}}
</li>
</ul>
</cdk-virtual-scroll-viewport>
</div> </div>
<ng-template #singleSelected>
<span
[innerHTML]="
((recommendation
? 'remove-redaction.dialog.content.list-item-false-recommendation'
: isFalsePositive
? 'remove-redaction.dialog.content.list-item-false-positive'
: 'remove-redaction.dialog.content.list-item'
)
| translate
: {
text: redactedTexts[0],
type: data.redactions[0].typeLabel
}
| replaceNbsp) + (isFalsePositive ? data.falsePositiveContext[0] : '')
"
></span>
</ng-template>
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio> <iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>
<div class="iqser-input-group w-450"> <div class="iqser-input-group w-450">

View File

@ -1,21 +1,4 @@
@use 'common-mixins';
.dialog-content { .dialog-content {
height: 408px; padding-top: 8px;
} padding-bottom: 35px;
cdk-virtual-scroll-viewport {
margin-top: 8px;
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
} }

View File

@ -1,12 +1,13 @@
import { Component } from '@angular/core'; import { Component, computed } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, UntypedFormGroup } from '@angular/forms';
import { DetailsRadioOption, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui'; import { DetailsRadioOption, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import { tap } from 'rxjs/operators'; import { getRemoveRedactionOptions, RemoveRedactionOption, RemoveRedactionOptions } from '../../utils/dialog-options';
import { getRemoveRedactionOptions, RemoveAnnotationOptions, RemoveRedactionOption } from '../../utils/dialog-options';
import { RemoveRedactionData, RemoveRedactionResult } from '../../utils/dialog-types'; import { RemoveRedactionData, RemoveRedactionResult } from '../../utils/dialog-types';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { DialogHelpModeKeys } from '../../utils/constants'; import { DialogHelpModeKeys } from '../../utils/constants';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
@Component({ @Component({
templateUrl: './remove-redaction-dialog.component.html', templateUrl: './remove-redaction-dialog.component.html',
@ -17,66 +18,88 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
RemoveRedactionData, RemoveRedactionData,
RemoveRedactionResult RemoveRedactionResult
> { > {
#applyToAllDossiers: boolean;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly options: DetailsRadioOption<RemoveRedactionOption>[]; readonly options: DetailsRadioOption<RemoveRedactionOption>[] = getRemoveRedactionOptions(this.data);
readonly recommendation: boolean; readonly recommendation = this.data.redactions.every(redaction => redaction.isRecommendation);
readonly hint: boolean; readonly hint = this.data.redactions[0].HINT;
readonly skipped: boolean; readonly skipped = this.data.redactions.some(annotation => annotation.isSkipped);
readonly redactedTexts: string[]; readonly redactedTexts = this.data.redactions.map(annotation => annotation.value);
protected readonly roles = Roles; protected readonly roles = Roles;
form!: UntypedFormGroup; form: UntypedFormGroup = this._formBuilder.group({
comment: [null],
option: [this.defaultOption],
});
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: 'Context',
show: this.isFalsePositive(),
},
]);
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() },
]),
);
constructor(private readonly _formBuilder: FormBuilder) { constructor(private readonly _formBuilder: FormBuilder) {
super(); super();
this.recommendation = this.data.redactions[0].isRecommendation;
this.hint = this.data.redactions[0].HINT;
this.skipped = this.data.redactions.some(annotation => annotation.isSkipped);
this.options = getRemoveRedactionOptions(this.data);
this.redactedTexts = this.data.redactions.map(annotation => annotation.value);
this.form = this.#getForm();
this.#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
this.form
.get('option')
.valueChanges.pipe(
tap(() => {
for (const option of this.options) {
if (option.extraOption) {
option.extraOption.checked = this.#applyToAllDossiers;
}
}
}),
takeUntilDestroyed(),
)
.subscribe();
}
get isFalsePositive(): boolean {
return this.form.get('option').value.value === RemoveAnnotationOptions['FALSE' + '_POSITIVE'];
} }
get helpButtonKey() { get helpButtonKey() {
if (this.hint) { if (this.hint) {
return DialogHelpModeKeys.HINT_REMOVE return DialogHelpModeKeys.HINT_REMOVE;
} }
if (this.recommendation) { if (this.recommendation) {
return DialogHelpModeKeys.RECOMMENDATION_REMOVE return DialogHelpModeKeys.RECOMMENDATION_REMOVE;
} }
if (this.skipped) { if (this.skipped) {
return DialogHelpModeKeys.SKIPPED_REMOVE return DialogHelpModeKeys.SKIPPED_REMOVE;
} }
return DialogHelpModeKeys.REDACTION_REMOVE; return DialogHelpModeKeys.REDACTION_REMOVE;
} }
get hasFalsePositiveOption() {
return !!this.options.find(option => option.value === RemoveRedactionOptions.FALSE_POSITIVE);
}
get defaultOption() {
const removeHereOption = this.options.find(option => option.value === RemoveRedactionOptions.ONLY_HERE);
if (!!removeHereOption && !removeHereOption.disabled) return removeHereOption;
return this.options[0];
}
get typeTranslationArg() {
return { type: this.hint ? 'hint' : this.recommendation ? 'recommendation' : 'redaction' };
}
get isBulk() {
return this.data.redactions.length > 1;
}
get redactedTextsAreaHeight() {
return this.redactedTexts.length <= 10 ? 18 * this.redactedTexts.length : 180;
}
get dialogContentHeight() {
return this.options.length * 75 + 230;
}
save(): void { save(): void {
this.close(this.form.getRawValue()); this.close(this.form.getRawValue());
} }
#getForm() {
return this._formBuilder.group({
comment: [null],
option: [this.options[0]],
});
}
} }

View File

@ -7,14 +7,18 @@
<div class="dialog-content redaction"> <div class="dialog-content redaction">
<ng-container *ngIf="!redaction.isImage && !redaction.AREA"> <ng-container *ngIf="!redaction.isImage && !redaction.AREA">
<div class="iqser-input-group w-450"> <div class="flex-start">
<label [translate]="'resize-redaction.dialog.content.original-text'" class="selected-text"></label> <label [translate]="'resize-redaction.dialog.content.original-text'"></label>
<span>{{ redaction.value }}</span> <span class="multi-line-ellipsis"
><b>{{ redaction.value }}</b></span
>
</div> </div>
<div class="iqser-input-group w-450"> <div class="flex-start">
<label [translate]="'resize-redaction.dialog.content.resized-text'" class="selected-text"></label> <label [translate]="'resize-redaction.dialog.content.resized-text'"></label>
<span>{{ data.text }}</span> <span class="multi-line-ellipsis"
><b>{{ data.text }}</b></span
>
</div> </div>
</ng-container> </ng-container>

View File

@ -0,0 +1,20 @@
.multi-line-ellipsis {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
.flex-start {
display: flex;
align-items: start;
padding: 0 13px;
label {
opacity: 0.7;
font-weight: normal;
min-width: 15%;
}
}

View File

@ -8,6 +8,7 @@ import { ResizeRedactionData, ResizeRedactionResult } from '../../utils/dialog-t
@Component({ @Component({
templateUrl: './resize-redaction-dialog.component.html', templateUrl: './resize-redaction-dialog.component.html',
styleUrls: ['./resize-redaction-dialog.component.scss'],
}) })
export class ResizeRedactionDialogComponent extends IqserDialogComponent< export class ResizeRedactionDialogComponent extends IqserDialogComponent<
ResizeRedactionDialogComponent, ResizeRedactionDialogComponent,
@ -29,7 +30,14 @@ export class ResizeRedactionDialogComponent extends IqserDialogComponent<
constructor(private readonly _formBuilder: FormBuilder) { constructor(private readonly _formBuilder: FormBuilder) {
super(); super();
this.options = getResizeRedactionOptions(this.redaction, this.#dossier, false, this.#applyToAllDossiers, this.data.isApprover); this.options = getResizeRedactionOptions(
this.redaction,
this.#dossier,
false,
this.#applyToAllDossiers,
this.data.isApprover,
this.data.permissions.canResizeInDictionary,
);
this.form = this.#getForm(); this.form = this.#getForm();
} }

View File

@ -94,7 +94,7 @@
</div> </div>
<div class="right-container"> <div class="right-container">
<redaction-file-preview-right-container iqserDisableStopPropagation></redaction-file-preview-right-container> <redaction-file-preview-right-container [iqserDisableStopPropagation]="state.isEditingReviewer()"></redaction-file-preview-right-container>
</div> </div>
</div> </div>
</section> </section>

View File

@ -70,6 +70,8 @@ import { DocumentUnloadedGuard } from './services/document-unloaded.guard';
import { FilePreviewDialogService } from './services/file-preview-dialog.service'; import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { ManualRedactionService } from './services/manual-redaction.service'; import { ManualRedactionService } from './services/manual-redaction.service';
import { TablesService } from './services/tables.service'; import { TablesService } from './services/tables.service';
import { SelectedAnnotationsTableComponent } from './components/selected-annotations-table/selected-annotations-table.component';
import { SelectedAnnotationsListComponent } from './components/selected-annotations-list/selected-annotations-list.component';
const routes: IqserRoutes = [ const routes: IqserRoutes = [
{ {
@ -152,6 +154,8 @@ const components = [
LogPipe, LogPipe,
ReplaceNbspPipe, ReplaceNbspPipe,
DisableStopPropagationDirective, DisableStopPropagationDirective,
SelectedAnnotationsTableComponent,
SelectedAnnotationsListComponent,
], ],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService], providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService],
}) })

View File

@ -16,7 +16,7 @@ import {
import { CommentsApiService } from '@services/comments-api.service'; import { CommentsApiService } from '@services/comments-api.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom, Observable, zip } from 'rxjs'; import { firstValueFrom, Observable } from 'rxjs';
import { getFirstRelevantTextPart } from '../../../utils'; import { getFirstRelevantTextPart } from '../../../utils';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service'; import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service'; import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
@ -32,6 +32,7 @@ import { ResizeRedactionDialogComponent } from '../dialogs/resize-redaction-dial
import { RemoveRedactionOptions } from '../utils/dialog-options'; import { RemoveRedactionOptions } from '../utils/dialog-options';
import { import {
EditRedactionData, EditRedactionData,
EditRedactResult,
RemoveRedactionData, RemoveRedactionData,
RemoveRedactionPermissions, RemoveRedactionPermissions,
RemoveRedactionResult, RemoveRedactionResult,
@ -92,7 +93,7 @@ export class AnnotationActionsService {
async editRedaction(annotations: AnnotationWrapper[]) { async editRedaction(annotations: AnnotationWrapper[]) {
const { dossierId, dossierTemplateId, fileId, file } = this._state; const { dossierId, dossierTemplateId, fileId, file } = this._state;
const isUnprocessed = annotations.every(annotation => annotation.pending); const includeUnprocessed = annotations.every(annotation => this.#includeUnprocessed(annotation, true));
const dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId); const dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
const data = { const data = {
annotations, annotations,
@ -101,47 +102,34 @@ export class AnnotationActionsService {
}; };
const result = await this.#getEditRedactionDialog(data).result(); const result = await this.#getEditRedactionDialog(data).result();
const requests: Observable<unknown>[] = [];
if (!result) { if (!result) {
return; return;
} }
if ( const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => {
!this.#isDocumine && const body = { annotationId: annotation.id, type: result.type ?? annotation.type };
(!annotations.every(annotation => annotation.legalBasis === result.legalBasis) || if (!this.#isDocumine) {
!annotations.every(annotation => annotation.section === result.section)) return {
) { ...body,
const changeLegalBasisBody = annotations.map(annotation => ({ legalBasis: result.legalBasis,
annotationId: annotation.id, section: result.section ?? annotation.section,
legalBasis: result.legalBasis, value: result.value ?? annotation.value,
section: result.section ?? annotation.section, };
value: result.value ?? annotation.value, }
})); return body;
requests.push( });
this._manualRedactionService.changeLegalBasis(
changeLegalBasisBody, await this.#processObsAndEmit(
dossierId, this._manualRedactionService
fileId, .recategorizeRedactions(
file().excludedFromAutomaticAnalysis && isUnprocessed,
),
);
}
if (result.type && !annotations.every(annotation => annotation.type === result.type)) {
const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => ({
annotationId: annotation.id,
type: result.type ?? annotation.type,
}));
requests.push(
this._manualRedactionService.recategorizeRedactions(
recategorizeBody, recategorizeBody,
dossierId, dossierId,
fileId, fileId,
file().excludedFromAutomaticAnalysis && isUnprocessed, this.#getChangedFields(annotations, result),
), includeUnprocessed,
); )
} .pipe(log()),
);
if (result.comment) { if (result.comment) {
try { try {
@ -152,11 +140,6 @@ export class AnnotationActionsService {
this._toaster.rawError(error.error.message); this._toaster.rawError(error.error.message);
} }
} }
if (!requests.length) {
return;
}
await this.#processObsAndEmit(zip(requests).pipe(log()));
} }
async removeRedaction(redactions: AnnotationWrapper[], permissions: AnnotationPermissions) { async removeRedaction(redactions: AnnotationWrapper[], permissions: AnnotationPermissions) {
@ -167,7 +150,6 @@ export class AnnotationActionsService {
}; };
const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId); const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
const isApprover = this._permissionsService.isApprover(this._state.dossier()); const isApprover = this._permissionsService.isApprover(this._state.dossier());
const isUnprocessed = redactions.every(annotation => annotation.pending);
const data = { const data = {
redactions, redactions,
@ -190,7 +172,7 @@ export class AnnotationActionsService {
) { ) {
this.#setAsFalsePositive(redactions, result); this.#setAsFalsePositive(redactions, result);
} else { } else {
this.#removeRedaction(redactions, result, this._state.file().excludedFromAutomaticAnalysis && isUnprocessed); this.#removeRedaction(redactions, result);
} }
} }
@ -240,8 +222,9 @@ export class AnnotationActionsService {
this._annotationManager.select(viewerAnnotation); this._annotationManager.select(viewerAnnotation);
} }
async acceptResize(annotation: AnnotationWrapper): Promise<void> { async acceptResize(annotation: AnnotationWrapper, permissions: AnnotationPermissions): Promise<void> {
const textAndPositions = await this.#extractTextAndPositions(annotation.id); const textAndPositions = await this.#extractTextAndPositions(annotation.id);
const includeUnprocessed = this.#includeUnprocessed(annotation);
if (annotation.isRecommendation) { if (annotation.isRecommendation) {
const recommendation = { const recommendation = {
...annotation, ...annotation,
@ -263,12 +246,16 @@ export class AnnotationActionsService {
const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId); const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
const isUnprocessed = annotation.pending; const isUnprocessed = annotation.pending;
const data = { const data: ResizeRedactionData = {
redaction: annotation, redaction: annotation,
text, text,
applyToAllDossiers: isApprover && dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault, applyToAllDossiers: isApprover && dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
isApprover, isApprover,
dossierId: dossier.dossierId, dossierId: dossier.dossierId,
permissions: {
canResizeAnnotation: permissions.canResizeAnnotation,
canResizeInDictionary: permissions.canResizeInDictionary,
},
}; };
const result = await this.#getResizeRedactionDialog(data).result(); const result = await this.#getResizeRedactionDialog(data).result();
@ -289,12 +276,7 @@ export class AnnotationActionsService {
await this.cancelResize(annotation); await this.cancelResize(annotation);
const { fileId, dossierId, file } = this._state; const { fileId, dossierId, file } = this._state;
const request = this._manualRedactionService.resize( const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId, includeUnprocessed);
[resizeRequest],
dossierId,
fileId,
isUnprocessed && file().excludedFromAutomaticAnalysis,
);
return this.#processObsAndEmit(request); return this.#processObsAndEmit(request);
} }
@ -449,8 +431,9 @@ export class AnnotationActionsService {
this.#processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId)).then(); this.#processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId)).then();
} }
#removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult, includeUnprocessed = false) { #removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) {
const removeFromDictionary = dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER; const removeFromDictionary = dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER;
const includeUnprocessed = redactions.every(redaction => this.#includeUnprocessed(redaction, true));
const body = redactions.map(redaction => ({ const body = redactions.map(redaction => ({
annotationId: redaction.id, annotationId: redaction.id,
comment: dialogResult.comment, comment: dialogResult.comment,
@ -498,4 +481,37 @@ export class AnnotationActionsService {
isApprover, isApprover,
}; };
} }
#getChangedFields(annotations: AnnotationWrapper[], result: EditRedactResult) {
const changedFields = [];
if (result.type && !annotations.every(annotation => annotation.type === result.type)) {
changedFields.push('type');
}
if (this.#isDocumine) {
return { changes: changedFields.join(', ') };
}
if (result.legalBasis && !annotations.every(annotation => annotation.legalBasis === result.legalBasis)) {
changedFields.push('reason');
}
if (typeof result.section === 'string' && !annotations.every(annotation => annotation.section === result.section)) {
changedFields.push('paragraph/location');
}
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;
}
return false;
}
} }

View File

@ -19,6 +19,7 @@ import {
} from '../utils/sort-by-page-rotation.utils'; } from '../utils/sort-by-page-rotation.utils';
import { FileDataService } from './file-data.service'; import { FileDataService } from './file-data.service';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
import { Engines } from '../components/annotation-details/annotation-details.component';
@Injectable() @Injectable()
export class AnnotationProcessingService { export class AnnotationProcessingService {
@ -50,7 +51,8 @@ export class AnnotationProcessingService {
label: _('filter-menu.redaction-changes'), label: _('filter-menu.redaction-changes'),
checked: false, checked: false,
topLevelFilter: true, topLevelFilter: true,
checker: (annotation: AnnotationWrapper) => annotation?.hasRedactionChanges, checker: (annotation: AnnotationWrapper) =>
annotation?.hasRedactionChanges && annotation?.engines?.includes(Engines.MANUAL),
}, },
{ {
id: 'unseen-pages', id: 'unseen-pages',
@ -124,7 +126,7 @@ export class AnnotationProcessingService {
}); });
for (const filter of filters) { for (const filter of filters) {
filter.children.sort((a, b) => a.id.localeCompare(b.id)); filter.children.sort((a, b) => a.id?.localeCompare(b.id));
handleCheckedValue(filter); handleCheckedValue(filter);
if (filter.checked || filter.indeterminate) { if (filter.checked || filter.indeterminate) {
filter.expanded = true; filter.expanded = true;

View File

@ -3,7 +3,18 @@ import { toObservable } from '@angular/core/rxjs-interop';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui'; import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ChangeType, ChangeTypes, File, IEntityLog, IEntityLogEntry, SuperTypeMapper, ViewedPage, ViewMode, ViewModes } from '@red/domain'; import {
ChangeType,
ChangeTypes,
EntryStates,
File,
IEntityLog,
IEntityLogEntry,
SuperTypeMapper,
ViewedPage,
ViewMode,
ViewModes,
} from '@red/domain';
import { DictionaryService } from '@services/entity-services/dictionary.service'; import { DictionaryService } from '@services/entity-services/dictionary.service';
import { EarmarksService } from '@services/files/earmarks.service'; import { EarmarksService } from '@services/files/earmarks.service';
import { EntityLogService } from '@services/files/entity-log.service'; import { EntityLogService } from '@services/files/entity-log.service';
@ -167,7 +178,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #convertData(entityLog: IEntityLog) { async #convertData(entityLog: IEntityLog) {
const file = this._state.file(); const file = this._state.file();
const annotations: AnnotationWrapper[] = []; let annotations: AnnotationWrapper[] = [];
const dictionaries = this._state.dictionaries; const dictionaries = this._state.dictionaries;
const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId); const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId);
let checkDictionary = true; let checkDictionary = true;
@ -180,22 +191,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
} }
const canBeMappedToASuperType = !!SuperTypeMapper[entry.entryType][entry.state](entry); const canBeMappedToASuperType = !!SuperTypeMapper[entry.entryType][entry.state](entry);
if (!canBeMappedToASuperType && this.#isIqserDevMode) {
this.#logger.error(
`[ENTITY_LOG] Entity ${entry.value} (${entry.entryType}, ${entry.state}) cannot be mapped to a super type!`,
entry,
);
this.#toaster.rawError(
`Skipping entity ${entry.value} (${entry.entryType}, ${entry.state}). It has unexpected state.
Check console for details.`,
{
timeOut: 10000,
easing: 'ease-in-out',
easeTime: 500,
},
);
}
if (!canBeMappedToASuperType) { if (!canBeMappedToASuperType) {
continue; continue;
} }
@ -213,6 +208,15 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
} }
const changeType = this.#getChangeLogType(entry, file); const changeType = this.#getChangeLogType(entry, file);
if (entry.state === EntryStates.PENDING) {
const originalAnnotation = this.#findOriginalAnnotation(annotations, entry);
if (originalAnnotation) {
entry.oldState = originalAnnotation.entry.state;
originalAnnotation.pending = true;
}
}
const annotation = AnnotationWrapper.fromData( const annotation = AnnotationWrapper.fromData(
entry, entry,
dictionary, dictionary,
@ -223,6 +227,10 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
); );
annotations.push(annotation); annotations.push(annotation);
if (annotation.pending) {
this.#createPendingAnnotations(annotation, annotations);
}
} }
return annotations; return annotations;
@ -271,4 +279,28 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
this._viewedPagesMapService.delete(this._state.fileId, viewedPage); this._viewedPagesMapService.delete(this._state.fileId, viewedPage);
} }
} }
#createPendingAnnotations(annotation: AnnotationWrapper, annotations: AnnotationWrapper[]) {
for (let i = 0; i < annotation.positions.length; i++) {
const pendingAnnotation = {
...annotation,
id: `${annotation.id}-${i}`,
positions: [annotation.positions[i]],
hiddenInWorkload: true,
} as AnnotationWrapper;
annotations.push(pendingAnnotation);
}
}
#findOriginalAnnotation(annotations: AnnotationWrapper[], pendingEntry: IEntityLogEntry) {
for (let i = annotations.length - 1; i >= 0; i--) {
const annotation = annotations[i];
if (annotation.positions[0].page !== pendingEntry.positions[0].pageNumber) {
return;
}
if (annotation.id === pendingEntry.id) {
return annotation;
}
}
}
} }

View File

@ -1,5 +1,5 @@
import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http'; import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http';
import { computed, effect, inject, Injectable, Signal } from '@angular/core'; import { computed, effect, inject, Injectable, signal, Signal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { LoadingService, wipeCache } from '@iqser/common-ui'; import { LoadingService, wipeCache } from '@iqser/common-ui';
import { getParam } from '@iqser/common-ui/lib/utils'; import { getParam } from '@iqser/common-ui/lib/utils';
@ -45,6 +45,7 @@ export class FilePreviewStateService {
readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly fileId = getParam(FILE_ID); readonly fileId = getParam(FILE_ID);
readonly updateExcludedPagesStyle = computed(() => this.file().excludedPages); readonly updateExcludedPagesStyle = computed(() => this.file().excludedPages);
readonly isEditingReviewer = signal(false);
constructor( constructor(
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,

View File

@ -18,7 +18,7 @@ import type {
import { dictionaryActionsTranslations, manualRedactionActionsTranslations } from '@translations/annotation-actions-translations'; import { dictionaryActionsTranslations, manualRedactionActionsTranslations } from '@translations/annotation-actions-translations';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { EMPTY, of, OperatorFunction, pipe } from 'rxjs'; import { EMPTY, of, pipe } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
function getResponseType(error: boolean, isConflict: boolean) { function getResponseType(error: boolean, isConflict: boolean) {
@ -56,18 +56,24 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
value: annotation.value, value: annotation.value,
reason: annotation.legalBasis ?? 'Dictionary Request', reason: annotation.legalBasis ?? 'Dictionary Request',
positions: annotation.positions, positions: annotation.positions,
type: redaction.type, type: redaction.type ?? annotation.type,
comment: redaction.comment, comment: redaction.comment,
})); }));
return this.addAnnotation(recommendations, dossierId, fileId); return this.addAnnotation(recommendations, dossierId, fileId);
} }
changeLegalBasis(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) { recategorizeRedactions(
return this.legalBasisChange(body, dossierId, fileId, includeUnprocessed).pipe(this.#showToast('change-legal-basis')); body: List<IRecategorizationRequest>,
} dossierId: string,
fileId: string,
recategorizeRedactions(body: List<IRecategorizationRequest>, dossierId: string, fileId: string, includeUnprocessed = false) { successMessageParameters?: {
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(this.#showToast('change-type')); [key: string]: string;
},
includeUnprocessed = false,
) {
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(
this.#showToast('recategorize-annotation', false, successMessageParameters),
);
} }
addAnnotation( addAnnotation(
@ -131,13 +137,6 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
); );
} }
legalBasisChange(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
return this._post(
body,
`${this.#bulkRedaction}/legalBasisChange/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`,
).pipe(this.#log('Legal basis change', body));
}
undo(annotationIds: List, dossierId: string, fileId: string) { undo(annotationIds: List, dossierId: string, fileId: string) {
const url = `${this._defaultModelPath}/bulk/undo/${dossierId}/${fileId}`; const url = `${this._defaultModelPath}/bulk/undo/${dossierId}/${fileId}`;
return super.delete(annotationIds, url).pipe(this.#log('Undo', annotationIds)); return super.delete(annotationIds, url).pipe(this.#log('Undo', annotationIds));
@ -165,7 +164,11 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
}); });
} }
#showToast(action: ManualRedactionActions | DictionaryActions, isDictionary = false) { #showToast(
action: ManualRedactionActions | DictionaryActions,
isDictionary = false,
successMessageParameters?: { [key: string]: string },
) {
return pipe( return pipe(
catchError((error: unknown) => { catchError((error: unknown) => {
const isConflict = (error as HttpErrorResponse).status === HttpStatusCode.Conflict; const isConflict = (error as HttpErrorResponse).status === HttpStatusCode.Conflict;
@ -175,7 +178,12 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
}); });
return EMPTY; return EMPTY;
}), }),
tap(() => this._toaster.success(getMessage(action, isDictionary), { positionClass: 'toast-file-preview' })), tap(() =>
this._toaster.success(getMessage(action, isDictionary), {
params: successMessageParameters,
positionClass: 'toast-file-preview',
}),
),
); );
} }

View File

@ -36,7 +36,7 @@ export class PdfAnnotationActionsService {
let acceptResizeButton: IHeaderElement; let acceptResizeButton: IHeaderElement;
if (this.#annotationManager.annotationHasBeenResized) { if (this.#annotationManager.annotationHasBeenResized) {
acceptResizeButton = this.#getButton('check', _('annotation-actions.resize-accept.label'), () => acceptResizeButton = this.#getButton('check', _('annotation-actions.resize-accept.label'), () =>
this.#annotationActionsService.acceptResize(firstAnnotation), this.#annotationActionsService.acceptResize(firstAnnotation, permissions),
); );
} else { } else {
acceptResizeButton = this.#getDisabledCheckButton(); acceptResizeButton = this.#getDisabledCheckButton();

View File

@ -396,8 +396,9 @@ export class PdfProxyService {
} }
const annotationChangesAllowed = !this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis; const annotationChangesAllowed = !this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis;
const somePending = annotationWrappers.some(a => a.pending);
actions = actions =
this._multiSelectService.inactive() && !this._documentViewer.selectedText.length this._multiSelectService.inactive() && !this._documentViewer.selectedText.length && !somePending
? [...actions, ...this._pdfAnnotationActionsService.get(annotationWrappers, annotationChangesAllowed)] ? [...actions, ...this._pdfAnnotationActionsService.get(annotationWrappers, annotationChangesAllowed)]
: []; : [];
this._pdf.instance.UI.annotationPopup.update(actions); this._pdf.instance.UI.annotationPopup.update(actions);

View File

@ -113,6 +113,7 @@ export const getResizeRedactionOptions = (
isRss: boolean, isRss: boolean,
applyToAllDossiers: boolean, applyToAllDossiers: boolean,
isApprover: boolean, isApprover: boolean,
canResizeInDictionary: boolean,
): DetailsRadioOption<ResizeRedactionOption>[] => { ): DetailsRadioOption<ResizeRedactionOption>[] => {
const translations = resizeRedactionTranslations; const translations = resizeRedactionTranslations;
const options: DetailsRadioOption<ResizeRedactionOption>[] = [ const options: DetailsRadioOption<ResizeRedactionOption>[] = [
@ -127,22 +128,23 @@ export const getResizeRedactionOptions = (
if (isRss) { if (isRss) {
return options; return options;
} }
if (canResizeInDictionary) {
const dictBasedType = redaction.isModifyDictionary; const dictBasedType = redaction.isModifyDictionary;
options.push({ options.push({
label: translations.inDossier.label, label: translations.inDossier.label,
description: translations.inDossier.description, description: translations.inDossier.description,
descriptionParams: { dossierName: dossier.dossierName }, descriptionParams: { dossierName: dossier.dossierName },
disabled: !dictBasedType || redaction.hasBeenRecategorized, disabled: !dictBasedType || redaction.hasBeenRecategorized,
tooltip: !dictBasedType ? translations.inDossier.tooltip : null, tooltip: !dictBasedType ? translations.inDossier.tooltip : null,
icon: FOLDER_ICON, icon: FOLDER_ICON,
value: RedactOrHintOptions.IN_DOSSIER, value: RedactOrHintOptions.IN_DOSSIER,
extraOption: { extraOption: {
label: translations.inDossier.extraOptionLabel, label: translations.inDossier.extraOptionLabel,
checked: applyToAllDossiers, checked: applyToAllDossiers,
hidden: !isApprover, hidden: !isApprover,
}, },
}); });
}
return options; return options;
}; };
@ -153,6 +155,7 @@ export const getRemoveRedactionOptions = (
const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations; const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations;
const { permissions, redactions, applyToAllDossiers, isApprover, falsePositiveContext } = data; const { permissions, redactions, applyToAllDossiers, isApprover, falsePositiveContext } = data;
const isBulk = redactions.length > 1; const isBulk = redactions.length > 1;
const hasReasonChanged = redactions.some(r => r.hasLegalBasisChanged || r.hasBeenRecategorized);
const options: DetailsRadioOption<RemoveRedactionOption>[] = []; const options: DetailsRadioOption<RemoveRedactionOption>[] = [];
if (permissions.canRemoveOnlyHere) { if (permissions.canRemoveOnlyHere) {
@ -167,7 +170,7 @@ export const getRemoveRedactionOptions = (
value: RemoveRedactionOptions.ONLY_HERE, value: RemoveRedactionOptions.ONLY_HERE,
}); });
} }
if (permissions.canRemoveFromDictionary) { if (permissions.canRemoveFromDictionary && !hasReasonChanged) {
options.push({ options.push({
label: isBulk ? translations.IN_DOSSIER.labelBulk : translations.IN_DOSSIER.label, label: isBulk ? translations.IN_DOSSIER.labelBulk : translations.IN_DOSSIER.label,
description: isBulk ? translations.IN_DOSSIER.descriptionBulk : translations.IN_DOSSIER.description, description: isBulk ? translations.IN_DOSSIER.descriptionBulk : translations.IN_DOSSIER.description,
@ -177,14 +180,14 @@ export const getRemoveRedactionOptions = (
extraOption: !isDocumine extraOption: !isDocumine
? { ? {
label: translations.IN_DOSSIER.extraOptionLabel, label: translations.IN_DOSSIER.extraOptionLabel,
checked: applyToAllDossiers ?? true, checked: redactions[0].isRecommendation && applyToAllDossiers,
hidden: !isApprover, hidden: !isApprover,
} }
: null, : null,
}); });
} }
if (permissions.canMarkAsFalsePositive) { if (permissions.canMarkAsFalsePositive) {
if (data.redactions[0].isRecommendation) { if (redactions[0].isRecommendation) {
options.push({ options.push({
label: translations.DO_NOT_RECOMMEND.label, label: translations.DO_NOT_RECOMMEND.label,
description: isBulk ? translations.DO_NOT_RECOMMEND.descriptionBulk : translations.DO_NOT_RECOMMEND.description, description: isBulk ? translations.DO_NOT_RECOMMEND.descriptionBulk : translations.DO_NOT_RECOMMEND.description,
@ -198,7 +201,7 @@ export const getRemoveRedactionOptions = (
extraOption: !isDocumine extraOption: !isDocumine
? { ? {
label: translations.DO_NOT_RECOMMEND.extraOptionLabel, label: translations.DO_NOT_RECOMMEND.extraOptionLabel,
checked: applyToAllDossiers ?? true, checked: applyToAllDossiers,
hidden: !isApprover, hidden: !isApprover,
} }
: null, : null,
@ -217,8 +220,9 @@ export const getRemoveRedactionOptions = (
extraOption: !isDocumine extraOption: !isDocumine
? { ? {
label: translations.FALSE_POSITIVE.extraOptionLabel, label: translations.FALSE_POSITIVE.extraOptionLabel,
checked: applyToAllDossiers ?? true, checked: false,
hidden: !isApprover, hidden: !isApprover,
description: translations.FALSE_POSITIVE.extraOptionDescription,
} }
: null, : null,
}); });

View File

@ -46,6 +46,11 @@ export interface EditRedactResult {
export type AddHintResult = RedactTextResult; export type AddHintResult = RedactTextResult;
export type AddAnnotationResult = RedactTextResult; export type AddAnnotationResult = RedactTextResult;
export interface ResizeRedactionPermissions {
canResizeAnnotation: boolean;
canResizeInDictionary: boolean;
}
export interface ResizeAnnotationData { export interface ResizeAnnotationData {
redaction: AnnotationWrapper; redaction: AnnotationWrapper;
text: string; text: string;
@ -55,6 +60,7 @@ export interface ResizeAnnotationData {
export interface ResizeRedactionData extends ResizeAnnotationData { export interface ResizeRedactionData extends ResizeAnnotationData {
applyToAllDossiers?: boolean; applyToAllDossiers?: boolean;
isApprover?: boolean; isApprover?: boolean;
permissions: ResizeRedactionPermissions;
} }
export interface ResizeAnnotationResult { export interface ResizeAnnotationResult {

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { List } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core } from '@pdftron/webviewer'; import { Core } from '@pdftron/webviewer';
import { IRectangle, ISectionRectangle, SuperTypes } from '@red/domain'; import { IPoint, IRectangle, ISectionRectangle, SuperTypes } from '@red/domain';
import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { hexToRgb } from '@utils/functions'; import { hexToRgb } from '@utils/functions';
import { BoundingBox, Table } from '../../file-preview/services/tables.service'; import { BoundingBox, Table } from '../../file-preview/services/tables.service';
@ -128,6 +128,27 @@ export class AnnotationDrawService {
return; return;
} }
if (annotationWrapper.positions.length === 0) {
return;
}
if (annotationWrapper.pending && annotationWrapper.id.includes('pending')) {
const polylineAnnot = this._pdf.polyline();
polylineAnnot.ReadOnly = true;
polylineAnnot.StrokeColor = this.convertColor(annotationWrapper.color);
polylineAnnot.StrokeThickness = 2;
polylineAnnot.Id = annotationWrapper.id;
polylineAnnot.PageNumber = pageNumber;
polylineAnnot.Opacity = polylineAnnot.Id.endsWith('pending') ? 0 : 0.5;
const points = this.#computePolylinePoints(annotationWrapper.positions, annotationWrapper.pageNumber);
for (let i = 0; i < points.length; i++) {
polylineAnnot.addPathPoint(points[i].x, points[i].y);
}
return polylineAnnot;
}
if (annotationWrapper.superType === SuperTypes.TextHighlight) { if (annotationWrapper.superType === SuperTypes.TextHighlight) {
const rectangleAnnot = this._pdf.rectangle(); const rectangleAnnot = this._pdf.rectangle();
const pageHeight = this._documentViewer.getHeight(pageNumber); const pageHeight = this._documentViewer.getHeight(pageNumber);
@ -178,24 +199,59 @@ export class AnnotationDrawService {
return annotation; return annotation;
} }
#computePolylinePoints(positions: IRectangle[], pageNumber: number): IPoint[] {
const pageHeight = this._documentViewer.getHeight(pageNumber);
const points = [this.#topLeft(positions[0], pageHeight)];
for (let i = 0; i < positions.length; i++) {
points.push(this.#topRight(positions[i], pageHeight));
points.push(this.#bottomRight(positions[i], pageHeight));
}
for (let i = positions.length - 1; i >= 0; i--) {
points.push(this.#bottomLeft(positions[i], pageHeight));
points.push(this.#topLeft(positions[i], pageHeight));
}
points.push(this.#topRight(positions[0], pageHeight));
return points;
}
#rectanglesToQuads(positions: IRectangle[], pageNumber: number): Quad[] { #rectanglesToQuads(positions: IRectangle[], pageNumber: number): Quad[] {
const pageHeight = this._documentViewer.getHeight(pageNumber); const pageHeight = this._documentViewer.getHeight(pageNumber);
return positions.map(p => this.#rectangleToQuad(p, pageHeight)); return positions.map(p => this.#rectangleToQuad(p, pageHeight));
} }
#rectangleToQuad(rectangle: IRectangle, pageHeight: number): Quad { #rectangleToQuad(rectangle: IRectangle, pageHeight: number): Quad {
const x1 = rectangle.topLeft.x; const { x: x1, y: y1 } = this.#topLeft(rectangle, pageHeight);
const y1 = pageHeight - (rectangle.topLeft.y + rectangle.height); const { x: x2, y: y2 } = this.#topRight(rectangle, pageHeight);
const { x: x3, y: y3 } = this.#bottomRight(rectangle, pageHeight);
const x2 = rectangle.topLeft.x + rectangle.width; const { x: x4, y: y4 } = this.#bottomLeft(rectangle, pageHeight);
const y2 = pageHeight - (rectangle.topLeft.y + rectangle.height);
const x3 = rectangle.topLeft.x + rectangle.width;
const y3 = pageHeight - rectangle.topLeft.y;
const x4 = rectangle.topLeft.x;
const y4 = pageHeight - rectangle.topLeft.y;
return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4); return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4);
} }
#topLeft(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x;
const y = pageHeight - (rectangle.topLeft.y + rectangle.height);
return { x, y };
}
#topRight(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x + rectangle.width;
const y = pageHeight - (rectangle.topLeft.y + rectangle.height);
return { x, y };
}
#bottomRight(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x + rectangle.width;
const y = pageHeight - rectangle.topLeft.y;
return { x, y };
}
#bottomLeft(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x;
const y = pageHeight - rectangle.topLeft.y;
return { x, y };
}
} }

View File

@ -140,12 +140,16 @@ export class REDAnnotationManager {
#listenForAnnotationSelected() { #listenForAnnotationSelected() {
this.#manager.addEventListener('annotationSelected', (annotations: Annotation[], action: string) => { this.#manager.addEventListener('annotationSelected', (annotations: Annotation[], action: string) => {
this.#logger.info('[PDF] Annotation selected: ', annotations, action); this.#logger.info('[PDF] Annotation selected: ', annotations, action);
this.#annotationSelected$.next([annotations, action]);
const annotation = annotations.length === 1 ? annotations[0] : undefined; const annotation = annotations.length === 1 ? annotations[0] : undefined;
if (annotation && annotation.ToolName === AnnotationToolNames.AnnotationCreateRectangle && action === 'deselected') { if (
annotation &&
annotation.ToolName === AnnotationToolNames.AnnotationCreateRectangle &&
action === 'deselected' &&
!this.resizingAnnotationId
) {
this.delete(annotation.Id); this.delete(annotation.Id);
} }
this.#annotationSelected$.next([annotations, action]);
}); });
} }

View File

@ -242,6 +242,10 @@ export class PdfViewer {
return new this.#instance.Core.Annotations.TextHighlightAnnotation(); return new this.#instance.Core.Annotations.TextHighlightAnnotation();
} }
polyline() {
return new this.#instance.Core.Annotations.PolylineAnnotation();
}
isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation { isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation {
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation; return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
} }
@ -323,15 +327,8 @@ export class PdfViewer {
return; return;
} }
const inputElement = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement; const clearSearchButton = iframeWindow.document.getElementsByClassName('clearSearch-button')[0] as HTMLButtonElement;
clearSearchButton?.click();
if (inputElement) {
setTimeout(() => (inputElement.value = ''), 0);
}
if (!event.detail.isVisible) {
this.documentViewer.clearSearchResults();
}
}); });
} }

View File

@ -265,6 +265,7 @@ export class FileActionsComponent implements OnChanges {
tooltip: _('dossier-overview.ocr-file'), tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr', icon: 'iqser:ocr',
show: this.showOCR, show: this.showOCR,
helpModeKey: 'automatic_text_recognition',
}, },
{ {
id: 'reanalyse-file-btn', id: 'reanalyse-file-btn',

View File

@ -0,0 +1,33 @@
<section class="dialog">
<div
[innerHTML]="'edit-dossier-dialog.dictionary.edit-dialog.title' | translate: { label: data.label }"
class="dialog-header heading-l"
></div>
<form (submit)="save()" [formGroup]="form">
<div class="dialog-content">
<div class="iqser-input-group w-300">
<mat-checkbox formControlName="addToDictionaryAction">
{{ 'edit-dossier-dialog.dictionary.edit-dialog.add-to-dictionary-action' | translate }}
</mat-checkbox>
</div>
</div>
<div class="dialog-actions" *ngIf="data.canEdit">
<iqser-icon-button
[disabled]="!form.valid || !changed"
[label]="'edit-dossier-dialog.dictionary.edit-dialog.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<div
[translate]="'edit-dossier-dialog.dictionary.edit-dialog.cancel'"
class="all-caps-label pointer cancel"
mat-dialog-close
></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
</section>

View File

@ -0,0 +1,64 @@
import { Component, HostListener } from '@angular/core';
import { IconButtonTypes, IqserDialogComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { FormBuilder } from '@angular/forms';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
interface DialogData {
addToDictionaryAction: boolean;
label: string;
type: string;
dossierTemplateId: string;
dossierId: string;
canEdit: boolean;
}
interface ReturnType {
addToDictionaryAction: boolean;
}
@Component({
selector: 'redaction-edit-dictionary-dialog',
templateUrl: './edit-dictionary-dialog.component.html',
})
export class EditDictionaryDialogComponent extends IqserDialogComponent<EditDictionaryDialogComponent, DialogData, ReturnType> {
readonly form = this._formBuilder.group({
addToDictionaryAction: [{ value: this.data.addToDictionaryAction, disabled: !this.data.canEdit }],
});
readonly initialFormValue = this.form.getRawValue();
constructor(
private _formBuilder: FormBuilder,
private _dictionaryService: DictionaryService,
private _loadingService: LoadingService,
private _toaster: Toaster,
) {
super();
}
@HostListener('window:keydown.Escape')
closeOnEscape(): void {
this.close();
}
async save() {
this._loadingService.start();
try {
await this._dictionaryService.updateFlag(
this.data.dossierTemplateId,
this.data.type,
this.data.dossierId,
this.form.controls.addToDictionaryAction.value,
);
this.close(this.form.value as ReturnType);
} catch (error) {
console.error(error);
this._toaster.error(_('edit-dossier-dialog.dictionary.edit-dialog.error.generic'));
this.close();
}
this._loadingService.stop();
}
protected readonly iconButtonTypes = IconButtonTypes;
}

View File

@ -39,7 +39,7 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
if (!dayjs(attr.value).isSame(dayjs(this.currentAttrValue(attr)), 'day')) { if (!dayjs(attr.value).isSame(dayjs(this.currentAttrValue(attr)), 'day')) {
return true; return true;
} }
} else if (this._parseAttrValue(attr.value) !== this._parseAttrValue(this.currentAttrValue(attr))) { } else if (this.#parseAttrValue(attr.value) !== this.#parseAttrValue(this.currentAttrValue(attr))) {
return true; return true;
} }
} }
@ -57,8 +57,8 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
async ngOnInit() { async ngOnInit() {
this._loadingService.start(); this._loadingService.start();
await this._loadAttributes(); await this.#loadAttributes();
this.form = this._getForm(); this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue(); this.initialFormValue = this.form.getRawValue();
this._loadingService.stop(); this._loadingService.stop();
} }
@ -74,7 +74,7 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
try { try {
await firstValueFrom(this._dossierAttributesService.setAttributes(this.dossier, dossierAttributeList)); await firstValueFrom(this._dossierAttributesService.setAttributes(this.dossier, dossierAttributeList));
await firstValueFrom(this._dossierAttributesService.loadAll(this.dossier.dossierTemplateId)); await firstValueFrom(this._dossierAttributesService.loadAll(this.dossier.dossierTemplateId));
await this._loadAttributes(); await this.#loadAttributes();
this.initialFormValue = this.form.getRawValue(); this.initialFormValue = this.form.getRawValue();
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@ -84,7 +84,7 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
} }
fileInputClick(attr: DossierAttributeWithValue) { fileInputClick(attr: DossierAttributeWithValue) {
this._getFileInputById(attr.id).nativeElement.click(); this.#getFileInputById(attr.id).nativeElement.click();
} }
isSpecificType(attr: DossierAttributeWithValue, type: DossierAttributeConfigType): boolean { isSpecificType(attr: DossierAttributeWithValue, type: DossierAttributeConfigType): boolean {
@ -105,11 +105,11 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
this.form.patchValue({ this.form.patchValue({
[attr.id]: result, [attr.id]: result,
}); });
this._getFileInputById(attr.id).nativeElement.value = null; this.#getFileInputById(attr.id).nativeElement.value = null;
} }
revert() { revert() {
this._loadAttributes().then(() => this.form.reset(this.initialFormValue)); this.#loadAttributes().then(() => this.form.reset(this.initialFormValue));
} }
currentAttrValue(attr: DossierAttributeWithValue): string { currentAttrValue(attr: DossierAttributeWithValue): string {
@ -122,21 +122,21 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
}); });
} }
private _getFileInputById(id: string): ElementRef { #getFileInputById(id: string): ElementRef {
return this._fileInputs.find(el => el.nativeElement.id === id); return this._fileInputs.find(el => el.nativeElement.name === id);
} }
private _parseAttrValue(value: any) { #parseAttrValue(value: any) {
return [null, undefined, ''].includes(value) ? undefined : value; return [null, undefined, ''].includes(value) ? undefined : value;
} }
private async _loadAttributes() { async #loadAttributes() {
this.attributes = await this._dossierAttributesService.getWithValues(this.dossier); this.attributes = await this._dossierAttributesService.getWithValues(this.dossier);
this.customAttributes = this.attributes.filter(attr => !this.isSpecificType(attr, this.dossierAttributeConfigTypes.IMAGE)); this.customAttributes = this.attributes.filter(attr => !this.isSpecificType(attr, this.dossierAttributeConfigTypes.IMAGE));
this.imageAttributes = this.attributes.filter(attr => this.isSpecificType(attr, this.dossierAttributeConfigTypes.IMAGE)); this.imageAttributes = this.attributes.filter(attr => this.isSpecificType(attr, this.dossierAttributeConfigTypes.IMAGE));
} }
private _getForm(): UntypedFormGroup { #getForm(): UntypedFormGroup {
const controlsConfig = {}; const controlsConfig = {};
for (const attribute of this.attributes) { for (const attribute of this.attributes) {
controlsConfig[attribute.id] = [{ value: attribute.value, disabled: this.disabled }]; controlsConfig[attribute.id] = [{ value: attribute.value, disabled: this.disabled }];

View File

@ -35,6 +35,14 @@
<div class="heading"> <div class="heading">
<div class="flex-align-items-center"> <div class="flex-align-items-center">
{{ selectedDictionary?.label }} {{ selectedDictionary?.label }}
<iqser-circle-button
*ngIf="selectedDictionary.dossierDictionaryOnly && selectedDictionary.hasDictionary"
(action)="openEditDictionaryModal()"
[size]="20"
[tooltip]="'edit-dossier-dialog.dictionary.edit-button-tooltip' | translate"
icon="iqser:edit"
class="p-left-8"
></iqser-circle-button>
</div> </div>
<div class="small-label stats-subtitle"> <div class="small-label stats-subtitle">
<div> <div>

View File

@ -1,13 +1,13 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { LoadingService } from '@iqser/common-ui'; import { IqserDialog, LoadingService } from '@iqser/common-ui';
import { List } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, Dossier } from '@red/domain'; import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, Dossier } from '@red/domain';
import { DictionaryService } from '@services/entity-services/dictionary.service'; import { DictionaryService } from '@services/entity-services/dictionary.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component'; import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { EditDossierSaveResult } from '../edit-dossier-section.interface'; import { EditDossierSaveResult } from '../edit-dossier-section.interface';
import { EditDictionaryDialogComponent } from '../../edit-dictionary-dialog/edit-dictionary-dialog.component';
@Component({ @Component({
selector: 'redaction-edit-dossier-dictionary', selector: 'redaction-edit-dossier-dictionary',
@ -17,6 +17,7 @@ import { EditDossierSaveResult } from '../edit-dossier-section.interface';
export class EditDossierDictionaryComponent implements OnInit { export class EditDossierDictionaryComponent implements OnInit {
@Input() dossier: Dossier; @Input() dossier: Dossier;
canEdit = false; canEdit = false;
canEditDictionaryFlag = false;
dictionaries: Dictionary[]; dictionaries: Dictionary[];
selectedDictionary: Dictionary; selectedDictionary: Dictionary;
activeEntryType = DictionaryEntryTypes.ENTRY; activeEntryType = DictionaryEntryTypes.ENTRY;
@ -40,12 +41,13 @@ export class EditDossierDictionaryComponent implements OnInit {
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _dialogService: DossiersDialogService, private readonly _iqserDialog: IqserDialog,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this._loadingService.start(); this._loadingService.start();
this.canEdit = this._permissionsService.canEditDossierDictionary(this.dossier); this.canEdit = this._permissionsService.canEditDossierDictionary(this.dossier);
this.canEditDictionaryFlag = this._permissionsService.isOwner(this.dossier);
await this.#updateDossierDictionary(); await this.#updateDossierDictionary();
this._loadingService.stop(); this._loadingService.stop();
} }
@ -101,19 +103,29 @@ export class EditDossierDictionaryComponent implements OnInit {
} }
} }
async #updateDossierDictionary() { async openEditDictionaryModal() {
const { dossierId, dossierTemplateId } = this.dossier; const { dossierId, dossierTemplateId } = this.dossier;
let dictionaryTypes = [ const result = await this._iqserDialog
...this._dictionaryService.getRedactTextDictionaries(dossierTemplateId, true), .openDefault(EditDictionaryDialogComponent, {
...this._dictionaryService.getAddHintDictionaries(dossierTemplateId, false, true), data: {
].map(d => d.type); addToDictionaryAction: this.selectedDictionary.addToDictionaryAction,
dictionaryTypes = [...new Set(dictionaryTypes)]; label: this.selectedDictionary.label,
this.dictionaries = await firstValueFrom( type: this.selectedDictionary.type,
this._dictionaryService.loadDictionaryEntriesByType(dictionaryTypes, dossierTemplateId, dossierId), dossierId,
); dossierTemplateId,
//TODO remove this when backend will send also the type canEdit: this.canEditDictionaryFlag,
this.#setType(dictionaryTypes); },
this.dictionaries = this.dictionaries.sort((a, b) => a.label.localeCompare(b.label)); })
.result();
if (result) {
this.selectedDictionary = { ...this.selectedDictionary, addToDictionaryAction: result.addToDictionaryAction } as Dictionary;
}
await this.#retrieveDictionaries();
}
async #updateDossierDictionary() {
await this.#retrieveDictionaries();
let dictionaryToSelect = this.dictionaries[0]; let dictionaryToSelect = this.dictionaries[0];
if (this.selectedDictionary) { if (this.selectedDictionary) {
dictionaryToSelect = this.dictionaries.find(d => d.type === this.selectedDictionary.type); dictionaryToSelect = this.dictionaries.find(d => d.type === this.selectedDictionary.type);
@ -121,6 +133,17 @@ export class EditDossierDictionaryComponent implements OnInit {
this.selectDictionary(dictionaryToSelect, this.activeEntryType); this.selectDictionary(dictionaryToSelect, this.activeEntryType);
} }
async #retrieveDictionaries() {
const { dossierId, dossierTemplateId } = this.dossier;
const dictionaryTypes = [...new Set(this._dictionaryService.getDictionaries(dossierTemplateId).map(d => d.type))];
this.dictionaries = await firstValueFrom(
this._dictionaryService.loadDictionaryEntriesByType(dictionaryTypes, dossierTemplateId, dossierId),
);
//TODO remove this when backend will send also the type
this.#setType(dictionaryTypes);
this.dictionaries.sort((a, b) => a.label.localeCompare(b.label));
}
//TODO remove this when backend will send also the type //TODO remove this when backend will send also the type
#setType(dictionaryTypes: string[]) { #setType(dictionaryTypes: string[]) {
for (let i = 0; i < this.dictionaries.length; i++) { for (let i = 0; i < this.dictionaries.length; i++) {

View File

@ -29,6 +29,7 @@ import { DossiersListingActionsComponent } from './components/dossiers-listing-a
import { IqserUsersModule } from '@iqser/common-ui/lib/users'; import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { SideNavComponent, SmallChipComponent, StatusBarComponent } from '@iqser/common-ui/lib/shared'; import { SideNavComponent, SmallChipComponent, StatusBarComponent } from '@iqser/common-ui/lib/shared';
import { SelectComponent } from '@shared/components/select/select.component'; import { SelectComponent } from '@shared/components/select/select.component';
import { EditDictionaryDialogComponent } from './dialogs/edit-dictionary-dialog/edit-dictionary-dialog.component';
const components = [ const components = [
FileActionsComponent, FileActionsComponent,
@ -40,6 +41,7 @@ const components = [
FileActionsComponent, FileActionsComponent,
DateColumnComponent, DateColumnComponent,
DossiersListingActionsComponent, DossiersListingActionsComponent,
EditDictionaryDialogComponent,
]; ];
const dialogs = [EditDossierDialogComponent, AssignReviewerApproverDialogComponent]; const dialogs = [EditDossierDialogComponent, AssignReviewerApproverDialogComponent];

View File

@ -46,10 +46,6 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
super(); super();
} }
get #isDossierRedaction(): boolean {
return this.entity?.type === 'dossier_redaction';
}
get #isSystemManaged(): boolean { get #isSystemManaged(): boolean {
return !!this.entity?.systemManaged; return !!this.entity?.systemManaged;
} }
@ -132,28 +128,27 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
hexColor: [this.entity?.hexColor, [Validators.required, Validators.minLength(7)]], hexColor: [this.entity?.hexColor, [Validators.required, Validators.minLength(7)]],
recommendationHexColor: [this.entity?.recommendationHexColor, [Validators.required, Validators.minLength(7)]], recommendationHexColor: [this.entity?.recommendationHexColor, [Validators.required, Validators.minLength(7)]],
skippedHexColor: [this.entity?.skippedHexColor, [Validators.required, Validators.minLength(7)]], skippedHexColor: [this.entity?.skippedHexColor, [Validators.required, Validators.minLength(7)]],
type: [this.entity?.type],
description: [this.entity?.description],
rank: [{ value: this.entity?.rank, disabled: this.#isSystemManaged }, Validators.required],
hint: [{ value: !!this.entity?.hint, disabled: this.#isSystemManaged }],
hasDictionary: [
{
value: !!this.entity?.hasDictionary,
disabled: this.#isSystemManaged || (this.entity?.dossierDictionaryOnly && this.entity?.hasDictionary),
},
],
dossierDictionaryOnly: [{ value: !!this.entity?.dossierDictionaryOnly, disabled: this.#isSystemManaged || this.entity }],
caseSensitive: [{ value: !this.entity?.caseInsensitive, disabled: this.#isSystemManaged }],
manageEntriesInDictionaryEditorOnly: [
{
value: this.entity?.addToDictionaryAction,
disabled: this.#isSystemManaged,
},
],
}; };
if (!this.#isDossierRedaction) { if (!this.entity?.hint) {
controlsConfig = {
...controlsConfig,
type: [this.entity?.type],
description: [this.entity?.description],
rank: [{ value: this.entity?.rank, disabled: this.#isSystemManaged }, Validators.required],
hint: [{ value: !!this.entity?.hint, disabled: this.#isSystemManaged }],
hasDictionary: [{ value: !!this.entity?.hasDictionary, disabled: this.#isSystemManaged }],
dossierDictionaryOnly: [{ value: !!this.entity?.dossierDictionaryOnly, disabled: this.#isSystemManaged || this.entity }],
caseSensitive: [{ value: !this.entity?.caseInsensitive, disabled: this.#isSystemManaged }],
manageEntriesInDictionaryEditorOnly: [
{
value: this.entity?.addToDictionaryAction,
disabled: this.#isSystemManaged && !this.#isDossierRedaction,
},
],
};
}
if (!this.entity?.hint && !this.#isDossierRedaction) {
controlsConfig = { controlsConfig = {
...controlsConfig, ...controlsConfig,
defaultReason: [{ value: null, disabled: true }], defaultReason: [{ value: null, disabled: true }],
@ -165,20 +160,18 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
this.#initializeColors(form); this.#initializeColors(form);
this.technicalName$ = form.get('label').valueChanges.pipe(map((value: string) => this.#toTechnicalName(value))); this.technicalName$ = form.get('label').valueChanges.pipe(map((value: string) => this.#toTechnicalName(value)));
if (!this.#isDossierRedaction) { form.get('hint').valueChanges.subscribe(isHint => {
form.get('hint').valueChanges.subscribe(isHint => { if (isHint) {
if (isHint) { REDACTION_FIELDS.forEach(field => form.removeControl(field));
REDACTION_FIELDS.forEach(field => form.removeControl(field)); } else {
} else { form.addControl('defaultReason', new UntypedFormControl({ value: null, disabled: true }));
form.addControl('defaultReason', new UntypedFormControl({ value: null, disabled: true }));
}
});
if (!this.entity) {
form.get('label').valueChanges.subscribe((label: string) => {
form.get('type').setValue(this.#toTechnicalName(label));
});
} }
});
if (!this.entity) {
form.get('label').valueChanges.subscribe((label: string) => {
form.get('type').setValue(this.#toTechnicalName(label));
});
} }
if (this.readOnly) { if (this.readOnly) {
@ -191,7 +184,10 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
} }
#toTechnicalName(value: string) { #toTechnicalName(value: string) {
const existingTechnicalNames = this._dictionariesMapService.get(this.dossierTemplateId).map(dict => dict.type); const existingTechnicalNames = this._dictionariesMapService
.get(this.dossierTemplateId)
.filter(dict => !dict.virtual)
.map(dict => dict.type);
const baseTechnicalName = toSnakeCase(value.trim()); const baseTechnicalName = toSnakeCase(value.trim());
let technicalName = baseTechnicalName.replaceAll(/[^A-Za-z0-9_-]/g, ''); let technicalName = baseTechnicalName.replaceAll(/[^A-Za-z0-9_-]/g, '');
if (!technicalName.length && baseTechnicalName.length) { if (!technicalName.length && baseTechnicalName.length) {
@ -217,29 +213,21 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
const hasDictionary = !!this.form.get('hasDictionary')?.value; const hasDictionary = !!this.form.get('hasDictionary')?.value;
const dossierDictionaryOnly = !!this.form.get('dossierDictionaryOnly')?.value; const dossierDictionaryOnly = !!this.form.get('dossierDictionaryOnly')?.value;
const entity = { return {
...this.entity, ...this.entity,
label: this.form.get('label').value, label: this.form.get('label').value,
hexColor: this.form.get('hexColor').value, hexColor: this.form.get('hexColor').value,
recommendationHexColor: this.form.get('recommendationHexColor').value, recommendationHexColor: this.form.get('recommendationHexColor').value,
skippedHexColor: this.form.get('skippedHexColor').value, skippedHexColor: this.form.get('skippedHexColor').value,
dossierTemplateId: this.dossierTemplateId, dossierTemplateId: this.dossierTemplateId,
type: this.form.get('type').value,
description: this.form.get('description').value,
hint: this.#isHint,
rank: this.form.get('rank').value,
caseInsensitive: !this.form.get('caseSensitive').value,
hasDictionary,
addToDictionaryAction, addToDictionaryAction,
dossierDictionaryOnly, dossierDictionaryOnly,
}; };
if (this.entity?.type !== 'dossier_redaction') {
return {
...entity,
type: this.form.get('type').value,
description: this.form.get('description').value,
hint: this.#isHint,
rank: this.form.get('rank').value,
caseInsensitive: !this.form.get('caseSensitive').value,
hasDictionary,
};
}
return entity;
} }
} }

View File

@ -121,9 +121,11 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
set selectedDictionary(dictionary: Dictionary) { set selectedDictionary(dictionary: Dictionary) {
this.selectedDictionaryType = dictionary.type; if (dictionary.type) {
this.#dictionary = dictionary; this.selectedDictionaryType = dictionary.type;
this.#onDossierChanged(this.#dossier.dossierTemplateId).then(entries => this.#updateDiffEditorText(entries)); this.#dictionary = dictionary;
this.#onDossierChanged(this.#dossier.dossierTemplateId).then(entries => this.#updateDiffEditorText(entries));
}
} }
get optionNotSelected() { get optionNotSelected() {

View File

@ -1,17 +1,17 @@
<ng-container *ngIf="componentContext$ | async as ctx"> <div #filenameColumn>
<div> <div
<div [attr.help-mode-key]="'document_list'"
[attr.help-mode-key]="'document_list'" [class.error]="file.isError"
[class.error]="file.isError" [class.initial-processing]="file.isInitialProcessing || (ocrByDefault && !file.lastOCRTime)"
[class.initial-processing]="file.isInitialProcessing || (ocrByDefault && !file.lastOCRTime)" [matTooltip]="file.filename"
[matTooltip]="file.filename" class="table-item-title"
class="table-item-title" matTooltipPosition="above"
matTooltipPosition="above" >
> {{ file.filename }}
{{ file.filename }}
</div>
</div> </div>
</div>
<ng-container *ngIf="componentContext$ | async as ctx">
<div *ngIf="ctx.primaryAttribute" class="primary-attribute"> <div *ngIf="ctx.primaryAttribute" class="primary-attribute">
<div class="small-label" *ngIf="file?.softDeletedTime; else editableFileAttribute"> <div class="small-label" *ngIf="file?.softDeletedTime; else editableFileAttribute">
<div> <div>
@ -26,6 +26,7 @@
[dossier]="dossier" [dossier]="dossier"
[fileAttribute]="ctx.primaryAttribute" [fileAttribute]="ctx.primaryAttribute"
[fileNameColumn]="true" [fileNameColumn]="true"
[width]="width"
></redaction-file-attribute> ></redaction-file-attribute>
</ng-template> </ng-template>
</div> </div>

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service'; import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service';
import { Dossier, File, IFileAttributeConfig, TrashFile } from '@red/domain'; import { Dossier, File, IFileAttributeConfig, TrashFile } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@ -18,15 +28,18 @@ interface FileNameColumnContext {
}) })
export class FileNameColumnComponent extends ContextComponent<FileNameColumnContext> implements OnInit, OnChanges { export class FileNameColumnComponent extends ContextComponent<FileNameColumnContext> implements OnInit, OnChanges {
readonly #reloadAttribute = new ReplaySubject<void>(1); readonly #reloadAttribute = new ReplaySubject<void>(1);
@ViewChild('filenameColumn', { static: true }) filenameColumn: ElementRef;
@Input() file?: File | TrashFile; @Input() file?: File | TrashFile;
@Input() dossier: Dossier; @Input() dossier: Dossier;
@Input() dossierTemplateId: string; @Input() dossierTemplateId: string;
ocrByDefault: boolean; ocrByDefault: boolean;
width: number;
constructor( constructor(
private readonly _fileAttributeService: FileAttributesService, private readonly _fileAttributeService: FileAttributesService,
private readonly _primaryFileAttributeService: PrimaryFileAttributeService, private readonly _primaryFileAttributeService: PrimaryFileAttributeService,
private readonly _dossierTemplateService: DossierTemplatesService, private readonly _dossierTemplateService: DossierTemplatesService,
private readonly _changeDetectorRef: ChangeDetectorRef,
) { ) {
super(); super();
} }
@ -37,6 +50,10 @@ export class FileNameColumnComponent extends ContextComponent<FileNameColumnCont
map(() => this.#findPrimaryAttribute()), map(() => this.#findPrimaryAttribute()),
); );
super._initContext({ primaryAttribute: primaryAttribute$ }); super._initContext({ primaryAttribute: primaryAttribute$ });
const _observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
this.#updateItemWidth(entries[0]);
});
_observer.observe(this.filenameColumn.nativeElement);
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@ -57,4 +74,9 @@ export class FileNameColumnComponent extends ContextComponent<FileNameColumnCont
const fileAttributes = this._fileAttributeService.getFileAttributeConfig(this.dossierTemplateId); const fileAttributes = this._fileAttributeService.getFileAttributeConfig(this.dossierTemplateId);
return fileAttributes?.fileAttributeConfigs.find(a => a.primaryAttribute); return fileAttributes?.fileAttributeConfigs.find(a => a.primaryAttribute);
} }
#updateItemWidth(resizeObserverEntry: ResizeObserverEntry) {
this.width = resizeObserverEntry.contentRect.width;
this._changeDetectorRef.markForCheck();
}
} }

View File

@ -1,5 +1,5 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form [formGroup]="form">
<div [translate]="'add-dossier-dialog.header-new'" class="dialog-header heading-l"></div> <div [translate]="'add-dossier-dialog.header-new'" class="dialog-header heading-l"></div>
<div *ngIf="config.IS_DOCUMINE && !availableReportTypes.length" class="inline-dialog-toast toast-warning"> <div *ngIf="config.IS_DOCUMINE && !availableReportTypes.length" class="inline-dialog-toast toast-warning">

View File

@ -5,7 +5,7 @@ import { interval, Subject, Subscription } from 'rxjs';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain'; import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
import { isAcceptedFileType, isCsv, isZip } from '@utils/file-drop-utils'; import { isAcceptedFileType, isCsv, isDocument, isZip } from '@utils/file-drop-utils';
import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui'; import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui';
import { FilesMapService } from '@services/files/files-map.service'; import { FilesMapService } from '@services/files/files-map.service';
import { switchMap, tap, throttleTime } from 'rxjs/operators'; import { switchMap, tap, throttleTime } from 'rxjs/operators';
@ -13,6 +13,8 @@ import { FilesService } from '@services/files/files.service';
import { UploadDownloadDialogService } from './upload-download-dialog.service'; import { UploadDownloadDialogService } from './upload-download-dialog.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HeadersConfiguration } from '@iqser/common-ui/lib/utils'; import { HeadersConfiguration } from '@iqser/common-ui/lib/utils';
import { LicenseService } from '@services/license.service';
import { LicenseFeatures } from '../../admin/screens/license/utils/constants';
export interface ActiveUpload { export interface ActiveUpload {
subscription: Subscription; subscription: Subscription;
@ -39,6 +41,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
private readonly _configService: ConfigService, private readonly _configService: ConfigService,
private readonly _dialogService: UploadDownloadDialogService, private readonly _dialogService: UploadDownloadDialogService,
private readonly _errorMessageService: ErrorMessageService, private readonly _errorMessageService: ErrorMessageService,
private readonly _licenseService: LicenseService,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
) { ) {
super(); super();
@ -72,6 +75,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB; const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB;
const maxSizeBytes = maxSizeMB * 1024 * 1024; const maxSizeBytes = maxSizeMB * 1024 * 1024;
const dossierFiles = this._filesMapService.get(dossierId); const dossierFiles = this._filesMapService.get(dossierId);
const supportMsOfficeFormats = this._licenseService.getFeature(LicenseFeatures.SUPPORT_MS_OFFICE_FORMATS)?.value as boolean;
let option: OverwriteFileOption = localStorage.getItem('overwriteFileOption') as OverwriteFileOption; let option: OverwriteFileOption = localStorage.getItem('overwriteFileOption') as OverwriteFileOption;
for (let idx = 0; idx < files.length; ++idx) { for (let idx = 0; idx < files.length; ++idx) {
const file = files[idx]; const file = files[idx];
@ -115,7 +119,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
continue; continue;
} }
} }
if (!isAcceptedFileType(file)) { if (!isAcceptedFileType(file, supportMsOfficeFormats)) {
file.completed = true; file.completed = true;
file.error = { file.error = {
message: this._translateService.instant('upload-status.error.file-type'), message: this._translateService.instant('upload-status.error.file-type'),

View File

@ -10,6 +10,7 @@ import { inject, Injectable, OnDestroy } from '@angular/core';
import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service'; import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { List } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy { export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy {
@ -18,6 +19,7 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
readonly #archivedDossiersService = inject(ArchivedDossiersService); readonly #archivedDossiersService = inject(ArchivedDossiersService);
readonly #dashboardStatsService = inject(DashboardStatsService); readonly #dashboardStatsService = inject(DashboardStatsService);
readonly #logger = inject(NGXLogger); readonly #logger = inject(NGXLogger);
readonly #dossierCache = inject(DossiersCacheService);
protected readonly _defaultModelPath = 'dossier'; protected readonly _defaultModelPath = 'dossier';
loadOnlyChanged(): Observable<IDossierChanges> { loadOnlyChanged(): Observable<IDossierChanges> {
@ -72,6 +74,7 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
} }
#load(id: string): Observable<DossierStats[]> { #load(id: string): Observable<DossierStats[]> {
if (!this.#dossierCache.get(id)) return of([]);
const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }]; const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe( return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity)), map(entity => new Dossier(entity)),

View File

@ -1,13 +1,11 @@
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntitiesService, QueryParam, Toaster } from '@iqser/common-ui'; import { EntitiesService, QueryParam, Toaster } from '@iqser/common-ui';
import { List } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, IDictionary, IUpdateDictionary, SuperTypes } from '@red/domain'; import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, IDictionary, IUpdateDictionary, SuperTypes } from '@red/domain';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service'; import { firstValueFrom, forkJoin, Observable } from 'rxjs';
import { FALLBACK_COLOR } from '@utils/constants'; import { map, switchMap, tap } from 'rxjs/operators';
import { firstValueFrom, forkJoin, Observable, throwError } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { IMAGE_CATEGORIES } from '../../modules/file-preview/utils/constants'; import { IMAGE_CATEGORIES } from '../../modules/file-preview/utils/constants';
import { DictionariesMapService } from './dictionaries-map.service'; import { DictionariesMapService } from './dictionaries-map.service';
import { DossierTemplateStatsService } from './dossier-template-stats.service'; import { DossierTemplateStatsService } from './dossier-template-stats.service';
@ -25,7 +23,6 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService, private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
private readonly _dossierDictionariesMapService: DossierDictionariesMapService,
) { ) {
super(); super();
} }
@ -49,8 +46,13 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
} }
getAllDictionaries(dossierTemplateId: string, dossierId?: string) { getAllDictionaries(dossierTemplateId: string, dossierId?: string) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined; const queryParams = [];
return this._getOne<{ types: IDictionary[] }>(['type', dossierTemplateId], this._defaultModelPath, queryParams); if (dossierId) {
queryParams.push({ key: 'dossierId', value: dossierId });
}
return this._getOne<{ types: IDictionary[] }>(['type', dossierTemplateId], this._defaultModelPath, queryParams).pipe(
map(typesResponse => typesResponse.types.map(type => new Dictionary(type))),
);
} }
/** /**
@ -61,7 +63,6 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`; const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`;
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined; const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const request = this._post(body, url, queryParams).pipe( const request = this._post(body, url, queryParams).pipe(
catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)), switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)),
switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])), switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])),
); );
@ -69,10 +70,19 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return await firstValueFrom(request); return await firstValueFrom(request);
} }
async updateFlag(dossierTemplateId: string, type: string, dossierId: string, addToDictionary: boolean): Promise<unknown> {
const url = `${this._defaultModelPath}/updateFlag/${type}/${dossierTemplateId}/${dossierId}`;
const queryParams = [{ key: 'addToDictionary', value: addToDictionary }];
const request = this._post(null, url, queryParams).pipe(
switchMap(() => this.loadDictionaryDataForDossier(dossierTemplateId, dossierId)),
);
return await firstValueFrom(request);
}
async add(dictionary: IDictionary, dossierId?: string): Promise<unknown> { async add(dictionary: IDictionary, dossierId?: string): Promise<unknown> {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined; const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const request = this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe( const request = this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
switchMap(() => this.loadDictionaryDataForDossierTemplate(dictionary.dossierTemplateId)), switchMap(() => this.loadDictionaryDataForDossierTemplate(dictionary.dossierTemplateId)),
switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])), switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])),
); );
@ -137,9 +147,14 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === type && !e.virtual); return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === type && !e.virtual);
} }
getRedactTextDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean): Dictionary[] { getDictionaries(dossierTemplateId: string) {
return this._dictionariesMapService return this._dictionariesMapService
.get(dossierTemplateId) .get(dossierTemplateId)
.filter(d => d.model['typeId'] && (d.dossierDictionaryOnly || d.addToDictionaryAction));
}
getRedactTextDictionaries(dossierId: string, dossierDictionaryOnly: boolean): Dictionary[] {
return this.#extractDossierLevelTypes(dossierId)
.filter(d => d.model['typeId'] && !d.hint && d.addToDictionaryAction && (dossierDictionaryOnly || !d.dossierDictionaryOnly)) .filter(d => d.model['typeId'] && !d.hint && d.addToDictionaryAction && (dossierDictionaryOnly || !d.dossierDictionaryOnly))
.sort((a, b) => a.label.localeCompare(b.label)); .sort((a, b) => a.label.localeCompare(b.label));
} }
@ -157,10 +172,9 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
.sort((a, b) => a.label.localeCompare(b.label)); .sort((a, b) => a.label.localeCompare(b.label));
} }
getAddHintDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean, dictionaryRequest: boolean): Dictionary[] { getAddHintDictionaries(dossierId: string, dossierDictionaryOnly: boolean, dictionaryRequest: boolean): Dictionary[] {
const dictionaries: Dictionary[] = []; const dictionaries: Dictionary[] = [];
this.#extractDossierLevelTypes(dossierId).forEach((d: Dictionary) => {
this._dictionariesMapService.get(dossierTemplateId).forEach((d: Dictionary) => {
if (d.hint) { if (d.hint) {
if (dictionaryRequest) { if (dictionaryRequest) {
if (d.hasDictionary && d.addToDictionaryAction) { if (d.hasDictionary && d.addToDictionaryAction) {
@ -182,51 +196,17 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
} }
getRedactionTypes(dossierTemplateId: string): Dictionary[] { getRedactionTypes(dossierTemplateId: string): Dictionary[] {
const possibleDictionaries: Dictionary[] = []; return this._dictionariesMapService
.get(dossierTemplateId)
for (const dictionary of this._dictionariesMapService.get(dossierTemplateId)) { .filter(dictionary => !dictionary.virtual && !dictionary.hint && !dictionary.systemManaged)
if (!dictionary.virtual && !dictionary.hint && !dictionary.systemManaged) { .sort((a, b) => a.label.localeCompare(b.label));
possibleDictionaries.push(dictionary);
}
}
return possibleDictionaries.sort((a, b) => a.label.localeCompare(b.label));
} }
async getDictionariesOptions(dossierTemplateId: string, dossierId: string): Promise<Dictionary[]> { getDictionariesOptions(dossierTemplateId: string): Dictionary[] {
const possibleDictionaries: Dictionary[] = []; return this._dictionariesMapService
.get(dossierTemplateId)
const dossierDictionary: Dictionary = this._dossierDictionariesMapService.get(dossierId, 'dossier_redaction'); .filter(dictionary => !dictionary.virtual && dictionary.addToDictionaryAction)
.sort((a, b) => a.label.localeCompare(b.label));
for (const dictionary of this._dictionariesMapService.get(dossierTemplateId)) {
if (!dictionary.virtual && dictionary.addToDictionaryAction) {
possibleDictionaries.push(dictionary);
}
}
if (dossierDictionary?.addToDictionaryAction) {
possibleDictionaries.push(dossierDictionary);
}
possibleDictionaries.sort((a, b) => a.label.localeCompare(b.label));
return possibleDictionaries;
}
async loadDossierRedaction(dossierTemplateId: string, dossierId: string): Promise<Dictionary> {
const promise = this.getForType(dossierTemplateId, 'dossier_redaction', dossierId);
const dict = await promise.catch(() => undefined);
if (dict) {
const dictionary = new Dictionary({
...dict,
type: 'dossier_redaction',
});
this._dossierDictionariesMapService.set(dossierId, [dictionary]);
return dictionary;
}
this._dossierDictionariesMapService.set(dossierId, []);
return undefined;
} }
loadDictionaryData(dossierTemplatesIds: string[]): Observable<Dictionary[][]> { loadDictionaryData(dossierTemplatesIds: string[]): Observable<Dictionary[][]> {
@ -239,33 +219,16 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
loadDictionaryDataForDossierTemplate(dossierTemplateId: string): Observable<Dictionary[]> { loadDictionaryDataForDossierTemplate(dossierTemplateId: string): Observable<Dictionary[]> {
return this.getAllDictionaries(dossierTemplateId).pipe( return this.getAllDictionaries(dossierTemplateId).pipe(
map(typesResponse => typesResponse.types.map(type => new Dictionary(type))),
map(dictionaries => {
let manualTypeExists = false;
for (const dictionary of dictionaries) {
if (dictionary.type === SuperTypes.ManualRedaction) {
manualTypeExists = true;
break;
}
}
if (!manualTypeExists) {
dictionaries.push(
new Dictionary(
{
hexColor: FALLBACK_COLOR,
type: SuperTypes.ManualRedaction,
},
true,
),
);
}
return dictionaries;
}),
tap(dictionaries => this._dictionariesMapService.set(dossierTemplateId, dictionaries)), tap(dictionaries => this._dictionariesMapService.set(dossierTemplateId, dictionaries)),
); );
} }
loadDictionaryDataForDossier(dossierTemplateId: string, dossierId: string): Observable<Dictionary[]> {
return this.getAllDictionaries(dossierTemplateId, dossierId).pipe(
tap(dictionaries => this._dictionariesMapService.set(dossierId, dictionaries)),
);
}
loadDictionaryEntriesByType(types: string[], dossierTemplateId: string, dossierId: string): Observable<Dictionary[]> { loadDictionaryEntriesByType(types: string[], dossierTemplateId: string, dossierId: string): Observable<Dictionary[]> {
const queryParams = [{ key: 'dossierId', value: dossierId }]; const queryParams = [{ key: 'dossierId', value: dossierId }];
const requests = []; const requests = [];
@ -303,14 +266,7 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return firstValueFrom(this._post(entries, url, queryParams)); return firstValueFrom(this._post(entries, url, queryParams));
} }
#addUpdateDictionaryErrorToast(error: HttpErrorResponse | unknown): Observable<never> { #extractDossierLevelTypes(dossierId: string) {
if ((error as HttpErrorResponse).status === HttpStatusCode.Conflict) { return this._dictionariesMapService.get(dossierId).filter(dictionary => dictionary.model['typeId']?.includes(dossierId));
this._toaster.error(_('add-edit-entity.error.entity-already-exists'));
} else if ((error as HttpErrorResponse).status === HttpStatusCode.BadRequest) {
this._toaster.error(_('add-edit-entity.error.invalid-color-or-rank'));
} else {
this._toaster.error(_('add-edit-entity.error.generic'));
}
return throwError(() => error);
} }
} }

View File

@ -115,10 +115,7 @@ export class TrashService extends EntitiesService<TrashItem, TrashItem> {
} }
private _hardDeleteFiles(dossierId: string, fileIds: List) { private _hardDeleteFiles(dossierId: string, fileIds: List) {
const queryParams = fileIds.map<QueryParam>(id => ({ key: 'fileIds', value: id })); return super._post(fileIds, `delete/hard-delete/${dossierId}`).pipe(switchMap(() => this._dossierStatsService.getFor([dossierId])));
return super
.delete({}, `delete/hard-delete/${dossierId}`, queryParams)
.pipe(switchMap(() => this._dossierStatsService.getFor([dossierId])));
} }
private _restoreFiles(dossierId: string, fileIds: List) { private _restoreFiles(dossierId: string, fileIds: List) {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { GenericService } from '@iqser/common-ui'; import { GenericService, isIqserDevMode, Toaster } from '@iqser/common-ui';
import { IEntityLog, ISectionGrid } from '@red/domain'; import { EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain';
import { firstValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -9,11 +9,14 @@ import { catchError } from 'rxjs/operators';
}) })
export class EntityLogService extends GenericService<unknown> { export class EntityLogService extends GenericService<unknown> {
protected readonly _defaultModelPath = ''; protected readonly _defaultModelPath = '';
readonly #isIqserDevMode = isIqserDevMode();
readonly #toaster = inject(Toaster);
async getEntityLog(dossierId: string, fileId: string) { async getEntityLog(dossierId: string, fileId: string) {
const queryParams = [{ key: 'includeUnprocessed', value: true }]; const queryParams = [{ key: 'includeUnprocessed', value: true }];
const entityLog$ = this._getOne<IEntityLog>([dossierId, fileId], 'entityLog', queryParams); const entityLog$ = this._getOne<IEntityLog>([dossierId, fileId], 'entityLog', queryParams);
const entityLog = await firstValueFrom(entityLog$.pipe(catchError(() => of({} as IEntityLog)))); const entityLog = await firstValueFrom(entityLog$.pipe(catchError(() => of({} as IEntityLog))));
entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry);
entityLog.entityLogEntry.sort((a, b) => a.positions[0].pageNumber - b.positions[0].pageNumber); entityLog.entityLogEntry.sort((a, b) => a.positions[0].pageNumber - b.positions[0].pageNumber);
return entityLog; return entityLog;
} }
@ -21,4 +24,16 @@ export class EntityLogService extends GenericService<unknown> {
getSectionGrid(dossierId: string, fileId: string) { getSectionGrid(dossierId: string, fileId: string) {
return this._getOne<ISectionGrid>([dossierId, fileId], 'sectionGrid'); return this._getOne<ISectionGrid>([dossierId, fileId], 'sectionGrid');
} }
#filterInvalidEntries(entityLogEntry: IEntityLogEntry[]) {
return entityLogEntry.filter(entry => {
entry.positions = entry.positions?.filter(p => !!p.rectangle?.length);
const hasPositions = !!entry.positions?.length;
const isRemoved = entry.state === EntryStates.REMOVED;
if (!hasPositions) {
this.#toaster.devInfo(`Entry ${entry.id} was skipped because it has no position`);
}
return hasPositions && !isRemoved;
});
}
} }

View File

@ -307,10 +307,7 @@ export class PermissionsService {
canEditDossierDictionary(dossier: Dossier): boolean { canEditDossierDictionary(dossier: Dossier): boolean {
return ( return (
dossier.isActive && dossier.isActive && this.isDossierMember(dossier) && this._iqserPermissionsService.has(Roles.dossiers.dictionaryEntries.write)
this._dossierDictionariesMapService.get(dossier.id).length > 0 &&
this.isDossierMember(dossier) &&
this._iqserPermissionsService.has(Roles.dossiers.dictionaryEntries.write)
); );
} }
@ -341,7 +338,7 @@ export class PermissionsService {
canDeleteComment(comment: IComment, file: File, dossier: Dossier) { canDeleteComment(comment: IComment, file: File, dossier: Dossier) {
return ( return (
this._iqserPermissionsService.has(Roles.comments.delete) && this._iqserPermissionsService.has(Roles.comments.delete) &&
(comment.user === this.#userId || this.isApprover(dossier)) && (comment.userId === this.#userId || this.isApprover(dossier)) &&
!file.isApproved !file.isApproved
); );
} }

View File

@ -28,10 +28,6 @@ export const manualRedactionActionsTranslations: Record<ManualRedactionActions,
error: _('annotation-actions.message.manual-redaction.add.error'), error: _('annotation-actions.message.manual-redaction.add.error'),
success: _('annotation-actions.message.manual-redaction.add.success'), success: _('annotation-actions.message.manual-redaction.add.success'),
}, },
'change-legal-basis': {
error: _('annotation-actions.message.manual-redaction.change-legal-basis.error'),
success: _('annotation-actions.message.manual-redaction.change-legal-basis.success'),
},
'force-redaction': { 'force-redaction': {
error: _('annotation-actions.message.manual-redaction.force-redaction.error'), error: _('annotation-actions.message.manual-redaction.force-redaction.error'),
success: _('annotation-actions.message.manual-redaction.force-redaction.success'), success: _('annotation-actions.message.manual-redaction.force-redaction.success'),
@ -44,9 +40,9 @@ export const manualRedactionActionsTranslations: Record<ManualRedactionActions,
error: _('annotation-actions.message.manual-redaction.recategorize-image.error'), error: _('annotation-actions.message.manual-redaction.recategorize-image.error'),
success: _('annotation-actions.message.manual-redaction.recategorize-image.success'), success: _('annotation-actions.message.manual-redaction.recategorize-image.success'),
}, },
'change-type': { 'recategorize-annotation': {
error: _('annotation-actions.message.manual-redaction.change-type.error'), error: _('annotation-actions.message.manual-redaction.recategorize-annotation.error'),
success: _('annotation-actions.message.manual-redaction.change-type.success'), success: _('annotation-actions.message.manual-redaction.recategorize-annotation.success'),
}, },
undo: { undo: {
error: _('annotation-actions.message.manual-redaction.undo.error'), error: _('annotation-actions.message.manual-redaction.undo.error'),

View File

@ -6,6 +6,7 @@ export interface DialogOption {
description: string; description: string;
descriptionBulk?: string; descriptionBulk?: string;
extraOptionLabel?: string; extraOptionLabel?: string;
extraOptionDescription?: string;
} }
export const redactTextTranslations: Record<'onlyHere' | 'inDossier', DialogOption> = { export const redactTextTranslations: Record<'onlyHere' | 'inDossier', DialogOption> = {

View File

@ -20,6 +20,7 @@ export const removeRedactionTranslations: { [key in RemoveRedactionOption]: Dial
description: _('remove-redaction.dialog.content.options.false-positive.description'), description: _('remove-redaction.dialog.content.options.false-positive.description'),
descriptionBulk: _('remove-redaction.dialog.content.options.false-positive.description-bulk'), descriptionBulk: _('remove-redaction.dialog.content.options.false-positive.description-bulk'),
extraOptionLabel: _('remove-redaction.dialog.content.options.false-positive.extraOptionLabel'), extraOptionLabel: _('remove-redaction.dialog.content.options.false-positive.extraOptionLabel'),
extraOptionDescription: _('remove-redaction.dialog.content.options.false-positive.extraOptionDescription'),
}, },
DO_NOT_RECOMMEND: { DO_NOT_RECOMMEND: {
label: _('remove-redaction.dialog.content.options.do-not-recommend.label'), label: _('remove-redaction.dialog.content.options.do-not-recommend.label'),

View File

@ -0,0 +1,6 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export const selectTenantTranslations: { [key in string]: string } = {
IS_LOGGED_OUT: _('tenant-resolve.header.youre-logged-out'),
NO_ROLE_LOG_OUT: _('tenant-resolve.header.no-role-log-out'),
} as const;

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