Compare commits

...

111 Commits

Author SHA1 Message Date
Dan Percic
101e8b8860 RED-3800: update version.properties 2023-06-08 14:19:50 +03:00
Dan Percic
bbeab30e5f RED-3800: add version properties 2023-06-08 14:11:18 +03:00
Dan Percic
923cfa856d RED-3800: try add gitlab build 2023-06-08 14:08:18 +03:00
Valentin Mihai
8fa23c33d9 RED-5912 - Suggestions make redactions disappear in PREVIEW mode 2023-06-08 11:59:13 +02:00
Valentin Mihai
7183995832 RED-5912 - Suggestions make redactions disappear in PREVIEW mode 2023-06-06 21:22:13 +02:00
Timo Bejan
e4a7597ed5 HotFix - bumped PDFTron version up from 8.x to 10.1 2023-06-02 14:40:05 +03:00
Atlassian Bamboo
75b335c1c1 chore(release) 2023-05-30 10:57:16 +02:00
Valentin Mihai
63595742b5 RED-5912 - Suggestions make redactions disappear in PREVIEW mode 2023-05-30 10:55:42 +02:00
Atlassian Bamboo
1d4184be1b chore(release) 2023-05-26 17:09:55 +02:00
George
b80486b702 RED-6786, made saving faster. 2023-05-26 18:08:07 +03:00
Atlassian Bamboo
edafd45e5c chore(release) 2023-05-26 00:23:59 +02:00
Dan Percic
eabad1b9bc RED-6786: faster dictionary saving 2023-05-26 01:22:33 +03:00
Atlassian Bamboo
fd637deb12 chore(release) 2023-05-25 23:45:09 +02:00
Dan Percic
e2bf87d147 RED-6786: update editor 2023-05-26 00:43:37 +03:00
Atlassian Bamboo
0c4a705e52 chore(release) 2023-05-25 23:29:24 +02:00
Dan Percic
45d6ca6166 RED-6786: faster text editor 2023-05-26 00:27:53 +03:00
Atlassian Bamboo
5bbcdac4df chore(release) 2023-05-24 18:48:15 +02:00
Dan Percic
c2753e6e10 RED-6786: fix editor performance issues 2023-05-24 19:46:48 +03:00
Atlassian Bamboo
e2619e9805 chore(release) 2023-05-24 17:52:20 +02:00
Dan Percic
d8c7c5be43 RED-6733: fix missing checkbox 2023-05-24 18:49:58 +03:00
Atlassian Bamboo
e260b5782f chore(release) 2023-05-24 10:00:05 +02:00
Dan Percic
758fecb94c RED-6813: check for undefined 2023-05-24 10:58:27 +03:00
Atlassian Bamboo
24ff24cfcb chore(release) 2023-05-23 20:47:42 +02:00
Valentin Mihai
b7a9b1250a RED-6012 - Watermark alignment settings 2023-05-23 21:44:49 +03:00
Atlassian Bamboo
67caaf9a2c chore(release) 2023-05-19 18:10:38 +02:00
George
4413c7570b RED-6786, add spinner when pasting text in dictionary editor. 2023-05-19 19:08:53 +03:00
Atlassian Bamboo
20a7a4efed chore(release) 2023-05-18 12:52:02 +02:00
Valentin Mihai
ded580b160 RED-6412 - INC15516657: Performance issues 2023-05-18 13:50:22 +03:00
Atlassian Bamboo
ca43cc406a chore(release) 2023-05-17 10:55:42 +02:00
Dan Percic
9903c808fa RED-6466: mark annotation as redaction if re-created 2023-05-17 11:54:15 +03:00
Atlassian Bamboo
c64d787e7f chore(release) 2023-05-17 10:44:44 +02:00
Dan Percic
1f1af16e0c RED-3800: update image url 2023-05-17 11:44:12 +03:00
Valentin Mihai
27ea01cff9 RED-6693 - Allow editing file attributes during reprocessing/queuing 2023-05-10 18:49:21 +03:00
Atlassian Bamboo
4f12b437f6 chore(release) 2023-05-02 13:12:45 +02:00
Nicoleta Panaghiu
f4a7eb35b3 RED-6643: Removed HTML escape entity in dialogs. 2023-05-02 14:10:59 +03:00
Atlassian Bamboo
608b7b06dc chore(release) 2023-04-25 13:16:44 +02:00
George
ac47c349c5 RED-6535, disable sonar to fix build 2023-04-25 14:15:12 +03:00
Valentin Mihai
8b2c765fa4 RED-6617 - Previously dict-based hint should become Ignored Hint 2023-04-23 19:30:05 +03:00
George
d29b522c0b RED-6535, refactor main file attribute display method. 2023-04-21 18:28:59 +03:00
Nicoleta Panaghiu
852fc1eab3 RED-6403: Changed values in License information page. 2023-04-21 15:57:00 +03:00
Atlassian Bamboo
128c597f71 chore(release) 2023-04-18 12:27:13 +02:00
Nicoleta Panaghiu
8d37d280cc RED-6579: Fixed HTML escape entity. 2023-04-18 13:24:21 +03:00
Atlassian Bamboo
615811dfd4 chore(release) 2023-04-12 18:18:07 +02:00
George
de6f5099e0 RED-6304, remove reason from ignored hints. 2023-04-12 19:13:14 +03:00
Atlassian Bamboo
491cb70e50 chore(release) 2023-04-12 17:48:53 +02:00
George
6bbb7aac4a RED-6590, fix user made changes resetting everytime dossier updates. 2023-04-12 18:45:53 +03:00
Atlassian Bamboo
68b8d3bd17 chore(release) 2023-04-11 20:44:26 +02:00
George
9fc485c7bf RED-6578, re-add old propagation stop method. Directive is not available in until ver. 4.0.0. 2023-04-11 21:41:40 +03:00
Atlassian Bamboo
943080db46 chore(release) 2023-04-11 20:27:21 +02:00
George
d0db9a116f RED-6578, fix build breaking change. 2023-04-11 21:24:43 +03:00
George
9d8124486e RED-6578, fix the incomprehensible cosmic horror thingy. 2023-04-11 21:16:48 +03:00
Atlassian Bamboo
1d8171e0f6 chore(release) 2023-04-10 11:42:41 +02:00
Nicoleta Panaghiu
4ffa30eb36 RED-6428: Created help mode link for Edit file attribute. 2023-04-10 12:39:47 +03:00
Nicoleta Panaghiu
1cfa6acae5 RED-6428: Created help mode link for Edit file attribute. 2023-04-10 12:38:39 +03:00
Atlassian Bamboo
1a55fb7c91 chore(release) 2023-04-06 12:48:22 +02:00
Valentin Mihai
d25c357c18 RED-6453 - update common ui 2023-04-06 13:45:28 +03:00
Valentin Mihai
f82361633f RED-6453 - Value for attribute in file list cannot be set 2023-04-06 13:44:02 +03:00
Atlassian Bamboo
783f6fc0e4 chore(release) 2023-04-04 18:42:58 +02:00
Nicoleta Panaghiu
634a1aa8de RED-6537: Fixed download size conversion. 2023-04-04 19:39:05 +03:00
Atlassian Bamboo
1fb671e8c2 chore(release) 2023-04-04 16:19:18 +02:00
Valentin Mihai
c266a39385 RED-6240 - changed 'redactionColor' with 'appliedRedactionColor' 2023-04-04 17:16:27 +03:00
Atlassian Bamboo
c05634c7ae chore(release) 2023-04-04 14:03:41 +02:00
Marius Schummer
1686581bbb Pull request #411: RED-6471 Corrected spelling of label "Auto-expand filters on my actions"
Merge in RED/ui from mschummer/enjson-1680608793566 to release/3.988.0

* commit '971d5896fc74ac52c4e3bbbccf9368acdee73ab1':
  RED-6471 Corrected spelling of label "Auto-expand filters on my actions"
2023-04-04 14:00:25 +02:00
Marius Schummer
971d5896fc RED-6471 Corrected spelling of label "Auto-expand filters on my actions" 2023-04-04 13:47:54 +02:00
Atlassian Bamboo
4533e9b54f chore(release) 2023-04-03 15:34:50 +02:00
Valentin Mihai
f00f772d39 RED-6240 - removed hardcoded color 2023-04-03 16:32:16 +03:00
Atlassian Bamboo
f6eba7be11 chore(release) 2023-04-03 15:23:50 +02:00
Valentin Mihai
c4300949d6 RED-6240 - Use Applied Redaction Color in Preview mode 2023-04-03 16:21:14 +03:00
Atlassian Bamboo
cc606ffbe3 chore(release) 2023-04-03 15:01:45 +02:00
Nicoleta Panaghiu
939e726e87 RED-6506: Clean up code. 2023-04-03 15:54:26 +03:00
Atlassian Bamboo
607c1646fc chore(release) 2023-04-03 14:26:38 +02:00
Nicoleta Panaghiu
a7d5a6ec13 RED-6506: Added confirmation dialog for false positive action. 2023-04-03 15:23:11 +03:00
Atlassian Bamboo
7e024d52ec chore(release) 2023-04-03 12:59:34 +02:00
Dan Percic
6add09e574 RED-6504: fix annotation shown as redacted 2023-04-03 13:56:54 +03:00
Atlassian Bamboo
a1d1342a04 chore(release) 2023-03-30 11:02:28 +02:00
Nicoleta Panaghiu
92d744c959 RED-6176: Fixed save button disabled. 2023-03-30 11:58:34 +03:00
Atlassian Bamboo
4e84fe3f03 chore(release) 2023-03-28 15:36:15 +02:00
Valentin Mihai
43a2191723 RED-6420 - No refreshes anymore for new user notifications 2023-03-28 16:33:14 +03:00
Atlassian Bamboo
e3f76fd7e0 chore(release) 2023-03-28 14:52:09 +02:00
Dan Percic
a71d53ec92 RED-6454: fix file attribute dialog title 2023-03-28 15:49:38 +03:00
Atlassian Bamboo
b87f5dca9d chore(release) 2023-03-28 12:28:29 +02:00
Adina Țeudan
2e26e7fbbb RED-6372: soft delete = hard delete = restore dossier permissions 2023-03-28 13:25:34 +03:00
Atlassian Bamboo
8e6d94f771 chore(release) 2023-03-27 12:59:08 +02:00
George
ad85677258 RED-6361, fix OCR documents image hint annotations showing the hide button when already hidden. 2023-03-27 13:08:16 +03:00
Atlassian Bamboo
00a4123dcf chore(release) 2023-03-21 17:00:23 +01:00
Nicoleta Panaghiu
e32071cf6c RED-6176: Sorted users 2. 2023-03-21 17:42:58 +02:00
Atlassian Bamboo
19ce27b046 chore(release) 2023-03-17 22:00:13 +01:00
Dan Percic
d0409df1fa RED-6403: reorder license information 2023-03-17 21:45:56 +02:00
Atlassian Bamboo
e6850fbc09 chore(release) 2023-03-17 12:56:09 +01:00
Valentin Mihai
ab07929942 Revert "RED-6381 - Display titles of file attributes in workflow view"
This reverts commit 692fce549e2e58d63aff4a0b4a56f756092d4ef6.
2023-03-17 13:53:17 +02:00
Atlassian Bamboo
9f71c120a0 chore(release) 2023-03-17 12:29:20 +01:00
Valentin Mihai
692fce549e RED-6381 - Display titles of file attributes in workflow view 2023-03-17 13:26:47 +02:00
Atlassian Bamboo
c0b192ddfd chore(release) 2023-03-17 11:19:20 +01:00
Valentin Mihai
6f64d4775f RED-6375 - Close "download options"-window if user click anywhere outside of the window 2023-03-17 12:16:38 +02:00
Atlassian Bamboo
4774062182 chore(release) 2023-03-17 10:36:31 +01:00
Dan Percic
ce30a177de RED-6332: fix no notifications icon 2023-03-17 11:33:32 +02:00
Atlassian Bamboo
fa550f1693 chore(release) 2023-03-16 20:51:13 +01:00
Adina Țeudan
344e7ac096 RED-6316: Small refactor 2023-03-16 21:48:39 +02:00
Atlassian Bamboo
9510f23280 chore(release) 2023-03-16 19:33:14 +01:00
Adina Țeudan
200b4f2d46 RED-6372: Only the dossier owner should be able to restore the dossier 2023-03-16 20:30:41 +02:00
Adina Țeudan
cdc8b7ed9e RED-6330: Fixed owner in list of member options (again) 2023-03-16 20:29:30 +02:00
Adina Țeudan
10e011a311 RED-6316: Fixed showing rotation buttons 2023-03-16 20:17:38 +02:00
Adina Țeudan
d7d8f6784a RED-6330: Fixed owner in list of member options 2023-03-16 20:16:40 +02:00
Atlassian Bamboo
32eaa42270 chore(release) 2023-03-16 18:29:19 +01:00
Dan Percic
dc28e59b40 RED-6394: fix dossier states save button not enabled 2023-03-16 19:26:25 +02:00
Dan Percic
5eab66c79e RED-6285: fix save button not hidden 2023-03-16 17:16:35 +02:00
Atlassian Bamboo
7ba8b51f8a chore(release) 2023-03-15 16:51:56 +01:00
Dan Percic
86a0f5f57a RED-6285: fix watermark double save 2023-03-15 17:45:56 +02:00
Dan Percic
fb3ef83cd2 RED-6332: cherry pick commit from master branch 2023-03-15 17:42:02 +02:00
Dan Percic
0723607a45 RED-6394: fix dossier state name 2023-03-15 17:39:27 +02:00
Atlassian Bamboo
95575d86fc chore(release) 2023-03-14 18:26:27 +01:00
89 changed files with 887 additions and 869 deletions

2
.gitignore vendored
View File

@ -41,8 +41,6 @@ testem.log
.DS_Store .DS_Store
Thumbs.db Thumbs.db
version.properties
paligo-styles/style.css* paligo-styles/style.css*
migrations.json migrations.json

14
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,14 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_FORCE_HTTPS: 'true'
PROJECT: red-ui
DOCKERFILELOCATION: 'docker/$PROJECT/Dockerfile'
workflow:
rules:
- when: always
include:
- project: 'gitlab/gitlab'
ref: 'main'
file: 'ci-templates/docker_build_nexus.yml'

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "libs/common-ui"] [submodule "libs/common-ui"]
path = libs/common-ui path = libs/common-ui
url = ssh://git@git.iqser.com:2222/sl/common-ui.git url = ../../fforesight/shared-ui-libraries/common-ui.git

View File

@ -29,7 +29,10 @@
<iqser-help-button *deny="roles.getRss" [iqserHelpMode]="'help_mode'" id="help-mode-button"></iqser-help-button> <iqser-help-button *deny="roles.getRss" [iqserHelpMode]="'help_mode'" id="help-mode-button"></iqser-help-button>
<redaction-notifications [iqserHelpMode]="'open_notifications'"></redaction-notifications> <redaction-notifications
*ngIf="currentUser.isUser || currentUser.isManager"
[iqserHelpMode]="'open_notifications'"
></redaction-notifications>
</div> </div>
<iqser-user-button [iqserHelpMode]="'open_usermenu'" [matMenuTriggerFor]="userMenu" id="userMenu"></iqser-user-button> <iqser-user-button [iqserHelpMode]="'open_usermenu'" [matMenuTriggerFor]="userMenu" id="userMenu"></iqser-user-button>

View File

@ -1,4 +1,9 @@
<iqser-circle-button [matMenuTriggerFor]="menu" [showDot]="hasUnreadNotifications$ | async" icon="red:notification"></iqser-circle-button> <iqser-circle-button
[matMenuTriggerFor]="menu"
[showDot]="hasUnreadNotifications$ | async"
buttonId="notification-button"
icon="red:notification"
></iqser-circle-button>
<mat-menu #menu="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before"> <mat-menu #menu="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
<ng-template matMenuContent> <ng-template matMenuContent>
@ -13,15 +18,21 @@
<div *ngFor="let group of groups; let first = first"> <div *ngFor="let group of groups; let first = first">
<div class="all-caps-label flex-align-items-center"> <div class="all-caps-label flex-align-items-center">
<div>{{ group.date }}</div> <div>{{ group.date }}</div>
<div (click)="markRead($event)" *ngIf="(hasUnreadNotifications$ | async) && first" class="view-all"> <div
(click)="markRead($event)"
*ngIf="(hasUnreadNotifications$ | async) && first"
class="view-all"
id="notifications-mark-all-as-read-btn"
>
{{ 'notifications.mark-all-as-read' | translate }} {{ 'notifications.mark-all-as-read' | translate }}
</div> </div>
</div> </div>
<div <div
(click)="markRead($event, [notification], true)" (click)="markRead($event, [notification], true)"
*ngFor="let notification of group.notifications" *ngFor="let notification of group.notifications; trackBy: trackBy"
[class.unread]="!notification.readDate" [class.unread]="!notification.readDate"
[id]="'notifications-mark-as-read-' + notification.id + '-btn'"
class="notification" class="notification"
mat-menu-item mat-menu-item
> >
@ -33,6 +44,7 @@
</div> </div>
<div <div
(click)="markRead($event, [notification], !notification.readDate)" (click)="markRead($event, [notification], !notification.readDate)"
[id]="'notifications-mark-' + notification.id"
class="dot" class="dot"
matTooltip="{{ 'notifications.mark-as' | translate : { type: notification.readDate ? 'unread' : 'read' } }}" matTooltip="{{ 'notifications.mark-as' | translate : { type: notification.readDate ? 'unread' : 'read' } }}"
matTooltipPosition="before" matTooltipPosition="before"

View File

