Compare commits

...

107 Commits

Author SHA1 Message Date
Nicoleta Panaghiu
9891b51e83 Merge branch 'RED-10433-bp' into 'release/4.951.x'
RED-10443 - 500 Error occurs when selecting ISO-8859-1 as Encoding Type for...

See merge request redactmanager/red-ui!715
2024-11-22 10:54:19 +01:00
corinaolariu
440384fa2d RED-10443 - 500 Error occurs when selecting ISO-8859-1 as Encoding Type for any CSV File Format - update - bp
- update also the key ISO encoding to ISO-8859-1
2024-11-22 11:52:11 +02:00
Nicoleta Panaghiu
3ab5ccfaec RED-10148: fixed text selection; implemented key listener for shift. 2024-11-21 18:13:48 +02:00
Nicoleta Panaghiu
d730875a45 RED-10405 - fixed toggle disabled state. 2024-11-21 17:27:25 +02:00
Nicoleta Panaghiu
87f7a8f394 RED-10484: removed multiplePages option for imported rectangles. 2024-11-21 14:53:04 +02:00
corinaolariu
b68127b35f RED-10443 - 500 Error occurs when selecting ISO-8859-1 as Encoding Type for any CSV File Format - bp
- update the value of ISO encoding to ISO-8859-1
2024-11-21 14:50:58 +02:00
Valentin Mihai
e4bd2eac67 RED-10483 - stop making additional dossier requests which leads to "Dossier not found" error when dossier is already deleted 2024-11-21 11:42:31 +02:00
Nicoleta Panaghiu
8cba3658f7 RED-10498 - added the locked rules indicator; implemented the reset 2024-11-20 17:28:55 +02:00
Valentin Mihai
883bda2efa RED-10396 - undefined checks 2024-11-20 11:39:21 +02:00
Nicoleta Panaghiu
b19c152d0c RED-10363: check for rules on analysis btn click; refactor to signals. 2024-11-19 18:06:55 +02:00
Valentin Mihai
d67e1ec285 RED-10491 - updated view mode when page is changed 2024-11-19 14:22:19 +02:00
Valentin Mihai
8dba17266c RED-9996 - pre-filter available annotation filter types in new DocuMine component view 2024-11-19 13:59:54 +02:00
Nicoleta Panaghiu
0a352821cf RED-10426: implemented another method of sorting rectangles. 2024-11-18 17:36:23 +02:00
Valentin Mihai
eddb70e116 RED-10461 - updated translation keys 2024-11-18 11:55:48 +02:00
Nicoleta Panaghiu
259d07f834 RED-10405: removed locked rules pending type and fixed pending count. 2024-11-18 11:40:56 +02:00
Valentin Mihai
1d4deacf1b RED-9417 - fix build 2024-11-14 14:39:17 +02:00
Valentin Mihai
1e00979f08 RED-9417 - updated the display condition for excluded page indicator 2024-11-14 11:25:24 +02:00
Valentin Mihai
332476ec46 Merge remote-tracking branch 'origin/release/4.951.x' into release/4.951.x 2024-11-14 11:24:28 +02:00
Valentin Mihai
2267af3a55 RED-10460 - enable switching the language in Edit Profile page in non-dev mode 2024-11-14 11:23:35 +02:00
Nicoleta Panaghiu
ab2af31f28 RED-10363: fixed cosmetical issue, added tooltips to and disabled btns. 2024-11-14 11:11:01 +02:00
Nicoleta Panaghiu
1d61d07305 RED-10405: check if file isError, indent pending type, remove unknown. 2024-11-13 15:07:43 +02:00
Nicoleta Panaghiu
1325031ff7 RED-10397: changed the active state for redacted btn; hid rectangle btn. 2024-11-13 13:36:30 +02:00
Valentin Mihai
0bb5a2158d RED-10453 - reload file or files (bulk case) after approval request 2024-11-13 11:50:46 +02:00
Nicoleta Panaghiu
b05024cb99 RED-10405: added labels and filtering for separate file pending types. 2024-11-13 10:30:08 +02:00
Valentin Mihai
e9bd45c7cc RED-10332 - fixed dictionary dropdown that did not update when another dossier template was selected 2024-11-13 09:34:05 +02:00
Valentin Mihai
d673348def RED-10373 - remove redaction dialog updates to be able to create multiple bulk requests for rectangle annotations 2024-11-12 18:45:44 +02:00
Nicoleta Panaghiu
0d639b96a2 RED-10440: map redacted image hints to redaction. 2024-11-12 13:52:22 +02:00
Nicoleta Panaghiu
740d4cf071 RED-3800: fixed generic errors. 2024-11-12 13:26:20 +02:00
Nicoleta Panaghiu
33441d16c9 RED-10363: fixed distinctUntilChanged comparator. 2024-11-12 12:53:26 +02:00
Valentin Mihai
a48c457615 RED-10396 - fixed current page active annotations re evaluation when they are updated 2024-11-12 10:55:36 +02:00
Timo Bejan
9484ce2377 RED-10422 - improved number of requests with help of new changes endpoint 2024-11-11 18:19:23 +02:00
Nicoleta Panaghiu
8871e24660 RED-10440: fixed redacted image hint display color. 2024-11-11 16:48:44 +02:00
Nicoleta Panaghiu
e8c40353a5 RED-10420: prevent page refresh by using stop propagation. 2024-11-11 15:02:36 +02:00
Nicoleta Panaghiu
6fb5af1820 RED-10427: send rectangle updated value for edit on multiple pages. 2024-11-11 14:02:14 +02:00
Nicoleta Panaghiu
493b40cdac RED-10363: disable analysis button when rules are locked. 2024-11-11 13:40:16 +02:00
Valentin Mihai
27981baa81 RED-10332 - Dossier Template not changeable when comparing dossier dictionaries in Edit dossier modal 2024-11-08 17:15:55 +02:00
Nicoleta Panaghiu
c2b27765d3 RED-10426: sort annotations according to visual alignment. 2024-11-08 17:03:52 +02:00
Nicoleta Panaghiu
c7f7ba28b6 RED-10423: fixed multiple pages rectangle remove action. 2024-11-07 13:22:49 +02:00
Nicoleta Panaghiu
a8941e2602 RED-9646: enable toaster notifications for zip files. 2024-11-06 16:10:05 +02:00
Adina Țeudan
e05af05c4a RED-10414 Removed offset and fixed notification polling 2024-11-06 15:57:45 +02:00
Timo Bejan
6ff4d2acfa Updated common-ui 2024-11-06 15:53:31 +02:00
Nicoleta Panaghiu
e036deb490 RED-3800: manual localazy sync. 2024-11-05 12:22:36 +02:00
Valentin Mihai
4f9dd026ab RED-10373 - Singular-Plural disctinction in translation for remove in document 2024-11-04 18:21:56 +02:00
Valentin Mihai
9bc95b8eee RED-10332 - Dossier Template not changeable when comparing dossier dictionaries in Edit dossier modal 2024-11-04 16:32:42 +02:00
Kilian Schüttler
e084cb6c3b Merge branch 'annotation-id-bp' into 'release/4.951.x'
Copy AnotationId to clipboard on ctrl + alt + click

See merge request redactmanager/red-ui!675
2024-11-04 13:39:25 +01:00
Kilian Schuettler
5a8f97ca8a Copy AnotationId to clipboard on ctrl + alt + click
(cherry picked from commit d66ea4e154e792e04d7165e51ec79aa442574c9d)
2024-11-04 13:31:32 +01:00
Nicoleta Panaghiu
e802ef3f7e Revert "RED-10326: fix display of documents in trash on retention capacity chart."
This reverts commit e167e94171cb500f0785f2f83be85810fd57021e.
2024-11-04 10:22:04 +02:00
Nicoleta Panaghiu
b2e40b8f86 RED-10206: disable new dossier button when the template is invalid. 2024-11-01 16:34:11 +02:00
Dan Percic
4dc7ecee78 add all available fonts 2024-11-01 16:19:58 +02:00
Valentin Mihai
416b0c925c RED-9944 - Action Items don't appear in document area in webviewer when bulk-select is still unintenionally active 2024-11-01 14:30:50 +02:00
Nicoleta Panaghiu
9f7ff828c9 RED-10363: added a toaster error for locked dossier template rules. 2024-11-01 11:22:42 +02:00
Dan Percic
a513a93927 untest fonts 2024-11-01 11:18:53 +02:00
Dan Percic
c206cb9b8f test fonts 2024-11-01 11:10:00 +02:00
Nicoleta Panaghiu
11b6912999 RED-10330: fixed remove in this context option permissions. 2024-11-01 10:35:53 +02:00
Nicoleta Panaghiu
f1680de597 RED-10331: quick fix for jumping selection circle button. 2024-11-01 08:43:17 +02:00
Nicoleta Panaghiu
08b6e1ff8c RED-10163: fix delete justification by using technical_name as id. 2024-10-31 14:07:06 +02:00
Nicoleta Panaghiu
66cc2a249f RED-10220: make legalBasis and description required again. 2024-10-31 13:41:05 +02:00
Dan Percic
baa0f74b92 fix text not displayed in webviewer? 2024-10-31 11:52:37 +02:00
Nicoleta Panaghiu
1b7ad118d4 RED-10331: fixed missing remove-button from pdf-viewer on multi-select. 2024-10-30 16:09:28 +02:00
Nicoleta Panaghiu
c977f95f1e RED-9663: removed UNASSIGNED option from the status filter. 2024-10-30 16:03:19 +02:00
Valentin Mihai
1da7b41692 RED-10198 - missleading error message - creating user with existing email 2024-10-30 14:36:32 +02:00
Valentin Mihai
6100c59a87 RED-10256 - Bulk-local: Changes should not be filtered + Remove for image-based redactions and hints 2024-10-30 14:24:16 +02:00
Valentin Mihai
860146cd1a RED-10048 - Improve display of error messages for csv files 2024-10-30 13:38:05 +02:00
Nicoleta Panaghiu
0b8a0f08d2 RED-10218: fixed fileNameColumn width overlap. 2024-10-30 12:30:58 +02:00
Nicoleta Panaghiu
e167e94171 RED-10326: fix display of documents in trash on retention capacity chart. 2024-10-29 16:41:00 +02:00
Nicoleta Panaghiu
315bd225af RED-10320: removed general configuration header subtitle. 2024-10-29 15:39:35 +02:00
Nicoleta Panaghiu
6a9f440b8a RED-10275: differentiate available types by applyToAll flag state. 2024-10-29 15:22:48 +02:00
Nicoleta Panaghiu
0ba173b4b9 RED-3800: localazy manual sync. 2024-10-29 14:21:25 +02:00
Nicoleta Panaghiu
6d74ab60cc RED-9733: translated initials in workload icons. 2024-10-29 13:11:04 +02:00
Nicoleta Panaghiu
1f36b0be04 RED-10244: fix dossier with files not having template field disabled. 2024-10-28 18:22:02 +02:00
Valentin Mihai
40d6718e8e RED-7340 - Rectangle redactions: Use bulk-local redactions + New dialog design 2024-10-28 17:58:03 +02:00
Nicoleta Panaghiu
cc22fdf538 RED-3800: localazy manual sync. 2024-10-28 17:45:49 +02:00
Nicoleta Panaghiu
4c6bb84567 RED-10262: fixed cancel resize. 2024-10-28 17:26:50 +02:00
Valentin Mihai
41a3dce600 RED-10256 - Bulk-local: Changes should not be filtered + Remove for image-based redactions 2024-10-28 17:24:32 +02:00
Valentin Mihai
53cfc669d6 RED-9944 - Action Items don't appear in document area in webviewer when bulk-select is still unintenionally active 2024-10-28 16:56:07 +02:00
Nicoleta Panaghiu
540649edbd RED-10258: try to get app to fetch the added fonts. 2024-10-28 14:30:16 +02:00
Valentin Mihai
32369d7121 RED-10256 - Bulk-local: Changes should not be filtered + Remove for image-based redactions 2024-10-28 13:57:48 +02:00
Nicoleta Panaghiu
a9a935b90d RED-10258: added missing fonts to pdftron. 2024-10-28 13:46:11 +02:00
Kilian Schüttler
491977cb49 Merge branch 'RED-10264-bp' into 'release/4.951.x'
RED-10264: remove includeUnprocessed from all manual change calls

See merge request redactmanager/red-ui!644
2024-10-28 12:40:39 +01:00
Kilian Schuettler
b620605613 RED-10264: remove includeUnprocessed from all manual change calls 2024-10-25 13:50:54 +02:00
Nicoleta Panaghiu
a28f5fd727 RED-3800: manual localazy sync. 2024-10-25 14:27:42 +03:00
Nicoleta Panaghiu
2e2eaf476d RED-9447, RED-10244: disable dossier template field if state is changed. 2024-10-25 13:39:10 +03:00
Dan Percic
b0d38e1b0f Merge branch 'feature/RED-10260-bp' into 'release/4.951.x'
feature/RED-10260: add quoteChar to componentMapping

See merge request redactmanager/red-ui!641
2024-10-24 10:00:04 +02:00
Kilian Schüttler
d4991f6806 feature/RED-10260: add quoteChar to componentMapping 2024-10-24 10:00:04 +02:00
Valentin Mihai
9d18113f56 RED-7340 - Rectangle redactions: Use bulk-local redactions + New dialog design 2024-10-23 23:48:02 +03:00
Valentin Mihai
d04f1b7b0b RED-9585 - Incorrect capitalization in German translation + missing singular/plural distinction 2024-10-23 22:25:10 +03:00
Nicoleta Panaghiu
b1948622fe RED-8277: update common ui. 2024-10-23 16:11:06 +03:00
Dan Percic
eb4368c3ce Merge branch 'RED-10072' into 'release/4.951.x'
Revert "RED-10072: AI description field and toggle for entities"

See merge request redactmanager/red-ui!638
2024-10-23 11:20:10 +02:00
Maverick Studer
9772145239 Revert "RED-10072: AI description field and toggle for entities" 2024-10-23 11:20:10 +02:00
Nicoleta Panaghiu
183b19c9da RED-10255: fixed force ignored hint action. 2024-10-22 19:19:50 +03:00
Nicoleta Panaghiu
4c9b487c78 RED-10227: fixed audit log date filters. 2024-10-22 18:59:58 +03:00
Nicoleta Panaghiu
ba311ad8e3 RED-8277: update common ui. 2024-10-22 18:03:08 +03:00
Nicoleta Panaghiu
62d4d18eaa RED-10220: description and legalBasis fields are no longer required. 2024-10-22 17:41:27 +03:00
Valentin Mihai
7f8dca3098 RED-9944 - Action Items don't appear in document area in webviewer when bulk-select is still unintenionally active 2024-10-22 17:23:32 +03:00
Valentin Mihai
289ee7f61d RED-9944 - Action Items don't appear in document area in webviewer when bulk-select is still unintenionally active 2024-10-22 16:42:15 +03:00
Nicoleta Panaghiu
bf1f25c0dd RED-10220: added support for justification technical name. 2024-10-22 12:46:48 +03:00
Valentin Mihai
773f2bfe9f RED-7340 - When a user has entered an invalid page range string, display a read border around the input field 2024-10-21 18:16:47 +03:00
Nicoleta Panaghiu
ac1780ade4 RED-3800: manual localazy sync. 2024-10-18 18:06:01 +03:00
Valentin Mihai
5f309bffe0 RED-9944 - Action Items don't appear in document area in webviewer when bulk-select is still unintenionally active 2024-10-18 14:35:52 +03:00
Nicoleta Panaghiu
611f293e64 RED-8277: made it possible to open dossiers and files in a new tab. 2024-10-17 18:10:25 +03:00
Valentin Mihai
bc7919551e Merge remote-tracking branch 'origin/release/4.951.x' into release/4.951.x 2024-10-16 23:12:59 +03:00
Valentin Mihai
4e3e64f2eb RED-9944 - cancel the bulk-selection mode when a user clicks somewhere else 2024-10-16 23:12:35 +03:00
Valentin Mihai
1233761ac3 RED-7340 - prefill the range fields in the edit and remove dialogs 2024-10-16 23:07:56 +03:00
Valentin Mihai
fd3b99a785 RED-7340 - prefill the range fields in the edit and remove dialogs 2024-10-16 14:42:40 +03:00
Nicoleta Panaghiu
f9361a2e82 RED-10180: sync localazy translation. 2024-10-16 14:10:23 +03:00
Nicoleta Panaghiu
4acb4d4eb7 RED-10190: fixed notification padding. 2024-10-16 14:01:22 +03:00
Valentin Mihai
7d4d2889da RED-7340 - Rectangle redactions: Use bulk-local redactions + New dialog design 2024-10-16 11:57:13 +03:00
428 changed files with 3230 additions and 1824 deletions

View File

@ -50,7 +50,7 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/@pdftron/webviewer/public/", "input": "node_modules/@pdftron/webviewer/public/",
"output": "/assets/wv-resources/10.10.1/" "output": "/assets/wv-resources/11.0.0/"
}, },
{ {
"glob": "**/*", "glob": "**/*",
@ -73,7 +73,7 @@
"stylePreprocessorOptions": { "stylePreprocessorOptions": {
"includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"] "includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"]
}, },
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js", "node_modules/chart.js/auto/auto.cjs"], "scripts": ["node_modules/chart.js/auto/auto.cjs"],
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"optimization": false, "optimization": false,

View File

@ -258,6 +258,7 @@ export const appModuleFactory = (config: AppConfig) => {
provide: MAT_TOOLTIP_DEFAULT_OPTIONS, provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: { useValue: {
disableTooltipInteractivity: true, disableTooltipInteractivity: true,
showDelay: 1,
}, },
}, },
BaseDatePipe, BaseDatePipe,

View File

@ -22,7 +22,7 @@
} }
.mat-mdc-menu-item.notification { .mat-mdc-menu-item.notification {
padding: 8px 26px 10px 8px; padding: 8px 26px 10px 8px !important;
margin: 2px 0 0 0; margin: 2px 0 0 0;
height: fit-content; height: fit-content;
position: relative; position: relative;

View File

@ -11,7 +11,9 @@ import { DictionaryService } from '@services/entity-services/dictionary.service'
import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { WatermarkService } from '@services/entity-services/watermark.service'; import { WatermarkService } from '@services/entity-services/watermark.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { getConfig } from '@iqser/common-ui'; import { getConfig, Toaster } from '@iqser/common-ui';
import { RulesService } from '../modules/admin/services/rules.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export function templateExistsWhenEnteringAdmin(): CanActivateFn { export function templateExistsWhenEnteringAdmin(): CanActivateFn {
return async function (route: ActivatedRouteSnapshot): Promise<boolean> { return async function (route: ActivatedRouteSnapshot): Promise<boolean> {
@ -21,12 +23,14 @@ export function templateExistsWhenEnteringAdmin(): CanActivateFn {
const defaultColorsService = inject(DefaultColorsService); const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService); const watermarksService = inject(WatermarkService);
const router = inject(Router); const router = inject(Router);
const rulesService = inject(RulesService);
const isDocumine = getConfig().IS_DOCUMINE; const isDocumine = getConfig().IS_DOCUMINE;
const dossierTemplate = inject(DossierTemplateStatsService).get(dossierTemplateId); const dossierTemplate = inject(DossierTemplateStatsService).get(dossierTemplateId);
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId)); await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId)); await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId)); await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (!isDocumine) { if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId)); await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
} }
@ -50,6 +54,8 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
const dictionaryService = inject(DictionaryService); const dictionaryService = inject(DictionaryService);
const defaultColorsService = inject(DefaultColorsService); const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService); const watermarksService = inject(WatermarkService);
const rulesService = inject(RulesService);
const toaster = inject(Toaster);
const isDocumine = getConfig().IS_DOCUMINE; const isDocumine = getConfig().IS_DOCUMINE;
await firstValueFrom(dashboardStatsService.loadForTemplate(dossierTemplateId)); await firstValueFrom(dashboardStatsService.loadForTemplate(dossierTemplateId));
@ -64,6 +70,10 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId)); await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId)); await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId)); await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
const rules = await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (rules.timeoutDetected) {
toaster.error(_('dossier-listing.rules.timeoutError'));
}
if (!isDocumine) { if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId)); await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
} }

View File

@ -16,11 +16,7 @@ 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.canBeMarkedAsFalsePositive && !annotation.hasBeenResizedLocally && annotationEntity?.hasDictionary;
!annotation.hasBeenResizedLocally &&
!annotation.isRemovedLocally &&
!annotation.hasBeenForcedRedaction &&
annotationEntity?.hasDictionary;
export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) => export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) =>
canAddRedaction && canAddRedaction &&

View File

@ -84,7 +84,10 @@ export class AnnotationWrapper implements IListable {
} }
get isRedactedImageHint() { get isRedactedImageHint() {
return this.IMAGE_HINT && this.superType === SuperTypes.Redaction; return (
(this.IMAGE_HINT && this.superType === SuperTypes.Redaction) ||
(this.IMAGE_HINT && this.superType === SuperTypes.ManualRedaction)
);
} }
get isSkippedImageHint() { get isSkippedImageHint() {
@ -109,7 +112,10 @@ export class AnnotationWrapper implements IListable {
get canBeMarkedAsFalsePositive() { get canBeMarkedAsFalsePositive() {
return ( return (
(this.isRecommendation || this.superType === SuperTypes.Redaction || (this.isSkipped && this.isDictBased)) && (this.isRecommendation ||
this.superType === SuperTypes.Redaction ||
(this.isSkipped && this.isDictBased) ||
(this.isRemovedLocally && this.isDictBased)) &&
!this.isImage && !this.isImage &&
!this.imported && !this.imported &&
!this.pending && !this.pending &&

View File

@ -8,10 +8,11 @@
<div class="content-container full-height"> <div class="content-container full-height">
<div class="overlay-shadow"></div> <div class="overlay-shadow"></div>
<div [ngClass]="!isWarningsScreen && 'dialog'"> <div [ngClass]="!isWarningsScreen && 'dialog'">
<div *ngIf="!isWarningsScreen" class="dialog-header"> @if (!isWarningsScreen) {
<div class="heading-l" [translate]="translations[path]"></div> <div class="dialog-header">
</div> <div class="heading-l" [translate]="translations[path]"></div>
</div>
}
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div> </div>

View File

@ -16,14 +16,16 @@
<input formControlName="lastName" name="lastName" type="text" /> <input formControlName="lastName" name="lastName" type="text" />
</div> </div>
<div *ngIf="devMode" class="iqser-input-group"> <div class="iqser-input-group">
<label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label> <label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label>
<mat-form-field> <mat-form-field>
<mat-select formControlName="language"> <mat-select formControlName="language">
<mat-select-trigger>{{ languageSelectLabel() | translate }}</mat-select-trigger> <mat-select-trigger>{{ languageSelectLabel() | translate }}</mat-select-trigger>
<mat-option *ngFor="let language of languages" [value]="language"> @for (language of languages; track language) {
{{ translations[language] | translate }} <mat-option [value]="language">
</mat-option> {{ translations[language] | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -32,11 +34,13 @@
<a (click)="resetPassword()" 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"> @if (devMode) {
<mat-slide-toggle color="primary" formControlName="darkTheme"> <div class="iqser-input-group">
{{ 'user-profile-screen.form.dark-theme' | translate }} <mat-slide-toggle color="primary" formControlName="darkTheme">
</mat-slide-toggle> {{ 'user-profile-screen.form.dark-theme' | translate }}
</div> </mat-slide-toggle>
</div>
}
</div> </div>
</div> </div>

View File

@ -23,7 +23,7 @@
<div class="mt-44"> <div class="mt-44">
<redaction-donut-chart <redaction-donut-chart
[config]="chartConfig" [config]="chartConfig()"
[radius]="63" [radius]="63"
[strokeWidth]="15" [strokeWidth]="15"
[subtitles]="['user-stats.chart.users' | translate]" [subtitles]="['user-stats.chart.users' | translate]"

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, input, Input, output, Output } from '@angular/core';
import { DonutChartConfig } from '@red/domain'; import { DonutChartConfig } from '@red/domain';
import { CircleButtonComponent } from '@iqser/common-ui'; import { CircleButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@ -12,6 +12,6 @@ import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.
imports: [CircleButtonComponent, TranslateModule, DonutChartComponent], imports: [CircleButtonComponent, TranslateModule, DonutChartComponent],
}) })
export class UsersStatsComponent { export class UsersStatsComponent {
@Output() toggleCollapse = new EventEmitter(); readonly chartConfig = input.required<DonutChartConfig[]>();
@Input() chartConfig: DonutChartConfig[]; readonly toggleCollapse = output();
} }

View File

@ -101,11 +101,7 @@ export class UserDetailsComponent extends BaseFormComponent implements OnInit {
this.closeDialog.emit(true); this.closeDialog.emit(true);
}) })
.catch(error => { .catch(error => {
if (error.status === HttpStatusCode.Conflict) { this._toaster.error(null, { error });
this._toaster.error(_('add-edit-user.error.email-already-used'));
} else {
this._toaster.error(_('add-edit-user.error.generic'));
}
this._loadingService.stop(); this._loadingService.stop();
}); });
} else { } else {

View File

@ -17,7 +17,7 @@ import { RouterHistoryService } from '@services/router-history.service';
import { auditCategoriesTranslations } from '@translations/audit-categories-translations'; import { auditCategoriesTranslations } from '@translations/audit-categories-translations';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { applyIntervalConstraints } from '@utils/date-inputs-utils'; import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { AuditService } from '../../services/audit.service'; import { AuditService } from '../../services/audit.service';
@ -139,16 +139,9 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnI
const promises = []; const promises = [];
const category = this.form.get('category').value; const category = this.form.get('category').value;
const userId = this.form.get('userId').value; const userId = this.form.get('userId').value;
const from = this.form.get('from').value; const from = this.form.get('from').value ? dayjs(this.form.get('from').value).startOf('day').toISOString() : null;
let to = this.form.get('to').value; const to = this.form.get('to').value ? dayjs(this.form.get('to').value).endOf('day').toISOString() : null;
if (to) {
const hoursLeft = new Date(to).getHours();
const minutesLeft = new Date(to).getMinutes();
to = to
.clone()
.add(24 - hoursLeft - 1, 'h')
.add(60 - minutesLeft - 1);
}
const logsRequestBody: IAuditSearchRequest = { const logsRequestBody: IAuditSearchRequest = {
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
page: page, page: page,

View File

@ -42,6 +42,15 @@
type="text" type="text"
/> />
</div> </div>
<div class="iqser-input-group required w-150">
<label translate="add-edit-component-mapping.form.quote-char"></label>
<input
[placeholder]="'add-edit-component-mapping.form.quote-char-placeholder' | translate"
formControlName="quoteChar"
name="quoteChar"
type="text"
/>
</div>
<div class="iqser-input-group required w-150"> <div class="iqser-input-group required w-150">
<label translate="add-edit-component-mapping.form.encoding-type"></label> <label translate="add-edit-component-mapping.form.encoding-type"></label>

View File

@ -17,12 +17,14 @@ interface DialogData {
dossierTemplateId: string; dossierTemplateId: string;
mapping: IComponentMapping; mapping: IComponentMapping;
} }
interface DialogResult { interface DialogResult {
id: string; id: string;
name: string; name: string;
file: Blob; file: Blob;
encoding: string; encoding: string;
delimiter: string; delimiter: string;
quoteChar: string;
fileName?: string; fileName?: string;
} }
@ -72,14 +74,14 @@ export class AddEditComponentMappingDialogComponent
const file = new Blob([fileContent.body as Blob], { type: 'text/csv' }); const file = new Blob([fileContent.body as Blob], { type: 'text/csv' });
this.form.get('file').setValue(file); this.form.get('file').setValue(file);
this.initialFormValue = this.form.getRawValue(); this.initialFormValue = this.form.getRawValue();
this.#disableEncodingAndDelimiter(); this.#disableEncodingAndQuoteCharAndDelimiter();
} }
} }
changeFile(file: File) { changeFile(file: File) {
this.form.get('file').setValue(file); this.form.get('file').setValue(file);
this.form.get('fileName').setValue(file?.name); this.form.get('fileName').setValue(file?.name);
this.#enableEncodingAndDelimiter(); this.#enableEncodingAndQuoteCharAndDelimiter();
} }
save() { save() {
@ -93,16 +95,19 @@ export class AddEditComponentMappingDialogComponent
fileName: [this.data?.mapping?.fileName, Validators.required], fileName: [this.data?.mapping?.fileName, Validators.required],
encoding: this.encodingTypeOptions.find(e => e === this.data?.mapping?.encoding) ?? this.encodingTypeOptions[0], encoding: this.encodingTypeOptions.find(e => e === this.data?.mapping?.encoding) ?? this.encodingTypeOptions[0],
delimiter: [this.data?.mapping?.delimiter ?? ',', Validators.required], delimiter: [this.data?.mapping?.delimiter ?? ',', Validators.required],
quoteChar: [this.data?.mapping?.quoteChar ?? '"', Validators.required],
}); });
} }
#disableEncodingAndDelimiter() { #disableEncodingAndQuoteCharAndDelimiter() {
this.form.get('encoding').disable(); this.form.get('encoding').disable();
this.form.get('delimiter').disable(); this.form.get('delimiter').disable();
this.form.get('quoteChar').disable();
} }
#enableEncodingAndDelimiter() { #enableEncodingAndQuoteCharAndDelimiter() {
this.form.get('encoding').enable(); this.form.get('encoding').enable();
this.form.get('delimiter').enable(); this.form.get('delimiter').enable();
this.form.get('quoteChar').enable();
} }
} }

View File