@ -4,7 +4,7 @@ import { NotificationsService } from '@services/notifications.service';
import { Notification } from '@red/domain'; import { Notification } from '@red/domain';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { isToday, shareLast } from '@iqser/common-ui'; import { isToday, shareLast, trackByFactory } from '@iqser/common-ui';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -29,6 +29,7 @@ function chronologically(first: string, second: string) {
export class NotificationsComponent { export class NotificationsComponent {
readonly hasUnreadNotifications$: Observable<boolean>; readonly hasUnreadNotifications$: Observable<boolean>;
readonly groupedNotifications$: Observable<NotificationsGroup[]>; readonly groupedNotifications$: Observable<NotificationsGroup[]>;
readonly trackBy = trackByFactory();
constructor( constructor(
private readonly _notificationsService: NotificationsService, private readonly _notificationsService: NotificationsService,

View File

@ -4,6 +4,7 @@ import {
annotationDefaultColorConfig, annotationDefaultColorConfig,
annotationEntityColorConfig, annotationEntityColorConfig,
AnnotationIconType, AnnotationIconType,
ChangeTypes,
DefaultColors, DefaultColors,
Dictionary, Dictionary,
Earmark, Earmark,
@ -23,6 +24,7 @@ import {
} from '@red/domain'; } from '@red/domain';
import { RedactionLogEntry } from '@models/file/redaction-log.entry'; import { RedactionLogEntry } from '@models/file/redaction-log.entry';
import { IListable, List } from '@iqser/common-ui'; import { IListable, List } from '@iqser/common-ui';
import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service';
export class AnnotationWrapper implements IListable, Record<string, unknown> { export class AnnotationWrapper implements IListable, Record<string, unknown> {
[x: string]: unknown; [x: string]: unknown;
@ -110,6 +112,10 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return this.type?.toLowerCase() === 'image' || this.image; return this.type?.toLowerCase() === 'image' || this.image;
} }
get isNotSignatureImage() {
return this.isImage && this.recategorizationType === 'signature';
}
get isOCR() { get isOCR() {
return this.type?.toLowerCase() === 'ocr'; return this.type?.toLowerCase() === 'ocr';
} }
@ -134,6 +140,10 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return this.type?.toLowerCase() === 'false_positive' && !!FalsePositiveSuperTypes[this.superType]; return this.type?.toLowerCase() === 'false_positive' && !!FalsePositiveSuperTypes[this.superType];
} }
get isSuggestionAddToFalsePositive() {
return this.typeLabel === annotationTypesTranslations[SuggestionAddFalsePositive];
}
get isDeclinedSuggestion() { get isDeclinedSuggestion() {
return this.superType === SuperTypes.DeclinedSuggestion; return this.superType === SuperTypes.DeclinedSuggestion;
} }
@ -186,10 +196,18 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return this.superType === SuperTypes.SuggestionRecategorizeImage; return this.superType === SuperTypes.SuggestionRecategorizeImage;
} }
get isSuggestionForceHint() {
return this.superType === SuperTypes.SuggestionForceHint;
}
get isSuggestionAdd() { get isSuggestionAdd() {
return !!SuggestionAddSuperTypes[this.superType]; return !!SuggestionAddSuperTypes[this.superType];
} }
get isSuggestionAddDictionary() {
return this.superType === SuperTypes.SuggestionAddDictionary;
}
get isSuggestionRemove() { get isSuggestionRemove() {
return !!SuggestionRemoveSuperTypes[this.superType]; return !!SuggestionRemoveSuperTypes[this.superType];
} }
@ -328,9 +346,14 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
const entity = dictionaries.find(d => d.type === annotationWrapper.typeValue); const entity = dictionaries.find(d => d.type === annotationWrapper.typeValue);
annotationWrapper.entity = entity.virtual ? null : entity; annotationWrapper.entity = entity.virtual ? null : entity;
const colorKey = annotationWrapper.isSuperTypeBasedColor let colorKey = annotationWrapper.isSuperTypeBasedColor
? annotationDefaultColorConfig[annotationWrapper.superType] ? annotationDefaultColorConfig[annotationWrapper.superType]
: annotationEntityColorConfig[annotationWrapper.superType]; : annotationEntityColorConfig[annotationWrapper.superType];
if (annotationWrapper.isSuperTypeBasedColor && annotationWrapper.isSuggestionResize && !annotationWrapper.isModifyDictionary) {
colorKey = annotationDefaultColorConfig[SuperTypes.SuggestionAdd];
}
annotationWrapper.color = annotationWrapper.isSuperTypeBasedColor ? defaultColors[colorKey] : (entity[colorKey] as string); annotationWrapper.color = annotationWrapper.isSuperTypeBasedColor ? defaultColors[colorKey] : (entity[colorKey] as string);
return annotationWrapper; return annotationWrapper;
@ -356,6 +379,15 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntry) { private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntry) {
if (redactionLogEntryWrapper.manualChanges?.length) { if (redactionLogEntryWrapper.manualChanges?.length) {
const lastRelevantManualChange = this._getLastRelevantManualChange(redactionLogEntryWrapper.manualChanges); const lastRelevantManualChange = this._getLastRelevantManualChange(redactionLogEntryWrapper.manualChanges);
const viableChanges = redactionLogEntryWrapper.changes.filter(c => c.analysisNumber > 1);
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChangeOccurredAfterLastManualChange =
lastChange && timestampOf(lastChange.dateTime) > timestampOf(lastRelevantManualChange.processedDate);
if (lastChangeOccurredAfterLastManualChange && lastChange.type === ChangeTypes.ADDED && redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
return;
}
annotationWrapper.pending = !lastRelevantManualChange.processed; annotationWrapper.pending = !lastRelevantManualChange.processed;
@ -494,7 +526,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
if (lastManualChange.processed) { if (lastManualChange.processed) {
switch (lastManualChange.annotationStatus) { switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED: case LogEntryStatuses.APPROVED:
return SuperTypes.Redaction; return redactionLogEntry.recommendation ? SuperTypes.Recommendation : SuperTypes.Skipped;
case LogEntryStatuses.DECLINED: case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped; return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED: case LogEntryStatuses.REQUESTED:

View File

@ -0,0 +1,5 @@
@use 'common-mixins';
.dialog-header {
@include common-mixins.line-clamp(1);
}

View File

@ -15,6 +15,7 @@ export interface AddEditDossierAttributeDialogData {
@Component({ @Component({
templateUrl: './add-edit-dossier-attribute-dialog.component.html', templateUrl: './add-edit-dossier-attribute-dialog.component.html',
styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss'],
}) })
export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent implements OnDestroy { export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent implements OnDestroy {
readonly dossierAttribute = this.data.dossierAttribute; readonly dossierAttribute = this.data.dossierAttribute;

View File

@ -1,3 +1,5 @@
@use 'common-mixins';
.options-wrapper { .options-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@ -7,3 +9,7 @@
margin-right: 32px; margin-right: 32px;
} }
} }
.dialog-header {
@include common-mixins.line-clamp(1);
}

View File

@ -1,4 +1,4 @@
<div [translateParams]="{ userName: user | name }" [translate]="'reset-password-dialog.header'" class="dialog-header heading-l"></div> <div [innerHTML]="'reset-password-dialog.header' | translate : { userName: user | name }" class="dialog-header heading-l"></div>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">

View File

@ -4,7 +4,7 @@
</div> </div>
<div class="dialog-content"> <div class="dialog-content">
<div class="heading">{{ 'confirm-delete-dossier-state.warning' | translate : translateArgs }}</div> <div [innerHTML]="'confirm-delete-dossier-state.warning' | translate : translateArgs" class="heading"></div>
<form *ngIf="data.dossierCount !== 0 && data.otherStates.length > 0" [formGroup]="form" class="mt-16"> <form *ngIf="data.dossierCount !== 0 && data.otherStates.length > 0" [formGroup]="form" class="mt-16">
<div class="iqser-input-group"> <div class="iqser-input-group">

View File

@ -1,5 +1,5 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l" translate="smtp-auth-config.title"></div> <div class="dialog-header heading-l" [translate]="'smtp-auth-config.title'"></div>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">

View File

@ -1,5 +1,5 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l" translate="upload-dictionary-dialog.title"></div> <div class="dialog-header heading-l" [translate]="'upload-dictionary-dialog.title'"></div>
<div class="dialog-content"> <div class="dialog-content">
<p translate="upload-dictionary-dialog.question"></p> <p translate="upload-dictionary-dialog.question"></p>

View File

@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component'; import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionaryService } from '@services/entity-services/dictionary.service'; import { DictionaryService } from '@services/entity-services/dictionary.service';
import { getCurrentUser, getParam, IqserPermissionsService, List, LoadingService } from '@iqser/common-ui'; import { getCurrentUser, getParam, IqserPermissionsService, List, LoadingService } from '@iqser/common-ui';
import { BehaviorSubject, firstValueFrom, lastValueFrom } from 'rxjs'; import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { DICTIONARY_TO_ENTRY_TYPE_MAP, DICTIONARY_TYPE_KEY_MAP, DictionaryType, DOSSIER_TEMPLATE_ID, ENTITY_TYPE, User } from '@red/domain'; import { DICTIONARY_TO_ENTRY_TYPE_MAP, DICTIONARY_TYPE_KEY_MAP, DictionaryType, DOSSIER_TEMPLATE_ID, ENTITY_TYPE, User } from '@red/domain';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
@ -47,8 +47,7 @@ export class DictionaryScreenComponent implements OnInit {
this._loadingService.start(); this._loadingService.start();
try { try {
await lastValueFrom( await this._dictionaryService.saveEntries(
this._dictionaryService.saveEntries(
entries, entries,
this.initialEntries$.value, this.initialEntries$.value,
this.#dossierTemplateId, this.#dossierTemplateId,
@ -56,7 +55,7 @@ export class DictionaryScreenComponent implements OnInit {
null, null,
true, true,
DICTIONARY_TO_ENTRY_TYPE_MAP[this.type], DICTIONARY_TO_ENTRY_TYPE_MAP[this.type],
), false,
); );
await this._loadEntries(); await this._loadEntries();
} catch (e) { } catch (e) {

View File

@ -1,10 +1,8 @@
<section class="dialog"> <section class="dialog">
<div <div
[translateParams]="{ [innerHTML]="
type: data.justification ? 'edit' : 'create', 'add-edit-justification.title' | translate : { type: data.justification ? 'edit' : 'create', name: data.justification?.name }
name: data.justification?.name "
}"
[translate]="'add-edit-justification.title'"
class="dialog-header heading-l" class="dialog-header heading-l"
></div> ></div>

View File

@ -93,7 +93,7 @@ export class LicenseChartComponent {
} }
} }
if (cumulativePages !== this._licenseService.currentInfo.numberOfAnalyzedPages) { if (cumulativePages !== this._licenseService.currentLicenseInfo.numberOfAnalyzedPages) {
this._licenseService.wipeStoredReportsAndReloadSelectedLicenseData(); this._licenseService.wipeStoredReportsAndReloadSelectedLicenseData();
} }

View File

@ -61,26 +61,8 @@
<div>{{ licenseService.totalLicensedNumberOfPages }}</div> <div>{{ licenseService.totalLicensedNumberOfPages }}</div>
</div> </div>
<div class="row">
<div translate="license-info-screen.analyzed-pages"></div>
<div>{{ licenseService.currentInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div translate="license-info-screen.ocr-analyzed-pages"></div>
<div>{{ licenseService.currentInfo.numberOfOcrPages }}</div>
</div>
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div> <div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div class="row">
<div
*ngIf="licenseService.annualInfo.startDate | date : 'longDate' as startDate"
[innerHTML]="'license-info-screen.total-analyzed' | translate : { date: startDate }"
></div>
<div>{{ licenseService.annualInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row"> <div class="row">
<div translate="license-info-screen.current-analyzed"></div> <div translate="license-info-screen.current-analyzed"></div>
<div> <div>
@ -89,10 +71,26 @@
</div> </div>
</div> </div>
<div class="row">
<div translate="license-info-screen.ocr-analyzed-pages"></div>
<div>{{ licenseService.currentLicenseInfo.numberOfOcrPages }}</div>
</div>
<div *ngIf="!!licenseService.unlicensedPages" class="row"> <div *ngIf="!!licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div> <div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div> <div>{{ licenseService.unlicensedPages }}</div>
</div> </div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-ocr-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfOcrPages }}</div>
</div>
</div> </div>
<redaction-license-chart></redaction-license-chart> <redaction-license-chart></redaction-license-chart>

View File

@ -62,7 +62,7 @@ export class LicenseScreenComponent {
const lineBreak = '%0D%0A'; const lineBreak = '%0D%0A';
const body = [ const body = [
this._translateService.instant('license-info-screen.email.body.analyzed', { this._translateService.instant('license-info-screen.email.body.analyzed', {
pages: this.licenseService.currentInfo.numberOfAnalyzedPages, pages: this.licenseService.currentLicenseInfo.numberOfAnalyzedPages,
}), }),
this._translateService.instant('license-info-screen.email.body.licensed', { this._translateService.instant('license-info-screen.email.body.licensed', {
pages: this.licenseService.totalLicensedNumberOfPages, pages: this.licenseService.totalLicensedNumberOfPages,

View File

@ -1,7 +1,7 @@
<div class="content-container"> <div class="content-container">
<div class="viewer" id="viewer"></div> <div #viewer class="viewer" id="viewer"></div>
<div *ngIf="changed && currentUser.isAdmin" class="changes-box"> <div *ngIf="!!instance && changed && currentUser.isAdmin" class="changes-box">
<iqser-icon-button <iqser-icon-button
(action)="save()" (action)="save()"
[disabled]="!valid" [disabled]="!valid"

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core'; import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer'; import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
@ -51,7 +51,7 @@ interface WatermarkForm {
templateUrl: './watermark-screen.component.html', templateUrl: './watermark-screen.component.html',
styleUrls: ['./watermark-screen.component.scss'], styleUrls: ['./watermark-screen.component.scss'],
}) })
export class WatermarkScreenComponent { export class WatermarkScreenComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly currentUser = getCurrentUser<User>(); readonly currentUser = getCurrentUser<User>();
readonly form = this._getForm(); readonly form = this._getForm();
@ -62,9 +62,10 @@ export class WatermarkScreenComponent {
{ value: 'courier', display: 'Courier' }, { value: 'courier', display: 'Courier' },
]; ];
readonly orientationOptions = ['DIAGONAL', 'HORIZONTAL', 'VERTICAL']; readonly orientationOptions = ['DIAGONAL', 'HORIZONTAL', 'VERTICAL'];
@ViewChild('viewer', { static: true }) viewer: ElementRef<HTMLDivElement>;
instance: WebViewerInstance;
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #watermarkId = Number(getParam(WATERMARK_ID)); readonly #watermarkId = Number(getParam(WATERMARK_ID));
private _instance: WebViewerInstance;
private _watermark: Partial<IWatermark> = {}; private _watermark: Partial<IWatermark> = {};
constructor( constructor(
@ -102,6 +103,10 @@ export class WatermarkScreenComponent {
return this.form.valid; return this.form.valid;
} }
async ngOnInit() {
await this._loadViewer();
}
@Debounce() @Debounce()
async configChanged() { async configChanged() {
await this._drawWatermark(); await this._drawWatermark();
@ -144,15 +149,14 @@ export class WatermarkScreenComponent {
private async _initForm(watermark: Partial<IWatermark>) { private async _initForm(watermark: Partial<IWatermark>) {
this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId }; this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark }); this.form.patchValue({ ...watermark });
await this._loadViewer();
} }
private async _loadViewer() { private async _loadViewer() {
if (this._instance) { if (this.instance) {
return; return;
} }
this._instance = await WebViewer( this.instance = await WebViewer(
{ {
licenseKey: this._licenseService.activeLicenseKey, licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'), path: this._convertPath('/assets/wv-resources'),
@ -161,18 +165,18 @@ export class WatermarkScreenComponent {
isReadOnly: true, isReadOnly: true,
backendType: 'ems', backendType: 'ems',
}, },
document.getElementById('viewer'), this.viewer.nativeElement,
); );
this._instance.UI.setTheme(this._userPreferenceService.getTheme()); this.instance.UI.setTheme(this._userPreferenceService.getTheme());
this._instance.Core.documentViewer.addEventListener('documentLoaded', async () => { this.instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this._loadingService.stop(); this._loadingService.stop();
await this._drawWatermark(); await this._drawWatermark();
}); });
if (environment.production) { if (environment.production) {
this._instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron')); this.instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron'));
} }
this._disableElements(); this._disableElements();
@ -181,16 +185,16 @@ export class WatermarkScreenComponent {
responseType: 'blob', responseType: 'blob',
}); });
const blobData = await firstValueFrom(request); const blobData = await firstValueFrom(request);
this._instance.UI.loadDocument(blobData, { filename: 'blank.pdf' }); this.instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
} }
private _disableElements() { private _disableElements() {
this._instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']); this.instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']);
} }
private async _drawWatermark() { private async _drawWatermark() {
const pdfNet = this._instance.Core.PDFNet; const pdfNet = this.instance.Core.PDFNet;
const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc(); const document = await this.instance.Core.documentViewer.getDocument().getPDFDoc();
await stampPDFPage( await stampPDFPage(
document, document,
@ -204,8 +208,8 @@ export class WatermarkScreenComponent {
[1], [1],
this._licenseService.activeLicenseKey, this._licenseService.activeLicenseKey,
); );
this._instance.Core.documentViewer.refreshAll(); this.instance.Core.documentViewer.refreshAll();
this._instance.Core.documentViewer.updateView([0], 0); this.instance.Core.documentViewer.updateView([0], 0);
} }
private _getForm() { private _getForm() {

View File

@ -4,41 +4,40 @@
<span class="clamp-3"> {{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }}</span> <span class="clamp-3"> {{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }}</span>
</ng-template> </ng-template>
<ng-container *ngIf="(isEditingFileAttribute$ | async) === false || isInEditMode"> <ng-container *ngIf="((fileAttributesService.isEditingFileAttribute$ | async) === false || isInEditMode) && !file.isInitialProcessing">
<div class="edit-button" *ngIf="!isInEditMode; else input"> <div *ngIf="!isInEditMode; else input" class="action-buttons edit-button">
<iqser-circle-button <iqser-circle-button
id="edit-attribute-button"
*ngIf="permissionsService.canEditFileAttributes(file, dossier)"
(action)="editFileAttribute($event)" (action)="editFileAttribute($event)"
[icon]="'iqser:edit'" *ngIf="permissionsService.canEditFileAttributes(file, dossier)"
[disabled]="!fileAttribute.editable" [disabled]="!fileAttribute.editable"
[icon]="'iqser:edit'"
[tooltip]="'file-attribute.actions.edit' | translate" [tooltip]="'file-attribute.actions.edit' | translate"
[iqserHelpMode]="'edit-file-attributes'"
id="edit-attribute-button"
></iqser-circle-button> ></iqser-circle-button>
</div> </div>
<ng-template #input> <ng-template #input>
<div class="edit-input" (click)="$event?.stopPropagation()"> <div class="edit-input" stopPropagation>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<iqser-dynamic-input <iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keydown.escape)="close()"
[formControlName]="fileAttribute.id" [formControlName]="fileAttribute.id"
[id]="fileAttribute.id" [id]="fileAttribute.id"
[type]="fileAttribute.type" [type]="fileAttribute.type"
(keydown.escape)="close()" ></iqser-dynamic-input>
(closedDatepicker)="closedDatepicker = $event"
>
</iqser-dynamic-input>
<iqser-circle-button <iqser-circle-button
class="save"
[icon]="'iqser:check'"
[disabled]="disabled"
(action)="save($event)" (action)="save($event)"
[disabled]="disabled"
[icon]="'iqser:check'"
class="save"
></iqser-circle-button> ></iqser-circle-button>
<iqser-circle-button [icon]="'iqser:close'" (action)="close($event)"></iqser-circle-button>
<iqser-circle-button (action)="close($event)" [icon]="'iqser:close'"></iqser-circle-button>
</form> </form>
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
</div> </div>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="selectedFile$ | async"></ng-container>

View File

@ -6,26 +6,19 @@
.edit-button { .edit-button {
position: absolute; position: absolute;
display: none;
background: radial-gradient(var(--iqser-side-nav) 10%, rgba(244, 245, 247, 0) 60%);
height: 100%; height: 100%;
width: 150%; right: 10%;
transform: translate(-25%); width: 90%;
background: linear-gradient(to left, var(--iqser-side-nav) 20%, rgba(244, 245, 247, 0) 60%);
iqser-circle-button { iqser-circle-button {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 80%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
} }
&:hover {
.edit-button {
display: block;
}
}
.edit-input { .edit-input {
cursor: default; cursor: default;
display: flex; display: flex;

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core'; import { Component, HostListener, Input, OnDestroy, OnInit } from '@angular/core';
import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain'; import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { BaseFormComponent, ListingService, Toaster } from '@iqser/common-ui'; import { BaseFormComponent, ListingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, UntypedFormGroup } from '@angular/forms';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs'; import { firstValueFrom, Subscription } from 'rxjs';
import { FilesService } from '@services/files/files.service'; import { FilesService } from '@services/files/files.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -15,48 +15,34 @@ import { filter, map, tap } from 'rxjs/operators';
selector: 'redaction-file-attribute [fileAttribute] [file] [dossier]', selector: 'redaction-file-attribute [fileAttribute] [file] [dossier]',
templateUrl: './file-attribute.component.html', templateUrl: './file-attribute.component.html',
styleUrls: ['./file-attribute.component.scss'], styleUrls: ['./file-attribute.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FileAttributeComponent extends BaseFormComponent implements OnInit { export class FileAttributeComponent extends BaseFormComponent implements OnDestroy {
@Input() fileAttribute!: IFileAttributeConfig; @Input() fileAttribute!: IFileAttributeConfig;
@Input() file!: File; @Input() file!: File;
@Input() dossier!: Dossier; @Input() dossier!: Dossier;
isInEditMode = false; isInEditMode = false;
closedDatepicker = true; closedDatepicker = true;
readonly isEditingFileAttribute$: BehaviorSubject<boolean>; readonly #subscriptions = new Subscription();
readonly selectedFile$: Observable<boolean>;
constructor( constructor(
private readonly _fileAttributesService: FileAttributesService, router: Router,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder, private readonly _formBuilder: FormBuilder,
private readonly _filesService: FilesService, private readonly _filesService: FilesService,
private readonly _router: Router,
private readonly _listingService: ListingService<File>,
readonly permissionsService: PermissionsService, readonly permissionsService: PermissionsService,
private readonly _listingService: ListingService<File>,
readonly fileAttributesService: FileAttributesService,
) { ) {
super(); super();
this.isEditingFileAttribute$ = this._fileAttributesService.isEditingFileAttribute$; const sub = router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => this.close());
this.#subscriptions.add(sub);
_router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => { const sub2 = this._listingService.selectedLength$.pipe(
this.close();
});
this.selectedFile$ = this._listingService.selectedLength$.pipe(
map(selectedLength => !!selectedLength), map(selectedLength => !!selectedLength),
tap(() => this.close()), tap(() => this.close()),
); );
} this.#subscriptions.add(sub2.subscribe());
ngOnInit(): void {
if (this.#noFileAttributes) {
this.#initFileAttributes();
}
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
} }
get isDate(): boolean { get isDate(): boolean {
@ -67,41 +53,27 @@ export class FileAttributeComponent extends BaseFormComponent implements OnInit
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id]; return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
} }
get #noFileAttributes(): boolean { ngOnDestroy() {
return JSON.stringify(this.file.fileAttributes.attributeIdToValue) === '{}'; this.#subscriptions.unsubscribe();
}
#initFileAttributes() {
const configs = this._fileAttributesService.getFileAttributeConfig(this.file.dossierTemplateId).fileAttributeConfigs;
configs.forEach(config => (this.file.fileAttributes.attributeIdToValue[config.id] = null));
} }
async editFileAttribute($event: MouseEvent): Promise<void> { async editFileAttribute($event: MouseEvent): Promise<void> {
$event?.stopPropagation(); $event.stopPropagation();
this.#toggleEdit(); this.#toggleEdit();
} }
#getForm(): UntypedFormGroup { async save($event?: MouseEvent) {
const config = {};
const fileAttributes = this.file.fileAttributes.attributeIdToValue;
Object.keys(fileAttributes).forEach(key => {
const attrValue = fileAttributes[key];
config[key] = [dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue];
});
return this._formBuilder.group(config);
}
async save($event?: MouseEvent): Promise<void> {
$event?.stopPropagation(); $event?.stopPropagation();
const rawFormValue = this.form.getRawValue(); const rawFormValue = this.form.getRawValue();
const fileAttrValue = rawFormValue[this.fileAttribute.id]; const fileAttrValue = rawFormValue[this.fileAttribute.id];
const attributeIdToValue = { const attributeIdToValue = {
...rawFormValue, ...this.#getForm().getRawValue(),
[this.fileAttribute.id]: this.#formatAttributeValue(fileAttrValue), [this.fileAttribute.id]: this.#formatAttributeValue(fileAttrValue),
}; };
try { try {
await firstValueFrom( await firstValueFrom(
this._fileAttributesService.setFileAttributes({ attributeIdToValue }, this.file.dossierId, this.file.fileId), this.fileAttributesService.setFileAttributes({ attributeIdToValue }, this.file.dossierId, this.file.fileId),
); );
await firstValueFrom(this._filesService.reload(this.file.dossierId, this.file)); await firstValueFrom(this._filesService.reload(this.file.dossierId, this.file));
this.initialFormValue = rawFormValue; this.initialFormValue = rawFormValue;
@ -115,19 +87,52 @@ export class FileAttributeComponent extends BaseFormComponent implements OnInit
close($event?: MouseEvent): void { close($event?: MouseEvent): void {
$event?.stopPropagation(); $event?.stopPropagation();
if (this.isInEditMode) { if (this.isInEditMode) {
this.form = this.#getForm(); this.form = this.#getForm();
this.#toggleEdit(); this.#toggleEdit();
} }
} }
@HostListener('document:click')
clickOutside() {
if (this.isInEditMode && this.closedDatepicker) {
this.close();
}
}
#initFileAttributes() {
const configs = this.fileAttributesService.getFileAttributeConfig(this.file.dossierTemplateId).fileAttributeConfigs;
configs.forEach(config => {
if (!this.file.fileAttributes.attributeIdToValue[config.id]) {
this.file.fileAttributes.attributeIdToValue[config.id] = null;
}
});
}
#getForm(): UntypedFormGroup {
const config = {};
const fileAttributes = this.file.fileAttributes.attributeIdToValue;
Object.keys(fileAttributes).forEach(key => {
const attrValue = fileAttributes[key];
config[key] = [dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue];
});
return this._formBuilder.group(config);
}
#formatAttributeValue(attrValue) { #formatAttributeValue(attrValue) {
return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue; return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue;
} }
#toggleEdit(): void { #toggleEdit(): void {
if (!this.isInEditMode) {
this.#initFileAttributes();
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
}
this.isInEditMode = !this.isInEditMode; this.isInEditMode = !this.isInEditMode;
this.isEditingFileAttribute$.next(this.isInEditMode); this.fileAttributesService.isEditingFileAttribute$.next(this.isInEditMode);
if (this.isInEditMode) { if (this.isInEditMode) {
this.#focusOnEditInput(); this.#focusOnEditInput();
@ -140,11 +145,4 @@ export class FileAttributeComponent extends BaseFormComponent implements OnInit
input.focus(); input.focus();
}, 100); }, 100);
} }
@HostListener('document:click')
clickOutside() {
if (this.isInEditMode && this.closedDatepicker) {
this.close();
}
}
} }

View File

@ -153,7 +153,7 @@ export class ConfigService {
checker: (dossier: Dossier, filter: INestedFilter) => this._dossierStatusChecker(dossier, filter), checker: (dossier: Dossier, filter: INestedFilter) => this._dossierStatusChecker(dossier, filter),
}); });
const peopleFilters = [...allDistinctPeople].map( const peopleFilters = this._sortByName([...allDistinctPeople]).map(
userId => userId =>
new NestedFilter({ new NestedFilter({
id: userId, id: userId,
@ -265,4 +265,8 @@ export class ConfigService {
} }
} }
}; };
private _sortByName(ids: string[]) {
return ids.sort((a, b) => this._userService.getName(a).localeCompare(this._userService.getName(b)));
}
} }

View File

@ -23,7 +23,9 @@
{{ annotation.recategorizationType ?? annotation.entity.label }} {{ annotation.recategorizationType ?? annotation.entity.label }}
</div> </div>
<div *deny="roles.getRss; if: !!annotation.shortContent && !annotation.isHint && !annotation.isSkipped"> <div
*deny="roles.getRss; if: !!annotation.shortContent && !annotation.isHint && !annotation.isSkipped && !annotation.isIgnoredHint"
>
<strong><span translate="content"></span>: </strong>{{ annotation.shortContent }} <strong><span translate="content"></span>: </strong>{{ annotation.shortContent }}
</div> </div>

View File

@ -28,6 +28,7 @@
[overlappingElements]="['USER_MENU']" [overlappingElements]="['USER_MENU']"
[primaryFiltersSlug]="'primaryFilters'" [primaryFiltersSlug]="'primaryFilters'"
[secondaryFiltersSlug]="'secondaryFilters'" [secondaryFiltersSlug]="'secondaryFilters'"
[fileId]="file.id"
></iqser-popup-filter> ></iqser-popup-filter>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationProcessingService } from '../../services/annotation-processing.service'; import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { MatDialogState } from '@angular/material/dialog'; import { MatDialogState } from '@angular/material/dialog';
@ -31,6 +31,7 @@ import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-ma
import { AnnotationsListingService } from '../../services/annotations-listing.service'; import { AnnotationsListingService } from '../../services/annotations-listing.service';
import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service'; import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service';
import { SuggestionsService } from '../../services/suggestions.service'; import { SuggestionsService } from '../../services/suggestions.service';
import { getLocalStorageDataByFileId } from '@utils/local-storage';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape']; const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -40,7 +41,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
templateUrl: './file-workload.component.html', templateUrl: './file-workload.component.html',
styleUrls: ['./file-workload.component.scss'], styleUrls: ['./file-workload.component.scss'],
}) })
export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy { export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
@ -362,7 +363,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
if (this._viewModeService.isRedacted) { if (this._viewModeService.isRedacted) {
annotations = annotations.filter(a => !bool(a.isChangeLogRemoved)); annotations = annotations.filter(a => !bool(a.isChangeLogRemoved));
annotations = this._suggestionsService.convertWorkloadRemoveSuggestionsToRedactions(annotations); annotations = this._suggestionsService.filterWorkloadSuggestionsInPreview(annotations);
} }
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary); this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary);
@ -456,4 +457,18 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
FileWorkloadComponent._scrollToFirstElement(elements); FileWorkloadComponent._scrollToFirstElement(elements);
} }
} }
ngOnInit(): void {
setTimeout(() => {
const showExcludePages = getLocalStorageDataByFileId(this.file.fileId, 'show-exclude-pages') ?? false;
if (showExcludePages) {
this.excludedPagesService.show();
}
const showDocumentInfo = getLocalStorageDataByFileId(this.file.fileId, 'show-document-info') ?? false;
if (showDocumentInfo) {
this.documentInfoService.show();
}
});
}
} }

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Dossier, File, StatusBarConfigs, User } from '@red/domain'; import { Dossier, File, StatusBarConfigs, User } from '@red/domain';
import { List, LoadingService, Toaster } from '@iqser/common-ui'; import { getCurrentUser, List, LoadingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { workflowFileStatusTranslations } from '@translations/file-status-translations'; import { workflowFileStatusTranslations } from '@translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -32,6 +32,7 @@ export class UserManagementComponent {
private readonly _dossier$: Observable<Dossier>; private readonly _dossier$: Observable<Dossier>;
private readonly _canAssignUser$: Observable<boolean>; private readonly _canAssignUser$: Observable<boolean>;
private readonly _canUnassignUser$: Observable<boolean>; private readonly _canUnassignUser$: Observable<boolean>;
readonly currentUserId = getCurrentUser().id;
constructor( constructor(
readonly fileAssignService: FileAssignService, readonly fileAssignService: FileAssignService,
@ -89,10 +90,10 @@ export class UserManagementComponent {
this.usersOptions$ = combineLatest([this._canUnassignUser$, this.stateService.file$, this._dossier$]).pipe( this.usersOptions$ = combineLatest([this._canUnassignUser$, this.stateService.file$, this._dossier$]).pipe(
map(([canUnassignUser, file, dossier]) => { map(([canUnassignUser, file, dossier]) => {
const unassignUser = canUnassignUser ? [undefined] : []; const unassignUser = canUnassignUser && file.assignee ? [undefined] : [];
return file.isUnderApproval return file.isUnderApproval
? this._customSort([...dossier.approverIds, ...unassignUser], file) ? this.#customSort([...dossier.approverIds, ...unassignUser])
: this._customSort([...dossier.memberIds, ...unassignUser], file); : this.#customSort([...dossier.memberIds, ...unassignUser]);
}), }),
); );
} }
@ -117,12 +118,12 @@ export class UserManagementComponent {
this.editingReviewer = false; this.editingReviewer = false;
} }
private _customSort(ids: string[], file: File) { #customSort(ids: string[]) {
let sorted = [...ids].sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b))); let sorted = [...ids].sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b)));
if (file.assignee) { sorted = moveElementInArray(sorted, this.currentUserId, 0);
sorted = moveElementInArray(sorted, file.assignee, 0); if (sorted.includes(undefined)) {
sorted = moveElementInArray(sorted, undefined, 1);
} }
sorted = moveElementInArray(sorted, undefined, file.assignee ? 1 : 0);
return sorted; return sorted;
} }
} }

View File

@ -1,10 +1,10 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="change-legal-basis-dialog.header"></div> <div class="dialog-header heading-l" [translate]="'change-legal-basis-dialog.header'"></div>
<div class="dialog-content"> <div class="dialog-content">
<div class="iqser-input-group required w-400"> <div class="iqser-input-group required w-400">
<label translate="change-legal-basis-dialog.content.reason"></label> <label [translate]="'change-legal-basis-dialog.content.reason'"></label>
<mat-form-field> <mat-form-field>
<mat-select <mat-select
[placeholder]="'change-legal-basis-dialog.content.reason-placeholder' | translate" [placeholder]="'change-legal-basis-dialog.content.reason-placeholder' | translate"
@ -19,22 +19,22 @@
</div> </div>
<div class="iqser-input-group w-400"> <div class="iqser-input-group w-400">
<label translate="change-legal-basis-dialog.content.legalBasis"></label> <label [translate]="'change-legal-basis-dialog.content.legalBasis'"></label>
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" /> <input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div> </div>
<div class="iqser-input-group w-400"> <div class="iqser-input-group w-400">
<label translate="change-legal-basis-dialog.content.section"></label> <label [translate]="'change-legal-basis-dialog.content.section'"></label>
<input formControlName="section" name="section" type="text" /> <input formControlName="section" name="section" type="text" />
</div> </div>
<div *ngIf="this.allRectangles" class="iqser-input-group w-400"> <div *ngIf="this.allRectangles" class="iqser-input-group w-400">
<label translate="change-legal-basis-dialog.content.classification"></label> <label [translate]="'change-legal-basis-dialog.content.classification'"></label>
<input formControlName="classification" name="classification" type="text" /> <input formControlName="classification" name="classification" type="text" />
</div> </div>
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="change-legal-basis-dialog.content.comment"></label> <label [translate]="'change-legal-basis-dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea> <textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<section *ngIf="!!form" class="dialog"> <section *ngIf="!!form" class="dialog">
<div class="dialog-header heading-l" translate="document-info.title"></div> <div class="dialog-header heading-l" [translate]="'document-info.title'"></div>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">

View File

@ -0,0 +1,31 @@
<section class="dialog">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" [translate]="'false-positive-dialog.header'"></div>
<div class="dialog-content">
<ul>
<li
*ngFor="let value of data"
[innerHTML]="'false-positive-dialog.content.body-text' | translate : { value: value.text, context: value.context }"
></li>
</ul>
<div class="iqser-input-group w-300">
<label [translate]="'false-positive-dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
[disabled]="!form.valid"
[label]="'false-positive-dialog.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="false-positive-dialog.actions.cancel"></div>
</div>
</form>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -0,0 +1,30 @@
import { Component, Inject, OnInit } from '@angular/core';
import { BaseDialogComponent } from '@iqser/common-ui';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
export interface FalsePositiveDialogInput {
text: string;
context: string;
}
@Component({
templateUrl: './false-positive-dialog.component.html',
})
export class FalsePositiveDialogComponent extends BaseDialogComponent implements OnInit {
constructor(
protected readonly _dialogRef: MatDialogRef<FalsePositiveDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: FalsePositiveDialogInput[],
) {
super(_dialogRef);
}
ngOnInit() {
const controlsConfig = { comment: [null] };
this.form = this._formBuilder.group(controlsConfig);
this.initialFormValue = this.form.getRawValue();
}
save(): void {
this._dialogRef.close(this.form.getRawValue());
}
}

View File

@ -1,11 +1,11 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div *ngIf="!isHintDialog" class="dialog-header heading-l" translate="manual-annotation.dialog.header.force-redaction"></div> <div *ngIf="!isHintDialog" class="dialog-header heading-l" [translate]="'manual-annotation.dialog.header.force-redaction'"></div>
<div *ngIf="isHintDialog" class="dialog-header heading-l" translate="manual-annotation.dialog.header.force-hint"></div> <div *ngIf="isHintDialog" class="dialog-header heading-l" [translate]="'manual-annotation.dialog.header.force-hint'"></div>
<div class="dialog-content"> <div class="dialog-content">
<div *ngIf="!isHintDialog" class="iqser-input-group required w-400"> <div *ngIf="!isHintDialog" class="iqser-input-group required w-400">
<label translate="manual-annotation.dialog.content.reason"></label> <label [translate]="'manual-annotation.dialog.content.reason'"></label>
<mat-form-field> <mat-form-field>
<mat-select <mat-select
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate" [placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
@ -20,12 +20,12 @@
</div> </div>
<div *ngIf="!isHintDialog" class="iqser-input-group w-400"> <div *ngIf="!isHintDialog" class="iqser-input-group w-400">
<label translate="manual-annotation.dialog.content.legalBasis"></label> <label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" /> <input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div> </div>
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label> <label [translate]="'manual-annotation.dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4"></textarea> <textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4"></textarea>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l" translate="import-redactions-dialog.title"></div> <div class="dialog-header heading-l" [translate]="'import-redactions-dialog.title'"></div>
<div class="dialog-content"> <div class="dialog-content">
<div class="mb-24" translate="import-redactions-dialog.details"></div> <div class="mb-24" [translate]="'import-redactions-dialog.details'"></div>
<iqser-upload-file (fileChanged)="fileChanged($event)"></iqser-upload-file> <iqser-upload-file (fileChanged)="fileChanged($event)"></iqser-upload-file>
<div class="only-for-pages"> <div class="only-for-pages">
@ -29,7 +29,7 @@
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
></iqser-icon-button> ></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="import-redactions-dialog.actions.cancel"></div> <div class="all-caps-label cancel" mat-dialog-close [translate]="'import-redactions-dialog.actions.cancel'"></div>
</div> </div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button> <iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>

View File

@ -4,7 +4,7 @@
<div class="dialog-content"> <div class="dialog-content">
<div *ngIf="!isRectangle" class="iqser-input-group w-450"> <div *ngIf="!isRectangle" class="iqser-input-group w-450">
<label translate="manual-annotation.dialog.content.text"></label> <label [translate]="'manual-annotation.dialog.content.text'"></label>
<div *ngIf="!isEditingSelectedText" class="flex-align-items-center"> <div *ngIf="!isEditingSelectedText" class="flex-align-items-center">
{{ form.get('selectedText').value }} {{ form.get('selectedText').value }}
<iqser-circle-button <iqser-circle-button
@ -28,15 +28,15 @@
</div> </div>
<div *ngIf="isRectangle" class="iqser-input-group"> <div *ngIf="isRectangle" class="iqser-input-group">
<label translate="manual-annotation.dialog.content.rectangle"></label> <label [translate]="'manual-annotation.dialog.content.rectangle'"></label>
</div> </div>
<div <div
*ngIf="!isFalsePositiveRequest && (isDictionaryRequest || !manualRedactionTypeExists)" *ngIf="!isFalsePositiveRequest && (isDictionaryRequest || !manualRedactionTypeExists)"
class="iqser-input-group required w-450" class="iqser-input-group required w-450"
> >
<label *ngIf="isDictionaryRequest" translate="manual-annotation.dialog.content.dictionary"></label> <label *ngIf="isDictionaryRequest" [translate]="'manual-annotation.dialog.content.dictionary'"></label>
<label *ngIf="!isDictionaryRequest" translate="manual-annotation.dialog.content.type"></label> <label *ngIf="!isDictionaryRequest" [translate]="'manual-annotation.dialog.content.type'"></label>
<mat-form-field> <mat-form-field>
<mat-select formControlName="dictionary"> <mat-select formControlName="dictionary">
@ -54,7 +54,7 @@
</div> </div>
<div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group required w-450"> <div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group required w-450">
<label translate="manual-annotation.dialog.content.reason"></label> <label [translate]="'manual-annotation.dialog.content.reason'"></label>
<mat-form-field> <mat-form-field>
<mat-select <mat-select
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate" [placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
@ -74,22 +74,22 @@
</div> </div>
<div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group w-450"> <div *deny="roles.getRss; if: !isDictionaryRequest" class="iqser-input-group w-450">
<label translate="manual-annotation.dialog.content.legalBasis"></label> <label [translate]="'manual-annotation.dialog.content.legalBasis'"></label>
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" /> <input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div> </div>
<div *ngIf="isRectangle" class="iqser-input-group w-450"> <div *ngIf="isRectangle" class="iqser-input-group w-450">
<label translate="manual-annotation.dialog.content.section"></label> <label [translate]="'manual-annotation.dialog.content.section'"></label>
<input formControlName="section" name="section" type="text" /> <input formControlName="section" name="section" type="text" />
</div> </div>
<div *ngIf="isRectangle" class="iqser-input-group w-450"> <div *ngIf="isRectangle" class="iqser-input-group w-450">
<label translate="manual-annotation.dialog.content.classification"></label> <label [translate]="'manual-annotation.dialog.content.classification'"></label>
<input formControlName="classification" name="classification" type="text" /> <input formControlName="classification" name="classification" type="text" />
</div> </div>
<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="4" type="text"></textarea>
</div> </div>