@ -99,8 +99,8 @@ export default class ComponentMappingsScreenComponent extends ListingComponent<C
const result = await dialog.result(); const result = await dialog.result();
if (result) { if (result) {
this._loadingService.start(); this._loadingService.start();
const { id, name, encoding, delimiter, fileName } = result; const { id, name, encoding, delimiter, fileName, quoteChar } = result;
const newMapping = { id, name, encoding, delimiter, fileName }; const newMapping = { id, name, encoding, delimiter, fileName, quoteChar };
await firstValueFrom( await firstValueFrom(
this._componentMappingService.createUpdateComponentMapping(this.#dossierTemplateId, newMapping, result.file), this._componentMappingService.createUpdateComponentMapping(this.#dossierTemplateId, newMapping, result.file),
); );

View File

@ -87,6 +87,7 @@
[routerLink]="dict.routerLink" [routerLink]="dict.routerLink"
[tooltip]="'entities-listing.action.edit' | translate" [tooltip]="'entities-listing.action.edit' | translate"
icon="iqser:edit" icon="iqser:edit"
iqserStopPropagation
></iqser-circle-button> ></iqser-circle-button>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ import {
ListingComponent, ListingComponent,
listingProvidersFactory, listingProvidersFactory,
LoadingService, LoadingService,
StopPropagationDirective,
TableColumnConfig, TableColumnConfig,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { getParam } from '@iqser/common-ui/lib/utils'; import { getParam } from '@iqser/common-ui/lib/utils';
@ -41,6 +42,7 @@ import { AdminDialogService } from '../../services/admin-dialog.service';
AnnotationIconComponent, AnnotationIconComponent,
AsyncPipe, AsyncPipe,
RouterLink, RouterLink,
StopPropagationDirective,
], ],
}) })
export class EntitiesListingScreenComponent extends ListingComponent<Dictionary> implements OnInit { export class EntitiesListingScreenComponent extends ListingComponent<Dictionary> implements OnInit {

View File

@ -1,6 +1,5 @@
<div class="dialog-header"> <div class="dialog-header">
<div class="heading-l" translate="general-config-screen.general.title"></div> <div class="heading-l" translate="general-config-screen.general.title"></div>
<div translate="general-config-screen.general.subtitle"></div>
</div> </div>
<form (submit)="save()" *ngIf="form" [formGroup]="form"> <form (submit)="save()" *ngIf="form" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">

View File

@ -34,6 +34,16 @@
<span [innerHTML]="'dossier-template-info-screen.created-on' | translate: { date: createdOn }"></span> <span [innerHTML]="'dossier-template-info-screen.created-on' | translate: { date: createdOn }"></span>
</div> </div>
<div *ngIf="areRulesLocked()">
<mat-icon
(click)="resetRules()"
[matTooltip]="'dossier-template-info-screen.rules-reset.tooltip' | translate"
svgIcon="iqser:alert-circle"
class="action-icon"
></mat-icon>
<span class="error">{{ 'dossier-template-info-screen.rules-reset.label' | translate }}</span>
</div>
<div> <div>
<mat-icon svgIcon="red:entries"></mat-icon> <mat-icon svgIcon="red:entries"></mat-icon>
{{ 'dossier-template-info-screen.entries' | translate: { count: ctx.stats.numberOfEntries } }} {{ 'dossier-template-info-screen.entries' | translate: { count: ctx.stats.numberOfEntries } }}

View File

@ -18,3 +18,11 @@
padding-right: 24px; padding-right: 24px;
margin-right: 24px; margin-right: 24px;
} }
.error {
color: var(--iqser-primary);
}
.action-icon {
cursor: pointer;
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, computed, Input, OnInit } from '@angular/core';
import { ContextComponent } from '@iqser/common-ui/lib/utils'; import { ContextComponent } from '@iqser/common-ui/lib/utils';
import { type DossierTemplate, type DossierTemplateStats } from '@red/domain'; import { type DossierTemplate, type DossierTemplateStats } from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@ -9,6 +9,11 @@ import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { InitialsAvatarComponent } from '@common-ui/users'; import { InitialsAvatarComponent } from '@common-ui/users';
import { DatePipe } from '@shared/pipes/date.pipe'; import { DatePipe } from '@shared/pipes/date.pipe';
import { RulesService } from '../../../services/rules.service';
import { Toaster } from '@iqser/common-ui';
import { MatTooltip } from '@angular/material/tooltip';
import { firstValueFrom } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
interface Context { interface Context {
readonly dossierTemplate: DossierTemplate; readonly dossierTemplate: DossierTemplate;
@ -20,16 +25,20 @@ interface Context {
templateUrl: './dossier-template-details.component.html', templateUrl: './dossier-template-details.component.html',
styleUrls: ['./dossier-template-details.component.scss'], styleUrls: ['./dossier-template-details.component.scss'],
standalone: true, standalone: true,
imports: [NgIf, AsyncPipe, MatIcon, TranslateModule, DatePipe, InitialsAvatarComponent], imports: [NgIf, AsyncPipe, MatIcon, TranslateModule, DatePipe, InitialsAvatarComponent, MatTooltip],
}) })
export class DossierTemplateDetailsComponent extends ContextComponent<Context> implements OnInit { export class DossierTemplateDetailsComponent extends ContextComponent<Context> implements OnInit {
readonly translations = dossierTemplateStatusTranslations; readonly translations = dossierTemplateStatusTranslations;
@Input({ required: true }) dossierTemplateId: string; @Input({ required: true }) dossierTemplateId: string;
readonly areRulesLocked = computed(() => {
return this._rulesService.currentTemplateRules().timeoutDetected;
});
constructor( constructor(
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService, private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
private readonly _rulesService: RulesService,
private readonly _toaster: Toaster,
) { ) {
super(); super();
} }
@ -40,4 +49,14 @@ export class DossierTemplateDetailsComponent extends ContextComponent<Context> i
stats: this._dossierTemplateStatsService.watch$(this.dossierTemplateId), stats: this._dossierTemplateStatsService.watch$(this.dossierTemplateId),
}); });
} }
async resetRules() {
try {
await firstValueFrom(this._rulesService.reset(this.dossierTemplateId));
this._toaster.success(_('dossier-template-info-screen.rules-reset.success'));
await firstValueFrom(this._rulesService.getFor(this.dossierTemplateId));
} catch (error) {
this._toaster.rawError(error.error.message);
}
}
} }

View File

@ -17,6 +17,15 @@
type="text" type="text"
/> />
</div> </div>
<div class="iqser-input-group">
<label translate="add-edit-entity.form.technical-name"></label>
<div class="technical-name">{{ this.technicalName() || '-' }}</div>
<span
[translateParams]="{ type: data.justification ? 'edit' : 'create' }"
[translate]="'add-edit-entity.form.technical-name-hint'"
class="hint"
></span>
</div>
<div class="iqser-input-group required w-400"> <div class="iqser-input-group required w-400">
<label translate="add-edit-justification.form.reason"></label> <label translate="add-edit-justification.form.reason"></label>

View File

@ -1,11 +1,13 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, Inject, untracked } from '@angular/core';
import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms'; import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Justification } from '@red/domain'; import { Justification } from '@red/domain';
import { JustificationsService } from '@services/entity-services/justifications.service'; import { JustificationsService } from '@services/entity-services/justifications.service';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui'; import { BaseDialogComponent, CircleButtonComponent, HasScrollbarDirective, IconButtonComponent } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { formControlToSignal } from '@utils/functions';
import { toSignal } from '@angular/core/rxjs-interop';
interface DialogData { interface DialogData {
justification?: Justification; justification?: Justification;
@ -16,9 +18,29 @@ interface DialogData {
templateUrl: './add-edit-justification-dialog.component.html', templateUrl: './add-edit-justification-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [TranslateModule, ReactiveFormsModule, IconButtonComponent, CircleButtonComponent], imports: [TranslateModule, ReactiveFormsModule, IconButtonComponent, CircleButtonComponent, HasScrollbarDirective],
}) })
export class AddEditJustificationDialogComponent extends BaseDialogComponent { export class AddEditJustificationDialogComponent extends BaseDialogComponent {
readonly form = this.#getForm();
readonly name = formControlToSignal(this.form.controls['name']);
readonly allJustifications = toSignal(this._justificationService.all$);
readonly technicalName = computed(() => {
if (this.data.justification) {
return this.data.justification.technicalName;
}
if (!this.name()) {
return null;
}
let currentTechnicalName = Justification.toTechnicalName(this.name());
const existingTechnicalNames = untracked(this.allJustifications).map(justification => justification.technicalName);
let suffix = 1;
while (existingTechnicalNames.includes(currentTechnicalName)) {
currentTechnicalName =
currentTechnicalName === '_' ? `${currentTechnicalName}${suffix++}` : [currentTechnicalName, suffix++].join('_');
}
return currentTechnicalName;
});
constructor( constructor(
private readonly _justificationService: JustificationsService, private readonly _justificationService: JustificationsService,
protected readonly _dialogRef: MatDialogRef<AddEditJustificationDialogComponent>, protected readonly _dialogRef: MatDialogRef<AddEditJustificationDialogComponent>,
@ -26,7 +48,6 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
) { ) {
super(_dialogRef, !!data.justification); super(_dialogRef, !!data.justification);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue(); this.initialFormValue = this.form.getRawValue();
} }
@ -34,7 +55,8 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
const dossierTemplateId = this.data.dossierTemplateId; const dossierTemplateId = this.data.dossierTemplateId;
this._loadingService.start(); this._loadingService.start();
try { try {
await firstValueFrom(this._justificationService.createOrUpdate(this.form.getRawValue() as Justification, dossierTemplateId)); const formValue = { ...this.form.getRawValue(), technicalName: this.technicalName() };
await firstValueFrom(this._justificationService.createOrUpdate(formValue as Justification, dossierTemplateId));
await firstValueFrom(this._justificationService.loadAll(dossierTemplateId)); await firstValueFrom(this._justificationService.loadAll(dossierTemplateId));
this._dialogRef.close(true); this._dialogRef.close(true);
} catch (error) { } catch (error) {
@ -43,11 +65,12 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
this._loadingService.stop(); this._loadingService.stop();
} }
private _getForm(): UntypedFormGroup { #getForm(): UntypedFormGroup {
return this._formBuilder.group({ return this._formBuilder.group({
name: [{ value: this.data.justification?.name, disabled: !!this.data.justification }, Validators.required], name: [{ value: this.data.justification?.name, disabled: !!this.data.justification }, Validators.required],
reason: [this.data.justification?.reason, Validators.required], reason: [this.data.justification?.reason, Validators.required],
description: [this.data.justification?.description, Validators.required], description: [this.data.justification?.description, Validators.required],
technicalName: [this.data.justification?.technicalName ?? null],
}); });
} }
} }

View File

@ -246,14 +246,15 @@ export class WatermarkScreenComponent implements OnInit {
} }
async #loadViewer() { async #loadViewer() {
this.instance = await WebViewer( this.instance = await WebViewer.Iframe(
{ {
licenseKey: this._licenseService.activeLicenseKey, licenseKey: this._licenseService.activeLicenseKey,
path: this.#convertPath('/assets/wv-resources/10.10.1'), path: this.#convertPath('/assets/wv-resources/11.0.0'),
css: this.#convertPath('/assets/pdftron/stylesheet.css'), css: this.#convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true, fullAPI: true,
isReadOnly: true, isReadOnly: true,
backendType: 'ems', backendType: 'ems',
ui: 'legacy',
}, },
// use nativeElement instead of document.getElementById('viwer') // use nativeElement instead of document.getElementById('viwer')
// because WebViewer works better with this approach // because WebViewer works better with this approach
@ -269,7 +270,7 @@ export class WatermarkScreenComponent implements OnInit {
}); });
if (environment.production) { if (environment.production) {
this.instance.Core.setCustomFontURL('https://' + window.location.host + this.#convertPath('/assets/pdftron')); this.instance.Core.setCustomFontURL(window.location.origin + this.#convertPath('/assets/pdftron/fonts'));
} }
this.#disableElements(); this.#disableElements();

View File

@ -1,9 +1,25 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { GenericService } from '@iqser/common-ui'; import { EntitiesService, QueryParam } from '@iqser/common-ui';
import { IRules } from '@red/domain'; import { IRules, Rules } from '@red/domain';
import { map, Observable, tap } from 'rxjs';
import { List } from '@common-ui/utils';
import { toSignal } from '@angular/core/rxjs-interop';
import { distinctUntilChanged, filter } from 'rxjs/operators';
@Injectable() @Injectable({ providedIn: 'root' })
export class RulesService extends GenericService<IRules> { export class RulesService extends EntitiesService<IRules, Rules> {
readonly currentTemplateRules = toSignal(
this.all$.pipe(
filter(all => !!all.length),
map(rules => rules[0]),
distinctUntilChanged(
(prev, curr) =>
prev.rules === curr.rules &&
prev.timeoutDetected === curr.timeoutDetected &&
prev.dossierTemplateId === curr.dossierTemplateId,
),
),
);
protected readonly _defaultModelPath = 'rules'; protected readonly _defaultModelPath = 'rules';
download(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') { download(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') {
@ -13,4 +29,12 @@ export class RulesService extends GenericService<IRules> {
uploadRules(body: IRules) { uploadRules(body: IRules) {
return this._post<unknown>({ ...body, ruleFileType: body.ruleFileType ?? 'ENTITY' }); return this._post<unknown>({ ...body, ruleFileType: body.ruleFileType ?? 'ENTITY' });
} }
getFor<R = IRules>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {
return super.getFor<R>(entityId, queryParams).pipe(tap(rules => this.setEntities([rules as Rules])));
}
reset(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') {
return this._put(null, `${this._defaultModelPath}/${dossierTemplateId}/${ruleFileType}/reset`);
}
} }

View File

@ -1,8 +1,8 @@
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress> <ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles().length" redactionLongPress>
<redaction-expandable-file-actions <redaction-expandable-file-actions
[actions]="buttons" [actions]="buttons()"
[buttonType]="buttonType" [buttonType]="buttonType()"
[maxWidth]="maxWidth" [maxWidth]="maxWidth()"
[tooltipPosition]="IqserTooltipPositions.above" [tooltipPosition]="IqserTooltipPositions.above"
> >
</redaction-expandable-file-actions> </redaction-expandable-file-actions>

View File

@ -1,6 +1,6 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, computed, input, signal } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CircleButtonType, CircleButtonTypes } from '@iqser/common-ui'; import { CircleButtonType, CircleButtonTypes, Toaster } from '@iqser/common-ui';
import { Action, ActionTypes, Dossier, File, ProcessingFileStatuses } from '@red/domain'; import { Action, ActionTypes, Dossier, File, ProcessingFileStatuses } from '@red/domain';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { LongPressDirective, LongPressEvent } from '@shared/directives/long-press.directive'; import { LongPressDirective, LongPressEvent } from '@shared/directives/long-press.directive';
@ -9,6 +9,8 @@ import { BulkActionsService } from '../../services/bulk-actions.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component'; import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { IqserTooltipPositions } from '@common-ui/utils'; import { IqserTooltipPositions } from '@common-ui/utils';
import { NgIf } from '@angular/common'; import { NgIf } from '@angular/common';
import { RulesService } from '../../../admin/services/rules.service';
import { firstValueFrom } from 'rxjs';
@Component({ @Component({
selector: 'redaction-dossier-overview-bulk-actions [dossier] [selectedFiles]', selector: 'redaction-dossier-overview-bulk-actions [dossier] [selectedFiles]',
@ -17,218 +19,194 @@ import { NgIf } from '@angular/common';
standalone: true, standalone: true,
imports: [LongPressDirective, ExpandableFileActionsComponent, NgIf], imports: [LongPressDirective, ExpandableFileActionsComponent, NgIf],
}) })
export class DossierOverviewBulkActionsComponent implements OnChanges { export class DossierOverviewBulkActionsComponent {
#analysisForced: boolean; readonly dossier = input<Dossier>();
#canAssignToSelf: boolean; readonly selectedFiles = input<File[]>();
#canAssign: boolean; readonly buttonType = input<CircleButtonType>(CircleButtonTypes.default);
#canDelete: boolean; readonly maxWidth = input<number>();
#canReanalyse: boolean; readonly buttons = computed(() => this.#buttons);
#canDisableAutoAnalysis: boolean;
#canEnableAutoAnalysis: boolean;
#canOcr: boolean;
#canSetToNew: boolean;
#canSetToUnderReview: boolean;
#canSetToUnderApproval: boolean;
#isReadyForApproval: boolean;
#canApprove: boolean;
#canUndoApproval: boolean;
#canToggleAnalysis: boolean;
#assignTooltip: string;
#toggleAnalysisTooltip: string;
#allFilesAreExcluded: boolean;
#canMoveToSameState: boolean;
@Input() dossier: Dossier;
@Input() selectedFiles: File[];
@Input() buttonType: CircleButtonType = CircleButtonTypes.default;
@Input() maxWidth: number;
buttons: Action[];
readonly IqserTooltipPositions = IqserTooltipPositions; readonly IqserTooltipPositions = IqserTooltipPositions;
readonly #areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected);
readonly #allFilesAreUnderReviewOrUnassigned = computed(() =>
this.selectedFiles().reduce((acc, file) => acc && (file.isUnderReview || file.isNew), true),
);
readonly #allFilesAreUnderApproval = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.isUnderApproval, true));
readonly #allFilesAreExcluded = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.excluded, true));
readonly #allFilesAreApproved = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.isApproved, true));
readonly #canMoveToSameState = computed(
() => this.#allFilesAreUnderReviewOrUnassigned() || this.#allFilesAreUnderApproval() || this.#allFilesAreApproved(),
);
readonly #canAssign = computed(
() =>
this.#canMoveToSameState() &&
(this._permissionsService.canAssignUser(this.selectedFiles(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.selectedFiles(), this.dossier())),
);
readonly #canAssignToSelf = computed(
() => this.#canMoveToSameState() && this._permissionsService.canAssignToSelf(this.selectedFiles(), this.dossier()),
);
readonly #canDelete = computed(() => this._permissionsService.canSoftDeleteFile(this.selectedFiles(), this.dossier()));
readonly #canReanalyse = computed(() => this._permissionsService.canReanalyseFile(this.selectedFiles(), this.dossier()));
readonly #canDisableAutoAnalysis = computed(() =>
this._permissionsService.canDisableAutoAnalysis(this.selectedFiles(), this.dossier()),
);
readonly #canEnableAutoAnalysis = computed(() => this._permissionsService.canEnableAutoAnalysis(this.selectedFiles(), this.dossier()));
readonly #canToggleAnalysis = computed(() => this._permissionsService.canToggleAnalysis(this.selectedFiles(), this.dossier()));
readonly #canOcr = computed(() => this._permissionsService.canOcrFile(this.selectedFiles(), this.dossier()));
readonly #canSetToNew = computed(() => this._permissionsService.canSetToNew(this.selectedFiles(), this.dossier()));
readonly #canSetToUnderReview = computed(() => this._permissionsService.canSetUnderReview(this.selectedFiles(), this.dossier()));
readonly #canSetToUnderApproval = computed(() => this._permissionsService.canSetUnderApproval(this.selectedFiles(), this.dossier()));
readonly #isReadyForApproval = computed(() => this._permissionsService.isReadyForApproval(this.selectedFiles(), this.dossier()));
readonly #canApprove = computed(() => this._permissionsService.canBeApproved(this.selectedFiles(), this.dossier()));
readonly #canUndoApproval = computed(() => this._permissionsService.canUndoApproval(this.selectedFiles(), this.dossier()));
readonly #assignTooltip = computed(() =>
this.#allFilesAreUnderApproval() ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'),
);
readonly #toggleAnalysisTooltip = computed(() =>
this.#allFilesAreExcluded() ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable'),
);
readonly #analysisForced = signal(false);
constructor( constructor(
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _bulkActionsService: BulkActionsService, private readonly _bulkActionsService: BulkActionsService,
private readonly _rulesService: RulesService,
private readonly _toaster: Toaster,
) {} ) {}
private get _buttons(): Action[] { get #buttons(): Action[] {
const actions: Action[] = [ const actions: Action[] = [
{ {
id: 'delete-files-btn', id: 'delete-files-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.delete(this.selectedFiles), action: () => this._bulkActionsService.delete(this.selectedFiles()),
tooltip: _('dossier-overview.bulk.delete'), tooltip: _('dossier-overview.bulk.delete'),
icon: 'iqser:trash', icon: 'iqser:trash',
show: this.#canDelete, show: this.#canDelete(),
}, },
{ {
id: 'assign-files-btn', id: 'assign-files-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assign(this.selectedFiles), action: () => this._bulkActionsService.assign(this.selectedFiles()),
tooltip: this.#assignTooltip, tooltip: this.#assignTooltip(),
icon: 'red:assign', icon: 'red:assign',
show: this.#canAssign, show: this.#canAssign(),
}, },
{ {
id: 'assign-files-to-me-btn', id: 'assign-files-to-me-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assignToMe(this.selectedFiles), action: () => this._bulkActionsService.assignToMe(this.selectedFiles()),
tooltip: _('dossier-overview.assign-me'), tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me', icon: 'red:assign-me',
show: this.#canAssignToSelf, show: this.#canAssignToSelf(),
}, },
{ {
id: 'to-new-btn', id: 'to-new-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToNew(this.selectedFiles), action: () => this._bulkActionsService.setToNew(this.selectedFiles()),
tooltip: _('dossier-overview.back-to-new'), tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo', icon: 'red:undo',
show: this.#canSetToNew, show: this.#canSetToNew(),
}, },
{ {
id: 'to-under-approval-btn', id: 'to-under-approval-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles), action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'), tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval', icon: 'red:ready-for-approval',
show: this.#canSetToUnderApproval, show: this.#canSetToUnderApproval(),
}, },
{ {
id: 'to-under-review-btn', id: 'to-under-review-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles), action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles()),
tooltip: _('dossier-overview.under-review'), tooltip: _('dossier-overview.under-review'),
icon: 'red:undo', icon: 'red:undo',
show: this.#canSetToUnderReview, show: this.#canSetToUnderReview(),
}, },
{ {
id: 'download-files-btn', id: 'download-files-btn',
type: ActionTypes.downloadBtn, type: ActionTypes.downloadBtn,
show: !this.selectedFiles.some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed), show: !this.selectedFiles().some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed),
files: this.selectedFiles, files: this.selectedFiles(),
dossier: this.dossier, dossier: this.dossier(),
}, },
{ {
id: 'approve-files-btn', id: 'approve-files-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.approve(this.selectedFiles), action: () => this._bulkActionsService.approve(this.selectedFiles()),
disabled: !this.#canApprove, disabled: !this.#canApprove,
tooltip: this.#canApprove ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'), tooltip: this.#canApprove ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved', icon: 'red:approved',
show: this.#isReadyForApproval, show: this.#isReadyForApproval(),
}, },
{ {
id: 'set-under-approval-btn', id: 'set-under-approval-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles), action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'), tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo', icon: 'red:undo',
show: this.#canUndoApproval, show: this.#canUndoApproval(),
}, },
{ {
id: 'ocr-files-btn', id: 'ocr-files-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.ocr(this.selectedFiles), action: () => this._bulkActionsService.ocr(this.selectedFiles()),
tooltip: _('dossier-overview.ocr-file'), tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr', icon: 'iqser:ocr',
show: this.#canOcr, show: this.#canOcr(),
}, },
{ {
id: 'reanalyse-files-btn', id: 'reanalyse-files-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.reanalyse(this.selectedFiles), action: () => this.#reanalyseBulk(this.selectedFiles()),
tooltip: _('dossier-overview.bulk.reanalyse'), tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.bulk.reanalyse'),
icon: 'iqser:refresh', icon: 'iqser:refresh',
disabled: this.#areRulesLocked(),
show: show:
this.#canReanalyse && this.#canReanalyse() &&
(this.#analysisForced || this.#canEnableAutoAnalysis || this.selectedFiles.every(file => file.isError)), (this.#analysisForced() || this.#canEnableAutoAnalysis() || this.selectedFiles().every(file => file.isError)),
}, },
{ {
id: 'stop-automatic-analysis-btn', id: 'stop-automatic-analysis-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles), action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.stop-auto-analysis'), tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis', icon: 'red:disable-analysis',
show: this.#canDisableAutoAnalysis, show: this.#canDisableAutoAnalysis(),
}, },
{ {
id: 'start-automatic-analysis-btn', id: 'start-automatic-analysis-btn',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles), action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.start-auto-analysis'), tooltip: _('dossier-overview.start-auto-analysis'),
icon: 'red:enable-analysis', icon: 'red:enable-analysis',
show: this.#canEnableAutoAnalysis, show: this.#canEnableAutoAnalysis(),
}, },
{ {
id: 'toggle-analysis-btn', id: 'toggle-analysis-btn',
type: ActionTypes.toggle, type: ActionTypes.toggle,
action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles, !this.#allFilesAreExcluded), action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles(), !this.#allFilesAreExcluded()),
tooltip: this.#toggleAnalysisTooltip, tooltip: this.#toggleAnalysisTooltip(),
checked: !this.#allFilesAreExcluded, checked: !this.#allFilesAreExcluded(),
show: this.#canToggleAnalysis, show: this.#canToggleAnalysis(),
}, },
]; ];
return actions.filter(btn => btn.show); return actions.filter(btn => btn.show);
} }
ngOnChanges() {
this._setup();
}
forceReanalysisAction($event: LongPressEvent) { forceReanalysisAction($event: LongPressEvent) {
this.#analysisForced = !$event.touchEnd && this._userPreferenceService.isIqserDevMode; this.#analysisForced.set(!$event.touchEnd && this._userPreferenceService.isIqserDevMode);
this._setup();
} }
private _setup() { async #reanalyseBulk(selectedFiles: File[]) {
if (!this.selectedFiles.length) { const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toaster.error(_('dossier-listing.rules.timeoutError'));
return; return;
} }
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce( await this._bulkActionsService.reanalyse(selectedFiles);
(acc, file) => acc && (file.isUnderReview || file.isNew),
true,
);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreApproved = this.selectedFiles.reduce((acc, file) => acc && file.isApproved, true);
this.#allFilesAreExcluded = this.selectedFiles.reduce((acc, file) => acc && file.excluded, true);
this.#canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval || allFilesAreApproved;
this.#canAssign =
this.#canMoveToSameState &&
(this._permissionsService.canAssignUser(this.selectedFiles, this.dossier) ||
this._permissionsService.canUnassignUser(this.selectedFiles, this.dossier));
this.#canAssignToSelf = this.#canMoveToSameState && this._permissionsService.canAssignToSelf(this.selectedFiles, this.dossier);
this.#canDelete = this._permissionsService.canSoftDeleteFile(this.selectedFiles, this.dossier);
this.#canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles, this.dossier);
this.#canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.selectedFiles, this.dossier);
this.#canOcr = this._permissionsService.canOcrFile(this.selectedFiles, this.dossier);
this.#canSetToNew = this._permissionsService.canSetToNew(this.selectedFiles, this.dossier);
this.#canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles, this.dossier);
this.#canSetToUnderApproval = this._permissionsService.canSetUnderApproval(this.selectedFiles, this.dossier);
this.#isReadyForApproval = this._permissionsService.isReadyForApproval(this.selectedFiles, this.dossier);
this.#canApprove = this._permissionsService.canBeApproved(this.selectedFiles, this.dossier);
this.#canUndoApproval = this._permissionsService.canUndoApproval(this.selectedFiles, this.dossier);
this.#assignTooltip = allFilesAreUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
this.#toggleAnalysisTooltip = this.#allFilesAreExcluded
? _('file-preview.toggle-analysis.enable')
: _('file-preview.toggle-analysis.disable');
this.buttons = this._buttons;
} }
} }

View File

@ -61,7 +61,8 @@
*ngFor="let config of statusConfig" *ngFor="let config of statusConfig"
[attr.help-mode-key]="'dashboard_in_dossier'" [attr.help-mode-key]="'dashboard_in_dossier'"
[config]="config" [config]="config"
filterKey="processingTypeFilters" [class.indent]="!!PendingTypes[config.id]"
[filterKey]="PendingTypes[config.id] ? 'pendingTypeFilters' : 'processingTypeFilters'"
></iqser-progress-bar> ></iqser-progress-bar>
</div> </div>

View File

@ -45,3 +45,7 @@
iqser-progress-bar:not(:last-child) { iqser-progress-bar:not(:last-child) {
margin-bottom: 10px; margin-bottom: 10px;
} }
.indent {
margin-left: 32px;
}

View File