View File

@ -1,10 +1,10 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="recategorize-image-dialog.header"></div> <div class="dialog-header heading-l" [translate]="'recategorize-image-dialog.header'"></div>
<div class="dialog-content"> <div class="dialog-content">
<div class="iqser-input-group required w-400"> <div class="iqser-input-group required w-400">
<label translate="recategorize-image-dialog.content.type"></label> <label [translate]="'recategorize-image-dialog.content.type'"></label>
<mat-form-field> <mat-form-field>
<mat-select <mat-select
[placeholder]="'recategorize-image-dialog.content.type-placeholder' | translate" [placeholder]="'recategorize-image-dialog.content.type-placeholder' | translate"
@ -19,7 +19,7 @@
</div> </div>
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="recategorize-image-dialog.content.comment"></label> <label [translate]="'recategorize-image-dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea> <textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
></iqser-icon-button> ></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="recategorize-image-dialog.actions.cancel"></div> <div class="all-caps-label cancel" mat-dialog-close [translate]="'recategorize-image-dialog.actions.cancel'"></div>
</div> </div>
</form> </form>

View File

@ -1,21 +1,23 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l"> <div
{{ class="dialog-header heading-l"
[innerHTML]="
(data.removeFromDictionary (data.removeFromDictionary
? 'remove-annotations-dialog.remove-from-dictionary.title' ? 'remove-annotations-dialog.remove-from-dictionary.title'
: 'remove-annotations-dialog.remove-only-here.title' : 'remove-annotations-dialog.remove-only-here.title'
) | translate : { hint: data.hint } ) | translate : { hint: data.hint }
}} "
</div> ></div>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div
{{ class="dialog-content"
[innerHTML]="
(data.removeFromDictionary (data.removeFromDictionary
? 'remove-annotations-dialog.remove-from-dictionary.question' ? 'remove-annotations-dialog.remove-from-dictionary.question'
: 'remove-annotations-dialog.remove-only-here.question' : 'remove-annotations-dialog.remove-only-here.question'
) | translate : { hint: data.hint } ) | translate : { hint: data.hint }
}} "
>
<div *ngIf="data.removeFromDictionary" class="content-wrapper"> <div *ngIf="data.removeFromDictionary" class="content-wrapper">
<table class="default-table"> <table class="default-table">
<thead> <thead>
@ -40,7 +42,7 @@
</ul> </ul>
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<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="4" type="text"></textarea>
</div> </div>
</div> </div>
@ -53,7 +55,7 @@
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
></iqser-icon-button> ></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="remove-annotations-dialog.cancel"></div> <div class="all-caps-label cancel" mat-dialog-close [translate]="'remove-annotations-dialog.cancel'"></div>
</div> </div>
</form> </form>

View File

@ -1,16 +1,16 @@
<section class="dialog"> <section class="dialog">
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="resize-annotation-dialog.header"></div> <div [translate]="'resize-annotation-dialog.header'" class="dialog-header heading-l"></div>
<div class="dialog-content"> <div class="dialog-content">
<div class="iqser-input-group w-300"> <div class="iqser-input-group w-300">
<label translate="resize-annotation-dialog.content.comment"></label> <label [translate]="'resize-annotation-dialog.content.comment'"></label>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea> <textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div> </div>
<div *ngIf="form.get('updateDictionary')" class="iqser-input-group"> <div *ngIf="form.get('updateDictionary')" class="iqser-input-group">
<mat-checkbox color="primary" formControlName="updateDictionary"> <mat-checkbox color="primary" formControlName="updateDictionary"
{{ 'resize-annotation-dialog.content.update-dictionary' | translate : { text: this.text } }} ><span [innerHTML]="'resize-annotation-dialog.content.update-dictionary' | translate : { text: this.text }"></span>
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -23,7 +23,7 @@
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
></iqser-icon-button> ></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="resize-annotation-dialog.actions.cancel"></div> <div [translate]="'resize-annotation-dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l" translate="rss-dialog.title"></div> <div class="dialog-header heading-l" [translate]="'rss-dialog.title'"></div>
<hr /> <hr />
<div class="dialog-content"> <div class="dialog-content">
@ -79,7 +79,7 @@
label="Export All" label="Export All"
></iqser-icon-button> ></iqser-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="rss-dialog.actions.close"></div> <div class="all-caps-label cancel" mat-dialog-close [translate]="'rss-dialog.actions.close'"></div>
</div> </div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button> <iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>

View File

@ -17,6 +17,7 @@ import {
CircleButtonTypes, CircleButtonTypes,
ConfirmationDialogInput, ConfirmationDialogInput,
ConfirmOptions, ConfirmOptions,
copyLocalStorageFiltersValues,
CustomError, CustomError,
Debounce, Debounce,
ErrorService, ErrorService,
@ -37,7 +38,7 @@ import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.se
import { AnnotationProcessingService } from './services/annotation-processing.service'; import { AnnotationProcessingService } from './services/annotation-processing.service';
import { Dictionary, File, ViewModes } from '@red/domain'; import { Dictionary, File, ViewModes } from '@red/domain';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { combineLatest, firstValueFrom, from, Observable, of, pairwise } from 'rxjs'; import { combineLatest, first, firstValueFrom, from, Observable, of, pairwise } from 'rxjs';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service'; import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils'; import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils';
import { FilesService } from '@services/files/files.service'; import { FilesService } from '@services/files/files.service';
@ -216,7 +217,6 @@ export class FilePreviewScreenComponent
switch (this._viewModeService.viewMode) { switch (this._viewModeService.viewMode) {
case ViewModes.STANDARD: { case ViewModes.STANDARD: {
this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor');
const wrappers = await this._fileDataService.annotations; const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
const standardEntries = annotations const standardEntries = annotations
@ -225,6 +225,7 @@ export class FilePreviewScreenComponent
const nonStandardEntries = annotations.filter( const nonStandardEntries = annotations.filter(
a => bool(a.getCustomData('changeLogRemoved')) || this._annotationManager.isHidden(a.Id), a => bool(a.getCustomData('changeLogRemoved')) || this._annotationManager.isHidden(a.Id),
); );
this._readableRedactionsService.setAnnotationsColor(standardEntries, 'annotationColor');
this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true); this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true);
this._annotationManager.show(standardEntries); this._annotationManager.show(standardEntries);
this._annotationManager.hide(nonStandardEntries); this._annotationManager.hide(nonStandardEntries);
@ -243,8 +244,8 @@ export class FilePreviewScreenComponent
const nonRedactionEntries = annotations.filter( const nonRedactionEntries = annotations.filter(
a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')), a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')),
); );
this._readableRedactionsService.setPreviewAnnotationsOpacity(redactions); this._readableRedactionsService.setAnnotationsColor(redactions, 'redactionColor');
this._readableRedactionsService.setPreviewAnnotationsColor(redactions); this._readableRedactionsService.setAnnotationsOpacity(redactions);
this._annotationManager.show(redactions); this._annotationManager.show(redactions);
this._annotationManager.hide(nonRedactionEntries); this._annotationManager.hide(nonRedactionEntries);
this._suggestionsService.hideSuggestionsInPreview(redactions); this._suggestionsService.hideSuggestionsInPreview(redactions);
@ -262,7 +263,7 @@ export class FilePreviewScreenComponent
ngOnDetach() { ngOnDetach() {
this._viewerHeaderService.resetCompareButtons(); this._viewerHeaderService.resetCompareButtons();
this._viewerHeaderService.enableLoadAllAnnotations(); this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
super.ngOnDetach(); super.ngOnDetach();
this._changeRef.markForCheck(); this._changeRef.markForCheck();
} }
@ -299,6 +300,8 @@ export class FilePreviewScreenComponent
} }
this.pdfProxyService.configureElements(); this.pdfProxyService.configureElements();
this.#restoreOldFilters();
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -691,12 +694,19 @@ export class FilePreviewScreenComponent
.subscribe(); .subscribe();
this.addActiveScreenSubscription = this._readableRedactionsService.active$.pipe(switchMap(() => this.updateViewMode())).subscribe(); this.addActiveScreenSubscription = this._readableRedactionsService.active$.pipe(switchMap(() => this.updateViewMode())).subscribe();
this.addActiveScreenSubscription = this._viewModeService.viewMode$
this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$, this._documentViewer.loaded$])
.pipe( .pipe(
tap(viewMode => map(([viewMode, file]) => {
viewMode === 'STANDARD' || viewMode === 'TEXT_HIGHLIGHTS' if (viewMode === 'REDACTED' && !this._readableRedactionsService.active) {
? this._viewerHeaderService.enableRotationButtons() this._readableRedactionsService.setCustomDrawHandler();
: this._viewerHeaderService.disableRotationButtons(), } else {
this._readableRedactionsService.restoreDraw();
}
return ['STANDARD', 'TEXT_HIGHLIGHTS'].includes(viewMode) && this.permissionsService.canRotatePage(file);
}),
tap(canRotate =>
canRotate ? this._viewerHeaderService.enableRotationButtons() : this._viewerHeaderService.disableRotationButtons(),
), ),
) )
.subscribe(); .subscribe();
@ -781,4 +791,18 @@ export class FilePreviewScreenComponent
private _isJapaneseString(text: string) { private _isJapaneseString(text: string) {
return text.match(/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]/); return text.match(/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]/);
} }
#restoreOldFilters() {
combineLatest([
this._filterService.getGroup$('primaryFilters').pipe(first(filterGroup => filterGroup !== undefined)),
this._filterService.getGroup$('secondaryFilters').pipe(first(secondaryFilters => secondaryFilters !== undefined)),
]).subscribe(([primaryFilters, secondaryFilters]) => {
const localStorageFiltersString = localStorage.getItem('workload-filters') ?? '{}';
const localStorageFilters = JSON.parse(localStorageFiltersString)[this.fileId];
if (localStorageFilters) {
copyLocalStorageFiltersValues(primaryFilters.filters, localStorageFilters.primaryFilters);
copyLocalStorageFiltersValues(secondaryFilters.filters, localStorageFilters.secondaryFilters);
}
});
}
} }

View File

@ -55,13 +55,13 @@ import { RssDialogComponent } from './dialogs/rss-dialog/rss-dialog.component';
import { ReadonlyBannerComponent } from './components/readonly-banner/readonly-banner.component'; import { ReadonlyBannerComponent } from './components/readonly-banner/readonly-banner.component';
import { SuggestionsService } from './services/suggestions.service'; import { SuggestionsService } from './services/suggestions.service';
import { PagesComponent } from './components/pages/pages.component'; import { PagesComponent } from './components/pages/pages.component';
import { FalsePositiveDialogComponent } from './dialogs/false-positive-dialog/false-positive-dialog.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: FilePreviewScreenComponent, component: FilePreviewScreenComponent,
pathMatch: 'full', pathMatch: 'full',
data: { reuse: true },
canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard], canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard],
}, },
]; ];
@ -78,6 +78,7 @@ const dialogs = [
DocumentInfoDialogComponent, DocumentInfoDialogComponent,
ImportRedactionsDialogComponent, ImportRedactionsDialogComponent,
RssDialogComponent, RssDialogComponent,
FalsePositiveDialogComponent,
]; ];
const components = [ const components = [

View File

@ -251,7 +251,8 @@ export class AnnotationActionsService {
markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[]) { markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[]) {
$event?.stopPropagation(); $event?.stopPropagation();
const data = annotations.map(annotation => ({ text: annotation.value, context: this._getFalsePositiveText(annotation) }));
this._dialogService.openDialog('falsePositive', null, data, (result: { comment: string }) => {
const requests: List<IAddRedactionRequest> = annotations.map(annotation => ({ const requests: List<IAddRedactionRequest> = annotations.map(annotation => ({
sourceId: annotation.id, sourceId: annotation.id,
value: this._getFalsePositiveText(annotation), value: this._getFalsePositiveText(annotation),
@ -262,10 +263,12 @@ export class AnnotationActionsService {
dictionaryEntryType: annotation.isRecommendation dictionaryEntryType: annotation.isRecommendation
? DictionaryEntryTypes.FALSE_RECOMMENDATION ? DictionaryEntryTypes.FALSE_RECOMMENDATION
: DictionaryEntryTypes.FALSE_POSITIVE, : DictionaryEntryTypes.FALSE_POSITIVE,
comment: result.comment ? { text: result.comment } : null,
})); }));
const { dossierId, fileId } = this._state; const { dossierId, fileId } = this._state;
this.#processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId)); this.#processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId));
});
} }
#generateRectangle(annotationWrapper: AnnotationWrapper) { #generateRectangle(annotationWrapper: AnnotationWrapper) {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, merge, Observable } from 'rxjs'; import { BehaviorSubject, firstValueFrom, merge, Observable } from 'rxjs';
import { shareLast } from '@iqser/common-ui'; import { shareLast } from '@iqser/common-ui';
import { map, startWith, tap, withLatestFrom } from 'rxjs/operators'; import { map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@ -49,6 +49,10 @@ export class DocumentInfoService {
); );
} }
shown() {
return firstValueFrom(this.shown$);
}
show() { show() {
this._show$.next(true); this._show$.next(true);
} }

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { shareDistinctLast } from '@iqser/common-ui'; import { shareDistinctLast } from '@iqser/common-ui';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -14,6 +14,10 @@ export class ExcludedPagesService {
this.hidden$ = this.shown$.pipe(map(value => !value)); this.hidden$ = this.shown$.pipe(map(value => !value));
} }
shown() {
return firstValueFrom(this.shown$);
}
show() { show() {
this._show$.next(true); this._show$.next(true);
} }

View File

@ -34,11 +34,11 @@ import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes; const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
function timestampOf(value: string) { export function timestampOf(value: string) {
return dayjs(value).valueOf(); return dayjs(value).valueOf();
} }
function chronologicallyBy<T>(property: (x: T) => string) { export function chronologicallyBy<T>(property: (x: T) => string) {
return (a: T, b: T) => timestampOf(property(a)) - timestampOf(property(b)); return (a: T, b: T) => timestampOf(property(a)) - timestampOf(property(b));
} }
@ -203,14 +203,20 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog)); const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog));
redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => { redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => {
const lastChange = entry.manualChanges.at(-1); const lastChange = entry.manualChanges.at(-1);
if (lastChange?.annotationStatus === LogEntryStatuses.REQUESTED && !entry.hint) {
if (
lastChange?.annotationStatus === LogEntryStatuses.REQUESTED &&
!entry.hint &&
!entry.reason.includes('requested to force hint')
) {
entry.manualChanges.pop(); entry.manualChanges.pop();
entry.reason = null; entry.reason = null;
filtered.push(entry); filtered.push(entry);
} }
return filtered; return filtered;
}, []); }, []);
this._suggestionsService.removedRedactions = await this.#buildAnnotations(redactionLogCopy, file); const annotations = await this.#buildAnnotations(redactionLogCopy, file);
this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped);
} }
} }

View File

@ -10,6 +10,7 @@ import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser
import { ResizeAnnotationDialogComponent } from '../dialogs/resize-annotation-dialog/resize-annotation-dialog.component'; import { ResizeAnnotationDialogComponent } from '../dialogs/resize-annotation-dialog/resize-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';
import { RssDialogComponent } from '../dialogs/rss-dialog/rss-dialog.component'; import { RssDialogComponent } from '../dialogs/rss-dialog/rss-dialog.component';
import { FalsePositiveDialogComponent } from '../dialogs/false-positive-dialog/false-positive-dialog.component';
type DialogType = type DialogType =
| 'confirm' | 'confirm'
@ -21,7 +22,8 @@ type DialogType =
| 'resizeAnnotation' | 'resizeAnnotation'
| 'forceAnnotation' | 'forceAnnotation'
| 'manualAnnotation' | 'manualAnnotation'
| 'highlightAction'; | 'highlightAction'
| 'falsePositive';
@Injectable() @Injectable()
export class FilePreviewDialogService extends DialogService<DialogType> { export class FilePreviewDialogService extends DialogService<DialogType> {
@ -60,6 +62,9 @@ export class FilePreviewDialogService extends DialogService<DialogType> {
component: RssDialogComponent, component: RssDialogComponent,
dialogConfig: { width: '90vw' }, dialogConfig: { width: '90vw' },
}, },
falsePositive: {
component: FalsePositiveDialogComponent,
},
}; };
constructor(protected readonly _dialog: MatDialog) { constructor(protected readonly _dialog: MatDialog) {

View File

@ -24,39 +24,47 @@ export class SuggestionsService {
} }
hideSuggestionsInPreview(annotations: Annotation[]): void { hideSuggestionsInPreview(annotations: Annotation[]): void {
if (!this._userPreferenceService.getDisplaySuggestionsInPreview()) { if (this._readableRedactionsService.active) {
const suggestions = annotations.filter(a => bool(a.getCustomData('suggestion'))); if (this._userPreferenceService.getDisplaySuggestionsInPreview()) {
this._annotationManager.hide(suggestions); const suggestionsRemove = annotations.filter(
this.#convertSuggestionsToRedactions(suggestions); a => bool(a.getCustomData('suggestionRemove')) || bool(a.getCustomData('suggestionForceHint')),
);
this._annotationManager.hide(suggestionsRemove);
return;
}
}
const suggestionsToHide = annotations.filter(
a =>
(bool(a.getCustomData('suggestionAdd')) && !bool(a.getCustomData('suggestionAddToFalsePositive'))) ||
bool(a.getCustomData('notSignatureImage')),
);
annotations.forEach(a => {
if (bool(a.getCustomData('suggestionRemove'))) {
const foundRedaction = this.#removedRedactions.find(r => r.id === a.Id);
if (!foundRedaction) {
suggestionsToHide.push(a);
}
}
});
this._annotationManager.hide(suggestionsToHide);
}
filterWorkloadSuggestionsInPreview(annotations: AnnotationWrapper[]): AnnotationWrapper[] {
if (this._readableRedactionsService.active) {
if (this._userPreferenceService.getDisplaySuggestionsInPreview()) {
return annotations.filter(a => !a.isSuggestionRemove && !a.isSuggestionForceHint);
} }
} }
convertWorkloadRemoveSuggestionsToRedactions(annotations: AnnotationWrapper[]): AnnotationWrapper[] { annotations = annotations.filter(a => (!a.isSuggestionAdd || a.isSuggestionAddToFalsePositive) && !a.isNotSignatureImage);
if (!this._userPreferenceService.getDisplaySuggestionsInPreview()) { for (let i = annotations.length - 1; i >= 0; i--) {
annotations = annotations.filter(a => !a.isSuggestion); const foundRemovedRedaction = this.#removedRedactions.find(r => r.id === annotations[i].id);
annotations = [...annotations, ...this.#removedRedactions]; if (foundRemovedRedaction) {
annotations[i] = foundRemovedRedaction;
} else if (annotations[i].isSuggestionRemove) {
annotations.splice(i, 1);
}
} }
return annotations; return annotations;
} }
#convertSuggestionsToRedactions(suggestions: Annotation[]): void {
suggestions = this.#filterSuggestions(suggestions);
suggestions.forEach(s => s.setCustomData('suggestion', 'false'));
this._readableRedactionsService.setPreviewAnnotationsOpacity(suggestions);
this._readableRedactionsService.setPreviewAnnotationsColor(suggestions);
this._annotationManager.show(suggestions);
}
#filterSuggestions(suggestions: Annotation[]): Annotation[] {
const filteredSuggestions = [];
this.#removedRedactions.forEach(r => {
const found = suggestions.find(s => s.Id === r.annotationId);
if (found) {
filteredSuggestions.push(found);
}
});
return filteredSuggestions;
}
} }

View File

@ -19,7 +19,6 @@ import Quad = Core.Math.Quad;
const DEFAULT_TEXT_ANNOTATION_OPACITY = 1; const DEFAULT_TEXT_ANNOTATION_OPACITY = 1;
const DEFAULT_REMOVED_ANNOTATION_OPACITY = 0.2; const DEFAULT_REMOVED_ANNOTATION_OPACITY = 0.2;
const FINAL_REDACTION_COLOR = '#000000';
@Injectable() @Injectable()
export class AnnotationDrawService { export class AnnotationDrawService {
@ -146,28 +145,43 @@ export class AnnotationDrawService {
annotation.Id = annotationWrapper.id; annotation.Id = annotationWrapper.id;
annotation.ReadOnly = true; annotation.ReadOnly = true;
const isOCR = annotationWrapper.isOCR && !annotationWrapper.isSuggestionResize;
if (isOCR && !this._annotationManager.isHidden(annotationWrapper.annotationId)) {
this._annotationManager.addToHidden(annotationWrapper.annotationId);
}
annotation.Hidden = annotation.Hidden =
annotationWrapper.isChangeLogRemoved || annotationWrapper.isChangeLogRemoved ||
(hideSkipped && annotationWrapper.isSkipped) || (hideSkipped && annotationWrapper.isSkipped) ||
(annotationWrapper.isOCR && !annotationWrapper.isSuggestionResize) ||
this._annotationManager.isHidden(annotationWrapper.annotationId); this._annotationManager.isHidden(annotationWrapper.annotationId);
annotation.setCustomData('redact-manager', 'true'); annotation.setCustomData('redact-manager', 'true');
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation)); annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
annotation.setCustomData('suggestion', String(annotationWrapper.isSuggestion)); annotation.setCustomData('suggestion', String(annotationWrapper.isSuggestion));
annotation.setCustomData('suggestionAdd', String(annotationWrapper.isSuggestionAdd));
annotation.setCustomData('suggestionAddToFalsePositive', String(annotationWrapper.isSuggestionAddToFalsePositive));
annotation.setCustomData('suggestionRemove', String(annotationWrapper.isSuggestionRemove)); annotation.setCustomData('suggestionRemove', String(annotationWrapper.isSuggestionRemove));
annotation.setCustomData('suggestionRecategorizeImage', String(annotationWrapper.isSuggestionRecategorizeImage));
annotation.setCustomData('suggestionForceHint', String(annotationWrapper.isSuggestionForceHint));
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped)); annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));
annotation.setCustomData('notSignatureImage', String(annotationWrapper.isNotSignatureImage));
annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry)); annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry));
annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved)); annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved));
annotation.setCustomData('opacity', String(annotation.Opacity)); annotation.setCustomData('opacity', String(annotation.Opacity));
const dictionaryRequestColor =
annotationWrapper.isSuggestionAddDictionary || (annotationWrapper.isSuggestionResize && annotationWrapper.isModifyDictionary);
const redactionColor = const redactionColor =
annotationWrapper.isSuggestion && this._userPreferenceService.getDisplaySuggestionsInPreview() annotationWrapper.isSuggestion && this._userPreferenceService.getDisplaySuggestionsInPreview()
? this._defaultColorsService.getColor(dossierTemplateId, 'requestAddColor') ? dictionaryRequestColor
? this._defaultColorsService.getColor(dossierTemplateId, 'dictionaryRequestColor')
: this._defaultColorsService.getColor(dossierTemplateId, 'requestAddColor')
: this._defaultColorsService.getColor(dossierTemplateId, 'previewColor'); : this._defaultColorsService.getColor(dossierTemplateId, 'previewColor');
annotation.setCustomData('redactionColor', String(redactionColor)); annotation.setCustomData('redactionColor', String(redactionColor));
annotation.setCustomData('finalRedactionColor', FINAL_REDACTION_COLOR);
annotation.setCustomData('annotationColor', String(annotationWrapper.color)); annotation.setCustomData('annotationColor', String(annotationWrapper.color));
const appliedRedactionColor = this._defaultColorsService.getColor(dossierTemplateId, 'appliedRedactionColor');
annotation.setCustomData('appliedRedactionColor', String(appliedRedactionColor));
return annotation; return annotation;
} }

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { BASE_HREF_FN, BaseHrefFn, bool } from '@iqser/common-ui'; import { BASE_HREF_FN } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HeaderElements } from '../../file-preview/utils/constants'; import { HeaderElements } from '../../file-preview/utils/constants';
@ -13,12 +13,12 @@ import Annotation = Core.Annotations.Annotation;
@Injectable() @Injectable()
export class ReadableRedactionsService { export class ReadableRedactionsService {
readonly active$: Observable<boolean>; readonly active$: Observable<boolean>;
private readonly _convertPath = inject(BASE_HREF_FN);
readonly #enableIcon = this._convertPath('/assets/icons/general/redaction-preview.svg'); readonly #enableIcon = this._convertPath('/assets/icons/general/redaction-preview.svg');
readonly #disableIcon = this._convertPath('/assets/icons/general/redaction-final.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(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _pdf: PdfViewer, private readonly _pdf: PdfViewer,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _annotationManager: REDAnnotationManager, private readonly _annotationManager: REDAnnotationManager,
@ -48,11 +48,39 @@ export class ReadableRedactionsService {
title: this.toggleReadableRedactionsBtnTitle, title: this.toggleReadableRedactionsBtnTitle,
img: this.toggleReadableRedactionsBtnIcon, img: this.toggleReadableRedactionsBtnIcon,
}); });
if (!this.active) {
this.setCustomDrawHandler();
} else {
this.restoreDraw();
}
}
setCustomDrawHandler(): void {
const annotationClass: any = this._pdf.instance.Core.Annotations.TextHighlightAnnotation;
this._pdf.instance.Core.Annotations.setCustomDrawHandler(
annotationClass,
(ctx: CanvasRenderingContext2D, pageMatrix, rotation, options) => {
const annotation = options.annotation;
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = annotation.getCustomData('appliedRedactionColor');
ctx.fillRect(annotation.getX(), annotation.getY(), annotation.getWidth(), annotation.getHeight());
},
{
generateAppearance: false,
},
);
}
restoreDraw(): void {
const annotationClass: any = this._pdf.instance.Core.Annotations.TextHighlightAnnotation;
this._pdf.instance.Core.Annotations.restoreDraw(annotationClass);
} }
setAnnotationsOpacity(annotations: Annotation[], restoreToOriginal = false) { setAnnotationsOpacity(annotations: Annotation[], restoreToOriginal = false) {
annotations.forEach(annotation => { annotations.forEach(annotation => {
annotation['Opacity'] = restoreToOriginal ? parseFloat(annotation.getCustomData('opacity')) : 0.5; const isSuggestion = annotation.getCustomData('suggestion');
annotation['Opacity'] = restoreToOriginal || isSuggestion ? parseFloat(annotation.getCustomData('opacity')) : 0.5;
}); });
} }
@ -63,20 +91,4 @@ export class ReadableRedactionsService {
annotation['FillColor'] = color; annotation['FillColor'] = color;
}); });
} }
setPreviewAnnotationsOpacity(annotations: Annotation[]) {
annotations.forEach(annotation => {
const isSuggestion = bool(annotation.getCustomData('suggestion'));
const restoreToOriginal = !this.active && !isSuggestion;
this.setAnnotationsOpacity([annotation], restoreToOriginal);
});
}
setPreviewAnnotationsColor(annotations: Annotation[]) {
annotations.forEach(annotation => {
const isSuggestion = bool(annotation.getCustomData('suggestion'));
const color = this.active || isSuggestion ? 'redactionColor' : 'finalRedactionColor';
this.setAnnotationsColor([annotation], color);
});
}
} }

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
import { IHeaderElement, RotationTypes } from '@red/domain'; import { IHeaderElement, RotationTypes } from '@red/domain';
import { HeaderElements, HeaderElementType } from '../../file-preview/utils/constants'; import { HeaderElements, HeaderElementType } from '../../file-preview/utils/constants';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BASE_HREF_FN, BaseHrefFn, IqserPermissionsService } from '@iqser/common-ui'; import { BASE_HREF_FN, BaseHrefFn } from '@iqser/common-ui';
import { TooltipsService } from './tooltips.service'; import { TooltipsService } from './tooltips.service';
import { PageRotationService } from './page-rotation.service'; import { PageRotationService } from './page-rotation.service';
import { PdfViewer } from './pdf-viewer.service'; import { PdfViewer } from './pdf-viewer.service';
@ -13,7 +13,6 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { fromEvent, Observable, Subject } from 'rxjs'; import { fromEvent, Observable, Subject } from 'rxjs';
import { ViewerEvent, VisibilityChangedEvent } from '../utils/types'; import { ViewerEvent, VisibilityChangedEvent } from '../utils/types';
import { ReadableRedactionsService } from './readable-redactions.service'; import { ReadableRedactionsService } from './readable-redactions.service';
import { ROLES } from '@users/roles';
import { filter, map, tap } from 'rxjs/operators'; import { filter, map, tap } from 'rxjs/operators';
const divider: IHeaderElement = { const divider: IHeaderElement = {
@ -23,6 +22,7 @@ const divider: IHeaderElement = {
@Injectable() @Injectable()
export class ViewerHeaderService { export class ViewerHeaderService {
readonly events$: Observable<ViewerEvent>; readonly events$: Observable<ViewerEvent>;
toggleLoadAnnotations$: Observable<boolean>;
#buttons: Map<HeaderElementType, IHeaderElement>; #buttons: Map<HeaderElementType, IHeaderElement>;
readonly #config = new Map<HeaderElementType, boolean>([ readonly #config = new Map<HeaderElementType, boolean>([
[HeaderElements.SHAPE_TOOL_GROUP_BUTTON, true], [HeaderElements.SHAPE_TOOL_GROUP_BUTTON, true],
@ -38,7 +38,6 @@ export class ViewerHeaderService {
]); ]);
#docBeforeCompare: Blob; #docBeforeCompare: Blob;
readonly #events$ = new Subject<ViewerEvent>(); readonly #events$ = new Subject<ViewerEvent>();
toggleLoadAnnotations$: Observable<boolean>;
constructor( constructor(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
@ -50,7 +49,6 @@ export class ViewerHeaderService {
private readonly _tooltipsService: TooltipsService, private readonly _tooltipsService: TooltipsService,
private readonly _readableRedactionsService: ReadableRedactionsService, private readonly _readableRedactionsService: ReadableRedactionsService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _iqserPermissionsService: IqserPermissionsService,
) { ) {
this.events$ = this.#events$.asObservable(); this.events$ = this.#events$.asObservable();
} }
@ -169,11 +167,6 @@ export class ViewerHeaderService {
}; };
} }
#discardRotation(): void {
this._rotationService.discardRotation();
this.disable(ROTATION_ACTION_BUTTONS);
}
private get _rotateRight(): IHeaderElement { private get _rotateRight(): IHeaderElement {
return { return {
type: 'actionButton', type: 'actionButton',
@ -305,10 +298,8 @@ export class ViewerHeaderService {
} }
enableRotationButtons(): void { enableRotationButtons(): void {
if (this._iqserPermissionsService.has(ROLES.files.rotatePage)) {
this.enable(ROTATION_BUTTONS); this.enable(ROTATION_BUTTONS);
} }
}
disableRotationButtons(): void { disableRotationButtons(): void {
this.disable(ROTATION_BUTTONS); this.disable(ROTATION_BUTTONS);
@ -320,6 +311,11 @@ export class ViewerHeaderService {
this.enable([HeaderElements.COMPARE_BUTTON]); this.enable([HeaderElements.COMPARE_BUTTON]);
} }
#discardRotation(): void {
this._rotationService.discardRotation();
this.disable(ROTATION_ACTION_BUTTONS);
}
#toggleRotationActionButtons() { #toggleRotationActionButtons() {
if (this._rotationService.hasRotations) { if (this._rotationService.hasRotations) {
this.enable(ROTATION_ACTION_BUTTONS); this.enable(ROTATION_ACTION_BUTTONS);

View File

@ -33,6 +33,7 @@ import { ViewerHeaderService } from '../../../pdf-viewer/services/viewer-header.
import { ROTATION_ACTION_BUTTONS } from '../../../pdf-viewer/utils/constants'; import { ROTATION_ACTION_BUTTONS } from '../../../pdf-viewer/utils/constants';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { setLocalStorageDataByFileId } from '@utils/local-storage';
@Component({ @Component({
selector: 'redaction-file-actions [file] [type] [dossier]', selector: 'redaction-file-actions [file] [type] [dossier]',
@ -166,7 +167,7 @@ export class FileActionsComponent implements OnChanges {
{ {
id: 'toggle-document-info-btn-' + fileId, id: 'toggle-document-info-btn-' + fileId,
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._documentInfoService.toggle(), action: () => this.#toggleDocumentInfo(),
tooltip: _('file-preview.document-info'), tooltip: _('file-preview.document-info'),
ariaExpanded: this._documentInfoService?.shown$, ariaExpanded: this._documentInfoService?.shown$,
icon: 'red:status-info', icon: 'red:status-info',
@ -175,7 +176,7 @@ export class FileActionsComponent implements OnChanges {
{ {
id: 'toggle-exclude-pages-btn-' + fileId, id: 'toggle-exclude-pages-btn-' + fileId,
type: ActionTypes.circleBtn, type: ActionTypes.circleBtn,
action: () => this._excludedPagesService.toggle(), action: () => this.#toggleExcludePages(),
tooltip: _('file-preview.exclude-pages'), tooltip: _('file-preview.exclude-pages'),
ariaExpanded: this._excludedPagesService?.shown$, ariaExpanded: this._excludedPagesService?.shown$,
showDot: !!this.file.excludedPages?.length, showDot: !!this.file.excludedPages?.length,
@ -481,4 +482,22 @@ export class FileActionsComponent implements OnChanges {
await this._filesService.setToNew(this.file); await this._filesService.setToNew(this.file);
this._loadingService.stop(); this._loadingService.stop();
} }
async #toggleExcludePages() {
this._excludedPagesService.toggle();
const shown = await this._excludedPagesService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', shown);
if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-document-info', false);
}
}
async #toggleDocumentInfo() {
this._documentInfoService.toggle();
const shown = await this._documentInfoService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-document-info', shown);
if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', false);
}
}
} }

View File

@ -1,5 +1,5 @@
<section class="dialog"> <section class="dialog">
<div [translateParams]="{ type: mode }" [translate]="'assign-owner.dialog.title'" class="dialog-header heading-l"></div> <div [innerHTML]="'assign-owner.dialog.title' | translate : { type: mode }" class="dialog-header heading-l"></div>
<form (submit)="save()" [formGroup]="form"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">

View File

@ -90,28 +90,11 @@ export class AssignReviewerApproverDialogComponent {
return this.permissionsService.canUnassignUser(this.data.files, this.dossier); return this.permissionsService.canUnassignUser(this.data.files, this.dossier);
} }
get #uniqueReviewers(): Set<string> {
const uniqueReviewers = new Set<string>();
for (const file of this.data.files) {
if (file.assignee) {
uniqueReviewers.add(file.assignee);
}
}
return uniqueReviewers;
}
get #user(): string { get #user(): string {
const userOptions = this.userOptions; if (this.data.files.every(file => !file.assignee)) {
return null;
if (this.data.withCurrentUserAsDefault && userOptions.includes(this.currentUser.id)) {
return this.currentUser.id;
} }
return this.data.files.length === 1 ? this.data.files[0].assignee : this.currentUser.id;
const uniqueReviewers = [...this.#uniqueReviewers.values()];
const user = uniqueReviewers.length === 1 ? uniqueReviewers[0] : this.currentUser.id;
return userOptions.indexOf(user) >= 0 ? userOptions[userOptions.indexOf(user)] : user;
} }
get #form() { get #form() {
@ -145,14 +128,10 @@ export class AssignReviewerApproverDialogComponent {
#customSort(ids: string[]) { #customSort(ids: string[]) {
let sorted = ids.sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b))); let sorted = ids.sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b)));
const fileHasAssignee = this.data.files.length === 1 && this.data.files[0].assignee; sorted = moveElementInArray(sorted, this.currentUser.id, 0);
if (fileHasAssignee) {
sorted = moveElementInArray(sorted, this.data.files[0].assignee, 0);
}
if (sorted.includes('undefined')) { if (sorted.includes('undefined')) {
sorted = moveElementInArray(sorted, 'undefined', fileHasAssignee ? 1 : 0); sorted = moveElementInArray(sorted, 'undefined', 1);
} }
return sorted; return sorted;

View File