@ -21,7 +21,9 @@ import {
DossierAttributeWithValue, DossierAttributeWithValue,
DossierStats, DossierStats,
File, File,
FileErrorCodes,
IDossierRequest, IDossierRequest,
PendingTypes,
ProcessingTypes, ProcessingTypes,
StatusSorter, StatusSorter,
User, User,
@ -74,6 +76,7 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
#currentChartSubtitleIndex = 0; #currentChartSubtitleIndex = 0;
readonly #dossierId = getParam(DOSSIER_ID); readonly #dossierId = getParam(DOSSIER_ID);
protected readonly circleButtonTypes = CircleButtonTypes; protected readonly circleButtonTypes = CircleButtonTypes;
protected readonly PendingTypes = PendingTypes;
@Input() dossierAttributes: DossierAttributeWithValue[]; @Input() dossierAttributes: DossierAttributeWithValue[];
@Output() readonly toggleCollapse = new EventEmitter(); @Output() readonly toggleCollapse = new EventEmitter();
editingOwner = false; editingOwner = false;
@ -97,13 +100,17 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
super(); super();
const dossier$ = _dossiersService.getEntityChanged$(this.#dossierId).pipe(shareLast()); const dossier$ = _dossiersService.getEntityChanged$(this.#dossierId).pipe(shareLast());
const filesChanged$ = _filesMapService.watchChanged$(this.#dossierId).pipe(shareLast()); const filesChanged$ = _filesMapService.watchChanged$(this.#dossierId).pipe(shareLast());
const files$ = _filesMapService.get$(this.#dossierId).pipe(shareLast());
const dossierStats$ = dossierStatsService.watch$(this.#dossierId).pipe(shareLast()); const dossierStats$ = dossierStatsService.watch$(this.#dossierId).pipe(shareLast());
const dossierStatsWithEffects$ = dossierStats$.pipe( const dossierStatsWithEffects$ = dossierStats$.pipe(
combineLatestWith(filesChanged$), combineLatestWith(filesChanged$),
tap(([stats]) => this.#calculateChartConfig(stats)), tap(([stats]) => this.#calculateChartConfig(stats)),
map(([stats]) => stats), map(([stats]) => stats),
); );
const statusConfig$ = dossierStats$.pipe(map(stats => this.#calculateStatusConfig(stats))); const statusConfig$ = dossierStats$.pipe(
combineLatestWith(files$),
map(([stats, files]) => this.#calculateStatusConfig(stats, files)),
);
super._initContext({ super._initContext({
needsWorkFilters: filterService.getFilterModels$('needsWorkFilters'), needsWorkFilters: filterService.getFilterModels$('needsWorkFilters'),
@ -152,7 +159,8 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
.reduce((sum: number, file: File) => sum + file.numberOfPages, 0); .reduce((sum: number, file: File) => sum + file.numberOfPages, 0);
} }
#calculateStatusConfig(stats: DossierStats): ProgressBarConfigModel[] { #calculateStatusConfig(stats: DossierStats, files: File[]): ProgressBarConfigModel[] {
const numberOfTimeoutFiles = files.filter(file => file.errorCode === FileErrorCodes.RULES_EXECUTION_TIMEOUT).length;
return [ return [
{ {
id: ProcessingTypes.pending, id: ProcessingTypes.pending,
@ -161,6 +169,13 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
count: stats.processingStats.pending, count: stats.processingStats.pending,
icon: 'red:reanalyse', icon: 'red:reanalyse',
}, },
{
id: PendingTypes.timeout,
label: _('processing-status.pending-timeout'),
total: stats.numberOfFiles,
count: numberOfTimeoutFiles,
icon: 'red:reanalyse',
},
{ {
id: ProcessingTypes.ocr, id: ProcessingTypes.ocr,
label: _('processing-status.ocr'), label: _('processing-status.ocr'),

View File

@ -9,7 +9,7 @@
<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>
<span <span
*ngIf="!isDate; else date" *ngIf="!isDate; else date"
[style.max-width]="attributeValueWidth" [style.max-width]="attributeValueWidth()"
[matTooltip]="fileAttributeValue" [matTooltip]="fileAttributeValue"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }" [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
> >
@ -56,13 +56,12 @@
class="edit-input" class="edit-input"
iqserStopPropagation iqserStopPropagation
> >
<form [formGroup]="form"> <form [formGroup]="form" [style.width]="inputFormWidth()">
<iqser-dynamic-input <iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event" (closedDatepicker)="closedDatepicker = $event"
(keyup.enter)="form.valid && save()" (keyup.enter)="form.valid && save()"
(keydown.escape)="close()" (keydown.escape)="close()"
[style.max-width]="editFieldWidth" [style.width]="inputFieldWidth()"
[style.min-width]="editFieldWidth"
[formControlName]="fileAttribute.id" [formControlName]="fileAttribute.id"
[id]="fileAttribute.id" [id]="fileAttribute.id"
[ngClass]="{ 'workflow-input': mode === 'workflow' || fileNameColumn, 'file-name-input': fileNameColumn }" [ngClass]="{ 'workflow-input': mode === 'workflow' || fileNameColumn, 'file-name-input': fileNameColumn }"

View File

@ -97,7 +97,7 @@
iqser-circle-button { iqser-circle-button {
margin-left: 15px; margin-left: 15px;
@media screen and (max-width: 1395px) { @media screen and (max-width: 1745px) {
margin-left: 6px; margin-left: 6px;
} }
} }

View File

@ -1,5 +1,5 @@
import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common'; import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, computed, effect, HostListener, Input, OnDestroy } from '@angular/core'; import { Component, computed, effect, HostListener, input, Input, OnDestroy } from '@angular/core';
import { AbstractControl, FormBuilder, FormsModule, ReactiveFormsModule, UntypedFormGroup, ValidatorFn } from '@angular/forms'; import { AbstractControl, FormBuilder, FormsModule, ReactiveFormsModule, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
@ -14,7 +14,7 @@ import {
StopPropagationDirective, StopPropagationDirective,
Toaster, Toaster,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { Debounce, log } from '@iqser/common-ui/lib/utils'; import { Debounce } from '@iqser/common-ui/lib/utils';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain'; import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@ -49,7 +49,6 @@ import { ConfigService } from '../../config.service';
}) })
export class FileAttributeComponent extends BaseFormComponent implements OnDestroy { export class FileAttributeComponent extends BaseFormComponent implements OnDestroy {
readonly #subscriptions = new Subscription(); readonly #subscriptions = new Subscription();
#widthFactor = window.innerWidth >= 1800 ? 0.85 : 0.7;
isInEditMode = false; isInEditMode = false;
closedDatepicker = true; closedDatepicker = true;
@Input({ required: true }) fileAttribute!: IFileAttributeConfig; @Input({ required: true }) fileAttribute!: IFileAttributeConfig;
@ -66,7 +65,10 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
@Input({ required: true }) dossier!: Dossier; @Input({ required: true }) dossier!: Dossier;
@Input() fileNameColumn = false; @Input() fileNameColumn = false;
readonlyAttrs: string[] = []; readonlyAttrs: string[] = [];
@Input() width?: number; readonly width = input<number>();
readonly inputFormWidth = computed(() => (this.width() ? this.width() + 'px' : 'unset'));
readonly inputFieldWidth = computed(() => (this.width() ? this.width() - 50 + 'px' : 'unset'));
readonly attributeValueWidth = computed(() => (this.width() ? `${this.width() * 0.9}px` : 'unset'));
constructor( constructor(
router: Router, router: Router,
@ -99,14 +101,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
); );
} }
get editFieldWidth(): string {
return this.width ? `${this.width * this.#widthFactor}px` : 'unset';
}
get attributeValueWidth(): string {
return this.width ? `${this.width * 0.9}px` : 'unset';
}
get isDate(): boolean { get isDate(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.DATE; return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
} }
@ -123,15 +117,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id]; return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
} }
@HostListener('window:resize')
onResize() {
if (window.innerWidth >= 1800) {
this.#widthFactor = 0.85;
} else {
this.#widthFactor = 0.7;
}
}
@Debounce(60) @Debounce(60)
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
clickOutside($event: MouseEvent) { clickOutside($event: MouseEvent) {
@ -147,6 +132,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
handleClick($event: MouseEvent) { handleClick($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault();
} }
ngOnDestroy() { ngOnDestroy() {
@ -154,12 +140,14 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
} }
handleFieldClick($event: MouseEvent) { handleFieldClick($event: MouseEvent) {
$event.preventDefault();
if (!this.fileNameColumn) { if (!this.fileNameColumn) {
this.editFileAttribute($event); this.editFileAttribute($event);
} }
} }
editFileAttribute($event: MouseEvent) { editFileAttribute($event: MouseEvent) {
$event.preventDefault();
if ( if (
!this.file.isInitialProcessing && !this.file.isInitialProcessing &&
this.permissionsService.canEditFileAttributes(this.file, this.dossier) && this.permissionsService.canEditFileAttributes(this.file, this.dossier) &&

View File

@ -1,5 +1,5 @@
<iqser-page-header <iqser-page-header
(closeAction)="router.navigate([dossier.dossiersListRouterLink])" (closeAction)="router.navigate([dossier().dossiersListRouterLink])"
[actionConfigs]="actionConfigs" [actionConfigs]="actionConfigs"
[helpModeKey]="'document'" [helpModeKey]="'document'"
[showCloseButton]="true" [showCloseButton]="true"
@ -10,14 +10,14 @@
[attr.help-mode-key]="isDocumine ? 'dossier_download_dossier' : 'download_dossier_in_dossier'" [attr.help-mode-key]="isDocumine ? 'dossier_download_dossier' : 'download_dossier_in_dossier'"
[buttonId]="'download-files-btn'" [buttonId]="'download-files-btn'"
[disabled]="downloadFilesDisabled$ | async" [disabled]="downloadFilesDisabled$ | async"
[dossier]="dossier" [dossier]="dossier()"
[files]="entitiesService.all$ | async" [files]="entitiesService.all$ | async"
dossierDownload dossierDownload
></redaction-file-download-btn> ></redaction-file-download-btn>
<iqser-circle-button <iqser-circle-button
(action)="downloadDossierAsCSV()" (action)="downloadDossierAsCSV()"
*ngIf="permissionsService.canDownloadCsvReport(dossier)" *ngIf="permissionsService.canDownloadCsvReport(dossier())"
[attr.help-mode-key]="'download_csv'" [attr.help-mode-key]="'download_csv'"
[disabled]="listingService.areSomeSelected$ | async" [disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:csv'" [icon]="'iqser:csv'"
@ -26,17 +26,20 @@
<iqser-circle-button <iqser-circle-button
(action)="reanalyseDossier()" (action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier)" *ngIf="permissionsService.displayReanalyseBtn(dossier())"
[disabled]="listingService.areSomeSelected$ | async" [disabled]="(listingService.areSomeSelected$ | async) || areRulesLocked()"
[icon]="'iqser:refresh'" [icon]="'iqser:refresh'"
[tooltipClass]="'small warn'" [tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate" [tooltip]="
(areRulesLocked() ? 'dossier-listing.rules.timeoutError' : 'dossier-overview.new-rule.toast.actions.reanalyse-all')
| translate
"
[type]="circleButtonTypes.warn" [type]="circleButtonTypes.warn"
></iqser-circle-button> ></iqser-circle-button>
<iqser-circle-button <iqser-circle-button
(action)="upload.emit()" (action)="upload.emit()"
*ngIf="permissionsService.canUploadFiles(dossier)" *ngIf="permissionsService.canUploadFiles(dossier())"
[attr.help-mode-key]="'upload_document'" [attr.help-mode-key]="'upload_document'"
[buttonId]="'upload-document-btn'" [buttonId]="'upload-document-btn'"
[icon]="'iqser:upload'" [icon]="'iqser:upload'"

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, EventEmitter, input, OnInit, Output } from '@angular/core';
import { import {
ActionConfig, ActionConfig,
CircleButtonComponent, CircleButtonComponent,
@ -6,7 +6,6 @@ import {
DisableStopPropagationDirective, DisableStopPropagationDirective,
EntitiesService, EntitiesService,
getConfig, getConfig,
IqserAllowDirective,
IqserListingModule, IqserListingModule,
ListingService, ListingService,
LoadingService, LoadingService,
@ -25,12 +24,11 @@ import { Router } from '@angular/router';
import { Roles } from '@users/roles'; import { Roles } from '@users/roles';
import { SortingService } from '@iqser/common-ui/lib/sorting'; import { SortingService } from '@iqser/common-ui/lib/sorting';
import { List, some } from '@iqser/common-ui/lib/utils'; import { List, some } from '@iqser/common-ui/lib/utils';
import { ComponentLogService } from '@services/files/component-log.service';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FileDownloadBtnComponent } from '@shared/components/buttons/file-download-btn/file-download-btn.component'; import { FileDownloadBtnComponent } from '@shared/components/buttons/file-download-btn/file-download-btn.component';
import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-selection.component'; import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-selection.component';
import { RulesService } from '../../../admin/services/rules.service';
@Component({ @Component({
selector: 'redaction-dossier-overview-screen-header [dossier] [upload]', selector: 'redaction-dossier-overview-screen-header [dossier] [upload]',
@ -39,20 +37,17 @@ import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-sel
imports: [ imports: [
IqserListingModule, IqserListingModule,
CircleButtonComponent, CircleButtonComponent,
MatMenuTrigger,
IqserAllowDirective,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
FileDownloadBtnComponent, FileDownloadBtnComponent,
NgIf, NgIf,
ViewModeSelectionComponent, ViewModeSelectionComponent,
DisableStopPropagationDirective, DisableStopPropagationDirective,
MatMenu,
MatMenuItem,
], ],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DossierOverviewScreenHeaderComponent implements OnInit { export class DossierOverviewScreenHeaderComponent implements OnInit {
@Input() dossier: Dossier; readonly dossier = input<Dossier>();
@Output() readonly upload = new EventEmitter<void>(); @Output() readonly upload = new EventEmitter<void>();
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles; readonly roles = Roles;
@ -60,6 +55,9 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
readonly downloadFilesDisabled$: Observable<boolean>; readonly downloadFilesDisabled$: Observable<boolean>;
readonly downloadComponentLogsDisabled$: Observable<boolean>; readonly downloadComponentLogsDisabled$: Observable<boolean>;
readonly isDocumine = getConfig().IS_DOCUMINE; readonly isDocumine = getConfig().IS_DOCUMINE;
readonly areRulesLocked = computed(() => {
return this._rulesService.currentTemplateRules().timeoutDetected;
});
constructor( constructor(
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
@ -73,6 +71,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
private readonly _reanalysisService: ReanalysisService, private readonly _reanalysisService: ReanalysisService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _primaryFileAttributeService: PrimaryFileAttributeService, private readonly _primaryFileAttributeService: PrimaryFileAttributeService,
private readonly _rulesService: RulesService,
) { ) {
const someNotProcessed$ = this.entitiesService.all$.pipe(some(file => !file.lastProcessed)); const someNotProcessed$ = this.entitiesService.all$.pipe(some(file => !file.lastProcessed));
this.downloadFilesDisabled$ = combineLatest([this.listingService.areSomeSelected$, someNotProcessed$]).pipe( this.downloadFilesDisabled$ = combineLatest([this.listingService.areSomeSelected$, someNotProcessed$]).pipe(
@ -84,13 +83,18 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.id, this.listingService.areSomeSelected$); this.actionConfigs = this.configService.actionConfig(this.dossier().id, this.listingService.areSomeSelected$);
} }
async reanalyseDossier() { async reanalyseDossier() {
this._loadingService.start(); this._loadingService.start();
const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toaster.error(_('dossier-listing.rules.timeoutError'));
return;
}
try { try {
await this._reanalysisService.reanalyzeDossier(this.dossier, true); await this._reanalysisService.reanalyzeDossier(this.dossier(), true);
this._toaster.success(_('dossier-overview.reanalyse-dossier.success')); this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) { } catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error')); this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
@ -101,12 +105,12 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
async downloadDossierAsCSV() { async downloadDossierAsCSV() {
const displayedEntities = await firstValueFrom(this.listingService.displayed$); const displayedEntities = await firstValueFrom(this.listingService.displayed$);
const entities = this.sortingService.defaultSort(displayedEntities); const entities = this.sortingService.defaultSort(displayedEntities);
const fileName = this.dossier.dossierName + '.export.csv'; const fileName = this.dossier().dossierName + '.export.csv';
const mapper = (file?: File) => ({ const mapper = (file?: File) => ({
...file, ...file,
hasAnnotations: file.hasRedactions, hasAnnotations: file.hasRedactions,
assignee: this._userService.getName(file.assignee) || '-', assignee: this._userService.getName(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId), primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier().dossierTemplateId),
}); });
const documineOnlyFields = ['hasAnnotations']; const documineOnlyFields = ['hasAnnotations'];
const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions']; const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions'];

View File

@ -2,18 +2,33 @@
<redaction-annotation-icon <redaction-annotation-icon
*ngIf="file.analysisRequired" *ngIf="file.analysisRequired"
[color]="analysisColor$ | async" [color]="analysisColor$ | async"
label="A" [label]="(workloadTranslations['analysis'] | translate)[0]"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="updated"
[color]="updatedColor$ | async"
[label]="(workloadTranslations['updated'] | translate)[0]"
type="square" type="square"
></redaction-annotation-icon> ></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="updated" [color]="updatedColor$ | async" label="U" type="square"></redaction-annotation-icon>
<redaction-annotation-icon <redaction-annotation-icon
*ngIf="file.hasRedactions" *ngIf="file.hasRedactions"
[color]="redactionColor$ | async" [color]="redactionColor$ | async"
[label]="'redaction-abbreviation' | translate" [label]="'redaction-abbreviation' | translate"
type="square" type="square"
></redaction-annotation-icon> ></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file.hasImages" [color]="imageColor$ | async" label="I" type="square"></redaction-annotation-icon> <redaction-annotation-icon
<redaction-annotation-icon *ngIf="file.hintsOnly" [color]="hintColor$ | async" label="H" type="circle"></redaction-annotation-icon> *ngIf="file.hasImages"
[color]="imageColor$ | async"
[label]="(workloadTranslations['image'] | translate)[0]"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="file.hintsOnly"
[color]="hintColor$ | async"
[label]="(workloadTranslations['hint'] | translate)[0]"
type="circle"
></redaction-annotation-icon>
<mat-icon *ngIf="file.hasAnnotationComments" svgIcon="red:comment"></mat-icon> <mat-icon *ngIf="file.hasAnnotationComments" svgIcon="red:comment"></mat-icon>
<ng-container *ngIf="noWorkloadItems"> -</ng-container> <ng-container *ngIf="noWorkloadItems"> -</ng-container>
</div> </div>

View File

@ -10,6 +10,7 @@ import { AnnotationIconComponent } from '@shared/components/annotation-icon/anno
import { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { workloadTranslations } from '@translations/workload-translations';
@Component({ @Component({
selector: 'redaction-file-workload', selector: 'redaction-file-workload',
@ -27,6 +28,7 @@ export class FileWorkloadComponent implements OnInit {
analysisColor$: Observable<string>; analysisColor$: Observable<string>;
hintColor$: Observable<string>; hintColor$: Observable<string>;
redactionColor$: Observable<string>; redactionColor$: Observable<string>;
readonly workloadTranslations = workloadTranslations;
constructor( constructor(
private readonly _userService: UserService, private readonly _userService: UserService,

View File

@ -39,7 +39,12 @@
</ng-container> </ng-container>
<div [class.extend-cols]="file.isError" class="status-container cell"> <div [class.extend-cols]="file.isError" class="status-container cell">
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div> <div
*ngIf="file.isError"
class="small-label error"
translate="dossier-overview.file-listing.file-entry.file-error"
[translateParams]="{ errorCode: file.errorCode }"
></div>
<div *ngIf="file.isUnprocessed" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div> <div *ngIf="file.isUnprocessed" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>

View File

@ -24,6 +24,7 @@ import {
FileAttributeConfigType, FileAttributeConfigType,
FileAttributeConfigTypes, FileAttributeConfigTypes,
IFileAttributeConfig, IFileAttributeConfig,
PendingType,
ProcessingType, ProcessingType,
StatusSorter, StatusSorter,
User, User,
@ -184,6 +185,7 @@ export class ConfigService {
const allDistinctPeople = new Set<string>(); const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>(); const allDistinctNeedsWork = new Set<string>();
const allDistinctProcessingTypes = new Set<ProcessingType>(); const allDistinctProcessingTypes = new Set<ProcessingType>();
const allDistinctPendingTypes = new Set<PendingType>();
const dynamicFilters = new Map<string, { type: FileAttributeConfigType; filterValue: Set<string> }>(); const dynamicFilters = new Map<string, { type: FileAttributeConfigType; filterValue: Set<string> }>();
@ -216,6 +218,7 @@ export class ConfigService {
} }
allDistinctProcessingTypes.add(file.processingType); allDistinctProcessingTypes.add(file.processingType);
allDistinctPendingTypes.add(file.pendingType);
// extract values for dynamic filters // extract values for dynamic filters
fileAttributeConfigs.forEach(config => { fileAttributeConfigs.forEach(config => {
@ -317,6 +320,14 @@ export class ConfigService {
hide: true, hide: true,
}); });
const pendingTypesFilters = [...allDistinctPendingTypes].map(item => new NestedFilter({ id: item, label: item }));
filterGroups.push({
slug: 'pendingTypeFilters',
filters: pendingTypesFilters,
checker: (file: File, filter: INestedFilter) => file.pendingType === filter.id,
hide: true,
});
dynamicFilters.forEach((value: { filterValue: Set<string>; type: FileAttributeConfigType }, filterKey: string) => { dynamicFilters.forEach((value: { filterValue: Set<string>; type: FileAttributeConfigType }, filterKey: string) => {
const id = filterKey.split(':')[0]; const id = filterKey.split(':')[0];
const key = filterKey.split(':')[1]; const key = filterKey.split(':')[1];

View File

@ -41,7 +41,7 @@ import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service'; import { UserPreferenceService } from '@users/user-preference.service';
import { convertFiles, Files, handleFileDrop } from '@utils/index'; import { convertFiles, Files, handleFileDrop } from '@utils/index';
import { merge, Observable } from 'rxjs'; import { merge, Observable } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators'; import { filter, map, 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'; import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
@ -145,8 +145,9 @@ export default class DossierOverviewScreenComponent extends ListingComponent<Fil
get #dossierFilesChange$() { get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe( return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId && !!this._dossiersCacheService.get(dossierId)), map(changes => changes[this.dossierId]),
switchMap(dossierId => this._filesService.loadAll(dossierId)), filter(changes => !!changes && !!this._dossiersCacheService.get(this.dossierId)),
switchMap(changes => this._filesService.loadByIds({ [this.dossierId]: changes }).pipe(map(files => files[this.dossierId]))),
); );
} }

View File

@ -113,11 +113,13 @@ export class BulkActionsService {
async approve(files: File[]): Promise<void> { async approve(files: File[]): Promise<void> {
this._loadingService.start(); this._loadingService.start();
const approvalResponse: ApproveResponse[] = await this._filesService.getApproveWarnings(files); const approvalResponse: ApproveResponse[] = await this._filesService.getApproveWarnings(files);
this._loadingService.stop();
const hasWarnings = approvalResponse.some(response => response.hasWarnings); const hasWarnings = approvalResponse.some(response => response.hasWarnings);
if (!hasWarnings) { if (!hasWarnings) {
await firstValueFrom(this._filesService.loadAll(files[0].dossierId));
this._loadingService.stop();
return; return;
} }
this._loadingService.stop();
const fileWarnings = approvalResponse const fileWarnings = approvalResponse
.filter(response => response.hasWarnings) .filter(response => response.hasWarnings)

View File

@ -15,7 +15,6 @@ import {
import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service'; import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { PermissionsService } from '@services/permissions.service';
import { SharedDialogService } from '@shared/services/dialog.service'; import { SharedDialogService } from '@shared/services/dialog.service';
import { workflowFileStatusTranslations } from '@translations/file-status-translations'; import { workflowFileStatusTranslations } from '@translations/file-status-translations';
import { workloadTranslations } from '@translations/workload-translations'; import { workloadTranslations } from '@translations/workload-translations';
@ -40,7 +39,6 @@ export class ConfigService {
private readonly _dossierStatsService: DossierStatsService, private readonly _dossierStatsService: DossierStatsService,
private readonly _dossierStatesMapService: DossierStatesMapService, private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dialogService: SharedDialogService, private readonly _dialogService: SharedDialogService,
private readonly _permissionsService: PermissionsService,
private readonly _defaultColorsService: DefaultColorsService, private readonly _defaultColorsService: DefaultColorsService,
) {} ) {}
@ -67,9 +65,10 @@ export class ConfigService {
{ {
label: _('dossier-listing.add-new'), label: _('dossier-listing.add-new'),
action: () => this.#openAddDossierDialog(dossierTemplate.id), action: () => this.#openAddDossierDialog(dossierTemplate.id),
hide: !this._permissionsService.canCreateDossier(dossierTemplate),
icon: 'iqser:plus', icon: 'iqser:plus',
type: 'primary', type: 'primary',
tooltip: dossierTemplate.isInactive ? _('dossier-listing.template-inactive') : null,
disabled: dossierTemplate.isInactive,
helpModeKey: 'new_dossier', helpModeKey: 'new_dossier',
}, },
]; ];

View File

@ -1,5 +1,5 @@
<section> <section>
<iqser-page-header [buttonConfigs]="buttonConfigs" [helpModeKey]="'dossier'"> <iqser-page-header [buttonConfigs]="buttonConfigs()" [helpModeKey]="'dossier'">
<ng-container slot="beforeFilters"> <ng-container slot="beforeFilters">
<redaction-dossiers-type-switch></redaction-dossiers-type-switch> <redaction-dossiers-type-switch></redaction-dossiers-type-switch>
</ng-container> </ng-container>
@ -18,7 +18,7 @@
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate" [noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
[noDataText]="'dossier-listing.no-data.title' | translate" [noDataText]="'dossier-listing.no-data.title' | translate"
[noMatchText]="'dossier-listing.no-match.title' | translate" [noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="permissionsService.canCreateDossier(dossierTemplate)" [showNoDataButton]="permissionsService.canCreateDossier(dossierTemplate())"
[tableColumnConfigs]="tableColumnConfigs" [tableColumnConfigs]="tableColumnConfigs"
[rowIdPrefix]="'dossier'" [rowIdPrefix]="'dossier'"
[namePropertyKey]="'dossierName'" [namePropertyKey]="'dossierName'"
@ -33,7 +33,7 @@
</section> </section>
<ng-template #needsWorkFilterTemplate let-filter="filter"> <ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [dossierTemplateId]="dossierTemplate.id" [filter]="filter"></redaction-type-filter> <redaction-type-filter [dossierTemplateId]="dossierTemplate().id" [filter]="filter"></redaction-type-filter>
</ng-template> </ng-template>
<ng-template #tableItemTemplate let-dossier="entity"> <ng-template #tableItemTemplate let-dossier="entity">

View File

@ -1,8 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DOSSIER_TEMPLATE_ID, DossierTemplate } from '@red/domain'; import { Dossier, DOSSIER_TEMPLATE_ID } from '@red/domain';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { import {
ButtonConfig,
HasScrollbarDirective, HasScrollbarDirective,
IqserListingModule, IqserListingModule,
ListingComponent, ListingComponent,
@ -25,6 +24,7 @@ import { DossiersListingDetailsComponent } from '../components/dossiers-listing-
import { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component'; import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
import { TableItemComponent } from '../components/table-item/table-item.component'; import { TableItemComponent } from '../components/table-item/table-item.component';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ @Component({
templateUrl: './dossiers-listing-screen.component.html', templateUrl: './dossiers-listing-screen.component.html',
@ -47,8 +47,10 @@ import { TableItemComponent } from '../components/table-item/table-item.componen
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach { export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach {
readonly tableColumnConfigs = this._configService.tableConfig; readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title'); readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: ButtonConfig[]; readonly dossierTemplateId = this.router.routerState.snapshot.root.firstChild.firstChild.paramMap.get(DOSSIER_TEMPLATE_ID);
readonly dossierTemplate: DossierTemplate; readonly dossierTemplates = toSignal(this.dossierTemplatesService.all$);
readonly dossierTemplate = computed(() => this.dossierTemplates().find(template => this.dossierTemplateId === template.id));
readonly buttonConfigs = computed(() => this._configService.buttonsConfig(this.dossierTemplate()));
readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters())); readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters()));
@ViewChild('needsWorkFilterTemplate', { @ViewChild('needsWorkFilterTemplate', {
read: TemplateRef, read: TemplateRef,
@ -68,20 +70,19 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
readonly dossierTemplatesService: DossierTemplatesService, readonly dossierTemplatesService: DossierTemplatesService,
) { ) {
super(); super();
const dossierTemplateId = router.routerState.snapshot.root.firstChild.firstChild.paramMap.get(DOSSIER_TEMPLATE_ID); this.entitiesService.setEntities(this._activeDossiersService.all.filter(d => d.dossierTemplateId === this.dossierTemplate().id));
this.dossierTemplate = dossierTemplatesService.find(dossierTemplateId);
this.buttonConfigs = this._configService.buttonsConfig(this.dossierTemplate);
this.entitiesService.setEntities(this._activeDossiersService.all.filter(d => d.dossierTemplateId === this.dossierTemplate.id));
} }
openAddDossierDialog(): void { openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', { dossierTemplateId: this.dossierTemplate.id }); this._dialogService.openDialog('addDossier', { dossierTemplateId: this.dossierTemplate().id });
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
await this._userPreferenceService.saveLastDossierTemplate(this.dossierTemplate.id); await this._userPreferenceService.saveLastDossierTemplate(this.dossierTemplate().id);
this.addSubscription = this._activeDossiersService.all$ this.addSubscription = this._activeDossiersService.all$
.pipe(tap(dossiers => this.entitiesService.setEntities(dossiers.filter(d => d.dossierTemplateId === this.dossierTemplate.id)))) .pipe(
tap(dossiers => this.entitiesService.setEntities(dossiers.filter(d => d.dossierTemplateId === this.dossierTemplate().id))),
)
.subscribe(); .subscribe();
this._loadingService.stop(); this._loadingService.stop();
} }
@ -95,7 +96,7 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
const filterGroups = this._configService.filterGroups( const filterGroups = this._configService.filterGroups(
this.entitiesService.all, this.entitiesService.all,
this._needsWorkFilterTemplate, this._needsWorkFilterTemplate,
this.dossierTemplate.id, this.dossierTemplate().id,
); );
this.filterService.addFilterGroups(filterGroups); this.filterService.addFilterGroups(filterGroups);
} }

View File

@ -133,14 +133,14 @@ export class AnnotationActionsComponent {
const viewerAnnotations = untracked(this.viewerAnnotations); const viewerAnnotations = untracked(this.viewerAnnotations);
this._annotationManager.hide(viewerAnnotations); this._annotationManager.hide(viewerAnnotations);
this._annotationManager.deselect(); this._annotationManager.deselect();
this._annotationManager.addToHidden(viewerAnnotations[0].Id); viewerAnnotations.forEach(a => this._annotationManager.addToHidden(a.Id));
} }
showAnnotation() { showAnnotation() {
const viewerAnnotations = untracked(this.viewerAnnotations); const viewerAnnotations = untracked(this.viewerAnnotations);
this._annotationManager.show(viewerAnnotations); this._annotationManager.show(viewerAnnotations);
this._annotationManager.deselect(); this._annotationManager.deselect();
this._annotationManager.removeFromHidden(viewerAnnotations[0].Id); viewerAnnotations.forEach(a => this._annotationManager.removeFromHidden(a.Id));
} }
resize() { resize() {

View File

@ -6,10 +6,9 @@
overflow-y: auto; overflow-y: auto;
@include common-mixins.scroll-bar; @include common-mixins.scroll-bar;
&.has-scrollbar:hover redaction-annotation-wrapper::ng-deep, redaction-annotation-wrapper.documine-wrapper {
&::ng-deep.documine-wrapper { &::ng-deep.annotation {
.annotation { padding-right: 10px;
padding-right: 5px;
} }
} }
} }

View File

@ -15,6 +15,7 @@ import { JsonPipe, NgForOf, NgIf } from '@angular/common';
import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component'; import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component';
import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component'; import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component';
import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component'; import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component';
import { Clipboard } from '@angular/cdk/clipboard';
@Component({ @Component({
selector: 'redaction-annotations-list', selector: 'redaction-annotations-list',
@ -54,11 +55,15 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
private readonly _annotationManager: REDAnnotationManager, private readonly _annotationManager: REDAnnotationManager,
private readonly _listingService: AnnotationsListingService, private readonly _listingService: AnnotationsListingService,
readonly annotationReferencesService: AnnotationReferencesService, readonly annotationReferencesService: AnnotationReferencesService,
private readonly clipboard: Clipboard,
) { ) {
super(_elementRef); super(_elementRef);
} }
annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void { annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void {
if ($event.ctrlKey && $event.altKey) {
this.clipboard.copy(annotation.id);
}
if (this._userPreferenceService.isIqserDevMode) { if (this._userPreferenceService.isIqserDevMode) {
console.log('Selected Annotation:', annotation); console.log('Selected Annotation:', annotation);
} }

View File

@ -1,10 +1,10 @@
import { Component, input, Input } from '@angular/core'; import { Component, input, Input } from '@angular/core';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { Dossier, File } from '@red/domain'; import { Dossier, File } from '@red/domain';
import { ComponentLogService } from '@services/files/component-log.service';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { ComponentLogService } from '@services/entity-services/component-log.service';
@Component({ @Component({
selector: 'redaction-documine-export', selector: 'redaction-documine-export',

View File

@ -88,7 +88,7 @@ export class EditableStructuredComponentValueComponent implements OnInit {
} }
this.deselectLast.emit(); this.deselectLast.emit();
this.selected = true; this.selected = true;
this._state.componentReferenceIds = this.#getUniqueReferencesIds(this.currentEntry().componentValues); this._state.componentReferenceIds.set(this.#getUniqueReferencesIds(this.currentEntry().componentValues));
} }
} }
@ -104,7 +104,7 @@ export class EditableStructuredComponentValueComponent implements OnInit {
$event?.stopImmediatePropagation(); $event?.stopImmediatePropagation();
this.selected = false; this.selected = false;
this.editing = false; this.editing = false;
this._state.componentReferenceIds = null; this._state.componentReferenceIds.set(null);
} }
cancel($event?: MouseEvent) { cancel($event?: MouseEvent) {

View File

@ -19,7 +19,6 @@ import {
getConfig, getConfig,
HelpModeService, HelpModeService,
IqserAllowDirective, IqserAllowDirective,
IqserDialog,
IqserPermissionsService, IqserPermissionsService,
isIqserDevMode, isIqserDevMode,
LoadingService, LoadingService,

View File

@ -146,7 +146,7 @@
id="annotations-list" id="annotations-list"
tabindex="1" tabindex="1"
> >
<ng-container *ngIf="pdf.currentPage() && !displayedAnnotations.get(pdf.currentPage())?.length"> <ng-container *ngIf="pdf.currentPage() && !displayedAnnotations().get(pdf.currentPage())?.length">
<iqser-empty-state <iqser-empty-state
[horizontalPadding]="24" [horizontalPadding]="24"
[text]="'file-preview.no-data.title' | translate" [text]="'file-preview.no-data.title' | translate"

View File

@ -8,6 +8,7 @@ import {
HostListener, HostListener,
OnDestroy, OnDestroy,
OnInit, OnInit,
signal,
TemplateRef, TemplateRef,
untracked, untracked,
viewChild, viewChild,
@ -107,8 +108,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
const file = this.state.file(); const file = this.state.file();
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED; return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}); });
protected displayedAnnotations = new Map<number, AnnotationWrapper[]>(); protected displayedAnnotations = signal(new Map<number, AnnotationWrapper[]>());
readonly activeAnnotations = computed(() => this.displayedAnnotations.get(this.pdf.currentPage()) || []); readonly activeAnnotations = computed(() => this.displayedAnnotations().get(this.pdf.currentPage()) || []);
protected displayedPages: number[] = []; protected displayedPages: number[] = [];
protected pagesPanelActive = true; protected pagesPanelActive = true;
protected enabledFilters = []; protected enabledFilters = [];
@ -362,11 +363,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
if ($event.key === 'ArrowDown') { if ($event.key === 'ArrowDown') {
const nextPage = this.#nextPageWithAnnotations(); const nextPage = this.#nextPageWithAnnotations();
return this.listingService.selectAnnotations(this.displayedAnnotations.get(nextPage)[0]); return this.listingService.selectAnnotations(this.displayedAnnotations().get(nextPage)[0]);
} }
const prevPage = this.#prevPageWithAnnotations(); const prevPage = this.#prevPageWithAnnotations();
const prevPageAnnotations = this.displayedAnnotations.get(prevPage); const prevPageAnnotations = this.displayedAnnotations().get(prevPage);
return this.listingService.selectAnnotations(getLast(prevPageAnnotations)); return this.listingService.selectAnnotations(getLast(prevPageAnnotations));
} }
@ -375,7 +376,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
const pageIdx = this.displayedPages.indexOf(page); const pageIdx = this.displayedPages.indexOf(page);
const nextPageIdx = pageIdx + 1; const nextPageIdx = pageIdx + 1;
const previousPageIdx = pageIdx - 1; const previousPageIdx = pageIdx - 1;
const annotationsOnPage = this.displayedAnnotations.get(page); const annotationsOnPage = this.displayedAnnotations().get(page);
const idx = annotationsOnPage.findIndex(a => a.id === this._firstSelectedAnnotation.id); const idx = annotationsOnPage.findIndex(a => a.id === this._firstSelectedAnnotation.id);
if ($event.key === 'ArrowDown') { if ($event.key === 'ArrowDown') {
@ -385,7 +386,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
} else if (nextPageIdx < this.displayedPages.length) { } else if (nextPageIdx < this.displayedPages.length) {
// If not last page // If not last page
for (let i = nextPageIdx; i < this.displayedPages.length; i++) { for (let i = nextPageIdx; i < this.displayedPages.length; i++) {
const nextPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]); const nextPageAnnotations = this.displayedAnnotations().get(this.displayedPages[i]);
if (nextPageAnnotations) { if (nextPageAnnotations) {
this.listingService.selectAnnotations(nextPageAnnotations[0]); this.listingService.selectAnnotations(nextPageAnnotations[0]);
break; break;
@ -403,7 +404,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
if (pageIdx) { if (pageIdx) {
// If not first page // If not first page
for (let i = previousPageIdx; i >= 0; i--) { for (let i = previousPageIdx; i >= 0; i--) {
const prevPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]); const prevPageAnnotations = this.displayedAnnotations().get(this.displayedPages[i]);
if (prevPageAnnotations) { if (prevPageAnnotations) {
this.listingService.selectAnnotations(getLast(prevPageAnnotations)); this.listingService.selectAnnotations(getLast(prevPageAnnotations));
break; break;
@ -451,8 +452,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
} }
} }
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary); this.displayedAnnotations.set(this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary));
const pagesThatDisplayAnnotations = [...this.displayedAnnotations.keys()]; const pagesThatDisplayAnnotations = [...this.displayedAnnotations().keys()];
this.enabledFilters = this.filterService.enabledFlatFilters; this.enabledFilters = this.filterService.enabledFlatFilters;
if (this.enabledFilters.some(f => f.id === 'pages-without-annotations')) { if (this.enabledFilters.some(f => f.id === 'pages-without-annotations')) {
if (this.enabledFilters.length === 1 && !onlyPageWithAnnotations) { if (this.enabledFilters.length === 1 && !onlyPageWithAnnotations) {
@ -461,7 +462,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
} else { } else {
this.#setDisplayedPages([]); this.#setDisplayedPages([]);
} }
this.displayedAnnotations.clear(); this.displayedAnnotations().clear();
} else if (this.enabledFilters.length || onlyPageWithAnnotations || componentReferenceIds) { } else if (this.enabledFilters.length || onlyPageWithAnnotations || componentReferenceIds) {
this.#setDisplayedPages(pagesThatDisplayAnnotations); this.#setDisplayedPages(pagesThatDisplayAnnotations);
} else { } else {
@ -469,7 +470,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
} }
this.displayedPages.sort((a, b) => a - b); this.displayedPages.sort((a, b) => a - b);
return this.displayedAnnotations; return this.displayedAnnotations();
} }
#selectFirstAnnotationOnCurrentPageIfNecessary() { #selectFirstAnnotationOnCurrentPageIfNecessary() {
@ -521,7 +522,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
const currentPage = untracked(this.pdf.currentPage); const currentPage = untracked(this.pdf.currentPage);
let idx = 0; let idx = 0;
for (const page of this.displayedPages) { for (const page of this.displayedPages) {
if (page > currentPage && this.displayedAnnotations.get(page)) { if (page > currentPage && this.displayedAnnotations().get(page)) {
break; break;
} }
++idx; ++idx;
@ -534,7 +535,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
let idx = this.displayedPages.length - 1; let idx = this.displayedPages.length - 1;
const reverseDisplayedPages = [...this.displayedPages].reverse(); const reverseDisplayedPages = [...this.displayedPages].reverse();
for (const page of reverseDisplayedPages) { for (const page of reverseDisplayedPages) {
if (page < currentPage && this.displayedAnnotations.get(page)) { if (page < currentPage && this.displayedAnnotations().get(page)) {
break; break;
} }
--idx; --idx;
@ -588,4 +589,16 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
this.displayedPages.every((value, index) => value === newDisplayedPages[index]) this.displayedPages.every((value, index) => value === newDisplayedPages[index])
); );
} }
@HostListener('click', ['$event'])
clickInsideWorkloadView($event: MouseEvent) {
$event?.stopPropagation();
}
@HostListener('document: click')
clickOutsideWorkloadView() {
if (this.multiSelectService.active() && !this._dialog.openDialogs.length) {
this.multiSelectService.deactivate();
}
}
} }

View File

@ -34,12 +34,14 @@ export class PagesComponent implements AfterViewInit {
// TODO: looks like this is not working // TODO: looks like this is not working
scrollToLastViewedPage() { scrollToLastViewedPage() {
const currentPdfPage = this._pdf.currentPage(); const currentPdfPage = this._pdf.currentPage();
scrollIntoView(document.getElementById(`quick-nav-page-${currentPdfPage}`), { const currentElement = document.getElementById(`quick-nav-page-${currentPdfPage}`);
behavior: 'smooth', if (currentElement)
scrollMode: 'if-needed', scrollIntoView(currentElement, {
block: 'start', behavior: 'smooth',
inline: 'start', scrollMode: 'if-needed',
}); block: 'start',
inline: 'start',
});
} }
pageSelectedByClick($event: number): void { pageSelectedByClick($event: number): void {

View File

@ -3,7 +3,7 @@
<iqser-popup-filter [primaryFiltersSlug]="'componentLogFilters'" [attr.help-mode-key]="'filter_components'"></iqser-popup-filter> <iqser-popup-filter [primaryFiltersSlug]="'componentLogFilters'" [attr.help-mode-key]="'filter_components'"></iqser-popup-filter>
</div> </div>
<div *ngIf="displayedComponents$ | async as displayedComponents" class="components-container" id="components-view"> <div *ngIf="componentLogService.all$ | async as components" class="components-container" id="components-view">
<div class="component-row"> <div class="component-row">
<div class="header"> <div class="header">
<div class="component">{{ 'component-management.table-header.component' | translate }}</div> <div class="component">{{ 'component-management.table-header.component' | translate }}</div>
@ -12,7 +12,7 @@
<div class="row-separator"></div> <div class="row-separator"></div>
</div> </div>
<div *ngFor="let entry of displayedComponents" class="component-row"> <div *ngFor="let entry of components" class="component-row">
<redaction-editable-structured-component-value <redaction-editable-structured-component-value
#editableComponent #editableComponent
[entry]="entry" [entry]="entry"

View File

@ -2,16 +2,14 @@ import { Component, effect, Input, OnInit, signal, ViewChildren } from '@angular
import { List } from '@common-ui/utils'; import { List } from '@common-ui/utils';
import { IconButtonTypes, LoadingService } from '@iqser/common-ui'; import { IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { ComponentLogEntry, Dictionary, File, IComponentLogEntry, WorkflowFileStatuses } from '@red/domain'; import { ComponentLogEntry, Dictionary, File, IComponentLogEntry, WorkflowFileStatuses } from '@red/domain';
import { ComponentLogService } from '@services/files/component-log.service';
import { combineLatest, firstValueFrom, Observable } from 'rxjs'; import { combineLatest, firstValueFrom, Observable } from 'rxjs';
import { EditableStructuredComponentValueComponent } from '../editable-structured-component-value/editable-structured-component-value.component'; import { EditableStructuredComponentValueComponent } from '../editable-structured-component-value/editable-structured-component-value.component';
import { FilterService, PopupFilterComponent } from '@common-ui/filtering'; import { FilterService, PopupFilterComponent } from '@common-ui/filtering';
import { ComponentLogFilterService } from '../../services/component-log-filter.service'; import { ComponentLogFilterService } from '../../services/component-log-filter.service';
import { map } from 'rxjs/operators';
import { toObservable } from '@angular/core/rxjs-interop';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common'; import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { ComponentLogService } from '@services/entity-services/component-log.service';
@Component({ @Component({
selector: 'redaction-structured-component-management', selector: 'redaction-structured-component-management',
@ -21,20 +19,17 @@ import { FilePreviewStateService } from '../../services/file-preview-state.servi
imports: [PopupFilterComponent, NgIf, AsyncPipe, TranslateModule, NgForOf, EditableStructuredComponentValueComponent], imports: [PopupFilterComponent, NgIf, AsyncPipe, TranslateModule, NgForOf, EditableStructuredComponentValueComponent],
}) })
export class StructuredComponentManagementComponent implements OnInit { export class StructuredComponentManagementComponent implements OnInit {
protected readonly componentLogData = signal<ComponentLogEntry[] | undefined>(undefined);
protected readonly componentLogData$ = toObservable(this.componentLogData);
protected readonly iconButtonTypes = IconButtonTypes; protected readonly iconButtonTypes = IconButtonTypes;
protected displayedComponents$: Observable<ComponentLogEntry[]>;
@Input() file: File; @Input() file: File;
@Input() dictionaries: Dictionary[]; @Input() dictionaries: Dictionary[];
@ViewChildren('editableComponent') editableComponents: List<EditableStructuredComponentValueComponent>; @ViewChildren('editableComponent') editableComponents: List<EditableStructuredComponentValueComponent>;
constructor( constructor(
private readonly _componentLogService: ComponentLogService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _componentLogFilterService: ComponentLogFilterService, private readonly _componentLogFilterService: ComponentLogFilterService,
private readonly _filterService: FilterService, private readonly _filterService: FilterService,
private readonly _state: FilePreviewStateService, private readonly _state: FilePreviewStateService,
protected readonly componentLogService: ComponentLogService,
) { ) {
effect(async () => { effect(async () => {
this._state.file(); this._state.file();
@ -48,7 +43,6 @@ export class StructuredComponentManagementComponent implements OnInit {
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
await this.#loadData(); await this.#loadData();
this.displayedComponents$ = this.#displayedComponents$();
} }
deselectLast() { deselectLast() {
@ -61,7 +55,7 @@ export class StructuredComponentManagementComponent implements OnInit {
async revertOverride(originalKey: string) { async revertOverride(originalKey: string) {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom( await firstValueFrom(
this._componentLogService.revertOverride(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, [originalKey]), this.componentLogService.revertOverride(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, [originalKey]),
); );
await this.#loadData(); await this.#loadData();
} }
@ -69,29 +63,21 @@ export class StructuredComponentManagementComponent implements OnInit {
async overrideValue(componentLogEntry: IComponentLogEntry) { async overrideValue(componentLogEntry: IComponentLogEntry) {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom( await firstValueFrom(
this._componentLogService.override(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, componentLogEntry), this.componentLogService.override(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, componentLogEntry),
); );
await this.#loadData(); await this.#loadData();
} }
#displayedComponents$() {
const componentLogFilters$ = this._filterService.getFilterModels$('componentLogFilters');
return combineLatest([this.componentLogData$, componentLogFilters$]).pipe(
map(([components, filters]) => this._componentLogFilterService.filterComponents(components, filters)),
);
}
async #loadData(): Promise<void> { async #loadData(): Promise<void> {
const componentLogData = await firstValueFrom( await firstValueFrom(
this._componentLogService.getComponentLogData( this.componentLogService.loadComponentLogData(
this.file.dossierTemplateId, this.file.dossierTemplateId,
this.file.dossierId, this.file.dossierId,
this.file.fileId, this.file.fileId,
this.dictionaries, this.dictionaries,
), ),
); );
this.#computeFilters(componentLogData); this.#computeFilters(this.componentLogService.all);
this.componentLogData.set(componentLogData);
this._loadingService.stop(); this._loadingService.stop();
} }

View File

@ -6,7 +6,7 @@
class="dialog-header heading-l" class="dialog-header heading-l"
></div> ></div>
<div [class.fixed-height]="isRedacted && isImage" class="dialog-content redaction"> <div [class.image-dialog]="isRedacted && isImage" [class.rectangle-dialog]="allRectangles" class="dialog-content redaction">
<div *ngIf="!isImage && redactedTexts.length && !allRectangles" class="iqser-input-group"> <div *ngIf="!isImage && redactedTexts.length && !allRectangles" class="iqser-input-group">
<redaction-selected-annotations-table <redaction-selected-annotations-table
[columns]="tableColumns" [columns]="tableColumns"
@ -90,7 +90,7 @@
</div> </div>
</ng-container> </ng-container>
<div *ngIf="allRectangles" class="iqser-input-group w-400"> <div *ngIf="allRectangles" class="iqser-input-group w-450">
<label [translate]="'change-legal-basis-dialog.content.classification'"></label> <label [translate]="'change-legal-basis-dialog.content.classification'"></label>
<input <input
[placeholder]="'edit-redaction.dialog.content.unchanged' | translate" [placeholder]="'edit-redaction.dialog.content.unchanged' | translate"
@ -107,7 +107,7 @@
formControlName="comment" formControlName="comment"
iqserHasScrollbar iqserHasScrollbar
name="comment" name="comment"
rows="4" rows="3"
type="text" type="text"
></textarea> ></textarea>
</div> </div>

View File

@ -1,8 +1,16 @@
.dialog-content { .dialog-content {
padding-top: 8px; padding-top: 8px;
&.fixed-height { &.rectangle-dialog {
height: 386px; height: 600px;
}
&.image-dialog {
height: 346px;
}
.rectangle-dialog,
.image-dialog {
overflow-y: auto; overflow-y: auto;
} }
} }

View File

@ -33,11 +33,12 @@ import {
LegalBasisOption, LegalBasisOption,
RectangleRedactOption, RectangleRedactOption,
RectangleRedactOptions, RectangleRedactOptions,
RedactOrHintOptions,
} from '../../utils/dialog-types'; } from '../../utils/dialog-types';
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component'; import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option'; import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import { validatePageRange } from '../../utils/form-validators'; import { validatePageRange } from '../../utils/form-validators';
import { parseRectanglePosition, parseSelectedPageNumbers } from '../../utils/enhance-manual-redaction-request.utils'; import { parseRectanglePosition, parseSelectedPageNumbers, prefillPageRange } from '../../utils/enhance-manual-redaction-request.utils';
interface TypeSelectOptions { interface TypeSelectOptions {
type: string; type: string;
@ -81,14 +82,14 @@ export class EditRedactionDialogComponent
readonly isManualRedaction = this.annotations.some(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 || annotation.type === 'imported_redaction');
readonly allRectangles = this.annotations.reduce((acc, a) => acc && a.AREA, true); readonly allRectangles = this.annotations.reduce((acc, a) => acc && a.AREA, true);
readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }]; readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }];
readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [ readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [
{ label: redaction.value, bold: true }, { label: redaction.value, bold: true },
{ label: redaction.typeLabel }, { label: redaction.typeLabel },
]); ]);
options = this.allRectangles ? getRectangleRedactOptions('edit') : getEditRedactionOptions(); options = this.allRectangles ? getRectangleRedactOptions('edit', this.data.annotations) : getEditRedactionOptions(this.isHint);
legalOptions: LegalBasisOption[] = []; legalOptions: LegalBasisOption[] = [];
dictionaries: Dictionary[] = []; dictionaries: Dictionary[] = [];
typeSelectOptions: TypeSelectOptions[] = []; typeSelectOptions: TypeSelectOptions[] = [];
@ -103,6 +104,13 @@ export class EditRedactionDialogComponent
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
) { ) {
super(); super();
if (this.allRectangles && !this.isImported) {
prefillPageRange(
this.data.annotations[0],
this.data.allFileAnnotations,
this.options as DetailsRadioOption<RectangleRedactOption>[],
);
}
} }
get displayedDictionaryLabel() { get displayedDictionaryLabel() {
@ -205,11 +213,7 @@ export class EditRedactionDialogComponent
const value = this.form.value; const value = this.form.value;
const initialReason: LegalBasisOption = this.initialFormValue.reason; const initialReason: LegalBasisOption = this.initialFormValue.reason;
const initialLegalBasis = initialReason?.legalBasis ?? ''; const initialLegalBasis = initialReason?.legalBasis ?? '';
const pageNumbers = parseSelectedPageNumbers( const pageNumbers = parseSelectedPageNumbers(this.form.get('option').value?.additionalInput?.value, this.data.file);
this.form.get('option').value.additionalInput?.value,
this.data.file,
this.data.annotations[0],
);
const position = parseRectanglePosition(this.annotations[0]); const position = parseRectanglePosition(this.annotations[0]);
this.close({ this.close({
@ -218,7 +222,7 @@ export class EditRedactionDialogComponent
comment: value.comment, comment: value.comment,
type: value.type, type: value.type,
value: this.allRectangles ? value.value : null, value: this.allRectangles ? value.value : null,
option: value.option.value, option: value.option?.value ?? RedactOrHintOptions.ONLY_HERE,
position, position,
pageNumbers, pageNumbers,
}); });
@ -255,7 +259,10 @@ export class EditRedactionDialogComponent
disabled: this.isImported, disabled: this.isImported,
}), }),
section: new FormControl<string>({ value: sameSection ? this.annotations[0].section : null, disabled: this.isImported }), section: new FormControl<string>({ value: sameSection ? this.annotations[0].section : null, disabled: this.isImported }),
option: new FormControl<DetailsRadioOption<EditRedactionOption | RectangleRedactOption>>(this.options[0], validatePageRange()), option: new FormControl<DetailsRadioOption<EditRedactionOption | RectangleRedactOption>>(
this.options[0],
validatePageRange(this.data.file.numberOfPages),
),
value: new FormControl<string>(this.allRectangles ? this.annotations[0].value : null), value: new FormControl<string>(this.allRectangles ? this.annotations[0].value : null),
}); });
} }

View File

@ -1,14 +1,13 @@
import { Component, Inject, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { import {
BaseDialogComponent,
CircleButtonComponent, CircleButtonComponent,
getConfig, getConfig,
HasScrollbarDirective, HasScrollbarDirective,
HelpButtonComponent, HelpButtonComponent,
IconButtonComponent, IconButtonComponent,
IqserDenyDirective, IqserDenyDirective,
IqserDialogComponent,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { JustificationsService } from '@services/entity-services/justifications.service'; import { JustificationsService } from '@services/entity-services/justifications.service';
import { Dossier, ILegalBasisChangeRequest } from '@red/domain'; import { Dossier, ILegalBasisChangeRequest } from '@red/domain';
@ -21,20 +20,19 @@ import {
ValueColumn, ValueColumn,
} from '../../components/selected-annotations-table/selected-annotations-table.component'; } from '../../components/selected-annotations-table/selected-annotations-table.component';
import { NgForOf, NgIf } from '@angular/common'; import { NgForOf, NgIf } from '@angular/common';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select'; import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import { ForceAnnotationData, ForceAnnotationOption, ForceAnnotationResult, LegalBasisOption } from '../../utils/dialog-types';
import { getForceAnnotationOptions } from '../../utils/dialog-options';
import { SystemDefaults } from '../../../account/utils/dialog-defaults';
import { MatFormField } from '@angular/material/form-field';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import { ForceAnnotationOption, LegalBasisOption } from '../../utils/dialog-types';
import { getForceAnnotationOptions } from '../../utils/dialog-options';
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component'; import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
import { SystemDefaults } from '../../../account/utils/dialog-defaults';
const DOCUMINE_LEGAL_BASIS = 'n-a.'; const DOCUMINE_LEGAL_BASIS = 'n-a.';
@Component({ @Component({
selector: 'redaction-force-annotation-dialog',
templateUrl: './force-annotation-dialog.component.html', templateUrl: './force-annotation-dialog.component.html',
styleUrls: ['./force-annotation-dialog.component.scss'], styleUrls: ['./force-annotation-dialog.component.scss'],
standalone: true, standalone: true,
@ -57,12 +55,16 @@ const DOCUMINE_LEGAL_BASIS = 'n-a.';
DetailsRadioComponent, DetailsRadioComponent,
], ],
}) })
export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit { export class ForceAnnotationDialogComponent
extends IqserDialogComponent<ForceAnnotationDialogComponent, ForceAnnotationData, ForceAnnotationResult>
implements OnInit
{
readonly isDocumine = getConfig().IS_DOCUMINE; readonly isDocumine = getConfig().IS_DOCUMINE;
readonly options: DetailsRadioOption<ForceAnnotationOption>[]; readonly options: DetailsRadioOption<ForceAnnotationOption>[];
readonly form: FormGroup;
readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }]; readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }];
readonly tableData: ValueColumn[][] = this._data.annotations.map(redaction => [ readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [
{ label: redaction.value, bold: true }, { label: redaction.value, bold: true },
{ label: redaction.typeLabel }, { label: redaction.typeLabel },
]); ]);
@ -72,21 +74,23 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
constructor( constructor(
private readonly _justificationsService: JustificationsService, private readonly _justificationsService: JustificationsService,
protected readonly _dialogRef: MatDialogRef<ForceAnnotationDialogComponent>, private readonly _formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA)
private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] },
) { ) {
super(_dialogRef); super();
this.options = getForceAnnotationOptions(this.isDocumine, this.isHintDialog); this.options = getForceAnnotationOptions(this.isDocumine, this.isHintDialog, this.isImageDialog);
this.form = this.#getForm(); this.form = this.#getForm();
} }
get isImageHint() { get isImageHint() {
return this._data.annotations.every(annotation => annotation.IMAGE_HINT); return this.data.annotations.every(annotation => annotation.IMAGE_HINT);
} }
get isHintDialog() { get isHintDialog() {
return this._data.hint; return this.data.hint;
}
get isImageDialog() {
return this.data.image;
} }
get disabled(): boolean { get disabled(): boolean {
@ -103,7 +107,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
async ngOnInit() { async ngOnInit() {
if (!this.isDocumine) { if (!this.isDocumine) {
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId)); const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.data.dossier.dossierTemplateId));
this.legalOptions = data.map(lbm => ({ this.legalOptions = data.map(lbm => ({
legalBasis: lbm.reason, legalBasis: lbm.reason,
@ -114,8 +118,8 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label)); this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
// Set pre-existing reason if it exists // Set pre-existing reason if it exists
const existingReason = this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis); const existingReason = this.legalOptions.find(option => option.legalBasis === this.data.annotations[0].legalBasis);
if (!this._data.hint && existingReason) { if (!this.data.hint && existingReason) {
this.form.patchValue({ reason: existingReason }, { emitEvent: false }); this.form.patchValue({ reason: existingReason }, { emitEvent: false });
} }
} }
@ -123,12 +127,12 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
} }
save() { save() {
this._dialogRef.close(this.#createForceRedactionRequest()); this.close(this.#createForceRedactionRequest());
} }
#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],
option: this.options.find(o => o.value === SystemDefaults.FORCE_REDACTION_DEFAULT), option: this.options.find(o => o.value === SystemDefaults.FORCE_REDACTION_DEFAULT),
}); });
@ -140,7 +144,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
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;
request.comment = this.form.get('comment').value; request.comment = this.form.get('comment').value;
request.reason = this.form.get('reason').value.description; request.reason = this.form.get('reason').value.description;
request.option = this.form.get('option').value.value; request.option = this.form.get('option').value?.value;
return request; return request;
} }

View File

@ -2,7 +2,7 @@
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div [translate]="'manual-annotation.dialog.header.redaction'" class="dialog-header heading-l"></div> <div [translate]="'manual-annotation.dialog.header.redaction'" class="dialog-header heading-l"></div>
<div class="dialog-content"> <div class="dialog-content redaction">
<iqser-details-radio <iqser-details-radio
[options]="options" [options]="options"
(extraOptionChanged)="extraOptionChanged($event)" (extraOptionChanged)="extraOptionChanged($event)"
@ -43,7 +43,7 @@
<div class="iqser-input-group w-450"> <div class="iqser-input-group w-450">
<label [translate]="'manual-annotation.dialog.content.comment'"></label> <label [translate]="'manual-annotation.dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea> <textarea formControlName="comment" iqserHasScrollbar name="comment" rows="3" type="text"></textarea>
</div> </div>
</div> </div>

View File

@ -3,8 +3,8 @@
} }
.dialog-content { .dialog-content {
height: 650px; height: 600px;
overflow-y: auto; padding-top: 8px;
} }
.apply-on-multiple-pages { .apply-on-multiple-pages {

View File

@ -130,7 +130,7 @@ export class RectangleAnnotationDialog
reason: [null, Validators.required], reason: [null, Validators.required],
comment: [null], comment: [null],
classification: [NON_READABLE_CONTENT], classification: [NON_READABLE_CONTENT],
option: [this.#getOption(SystemDefaults.RECTANGLE_REDACT_DEFAULT), validatePageRange()], option: [this.#getOption(SystemDefaults.RECTANGLE_REDACT_DEFAULT), validatePageRange(this.data.file.numberOfPages)],
}); });
} }

View File

@ -162,7 +162,11 @@ export class RedactRecommendationDialogComponent
} }
#setDictionaries() { #setDictionaries() {
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers); this.dictionaries = this._dictionaryService.getRedactTextDictionaries(
this.#dossier.dossierId,
!this.#applyToAllDossiers,
this.#dossier.dossierTemplateId,
);
} }
#selectReason() { #selectReason() {

View File

@ -219,7 +219,11 @@ export class RedactTextDialogComponent
} }
#setDictionaries() { #setDictionaries() {
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierId, !this.#applyToAllDossiers); this.dictionaries = this._dictionaryService.getRedactTextDictionaries(
this.#dossier.dossierId,
!this.#applyToAllDossiers,
this.#dossier.dossierTemplateId,
);
} }
#getForm(): FormGroup { #getForm(): FormGroup {

View File

@ -36,7 +36,7 @@ import {
} from '../../utils/dialog-types'; } from '../../utils/dialog-types';
import { isJustOne } from '@common-ui/utils'; import { isJustOne } from '@common-ui/utils';
import { validatePageRange } from '../../utils/form-validators'; import { validatePageRange } from '../../utils/form-validators';
import { parseRectanglePosition, parseSelectedPageNumbers } from '../../utils/enhance-manual-redaction-request.utils'; import { parseRectanglePosition, parseSelectedPageNumbers, prefillPageRange } from '../../utils/enhance-manual-redaction-request.utils';
@Component({ @Component({
templateUrl: './remove-redaction-dialog.component.html', templateUrl: './remove-redaction-dialog.component.html',
@ -94,6 +94,9 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
extra: false, extra: false,
}, },
}; };
readonly isImported: boolean = this.data.redactions.every(
annotation => annotation.imported || annotation.type === 'imported_redaction',
);
readonly #allRectangles = this.data.redactions.reduce((acc, a) => acc && a.AREA, true); readonly #allRectangles = this.data.redactions.reduce((acc, a) => acc && a.AREA, true);
readonly #applyToAllDossiers = this.systemDefaultByType[this.annotationsType].extra; readonly #applyToAllDossiers = this.systemDefaultByType[this.annotationsType].extra;
readonly isSystemDefault = this.optionByType[this.annotationsType].main === SystemDefaultOption.SYSTEM_DEFAULT; readonly isSystemDefault = this.optionByType[this.annotationsType].main === SystemDefaultOption.SYSTEM_DEFAULT;
@ -103,7 +106,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
: this.optionByType[this.annotationsType].main; : this.optionByType[this.annotationsType].main;
readonly extraOptionPreference = stringToBoolean(this.optionByType[this.annotationsType].extra); readonly extraOptionPreference = stringToBoolean(this.optionByType[this.annotationsType].extra);
readonly options: DetailsRadioOption<RectangleRedactOption | RemoveRedactionOption>[] = this.#allRectangles readonly options: DetailsRadioOption<RectangleRedactOption | RemoveRedactionOption>[] = this.#allRectangles
? getRectangleRedactOptions('remove') ? getRectangleRedactOptions('remove', this.data.redactions)
: getRemoveRedactionOptions( : getRemoveRedactionOptions(
this.data, this.data,
this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference, this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference,
@ -112,7 +115,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
readonly redactedTexts = this.data.redactions.map(annotation => annotation.value); readonly redactedTexts = this.data.redactions.map(annotation => annotation.value);
form: UntypedFormGroup = this._formBuilder.group({ form: UntypedFormGroup = this._formBuilder.group({
comment: [null], comment: [null],
option: [this.defaultOption, validatePageRange(true)], option: [this.defaultOption, validatePageRange(this.data.file.numberOfPages, true)],
}); });
readonly selectedOption = toSignal(this.form.get('option').valueChanges.pipe(map(value => value.value))); readonly selectedOption = toSignal(this.form.get('option').valueChanges.pipe(map(value => value.value)));
@ -140,6 +143,14 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
private readonly _userPreferences: UserPreferenceService, private readonly _userPreferences: UserPreferenceService,
) { ) {
super(); super();
if (this.#allRectangles && !this.isImported) {
prefillPageRange(
this.data.redactions[0],
this.data.allFileRedactions,
this.options as DetailsRadioOption<RectangleRedactOption>[],
);
}
} }
get hasFalsePositiveOption() { get hasFalsePositiveOption() {
@ -177,19 +188,19 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
} }
save(): void { save(): void {
const optionValue = this.form.controls.option.value.value; const optionValue = this.form.controls.option?.value?.value;
const pageNumbers = parseSelectedPageNumbers( const optionInputValue = this.form.controls.option?.value?.additionalInput?.value;
this.form.get('option').value.additionalInput?.value, const pageNumbers = parseSelectedPageNumbers(optionInputValue, this.data.file);
this.data.file, const positions = [];
this.data.redactions[0], for (const redaction of this.data.redactions) {
); positions.push(parseRectanglePosition(redaction));
const position = parseRectanglePosition(this.data.redactions[0]); }
this.close({ this.close({
...this.form.getRawValue(), ...this.form.getRawValue(),
bulkLocal: optionValue === ResizeOptions.IN_DOCUMENT || optionValue === RectangleRedactOptions.MULTIPLE_PAGES, bulkLocal: optionValue === ResizeOptions.IN_DOCUMENT || optionValue === RectangleRedactOptions.MULTIPLE_PAGES,
pageNumbers, pageNumbers,
position, positions,
}); });
} }

View File

@ -77,7 +77,7 @@ export class ResizeRedactionDialogComponent extends IqserDialogComponent<
save() { save() {
const formValue = this.form.getRawValue(); const formValue = this.form.getRawValue();
const updateDictionary = formValue.option.value === ResizeOptions.IN_DOSSIER; const updateDictionary = formValue.option?.value === ResizeOptions.IN_DOSSIER;
super.close({ super.close({
comment: formValue.comment, comment: formValue.comment,

View File

@ -17,7 +17,15 @@ import {
LoadingService, LoadingService,
Toaster, Toaster,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering'; import {
copyLocalStorageFiltersValues,
Filter,
FilterService,
IFilter,
INestedFilter,
NestedFilter,
processFilters,
} from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils'; import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
@ -72,6 +80,8 @@ import { FileHeaderComponent } from './components/file-header/file-header.compon
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component'; import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
import { DocumentInfoService } from './services/document-info.service'; import { DocumentInfoService } from './services/document-info.service';
import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component'; import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
import { ANNOTATION_ACTION_ICONS, ANNOTATION_ACTIONS } from './utils/constants';
import { ComponentLogService } from '@services/entity-services/component-log.service';
@Component({ @Component({
templateUrl: './file-preview-screen.component.html', templateUrl: './file-preview-screen.component.html',
@ -146,6 +156,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _multiSelectService: MultiSelectService, private readonly _multiSelectService: MultiSelectService,
private readonly _documentInfoService: DocumentInfoService, private readonly _documentInfoService: DocumentInfoService,
private readonly _componentLogService: ComponentLogService,
) { ) {
super(); super();
effect(() => { effect(() => {
@ -180,6 +191,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
effect(() => { effect(() => {
this._viewModeService.viewMode(); this._viewModeService.viewMode();
this.pdf.currentPage();
this.#updateViewMode().then(); this.#updateViewMode().then();
}); });
@ -196,6 +208,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._documentInfoService.shown(); this._documentInfoService.shown();
this.#updateViewerPosition(); this.#updateViewerPosition();
}); });
effect(() => {
this.state.componentReferenceIds();
this.#rebuildFilters();
});
} }
get changed() { get changed() {
@ -248,11 +265,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files) this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
super.ngOnDetach(); super.ngOnDetach();
this.pdf.instance.UI.hotkeys.off('esc'); this.pdf.instance.UI.hotkeys.off('esc');
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
this._changeRef.markForCheck(); this._changeRef.markForCheck();
} }
ngOnDestroy() { ngOnDestroy() {
this.pdf.instance.UI.hotkeys.off('esc'); this.pdf.instance.UI.hotkeys.off('esc');
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
super.ngOnDestroy(); super.ngOnDestroy();
} }
@ -277,6 +296,31 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
} }
@Bind()
handleViewerClick(event: MouseEvent) {
this._ngZone.run(() => {
if (event.isTrusted) {
const clickedElement = event.target as HTMLElement;
const actionIconClicked = ANNOTATION_ACTION_ICONS.some(action =>
(clickedElement as HTMLImageElement).src?.includes(action),
);
const actionClicked = ANNOTATION_ACTIONS.some(action => clickedElement.getAttribute('aria-label')?.includes(action));
if (this._multiSelectService.active() && !actionIconClicked && !actionClicked) {
if (
clickedElement.querySelector('#selectionrect') ||
clickedElement.id === `pageWidgetContainer${this.pdf.currentPage()}`
) {
if (!this._annotationManager.selected.length) {
this._multiSelectService.deactivate();
}
} else {
this._multiSelectService.deactivate();
}
}
}
});
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) { async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
if (!this.state.file().canBeOpened) { if (!this.state.file().canBeOpened) {
return this.#navigateToDossier(); return this.#navigateToDossier();
@ -307,6 +351,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.#restoreOldFilters(); this.#restoreOldFilters();
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer); this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
this._viewerHeaderService.resetLayers(); this._viewerHeaderService.resetLayers();
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
this.pdf.instance.UI.iframeWindow.document.addEventListener('click', this.handleViewerClick);
} }
async openRectangleAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) { async openRectangleAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
@ -508,8 +554,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
#rebuildFilters() { #rebuildFilters() {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(); let annotationFilters = this._annotationProcessingService.getAnnotationFilter();
const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters; const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters;
if (this.isDocumine) {
annotationFilters = this.#filterAnnotationFilters(annotationFilters);
}
this._filterService.addFilterGroup({ this._filterService.addFilterGroup({
slug: 'primaryFilters', slug: 'primaryFilters',
filterTemplate: this._filterTemplate, filterTemplate: this._filterTemplate,
@ -802,6 +853,38 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}; };
} }
#filterAnnotationFilters(annotationFilters: INestedFilter[]) {
const components = this._componentLogService.all;
const filteredComponentIds = untracked(this.state.componentReferenceIds);
if (filteredComponentIds && annotationFilters) {
const filteredComponentIdsSet = new Set(filteredComponentIds);
const references = new Set<string>();
for (const component of components) {
for (const componentValue of component.componentValues) {
for (const reference of componentValue.entityReferences) {
if (filteredComponentIdsSet.has(reference.id)) {
references.add(reference.type);
}
}
}
}
return annotationFilters
.map(filter => {
const filteredChildren = filter.children.filter(c => references.has(c.label.replace(/ /g, '_').toLowerCase()));
if (filteredChildren.length) {
return { ...filter, children: filteredChildren };
}
return null;
})
.filter(f => f !== null);
}
return annotationFilters;
}
#updateViewerPosition() { #updateViewerPosition() {
if (this.isDocumine) { if (this.isDocumine) {
if (this._documentInfoService.shown()) { if (this._documentInfoService.shown()) {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service'; import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service';
import { getConfig } from '@iqser/common-ui'; import { getConfig } from '@iqser/common-ui';
import { List, log } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core } from '@pdftron/webviewer'; import { Core } from '@pdftron/webviewer';
@ -10,7 +10,6 @@ import {
EarmarkOperation, EarmarkOperation,
type IBulkLocalRemoveRequest, type IBulkLocalRemoveRequest,
IBulkRecategorizationRequest, IBulkRecategorizationRequest,
ILegalBasisChangeRequest,
IRecategorizationRequest, IRecategorizationRequest,
IRectangle, IRectangle,
type IRemoveRedactionRequest, type IRemoveRedactionRequest,
@ -50,7 +49,7 @@ import { FilePreviewDialogService } from './file-preview-dialog.service';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
import { ManualRedactionService } from './manual-redaction.service'; import { ManualRedactionService } from './manual-redaction.service';
import { SkippedService } from './skipped.service'; import { SkippedService } from './skipped.service';
import { NON_READABLE_CONTENT } from '../dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component'; import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
@Injectable() @Injectable()
export class AnnotationActionsService { export class AnnotationActionsService {
@ -81,41 +80,49 @@ export class AnnotationActionsService {
this._dialogService.openDialog('highlightAction', data); this._dialogService.openDialog('highlightAction', data);
} }
forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) { async forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) {
const { dossierId, fileId } = this._state; const { dossierId, fileId } = this._state;
const data = { dossier: this._state.dossier(), annotations, hint }; const image = annotations.every(a => a.isImage);
this._dialogService.openDialog('forceAnnotation', data, (request: ILegalBasisChangeRequest) => { const data = { dossier: this._state.dossier(), annotations, hint, image };
let obs$: Observable<unknown>;
if (request.option === ForceAnnotationOptions.ONLY_HERE) { const dialogRef = this._iqserDialog.openDefault(ForceAnnotationDialogComponent, { data });
obs$ = this._manualRedactionService.bulkForce( const result = await dialogRef.result();
annotations.map(a => ({ ...request, annotationId: a.id })),
dossierId, if (!result) {
fileId, return;
annotations[0].isIgnoredHint, }
);
} else { let obs$: Observable<unknown>;
const addAnnotationRequest = annotations.map(a => ({ if (result.option === ForceAnnotationOptions.ONLY_HERE || hint || image) {
comment: { text: request.comment }, obs$ = this._manualRedactionService.bulkForce(
legalBasis: request.legalBasis, annotations.map(a => ({ ...result, annotationId: a.id })),
reason: request.reason, dossierId,
positions: a.positions, fileId,
type: a.type, annotations[0].isIgnoredHint,
value: a.value, );
})); } else {
obs$ = this._manualRedactionService.addAnnotation(addAnnotationRequest, dossierId, fileId, { const addAnnotationRequest = annotations.map(a => ({
hint, comment: result.comment,
bulkLocal: true, legalBasis: result.legalBasis,
}); reason: result.reason,
} positions: a.positions,
this.#processObsAndEmit(obs$).then(); type: a.type,
}); value: a.value,
}));
obs$ = this._manualRedactionService.addAnnotation(addAnnotationRequest, dossierId, fileId, {
hint,
bulkLocal: true,
});
}
this.#processObsAndEmit(obs$).then();
} }
async editRedaction(annotations: AnnotationWrapper[]) { async editRedaction(annotations: AnnotationWrapper[]) {
const { dossierId, file } = this._state; const { dossierId, file } = this._state;
const includeUnprocessed = annotations.every(annotation => this.#includeUnprocessed(annotation, true)); const allFileAnnotations = this._fileDataService.annotations();
const data = { const data = {
annotations, annotations,
allFileAnnotations,
dossierId, dossierId,
file: file(), file: file(),
}; };
@ -145,15 +152,11 @@ export class AnnotationActionsService {
return body; return body;
}); });
} else { } else {
const originTypes = annotations.map(a => a.type);
const originLegalBases = annotations.map(a => a.legalBasis);
recategorizeBody = { recategorizeBody = {
value: annotations[0].value, value: result.value ?? annotations[0].value,
type: result.type, type: result.type,
legalBasis: result.legalBasis, legalBasis: result.legalBasis,
section: result.section, section: result.section,
originTypes,
originLegalBases,
rectangle: annotations[0].AREA, rectangle: annotations[0].AREA,
pageNumbers: result.pageNumbers, pageNumbers: result.pageNumbers,
position: result.position, position: result.position,
@ -162,16 +165,13 @@ export class AnnotationActionsService {
} }
await this.#processObsAndEmit( await this.#processObsAndEmit(
this._manualRedactionService this._manualRedactionService.recategorizeRedactions(
.recategorizeRedactions( recategorizeBody,
recategorizeBody, dossierId,
dossierId, file().id,
file().id, this.#getChangedFields(annotations, result),
this.#getChangedFields(annotations, result), result.option === RedactOrHintOptions.IN_DOCUMENT || !!result.pageNumbers.length,
includeUnprocessed, ),
result.option === RedactOrHintOptions.IN_DOCUMENT || !!result.pageNumbers.length,
)
.pipe(log()),
); );
} }
@ -184,9 +184,11 @@ 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 { file } = this._state; const { file } = this._state;
const allFileRedactions = this._fileDataService.annotations();
const data = { const data = {
redactions, redactions,
allFileRedactions,
file: file(), file: file(),
dossier: this._state.dossier(), dossier: this._state.dossier(),
falsePositiveContext: redactions.map(r => this.#getFalsePositiveText(r)), falsePositiveContext: redactions.map(r => this.#getFalsePositiveText(r)),
@ -202,8 +204,8 @@ export class AnnotationActionsService {
} }
if ( if (
result.option.value === RemoveRedactionOptions.FALSE_POSITIVE || result.option?.value === RemoveRedactionOptions.FALSE_POSITIVE ||
result.option.value === RemoveRedactionOptions.DO_NOT_RECOMMEND result.option?.value === RemoveRedactionOptions.DO_NOT_RECOMMEND
) { ) {
this.#setAsFalsePositive(redactions, result); this.#setAsFalsePositive(redactions, result);
} else { } else {
@ -265,7 +267,6 @@ export class AnnotationActionsService {
async acceptResize(annotation: AnnotationWrapper, permissions: AnnotationPermissions): 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,
@ -316,16 +317,16 @@ export class AnnotationActionsService {
await this.cancelResize(annotation); await this.cancelResize(annotation);
const { fileId, dossierId } = this._state; const { fileId, dossierId } = this._state;
const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId, includeUnprocessed); const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId);
return this.#processObsAndEmit(request); return this.#processObsAndEmit(request);
} }
async cancelResize(annotationWrapper: AnnotationWrapper) { async cancelResize(annotationWrapper: AnnotationWrapper) {
this._annotationManager.resizingAnnotationId = undefined; this._annotationManager.resizingAnnotationId = undefined;
this._annotationManager.annotationHasBeenResized = false; this._annotationManager.annotationHasBeenResized = false;
this._annotationManager.deselect();
this._annotationManager.delete(annotationWrapper); this._annotationManager.delete(annotationWrapper);
await this._annotationDrawService.draw([annotationWrapper], this._skippedService.hideSkipped(), this._state.dossierTemplateId); await this._annotationDrawService.draw([annotationWrapper], this._skippedService.hideSkipped(), this._state.dossierTemplateId);
this._annotationManager.deselect();
} }
#generateRectangle(annotationWrapper: AnnotationWrapper) { #generateRectangle(annotationWrapper: AnnotationWrapper) {
@ -472,13 +473,13 @@ export class AnnotationActionsService {
} }
#removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) { #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 = this.#getRemoveRedactionBody(redactions, dialogResult); const body = this.#getRemoveRedactionBody(redactions, dialogResult);
// todo: might not be correct, probably shouldn't get to this point if they are not all the same // todo: might not be correct, probably shouldn't get to this point if they are not all the same
const isHint = redactions.every(r => r.isHint); const isHint = redactions.every(r => r.isHint);
const { dossierId, fileId } = this._state; const { dossierId, fileId } = this._state;
const maximumNumberEntries = 100; const maximumNumberEntries = 100;
const bulkLocal = dialogResult.bulkLocal || !!dialogResult.pageNumbers.length;
if (removeFromDictionary && (body as List<IRemoveRedactionRequest>).length > maximumNumberEntries) { if (removeFromDictionary && (body as List<IRemoveRedactionRequest>).length > maximumNumberEntries) {
const requests = body as List<IRemoveRedactionRequest>; const requests = body as List<IRemoveRedactionRequest>;
const splitNumber = Math.floor(requests.length / maximumNumberEntries); const splitNumber = Math.floor(requests.length / maximumNumberEntries);
@ -493,16 +494,28 @@ export class AnnotationActionsService {
const promises = []; const promises = [];
for (const split of splitRequests) { for (const split of splitRequests) {
promises.push(
firstValueFrom(
this._manualRedactionService.removeRedaction(split, dossierId, fileId, removeFromDictionary, isHint, bulkLocal),
),
);
}
Promise.all(promises).finally(() => this._fileDataService.annotationsChanged());
return;
}
if (redactions[0].AREA && bulkLocal) {
const promises = [];
for (const request of body) {
promises.push( promises.push(
firstValueFrom( firstValueFrom(
this._manualRedactionService.removeRedaction( this._manualRedactionService.removeRedaction(
split, request as IBulkLocalRemoveRequest,
dossierId, dossierId,
fileId, fileId,
removeFromDictionary, removeFromDictionary,
isHint, isHint,
includeUnprocessed, bulkLocal,
dialogResult.bulkLocal,
), ),
), ),
); );
@ -510,16 +523,9 @@ export class AnnotationActionsService {
Promise.all(promises).finally(() => this._fileDataService.annotationsChanged()); Promise.all(promises).finally(() => this._fileDataService.annotationsChanged());
return; return;
} }
this.#processObsAndEmit( this.#processObsAndEmit(
this._manualRedactionService.removeRedaction( this._manualRedactionService.removeRedaction(body, dossierId, fileId, removeFromDictionary, isHint, bulkLocal),
body,
dossierId,
fileId,
removeFromDictionary,
isHint,
includeUnprocessed,
dialogResult.bulkLocal || !!dialogResult.pageNumbers.length,
),
).then(); ).then();
} }
@ -576,43 +582,27 @@ export class AnnotationActionsService {
return { changes: changedFields.join(', ') }; 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;
}
#getRemoveRedactionBody( #getRemoveRedactionBody(
redactions: AnnotationWrapper[], redactions: AnnotationWrapper[],
dialogResult: RemoveRedactionResult, dialogResult: RemoveRedactionResult,
): List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest { ): List<IRemoveRedactionRequest | IBulkLocalRemoveRequest> {
if (dialogResult.bulkLocal || !!dialogResult.pageNumbers.length) { if (dialogResult.bulkLocal || !!dialogResult.pageNumbers.length) {
const redaction = redactions[0]; const redaction = redactions[0];
return { return dialogResult.positions.map(position => ({
value: redaction.value, value: redaction.value,
rectangle: redaction.value === NON_READABLE_CONTENT, rectangle: redaction.AREA,
originTypes: [redaction.entry.type],
originLegalBases: [redaction.legalBasis],
pageNumbers: dialogResult.pageNumbers, pageNumbers: dialogResult.pageNumbers,
position: dialogResult.position, position: position,
comment: dialogResult.comment, comment: dialogResult.comment,
}; }));
} }
return redactions.map(redaction => ({ return redactions.map(redaction => ({
annotationId: redaction.id, annotationId: redaction.id,
value: redaction.value, value: redaction.value,
comment: dialogResult.comment, comment: dialogResult.comment,
removeFromDictionary: dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER, removeFromDictionary: dialogResult.option?.value === RemoveRedactionOptions.IN_DOSSIER,
removeFromAllDossiers: !!dialogResult.option.additionalCheck?.checked || !!dialogResult.applyToAllDossiers, removeFromAllDossiers: !!dialogResult.option?.additionalCheck?.checked || !!dialogResult.applyToAllDossiers,
})); }));
} }
} }

View File

@ -270,7 +270,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const viewTime = timestampOf(viewedPage.viewedTime) - DELTA_VIEW_TIME; const viewTime = timestampOf(viewedPage.viewedTime) - DELTA_VIEW_TIME;
let changeOccurredAfterPageIsViewed = lastChange && timestampOf(lastChange.dateTime) > viewTime; let changeOccurredAfterPageIsViewed = lastChange && timestampOf(lastChange.dateTime) > viewTime;
if (changeOccurredAfterPageIsViewed) { if (changeOccurredAfterPageIsViewed !== undefined) {
this.#markPageAsUnseenIfNeeded(viewedPage, lastChange.dateTime); this.#markPageAsUnseenIfNeeded(viewedPage, lastChange.dateTime);
return lastChange?.type; return lastChange?.type;
} }
@ -281,7 +281,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const processedTime = lastManualChange?.processedDate; const processedTime = lastManualChange?.processedDate;
changeOccurredAfterPageIsViewed = processedTime && timestampOf(processedTime) > viewTime; changeOccurredAfterPageIsViewed = processedTime && timestampOf(processedTime) > viewTime;
if (changeOccurredAfterPageIsViewed) { if (changeOccurredAfterPageIsViewed !== undefined) {
this.#markPageAsUnseenIfNeeded(viewedPage, processedTime); this.#markPageAsUnseenIfNeeded(viewedPage, processedTime);
return ChangeTypes.CHANGED; return ChangeTypes.CHANGED;
} }

View File

@ -3,10 +3,9 @@ import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser/common-ui'; import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser/common-ui';
import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component'; import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/document-info-dialog.component'; import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/document-info-dialog.component';
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
import { HighlightActionDialogComponent } from '../dialogs/highlight-action-dialog/highlight-action-dialog.component'; import { HighlightActionDialogComponent } from '../dialogs/highlight-action-dialog/highlight-action-dialog.component';
type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'forceAnnotation' | 'highlightAction'; type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'highlightAction';
@Injectable() @Injectable()
export class FilePreviewDialogService extends DialogService<DialogType> { export class FilePreviewDialogService extends DialogService<DialogType> {
@ -22,9 +21,6 @@ export class FilePreviewDialogService extends DialogService<DialogType> {
changeLegalBasis: { changeLegalBasis: {
component: ChangeLegalBasisDialogComponent, component: ChangeLegalBasisDialogComponent,
}, },
forceAnnotation: {
component: ForceAnnotationDialogComponent,
},
highlightAction: { highlightAction: {
component: HighlightActionDialogComponent, component: HighlightActionDialogComponent,
}, },

View File

@ -1,6 +1,6 @@
import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http'; import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http';
import { computed, effect, inject, Injectable, signal, Signal, WritableSignal } from '@angular/core'; import { computed, effect, inject, Injectable, signal, Signal, WritableSignal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toObservable, 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';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -14,7 +14,7 @@ import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service'; import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { BehaviorSubject, firstValueFrom, from, merge, Observable, of, pairwise, Subject, switchMap } from 'rxjs'; import { firstValueFrom, from, merge, Observable, of, pairwise, Subject, switchMap } from 'rxjs';
import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators'; import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { ViewModeService } from './view-mode.service'; import { ViewModeService } from './view-mode.service';
@ -42,7 +42,7 @@ export class FilePreviewStateService {
readonly dossierDictionary: Signal<Dictionary>; readonly dossierDictionary: Signal<Dictionary>;
readonly blob$: Observable<Blob>; readonly blob$: Observable<Blob>;
readonly componentReferenceIds$: Observable<string[] | null>; readonly componentReferenceIds$: Observable<string[] | null>;
readonly #componentReferenceIds$ = new BehaviorSubject<string[] | null>(null); readonly componentReferenceIds = signal<string[]>([]);
readonly dossierId = getParam(DOSSIER_ID); readonly dossierId = getParam(DOSSIER_ID);
readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly fileId = getParam(FILE_ID); readonly fileId = getParam(FILE_ID);
@ -64,7 +64,7 @@ export class FilePreviewStateService {
this.dossier = toSignal(dossiersServiceResolver().getEntityChanged$(this.dossierId)); this.dossier = toSignal(dossiersServiceResolver().getEntityChanged$(this.dossierId));
this.file$ = inject(FilesMapService).watch$(this.dossierId, this.fileId); this.file$ = inject(FilesMapService).watch$(this.dossierId, this.fileId);
this.file = toSignal(this.file$); this.file = toSignal(this.file$);
this.componentReferenceIds$ = this.#componentReferenceIds$.asObservable(); this.componentReferenceIds$ = toObservable(this.componentReferenceIds);
this.excludedPages = signal(this.file().excludedPages); this.excludedPages = signal(this.file().excludedPages);
this.isWritable = computed(() => { this.isWritable = computed(() => {
const isWritable = this._permissionsService.canPerformAnnotationActions(this.file(), this.dossier()); const isWritable = this._permissionsService.canPerformAnnotationActions(this.file(), this.dossier());
@ -94,10 +94,6 @@ export class FilePreviewStateService {
); );
} }
set componentReferenceIds(ids: string[]) {
this.#componentReferenceIds$.next(ids);
}
get dictionaries(): Dictionary[] { get dictionaries(): Dictionary[] {
const dictionaries = this._dictionariesMapService.get(this.dossierTemplateId); const dictionaries = this._dictionariesMapService.get(this.dossierTemplateId);
if (this.dossierDictionary()) { if (this.dossierDictionary()) {
@ -128,15 +124,12 @@ export class FilePreviewStateService {
get #dossierFilesChange$() { get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe( return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId), map(changes => changes[this.dossierId]),
filter(fileIds => fileIds && fileIds.length > 0),
map(() => true), map(() => true),
); );
} }
get componentReferenceIds() {
return this.#componentReferenceIds$.getValue();
}
reloadBlob(): void { reloadBlob(): void {
this.#reloadBlob$.next(true); this.#reloadBlob$.next(true);
} }

View File

@ -75,13 +75,10 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest, body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
dossierId: string, dossierId: string,
fileId: string, fileId: string,
successMessageParameters?: { successMessageParameters?: { [p: string]: string },
[key: string]: string;
},
includeUnprocessed = false,
bulkLocal = false, bulkLocal = false,
) { ) {
return this.#recategorize(body, dossierId, fileId, includeUnprocessed, bulkLocal).pipe( return this.#recategorize(body, dossierId, fileId, bulkLocal).pipe(
this.#showToast('recategorize-annotation', false, successMessageParameters), this.#showToast('recategorize-annotation', false, successMessageParameters),
); );
} }
@ -117,10 +114,9 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
fileId: string, fileId: string,
removeFromDictionary = false, removeFromDictionary = false,
isHint = false, isHint = false,
includeUnprocessed = false,
bulkLocal = false, bulkLocal = false,
) { ) {
return this.#remove(body, dossierId, fileId, includeUnprocessed, bulkLocal).pipe( return this.#remove(body, dossierId, fileId, bulkLocal).pipe(
this.#showToast(!isHint ? 'remove' : 'remove-hint', removeFromDictionary), this.#showToast(!isHint ? 'remove' : 'remove-hint', removeFromDictionary),
); );
} }
@ -154,36 +150,23 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this._post(body, `${this.#bulkRedaction}/force/${dossierId}/${fileId}`).pipe(this.#log('Force redaction', body)); return this._post(body, `${this.#bulkRedaction}/force/${dossierId}/${fileId}`).pipe(this.#log('Force redaction', body));
} }
resize(body: List<IResizeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) { resize(body: List<IResizeRequest>, dossierId: string, fileId: string) {
return this._post(body, `${this.#bulkRedaction}/resize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe( return this._post(body, `${this.#bulkRedaction}/resize/${dossierId}/${fileId}`).pipe(this.#log('Resize', body));
this.#log('Resize', body),
);
} }
#recategorize( #recategorize(
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest, body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
dossierId: string, dossierId: string,
fileId: string, fileId: string,
includeUnprocessed = false,
bulkLocal = false, bulkLocal = false,
) { ) {
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction; const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
return this._post(body, `${bulkPath}/recategorize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe( return this._post(body, `${bulkPath}/recategorize/${dossierId}/${fileId}`).pipe(this.#log('Recategorize', body));
this.#log('Recategorize', body),
);
} }
#remove( #remove(body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest, dossierId: string, fileId: string, bulkLocal = false) {
body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest,
dossierId: string,
fileId: string,
includeUnprocessed = false,
bulkLocal = false,
) {
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction; const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
return this._post(body, `${bulkPath}/remove/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe( return this._post(body, `${bulkPath}/remove/${dossierId}/${fileId}`).pipe(this.#log('Remove', body));
this.#log('Remove', body),
);
} }
#log(action: string, body: unknown) { #log(action: string, body: unknown) {

View File

@ -26,7 +26,7 @@ export class PdfAnnotationActionsService {
get(annotations: AnnotationWrapper[], annotationChangesAllowed: boolean): IHeaderElement[] { get(annotations: AnnotationWrapper[], annotationChangesAllowed: boolean): IHeaderElement[] {
const availableActions: IHeaderElement[] = []; const availableActions: IHeaderElement[] = [];
const permissions = this.#getAnnotationsPermissions(annotations); const permissions = this.#getAnnotationsPermissions(annotations);
const sameType = annotations.every(a => a.type === annotations[0].type); const sameType = annotations.every(a => a.superType === annotations[0].superType);
// you can only resize one annotation at a time // you can only resize one annotation at a time
if (permissions.canResizeAnnotation && annotationChangesAllowed) { if (permissions.canResizeAnnotation && annotationChangesAllowed) {

View File

@ -117,8 +117,10 @@ export class PdfProxyService {
effect(() => { effect(() => {
if (this._viewModeService.isRedacted()) { if (this._viewModeService.isRedacted()) {
this._viewerHeaderService.disable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
this._viewerHeaderService.enable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); this._viewerHeaderService.enable([HeaderElements.TOGGLE_READABLE_REDACTIONS]);
} else { } else {
this._viewerHeaderService.enable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]);
} }
}); });
@ -389,10 +391,10 @@ export class PdfProxyService {
this._ngZone.run(() => { this._ngZone.run(() => {
if (allAreVisible) { if (allAreVisible) {
this._annotationManager.hide(viewerAnnotations); this._annotationManager.hide(viewerAnnotations);
this._annotationManager.addToHidden(viewerAnnotations[0].Id); viewerAnnotations.forEach(a => this._annotationManager.addToHidden(a.Id));
} else { } else {
this._annotationManager.show(viewerAnnotations); this._annotationManager.show(viewerAnnotations);
this._annotationManager.removeFromHidden(viewerAnnotations[0].Id); viewerAnnotations.forEach(a => this._annotationManager.removeFromHidden(a.Id));
} }
this._annotationManager.deselect(); this._annotationManager.deselect();
}); });

View File

@ -45,3 +45,31 @@ export const TextPopups = {
} as const; } as const;
export const HIDE_SKIPPED = 'hide-skipped'; export const HIDE_SKIPPED = 'hide-skipped';
export const ANNOTATION_ACTION_ICONS = [
'resize',
'edit',
'trash',
'check',
'thumb-up',
'pdftron-action-add-redaction',
'visibility-off',
] as const;
export const ANNOTATION_ACTIONS = [
'Resize',
'Größe ändern',
'Edit',
'Bearbeiten',
'Remove',
'Entfernen',
'Accept recommendation',
'Empfehlung annehmen',
'Force redaction',
'Schwärzung erzwingen',
'Force hint',
'Hinweis erzwingen',
'Redact',
'Schwärzen',
'Hide',
'Ausblenden',
] as const;

View File

@ -30,7 +30,11 @@ const DOCUMENT_ICON = 'iqser:document';
const FOLDER_ICON = 'red:folder'; const FOLDER_ICON = 'red:folder';
const REMOVE_FROM_DICT_ICON = 'red:remove-from-dict'; const REMOVE_FROM_DICT_ICON = 'red:remove-from-dict';
export const getEditRedactionOptions = (): DetailsRadioOption<EditRedactionOption>[] => { export const getEditRedactionOptions = (hint: boolean): DetailsRadioOption<EditRedactionOption>[] => {
if (hint) {
return [];
}
return [ return [
{ {
label: editRedactionTranslations.onlyHere.label, label: editRedactionTranslations.onlyHere.label,
@ -98,19 +102,27 @@ export const getRedactOrHintOptions = (
return options; return options;
}; };
export const getRectangleRedactOptions = (action: 'add' | 'edit' | 'remove' = 'add'): DetailsRadioOption<RectangleRedactOption>[] => { export const getRectangleRedactOptions = (
action: 'add' | 'edit' | 'remove' = 'add',
redactions: AnnotationWrapper[] = [],
): DetailsRadioOption<RectangleRedactOption>[] => {
const translations = const translations =
action === 'add' ? rectangleRedactTranslations : action === 'edit' ? editRectangleTranslations : removeRectangleTranslations; action === 'add' ? rectangleRedactTranslations : action === 'edit' ? editRectangleTranslations : removeRectangleTranslations;
return [ const options: DetailsRadioOption<RectangleRedactOption>[] = [
{ {
label: translations.onlyThisPage.label, label: translations.onlyThisPage.label,
description: translations.onlyThisPage.description, description: translations.onlyThisPage.description,
descriptionParams: { length: redactions.length },
icon: PIN_ICON, icon: PIN_ICON,
value: RectangleRedactOptions.ONLY_THIS_PAGE, value: RectangleRedactOptions.ONLY_THIS_PAGE,
}, },
{ ];
const isImportedWithoutValue = redactions.some(redaction => redaction.type === 'imported_redaction' && !redaction.value);
if (!['edit', 'remove'].includes(action) || !isImportedWithoutValue) {
options.push({
label: translations.multiplePages.label, label: translations.multiplePages.label,
description: translations.multiplePages.description, description: translations.multiplePages.description,
descriptionParams: { length: redactions.length },
icon: DOCUMENT_ICON, icon: DOCUMENT_ICON,
value: RectangleRedactOptions.MULTIPLE_PAGES, value: RectangleRedactOptions.MULTIPLE_PAGES,
additionalInput: { additionalInput: {
@ -118,9 +130,11 @@ export const getRectangleRedactOptions = (action: 'add' | 'edit' | 'remove' = 'a
description: translations.multiplePages.extraOptionDescription, description: translations.multiplePages.extraOptionDescription,
placeholder: translations.multiplePages.extraOptionPlaceholder, placeholder: translations.multiplePages.extraOptionPlaceholder,
value: '', value: '',
errorCode: 'invalidRange',
}, },
}, });
]; }
return options;
}; };
export const getResizeRedactionOptions = ( export const getResizeRedactionOptions = (
@ -131,22 +145,21 @@ export const getResizeRedactionOptions = (
isApprover: boolean, isApprover: boolean,
canResizeInDictionary: boolean, canResizeInDictionary: boolean,
): DetailsRadioOption<ResizeRedactionOption>[] => { ): DetailsRadioOption<ResizeRedactionOption>[] => {
if (isRss || !canResizeInDictionary) {
return [];
}
const translations = resizeRedactionTranslations; const translations = resizeRedactionTranslations;
const options: DetailsRadioOption<ResizeRedactionOption>[] = [ const dictBasedType = redaction.isModifyDictionary;
return [
{ {
label: translations.onlyHere.label, label: translations.onlyHere.label,
description: translations.onlyHere.description, description: translations.onlyHere.description,
icon: PIN_ICON, icon: PIN_ICON,
value: ResizeOptions.ONLY_HERE, value: ResizeOptions.ONLY_HERE,
}, },
]; {
if (isRss) {
return options;
}
if (canResizeInDictionary) {
const dictBasedType = redaction.isModifyDictionary;
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 },
@ -159,9 +172,8 @@ export const getResizeRedactionOptions = (
checked: applyToAllDossiers, checked: applyToAllDossiers,
hidden: !isApprover, hidden: !isApprover,
}, },
}); },
} ];
return options;
}; };
export const getRemoveRedactionOptions = ( export const getRemoveRedactionOptions = (
@ -172,30 +184,31 @@ export const getRemoveRedactionOptions = (
const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations; const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations;
const { permissions, redactions, isApprover, falsePositiveContext } = data; const { permissions, redactions, isApprover, falsePositiveContext } = data;
const isBulk = redactions.length > 1; const isBulk = redactions.length > 1;
const isImage = redactions.reduce((acc, next) => acc && next.isImage, true);
const options: DetailsRadioOption<RemoveRedactionOption>[] = []; const options: DetailsRadioOption<RemoveRedactionOption>[] = [];
if (permissions.canRemoveOnlyHere) { if (permissions.canRemoveOnlyHere && !isImage) {
options.push({ options.push({
label: translations.ONLY_HERE.label, label: translations.ONLY_HERE.label,
description: isBulk ? translations.ONLY_HERE.descriptionBulk : translations.ONLY_HERE.description, description: isBulk ? translations.ONLY_HERE.descriptionBulk : translations.ONLY_HERE.description,
descriptionParams: { descriptionParams: {
value: redactions[0].value, value: redactions[0].value,
type: redactions[0].HINT ? 'hint' : redactions[0].typeLabel, type: redactions[0].HINT ? 'hint' : redactions[0].typeLabel,
isImage: redactions[0].isImage ? 'image' : redactions[0].typeLabel,
}, },
icon: PIN_ICON, icon: PIN_ICON,
value: RemoveRedactionOptions.ONLY_HERE, value: RemoveRedactionOptions.ONLY_HERE,
}); });
options.push({ const isHint = redactions.reduce((acc, next) => acc && next.isHint, true);
label: removeRedactionTranslations.IN_DOCUMENT.label, if (!isHint) {
description: removeRedactionTranslations.IN_DOCUMENT.description, options.push({
descriptionParams: { label: removeRedactionTranslations.IN_DOCUMENT.label,
isImage: redactions[0].isImage ? 'image' : redactions[0].typeLabel, description: removeRedactionTranslations.IN_DOCUMENT.description,
}, icon: DOCUMENT_ICON,
icon: DOCUMENT_ICON, value: RemoveRedactionOptions.IN_DOCUMENT,
value: RemoveRedactionOptions.IN_DOCUMENT, descriptionParams: { length: redactions.length },
}); });
}
} }
if (permissions.canRemoveFromDictionary) { if (permissions.canRemoveFromDictionary) {
options.push({ options.push({
@ -264,8 +277,12 @@ export const getRemoveRedactionOptions = (
return options; return options;
}; };
export const getForceAnnotationOptions = (isDocumine: boolean, isHint: boolean): DetailsRadioOption<ForceAnnotationOption>[] => { export const getForceAnnotationOptions = (
if (isDocumine || isHint) { isDocumine: boolean,
isHint: boolean,
isImage: boolean,
): DetailsRadioOption<ForceAnnotationOption>[] => {
if (isDocumine || isHint || isImage) {
return []; return [];
} }

View File

@ -64,6 +64,7 @@ export interface RedactTextData {
export interface EditRedactionData { export interface EditRedactionData {
annotations: AnnotationWrapper[]; annotations: AnnotationWrapper[];
allFileAnnotations?: AnnotationWrapper[];
dossierId: string; dossierId: string;
file: File; file: File;
isApprover?: boolean; isApprover?: boolean;
@ -72,6 +73,21 @@ export interface EditRedactionData {
export type AddAnnotationData = RedactTextData; export type AddAnnotationData = RedactTextData;
export type AddHintData = RedactTextData; export type AddHintData = RedactTextData;
export interface ForceAnnotationData {
readonly dossier: Dossier;
readonly annotations: AnnotationWrapper[];
readonly hint: boolean;
readonly image: boolean;
}
export interface ForceAnnotationResult {
readonly annotationId?: string;
readonly comment?: string;
readonly legalBasis?: string;
readonly reason?: string;
readonly option?: ForceAnnotationOption;
}
export interface RedactTextResult { export interface RedactTextResult {
redaction: IManualRedactionEntry; redaction: IManualRedactionEntry;
dictionary: Dictionary; dictionary: Dictionary;
@ -135,6 +151,7 @@ export interface RemoveRedactionPermissions {
export interface RemoveRedactionData { export interface RemoveRedactionData {
redactions: AnnotationWrapper[]; redactions: AnnotationWrapper[];
allFileRedactions?: AnnotationWrapper[];
dossier: Dossier; dossier: Dossier;
file?: File; file?: File;
falsePositiveContext: string[]; falsePositiveContext: string[];
@ -151,7 +168,7 @@ export interface RemoveRedactionResult {
applyToAllDossiers?: boolean; applyToAllDossiers?: boolean;
bulkLocal?: boolean; bulkLocal?: boolean;
pageNumbers?: number[]; pageNumbers?: number[];
position: IEntityLogEntryPosition; positions: IEntityLogEntryPosition[];
} }
export type RemoveAnnotationResult = RemoveRedactionResult; export type RemoveAnnotationResult = RemoveRedactionResult;

View File

@ -1,7 +1,8 @@
import { Dictionary, File, IAddRedactionRequest, IEntityLogEntryPosition, SuperType } from '@red/domain'; import { Dictionary, File, IAddRedactionRequest, IEntityLogEntryPosition, SuperType } from '@red/domain';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { LegalBasisOption } from './dialog-types'; import { LegalBasisOption, RectangleRedactOption, RectangleRedactOptions } from './dialog-types';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
export interface EnhanceRequestData { export interface EnhanceRequestData {
readonly type: SuperType | null; readonly type: SuperType | null;
@ -48,7 +49,7 @@ export const enhanceManualRedactionRequest = (addRedactionRequest: IAddRedaction
addRedactionRequest.addToAllDossiers = data.isApprover && data.dictionaryRequest && data.applyToAllDossiers; addRedactionRequest.addToAllDossiers = data.isApprover && data.dictionaryRequest && data.applyToAllDossiers;
}; };
export const parseSelectedPageNumbers = (inputValue: string, file: File, annotation: AnnotationWrapper) => { export const parseSelectedPageNumbers = (inputValue: string, file: File) => {
if (!inputValue) { if (!inputValue) {
return []; return [];
} }
@ -79,3 +80,60 @@ export const parseRectanglePosition = (annotation: AnnotationWrapper) => {
pageNumber: position.page, pageNumber: position.page,
} as IEntityLogEntryPosition; } as IEntityLogEntryPosition;
}; };
export const prefillPageRange = (
annotation: AnnotationWrapper,
allFileAnnotations: AnnotationWrapper[],
options: DetailsRadioOption<RectangleRedactOption>[],
) => {
const option = options.find(o => o.value === RectangleRedactOptions.MULTIPLE_PAGES);
const pages = extractPages(annotation, allFileAnnotations);
option.additionalInput.value = toRangeString(pages);
};
const extractPages = (annotation: AnnotationWrapper, allFileAnnotations: AnnotationWrapper[]): number[] => {
return allFileAnnotations.reduce((pages, a) => {
const position = a.positions[0];
const annotationPosition = annotation.positions[0];
if (
position.height === annotationPosition.height &&
position.width === annotationPosition.width &&
position.topLeft.x === annotationPosition.topLeft.x &&
position.topLeft.y === annotationPosition.topLeft.y
) {
pages.push(position.page);
}
return pages;
}, []);
};
const toRangeString = (pages: number[]): string => {
if (pages.length === 0) {
return '';
}
let ranges = [];
let start = pages[0];
let end = pages[0];
for (let i = 1; i < pages.length; i++) {
if (pages[i] === end + 1) {
end = pages[i];
} else {
if (start === end) {
ranges.push(`${start}`);
} else {
ranges.push(`${start}-${end}`);
}
start = end = pages[i];
}
}
if (start === end) {
ranges.push(`${start}`);
} else {
ranges.push(`${start}-${end}`);
}
return ranges.join(',');
};

View File

@ -1,12 +1,26 @@
import { AbstractControl, ValidatorFn } from '@angular/forms'; import { AbstractControl, ValidatorFn } from '@angular/forms';
export const validatePageRange = (allowEmpty = false): ValidatorFn => { export const validatePageRange = (numberOfPages: number, allowEmpty = false): ValidatorFn => {
return (control: AbstractControl): { [key: string]: any } | null => { return (control: AbstractControl): { [key: string]: any } | null => {
const option = control.value; const option = control.value;
if (option?.additionalInput) { if (option?.additionalInput) {
const value = option.additionalInput.value; const value = option.additionalInput.value;
const validRange = /^(\d+(-\d+)?)(,\d+(-\d+)?)*$/.test(value); const validRange = /^(\d+(-\d+)?)(,\d+(-\d+)?)*$/.test(value);
return validRange || (!value.length && allowEmpty) ? null : { invalidRange: true };
if (!validRange && !(value.length === 0 && allowEmpty)) {
return { invalidRange: true };
}
const ranges = value.split(',');
const isWithinRange = ranges.every(range => {
const [start, end] = range.split('-').map(Number);
if (end) {
return start >= 1 && end <= numberOfPages && start < end;
}
return start >= 1 && start <= numberOfPages;
});
return isWithinRange || (value.length === 0 && allowEmpty) ? null : { invalidRange: true };
} }
return null; return null;
}; };

View File

@ -1,6 +1,14 @@
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
export const sortTopLeftToBottomRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { export const sortTopLeftToBottomRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => {
const top = Math.min(a1.y, a2.y);
const bottom = Math.max(a1.y - a1.height, a2.y - a2.height);
const intersectionHeight = Math.max(0, top - bottom);
const a1IntersectionPercentage = (intersectionHeight / a1.height) * 100;
const a2IntersectionPercentage = (intersectionHeight / a2.height) * 100;
if (a1IntersectionPercentage || a2IntersectionPercentage) {
return a1.x < a2.x ? -1 : 1;
}
if (a1.y > a2.y) { if (a1.y > a2.y) {
return -1; return -1;
} }
@ -11,6 +19,14 @@ export const sortTopLeftToBottomRight = (a1: AnnotationWrapper, a2: AnnotationWr
}; };
export const sortBottomLeftToTopRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { export const sortBottomLeftToTopRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => {
const top = Math.max(a1.x, a2.x);
const bottom = Math.min(a1.x + a1.width, a2.x + a2.width);
const intersectionHeight = Math.max(0, bottom - top);
const a1IntersectionPercentage = (intersectionHeight / a1.width) * 100;
const a2IntersectionPercentage = (intersectionHeight / a2.width) * 100;
if (a1IntersectionPercentage || a2IntersectionPercentage) {
return a1.y < a2.y ? -1 : 1;
}
if (a1.x < a2.x) { if (a1.x < a2.x) {
return -1; return -1;
} }
@ -21,6 +37,14 @@ export const sortBottomLeftToTopRight = (a1: AnnotationWrapper, a2: AnnotationWr
}; };
export const sortBottomRightToTopLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { export const sortBottomRightToTopLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => {
const top = Math.max(a1.y, a2.y);
const bottom = Math.min(a1.y + a1.height, a2.y + a2.height);
const intersectionHeight = Math.max(0, bottom - top);
const a1IntersectionPercentage = (intersectionHeight / a1.height) * 100;
const a2IntersectionPercentage = (intersectionHeight / a2.height) * 100;
if (a1IntersectionPercentage || a2IntersectionPercentage) {
return a1.x > a2.x ? -1 : 1;
}
if (a1.y < a2.y) { if (a1.y < a2.y) {
return -1; return -1;
} }
@ -29,8 +53,15 @@ export const sortBottomRightToTopLeft = (a1: AnnotationWrapper, a2: AnnotationWr
} }
return a1.x > a2.x ? -1 : 1; return a1.x > a2.x ? -1 : 1;
}; };
export const sortTopRightToBottomLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { export const sortTopRightToBottomLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => {
const top = Math.min(a1.x + a1.width, a2.x + a2.width);
const bottom = Math.max(a1.x, a2.x);
const intersectionHeight = Math.max(0, top - bottom);
const a1IntersectionPercentage = (intersectionHeight / a1.width) * 100;
const a2IntersectionPercentage = (intersectionHeight / a2.width) * 100;
if (a1IntersectionPercentage || a2IntersectionPercentage) {
return a1.y > a2.y ? -1 : 1;
}
if (a1.x > a2.x) { if (a1.x > a2.x) {
return -1; return -1;
} }

View File

@ -78,7 +78,7 @@ export class REDDocumentViewer {
} }
return ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input'; return ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input';
}), }),
filter($event => $event.key.startsWith('Arrow') || ['f', 'h', 'H', 'Escape'].includes($event.key)), filter($event => $event.key.startsWith('Arrow') || ['f', 'h', 'H', 'Escape', 'Shift'].includes($event.key)),
tap<KeyboardEvent>(stopAndPrevent), tap<KeyboardEvent>(stopAndPrevent),
log('[PDF] Keyboard shortcut'), log('[PDF] Keyboard shortcut'),
); );

View File

@ -13,7 +13,7 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { combineLatest, fromEvent, Observable } from 'rxjs'; import { combineLatest, fromEvent, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from '../utils/constants'; import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, SelectionModes, USELESS_ELEMENTS } from '../utils/constants';
import { asList } from '../utils/functions'; import { asList } from '../utils/functions';
import { Rgb } from '../utils/types'; import { Rgb } from '../utils/types';
import { REDAnnotationManager } from './annotation-manager.service'; import { REDAnnotationManager } from './annotation-manager.service';
@ -148,7 +148,7 @@ export class PdfViewer {
this.#instance = await this.#getInstance(htmlElement); this.#instance = await this.#getInstance(htmlElement);
if (environment.production) { if (environment.production) {
this.#instance.Core.setCustomFontURL('https://' + window.location.host + this.#convertPath('/assets/pdftron')); this.#instance.Core.setCustomFontURL(window.location.origin + this.#convertPath('/assets/pdftron/fonts'));
} }
await this.runWithCleanup(async () => { await this.runWithCleanup(async () => {
@ -160,12 +160,13 @@ export class PdfViewer {
this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast()); this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast());
this.#totalPages$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(pages => this.#totalPages.set(pages)); this.#totalPages$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(pages => this.#totalPages.set(pages));
this.#setSelectionMode(); this.#setSelectionMode(this.#config.SELECTION_MODE);
this.#configureElements(); this.#configureElements();
this.#disableHotkeys(); this.#disableHotkeys();
this.#getSelectedText(); this.#getSelectedText();
this.#listenForCommandF(); this.#listenForCommandF();
this.#listenForEsc(); this.#listenForEsc();
this.#listenForShift();
this.#clearSearchResultsWhenVisibilityChanged(); this.#clearSearchResultsWhenVisibilityChanged();
}); });
@ -258,7 +259,7 @@ export class PdfViewer {
} }
#listenForCommandF() { #listenForCommandF() {
this.#instance.UI.hotkeys.on('command+f, ctrl+f', e => { this.#instance.UI.hotkeys.on('command+f, ctrl+f', (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
if (this.#isElementActive('searchPanel')) { if (this.#isElementActive('searchPanel')) {
this.#updateSearchOptions(); this.#updateSearchOptions();
@ -274,11 +275,11 @@ export class PdfViewer {
#listenForEsc() { #listenForEsc() {
this.#instance.UI.hotkeys.on('esc', { this.#instance.UI.hotkeys.on('esc', {
keydown: e => { keydown: (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
this.#clickSelectToolButton(); this.#clickSelectToolButton();
}, },
keyup: e => { keyup: (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
if (this.#isElementActive('searchPanel') && !this._annotationManager.resizingAnnotationId) { if (this.#isElementActive('searchPanel') && !this._annotationManager.resizingAnnotationId) {
this.#focusViewer(); this.#focusViewer();
@ -289,6 +290,21 @@ export class PdfViewer {
}); });
} }
#listenForShift() {
this.#instance.UI.iframeWindow.addEventListener('keydown', e => {
e.preventDefault();
if (e.key === 'Shift') {
this.#setSelectionMode(SelectionModes.RECTANGULAR);
}
});
this.#instance.UI.iframeWindow.addEventListener('keyup', e => {
e.preventDefault();
if (e.key === 'Shift') {
this.#setSelectionMode(SelectionModes.STRUCTURAL);
}
});
}
#getSearchOption(optionId: string): boolean { #getSearchOption(optionId: string): boolean {
const iframeWindow = this.#instance.UI.iframeWindow; const iframeWindow = this.#instance.UI.iframeWindow;
const checkbox = iframeWindow.document.getElementById(optionId) as HTMLInputElement; const checkbox = iframeWindow.document.getElementById(optionId) as HTMLInputElement;
@ -350,21 +366,22 @@ export class PdfViewer {
this.#instance.UI.disableElements(USELESS_ELEMENTS); this.#instance.UI.disableElements(USELESS_ELEMENTS);
} }
#setSelectionMode(): void { #setSelectionMode(selectionMode: string): void {
const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool; const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool;
textTool.SELECTION_MODE = this.#config.SELECTION_MODE; textTool.SELECTION_MODE = selectionMode;
} }
#getInstance(htmlElement: HTMLElement) { #getInstance(htmlElement: HTMLElement) {
const options: WebViewerOptions = { const options: WebViewerOptions = {
licenseKey: this.#licenseKey, licenseKey: this.#licenseKey,
fullAPI: true, fullAPI: true,
path: this.#convertPath('/assets/wv-resources/10.10.1'), path: this.#convertPath('/assets/wv-resources/11.0.0'),
css: this.#convertPath('/assets/pdftron/stylesheet.css'), css: this.#convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems', backendType: 'ems',
ui: 'legacy',
}; };
return WebViewer(options, htmlElement); return WebViewer.Iframe(options, htmlElement);
} }
#isElementActive(element: string): boolean { #isElementActive(element: string): boolean {

View File

@ -13,8 +13,7 @@ import Annotation = Core.Annotations.Annotation;
export class ReadableRedactionsService { export class ReadableRedactionsService {
readonly active$: Observable<boolean>; readonly active$: Observable<boolean>;
readonly #convertPath = inject(UI_ROOT_PATH_FN); readonly #convertPath = inject(UI_ROOT_PATH_FN);
readonly #enableIcon = this.#convertPath('/assets/icons/general/redaction-preview.svg'); readonly #icon = this.#convertPath('/assets/icons/general/redaction-preview.svg');
readonly #disableIcon = this.#convertPath('/assets/icons/general/redaction-final.svg');
readonly #active$ = new BehaviorSubject<boolean>(true); readonly #active$ = new BehaviorSubject<boolean>(true);
constructor( constructor(
@ -29,8 +28,8 @@ export class ReadableRedactionsService {
return this.#active$.getValue(); return this.#active$.getValue();
} }
get toggleReadableRedactionsBtnIcon(): string { get icon() {
return this.active ? this.#enableIcon : this.#disableIcon; return this.#icon;
} }
toggleReadableRedactions(): void { toggleReadableRedactions(): void {
@ -79,11 +78,23 @@ export class ReadableRedactionsService {
} }
updateState() { updateState() {
this.#updateIconState();
this._pdf.instance.UI.updateElement(HeaderElements.TOGGLE_READABLE_REDACTIONS, { this._pdf.instance.UI.updateElement(HeaderElements.TOGGLE_READABLE_REDACTIONS, {
title: this._translateService.instant(_('pdf-viewer.header.toggle-readable-redactions'), { title: this._translateService.instant(_('pdf-viewer.header.toggle-readable-redactions'), {
active: this.active, active: this.active,
}), }),
img: this.toggleReadableRedactionsBtnIcon,
}); });
} }
#updateIconState() {
const element = this._pdf.instance.UI.iframeWindow.document.querySelector(
`[data-element=${HeaderElements.TOGGLE_READABLE_REDACTIONS}]`,
);
if (!element) return;
if (!this.active) {
element.classList.add('active');
} else {
element.classList.remove('active');
}
}
} }

View File

@ -24,6 +24,7 @@ export class TooltipsService {
updateIconState() { updateIconState() {
const element = this._pdf.instance.UI.iframeWindow.document.querySelector(`[data-element=${HeaderElements.TOGGLE_TOOLTIPS}]`); const element = this._pdf.instance.UI.iframeWindow.document.querySelector(`[data-element=${HeaderElements.TOGGLE_TOOLTIPS}]`);
if (!element) return;
if (this._userPreferenceService.getFilePreviewTooltipsPreference()) { if (this._userPreferenceService.getFilePreviewTooltipsPreference()) {
element.classList.add('active'); element.classList.add('active');
} else { } else {

View File

@ -121,7 +121,7 @@ export class ViewerHeaderService {
type: 'actionButton', type: 'actionButton',
element: HeaderElements.TOGGLE_READABLE_REDACTIONS, element: HeaderElements.TOGGLE_READABLE_REDACTIONS,
dataElement: HeaderElements.TOGGLE_READABLE_REDACTIONS, dataElement: HeaderElements.TOGGLE_READABLE_REDACTIONS,
img: this._readableRedactionsService.toggleReadableRedactionsBtnIcon, img: this._readableRedactionsService.icon,
onClick: () => this._ngZone.run(() => this._readableRedactionsService.toggleReadableRedactions()), onClick: () => this._ngZone.run(() => this._readableRedactionsService.toggleReadableRedactions()),
}; };
} }
@ -289,20 +289,26 @@ export class ViewerHeaderService {
], ],
]; ];
header.get('selectToolButton').insertAfter(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON)); const shouldHideRectangleButton = this.#isEnabled(HeaderElements.SHAPE_TOOL_GROUP_BUTTON) ? 0 : 1;
if (!shouldHideRectangleButton) {
header.get('selectToolButton').insertAfter(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON));
} else if (header.getItems().includes(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON))) {
header.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON).delete();
}
groups.forEach(group => this.#pushGroup(enabledItems, group)); groups.forEach(group => this.#pushGroup(enabledItems, group));
const loadAllAnnotationsButton = this.#buttons.get(HeaderElements.LOAD_ALL_ANNOTATIONS); const loadAllAnnotationsButton = this.#buttons.get(HeaderElements.LOAD_ALL_ANNOTATIONS);
let startButtons = 11 - documineButtons; let startButtons = 11 - documineButtons - shouldHideRectangleButton;
let deleteCount = 15 - documineButtons; let deleteCount = 15 - documineButtons - shouldHideRectangleButton;
if (this.#isEnabled(HeaderElements.LOAD_ALL_ANNOTATIONS)) { if (this.#isEnabled(HeaderElements.LOAD_ALL_ANNOTATIONS)) {
if (!header.getItems().includes(loadAllAnnotationsButton)) { if (!header.getItems().includes(loadAllAnnotationsButton)) {
header.get('leftPanelButton').insertAfter(loadAllAnnotationsButton); header.get('leftPanelButton').insertAfter(loadAllAnnotationsButton);
} }
startButtons = 12 - documineButtons; startButtons = 12 - documineButtons - shouldHideRectangleButton;
deleteCount = 16 - documineButtons; deleteCount = 16 - documineButtons - shouldHideRectangleButton;
} else { } else if (header.getItems().includes(loadAllAnnotationsButton)) {
header.delete(HeaderElements.LOAD_ALL_ANNOTATIONS); header.delete(HeaderElements.LOAD_ALL_ANNOTATIONS);
} }

View File

@ -87,3 +87,8 @@ export const DISABLED_HOTKEYS = [
export const AnnotationToolNames = { export const AnnotationToolNames = {
AnnotationCreateRectangle: 'AnnotationCreateRectangle', AnnotationCreateRectangle: 'AnnotationCreateRectangle',
} as const; } as const;
export const SelectionModes = {
RECTANGULAR: 'rectangular',
STRUCTURAL: 'structural',
} as const;

View File

@ -1,26 +1,26 @@
<div *ngIf="isDossierOverviewList && fileAttributesService.isEditingFileAttribute() === false" class="action-buttons"> <div *ngIf="isDossierOverviewList() && fileAttributesService.isEditingFileAttribute() === false" class="action-buttons">
<ng-container *ngTemplateOutlet="actions"></ng-container> <ng-container *ngTemplateOutlet="actions"></ng-container>
<redaction-processing-indicator *ngIf="showStatusBar" [file]="file"></redaction-processing-indicator> <redaction-processing-indicator *ngIf="showStatusBar()" [file]="file()"></redaction-processing-indicator>
<iqser-status-bar *ngIf="showStatusBar" [configs]="[{ color: file.workflowStatus, length: 1 }]"></iqser-status-bar> <iqser-status-bar *ngIf="showStatusBar()" [configs]="[{ color: file().workflowStatus, length: 1 }]"></iqser-status-bar>
</div> </div>
<ng-container *ngIf="isFilePreview || isDossierOverviewWorkflow"> <ng-container *ngIf="isFilePreview() || isDossierOverviewWorkflow()">
<ng-container *ngTemplateOutlet="actions"></ng-container> <ng-container *ngTemplateOutlet="actions"></ng-container>
</ng-container> </ng-container>
<ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress> <ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress>
<div class="file-actions"> <div class="file-actions">
<redaction-expandable-file-actions <redaction-expandable-file-actions
[actions]="buttons" [actions]="buttons()"
[id]="'actions-for-' + file.fileId" [id]="'actions-for-' + file().fileId"
[maxWidth]="maxWidth" [maxWidth]="maxWidth()"
[minWidth]="minWidth" [minWidth]="minWidth()"
[tooltipPosition]="tooltipPosition" [tooltipPosition]="tooltipPosition"
[helpModeKeyPrefix]="helpModeKeyPrefix" [helpModeKeyPrefix]="helpModeKeyPrefix()"
[isDossierOverviewWorkflow]="isDossierOverviewWorkflow" [isDossierOverviewWorkflow]="isDossierOverviewWorkflow()"
[singleEntityAction]="singleEntityAction" [singleEntityAction]="singleEntityAction()"
></redaction-expandable-file-actions> ></redaction-expandable-file-actions>
</div> </div>
</ng-template> </ng-template>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, HostBinding, Injector, Input, OnChanges, Optional, signal, ViewChild } from '@angular/core'; import { Component, computed, HostBinding, Injector, input, Optional, signal, ViewChild } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop'; import { toObservable } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -37,6 +37,7 @@ import { ProcessingIndicatorComponent } from '@shared/components/processing-indi
import { StatusBarComponent } from '@common-ui/shared'; import { StatusBarComponent } from '@common-ui/shared';
import { NgIf, NgTemplateOutlet } from '@angular/common'; import { NgIf, NgTemplateOutlet } from '@angular/common';
import { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component'; import { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component';
import { RulesService } from '../../../admin/services/rules.service';
@Component({ @Component({
selector: 'redaction-file-actions', selector: 'redaction-file-actions',
@ -45,52 +46,92 @@ import { ApproveWarningDetailsComponent } from '@shared/components/approve-warni
standalone: true, standalone: true,
imports: [ProcessingIndicatorComponent, StatusBarComponent, LongPressDirective, ExpandableFileActionsComponent, NgTemplateOutlet, NgIf], imports: [ProcessingIndicatorComponent, StatusBarComponent, LongPressDirective, ExpandableFileActionsComponent, NgTemplateOutlet, NgIf],
}) })
export class FileActionsComponent implements OnChanges { export class FileActionsComponent {
@Input({ required: true }) file: File;
@Input({ required: true }) dossier: Dossier;
@Input({ required: true }) type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
@Input() maxWidth: number;
@Input() minWidth: number;
@Input() helpModeKeyPrefix: 'dossier' | 'editor' = 'dossier';
@Input() singleEntityAction = false;
readonly currentUser = getCurrentUser<User>();
toggleTooltip?: string;
assignTooltip?: string;
showDownload = false;
showSetToNew = false;
showUndoApproval = false;
showAssignToSelf = false;
showImportRedactions = false;
showAssign = false;
showDelete = false;
showOCR = false;
canReanalyse = false;
canDisableAutoAnalysis = false;
canEnableAutoAnalysis = false;
showUnderReview = false;
showUnderApproval = false;
showApprove = false;
canToggleAnalysis = false;
showToggleAnalysis = false;
showStatusBar = false;
showReanalyseFilePreview = false;
showReanalyseDossierOverview = false;
analysisForced = false;
isDossierOverview = false;
isDossierOverviewList = false;
isDossierOverviewWorkflow = false;
isFilePreview = false;
isDossierMember = false;
tooltipPosition = IqserTooltipPositions.above;
buttons: Action[];
@ViewChild(ExpandableFileActionsComponent) @ViewChild(ExpandableFileActionsComponent)
private readonly _expandableActionsComponent: ExpandableFileActionsComponent; private readonly _expandableActionsComponent: ExpandableFileActionsComponent;
readonly file = input.required<File>();
readonly dossier = input.required<Dossier>();
readonly type = input.required<'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow'>();
readonly maxWidth = input<number>();
readonly minWidth = input<number>();
readonly helpModeKeyPrefix = input<'dossier' | 'editor'>('dossier');
readonly singleEntityAction = input(false);
readonly currentUser = getCurrentUser<User>();
readonly tooltipPosition = IqserTooltipPositions.above;
readonly isDossierOverview = computed(() => this.type().startsWith('dossier-overview'));
readonly isDossierOverviewList = computed(() => this.type() === 'dossier-overview-list');
readonly isDossierOverviewWorkflow = computed(() => this.type() === 'dossier-overview-workflow');
readonly isFilePreview = computed(() => this.type() === 'file-preview');
readonly buttons = computed(() => this.#buttons);
readonly showStatusBar = computed(() => !this.file().isError && !this.file().isUnprocessed && this.isDossierOverviewList());
readonly #assignTooltip? = computed(() =>
this.file().isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'),
);
readonly #showSetToNew = computed(
() => this._permissionsService.canSetToNew(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow(),
);
readonly #showUndoApproval = computed(
() => this._permissionsService.canUndoApproval(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow(),
);
readonly #showAssignToSelf = computed(
() => this._permissionsService.canAssignToSelf(this.file(), this.dossier()) && this.isDossierOverview(),
);
readonly #showImportRedactions = computed(() => this._permissionsService.canImportRedactions(this.file(), this.dossier()));
readonly #showAssign = computed(
() =>
(this._permissionsService.canAssignUser(this.file(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.file(), this.dossier())) &&
this.isDossierOverview(),
);
readonly #showDelete = computed(() => this._permissionsService.canSoftDeleteFile(this.file(), this.dossier()));
readonly #showOCR = computed(() => this._permissionsService.canOcrFile(this.file(), this.dossier()));
readonly #canReanalyse = computed(() => this._permissionsService.canReanalyseFile(this.file(), this.dossier()));
readonly #canEnableAutoAnalysis = computed(() => this._permissionsService.canEnableAutoAnalysis([this.file()], this.dossier()));
readonly #showUnderReview = computed(
() => this._permissionsService.canSetUnderReview(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow(),
);
readonly #showUnderApproval = computed(
() => this._permissionsService.canSetUnderApproval(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow(),
);
readonly #showApprove = computed(
() => this._permissionsService.isReadyForApproval(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow(),
);
readonly #canToggleAnalysis = computed(() => this._permissionsService.canToggleAnalysis(this.file(), this.dossier()));
readonly #toggleTooltip? = computed(() => {
if (!this.#canToggleAnalysis()) {
return _('file-preview.toggle-analysis.only-managers');
}
return this.file()?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
});
readonly #showToggleAnalysis = computed(
() => !!this.file().lastProcessed && this._permissionsService.showToggleAnalysis(this.dossier()),
);
readonly #analysisForced = signal(false);
readonly #showReanalyse = computed(
() => (this.#canReanalyse() || this.file().excludedFromAutomaticAnalysis || this.#analysisForced()) && !this.file().dossierArchived,
);
readonly #isDossierMember = computed(() => this._permissionsService.isDossierMember(this.dossier()));
readonly #showDownload = computed(
() => this._permissionsService.canDownloadRedactedFile() && !!this.file().lastProcessed && this.#isDossierMember(),
);
readonly #showReanalyseFilePreview = computed(
() => this.#showReanalyse() && this.isFilePreview() && !this.file().isApproved && this.#isDossierMember(),
);
readonly #showReanalyseDossierOverview = computed(
() => this.#showReanalyse() && this.isDossierOverview() && !this.file().isApproved && this.#isDossierMember(),
);
readonly #ariaExpanded$ = toObservable(this._documentInfoService?.shown);
readonly #areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected);
readonly #isDocumine = getConfig().IS_DOCUMINE; readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #canDisableAutoAnalysis = computed(
() => !this.#isDocumine && this._permissionsService.canDisableAutoAnalysis([this.file()], this.dossier()),
);
constructor( constructor(
private readonly _injector: Injector, private readonly _injector: Injector,
private readonly _filesService: FilesService, private readonly _filesService: FilesService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _dialogService: DossiersDialogService, private readonly _dialogService: DossiersDialogService,
private readonly _iqserDialog: IqserDialog, private readonly _iqserDialog: IqserDialog,
@ -101,6 +142,8 @@ export class FileActionsComponent implements OnChanges {
private readonly _activeDossiersService: ActiveDossiersService, private readonly _activeDossiersService: ActiveDossiersService,
private readonly _fileManagementService: FileManagementService, private readonly _fileManagementService: FileManagementService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _rulesService: RulesService,
private readonly _toasterService: Toaster,
readonly fileAttributesService: FileAttributesService, readonly fileAttributesService: FileAttributesService,
@Optional() private readonly _documentInfoService: DocumentInfoService, @Optional() private readonly _documentInfoService: DocumentInfoService,
@Optional() private readonly _excludedPagesService: ExcludedPagesService, @Optional() private readonly _excludedPagesService: ExcludedPagesService,
@ -111,24 +154,16 @@ export class FileActionsComponent implements OnChanges {
return !!this._expandableActionsComponent?.expanded; return !!this._expandableActionsComponent?.expanded;
} }
private get _toggleTooltip(): string { get #buttons() {
if (!this.canToggleAnalysis) {
return _('file-preview.toggle-analysis.only-managers');
}
return this.file?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
}
private get _buttons(): Action[] {
const actions: Action[] = [ const actions: Action[] = [
{ {
id: 'btn-download_file', id: 'btn-download_file',
type: ActionTypes.downloadBtn, type: ActionTypes.downloadBtn,
files: [this.file], files: [this.file()],
dossier: this.dossier, dossier: this.dossier(),
tooltipClass: 'small', tooltipClass: 'small',
show: this.showDownload, show: this.#showDownload(),
disabled: this.file.processingStatus === ProcessingFileStatuses.ERROR, disabled: this.file().processingStatus === ProcessingFileStatuses.ERROR,
helpModeKey: this.#isDocumine ? 'download_document' : 'download', helpModeKey: this.#isDocumine ? 'download_document' : 'download',
}, },
{ {
@ -137,16 +172,16 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#openDeleteFileDialog(), action: () => this.#openDeleteFileDialog(),
tooltip: _('dossier-overview.delete.action'), tooltip: _('dossier-overview.delete.action'),
icon: 'iqser:trash', icon: 'iqser:trash',
show: this.showDelete, show: this.#showDelete(),
helpModeKey: 'delete_file', helpModeKey: 'delete_file',
}, },
{ {
id: 'btn-assign', id: 'btn-assign',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#assign(), action: () => this.#assign(),
tooltip: this.assignTooltip, tooltip: this.#assignTooltip(),
icon: 'red:assign', icon: 'red:assign',
show: this.showAssign, show: this.#showAssign(),
helpModeKey: 'assign_user', helpModeKey: 'assign_user',
}, },
{ {
@ -155,7 +190,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#assignToMe(), action: () => this.#assignToMe(),
tooltip: _('dossier-overview.assign-me'), tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me', icon: 'red:assign-me',
show: this.showAssignToSelf, show: this.#showAssignToSelf(),
helpModeKey: 'assign_user', helpModeKey: 'assign_user',
}, },
{ {
@ -164,7 +199,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#openImportRedactionsDialog(), action: () => this.#openImportRedactionsDialog(),
tooltip: _('dossier-overview.import-redactions'), tooltip: _('dossier-overview.import-redactions'),
icon: 'red:import_redactions', icon: 'red:import_redactions',
show: this.showImportRedactions && !this._iqserPermissionsService.has(Roles.getRss), show: this.#showImportRedactions() && !this._iqserPermissionsService.has(Roles.getRss),
helpModeKey: 'import_redactions', helpModeKey: 'import_redactions',
}, },
{ {
@ -172,7 +207,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#toggleDocumentInfo(), action: () => this.#toggleDocumentInfo(),
tooltip: _('file-preview.document-info'), tooltip: _('file-preview.document-info'),
ariaExpanded: toObservable(this._documentInfoService?.shown, { injector: this._injector }), ariaExpanded: this.#ariaExpanded$,
icon: 'red:status-info', icon: 'red:status-info',
show: !!this._documentInfoService, show: !!this._documentInfoService,
helpModeKey: 'document_info', helpModeKey: 'document_info',
@ -182,10 +217,10 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#toggleExcludePages(), action: () => this.#toggleExcludePages(),
tooltip: _('file-preview.exclude-pages'), tooltip: _('file-preview.exclude-pages'),
ariaExpanded: toObservable(this._excludedPagesService?.shown, { injector: this._injector }), ariaExpanded: this.#ariaExpanded$,
showDot: !!this.file.excludedPages?.length, showDot: !!this.file().excludedPages?.length,
icon: 'red:exclude-pages', icon: 'red:exclude-pages',
show: !!this._excludedPagesService && this._permissionsService.canExcludePages(this.file, this.dossier), show: !!this._excludedPagesService && this._permissionsService.canExcludePages(this.file(), this.dossier()),
helpModeKey: 'exclude_pages', helpModeKey: 'exclude_pages',
}, },
{ {
@ -194,7 +229,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setToNew(), action: () => this.#setToNew(),
tooltip: _('dossier-overview.back-to-new'), tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo', icon: 'red:undo',
show: this.showSetToNew, show: this.#showSetToNew(),
helpModeKey: 'change_status', helpModeKey: 'change_status',
}, },
{ {
@ -203,7 +238,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderApproval(), action: () => this.#setFileUnderApproval(),
tooltip: _('dossier-overview.under-approval'), tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval', icon: 'red:ready-for-approval',
show: this.showUnderApproval, show: this.#showUnderApproval(),
helpModeKey: 'change_status', helpModeKey: 'change_status',
}, },
{ {
@ -212,17 +247,17 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderReview(), action: () => this.#setFileUnderReview(),
tooltip: _('dossier-overview.under-review'), tooltip: _('dossier-overview.under-review'),
icon: 'red:undo', icon: 'red:undo',
show: this.showUnderReview, show: this.#showUnderReview(),
helpModeKey: 'change_status', helpModeKey: 'change_status',
}, },
{ {
id: 'btn-set_file_approved', id: 'btn-set_file_approved',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.setFileApproved(), action: () => this.setFileApproved(),
tooltip: this.file.canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'), tooltip: this.file().canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved', icon: 'red:approved',
disabled: !this.file.canBeApproved, disabled: !this.file().canBeApproved,
show: this.showApprove, show: this.#showApprove(),
helpModeKey: 'change_status', helpModeKey: 'change_status',
}, },
{ {
@ -231,18 +266,17 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#toggleAutomaticAnalysis(), action: () => this.#toggleAutomaticAnalysis(),
tooltip: _('dossier-overview.stop-auto-analysis'), tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis', icon: 'red:disable-analysis',
show: this.canDisableAutoAnalysis, show: this.#canDisableAutoAnalysis(),
helpModeKey: 'stop_analysis', helpModeKey: 'stop_analysis',
}, },
{ {
id: 'btn-reanalyse_file_preview', id: 'btn-reanalyse_file_preview',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#reanalyseFile(), action: () => this.#reanalyseFile(),
tooltip: _('file-preview.reanalyse-notification'), tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('file-preview.reanalyse-notification'),
tooltipClass: 'small',
icon: 'iqser:refresh', icon: 'iqser:refresh',
show: this.showReanalyseFilePreview, show: this.#showReanalyseFilePreview(),
disabled: this.file.isProcessing, disabled: this.file().isProcessing || this.#areRulesLocked(),
helpModeKey: 'stop_analysis', helpModeKey: 'stop_analysis',
}, },
{ {
@ -250,9 +284,9 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#toggleAutomaticAnalysis(), action: () => this.#toggleAutomaticAnalysis(),
tooltip: _('dossier-overview.start-auto-analysis'), tooltip: _('dossier-overview.start-auto-analysis'),
buttonType: this.isFilePreview ? CircleButtonTypes.warn : CircleButtonTypes.default, buttonType: this.isFilePreview() ? CircleButtonTypes.warn : CircleButtonTypes.default,
icon: 'red:enable-analysis', icon: 'red:enable-analysis',
show: this.canEnableAutoAnalysis, show: this.#canEnableAutoAnalysis(),
helpModeKey: 'stop_analysis', helpModeKey: 'stop_analysis',
}, },
{ {
@ -261,7 +295,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderApproval(), action: () => this.#setFileUnderApproval(),
tooltip: _('dossier-overview.under-approval'), tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo', icon: 'red:undo',
show: this.showUndoApproval, show: this.#showUndoApproval(),
helpModeKey: 'change_status', helpModeKey: 'change_status',
}, },
{ {
@ -270,27 +304,28 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#ocrFile(), action: () => this.#ocrFile(),
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', helpModeKey: 'automatic_text_recognition',
}, },
{ {
id: 'btn-reanalyse_file', id: 'btn-reanalyse_file',
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this.#reanalyseFile(), action: () => this.#reanalyseFile(),
tooltip: _('dossier-overview.reanalyse.action'), tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.reanalyse.action'),
icon: 'iqser:refresh', icon: 'iqser:refresh',
show: this.showReanalyseDossierOverview, show: this.#showReanalyseDossierOverview(),
disabled: this.#areRulesLocked(),
helpModeKey: 'stop_analysis', helpModeKey: 'stop_analysis',
}, },
{ {
id: 'btn-toggle_analysis', id: 'btn-toggle_analysis',
type: ActionTypes.toggle, type: ActionTypes.toggle,
action: () => this.#toggleAnalysis(), action: () => this.#toggleAnalysis(),
disabled: !this.canToggleAnalysis, disabled: !this.#canToggleAnalysis(),
tooltip: this.toggleTooltip, tooltip: this.#toggleTooltip(),
class: { 'mr-24': this.isDossierOverviewList }, class: { 'mr-24': this.isDossierOverviewList() },
checked: !this.file.excluded, checked: !this.file().excluded,
show: this.showToggleAnalysis && this.isDossierMember, show: this.#showToggleAnalysis() && this.#isDossierMember(),
helpModeKey: 'disable_extraction', helpModeKey: 'disable_extraction',
}, },
]; ];
@ -298,17 +333,15 @@ export class FileActionsComponent implements OnChanges {
return actions.filter(btn => btn.show); return actions.filter(btn => btn.show);
} }
ngOnChanges() {
this.#setup();
}
async setFileApproved() { async setFileApproved() {
this._loadingService.start(); this._loadingService.start();
const approvalResponse: ApproveResponse = (await this._filesService.getApproveWarnings([this.file]))[0]; const approvalResponse: ApproveResponse = (await this._filesService.getApproveWarnings([this.file()]))[0];
this._loadingService.stop();
if (!approvalResponse.hasWarnings) { if (!approvalResponse.hasWarnings) {
await this._filesService.reload(this.file().dossierId, this.file());
this._loadingService.stop();
return; return;
} }
this._loadingService.stop();
const data: IConfirmationDialogData = { const data: IConfirmationDialogData = {
title: _('confirmation-dialog.approve-file.title'), title: _('confirmation-dialog.approve-file.title'),
@ -325,8 +358,7 @@ export class FileActionsComponent implements OnChanges {
} }
forceReanalysisAction($event: LongPressEvent) { forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.isIqserDevMode; this.#analysisForced.set(!$event.touchEnd && this._userPreferenceService.isIqserDevMode);
this.#setup();
} }
#showOCRConfirmationDialog(): Observable<boolean> { #showOCRConfirmationDialog(): Observable<boolean> {
@ -341,7 +373,7 @@ export class FileActionsComponent implements OnChanges {
} }
#openImportRedactionsDialog() { #openImportRedactionsDialog() {
this._dialogService.openDialog('importRedactions', { dossierId: this.file.dossierId, fileId: this.file.fileId }); this._dialogService.openDialog('importRedactions', { dossierId: this.file().dossierId, fileId: this.file().fileId });
} }
#openDeleteFileDialog() { #openDeleteFileDialog() {
@ -354,8 +386,8 @@ export class FileActionsComponent implements OnChanges {
async () => { async () => {
this._loadingService.start(); this._loadingService.start();
try { try {
const dossier = this._activeDossiersService.find(this.file.dossierId); const dossier = this._activeDossiersService.find(this.file().dossierId);
await firstValueFrom(this._fileManagementService.delete([this.file], this.file.dossierId)); await firstValueFrom(this._fileManagementService.delete([this.file()], this.file().dossierId));
await this._injector.get(Router).navigate([dossier.routerLink]); await this._injector.get(Router).navigate([dossier.routerLink]);
} catch (error) { } catch (error) {
this._injector.get(Toaster).error(_('error.http.generic'), { params: error }); this._injector.get(Toaster).error(_('error.http.generic'), { params: error });
@ -366,8 +398,8 @@ export class FileActionsComponent implements OnChanges {
} }
#assign() { #assign() {
const files = [this.file]; const files = [this.file()];
const targetStatus = this.file.workflowStatus; const targetStatus = this.file().workflowStatus;
const withCurrentUserAsDefault = true; const withCurrentUserAsDefault = true;
const withUnassignedOption = true; const withUnassignedOption = true;
this._iqserDialog.openDefault(AssignReviewerApproverDialogComponent, { this._iqserDialog.openDefault(AssignReviewerApproverDialogComponent, {
@ -381,29 +413,34 @@ export class FileActionsComponent implements OnChanges {
} }
async #assignToMe() { async #assignToMe() {
await this._fileAssignService.assignToMe([this.file]); await this._fileAssignService.assignToMe([this.file()]);
} }
async #reanalyseFile() { async #reanalyseFile() {
const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toasterService.error(_('dossier-listing.rules.timeoutError'));
return;
}
const params: ReanalyzeQueryParams = { const params: ReanalyzeQueryParams = {
force: true, force: true,
triggeredByUser: true, triggeredByUser: true,
}; };
await this._reanalysisService.reanalyzeFilesForDossier([this.file], this.file.dossierId, params); await this._reanalysisService.reanalyzeFilesForDossier([this.file()], this.file().dossierId, params);
} }
async #toggleAutomaticAnalysis() { async #toggleAutomaticAnalysis() {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file.dossierId, [this.file])); await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file().dossierId, [this.file()]));
this._loadingService.stop(); this._loadingService.stop();
} }
async #setFileUnderApproval() { async #setFileUnderApproval() {
await this._fileAssignService.assignApprover(this.file, true); await this._fileAssignService.assignApprover(this.file(), true);
} }
async #ocrFile() { async #ocrFile() {
if (this.file.lastManualChangeDate) { if (this.file().lastManualChangeDate) {
const confirm = await firstValueFrom(this.#showOCRConfirmationDialog()); const confirm = await firstValueFrom(this.#showOCRConfirmationDialog());
if (!confirm) { if (!confirm) {
return; return;
@ -416,93 +453,47 @@ export class FileActionsComponent implements OnChanges {
viewerHeaderService.disableRotationButtons(); viewerHeaderService.disableRotationButtons();
this._loadingService.start(); this._loadingService.start();
await this._reanalysisService.ocrFiles([this.file], this.file.dossierId); await this._reanalysisService.ocrFiles([this.file()], this.file().dossierId);
this._loadingService.stop(); this._loadingService.stop();
} }
async #setFileUnderReview() { async #setFileUnderReview() {
await this._fileAssignService.assignReviewer(this.file, true); await this._fileAssignService.assignReviewer(this.file(), true);
} }
async #toggleAnalysis() { async #toggleAnalysis() {
this._loadingService.start(); this._loadingService.start();
await this._reanalysisService.toggleAnalysis(this.file.dossierId, [this.file], !this.file.excluded); await this._reanalysisService.toggleAnalysis(this.file().dossierId, [this.file()], !this.file().excluded);
this._loadingService.stop(); this._loadingService.stop();
} }
#setup() {
this.isDossierOverviewList = this.type === 'dossier-overview-list';
this.isDossierOverviewWorkflow = this.type === 'dossier-overview-workflow';
this.isDossierOverview = this.type.startsWith('dossier-overview');
this.isFilePreview = this.type === 'file-preview';
this.assignTooltip = this.file.isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
this.showAssign =
(this._permissionsService.canAssignUser(this.file, this.dossier) ||
this._permissionsService.canUnassignUser(this.file, this.dossier)) &&
this.isDossierOverview;
this.showAssignToSelf = this._permissionsService.canAssignToSelf(this.file, this.dossier) && this.isDossierOverview;
this.showImportRedactions = this._permissionsService.canImportRedactions(this.file, this.dossier);
this.showSetToNew = this._permissionsService.canSetToNew(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUndoApproval = this._permissionsService.canUndoApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUnderReview = this._permissionsService.canSetUnderReview(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUnderApproval = this._permissionsService.canSetUnderApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showApprove = this._permissionsService.isReadyForApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.file, this.dossier);
this.showToggleAnalysis = !!this.file.lastProcessed && this._permissionsService.showToggleAnalysis(this.dossier);
this.toggleTooltip = this._toggleTooltip;
this.isDossierMember = this._permissionsService.isDossierMember(this.dossier);
this.showDelete = this._permissionsService.canSoftDeleteFile(this.file, this.dossier);
this.showOCR = this._permissionsService.canOcrFile(this.file, this.dossier);
this.canReanalyse = this._permissionsService.canReanalyseFile(this.file, this.dossier);
this.canDisableAutoAnalysis = !this.#isDocumine && this._permissionsService.canDisableAutoAnalysis([this.file], this.dossier);
this.canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis([this.file], this.dossier);
this.showStatusBar = !this.file.isError && !this.file.isUnprocessed && this.isDossierOverviewList;
const showReanalyse =
(this.canReanalyse || this.file.excludedFromAutomaticAnalysis || this.analysisForced) && !this.file.dossierArchived;
this.showReanalyseFilePreview = showReanalyse && this.isFilePreview && !this.file.isApproved && this.isDossierMember;
this.showReanalyseDossierOverview = showReanalyse && this.isDossierOverview && !this.file.isApproved && this.isDossierMember;
this.showDownload = this._permissionsService.canDownloadRedactedFile() && !!this.file.lastProcessed && this.isDossierMember;
this.buttons = this._buttons;
this._changeRef.markForCheck();
}
async #setFileApproved() { async #setFileApproved() {
this._loadingService.start(); this._loadingService.start();
await this._filesService.setApproved(this.file); await this._filesService.setApproved(this.file());
this._loadingService.stop(); this._loadingService.stop();
} }
async #setToNew() { async #setToNew() {
this._loadingService.start(); this._loadingService.start();
await this._filesService.setToNew(this.file); await this._filesService.setToNew(this.file());
this._loadingService.stop(); this._loadingService.stop();
} }
#toggleExcludePages() { #toggleExcludePages() {
this._excludedPagesService.toggle(); this._excludedPagesService.toggle();
const shown = this._excludedPagesService.shown(); const shown = this._excludedPagesService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', shown); setLocalStorageDataByFileId(this.file().id, 'show-exclude-pages', shown);
if (shown) { if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-document-info', false); setLocalStorageDataByFileId(this.file().id, 'show-document-info', false);
} }
} }
#toggleDocumentInfo() { #toggleDocumentInfo() {
this._documentInfoService.toggle(); this._documentInfoService.toggle();
const shown = this._documentInfoService.shown(); const shown = this._documentInfoService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-document-info', shown); setLocalStorageDataByFileId(this.file().id, 'show-document-info', shown);
if (shown) { if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', false); setLocalStorageDataByFileId(this.file().id, 'show-exclude-pages', false);
} }
} }
} }