@ -50,17 +50,14 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
async save(): EditDossierSaveResult { async save(): EditDossierSaveResult {
try { try {
await firstValueFrom( await this._dictionaryService.saveEntries(
this._dictionaryService.saveEntries(
this._dictionaryManager.editor.currentEntries, this._dictionaryManager.editor.currentEntries,
this._dictionaryManager.initialEntries, this._dictionaryManager.initialEntries,
this.dossier.dossierTemplateId, this.dossier.dossierTemplateId,
this.dossierDictionary.type, this.dossierDictionary.type,
this.dossier.id, this.dossier.id,
false, false,
),
); );
await this._updateDossierDictionary(); await this._updateDossierDictionary();
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@ -10,7 +10,8 @@
</mat-form-field> </mat-form-field>
</div> </div>
<ng-container *ngIf="selectedApproversList.length"> <ng-container *ngIf="selectedApprovers$ | async as selectedApprovers">
<ng-container *ngIf="selectedApprovers.length">
<div class="all-caps-label mt-16" id="approversLabel" translate="assign-dossier-owner.dialog.approvers"></div> <div class="all-caps-label mt-16" id="approversLabel" translate="assign-dossier-owner.dialog.approvers"></div>
<redaction-team-members <redaction-team-members
(remove)="toggleSelected($event)" (remove)="toggleSelected($event)"
@ -18,11 +19,12 @@
[canRemove]="hasOwner && !disabled" [canRemove]="hasOwner && !disabled"
[dossierId]="dossier.id" [dossierId]="dossier.id"
[largeSpacing]="true" [largeSpacing]="true"
[memberIds]="selectedApproversList" [memberIds]="selectedApprovers"
[perLine]="13" [perLine]="13"
[unremovableMembers]="[selectedOwnerId]" [unremovableMembers]="[this.form.value.owner]"
></redaction-team-members> ></redaction-team-members>
</ng-container> </ng-container>
</ng-container>
<div class="all-caps-label mt-16" id="reviewersLabel" translate="assign-dossier-owner.dialog.reviewers"></div> <div class="all-caps-label mt-16" id="reviewersLabel" translate="assign-dossier-owner.dialog.reviewers"></div>
@ -34,7 +36,7 @@
[largeSpacing]="true" [largeSpacing]="true"
[memberIds]="selectedReviewers$ | async" [memberIds]="selectedReviewers$ | async"
[perLine]="13" [perLine]="13"
[unremovableMembers]="[selectedOwnerId]" [unremovableMembers]="[this.form.value.owner]"
></redaction-team-members> ></redaction-team-members>
<ng-container *ngIf="!(selectedReviewers$ | async)?.length"> <ng-container *ngIf="!(selectedReviewers$ | async)?.length">

View File

@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import { UserService } from '@users/user.service'; import { UserService } from '@users/user.service';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Dossier, IDossierRequest, User } from '@red/domain'; import { Dossier, IDossierRequest } from '@red/domain';
import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface'; import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { DossiersService } from '@services/dossiers/dossiers.service'; import { DossiersService } from '@services/dossiers/dossiers.service';
import { compareLists } from '@utils/functions'; import { compareLists } from '@utils/functions';
import { FilesService } from '@services/files/files.service'; import { FilesService } from '@services/files/files.service';
import { getCurrentUser } from '@iqser/common-ui'; import { Debounce } from '@iqser/common-ui';
import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'redaction-edit-dossier-team', selector: 'redaction-edit-dossier-team',
@ -16,43 +17,26 @@ import { getCurrentUser } from '@iqser/common-ui';
styleUrls: ['./edit-dossier-team.component.scss'], styleUrls: ['./edit-dossier-team.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class EditDossierTeamComponent implements EditDossierSectionInterface, OnInit { export class EditDossierTeamComponent implements EditDossierSectionInterface, OnChanges {
form: UntypedFormGroup; form = this.#getForm();
searchQuery = ''; searchQuery = '';
@Input() dossier: Dossier; @Input() dossier: Dossier;
membersSelectOptions: string[] = []; membersSelectOptions: string[] = [];
readonly ownersSelectOptions = this.userService.all.filter(u => u.isManager).map(m => m.id); readonly #userService = inject(UserService);
readonly selectedReviewers$ = new BehaviorSubject<string[]>([]); readonly #dossiersService = inject(DossiersService);
readonly #currentUser = getCurrentUser<User>(); readonly #permissionsService = inject(PermissionsService);
readonly #filesService = inject(FilesService);
constructor( readonly ownersSelectOptions = this.#userService.all.filter(u => u.isManager).map(m => m.id);
readonly userService: UserService, readonly #formValue$ = this.form.valueChanges;
private readonly _formBuilder: UntypedFormBuilder, readonly selectedReviewers$ = this.#formValue$.pipe(map(v => v.members.filter(m => !v.approvers.includes(m))));
private readonly _dossiersService: DossiersService, readonly selectedApprovers$ = this.#formValue$.pipe(map(v => v.approvers));
private readonly _permissionsService: PermissionsService,
private readonly _filesService: FilesService,
) {}
get selectedOwnerId(): string {
return this.form.get('owner').value;
}
get selectedApproversList(): string[] {
return this.form.get('approvers').value;
}
get selectedMembersList(): string[] {
return this.form.get('members').value;
}
get valid(): boolean { get valid(): boolean {
return this.form.valid; return this.form.valid;
} }
get disabled() { get disabled() {
return !this._permissionsService.canEditTeamMembers() || !this.form.get('owner').value; return !this.#permissionsService.canEditTeamMembers() || !this.form.get('owner').value;
} }
get hasOwner() { get hasOwner() {
@ -60,15 +44,17 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
} }
get changed() { get changed() {
if (this.dossier.ownerId !== this.selectedOwnerId) { const { owner, members, approvers } = this.form.value;
if (this.dossier.ownerId !== owner) {
return true; return true;
} }
const initialMembers = [...this.dossier.memberIds].sort(); const initialMembers = [...this.dossier.memberIds].sort();
const currentMembers = this.selectedMembersList.sort(); const currentMembers = members.sort();
const initialApprovers = [...this.dossier.approverIds].sort(); const initialApprovers = [...this.dossier.approverIds].sort();
const currentApprovers = this.selectedApproversList.sort(); const currentApprovers = approvers.sort();
return compareLists(initialMembers, currentMembers) || compareLists(initialApprovers, currentApprovers); return compareLists(initialMembers, currentMembers) || compareLists(initialApprovers, currentApprovers);
} }
@ -78,105 +64,113 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
if (!this.isApprover(ownerId)) { if (!this.isApprover(ownerId)) {
this.toggleApprover(ownerId); this.toggleApprover(ownerId);
} }
this._updateLists(); this.#updateLists();
} }
const { owner, members, approvers } = this.form.value;
const dossier = { const dossier = {
...this.dossier, ...this.dossier,
memberIds: this.selectedMembersList, memberIds: members,
approverIds: this.selectedApproversList, approverIds: approvers,
ownerId: this.selectedOwnerId, ownerId: owner,
} as IDossierRequest; } as IDossierRequest;
const updatedDossier = await firstValueFrom(this._dossiersService.createOrUpdate(dossier)); const updatedDossier = await firstValueFrom(this.#dossiersService.createOrUpdate(dossier));
await firstValueFrom(this._filesService.loadAll(updatedDossier.dossierId)); await firstValueFrom(this.#filesService.loadAll(updatedDossier.dossierId));
return { success: !!updatedDossier }; return { success: !!updatedDossier };
} }
isMemberSelected(userId: string): boolean { isMemberSelected(userId: string): boolean {
return this.selectedMembersList.indexOf(userId) !== -1; return this.form.value.members.includes(userId);
} }
isApprover(userId: string): boolean { isApprover(userId: string): boolean {
return this.selectedApproversList.indexOf(userId) !== -1; return this.form.value.approvers.includes(userId);
} }
toggleApprover(userId: string, $event?: MouseEvent) { toggleApprover(userId: string, event?) {
$event?.stopPropagation(); event?.stopPropagation();
const currentApprovers = this.form.value.approvers;
const approversControl = this.form.controls.approvers;
if (this.isApprover(userId)) { if (this.isApprover(userId)) {
this.selectedApproversList.splice(this.selectedApproversList.indexOf(userId), 1); approversControl.patchValue(currentApprovers.filter(a => a !== userId));
} else { } else {
this.selectedApproversList.push(userId); approversControl.patchValue([...currentApprovers, userId]);
if (!this.isMemberSelected(userId)) { if (!this.isMemberSelected(userId)) {
this.toggleSelected(userId); this.toggleSelected(userId);
} }
} }
this.#updateLists();
this._updateLists();
} }
toggleSelected(userId: string) { toggleSelected(userId: string) {
const { members, approvers } = this.form.value;
const { members: membersControl, approvers: approversControl } = this.form.controls;
if (this.isMemberSelected(userId)) { if (this.isMemberSelected(userId)) {
this.selectedMembersList.splice(this.selectedMembersList.indexOf(userId), 1); membersControl.patchValue(members.filter(m => m !== userId));
if (this.isApprover(userId)) { if (this.isApprover(userId)) {
this.selectedApproversList.splice(this.selectedApproversList.indexOf(userId), 1); approversControl.patchValue(approvers.filter(a => a !== userId));
} }
} else { } else {
this.selectedMembersList.push(userId); membersControl.patchValue([...members, userId]);
} }
this._updateLists(); this.#updateLists();
} }
ngOnInit() { ngOnChanges(changes: SimpleChanges): void {
this._loadData(); if (changes.dossier.isFirstChange()) {
setTimeout(() => this.#resetForm());
}
} }
revert() { revert() {
this._loadData(); this.#resetForm();
} }
setMembersSelectOptions(value = this.searchQuery): void { setMembersSelectOptions(value = this.searchQuery): void {
const possibleMembers = this.userService.all.filter(user => user.isUser || user.isManager); const possibleMembers = this.#userService.all.filter(user => user.isUser || user.isManager);
this.membersSelectOptions = possibleMembers this.membersSelectOptions = possibleMembers
.filter(user => this.userService.getName(user.id).toLowerCase().includes(value.toLowerCase())) .filter(user => this.#userService.getName(user.id).toLowerCase().includes(value.toLowerCase()))
.filter(user => this.selectedOwnerId !== user.id) .filter(user => this.form.value.owner !== user.id)
.map(user => user.id); .map(user => user.id)
.sort((a, b) => this.#userService.getName(a).localeCompare(this.#userService.getName(b)));
} }
@Debounce(0)
onChangeOwner(ownerId: string) { onChangeOwner(ownerId: string) {
if (this.hasOwner) { if (this.hasOwner) {
if (!this.isApprover(ownerId)) { if (!this.isApprover(ownerId)) {
this.toggleApprover(ownerId); this.toggleApprover(ownerId);
} }
// If it is an approver, it is already a member, no need to check // If it is an approver, it is already a member, no need to check
this._updateLists(); this.#updateLists();
} }
} }
private _setSelectedReviewersList() { #updateLists() {
const selectedReviewers = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
this.selectedReviewers$.next(selectedReviewers);
}
private _loadData() {
this.form = this._formBuilder.group({
owner: [
{
value: this.dossier.ownerId,
disabled: !this._permissionsService.canEditTeamMembers(),
},
Validators.required,
],
approvers: [[...this.dossier.approverIds]],
members: [[...this.dossier.memberIds]],
});
this._updateLists();
}
private _updateLists() {
this._setSelectedReviewersList();
this.setMembersSelectOptions(); this.setMembersSelectOptions();
} }
#sortByName(ids: string[]) {
return ids.sort((a, b) => this.#userService.getName(a).localeCompare(this.#userService.getName(b)));
}
#getForm() {
return new FormGroup({
owner: new FormControl({ value: '', disabled: false }, Validators.required),
approvers: new FormControl([]),
members: new FormControl([]),
});
}
#resetForm() {
this.form.reset(
{
owner: { value: this.dossier.ownerId, disabled: !this.#permissionsService.canEditTeamMembers() },
approvers: this.#sortByName([...this.dossier.approverIds]),
members: this.#sortByName([...this.dossier.memberIds]),
} as unknown,
{ emitEvent: true },
);
this.#updateLists();
}
} }

View File

@ -94,6 +94,7 @@
<div *ngIf="withFloatingActions && !!editor?.hasChanges && canEdit && !isLeavingPage" [class.offset]="compare" class="changes-box"> <div *ngIf="withFloatingActions && !!editor?.hasChanges && canEdit && !isLeavingPage" [class.offset]="compare" class="changes-box">
<iqser-icon-button <iqser-icon-button
(action)="saveDictionary.emit()" (action)="saveDictionary.emit()"
[disabled]="!!(_loadingService.isLoading$ | async)"
[label]="'dictionary-overview.save-changes' | translate" [label]="'dictionary-overview.save-changes' | translate"
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
icon="iqser:check" icon="iqser:check"

View File

@ -56,7 +56,7 @@ export class DictionaryManagerComponent implements OnChanges {
constructor( constructor(
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
private readonly _loadingService: LoadingService, protected readonly _loadingService: LoadingService,
private readonly _changeRef: ChangeDetectorRef, private readonly _changeRef: ChangeDetectorRef,
readonly activeDossiersService: ActiveDossiersService, readonly activeDossiersService: ActiveDossiersService,
readonly dossierTemplatesService: DossierTemplatesService, readonly dossierTemplatesService: DossierTemplatesService,

View File

@ -1,6 +1,7 @@
<ngx-monaco-editor <ngx-monaco-editor
(init)="onCodeEditorInit($event)" (init)="onCodeEditorInit($event)"
(ngModelChange)="codeEditorTextChanged()" (ngModelChange)="_editorTextChanged$.next($event)"
(paste)="onPaste($event)"
*ngIf="!showDiffEditor" *ngIf="!showDiffEditor"
[(ngModel)]="value" [(ngModel)]="value"
[options]="editorOptions" [options]="editorOptions"

View File

@ -1,7 +1,8 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { Debounce, List, OnChange } from '@iqser/common-ui'; import { List, LoadingService, OnChange } from '@iqser/common-ui';
import { UserPreferenceService } from '@users/user-preference.service';
import { EditorThemeService } from '@services/editor-theme.service'; import { EditorThemeService } from '@services/editor-theme.service';
import { debounceTime, filter, tap } from 'rxjs/operators';
import { Subject, Subscription } from 'rxjs';
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions; import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import ICodeEditor = monaco.editor.ICodeEditor; import ICodeEditor = monaco.editor.ICodeEditor;
import IDiffEditor = monaco.editor.IDiffEditor; import IDiffEditor = monaco.editor.IDiffEditor;
@ -26,7 +27,7 @@ const notZero = (lineChange: ILineChange) => lineChange.originalEndLineNumber !=
templateUrl: './editor.component.html', templateUrl: './editor.component.html',
styleUrls: ['./editor.component.scss'], styleUrls: ['./editor.component.scss'],
}) })
export class EditorComponent implements OnInit, OnChanges { export class EditorComponent implements OnInit, OnChanges, OnDestroy {
@Input() showDiffEditor = false; @Input() showDiffEditor = false;
@Input() diffEditorText: string; @Input() diffEditorText: string;
@Input() @OnChange<List, EditorComponent>('revert') initialEntries: List; @Input() @OnChange<List, EditorComponent>('revert') initialEntries: List;
@ -40,10 +41,25 @@ export class EditorComponent implements OnInit, OnChanges {
editorOptions: IStandaloneEditorConstructionOptions = {}; editorOptions: IStandaloneEditorConstructionOptions = {};
codeEditor: ICodeEditor; codeEditor: ICodeEditor;
value: string; value: string;
protected readonly _editorTextChanged$ = new Subject<string>();
private _diffEditor: IDiffEditor; private _diffEditor: IDiffEditor;
private _decorations: string[] = []; private _decorations: string[] = [];
private readonly _sub$ = new Subscription();
private _initialEntriesSet = new Set<string>();
constructor(private readonly _userPreferenceService: UserPreferenceService, private readonly _editorThemeService: EditorThemeService) {} constructor(private readonly _loadingService: LoadingService, private readonly _editorThemeService: EditorThemeService) {
const textChanged$ = this._editorTextChanged$.pipe(
debounceTime(300), // prevent race condition with onPaste event
filter(text => text.length > 0),
tap(newText => {
const newDecorations = this._getDecorations(newText);
this._decorations = this.codeEditor.deltaDecorations(this._decorations, newDecorations);
this.diffValue = this.value;
this._loadingService.stop();
}),
);
this._sub$.add(textChanged$.subscribe());
}
get currentEntries(): string[] { get currentEntries(): string[] {
return this.value.split('\n'); return this.value.split('\n');
@ -57,6 +73,16 @@ export class EditorComponent implements OnInit, OnChanges {
return this.currentEntries.length; return this.currentEntries.length;
} }
ngOnDestroy() {
this._sub$.unsubscribe();
}
onPaste(event: ClipboardEvent) {
if ((event.target as HTMLTextAreaElement).ariaRoleDescription === 'editor') {
this._loadingService.start();
}
}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes.diffEditorText) { if (changes.diffEditorText) {
this._diffEditor?.getOriginalEditor().setValue(this.diffEditorText); this._diffEditor?.getOriginalEditor().setValue(this.diffEditorText);
@ -92,17 +118,33 @@ export class EditorComponent implements OnInit, OnChanges {
this._setTheme(); this._setTheme();
} }
@Debounce() _getDecorations(text: string) {
codeEditorTextChanged() { const currentEntries = text.split('\n');
const newDecorations = this.currentEntries.filter(entry => this._isNew(entry)).map(entry => this._getDecoration(entry)); const newDecorations: IModelDeltaDecoration[] = [];
this._decorations = this.codeEditor.deltaDecorations(this._decorations, newDecorations);
this.diffValue = this.value; for (let index = 0; index < currentEntries.length; index++) {
const entry = currentEntries.at(index)?.trim();
if (!entry || entry.length === 0) {
continue;
}
if (this._initialEntriesSet.has(entry)) {
continue;
}
const line = index + 1;
newDecorations.push(this._getDecoration(entry, line));
}
return newDecorations;
} }
revert() { revert() {
this.value = this.initialEntries.join('\n'); this.value = this.initialEntries.join('\n');
this._initialEntriesSet = new Set<string>(this.initialEntries);
this.diffValue = this.value; this.diffValue = this.value;
this._diffEditor?.getModifiedEditor().setValue(this.diffValue); this._diffEditor?.getModifiedEditor().setValue(this.diffValue);
this._editorTextChanged$.next(this.value);
} }
private _defineThemes(): void { private _defineThemes(): void {
@ -157,12 +199,7 @@ export class EditorComponent implements OnInit, OnChanges {
}); });
} }
private _isNew(entry: string): boolean { private _getDecoration(entry: string, line: number): IModelDeltaDecoration {
return this.initialEntries.indexOf(entry) < 0 && entry?.trim().length > 0;
}
private _getDecoration(entry: string): IModelDeltaDecoration {
const line = this.currentEntries.indexOf(entry) + 1;
const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker'; const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker';
const range = new monaco.Range(line, 1, line, 1); const range = new monaco.Range(line, 1, line, 1);

View File

@ -13,10 +13,10 @@
</div> </div>
</div> </div>
<div *ngIf="primaryAttribute" class="small-label"> <div *ngIf="ctx.primaryAttribute" class="small-label">
<div class="primary-attribute"> <div class="primary-attribute">
<span [matTooltip]="primaryAttribute" matTooltipPosition="above"> <span [matTooltip]="ctx.primaryAttribute" matTooltipPosition="above">
{{ primaryAttribute }} {{ ctx.primaryAttribute }}
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,10 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service'; import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service';
import { FileAttributes } from '@red/domain'; import { FileAttributes } from '@red/domain';
import { ContextComponent, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui'; import { ContextComponent, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui';
import { FileAttributesConfigMap, FileAttributesService } from '@services/entity-services/file-attributes.service'; import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { tap } from 'rxjs/operators'; import { combineLatest, map, ReplaySubject } from 'rxjs';
import { BehaviorSubject, combineLatestWith, map } from 'rxjs';
interface PartialFile { interface PartialFile {
readonly isError: boolean; readonly isError: boolean;
@ -18,7 +17,7 @@ interface PartialFile {
} }
interface FileNameColumnContext { interface FileNameColumnContext {
fileAttributesConfig: FileAttributesConfigMap; primaryAttribute: string;
} }
@Component({ @Component({
@ -30,36 +29,28 @@ interface FileNameColumnContext {
export class FileNameColumnComponent extends ContextComponent<FileNameColumnContext> implements OnInit, OnChanges { export class FileNameColumnComponent extends ContextComponent<FileNameColumnContext> implements OnInit, OnChanges {
@Input() file: PartialFile; @Input() file: PartialFile;
@Input() dossierTemplateId: string; @Input() dossierTemplateId: string;
primaryAttribute: string; readonly #reloadAttribute = new ReplaySubject<void>(1);
readonly #reloadAttribute = new BehaviorSubject(null);
constructor( constructor(
private readonly _fileAttributeService: FileAttributesService, private readonly _fileAttributeService: FileAttributesService,
private readonly _primaryFileAttributeService: PrimaryFileAttributeService, private readonly _primaryFileAttributeService: PrimaryFileAttributeService,
private readonly _changeDetectorRef: ChangeDetectorRef,
) { ) {
super(); super();
} }
ngOnInit(): void { ngOnInit(): void {
const fileAttributesConfig$ = this._fileAttributeService.fileAttributesConfig$.pipe( const primaryAttribute$ = combineLatest([this._fileAttributeService.fileAttributesConfig$, this.#reloadAttribute]).pipe(
combineLatestWith(this.#reloadAttribute), map(() => this._primaryFileAttributeService.getPrimaryFileAttributeValue(this.file, this.dossierTemplateId)),
tap(() => {
this.primaryAttribute = this._primaryFileAttributeService.getPrimaryFileAttributeValue(this.file, this.dossierTemplateId);
this._changeDetectorRef.detectChanges();
}),
map(([attributes]) => attributes),
); );
super._initContext({ super._initContext({
fileAttributesConfig: fileAttributesConfig$, primaryAttribute: primaryAttribute$,
}); });
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (!changes.file) { if (changes.file) {
return; this.#reloadAttribute.next();
} }
this.#reloadAttribute.next(null);
} }
get scrollableParentView(): ScrollableParentView { get scrollableParentView(): ScrollableParentView {

View File

@ -1,18 +1,20 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, inject, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { CircleButtonTypes, getCurrentUser, List } from '@iqser/common-ui'; import { CircleButtonTypes, getCurrentUser, List } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../shared-dossiers/services/dossiers-dialog.service'; import { DossiersDialogService } from '../../../shared-dossiers/services/dossiers-dialog.service';
import { ROLES } from '@users/roles'; import { ROLES } from '@users/roles';
import { User } from '@red/domain'; import { User } from '@red/domain';
import { UserService } from '@users/user.service';
@Component({ @Component({
selector: 'redaction-team-members', selector: 'redaction-team-members',
templateUrl: './team-members.component.html', templateUrl: './team-members.component.html',
styleUrls: ['./team-members.component.scss'], styleUrls: ['./team-members.component.scss'],
}) })
export class TeamMembersComponent { export class TeamMembersComponent implements OnChanges {
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
readonly roles = ROLES; readonly roles = ROLES;
readonly currentUser = getCurrentUser<User>(); readonly currentUser = getCurrentUser<User>();
readonly userService = inject(UserService);
@Input() memberIds: List; @Input() memberIds: List;
@Input() perLine: number; @Input() perLine: number;
@ -29,6 +31,11 @@ export class TeamMembersComponent {
constructor(private readonly _dialogService: DossiersDialogService) {} constructor(private readonly _dialogService: DossiersDialogService) {}
ngOnChanges() {
this.memberIds ??= [];
this.memberIds = [...this.memberIds].sort((a, b) => this.userService.getName(a).localeCompare(this.userService.getName(b)));
}
get maxTeamMembersBeforeExpand(): number { get maxTeamMembersBeforeExpand(): number {
return this.perLine - (this.canAdd ? 1 : 0); return this.perLine - (this.canAdd ? 1 : 0);
} }
@ -46,7 +53,7 @@ export class TeamMembersComponent {
} }
canRemoveMember(userId: string) { canRemoveMember(userId: string) {
return this.canRemove && this.unremovableMembers.indexOf(userId) === -1; return this.canRemove && !this.unremovableMembers.includes(userId);
} }
openEditDossierDialog(): void { openEditDossierDialog(): void {

View File

@ -2,10 +2,10 @@ import { Component, Inject } from '@angular/core';
import { Dossier, DownloadFileType, DownloadFileTypes, IReportTemplate, File, WorkflowFileStatuses } from '@red/domain'; import { Dossier, DownloadFileType, DownloadFileTypes, IReportTemplate, File, WorkflowFileStatuses } from '@red/domain';
import { downloadTypesForDownloadTranslations } from '@translations/download-types-translations'; import { downloadTypesForDownloadTranslations } from '@translations/download-types-translations';
import { ReportTemplateService } from '@services/report-template.service'; import { ReportTemplateService } from '@services/report-template.service';
import { AbstractControl, FormBuilder } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { IconButtonTypes, List } from '@iqser/common-ui'; import { BaseDialogComponent, IconButtonTypes, List } from '@iqser/common-ui';
export interface DownloadDialogData { export interface DownloadDialogData {
readonly dossier: Dossier; readonly dossier: Dossier;
@ -23,19 +23,21 @@ export interface DownloadDialogResult {
templateUrl: './download-dialog.component.html', templateUrl: './download-dialog.component.html',
styleUrls: ['./download-dialog.component.scss'], styleUrls: ['./download-dialog.component.scss'],
}) })
export class DownloadDialogComponent { export class DownloadDialogComponent extends BaseDialogComponent {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly downloadTypes: { key: DownloadFileType; label: string }[] = this._formDownloadTypes; readonly downloadTypes: { key: DownloadFileType; label: string }[] = this._formDownloadTypes;
readonly availableReportTypes = this._availableReportTypes; readonly availableReportTypes = this._availableReportTypes;
readonly form = this._getForm();
constructor( constructor(
private readonly _defaultColorsService: DefaultColorsService, private readonly _defaultColorsService: DefaultColorsService,
private readonly _reportTemplateController: ReportTemplateService, private readonly _reportTemplateController: ReportTemplateService,
private readonly _formBuilder: FormBuilder, protected readonly _dialogRef: MatDialogRef<DownloadDialogComponent, DownloadDialogResult>,
private readonly _dialogRef: MatDialogRef<DownloadDialogComponent, DownloadDialogResult>,
@Inject(MAT_DIALOG_DATA) readonly data: DownloadDialogData, @Inject(MAT_DIALOG_DATA) readonly data: DownloadDialogData,
) {} ) {
super(_dialogRef);
this.form = this._getForm();
}
get reportTypesLength() { get reportTypesLength() {
return this.form.controls.reportTemplateIds?.value?.length || 0; return this.form.controls.reportTemplateIds?.value?.length || 0;

View File

@ -1,4 +1,4 @@
import { GenericService, List, QueryParam } from '@iqser/common-ui'; import { GenericService, LAST_CHECKED_OFFSET, List, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossierChanges } from '@red/domain'; import { Dossier, DossierStats, IDossierChanges } from '@red/domain';
import { forkJoin, Observable, of, throwError, timer } from 'rxjs'; import { forkJoin, Observable, of, throwError, timer } from 'rxjs';
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
@ -43,11 +43,11 @@ export class DossiersChangesService extends GenericService<Dossier> {
} }
hasChangesDetails$(): Observable<IDossierChanges> { hasChangesDetails$(): Observable<IDossierChanges> {
const body = { value: this._lastCheckedForChanges.get('root') }; const body = { value: this._lastCheckedForChanges.get(ROOT_CHANGES_KEY) };
const dateBeforeRequest = new Date().toISOString(); const dateBeforeRequest = new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString();
return this._post<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe( return this._post<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.length > 0), filter(changes => changes.length > 0),
tap(() => this._lastCheckedForChanges.set('root', dateBeforeRequest)), tap(() => this._lastCheckedForChanges.set(ROOT_CHANGES_KEY, dateBeforeRequest)),
); );
} }

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { forkJoin, Observable, of, throwError, zip } from 'rxjs'; import { firstValueFrom, forkJoin, Observable, of, throwError } from 'rxjs';
import { EntitiesService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; import { EntitiesService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, IDictionary, IUpdateDictionary, SuperTypes } from '@red/domain'; import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, IDictionary, IUpdateDictionary, SuperTypes } from '@red/domain';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
@ -95,7 +95,7 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
); );
} }
saveEntries( async saveEntries(
entries: List, entries: List,
initialEntries: List, initialEntries: List,
dossierTemplateId: string, dossierTemplateId: string,
@ -103,42 +103,52 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
dossierId: string, dossierId: string,
showToast = true, showToast = true,
dictionaryEntryType = DictionaryEntryTypes.ENTRY, dictionaryEntryType = DictionaryEntryTypes.ENTRY,
): Observable<unknown> { removeCurrent = true,
const entriesToAdd = entries.map(e => e.trim()).filter(e => !!e); ) {
const deletedEntries = initialEntries.filter(e => !entries.includes(e)); const entriesToAdd: Array<string> = [];
console.log({ entriesToAdd, deletedEntries }); const initialEntriesSet = new Set(initialEntries);
// remove empty lines let hasInvalidRows = false;
const invalidRowsExist = entriesToAdd.filter(e => e.length < MIN_WORD_LENGTH); for (let i = 0; i < entries.length; i++) {
if (invalidRowsExist.length === 0) { const entry = entries.at(i);
// can add at least 1 - block UI if (!entry.trim() || initialEntriesSet.has(entry)) {
const obs: Observable<IDictionary>[] = []; continue;
}
hasInvalidRows ||= entry.length < MIN_WORD_LENGTH;
entriesToAdd.push(entry);
}
if (hasInvalidRows) {
this._toaster.error(_('dictionary-overview.error.entries-too-short'));
throw new Error('Entries too short');
}
const deletedEntries: Array<string> = [];
const entriesSet = new Set(entries);
for (let i = 0; i < initialEntries.length; i++) {
const entry = initialEntries.at(i);
if (entriesSet.has(entry)) {
continue;
}
deletedEntries.push(entry);
}
try {
if (deletedEntries.length) { if (deletedEntries.length) {
obs.push(this._deleteEntries(deletedEntries, dossierTemplateId, type, dictionaryEntryType, dossierId)); await this._deleteEntries(deletedEntries, dossierTemplateId, type, dictionaryEntryType, dossierId);
} }
if (entriesToAdd.filter(e => !initialEntries.includes(e)).length) { if (entriesToAdd.length) {
obs.push(this._addEntries(entriesToAdd, dossierTemplateId, type, dictionaryEntryType, dossierId)); await this._addEntries(entriesToAdd, dossierTemplateId, type, dictionaryEntryType, dossierId, removeCurrent);
} }
return zip(obs).pipe(
switchMap(dictionary => this._dossierTemplateStatsService.getFor([dossierTemplateId]).pipe(map(() => dictionary))),
tap({
next: () => {
if (showToast) { if (showToast) {
this._toaster.success(_('dictionary-overview.success.generic')); this._toaster.success(_('dictionary-overview.success.generic'));
} }
}, } catch (error) {
error: error => { if ((error as HttpErrorResponse).status === 400) {
if (error.status === 400) {
this._toaster.error(_('dictionary-overview.error.400')); this._toaster.error(_('dictionary-overview.error.400'));
} else { } else {
this._toaster.error(_('dictionary-overview.error.generic')); this._toaster.error(_('dictionary-overview.error.generic'));
} }
},
}),
);
} }
this._toaster.error(_('dictionary-overview.error.entries-too-short'));
return throwError(() => 'Entries too short');
} }
hasManualType(dossierTemplateId: string): boolean { hasManualType(dossierTemplateId: string): boolean {
@ -248,14 +258,15 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
type: string, type: string,
dictionaryEntryType: DictionaryEntryType, dictionaryEntryType: DictionaryEntryType,
dossierId: string, dossierId: string,
removeCurrent = true,
) { ) {
const queryParams: List<QueryParam> = [ const queryParams: List<QueryParam> = [
{ key: 'dossierId', value: dossierId }, { key: 'dossierId', value: dossierId },
{ key: 'dictionaryEntryType', value: dictionaryEntryType }, { key: 'dictionaryEntryType', value: dictionaryEntryType },
{ key: 'removeCurrent', value: true }, { key: 'removeCurrent', value: removeCurrent },
]; ];
const url = `${this._defaultModelPath}/${type}/${dossierTemplateId}`; const url = `${this._defaultModelPath}/${type}/${dossierTemplateId}`;
return this._post(entries, url, queryParams); return firstValueFrom(this._post(entries, url, queryParams));
} }
/** /**
@ -273,6 +284,6 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
? [{ key: 'dossierId', value: dossierId }] ? [{ key: 'dossierId', value: dossierId }]
: [{ key: 'dictionaryEntryType', value: dictionaryEntryType }]; : [{ key: 'dictionaryEntryType', value: dictionaryEntryType }];
const url = `${this._defaultModelPath}/delete/${type}/${dossierTemplateId}`; const url = `${this._defaultModelPath}/delete/${type}/${dossierTemplateId}`;
return this._post(entries, url, queryParams); return firstValueFrom(this._post(entries, url, queryParams));
} }
} }

View File

@ -50,8 +50,8 @@ export class LicenseService extends GenericService<ILicenseReport> {
readonly selectedLicense$: Observable<ILicense>; readonly selectedLicense$: Observable<ILicense>;
activeLicenseId: string; activeLicenseId: string;
totalLicensedNumberOfPages = 0; totalLicensedNumberOfPages = 0;
currentInfo: ILicenseReport = {}; currentLicenseInfo: ILicenseReport = {};
annualInfo: ILicenseReport = {}; allLicensesInfo: ILicenseReport = {};
unlicensedPages = 0; unlicensedPages = 0;
analyzedPagesInCurrentLicensingPeriod = 0; analyzedPagesInCurrentLicensingPeriod = 0;
protected readonly _defaultModelPath = 'report'; protected readonly _defaultModelPath = 'report';
@ -96,27 +96,27 @@ export class LicenseService extends GenericService<ILicenseReport> {
const startDate = dayjs(license.validFrom); const startDate = dayjs(license.validFrom);
const endDate = dayjs(license.validUntil); const endDate = dayjs(license.validUntil);
const currentConfig = { const currentLicenseConfig = {
startDate: startDate.toDate(), startDate: startDate.toDate(),
endDate: endDate.toDate(), endDate: endDate.toDate(),
}; };
const thisYearConfig = { const allLicensesConfig = {
startDate: `${startDate.year()}-01-01T00:00:00.000Z`, startDate: '2020-01-01T00:00:00.000Z',
endDate: `${startDate.year()}-12-31T00:00:00.000Z`, endDate: '2023-12-31T00:00:00.000Z',
}; };
const configs = [currentConfig, thisYearConfig];
const configs = [currentLicenseConfig, allLicensesConfig];
const reports = configs.map(config => this.getReport(config)); const reports = configs.map(config => this.getReport(config));
[this.currentInfo, this.annualInfo] = await Promise.all(reports); [this.currentLicenseInfo, this.allLicensesInfo] = await Promise.all(reports);
if (this.currentInfo.numberOfAnalyzedPages > this.totalLicensedNumberOfPages) { if (this.currentLicenseInfo.numberOfAnalyzedPages > this.totalLicensedNumberOfPages) {
this.unlicensedPages = this.currentInfo.numberOfAnalyzedPages - this.totalLicensedNumberOfPages; this.unlicensedPages = this.currentLicenseInfo.numberOfAnalyzedPages - this.totalLicensedNumberOfPages;
this.analyzedPagesInCurrentLicensingPeriod = this.totalLicensedNumberOfPages;
} else { } else {
this.unlicensedPages = 0; this.unlicensedPages = 0;
this.analyzedPagesInCurrentLicensingPeriod = this.currentInfo.numberOfAnalyzedPages;
} }
this.analyzedPagesInCurrentLicensingPeriod = this.currentLicenseInfo.numberOfAnalyzedPages;
} }
getTotalLicensedNumberOfPages(license: ILicense) { getTotalLicensedNumberOfPages(license: ILicense) {

View File

@ -274,11 +274,11 @@ export class PermissionsService {
} }
canHardDeleteDossier(dossier: IDossier): boolean { canHardDeleteDossier(dossier: IDossier): boolean {
return this._iqserPermissionsService.has(ROLES.dossiers.delete) && this.isOwner(dossier); return this.canSoftDeleteDossier(dossier);
} }
canRestoreDossier(dossier: IDossier): boolean { canRestoreDossier(dossier: IDossier): boolean {
return this._iqserPermissionsService.has(ROLES.dossiers.delete) && (this.isOwner(dossier) || this.isDossierMember(dossier)); return this.canSoftDeleteDossier(dossier);
} }
canCreateDossier(dossierTemplate: DossierTemplate | DashboardStats): boolean { canCreateDossier(dossierTemplate: DossierTemplate | DashboardStats): boolean {
@ -349,6 +349,10 @@ export class PermissionsService {
); );
} }
canRotatePage(file: File) {
return this.isFileAssignee(file) && this._iqserPermissionsService.has(ROLES.files.rotatePage);
}
canDownloadRedactedFile() { canDownloadRedactedFile() {
return this._iqserPermissionsService.has(ROLES.files.processDownload); return this._iqserPermissionsService.has(ROLES.files.processDownload);
} }

View File

@ -0,0 +1,12 @@
export function setLocalStorageDataByFileId(fileId: string, key: string, value: string | number | boolean): void {
let data = localStorage.getItem(key) ?? '{}';
data = JSON.parse(data);
data[fileId] = value;
localStorage.setItem(key, JSON.stringify(data));
}
export function getLocalStorageDataByFileId(fileId: string, key: string): string | number | boolean {
let data = localStorage.getItem(key) ?? '{}';
data = JSON.parse(data);
return data[fileId];
}

View File

@ -1,7 +1,7 @@
{ {
"ADMIN_CONTACT_NAME": null, "ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null, "ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-08.iqser.cloud/redaction-gateway-v1", "API_URL": "https://red-staging.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager", "APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3, "AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40", "BACKEND_APP_VERSION": "4.4.40",
@ -11,7 +11,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3, "MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction", "OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null, "OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-08.iqser.cloud/auth/realms/redaction", "OAUTH_URL": "https://red-staging.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24, "RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural", "SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview", "MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",

View File

@ -526,5 +526,11 @@
"de": "", "de": "",
"it": "", "it": "",
"fr": "" "fr": ""
},
"edit-file-attributes": {
"en": "/en/index-en.html?contextId=document_list",
"de": "",
"it": "",
"fr": ""
} }
} }

View File

@ -1633,7 +1633,6 @@
"table-header": "{length} {length, plural, one{Begründung} other{Begründung}}" "table-header": "{length} {length, plural, one{Begründung} other{Begründung}}"
}, },
"license-info-screen": { "license-info-screen": {
"analyzed-pages": "Analysierte Seiten",
"backend-version": "Backend-Version der Anwendung", "backend-version": "Backend-Version der Anwendung",
"chart": { "chart": {
"cumulative": "Seiten insgesamt", "cumulative": "Seiten insgesamt",
@ -1666,6 +1665,7 @@
"inactive": "" "inactive": ""
}, },
"total-analyzed": "Seit {date} insgesamt analysierte Seiten", "total-analyzed": "Seit {date} insgesamt analysierte Seiten",
"total-ocr-analyzed": "",
"unlicensed-analyzed": "Über Lizenz hinaus analysierte Seiten", "unlicensed-analyzed": "Über Lizenz hinaus analysierte Seiten",
"usage-details": "Nutzungsdetails" "usage-details": "Nutzungsdetails"
}, },
@ -1998,6 +1998,17 @@
"annotations": "", "annotations": "",
"title": "" "title": ""
}, },
"false-positive-dialog": {
"actions": {
"cancel": "",
"save": ""
},
"content": {
"comment": "",
"body-text": ""
},
"header": ""
},
"rules-screen": { "rules-screen": {
"error": { "error": {
"generic": "Es ist ein Fehler aufgetreten ... Die Regeln konnten nicht aktualisiert werden!" "generic": "Es ist ein Fehler aufgetreten ... Die Regeln konnten nicht aktualisiert werden!"

View File

@ -1633,7 +1633,6 @@
"table-header": "{length} {length, plural, one{justification} other{justifications}}" "table-header": "{length} {length, plural, one{justification} other{justifications}}"
}, },
"license-info-screen": { "license-info-screen": {
"analyzed-pages": "Analyzed pages",
"backend-version": "Backend Application Version", "backend-version": "Backend Application Version",
"chart": { "chart": {
"cumulative": "Cumulative Pages", "cumulative": "Cumulative Pages",
@ -1643,7 +1642,7 @@
}, },
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)", "copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright Claim", "copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Current Licensing Period", "current-analyzed": "Analyzed Pages in Licensing Period",
"custom-app-title": "Custom Application Title", "custom-app-title": "Custom Application Title",
"email-report": "Email Report", "email-report": "Email Report",
"email": { "email": {
@ -1656,16 +1655,17 @@
"end-user-license-text": "The use of this product is subject to the terms of the Redaction End User Agreement, unless otherwise specified therein.", "end-user-license-text": "The use of this product is subject to the terms of the Redaction End User Agreement, unless otherwise specified therein.",
"end-user-license-title": "End User License Agreement", "end-user-license-title": "End User License Agreement",
"license-title": "License Title", "license-title": "License Title",
"licensed-page-count": "Number of licensed pages", "licensed-page-count": "Licensed Pages",
"licensed-to": "Licensed to", "licensed-to": "Licensed to",
"licensing-details": "Licensing Details", "licensing-details": "Licensing Details",
"licensing-period": "Licensing Period", "licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Analyzed Pages", "ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"status": { "status": {
"active": "Active", "active": "Active",
"inactive": "Inactive" "inactive": "Inactive"
}, },
"total-analyzed": "Total Analyzed Pages Since {date}", "total-analyzed": "Total Analyzed Pages",
"total-ocr-analyzed": "Total OCR Processed Pages",
"unlicensed-analyzed": "Unlicensed Analyzed Pages", "unlicensed-analyzed": "Unlicensed Analyzed Pages",
"usage-details": "Usage Details" "usage-details": "Usage Details"
}, },
@ -1847,7 +1847,7 @@
"save": "Save changes" "save": "Save changes"
}, },
"form": { "form": {
"auto-expand-filters-on-action": "Auto expand filters on my actions", "auto-expand-filters-on-action": "Auto-expand filters on my actions",
"load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview", "load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview",
"show-suggestions-in-preview": "Display suggestions in document preview", "show-suggestions-in-preview": "Display suggestions in document preview",
"unapproved-suggestions-warning": "Warning regarding unapproved suggestions in document Preview mode" "unapproved-suggestions-warning": "Warning regarding unapproved suggestions in document Preview mode"
@ -1998,6 +1998,17 @@
"annotations": "", "annotations": "",
"title": "Structured Component Management" "title": "Structured Component Management"
}, },
"false-positive-dialog": {
"actions": {
"cancel": "Cancel",
"save": "Yes, proceed"
},
"content": {
"comment": "Comment",
"body-text": "''{value}'' is a false positive in this context: {context}"
},
"header": "False Positive"
},
"rules-screen": { "rules-screen": {
"error": { "error": {
"generic": "Something went wrong... Rules update failed!" "generic": "Something went wrong... Rules update failed!"

View File

@ -1633,7 +1633,6 @@
"table-header": "{length} {length, plural, one{Begründung} other{Begründung}}" "table-header": "{length} {length, plural, one{Begründung} other{Begründung}}"
}, },
"license-info-screen": { "license-info-screen": {
"analyzed-pages": "Analysierte Seiten",
"backend-version": "Backend-Version der Anwendung", "backend-version": "Backend-Version der Anwendung",
"chart": { "chart": {
"cumulative": "Seiten insgesamt", "cumulative": "Seiten insgesamt",
@ -1666,6 +1665,7 @@
"inactive": "" "inactive": ""
}, },
"total-analyzed": "Seit {date} insgesamt analysierte Seiten", "total-analyzed": "Seit {date} insgesamt analysierte Seiten",
"total-ocr-analyzed": "",
"unlicensed-analyzed": "Über Lizenz hinaus analysierte Seiten", "unlicensed-analyzed": "Über Lizenz hinaus analysierte Seiten",
"usage-details": "Nutzungsdetails" "usage-details": "Nutzungsdetails"
}, },

View File

@ -1633,7 +1633,6 @@
"table-header": "{length} {length, plural, one{justification} other{justifications}}" "table-header": "{length} {length, plural, one{justification} other{justifications}}"
}, },
"license-info-screen": { "license-info-screen": {
"analyzed-pages": "Analyzed pages",
"backend-version": "Backend Application Version", "backend-version": "Backend Application Version",
"chart": { "chart": {
"cumulative": "Cumulative Pages", "cumulative": "Cumulative Pages",
@ -1643,7 +1642,7 @@
}, },
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)", "copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright Claim", "copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Current Licensing Period", "current-analyzed": "Analyzed Pages in Licensing Period",
"custom-app-title": "Custom Application Title", "custom-app-title": "Custom Application Title",
"email-report": "Email Report", "email-report": "Email Report",
"email": { "email": {
@ -1656,16 +1655,17 @@
"end-user-license-text": "The use of this product is subject to the terms of the Component End User Agreement, unless otherwise specified therein.", "end-user-license-text": "The use of this product is subject to the terms of the Component End User Agreement, unless otherwise specified therein.",
"end-user-license-title": "End User License Agreement", "end-user-license-title": "End User License Agreement",
"license-title": "License Title", "license-title": "License Title",
"licensed-page-count": "Number of licensed pages", "licensed-page-count": "Licensed pages",
"licensed-to": "Licensed to", "licensed-to": "Licensed to",
"licensing-details": "Licensing Details", "licensing-details": "Licensing Details",
"licensing-period": "Licensing Period", "licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Analyzed Pages", "ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"status": { "status": {
"active": "Active", "active": "Active",
"inactive": "Inactive" "inactive": "Inactive"
}, },
"total-analyzed": "Total Analyzed Pages Since {date}", "total-analyzed": "Total Analyzed Pages Since {date}",
"total-ocr-analyzed": "Total OCR Processed Pages Since {date}",
"unlicensed-analyzed": "Unlicensed Analyzed Pages", "unlicensed-analyzed": "Unlicensed Analyzed Pages",
"usage-details": "Usage Details" "usage-details": "Usage Details"
}, },

View File

@ -1 +0,0 @@
target/

View File

@ -1,36 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atlassian.bamboo</groupId>
<artifactId>bamboo-specs-parent</artifactId>
<version>8.1.1</version>
<relativePath/>
</parent>
<artifactId>bamboo-specs</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.atlassian.bamboo</groupId>
<artifactId>bamboo-specs-api</artifactId>
</dependency>
<dependency>
<groupId>com.atlassian.bamboo</groupId>
<artifactId>bamboo-specs</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- run 'mvn test' to perform offline validation of the plan -->
<!-- run 'mvn -Ppublish-specs' to upload the plan to your Bamboo server -->
</project>

View File

@ -1,128 +0,0 @@
package buildjob;
import com.atlassian.bamboo.specs.api.BambooSpec;
import com.atlassian.bamboo.specs.api.builders.BambooKey;
import com.atlassian.bamboo.specs.api.builders.credentials.SharedCredentialsIdentifier;
import com.atlassian.bamboo.specs.api.builders.credentials.SharedCredentialsScope;
import com.atlassian.bamboo.specs.api.builders.docker.DockerConfiguration;
import com.atlassian.bamboo.specs.api.builders.permission.PermissionType;
import com.atlassian.bamboo.specs.api.builders.permission.Permissions;
import com.atlassian.bamboo.specs.api.builders.permission.PlanPermissions;
import com.atlassian.bamboo.specs.api.builders.plan.Job;
import com.atlassian.bamboo.specs.api.builders.plan.Plan;
import com.atlassian.bamboo.specs.api.builders.plan.PlanIdentifier;
import com.atlassian.bamboo.specs.api.builders.plan.Stage;
import com.atlassian.bamboo.specs.api.builders.plan.artifact.Artifact;
import com.atlassian.bamboo.specs.api.builders.plan.branches.BranchCleanup;
import com.atlassian.bamboo.specs.api.builders.plan.branches.PlanBranchManagement;
import com.atlassian.bamboo.specs.api.builders.project.Project;
import com.atlassian.bamboo.specs.api.builders.repository.VcsRepositoryIdentifier;
import com.atlassian.bamboo.specs.builders.repository.git.GitRepository;
import com.atlassian.bamboo.specs.builders.task.*;
import com.atlassian.bamboo.specs.builders.trigger.BitbucketServerTrigger;
import com.atlassian.bamboo.specs.model.task.ScriptTaskProperties;
import com.atlassian.bamboo.specs.util.BambooServer;
/**
* Plan configuration for Bamboo.
* Learn more on: <a href="https://confluence.atlassian.com/display/BAMBOO/Bamboo+Specs">https://confluence.atlassian.com/display/BAMBOO/Bamboo+Specs</a>
*/
@BambooSpec
public class PlanSpec {
/**
* Run main to publish plan on Bamboo
*/
public static void main(final String[] args) throws Exception {
//By default credentials are read from the '.credentials' file.
BambooServer bambooServer = new BambooServer("http://localhost:8085");
Plan buildPlan = new PlanSpec().createDockerBuildPlan();
bambooServer.publish(buildPlan);
PlanPermissions buildPlanPermissions = new PlanSpec().createPlanPermission(buildPlan.getIdentifier());
bambooServer.publish(buildPlanPermissions);
}
private PlanPermissions createPlanPermission(PlanIdentifier planIdentifier) {
Permissions permission = new Permissions()
.userPermissions("atlbamboo", PermissionType.EDIT, PermissionType.VIEW, PermissionType.ADMIN, PermissionType.CLONE, PermissionType.BUILD)
.userPermissions("tbejan", PermissionType.ADMIN, PermissionType.EDIT, PermissionType.VIEW, PermissionType.CLONE, PermissionType.BUILD)
.groupPermissions("devplant", PermissionType.EDIT, PermissionType.VIEW, PermissionType.BUILD)
.groupPermissions("Documentation", PermissionType.VIEW)
.loggedInUserPermissions(PermissionType.VIEW).anonymousUserPermissionView();
return new PlanPermissions(planIdentifier.getProjectKey(), planIdentifier.getPlanKey()).permissions(permission);
}
private Project project() {
return new Project().name("RED").key(new BambooKey("RED"));
}
public Plan createDockerBuildPlan() {
return new Plan(project(), "Redaction UI", new BambooKey("UI"))
.description("Docker build for Redaction UI.")
.stages(new Stage("UI Build Stage")
.jobs(creatGinCloudPlatformImagesJob("red-ui")))
.stages(new Stage("Release")
.manual(true)
.jobs(createRelease()))
.linkedRepositories("RED / ui")
.linkedRepositories("Shared Libraries / common-ui")
.triggers(new BitbucketServerTrigger().selectedTriggeringRepositories(new VcsRepositoryIdentifier("RED / ui")))
.planBranchManagement(new PlanBranchManagement().createForVcsBranch().delete(new BranchCleanup().whenInactiveInRepositoryAfterDays(30)).notificationForCommitters());
}
public Job creatGinCloudPlatformImagesJob(String project) {
return new Job("Build Job UI" , new BambooKey("UIBUILD"))
.tasks(
new CleanWorkingDirectoryTask().description("My clean working directory task"),
// Checkout
new VcsCheckoutTask().description("Checkout Default Repository")
.checkoutItems(new CheckoutItem().defaultRepository().path("redaction-ui")),
// Build
new ScriptTask().description("Build")
.location(ScriptTaskProperties.Location.FILE)
.workingSubdirectory("redaction-ui")
.fileFromPath("bamboo-specs/src/main/resources/scripts/build.sh")
.environmentVariables(
"PROJECT=\"" + project + "\" " +
"BAMBOO_DOWNLOAD_PASS=\"${bamboo.bamboo_download_pass}\" " +
"BAMBOO_DOWNLOAD_USER=\"${bamboo.bamboo_download_user}\" "),
// read version from artifact
new InjectVariablesTask().path("redaction-ui/version.properties"),
// commit release
new VcsCommitTask().commitMessage("chore(release)").repository("RED / ui"),
// create tag with this version
new VcsTagTask().tagName("${bamboo.inject.APP_VERSION}").repository("RED / ui")
).dockerConfiguration(
new DockerConfiguration().image("nexus.iqser.com:5001/infra/release_build:4.2.0")
.volume("/var/run/docker.sock", "/var/run/docker.sock"))
.artifacts(new Artifact("version").location(".").copyPattern("**/version.properties").shared(true),
new Artifact("paligo-theme.tar.gz").location(".").copyPattern("**/paligo-theme.tar.gz").shared(true));
}
public Job createRelease() {
return new Job("Create Release", new BambooKey("CRLS"))
.tasks(
new CleanWorkingDirectoryTask().description("My clean working directory task"),
new VcsCheckoutTask().description("Checkout Default Repository")
.checkoutItems(new CheckoutItem().defaultRepository()).cleanCheckout(true),
new ArtifactDownloaderTask().description("Download version artifact")
.sourcePlan(new PlanIdentifier("RED", "UI"))
.artifacts(new DownloadItem().artifact("version")),
// read version from artifact
new InjectVariablesTask().path("redaction-ui/version.properties"),
new ScriptTask().description("checkout tag").inlineBody("git checkout tags/${bamboo.inject.APP_VERSION}"),
new VcsBranchTask().branchName("release/${bamboo.inject.APP_VERSION}").repository("RED / ui"))
.dockerConfiguration(new DockerConfiguration().image("nexus.iqser.com:5001/infra/release_build:2.9.1")
.volume("/var/run/docker.sock", "/var/run/docker.sock"));
}
}

View File

@ -1,62 +0,0 @@
#!/bin/bash
set -e
imageName="nexus.iqser.com:5001/red/$PROJECT"
dockerfileLocation="docker/$PROJECT/Dockerfile"
echo "submodule status"
git submodule status
echo "On branch $bamboo_planRepository_branchName building project $PROJECT"
# shellcheck disable=SC2154
if [[ "$bamboo_planRepository_branchName" == "master" ]]
then
./versions.sh minor
version=$(jq -r '.version' < package.json)
fi
if [[ "$bamboo_planRepository_branchName" == release* ]]
then
./versions.sh patch
version=$(jq -r '.version' < package.json)
fi
echo "Building version $version"
docker build --build-arg bamboo_sonarqube_api_token_secret=${bamboo_sonarqube_api_token_secret} -t "$imageName":latest -f "$dockerfileLocation" .
if [[ -n ${version+z} ]]
then
echo "APP_VERSION=${version}" > version.properties
echo "Publishing Images with version $version"
echo "$BAMBOO_DOWNLOAD_PASS" | docker login -u "$BAMBOO_DOWNLOAD_USER" --password-stdin nexus.iqser.com:5001
# re-build intermediate build stage from layer cache, run image and get artifacts ( paligo theme )
docker build --build-arg bamboo_sonarqube_api_token_secret=${bamboo_sonarqube_api_token_secret} --target builder -t builder-image:latest -f "$dockerfileLocation" .
mkdir -p ./paligo-theme
docker run -v "$(pwd)"/paligo-theme:/tmp/styles-export builder-image:latest
tar -czvf paligo-theme.tar.gz ./paligo-theme
docker push "$imageName:latest"
docker tag "$imageName:latest" "$imageName:$version"
docker push "$imageName:$version"
else
echo "Not on a relevant branch $bamboo_planRepository_branchName ... skipping."
echo "APP_VERSION=BRANCH-$bamboo_planRepository_branchName-$bamboo_buildNumber" > version.properties
if [[ ! -z "$bamboo_version_tag" ]]
then
echo "$BAMBOO_DOWNLOAD_PASS" | docker login -u "$BAMBOO_DOWNLOAD_USER" --password-stdin nexus.iqser.com:5001
echo "Pushing custom tag: $bamboo_version_tag"
docker tag "$imageName:latest" "$imageName:$bamboo_version_tag"
docker push "$imageName:$bamboo_version_tag"
fi
fi

View File

@ -1,16 +0,0 @@
package buildjob;
import com.atlassian.bamboo.specs.api.builders.plan.Plan;
import com.atlassian.bamboo.specs.api.exceptions.PropertiesValidationException;
import com.atlassian.bamboo.specs.api.util.EntityPropertiesBuilders;
import org.junit.Test;
public class PlanSpecTest {
@Test
public void checkYourPlanOffline() throws PropertiesValidationException {
Plan plan = new PlanSpec().createDockerBuildPlan();
EntityPropertiesBuilders.build(plan);
}
}

View File

@ -30,12 +30,12 @@ COPY angular.json angular.json
COPY nx.json nx.json COPY nx.json nx.json
COPY .eslintrc.json .eslintrc.json COPY .eslintrc.json .eslintrc.json
COPY tsconfig.json tsconfig.json COPY tsconfig.json tsconfig.json
COPY versions.sh version.sh
COPY paligo-styles paligo-styles COPY paligo-styles paligo-styles
COPY sonar.js sonar.js COPY sonar.js sonar.js
## Build the angular app in production mode and store the artifacts in dist folder ## Build the angular app in production mode and store the artifacts in dist folder
RUN node sonar.js ## Fix Sonar Auth problem the uncomment
## RUN node sonar.js
RUN yarn run build-lint-all RUN yarn run build-lint-all
RUN yarn run build-paligo-styles RUN yarn run build-paligo-styles
@ -59,9 +59,7 @@ RUN chmod o+r -R /usr/share/nginx/html
RUN chmod g+r -R /usr/share/nginx/html RUN chmod g+r -R /usr/share/nginx/html
## Change permissions to enable openShift functionality ## Change permissions to enable openShift functionality
RUN chmod -R g+rwx /var/cache/nginx /var/run /var/log/nginx /usr/share /etc/nginx # RUN chmod -R g+rwx /var/cache/nginx /var/run /var/log/nginx /usr/share /etc/nginx
USER 1001
COPY docker/red-ui/docker-entrypoint.sh / COPY docker/red-ui/docker-entrypoint.sh /
CMD ["/docker-entrypoint.sh"] CMD ["/docker-entrypoint.sh"]

@ -1 +1 @@
Subproject commit d09078e44c8c294c78c44080d0bb8403d0ec6c34 Subproject commit 406f7b1fdd025b4e87273c2867ea6fdbc16bab3b

View File

@ -45,7 +45,7 @@ export class DownloadStatus implements IDownloadStatus, IListable {
} }
private get _size() { private get _size() {
const i = this.fileSize === 0 ? 0 : Math.floor(Math.log(this.fileSize) / Math.log(1024)); const i = this.fileSize === 0 ? 0 : Math.floor(Math.log(this.fileSize) / Math.log(1000));
return (this.fileSize / Math.pow(1024, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; return (this.fileSize / Math.pow(1000, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "redaction", "name": "redaction",
"version": "3.988.0", "version": "3.988.46",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -39,7 +39,7 @@
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0", "@ngx-translate/http-loader": "^7.0.0",
"@nrwl/angular": "15.6.3", "@nrwl/angular": "15.6.3",
"@pdftron/webviewer": "8.11.0", "@pdftron/webviewer": "10.1.0",
"@swimlane/ngx-charts": "^20.0.1", "@swimlane/ngx-charts": "^20.0.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",

Binary file not shown.

1
version.properties Normal file
View File

@ -0,0 +1 @@
APP_VERSION=3.988.46

View File

@ -1,47 +0,0 @@
#!/bin/bash
function bump() {
if [ "$1" == "major" ] || [ "$1" == "minor" ] || [ "$1" == "patch" ]; then
current_version=$(cat package.json | jq -r '.version')
IFS='.' read -a version_parts <<< "$current_version"
major=${version_parts[0]}
minor=${version_parts[1]}
patch=${version_parts[2]}
case "$1" in
"major")
major=$((major + 1))
minor=0
patch=0
;;
"minor")
minor=$((minor + 1))
patch=0
;;
"patch")
patch=$((patch + 1))
;;
esac
new_version="$major.$minor.$patch"
echo "New Version is $new_version"
cat package.json | jq ".version = \"$new_version\"" > temp.json
mv temp.json package.json
cat package-lock.json | jq ".version = \"$new_version\"" > temp.json
mv temp.json package-lock.json
else
echo >&2 "No patch type set. Aborting."
fi
}
echo "Bumping version ... "
bump $1

View File

@ -3424,10 +3424,10 @@
node-addon-api "^3.2.1" node-addon-api "^3.2.1"
node-gyp-build "^4.3.0" node-gyp-build "^4.3.0"
"@pdftron/webviewer@8.11.0": "@pdftron/webviewer@10.1.0":
version "8.11.0" version "10.1.0"
resolved "https://registry.yarnpkg.com/@pdftron/webviewer/-/webviewer-8.11.0.tgz#11f948f93bcb701c5d32357d650a17455bfa14b7" resolved "https://registry.yarnpkg.com/@pdftron/webviewer/-/webviewer-10.1.0.tgz#51bdcc91185629223340600efa400150b1f0132a"
integrity sha512-TWtvSDAvig/7IQq9gqn1SSYL0cpWSfWo5gEsqmNqRc/qUEEsuZUqpSDXi5zQFX0xr5wWnLSQvrnGKK0iJdKWPg== integrity sha512-ZGpVO02qfM9u/kAZpG5io6xHiwhz4IKWIsUYX+HgPbotaQpeLYJjZjUni/99UPQCMOVVkC2mNZcIlOlJWKK3UQ==
"@phenomnomnominal/tsquery@4.1.1": "@phenomnomnominal/tsquery@4.1.1":
version "4.1.1" version "4.1.1"