View File

@ -8,7 +8,9 @@
> >
<redaction-annotation-icon <redaction-annotation-icon
[color]="dictionary.hexColor" [color]="dictionary.hexColor"
[label]="dictionary.hint ? 'H' : 'R'" [label]="
dictionary.hint ? (workloadTranslations['hint'] | translate)[0] : (workloadTranslations['redaction'] | translate)[0]
"
type="square" type="square"
></redaction-annotation-icon> ></redaction-annotation-icon>
<div class="details"> <div class="details">
@ -52,8 +54,8 @@
[currentDossierTemplateId]="dossier.dossierTemplateId" [currentDossierTemplateId]="dossier.dossierTemplateId"
[hint]="selectedDictionary.hint" [hint]="selectedDictionary.hint"
[initialEntries]="entriesToDisplay || []" [initialEntries]="entriesToDisplay || []"
[selectedDictionaryTypeLabel]="selectedDictionary.label"
[selectedDictionaryType]="selectedDictionary.type" [selectedDictionaryType]="selectedDictionary.type"
[activeDictionary]="selectedDictionary"
[withFloatingActions]="false" [withFloatingActions]="false"
> >
<ng-container slot="typeSwitch"> <ng-container slot="typeSwitch">

View File

@ -20,6 +20,7 @@ import { AnnotationIconComponent } from '@shared/components/annotation-icon/anno
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { workloadTranslations } from '@translations/workload-translations';
@Component({ @Component({
selector: 'redaction-edit-dossier-dictionary', selector: 'redaction-edit-dossier-dictionary',
@ -47,6 +48,7 @@ export class EditDossierDictionaryComponent implements OnInit {
activeEntryType = DictionaryEntryTypes.ENTRY; activeEntryType = DictionaryEntryTypes.ENTRY;
entriesToDisplay: List = []; entriesToDisplay: List = [];
readonly entryTypes = DictionaryEntryTypes; readonly entryTypes = DictionaryEntryTypes;
protected readonly workloadTranslations = workloadTranslations;
@ViewChild(DictionaryManagerComponent, { static: false }) private readonly _dictionaryManager: DictionaryManagerComponent; @ViewChild(DictionaryManagerComponent, { static: false }) private readonly _dictionaryManager: DictionaryManagerComponent;
constructor( constructor(
@ -81,7 +83,7 @@ export class EditDossierDictionaryComponent implements OnInit {
try { try {
await this._dictionaryService.saveEntries( await this._dictionaryService.saveEntries(
this._dictionaryManager.editor.currentEntries, this._dictionaryManager.editor.currentEntries,
this._dictionaryManager.initialEntries, this._dictionaryManager.initialEntries(),
this.dossier.dossierTemplateId, this.dossier.dossierTemplateId,
this.selectedDictionary.type, this.selectedDictionary.type,
this.dossier.id, this.dossier.id,

View File

@ -44,11 +44,15 @@
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="edit-dossier-dialog.general-info.form.dossier-state.label"></label> <label translate="edit-dossier-dialog.general-info.form.dossier-state.label"></label>
<mat-form-field> <mat-form-field>
<mat-select [placeholder]="statusPlaceholder" formControlName="dossierStatusId"> <mat-select [placeholder]="statePlaceholder()" formControlName="dossierStatusId">
<mat-option *ngFor="let stateId of states" [value]="stateId"> <mat-option *ngFor="let stateId of states()" [value]="stateId">
<div [matTooltip]="getStateName(stateId)" class="flex-align-items-center" matTooltipPosition="after"> <div
<iqser-small-chip *ngIf="!!stateId" [color]="getStateColor(stateId)"></iqser-small-chip> [matTooltip]="stateNameAndColor()[stateId]?.name"
<div class="clamp-1">{{ getStateName(stateId) }}</div> class="flex-align-items-center"
matTooltipPosition="after"
>
<iqser-small-chip *ngIf="!!stateId" [color]="stateNameAndColor()[stateId]?.color"></iqser-small-chip>
<div class="clamp-1">{{ stateNameAndColor()[stateId]?.name }}</div>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -80,7 +84,7 @@
<div class="dialog-actions"> <div class="dialog-actions">
<iqser-icon-button <iqser-icon-button
(action)="deleteDossier()" (action)="deleteDossier()"
*ngIf="permissionsService.canDeleteDossier(dossier)" *ngIf="permissionsService.canDeleteDossier(dossier())"
[attr.help-mode-key]="'edit_dossier_delete_dossier_DIALOG'" [attr.help-mode-key]="'edit_dossier_delete_dossier_DIALOG'"
[buttonId]="'deleteDossier'" [buttonId]="'deleteDossier'"
[icon]="'iqser:trash'" [icon]="'iqser:trash'"
@ -90,7 +94,7 @@
<iqser-icon-button <iqser-icon-button
(action)="archiveDossier()" (action)="archiveDossier()"
*ngIf="permissionsService.canArchiveDossier(dossier)" *ngIf="permissionsService.canArchiveDossier(dossier())"
[attr.help-mode-key]="'edit_dossier_archive_dossier_DIALOG'" [attr.help-mode-key]="'edit_dossier_archive_dossier_DIALOG'"
[icon]="'red:archive'" [icon]="'red:archive'"
[label]="'dossier-listing.archive.action' | translate" [label]="'dossier-listing.archive.action' | translate"

View File

@ -1,12 +1,12 @@
import { NgForOf, NgIf } from '@angular/common'; import { NgForOf, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, effect, input, OnInit, signal, untracked } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { FormGroup, ReactiveFormsModule, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { MatFormField, MatSuffix } from '@angular/material/form-field'; import { MatFormField, MatSuffix } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatOption, MatSelect } from '@angular/material/select'; import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -30,12 +30,22 @@ import { DossiersService } from '@services/dossiers/dossiers.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service'; import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { TrashService } from '@services/entity-services/trash.service'; import { TrashService } from '@services/entity-services/trash.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { dateWithoutTime } from '@utils/functions'; import { dateWithoutTime, formControlToSignal } from '@utils/functions';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service'; import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { type EditDossierDialogComponent } from '../edit-dossier-dialog.component'; import { type EditDossierDialogComponent } from '../edit-dossier-dialog.component';
import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface'; import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { AsControl, isJustOne } from '@common-ui/utils';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
interface GeneralInfoForm {
dossierName: string;
dossierTemplateId: string;
dossierStatusId?: string;
description?: string;
dueDate?: string;
}
@Component({ @Component({
selector: 'redaction-edit-dossier-general-info', selector: 'redaction-edit-dossier-general-info',
@ -59,18 +69,36 @@ import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-doss
MatSuffix, MatSuffix,
IconButtonComponent, IconButtonComponent,
NgIf, NgIf,
MatSelectTrigger,
], ],
}) })
export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSectionInterface { export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSectionInterface {
@Input() dossier: Dossier;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly dossier = input<Dossier>();
form: UntypedFormGroup;
statusPlaceholder: string;
hasDueDate: boolean; hasDueDate: boolean;
dossierTemplates: IDossierTemplate[]; dossierTemplates: IDossierTemplate[];
states: string[]; form: FormGroup<AsControl<GeneralInfoForm>> = this._formBuilder.group({
dossierName: [null, Validators.required],
dossierTemplateId: [null, Validators.required],
dossierStatusId: [null],
description: [null],
dueDate: [null],
});
initialFormValue: GeneralInfoForm;
readonly dossierStatusIdControl = formControlToSignal(this.form.controls.dossierStatusId);
readonly dossierTemplateIdControl = formControlToSignal<GeneralInfoForm['dossierTemplateId']>(this.form.controls.dossierTemplateId);
readonly states = signal([null]);
readonly stateNameAndColor = computed(() => {
const nameAndColor = {};
this.states().forEach(stateId => {
nameAndColor[stateId] = {
name: this.#getStateName(stateId, untracked(this.dossierTemplateIdControl)),
color: this.#getStateColor(stateId, untracked(this.dossierTemplateIdControl)),
};
});
return nameAndColor;
});
readonly statePlaceholder = computed(() => this.#statePlaceholder);
constructor( constructor(
readonly permissionsService: PermissionsService, readonly permissionsService: PermissionsService,
@ -87,18 +115,40 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _archivedDossiersService: ArchivedDossiersService, private readonly _archivedDossiersService: ArchivedDossiersService,
) {} private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dossierStatesService: DossierStatesService,
) {
effect(() => {
const shouldBeDisabled = this.#formValue.find(item => item.key === 'dossierTemplateId')?.disabled;
if (
(this.dossierStatusIdControl() !== this.initialFormValue.dossierStatusId && this.dossierStatusIdControl()) ||
shouldBeDisabled
) {
this.form.controls.dossierTemplateId.disable();
} else {
this.form.controls.dossierTemplateId.enable();
}
});
effect(
() => {
this.states.set(this.#statesForDossierTemplate);
this.#onDossierTemplateChange();
},
{ allowSignalWrites: true },
);
}
get changed(): boolean { get changed(): boolean {
for (const key of Object.keys(this.form.getRawValue())) { for (const key of Object.keys(this.form.getRawValue())) {
if (key === 'dueDate') { if (key === 'dueDate') {
if (this.hasDueDate !== !!this.dossier.dueDate) { if (this.hasDueDate !== !!this.dossier().dueDate) {
return true; return true;
} }
if (this.hasDueDate && !dayjs(this.dossier.dueDate).isSame(dayjs(this.form.get(key).value), 'day')) { if (this.hasDueDate && !dayjs(this.dossier().dueDate).isSame(dayjs(this.form.get(key).value), 'day')) {
return true; return true;
} }
} else if (this.dossier[key] !== this.form.get(key).value) { } else if (this.dossier()[key] !== this.form.get(key).value) {
return true; return true;
} }
} }
@ -114,40 +164,87 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
return this.hasDueDate && this.form.get('dueDate').value === null; return this.hasDueDate && this.form.get('dueDate').value === null;
} }
get #statusPlaceholder(): string { get #statePlaceholder(): string {
return this._translateService.instant( return this._translateService.instant(
this.states.length === 1 isJustOne(this.states())
? 'edit-dossier-dialog.general-info.form.dossier-state.no-state-placeholder' ? 'edit-dossier-dialog.general-info.form.dossier-state.no-state-placeholder'
: 'dossier-state.placeholder', : 'dossier-state.placeholder',
) as string; ) as string;
} }
get #statesForDossierTemplate() {
return [
null,
...this._dossierStatesMapService
.get(this.dossierTemplateIdControl() ?? untracked(this.dossier).dossierTemplateId)
.map(s => s.id),
];
}
get #formValue(): { key: string; value: string; disabled: boolean }[] {
const dossier = untracked(this.dossier);
const formFieldWithArchivedCheck = value => ({ value, disabled: !dossier.isActive });
const states = untracked(this.states);
return [
{
key: 'dossierName',
...formFieldWithArchivedCheck(dossier.dossierName),
},
{
key: 'dossierTemplateId',
value: dossier.dossierTemplateId,
disabled: this._dossierStatsService.get(dossier.id).hasFiles || !dossier.isActive,
},
{
key: 'dossierStatusId',
value: dossier.dossierStatusId,
disabled: isJustOne(states) || !dossier.isActive,
},
{
key: 'description',
...formFieldWithArchivedCheck(dossier.description),
},
{
key: 'dueDate',
...formFieldWithArchivedCheck(dossier.dueDate),
},
];
}
ngOnInit() { ngOnInit() {
this.states = [null, ...this._dossierStatesMapService.get(this.dossier.dossierTemplateId).map(s => s.id)]; if (isJustOne(this._dossierTemplatesService.all)) {
this.statusPlaceholder = this.#statusPlaceholder; this._loadingService.loadWhile(
this.#filterInvalidDossierTemplates(); firstValueFrom(this._dossierTemplatesService.loadOnlyDossierTemplates()).then(async () => {
this.form = this.#getForm(); await firstValueFrom(this._dossierStatesService.loadAllForAllTemplates());
if (!this.permissionsService.canEditDossier(this.dossier)) { this.#filterInvalidDossierTemplates();
}),
);
} else {
this.#filterInvalidDossierTemplates();
}
this.#patchFormValue();
if (!this.permissionsService.canEditDossier(this.dossier())) {
this.form.disable(); this.form.disable();
} }
this.hasDueDate = !!this.dossier.dueDate; this.hasDueDate = !!this.dossier().dueDate;
} }
revert() { revert() {
this.form.reset({ this.form.reset({
dossierName: this.dossier.dossierName, dossierName: this.dossier().dossierName,
dossierTemplateId: this.dossier.dossierTemplateId, dossierTemplateId: this.dossier().dossierTemplateId,
dossierStatusId: this.dossier.dossierStatusId, dossierStatusId: this.dossier().dossierStatusId,
description: this.dossier.description, description: this.dossier().description,
dueDate: this.dossier.dueDate, dueDate: this.dossier().dueDate,
}); });
this.hasDueDate = !!this.dossier.dueDate; this.hasDueDate = !!this.dossier().dueDate;
this.initialFormValue = this.form.getRawValue();
} }
async save(): EditDossierSaveResult { async save(): EditDossierSaveResult {
const dueDate = dateWithoutTime(dayjs(this.form.get('dueDate').value)); const dueDate = dateWithoutTime(dayjs(this.form.get('dueDate').value));
const dossier = { const dossier = {
...this.dossier, ...this.dossier(),
dossierName: this.form.get('dossierName').value, dossierName: this.form.get('dossierName').value,
description: this.form.get('description').value, description: this.form.get('description').value,
dueDate: dueDate.isValid() ? dueDate.toISOString() : undefined, dueDate: dueDate.isValid() ? dueDate.toISOString() : undefined,
@ -156,9 +253,10 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
} as IDossierRequest; } as IDossierRequest;
const updatedDossier = await firstValueFrom(this._dossiersService.createOrUpdate(dossier)); const updatedDossier = await firstValueFrom(this._dossiersService.createOrUpdate(dossier));
if (updatedDossier && updatedDossier.dossierTemplateId !== this.dossier.dossierTemplateId) { if (updatedDossier && updatedDossier.dossierTemplateId !== this.dossier().dossierTemplateId) {
await this._router.navigate([updatedDossier.routerLink]); await this._router.navigate([updatedDossier.routerLink]);
} }
this.initialFormValue = this.form.getRawValue();
return { success: !!updatedDossier }; return { success: !!updatedDossier };
} }
@ -171,14 +269,14 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
requireInput: true, requireInput: true,
denyText: _('confirmation-dialog.delete-dossier.deny-text'), denyText: _('confirmation-dialog.delete-dossier.deny-text'),
translateParams: { translateParams: {
dossierName: this.dossier.dossierName, dossierName: this.dossier().dossierName,
dossiersCount: 1, dossiersCount: 1,
}, },
}; };
this._dialogService.openDialog('confirm', data, async () => { this._dialogService.openDialog('confirm', data, async () => {
this._loadingService.start(); this._loadingService.start();
const successful = await this._trashService.deleteDossier(this.dossier); const successful = await this._trashService.deleteDossier(this.dossier());
if (successful) { if (successful) {
await this.#closeDialogAndRedirectToDossier(); await this.#closeDialogAndRedirectToDossier();
} }
@ -194,7 +292,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
confirmationText: _('confirm-archive-dossier.archive'), confirmationText: _('confirm-archive-dossier.archive'),
denyText: _('confirm-archive-dossier.cancel'), denyText: _('confirm-archive-dossier.cancel'),
titleColor: TitleColors.WARN, titleColor: TitleColors.WARN,
translateParams: { ...this.dossier }, translateParams: { ...this.dossier() },
checkboxes: [{ value: false, label: _('confirm-archive-dossier.checkbox.documents') }], checkboxes: [{ value: false, label: _('confirm-archive-dossier.checkbox.documents') }],
toastMessage: _('confirm-archive-dossier.toast-error'), toastMessage: _('confirm-archive-dossier.toast-error'),
}; };
@ -202,10 +300,10 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
this._dialogService.openDialog('confirm', data, async result => { this._dialogService.openDialog('confirm', data, async result => {
if (result === ConfirmOptions.CONFIRM) { if (result === ConfirmOptions.CONFIRM) {
this._loadingService.start(); this._loadingService.start();
await firstValueFrom(this._archivedDossiersService.archive([this.dossier])); await firstValueFrom(this._archivedDossiersService.archive([this.dossier()]));
this._toaster.success(_('dossier-listing.archive.archive-succeeded'), { this._toaster.success(_('dossier-listing.archive.archive-succeeded'), {
params: { params: {
dossierName: this.dossier.dossierName, dossierName: this.dossier().dossierName,
}, },
}); });
this._editDossierDialogRef.close(); this._editDossierDialogRef.close();
@ -214,15 +312,6 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
}); });
} }
getStateName(stateId: string): string {
return (this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId)?.name ||
this._translateService.instant('dossier-state.placeholder')) as string;
}
getStateColor(stateId: string): string {
return this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId).color;
}
toggleDueDateField() { toggleDueDateField() {
this.hasDueDate = !this.hasDueDate; this.hasDueDate = !this.hasDueDate;
if (!this.hasDueDate) { if (!this.hasDueDate) {
@ -230,46 +319,63 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
} }
} }
#getStateName(stateId: string, templateId: string): string {
return (this._dossierStatesMapService.get(templateId, stateId)?.name ||
this._translateService.instant('dossier-state.placeholder')) as string;
}
#getStateColor(stateId: string, templateId: string): string {
return this._dossierStatesMapService.get(templateId, stateId)?.color;
}
#patchFormValue() {
this.#formValue.forEach(formValue => {
this.form.patchValue({ [formValue.key]: formValue.value });
if (formValue.disabled) this.form.get(formValue.key).disable();
});
this.initialFormValue = this.form.getRawValue();
}
async #closeDialogAndRedirectToDossier() { async #closeDialogAndRedirectToDossier() {
this._editDossierDialogRef.close(); this._editDossierDialogRef.close();
await this._router.navigate([this.dossier.dossiersListRouterLink]); await this._router.navigate([this.dossier().dossiersListRouterLink]);
this._toaster.success(_('edit-dossier-dialog.delete-successful'), { this._toaster.success(_('edit-dossier-dialog.delete-successful'), {
params: { params: {
dossierName: this.dossier.dossierName, dossierName: this.dossier().dossierName,
}, },
}); });
} }
#getForm(): UntypedFormGroup {
const formFieldWithArchivedCheck = value => ({ value, disabled: !this.dossier.isActive });
return this._formBuilder.group({
dossierName: [formFieldWithArchivedCheck(this.dossier.dossierName), Validators.required],
dossierTemplateId: [
{
value: this.dossier.dossierTemplateId,
disabled: this._dossierStatsService.get(this.dossier.id).hasFiles || !this.dossier.isActive,
},
Validators.required,
],
dossierStatusId: [
{
value: this.dossier.dossierStatusId,
disabled: this.states.length === 1 || !this.dossier.isActive,
},
],
description: [formFieldWithArchivedCheck(this.dossier.description)],
dueDate: [formFieldWithArchivedCheck(this.dossier.dueDate)],
});
}
#filterInvalidDossierTemplates() { #filterInvalidDossierTemplates() {
const dossier = untracked(this.dossier);
this.dossierTemplates = this._dossierTemplatesService.all.filter(r => { this.dossierTemplates = this._dossierTemplatesService.all.filter(r => {
if (this.dossier?.dossierTemplateId === r.dossierTemplateId) { if (dossier.dossierTemplateId === r.dossierTemplateId) {
return true; return true;
} }
const notYetValid = !!r.validFrom && dayjs(r.validFrom).isAfter(dayjs()); const notYetValid = !!r.validFrom && dayjs(r.validFrom).isAfter(dayjs());
const notValidAnymore = !!r.validTo && dayjs(r.validTo).add(1, 'd').isBefore(dayjs()); const notValidAnymore = !!r.validTo && dayjs(r.validTo).add(1, 'd').isBefore(dayjs());
this._changeDetectorRef.markForCheck();
return !(notYetValid || notValidAnymore) && r.isActive; return !(notYetValid || notValidAnymore) && r.isActive;
}); });
} }
#onDossierTemplateChange() {
const dossierStateId = untracked(this.dossierStatusIdControl);
const dossierTemplateId = untracked(this.dossierTemplateIdControl);
if (!!dossierStateId && dossierTemplateId !== this.initialFormValue.dossierTemplateId) {
this.form.controls.dossierStatusId.setValue(null);
}
const dossier = untracked(this.dossier);
if (dossierTemplateId === this.initialFormValue.dossierTemplateId) {
this.form.controls.dossierStatusId.setValue(dossier.dossierStatusId);
}
const states = untracked(this.states);
if (isJustOne(states) || !dossier.isActive) {
this.form.controls.dossierStatusId.disable();
} else {
this.form.controls.dossierStatusId.enable();
}
this._changeDetectorRef.markForCheck();
}
} }

View File

@ -78,24 +78,6 @@
></textarea> ></textarea>
</div> </div>
<div *ngIf="isIqserDevMode && form.get('aiCreationEnabled')" class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="aiCreationEnabled">
{{ 'add-edit-entity.form.ai-creation-enabled' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="isIqserDevMode && form.get('aiCreationEnabled')?.value && form.get('aiDescription')" class="iqser-input-group w-400">
<label translate="add-edit-entity.form.ai-description"></label>
<textarea
[placeholder]="'add-edit-entity.form.ai-description-placeholder' | translate"
formControlName="aiDescription"
iqserHasScrollbar
name="aiDescription"
rows="4"
type="text"
></textarea>
</div>
<div *ngIf="form.get('hasDictionary')" class="iqser-input-group"> <div *ngIf="form.get('hasDictionary')" class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="hasDictionary"> <mat-slide-toggle color="primary" formControlName="hasDictionary">
{{ 'add-edit-entity.form.has-dictionary' | translate }} {{ 'add-edit-entity.form.has-dictionary' | translate }}

View File

@ -9,7 +9,7 @@ import { MatSelect } from '@angular/material/select';
import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatSlideToggle } from '@angular/material/slide-toggle';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RoundCheckboxComponent } from '@common-ui/inputs/round-checkbox/round-checkbox.component'; import { RoundCheckboxComponent } from '@common-ui/inputs/round-checkbox/round-checkbox.component';
import { BaseFormComponent, getConfig, isIqserDevMode, HasScrollbarDirective, LoadingService, Toaster } from '@iqser/common-ui'; import { BaseFormComponent, getConfig, HasScrollbarDirective, LoadingService, Toaster } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Dictionary, IDictionary } from '@red/domain'; import { Dictionary, IDictionary } from '@red/domain';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
@ -62,7 +62,6 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
colors: Color[]; colors: Color[];
readonly isDocumine = getConfig().IS_DOCUMINE; readonly isDocumine = getConfig().IS_DOCUMINE;
readonly isIqserDevMode = isIqserDevMode();
constructor( constructor(
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
@ -159,8 +158,6 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
skippedHexColor: [this.entity?.skippedHexColor, [Validators.required, Validators.minLength(7)]], skippedHexColor: [this.entity?.skippedHexColor, [Validators.required, Validators.minLength(7)]],
type: [this.entity?.type], type: [this.entity?.type],
description: [this.entity?.description], description: [this.entity?.description],
aiCreationEnabled: [this.entity?.aiCreationEnabled],
aiDescription: [this.entity?.aiDescription],
rank: [{ value: this.entity?.rank, disabled: this.#isSystemManaged }, Validators.required], rank: [{ value: this.entity?.rank, disabled: this.#isSystemManaged }, Validators.required],
hint: [{ value: !!this.entity?.hint, disabled: this.#isSystemManaged }], hint: [{ value: !!this.entity?.hint, disabled: this.#isSystemManaged }],
hasDictionary: [ hasDictionary: [
@ -253,8 +250,6 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
dossierTemplateId: this.dossierTemplateId, dossierTemplateId: this.dossierTemplateId,
type: this.form.get('type').value, type: this.form.get('type').value,
description: this.form.get('description').value, description: this.form.get('description').value,
aiCreationEnabled: this.form.get('aiCreationEnabled').value,
aiDescription: this.form.get('aiDescription').value,
hint: this.#isHint, hint: this.#isHint,
rank: this.form.get('rank').value, rank: this.form.get('rank').value,
caseInsensitive: !this.form.get('caseSensitive').value, caseInsensitive: !this.form.get('caseSensitive').value,

View File

@ -9,7 +9,7 @@ import { APP_BASE_HREF } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { ComponentLogService } from '@services/files/component-log.service'; import { ComponentLogService } from '@services/entity-services/component-log.service';
@Component({ @Component({
selector: 'redaction-file-download-btn', selector: 'redaction-file-download-btn',
@ -24,8 +24,8 @@ export class FileDownloadBtnComponent implements OnChanges {
readonly tooltipPosition = input<'above' | 'below' | 'before' | 'after'>('above'); readonly tooltipPosition = input<'above' | 'below' | 'before' | 'after'>('above');
readonly type = input<CircleButtonType>(CircleButtonTypes.default); readonly type = input<CircleButtonType>(CircleButtonTypes.default);
readonly tooltipClass = input<string>(); readonly tooltipClass = input<string>();
readonly disabled = input<boolean>(false); readonly disabled = input(false, { transform: booleanAttribute });
readonly singleFileDownload = input<boolean>(false); readonly singleFileDownload = input(false, { transform: booleanAttribute });
readonly dossierDownload = input(false, { transform: booleanAttribute }); readonly dossierDownload = input(false, { transform: booleanAttribute });
readonly dropdownButton = computed(() => this.isDocumine && (this.dossierDownload() || this.singleFileDownload())); readonly dropdownButton = computed(() => this.isDocumine && (this.dossierDownload() || this.singleFileDownload()));
tooltip: string; tooltip: string;

View File

@ -5,7 +5,7 @@
<iqser-circle-button <iqser-circle-button
(action)="download()" (action)="download()"
*ngIf="canDownload" *ngIf="canDownload()"
[attr.help-mode-key]="helpModeKey" [attr.help-mode-key]="helpModeKey"
[matTooltip]="'dictionary-overview.download' | translate" [matTooltip]="'dictionary-overview.download' | translate"
class="ml-8" class="ml-8"
@ -52,26 +52,34 @@
</div> </div>
<div class="iqser-input-group w-200 mt-0"> <div class="iqser-input-group w-200 mt-0">
<mat-form-field *ngIf="initialDossierTemplateId"> @if (initialDossierTemplateId) {
<mat-select <mat-form-field>
[(ngModel)]="selectedDossier" <mat-select
[disabled]="!compare || (dossiers.length === 1 && !optionNotSelected)" [(ngModel)]="selectedDossier"
[placeholder]="selectedDossier.dossierId ? selectedDossier.dossierName : (selectDictionary.label | translate)" [disabled]="!compare || (dossiers.length === 1 && !optionNotSelected)"
> [placeholder]="
<ng-container *ngFor="let dossier of dossiers; let index = index"> selectedDossier.dossierId ? selectedDossier.dossierName : (selectDictionary.label | translate)
<mat-option "
*ngIf="dossier.dossierId !== selectedDossier.dossierId" >
[class.mat-mdc-option-active]="false" @for (dossier of dossiers; track dossier; let index = $index) {
[value]="dossier" @if (dossier.dossierId !== selectedDossier.dossierId) {
> @if (!activeDictionary().dossierDictionaryOnly) {
{{ dossier.dossierName }} <mat-option [class.mat-mdc-option-active]="false" [value]="dossier">
</mat-option> {{ dossier.dossierName }}
<mat-divider </mat-option>
*ngIf="index === dossiers.length - 2 && !selectedDossier.dossierId?.includes('template')" @if (index === dossiers.length - 2 && !selectedDossier.dossierId?.includes('template')) {
></mat-divider> <mat-divider></mat-divider>
</ng-container> }
</mat-select> } @else if (!dossier.dossierId?.includes('template')) {
</mat-form-field> <mat-option [class.mat-mdc-option-active]="false" [value]="dossier">
{{ dossier.dossierName }}
</mat-option>
}
}
}
</mat-select>
</mat-form-field>
}
<mat-form-field *ngIf="!initialDossierTemplateId"> <mat-form-field *ngIf="!initialDossierTemplateId">
<mat-select <mat-select
@ -94,9 +102,9 @@
<div class="editor-container"> <div class="editor-container">
<redaction-editor <redaction-editor
[(isSearchOpen)]="_isSearchOpen" [(isSearchOpen)]="_isSearchOpen"
[canEdit]="canEdit" [canEdit]="canEdit()"
[diffEditorText]="diffEditorText" [diffEditorText]="diffEditorText"
[initialEntries]="initialEntries" [initialEntries]="initialEntries()"
[showDiffEditor]="compare && showDiffEditor" [showDiffEditor]="compare && showDiffEditor"
></redaction-editor> ></redaction-editor>
@ -106,14 +114,16 @@
</div> </div>
</div> </div>
<div *ngIf="withFloatingActions && !!editor?.hasChanges && canEdit && !isLeavingPage" [class.offset]="compare" class="changes-box"> @if (withFloatingActions() && !!editor?.hasChanges && canEdit() && !isLeavingPage()) {
<iqser-icon-button <div [class.offset]="compare" class="changes-box">
(action)="saveDictionary.emit()" <iqser-icon-button
[disabled]="!!_loadingService.isLoading()" (action)="saveDictionary.emit()"
[label]="'dictionary-overview.save-changes' | translate" [disabled]="!!_loadingService.isLoading()"
[type]="iconButtonTypes.primary" [label]="'dictionary-overview.save-changes' | translate"
icon="iqser:check" [type]="iconButtonTypes.primary"
></iqser-icon-button> icon="iqser:check"
<div (click)="revert()" class="all-caps-label cancel" translate="dictionary-overview.revert-changes"></div> ></iqser-icon-button>
</div> <div (click)="revert()" class="all-caps-label cancel" translate="dictionary-overview.revert-changes"></div>
</div>
}
</div> </div>

View File

@ -1,13 +1,14 @@
import { import {
booleanAttribute,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
EventEmitter, effect,
Input, input,
OnChanges, model,
OnInit, OnInit,
Output, output,
signal, signal,
SimpleChanges, untracked,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService } from '@iqser/common-ui'; import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService } from '@iqser/common-ui';
@ -60,25 +61,25 @@ const HELP_MODE_KEYS = {
EditorComponent, EditorComponent,
], ],
}) })
export class DictionaryManagerComponent implements OnChanges, OnInit { export class DictionaryManagerComponent implements OnInit {
@Input() type: DictionaryType = 'dictionary'; readonly type = input<DictionaryType>('dictionary');
@Input() entityType?: string; readonly entityType = input<string>();
@Input() currentDossierId: string; readonly currentDossierId = input<string>();
@Input() currentDossierTemplateId: string; readonly currentDossierTemplateId = model<string>();
@Input() withFloatingActions = true; readonly withFloatingActions = input(true, { transform: booleanAttribute });
@Input() initialEntries: List; readonly initialEntries = input.required<List>();
@Input() canEdit = false; readonly canEdit = input(false, { transform: booleanAttribute });
@Input() canDownload = false; readonly canDownload = input(false, { transform: booleanAttribute });
@Input() isLeavingPage = false; readonly isLeavingPage = input(false, { transform: booleanAttribute });
@Input() hint = false; readonly hint = input(false, { transform: booleanAttribute });
@Input() selectedDictionaryType = 'dossier_redaction'; readonly activeDictionary = input<Dictionary>();
@Input() selectedDictionaryTypeLabel: string; readonly selectedDictionaryType = model<string>('dossier_redaction');
@Input() activeEntryType: DictionaryEntryType = DictionaryEntryTypes.ENTRY; readonly activeEntryType = input<DictionaryEntryType>(DictionaryEntryTypes.ENTRY);
@Output() readonly saveDictionary = new EventEmitter<string[]>(); readonly saveDictionary = output();
@ViewChild(EditorComponent) readonly editor: EditorComponent; @ViewChild(EditorComponent) readonly editor: EditorComponent;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
dossiers: Dossier[]; dossiers: Dossier[];
dossierTemplates: DossierTemplate[] = this.dossierTemplatesService.all; dossierTemplates: DossierTemplate[];
diffEditorText = ''; diffEditorText = '';
showDiffEditor = false; showDiffEditor = false;
selectDossier = { dossierName: _('dictionary-overview.compare.select-dossier') } as Dossier; selectDossier = { dossierName: _('dictionary-overview.compare.select-dossier') } as Dossier;
@ -91,7 +92,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
protected readonly _isSearchOpen = signal(false); protected readonly _isSearchOpen = signal(false);
protected initialDossierTemplateId: string; protected initialDossierTemplateId: string;
readonly #currentTab = window.location.href.split('/').pop(); readonly #currentTab = window.location.href.split('/').pop();
#dossierTemplate = this.dossierTemplatesService.all[0]; #dossierTemplate;
#dossier = this.selectDossier; #dossier = this.selectDossier;
#dictionary = this.selectDictionary; #dictionary = this.selectDictionary;
@ -100,9 +101,26 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
private readonly _activeDossiersService: ActiveDossiersService, private readonly _activeDossiersService: ActiveDossiersService,
private readonly _changeRef: ChangeDetectorRef, private readonly _changeRef: ChangeDetectorRef,
private readonly _dossierTemplatesService: DossierTemplatesService,
protected readonly _loadingService: LoadingService, protected readonly _loadingService: LoadingService,
readonly dossierTemplatesService: DossierTemplatesService, ) {
) {} effect(() => {
if (this.activeEntryType() && this.#dossier?.dossierTemplateId && this.selectedDossier?.dossierId) {
this.#onDossierChanged(this.#dossier.dossierTemplateId, this.#dossier.dossierId).then(entries =>
this.#updateDiffEditorText(entries),
);
}
});
effect(
() => {
if (this.selectedDictionaryType()) {
this.#disableDiffEditor();
this.#updateDropdownsOptions();
}
},
{ allowSignalWrites: true },
);
}
get selectedDossierTemplate() { get selectedDossierTemplate() {
return this.#dossierTemplate; return this.#dossierTemplate;
@ -115,12 +133,12 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
: this.selectDossierTemplate; : this.selectDossierTemplate;
} }
this.#dossierTemplate = value; this.#dossierTemplate = value;
this.currentDossierTemplateId = value.dossierTemplateId; this.currentDossierTemplateId.set(value.dossierTemplateId);
this.#dossier = this.selectDossier; this.#dossier = this.selectDossier;
this.dictionaries = this.#dictionaries; this.dictionaries = this.#dictionaries;
this.#disableDiffEditor(); this.#disableDiffEditor();
if (!this.initialDossierTemplateId && !this.currentDossierId) { if (!this.initialDossierTemplateId && !this.currentDossierId()) {
this.selectedDictionary = this.selectDictionary; this.selectedDictionary = this.selectDictionary;
} }
@ -148,9 +166,8 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
set selectedDictionary(dictionary: Dictionary) { set selectedDictionary(dictionary: Dictionary) {
if (dictionary.type) { if (dictionary.type) {
this.selectedDictionaryType = dictionary.type; this.selectedDictionaryType.set(dictionary.type);
this.#dictionary = dictionary; this.#dictionary = dictionary;
console.log(dictionary);
this.#onDossierChanged(this.#dossier.dossierTemplateId).then(entries => this.#updateDiffEditorText(entries)); this.#onDossierChanged(this.#dossier.dossierTemplateId).then(entries => this.#updateDiffEditorText(entries));
} }
} }
@ -181,13 +198,17 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
get #templatesWithCurrentEntityType() { get #templatesWithCurrentEntityType() {
return this.dossierTemplatesService.all.filter(t => return this._dossierTemplatesService.all.filter(t =>
this._dictionaryService.hasType(t.dossierTemplateId, this.selectedDictionaryType), this._dictionaryService.hasType(t.dossierTemplateId, untracked(this.selectedDictionaryType)),
); );
} }
ngOnInit() { async ngOnInit() {
this.initialDossierTemplateId = this.currentDossierTemplateId; await firstValueFrom(this._dossierTemplatesService.loadAll());
this.dossierTemplates = this._dossierTemplatesService.all;
await firstValueFrom(this._dictionaryService.loadDictionaryDataForDossierTemplates(this.dossierTemplates.map(t => t.id)));
this.#dossierTemplate = this._dossierTemplatesService.all[0];
this.initialDossierTemplateId = this.currentDossierTemplateId();
this.#updateDropdownsOptions(); this.#updateDropdownsOptions();
} }
@ -196,7 +217,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
const blob = new Blob([content], { const blob = new Blob([content], {
type: 'text/plain;charset=utf-8', type: 'text/plain;charset=utf-8',
}); });
saveAs(blob, `${this.entityType}-${this.type}.txt`); saveAs(blob, `${this.entityType()}-${this.type()}.txt`);
} }
revert() { revert() {
@ -210,65 +231,58 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes.activeEntryType && this.#dossier?.dossierTemplateId && this.selectedDossier?.dossierId) {
this.#onDossierChanged(this.#dossier.dossierTemplateId, this.#dossier.dossierId).then(entries =>
this.#updateDiffEditorText(entries),
);
}
if (changes.selectedDictionaryType) {
this.#disableDiffEditor();
this.#updateDropdownsOptions();
}
}
async #onDossierChanged(dossierTemplateId: string, dossierId?: string) { async #onDossierChanged(dossierTemplateId: string, dossierId?: string) {
const selectedDictionaryByType = untracked(this.selectedDictionaryType);
const activeEntryType = untracked(this.activeEntryType);
let dictionary: IDictionary; let dictionary: IDictionary;
if (dossierId === 'template') { if (dossierId === 'template') {
dictionary = await this._dictionaryService.getForType(dossierTemplateId, this.selectedDictionaryType); dictionary = await this._dictionaryService.getForType(dossierTemplateId, selectedDictionaryByType);
} else { } else {
if (dossierId) { if (dossierId) {
dictionary = ( dictionary = (
await firstValueFrom( await firstValueFrom(
this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId), this._dictionaryService.loadDictionaryEntriesByType([selectedDictionaryByType], dossierTemplateId, dossierId),
).catch(() => { ).catch(() => {
return [{ entries: [COMPARE_ENTRIES_ERROR], type: '' }]; return [{ entries: [COMPARE_ENTRIES_ERROR], type: '' }];
}) })
)[0]; )[0];
} else { } else {
dictionary = this.selectedDictionaryType dictionary = selectedDictionaryByType
? await this._dictionaryService.getForType(this.currentDossierTemplateId, this.selectedDictionaryType) ? await this._dictionaryService.getForType(this.currentDossierTemplateId(), selectedDictionaryByType)
: { entries: [COMPARE_ENTRIES_ERROR], type: '' }; : { entries: [COMPARE_ENTRIES_ERROR], type: '' };
} }
} }
const activeEntries = const activeEntries =
this.activeEntryType === DictionaryEntryTypes.ENTRY || this.hint activeEntryType === DictionaryEntryTypes.ENTRY || this.hint()
? [...dictionary.entries] ? [...dictionary.entries]
: this.activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE : activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE
? [...dictionary.falsePositiveEntries] ? [...dictionary.falsePositiveEntries]
: [...dictionary.falseRecommendationEntries]; : [...dictionary.falseRecommendationEntries];
return activeEntries.join('\n'); return activeEntries.join('\n');
} }
#updateDropdownsOptions(updateSelectedDossierTemplate = true) { #updateDropdownsOptions(updateSelectedDossierTemplate = true) {
const currentDossierTemplateId = untracked(this.currentDossierTemplateId);
const currentDossierId = untracked(this.currentDossierId);
if (updateSelectedDossierTemplate) { if (updateSelectedDossierTemplate) {
this.currentDossierTemplateId = this.initialDossierTemplateId ?? this.currentDossierTemplateId; this.currentDossierTemplateId.set(this.initialDossierTemplateId ?? currentDossierTemplateId);
this.dossierTemplates = this.currentDossierTemplateId ? this.#templatesWithCurrentEntityType : this.dossierTemplatesService.all; this.dossierTemplates = this.currentDossierTemplateId
? this.#templatesWithCurrentEntityType
: this._dossierTemplatesService.all;
if (!this.currentDossierTemplateId) { if (!this.currentDossierTemplateId) {
this.dossierTemplates = [this.selectDossierTemplate, ...this.dossierTemplates]; this.dossierTemplates = [this.selectDossierTemplate, ...this.dossierTemplates];
} }
this.selectedDossierTemplate = this.dossierTemplates.find(t => t.id === this.currentDossierTemplateId); this.selectedDossierTemplate = this.dossierTemplates.find(t => t.id === currentDossierTemplateId);
} }
this.dossiers = this._activeDossiersService.all.filter( this.dossiers = this._activeDossiersService.all.filter(
d => d.dossierTemplateId === this.currentDossierTemplateId && d.id !== this.currentDossierId, d => d.dossierTemplateId === currentDossierTemplateId && d.id !== currentDossierId,
); );
const templateDictionary = { const templateDictionary = {
id: 'template', id: 'template',
dossierId: 'template', dossierId: 'template',
dossierName: 'Template Dictionary', dossierName: 'Template Dictionary',
dossierTemplateId: this.currentDossierTemplateId, dossierTemplateId: currentDossierTemplateId,
} as Dossier; } as Dossier;
this.dossiers.push(templateDictionary); this.dossiers.push(templateDictionary);
} }

View File

@ -237,6 +237,9 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
if (isCsv(uploadFile)) { if (isCsv(uploadFile)) {
this._toaster.success(_('file-upload.type.csv')); this._toaster.success(_('file-upload.type.csv'));
} }
if (isZip(uploadFile)) {
this._toaster.success(_('file-upload.type.zip'));
}
} else { } else {
uploadFile.completed = true; uploadFile.completed = true;
uploadFile.error = { uploadFile.error = {

View File

@ -99,6 +99,8 @@
.error-message { .error-message {
margin-top: 2px; margin-top: 2px;
color: var(--iqser-primary); color: var(--iqser-primary);
white-space: normal;
word-wrap: break-word;
} }
} }

View File

@ -45,6 +45,13 @@ export class DossierTemplatesService extends EntitiesService<IDossierTemplate, D
); );
} }
loadOnlyDossierTemplates(): Observable<DossierTemplate[]> {
return this.getAll().pipe(
mapEach(entity => new DossierTemplate(entity)),
tap(templates => this.setEntities(templates)),
);
}
loadDossierTemplate(dossierTemplateId: string) { loadDossierTemplate(dossierTemplateId: string) {
return this._getOne([dossierTemplateId], this._defaultModelPath).pipe( return this._getOne([dossierTemplateId], this._defaultModelPath).pipe(
map(entity => new DossierTemplate(entity)), map(entity => new DossierTemplate(entity)),

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