diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 52a8534ed..82d09377f 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -258,6 +258,7 @@ export const appModuleFactory = (config: AppConfig) => { provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { disableTooltipInteractivity: true, + showDelay: 1, }, }, BaseDatePipe, diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index 830177bb0..de9d41047 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -84,7 +84,10 @@ export class AnnotationWrapper implements IListable { } get isRedactedImageHint() { - return this.IMAGE_HINT && this.superType === SuperTypes.Redaction; + return ( + (this.IMAGE_HINT && this.superType === SuperTypes.Redaction) || + (this.IMAGE_HINT && this.superType === SuperTypes.ManualRedaction) + ); } get isSkippedImageHint() { diff --git a/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html index 6335575a8..52e91a811 100644 --- a/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html +++ b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html @@ -8,10 +8,11 @@
-
-
-
- + @if (!isWarningsScreen) { +
+
+
+ }
diff --git a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html index 6c7e7b9aa..bb2764e20 100644 --- a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html @@ -16,14 +16,16 @@ -
+
{{ languageSelectLabel() | translate }} - - {{ translations[language] | translate }} - + @for (language of languages; track language) { + + {{ translations[language] | translate }} + + }
@@ -32,11 +34,13 @@ {{ 'user-profile-screen.actions.change-password' | translate }}
-
- - {{ 'user-profile-screen.form.dark-theme' | translate }} - -
+ @if (devMode) { +
+ + {{ 'user-profile-screen.form.dark-theme' | translate }} + +
+ } diff --git a/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.html index 208e2504f..c6503778c 100644 --- a/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.html @@ -87,6 +87,7 @@ [routerLink]="dict.routerLink" [tooltip]="'entities-listing.action.edit' | translate" icon="iqser:edit" + iqserStopPropagation > diff --git a/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.ts index 43c425a5a..9ff6a1d6b 100644 --- a/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/entities-listing/entities-listing-screen.component.ts @@ -11,6 +11,7 @@ import { ListingComponent, listingProvidersFactory, LoadingService, + StopPropagationDirective, TableColumnConfig, } from '@iqser/common-ui'; import { getParam } from '@iqser/common-ui/lib/utils'; @@ -41,6 +42,7 @@ import { AdminDialogService } from '../../services/admin-dialog.service'; AnnotationIconComponent, AsyncPipe, RouterLink, + StopPropagationDirective, ], }) export class EntitiesListingScreenComponent extends ListingComponent implements OnInit { diff --git a/apps/red-ui/src/app/modules/admin/services/rules.service.ts b/apps/red-ui/src/app/modules/admin/services/rules.service.ts index bb89c2ad8..09c20cc03 100644 --- a/apps/red-ui/src/app/modules/admin/services/rules.service.ts +++ b/apps/red-ui/src/app/modules/admin/services/rules.service.ts @@ -1,11 +1,25 @@ import { Injectable } from '@angular/core'; -import { GenericService, QueryParam } from '@iqser/common-ui'; -import { IRules } from '@red/domain'; -import { Observable } from 'rxjs'; +import { EntitiesService, QueryParam } from '@iqser/common-ui'; +import { IRules, Rules } from '@red/domain'; +import { map, Observable, tap } from 'rxjs'; import { List } from '@common-ui/utils'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { distinctUntilChanged, filter } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) -export class RulesService extends GenericService { +export class RulesService extends EntitiesService { + readonly currentTemplateRules = toSignal( + this.all$.pipe( + filter(all => !!all.length), + map(rules => rules[0]), + distinctUntilChanged( + (prev, curr) => + prev.rules === curr.rules && + prev.timeoutDetected === curr.timeoutDetected && + prev.dossierTemplateId === curr.dossierTemplateId, + ), + ), + ); protected readonly _defaultModelPath = 'rules'; download(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') { @@ -17,6 +31,6 @@ export class RulesService extends GenericService { } getFor(entityId: string, queryParams?: List): Observable { - return super.getFor(entityId, queryParams); + return super.getFor(entityId, queryParams).pipe(tap(rules => this.setEntities([rules as Rules]))); } } diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.ts index 52fcb2097..ee0d2ca94 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges } from '@angular/core'; +import { Component, computed, Input, OnChanges } from '@angular/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { CircleButtonType, CircleButtonTypes } from '@iqser/common-ui'; import { Action, ActionTypes, Dossier, File, ProcessingFileStatuses } from '@red/domain'; @@ -9,6 +9,7 @@ import { BulkActionsService } from '../../services/bulk-actions.service'; import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component'; import { IqserTooltipPositions } from '@common-ui/utils'; import { NgIf } from '@angular/common'; +import { RulesService } from '../../../admin/services/rules.service'; @Component({ selector: 'redaction-dossier-overview-bulk-actions [dossier] [selectedFiles]', @@ -43,11 +44,13 @@ export class DossierOverviewBulkActionsComponent implements OnChanges { @Input() maxWidth: number; buttons: Action[]; readonly IqserTooltipPositions = IqserTooltipPositions; + readonly areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected); constructor( private readonly _permissionsService: PermissionsService, private readonly _userPreferenceService: UserPreferenceService, private readonly _bulkActionsService: BulkActionsService, + private readonly _rulesService: RulesService, ) {} private get _buttons(): Action[] { @@ -136,8 +139,9 @@ export class DossierOverviewBulkActionsComponent implements OnChanges { id: 'reanalyse-files-btn', type: ActionTypes.circleBtn, action: () => this._bulkActionsService.reanalyse(this.selectedFiles), - tooltip: _('dossier-overview.bulk.reanalyse'), + tooltip: this.areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.bulk.reanalyse'), icon: 'iqser:refresh', + disabled: this.areRulesLocked(), show: this.#canReanalyse && (this.#analysisForced || this.#canEnableAutoAnalysis || this.selectedFiles.every(file => file.isError)), diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html index 09ba451ce..3670a7aff 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html @@ -61,7 +61,8 @@ *ngFor="let config of statusConfig" [attr.help-mode-key]="'dashboard_in_dossier'" [config]="config" - filterKey="processingTypeFilters" + [class.indent]="!!PendingTypes[config.id]" + [filterKey]="PendingTypes[config.id] ? 'pendingTypeFilters' : 'processingTypeFilters'" > diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.scss b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.scss index e415ad1bc..8a1d20f28 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.scss +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.scss @@ -45,3 +45,7 @@ iqser-progress-bar:not(:last-child) { margin-bottom: 10px; } + +.indent { + margin-left: 32px; +} diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts index 75c7cf9d5..40e99726e 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts @@ -21,7 +21,9 @@ import { DossierAttributeWithValue, DossierStats, File, + FileErrorCodes, IDossierRequest, + PendingTypes, ProcessingTypes, StatusSorter, User, @@ -74,6 +76,7 @@ export class DossierDetailsComponent extends ContextComponent file.errorCode === FileErrorCodes.LOCKED_RULES).length; + const numberOfTimeoutFiles = files.filter(file => file.errorCode === FileErrorCodes.RULES_EXECUTION_TIMEOUT).length; return [ { id: ProcessingTypes.pending, @@ -161,6 +167,20 @@ export class DossierDetailsComponent extends ContextComponent (); @Output() readonly upload = new EventEmitter(); readonly circleButtonTypes = CircleButtonTypes; readonly roles = Roles; @@ -60,6 +61,9 @@ export class DossierOverviewScreenHeaderComponent implements OnInit { readonly downloadFilesDisabled$: Observable; readonly downloadComponentLogsDisabled$: Observable; readonly isDocumine = getConfig().IS_DOCUMINE; + readonly areRulesLocked = computed(() => { + return this._rulesService.currentTemplateRules().timeoutDetected; + }); constructor( private readonly _toaster: Toaster, @@ -73,6 +77,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit { private readonly _reanalysisService: ReanalysisService, private readonly _loadingService: LoadingService, private readonly _primaryFileAttributeService: PrimaryFileAttributeService, + private readonly _rulesService: RulesService, ) { const someNotProcessed$ = this.entitiesService.all$.pipe(some(file => !file.lastProcessed)); this.downloadFilesDisabled$ = combineLatest([this.listingService.areSomeSelected$, someNotProcessed$]).pipe( @@ -84,13 +89,13 @@ export class DossierOverviewScreenHeaderComponent implements OnInit { } ngOnInit() { - this.actionConfigs = this.configService.actionConfig(this.dossier.id, this.listingService.areSomeSelected$); + this.actionConfigs = this.configService.actionConfig(this.dossier().id, this.listingService.areSomeSelected$); } async reanalyseDossier() { this._loadingService.start(); try { - await this._reanalysisService.reanalyzeDossier(this.dossier, true); + await this._reanalysisService.reanalyzeDossier(this.dossier(), true); this._toaster.success(_('dossier-overview.reanalyse-dossier.success')); } catch (e) { this._toaster.error(_('dossier-overview.reanalyse-dossier.error')); @@ -101,12 +106,12 @@ export class DossierOverviewScreenHeaderComponent implements OnInit { async downloadDossierAsCSV() { const displayedEntities = await firstValueFrom(this.listingService.displayed$); const entities = this.sortingService.defaultSort(displayedEntities); - const fileName = this.dossier.dossierName + '.export.csv'; + const fileName = this.dossier().dossierName + '.export.csv'; const mapper = (file?: File) => ({ ...file, hasAnnotations: file.hasRedactions, assignee: this._userService.getName(file.assignee) || '-', - primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId), + primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier().dossierTemplateId), }); const documineOnlyFields = ['hasAnnotations']; const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions']; diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html index 3799cc423..5c53ae450 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html @@ -39,7 +39,12 @@
-
+
diff --git a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts index 7ffebb28c..b74de56a6 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts @@ -24,6 +24,7 @@ import { FileAttributeConfigType, FileAttributeConfigTypes, IFileAttributeConfig, + PendingType, ProcessingType, StatusSorter, User, @@ -184,6 +185,7 @@ export class ConfigService { const allDistinctPeople = new Set(); const allDistinctNeedsWork = new Set(); const allDistinctProcessingTypes = new Set(); + const allDistinctPendingTypes = new Set(); const dynamicFilters = new Map }>(); @@ -216,6 +218,7 @@ export class ConfigService { } allDistinctProcessingTypes.add(file.processingType); + allDistinctPendingTypes.add(file.pendingType); // extract values for dynamic filters fileAttributeConfigs.forEach(config => { @@ -317,6 +320,14 @@ export class ConfigService { hide: true, }); + const pendingTypesFilters = [...allDistinctPendingTypes].map(item => new NestedFilter({ id: item, label: item })); + filterGroups.push({ + slug: 'pendingTypeFilters', + filters: pendingTypesFilters, + checker: (file: File, filter: INestedFilter) => file.pendingType === filter.id, + hide: true, + }); + dynamicFilters.forEach((value: { filterValue: Set; type: FileAttributeConfigType }, filterKey: string) => { const id = filterKey.split(':')[0]; const key = filterKey.split(':')[1]; diff --git a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts index 9e6bfb412..8f00fd038 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts @@ -41,7 +41,7 @@ import { Roles } from '@users/roles'; import { UserPreferenceService } from '@users/user-preference.service'; import { convertFiles, Files, handleFileDrop } from '@utils/index'; import { merge, Observable } from 'rxjs'; -import { filter, skip, switchMap, tap } from 'rxjs/operators'; +import { filter, map, skip, switchMap, tap } from 'rxjs/operators'; import { ConfigService } from '../config.service'; import { BulkActionsService } from '../services/bulk-actions.service'; import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service'; @@ -145,8 +145,9 @@ export default class DossierOverviewScreenComponent extends ListingComponent dossierId === this.dossierId && !!this._dossiersCacheService.get(dossierId)), - switchMap(dossierId => this._filesService.loadAll(dossierId)), + map(changes => changes[this.dossierId]), + filter(changes => !!changes && !!this._dossiersCacheService.get(this.dossierId)), + switchMap(changes => this._filesService.loadByIds({ [this.dossierId]: changes }).pipe(map(files => files[this.dossierId]))), ); } diff --git a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts index d0517aef4..cc289af02 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/services/bulk-actions.service.ts @@ -113,11 +113,13 @@ export class BulkActionsService { async approve(files: File[]): Promise { this._loadingService.start(); const approvalResponse: ApproveResponse[] = await this._filesService.getApproveWarnings(files); - this._loadingService.stop(); const hasWarnings = approvalResponse.some(response => response.hasWarnings); if (!hasWarnings) { + await firstValueFrom(this._filesService.loadAll(files[0].dossierId)); + this._loadingService.stop(); return; } + this._loadingService.stop(); const fileWarnings = approvalResponse .filter(response => response.hasWarnings) diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts index c62b29a40..da73d183e 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts @@ -19,7 +19,6 @@ import { getConfig, HelpModeService, IqserAllowDirective, - IqserDialog, IqserPermissionsService, isIqserDevMode, LoadingService, diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html index 47d3dc13b..d202d612c 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html @@ -146,7 +146,7 @@ id="annotations-list" tabindex="1" > - + (); - readonly activeAnnotations = computed(() => this.displayedAnnotations.get(this.pdf.currentPage()) || []); + protected displayedAnnotations = signal(new Map()); + readonly activeAnnotations = computed(() => this.displayedAnnotations().get(this.pdf.currentPage()) || []); protected displayedPages: number[] = []; protected pagesPanelActive = true; protected enabledFilters = []; @@ -362,11 +363,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On if ($event.key === 'ArrowDown') { const nextPage = this.#nextPageWithAnnotations(); - return this.listingService.selectAnnotations(this.displayedAnnotations.get(nextPage)[0]); + return this.listingService.selectAnnotations(this.displayedAnnotations().get(nextPage)[0]); } const prevPage = this.#prevPageWithAnnotations(); - const prevPageAnnotations = this.displayedAnnotations.get(prevPage); + const prevPageAnnotations = this.displayedAnnotations().get(prevPage); return this.listingService.selectAnnotations(getLast(prevPageAnnotations)); } @@ -375,7 +376,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On const pageIdx = this.displayedPages.indexOf(page); const nextPageIdx = pageIdx + 1; const previousPageIdx = pageIdx - 1; - const annotationsOnPage = this.displayedAnnotations.get(page); + const annotationsOnPage = this.displayedAnnotations().get(page); const idx = annotationsOnPage.findIndex(a => a.id === this._firstSelectedAnnotation.id); if ($event.key === 'ArrowDown') { @@ -385,7 +386,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On } else if (nextPageIdx < this.displayedPages.length) { // If not last page for (let i = nextPageIdx; i < this.displayedPages.length; i++) { - const nextPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]); + const nextPageAnnotations = this.displayedAnnotations().get(this.displayedPages[i]); if (nextPageAnnotations) { this.listingService.selectAnnotations(nextPageAnnotations[0]); break; @@ -403,7 +404,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On if (pageIdx) { // If not first page for (let i = previousPageIdx; i >= 0; i--) { - const prevPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]); + const prevPageAnnotations = this.displayedAnnotations().get(this.displayedPages[i]); if (prevPageAnnotations) { this.listingService.selectAnnotations(getLast(prevPageAnnotations)); break; @@ -451,8 +452,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On } } - this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary); - const pagesThatDisplayAnnotations = [...this.displayedAnnotations.keys()]; + this.displayedAnnotations.set(this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary)); + const pagesThatDisplayAnnotations = [...this.displayedAnnotations().keys()]; this.enabledFilters = this.filterService.enabledFlatFilters; if (this.enabledFilters.some(f => f.id === 'pages-without-annotations')) { if (this.enabledFilters.length === 1 && !onlyPageWithAnnotations) { @@ -461,7 +462,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On } else { this.#setDisplayedPages([]); } - this.displayedAnnotations.clear(); + this.displayedAnnotations().clear(); } else if (this.enabledFilters.length || onlyPageWithAnnotations || componentReferenceIds) { this.#setDisplayedPages(pagesThatDisplayAnnotations); } else { @@ -469,7 +470,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On } this.displayedPages.sort((a, b) => a - b); - return this.displayedAnnotations; + return this.displayedAnnotations(); } #selectFirstAnnotationOnCurrentPageIfNecessary() { @@ -521,7 +522,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On const currentPage = untracked(this.pdf.currentPage); let idx = 0; for (const page of this.displayedPages) { - if (page > currentPage && this.displayedAnnotations.get(page)) { + if (page > currentPage && this.displayedAnnotations().get(page)) { break; } ++idx; @@ -534,7 +535,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On let idx = this.displayedPages.length - 1; const reverseDisplayedPages = [...this.displayedPages].reverse(); for (const page of reverseDisplayedPages) { - if (page < currentPage && this.displayedAnnotations.get(page)) { + if (page < currentPage && this.displayedAnnotations().get(page)) { break; } --idx; diff --git a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts index 9c8725e46..1a3d4876a 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts @@ -34,12 +34,14 @@ export class PagesComponent implements AfterViewInit { // TODO: looks like this is not working scrollToLastViewedPage() { const currentPdfPage = this._pdf.currentPage(); - scrollIntoView(document.getElementById(`quick-nav-page-${currentPdfPage}`), { - behavior: 'smooth', - scrollMode: 'if-needed', - block: 'start', - inline: 'start', - }); + const currentElement = document.getElementById(`quick-nav-page-${currentPdfPage}`); + if (currentElement) + scrollIntoView(currentElement, { + behavior: 'smooth', + scrollMode: 'if-needed', + block: 'start', + inline: 'start', + }); } pageSelectedByClick($event: number): void { diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.html b/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.html index 1b8ba9d00..f98e8d31e 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.html +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.html @@ -104,6 +104,14 @@ > + +
diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.ts index 2826b01a4..ecff2af4c 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/add-hint-dialog/add-hint-dialog.component.ts @@ -189,6 +189,15 @@ export class AddHintDialogComponent extends IqserDialogComponent + +
diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/redact-text-dialog/redact-text-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/redact-text-dialog/redact-text-dialog.component.ts index b2fd686ef..e65ef165f 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/redact-text-dialog/redact-text-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/redact-text-dialog/redact-text-dialog.component.ts @@ -191,6 +191,15 @@ export class RedactTextDialogComponent }); } + async saveAndRemember() { + const option = this.form.controls.option?.value; + await this._userPreferences.saveAddRedactionDefaultOption(option?.value ?? SystemDefaults.ADD_REDACTION_DEFAULT); + if (option?.additionalCheck) { + await this._userPreferences.saveAddRedactionDefaultExtraOption(option.additionalCheck.checked); + } + this.save(); + } + toggleEditingSelectedText() { this.isEditingSelectedText = !this.isEditingSelectedText; if (this.isEditingSelectedText) { diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.html b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.html index af6631396..cea51e224 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.html +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.html @@ -42,6 +42,16 @@ >
+ @if (!allRectangles) { + + } +
diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts index 0ffa843bb..87a11092e 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts @@ -38,6 +38,12 @@ import { isJustOne } from '@common-ui/utils'; import { validatePageRange } from '../../utils/form-validators'; import { parseRectanglePosition, parseSelectedPageNumbers, prefillPageRange } from '../../utils/enhance-manual-redaction-request.utils'; +const ANNOTATION_TYPES = { + REDACTION: 'redaction', + HINT: 'hint', + RECOMMENDATION: 'recommendation', +}; + @Component({ templateUrl: './remove-redaction-dialog.component.html', styleUrls: ['./remove-redaction-dialog.component.scss'], @@ -65,7 +71,11 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< readonly iconButtonTypes = IconButtonTypes; readonly recommendation = this.data.redactions.every(redaction => redaction.isRecommendation); readonly hint = this.data.redactions.every(redaction => redaction.isHint); - readonly annotationsType = this.hint ? 'hint' : this.recommendation ? 'recommendation' : 'redaction'; + readonly annotationsType = this.hint + ? ANNOTATION_TYPES.HINT + : this.recommendation + ? ANNOTATION_TYPES.RECOMMENDATION + : ANNOTATION_TYPES.REDACTION; readonly optionByType = { recommendation: { main: this._userPreferences.getRemoveRecommendationDefaultOption(), @@ -94,7 +104,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< extra: false, }, }; - readonly #allRectangles = this.data.redactions.reduce((acc, a) => acc && a.AREA, true); + readonly allRectangles = this.data.redactions.reduce((acc, a) => acc && a.AREA, true); readonly #applyToAllDossiers = this.systemDefaultByType[this.annotationsType].extra; readonly isSystemDefault = this.optionByType[this.annotationsType].main === SystemDefaultOption.SYSTEM_DEFAULT; readonly isExtraOptionSystemDefault = this.optionByType[this.annotationsType].extra === 'undefined'; @@ -102,8 +112,8 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< ? this.systemDefaultByType[this.annotationsType].main : this.optionByType[this.annotationsType].main; readonly extraOptionPreference = stringToBoolean(this.optionByType[this.annotationsType].extra); - readonly options: DetailsRadioOption[] = this.#allRectangles - ? getRectangleRedactOptions('remove') + readonly options: DetailsRadioOption[] = this.allRectangles + ? getRectangleRedactOptions('remove', this.data) : getRemoveRedactionOptions( this.data, this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference, @@ -141,7 +151,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< ) { super(); - if (this.#allRectangles) { + if (this.allRectangles) { prefillPageRange( this.data.redactions[0], this.data.allFileRedactions, @@ -187,17 +197,54 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< save(): void { const optionValue = this.form.controls.option?.value?.value; const optionInputValue = this.form.controls.option?.value?.additionalInput?.value; - const pageNumbers = parseSelectedPageNumbers(optionInputValue, this.data.file, this.data.redactions[0]); - const position = parseRectanglePosition(this.data.redactions[0]); + const pageNumbers = parseSelectedPageNumbers(optionInputValue, this.data.file); + const positions = []; + for (const redaction of this.data.redactions) { + positions.push(parseRectanglePosition(redaction)); + } this.close({ ...this.form.getRawValue(), bulkLocal: optionValue === ResizeOptions.IN_DOCUMENT || optionValue === RectangleRedactOptions.MULTIPLE_PAGES, pageNumbers, - position, + positions, }); } + async saveAndRemember() { + const option = this.form.controls.option?.value; + + switch (this.annotationsType) { + case ANNOTATION_TYPES.REDACTION: + await this._userPreferences.saveRemoveRedactionDefaultOption(option?.value ?? SystemDefaults.REMOVE_REDACTION_DEFAULT); + break; + case ANNOTATION_TYPES.HINT: + await this._userPreferences.saveRemoveHintDefaultOption(option?.value ?? SystemDefaults.REMOVE_HINT_DEFAULT); + break; + case ANNOTATION_TYPES.RECOMMENDATION: + await this._userPreferences.saveRemoveRecommendationDefaultOption( + option?.value ?? SystemDefaults.REMOVE_RECOMMENDATION_DEFAULT, + ); + break; + } + + if (option?.additionalCheck) { + switch (this.annotationsType) { + case ANNOTATION_TYPES.REDACTION: + await this._userPreferences.saveRemoveRedactionDefaultExtraOption(option.additionalCheck.checked); + break; + case ANNOTATION_TYPES.HINT: + await this._userPreferences.saveRemoveHintDefaultExtraOption(option.additionalCheck.checked); + break; + case ANNOTATION_TYPES.RECOMMENDATION: + await this._userPreferences.saveRemoveRecommendationDefaultExtraOption(option.additionalCheck.checked); + break; + } + } + + this.save(); + } + #getOption(option: RemoveRedactionOption): DetailsRadioOption { return this.options.find(o => o.value === option); } diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 7a91628b3..8df5c15c7 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service'; import { getConfig } from '@iqser/common-ui'; -import { List, log } from '@iqser/common-ui/lib/utils'; +import { List } from '@iqser/common-ui/lib/utils'; import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { Core } from '@pdftron/webviewer'; @@ -153,7 +153,7 @@ export class AnnotationActionsService { }); } else { recategorizeBody = { - value: annotations[0].value, + value: result.value ?? annotations[0].value, type: result.type, legalBasis: result.legalBasis, section: result.section, @@ -165,15 +165,13 @@ export class AnnotationActionsService { } await this.#processObsAndEmit( - this._manualRedactionService - .recategorizeRedactions( - recategorizeBody, - dossierId, - file().id, - this.#getChangedFields(annotations, result), - result.option === RedactOrHintOptions.IN_DOCUMENT || !!result.pageNumbers.length, - ) - .pipe(log()), + this._manualRedactionService.recategorizeRedactions( + recategorizeBody, + dossierId, + file().id, + this.#getChangedFields(annotations, result), + result.option === RedactOrHintOptions.IN_DOCUMENT || !!result.pageNumbers.length, + ), ); } @@ -481,6 +479,7 @@ export class AnnotationActionsService { const isHint = redactions.every(r => r.isHint); const { dossierId, fileId } = this._state; const maximumNumberEntries = 100; + const bulkLocal = dialogResult.bulkLocal || !!dialogResult.pageNumbers.length; if (removeFromDictionary && (body as List).length > maximumNumberEntries) { const requests = body as List; const splitNumber = Math.floor(requests.length / maximumNumberEntries); @@ -495,15 +494,28 @@ export class AnnotationActionsService { const promises = []; for (const split of splitRequests) { + promises.push( + firstValueFrom( + this._manualRedactionService.removeRedaction(split, dossierId, fileId, removeFromDictionary, isHint, bulkLocal), + ), + ); + } + Promise.all(promises).finally(() => this._fileDataService.annotationsChanged()); + return; + } + + if (redactions[0].AREA && bulkLocal) { + const promises = []; + for (const request of body) { promises.push( firstValueFrom( this._manualRedactionService.removeRedaction( - split, + request as IBulkLocalRemoveRequest, dossierId, fileId, removeFromDictionary, isHint, - dialogResult.bulkLocal, + bulkLocal, ), ), ); @@ -511,15 +523,9 @@ export class AnnotationActionsService { Promise.all(promises).finally(() => this._fileDataService.annotationsChanged()); return; } + this.#processObsAndEmit( - this._manualRedactionService.removeRedaction( - body, - dossierId, - fileId, - removeFromDictionary, - isHint, - dialogResult.bulkLocal || !!dialogResult.pageNumbers.length, - ), + this._manualRedactionService.removeRedaction(body, dossierId, fileId, removeFromDictionary, isHint, bulkLocal), ).then(); } @@ -579,16 +585,16 @@ export class AnnotationActionsService { #getRemoveRedactionBody( redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult, - ): List | IBulkLocalRemoveRequest { + ): List { if (dialogResult.bulkLocal || !!dialogResult.pageNumbers.length) { const redaction = redactions[0]; - return { + return dialogResult.positions.map(position => ({ value: redaction.value, rectangle: redaction.AREA, pageNumbers: dialogResult.pageNumbers, - position: dialogResult.position, + position: position, comment: dialogResult.comment, - }; + })); } return redactions.map(redaction => ({ diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts index 50b8f579e..ff821733f 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts @@ -128,7 +128,8 @@ export class FilePreviewStateService { get #dossierFilesChange$() { return this._dossiersService.dossierFileChanges$.pipe( - filter(dossierId => dossierId === this.dossierId), + map(changes => changes[this.dossierId]), + filter(fileIds => fileIds && fileIds.length > 0), map(() => true), ); } diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index c0804337c..5bad3d053 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -117,8 +117,10 @@ export class PdfProxyService { effect(() => { if (this._viewModeService.isRedacted()) { + this._viewerHeaderService.disable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]); this._viewerHeaderService.enable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); } else { + this._viewerHeaderService.enable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]); this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); } }); diff --git a/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts b/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts index a512cf90a..bd35ccf4c 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts @@ -102,19 +102,25 @@ export const getRedactOrHintOptions = ( return options; }; -export const getRectangleRedactOptions = (action: 'add' | 'edit' | 'remove' = 'add'): DetailsRadioOption[] => { +export const getRectangleRedactOptions = ( + action: 'add' | 'edit' | 'remove' = 'add', + data?: RemoveRedactionData, +): DetailsRadioOption[] => { const translations = action === 'add' ? rectangleRedactTranslations : action === 'edit' ? editRectangleTranslations : removeRectangleTranslations; + const redactions = data?.redactions ?? []; return [ { label: translations.onlyThisPage.label, description: translations.onlyThisPage.description, + descriptionParams: { length: redactions.length }, icon: PIN_ICON, value: RectangleRedactOptions.ONLY_THIS_PAGE, }, { label: translations.multiplePages.label, description: translations.multiplePages.description, + descriptionParams: { length: redactions.length }, icon: DOCUMENT_ICON, value: RectangleRedactOptions.MULTIPLE_PAGES, additionalInput: { diff --git a/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts b/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts index 7f1b6931b..05b705e84 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts @@ -169,7 +169,7 @@ export interface RemoveRedactionResult { applyToAllDossiers?: boolean; bulkLocal?: boolean; pageNumbers?: number[]; - position: IEntityLogEntryPosition; + positions: IEntityLogEntryPosition[]; } export type RemoveAnnotationResult = RemoveRedactionResult; diff --git a/apps/red-ui/src/app/modules/file-preview/utils/enhance-manual-redaction-request.utils.ts b/apps/red-ui/src/app/modules/file-preview/utils/enhance-manual-redaction-request.utils.ts index e1df8eaa2..842f5a066 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/enhance-manual-redaction-request.utils.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/enhance-manual-redaction-request.utils.ts @@ -49,7 +49,7 @@ export const enhanceManualRedactionRequest = (addRedactionRequest: IAddRedaction addRedactionRequest.addToAllDossiers = data.isApprover && data.dictionaryRequest && data.applyToAllDossiers; }; -export const parseSelectedPageNumbers = (inputValue: string, file: File, annotation: AnnotationWrapper) => { +export const parseSelectedPageNumbers = (inputValue: string, file: File) => { if (!inputValue) { return []; } diff --git a/apps/red-ui/src/app/modules/file-preview/utils/sort-by-page-rotation.utils.ts b/apps/red-ui/src/app/modules/file-preview/utils/sort-by-page-rotation.utils.ts index 4ccce0755..6a18812ac 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/sort-by-page-rotation.utils.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/sort-by-page-rotation.utils.ts @@ -1,6 +1,12 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper'; export const sortTopLeftToBottomRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { + const A = a1.y; + const B = a1.y - a1.height; + const median = (A + B) / 2; + if (median > a2.y - a2.height && median < a2.y) { + return a1.x < a2.x ? -1 : 1; + } if (a1.y > a2.y) { return -1; } @@ -11,6 +17,12 @@ export const sortTopLeftToBottomRight = (a1: AnnotationWrapper, a2: AnnotationWr }; export const sortBottomLeftToTopRight = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { + const A = a1.x; + const B = a1.x + a1.width; + const median = (A + B) / 2; + if (median < a2.x + a2.width && median > a2.x) { + return a1.y < a2.y ? -1 : 1; + } if (a1.x < a2.x) { return -1; } @@ -21,6 +33,12 @@ export const sortBottomLeftToTopRight = (a1: AnnotationWrapper, a2: AnnotationWr }; export const sortBottomRightToTopLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { + const A = a1.y; + const B = a1.y + a1.height; + const median = (A + B) / 2; + if (median < a2.y + a2.height && median > a2.y) { + return a1.x > a2.x ? -1 : 1; + } if (a1.y < a2.y) { return -1; } @@ -31,6 +49,12 @@ export const sortBottomRightToTopLeft = (a1: AnnotationWrapper, a2: AnnotationWr }; export const sortTopRightToBottomLeft = (a1: AnnotationWrapper, a2: AnnotationWrapper): number => { + const A = a1.x; + const B = a1.x - a1.width; + const median = (A + B) / 2; + if (median > a2.x - a2.width && median < a2.x) { + return a1.y > a2.y ? -1 : 1; + } if (a1.x > a2.x) { return -1; } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/readable-redactions.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/readable-redactions.service.ts index 5fa0bcd4f..515c4bf26 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/readable-redactions.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/readable-redactions.service.ts @@ -13,8 +13,7 @@ import Annotation = Core.Annotations.Annotation; export class ReadableRedactionsService { readonly active$: Observable; readonly #convertPath = inject(UI_ROOT_PATH_FN); - readonly #enableIcon = this.#convertPath('/assets/icons/general/redaction-preview.svg'); - readonly #disableIcon = this.#convertPath('/assets/icons/general/redaction-final.svg'); + readonly #icon = this.#convertPath('/assets/icons/general/redaction-preview.svg'); readonly #active$ = new BehaviorSubject(true); constructor( @@ -29,8 +28,8 @@ export class ReadableRedactionsService { return this.#active$.getValue(); } - get toggleReadableRedactionsBtnIcon(): string { - return this.active ? this.#enableIcon : this.#disableIcon; + get icon() { + return this.#icon; } toggleReadableRedactions(): void { @@ -79,11 +78,23 @@ export class ReadableRedactionsService { } updateState() { + this.#updateIconState(); this._pdf.instance.UI.updateElement(HeaderElements.TOGGLE_READABLE_REDACTIONS, { title: this._translateService.instant(_('pdf-viewer.header.toggle-readable-redactions'), { active: this.active, }), - img: this.toggleReadableRedactionsBtnIcon, }); } + + #updateIconState() { + const element = this._pdf.instance.UI.iframeWindow.document.querySelector( + `[data-element=${HeaderElements.TOGGLE_READABLE_REDACTIONS}]`, + ); + if (!element) return; + if (!this.active) { + element.classList.add('active'); + } else { + element.classList.remove('active'); + } + } } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/tooltips.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/tooltips.service.ts index e3a17eeb9..d7a3e00bc 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/tooltips.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/tooltips.service.ts @@ -24,6 +24,7 @@ export class TooltipsService { updateIconState() { const element = this._pdf.instance.UI.iframeWindow.document.querySelector(`[data-element=${HeaderElements.TOGGLE_TOOLTIPS}]`); + if (!element) return; if (this._userPreferenceService.getFilePreviewTooltipsPreference()) { element.classList.add('active'); } else { diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts index 7c7a57eb9..41694ec6f 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts @@ -121,7 +121,7 @@ export class ViewerHeaderService { type: 'actionButton', element: HeaderElements.TOGGLE_READABLE_REDACTIONS, dataElement: HeaderElements.TOGGLE_READABLE_REDACTIONS, - img: this._readableRedactionsService.toggleReadableRedactionsBtnIcon, + img: this._readableRedactionsService.icon, onClick: () => this._ngZone.run(() => this._readableRedactionsService.toggleReadableRedactions()), }; } @@ -289,20 +289,26 @@ export class ViewerHeaderService { ], ]; - header.get('selectToolButton').insertAfter(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON)); + const shouldHideRectangleButton = this.#isEnabled(HeaderElements.SHAPE_TOOL_GROUP_BUTTON) ? 0 : 1; + if (!shouldHideRectangleButton) { + header.get('selectToolButton').insertAfter(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON)); + } else if (header.getItems().includes(this.#buttons.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON))) { + header.get(HeaderElements.SHAPE_TOOL_GROUP_BUTTON).delete(); + } + groups.forEach(group => this.#pushGroup(enabledItems, group)); const loadAllAnnotationsButton = this.#buttons.get(HeaderElements.LOAD_ALL_ANNOTATIONS); - let startButtons = 11 - documineButtons; - let deleteCount = 15 - documineButtons; + let startButtons = 11 - documineButtons - shouldHideRectangleButton; + let deleteCount = 15 - documineButtons - shouldHideRectangleButton; if (this.#isEnabled(HeaderElements.LOAD_ALL_ANNOTATIONS)) { if (!header.getItems().includes(loadAllAnnotationsButton)) { header.get('leftPanelButton').insertAfter(loadAllAnnotationsButton); } - startButtons = 12 - documineButtons; - deleteCount = 16 - documineButtons; - } else { + startButtons = 12 - documineButtons - shouldHideRectangleButton; + deleteCount = 16 - documineButtons - shouldHideRectangleButton; + } else if (header.getItems().includes(loadAllAnnotationsButton)) { header.delete(HeaderElements.LOAD_ALL_ANNOTATIONS); } diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index 760c59c66..9a0eec5a6 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -1,4 +1,15 @@ -import { ChangeDetectorRef, Component, HostBinding, Injector, Input, OnChanges, Optional, signal, ViewChild } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + computed, + HostBinding, + Injector, + Input, + OnChanges, + Optional, + signal, + ViewChild, +} from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; @@ -37,6 +48,7 @@ import { ProcessingIndicatorComponent } from '@shared/components/processing-indi import { StatusBarComponent } from '@common-ui/shared'; import { NgIf, NgTemplateOutlet } from '@angular/common'; import { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component'; +import { RulesService } from '../../../admin/services/rules.service'; @Component({ selector: 'redaction-file-actions', @@ -86,6 +98,7 @@ export class FileActionsComponent implements OnChanges { @ViewChild(ExpandableFileActionsComponent) private readonly _expandableActionsComponent: ExpandableFileActionsComponent; readonly #isDocumine = getConfig().IS_DOCUMINE; + readonly areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected); constructor( private readonly _injector: Injector, @@ -101,6 +114,7 @@ export class FileActionsComponent implements OnChanges { private readonly _activeDossiersService: ActiveDossiersService, private readonly _fileManagementService: FileManagementService, private readonly _userPreferenceService: UserPreferenceService, + private readonly _rulesService: RulesService, readonly fileAttributesService: FileAttributesService, @Optional() private readonly _documentInfoService: DocumentInfoService, @Optional() private readonly _excludedPagesService: ExcludedPagesService, @@ -238,11 +252,10 @@ export class FileActionsComponent implements OnChanges { id: 'btn-reanalyse_file_preview', type: ActionTypes.circleBtn, action: () => this.#reanalyseFile(), - tooltip: _('file-preview.reanalyse-notification'), - tooltipClass: 'small', + tooltip: this.areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('file-preview.reanalyse-notification'), icon: 'iqser:refresh', show: this.showReanalyseFilePreview, - disabled: this.file.isProcessing, + disabled: this.file.isProcessing || this.areRulesLocked(), helpModeKey: 'stop_analysis', }, { @@ -277,9 +290,10 @@ export class FileActionsComponent implements OnChanges { id: 'btn-reanalyse_file', type: ActionTypes.circleBtn, action: () => this.#reanalyseFile(), - tooltip: _('dossier-overview.reanalyse.action'), + tooltip: this.areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.reanalyse.action'), icon: 'iqser:refresh', show: this.showReanalyseDossierOverview, + disabled: this.areRulesLocked(), helpModeKey: 'stop_analysis', }, { @@ -305,10 +319,12 @@ export class FileActionsComponent implements OnChanges { async setFileApproved() { this._loadingService.start(); const approvalResponse: ApproveResponse = (await this._filesService.getApproveWarnings([this.file]))[0]; - this._loadingService.stop(); if (!approvalResponse.hasWarnings) { + await this._filesService.reload(this.file.dossierId, this.file); + this._loadingService.stop(); return; } + this._loadingService.stop(); const data: IConfirmationDialogData = { title: _('confirmation-dialog.approve-file.title'), diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html index 7adbcaea3..7f29746f6 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html @@ -54,8 +54,8 @@ [currentDossierTemplateId]="dossier.dossierTemplateId" [hint]="selectedDictionary.hint" [initialEntries]="entriesToDisplay || []" - [selectedDictionaryTypeLabel]="selectedDictionary.label" [selectedDictionaryType]="selectedDictionary.type" + [activeDictionary]="selectedDictionary" [withFloatingActions]="false" > diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts index 28d5e725f..716d29aca 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts @@ -83,7 +83,7 @@ export class EditDossierDictionaryComponent implements OnInit { try { await this._dictionaryService.saveEntries( this._dictionaryManager.editor.currentEntries, - this._dictionaryManager.initialEntries, + this._dictionaryManager.initialEntries(), this.dossier.dossierTemplateId, this.selectedDictionary.type, this.dossier.id, diff --git a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts index 83b6089e4..1107af8b2 100644 --- a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts @@ -24,8 +24,8 @@ export class FileDownloadBtnComponent implements OnChanges { readonly tooltipPosition = input<'above' | 'below' | 'before' | 'after'>('above'); readonly type = input(CircleButtonTypes.default); readonly tooltipClass = input(); - readonly disabled = input(false); - readonly singleFileDownload = input(false); + readonly disabled = input(false, { transform: booleanAttribute }); + readonly singleFileDownload = input(false, { transform: booleanAttribute }); readonly dossierDownload = input(false, { transform: booleanAttribute }); readonly dropdownButton = computed(() => this.isDocumine && (this.dossierDownload() || this.singleFileDownload())); tooltip: string; diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html index c1429100c..a7794e65d 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html @@ -5,7 +5,7 @@ -
- - - - - {{ dossierTemplate.id ? dossierTemplate.name : (dossierTemplate.name | translate) }} - - - - -
+ @if (dossierTemplates) { +
+ + + @for (dossierTemplate of dossierTemplates; track dossierTemplate) { + @if (!initialDossierTemplateId || dossierTemplate?.id !== selectedDossierTemplate.id) { + + {{ dossierTemplate.id ? dossierTemplate.name : (dossierTemplate.name | translate) }} + + } + } + + +
+ }
- - - - - {{ dossier.dossierName }} - - - - - + @if (initialDossierTemplateId) { + + + @for (dossier of dossiers; track dossier; let index = $index) { + @if (dossier.dossierId !== selectedDossier.dossierId) { + @if (!activeDictionary().dossierDictionaryOnly) { + + {{ dossier.dossierName }} + + @if (index === dossiers.length - 2 && !selectedDossier.dossierId?.includes('template')) { + + } + } @else if (!dossier.dossierId?.includes('template')) { + + {{ dossier.dossierName }} + + } + } + } + + + } - - - - - {{ dictionary.id ? dictionary.label : (dictionary.label | translate) }} - - - - + @if (!initialDossierTemplateId) { + + + @for (dictionary of dictionaries; track dictionary; let index = $index) { + + {{ dictionary.id ? dictionary.label : (dictionary.label | translate) }} + + } + + + }
@@ -94,26 +104,30 @@
-
- - -
+ @if (compare && optionNotSelected) { +
+ + +
+ }
-
- -
-
+ @if (withFloatingActions() && !!editor?.hasChanges && canEdit() && !isLeavingPage()) { +
+ +
+
+ } diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts index 9d97f2001..b041d7a02 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -1,13 +1,14 @@ import { + booleanAttribute, ChangeDetectorRef, Component, - EventEmitter, - Input, - OnChanges, + effect, + input, + model, OnInit, - Output, + output, signal, - SimpleChanges, + untracked, ViewChild, } from '@angular/core'; import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService } from '@iqser/common-ui'; @@ -60,21 +61,21 @@ const HELP_MODE_KEYS = { EditorComponent, ], }) -export class DictionaryManagerComponent implements OnChanges, OnInit { - @Input() type: DictionaryType = 'dictionary'; - @Input() entityType?: string; - @Input() currentDossierId: string; - @Input() currentDossierTemplateId: string; - @Input() withFloatingActions = true; - @Input() initialEntries: List; - @Input() canEdit = false; - @Input() canDownload = false; - @Input() isLeavingPage = false; - @Input() hint = false; - @Input() selectedDictionaryType = 'dossier_redaction'; - @Input() selectedDictionaryTypeLabel: string; - @Input() activeEntryType: DictionaryEntryType = DictionaryEntryTypes.ENTRY; - @Output() readonly saveDictionary = new EventEmitter(); +export class DictionaryManagerComponent implements OnInit { + readonly type = input('dictionary'); + readonly entityType = input(); + readonly currentDossierId = input(); + readonly currentDossierTemplateId = model(); + readonly withFloatingActions = input(true, { transform: booleanAttribute }); + readonly initialEntries = input.required(); + readonly canEdit = input(false, { transform: booleanAttribute }); + readonly canDownload = input(false, { transform: booleanAttribute }); + readonly isLeavingPage = input(false, { transform: booleanAttribute }); + readonly hint = input(false, { transform: booleanAttribute }); + readonly activeDictionary = input(); + readonly selectedDictionaryType = model('dossier_redaction'); + readonly activeEntryType = input(DictionaryEntryTypes.ENTRY); + readonly saveDictionary = output(); @ViewChild(EditorComponent) readonly editor: EditorComponent; readonly iconButtonTypes = IconButtonTypes; dossiers: Dossier[]; @@ -102,7 +103,24 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { private readonly _changeRef: ChangeDetectorRef, private readonly _dossierTemplatesService: DossierTemplatesService, protected readonly _loadingService: LoadingService, - ) {} + ) { + effect(() => { + if (this.activeEntryType() && this.#dossier?.dossierTemplateId && this.selectedDossier?.dossierId) { + this.#onDossierChanged(this.#dossier.dossierTemplateId, this.#dossier.dossierId).then(entries => + this.#updateDiffEditorText(entries), + ); + } + }); + effect( + () => { + if (this.selectedDictionaryType()) { + this.#disableDiffEditor(); + this.#updateDropdownsOptions(); + } + }, + { allowSignalWrites: true }, + ); + } get selectedDossierTemplate() { return this.#dossierTemplate; @@ -115,12 +133,12 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { : this.selectDossierTemplate; } this.#dossierTemplate = value; - this.currentDossierTemplateId = value.dossierTemplateId; + this.currentDossierTemplateId.set(value.dossierTemplateId); this.#dossier = this.selectDossier; this.dictionaries = this.#dictionaries; this.#disableDiffEditor(); - if (!this.initialDossierTemplateId && !this.currentDossierId) { + if (!this.initialDossierTemplateId && !this.currentDossierId()) { this.selectedDictionary = this.selectDictionary; } @@ -148,7 +166,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { set selectedDictionary(dictionary: Dictionary) { if (dictionary.type) { - this.selectedDictionaryType = dictionary.type; + this.selectedDictionaryType.set(dictionary.type); this.#dictionary = dictionary; this.#onDossierChanged(this.#dossier.dossierTemplateId).then(entries => this.#updateDiffEditorText(entries)); } @@ -181,7 +199,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { get #templatesWithCurrentEntityType() { return this._dossierTemplatesService.all.filter(t => - this._dictionaryService.hasType(t.dossierTemplateId, this.selectedDictionaryType), + this._dictionaryService.hasType(t.dossierTemplateId, untracked(this.selectedDictionaryType)), ); } @@ -190,7 +208,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { this.dossierTemplates = this._dossierTemplatesService.all; await firstValueFrom(this._dictionaryService.loadDictionaryDataForDossierTemplates(this.dossierTemplates.map(t => t.id))); this.#dossierTemplate = this._dossierTemplatesService.all[0]; - this.initialDossierTemplateId = this.currentDossierTemplateId; + this.initialDossierTemplateId = this.currentDossierTemplateId(); this.#updateDropdownsOptions(); } @@ -199,7 +217,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { const blob = new Blob([content], { type: 'text/plain;charset=utf-8', }); - saveAs(blob, `${this.entityType}-${this.type}.txt`); + saveAs(blob, `${this.entityType()}-${this.type()}.txt`); } revert() { @@ -213,67 +231,58 @@ export class DictionaryManagerComponent implements OnChanges, OnInit { } } - ngOnChanges(changes: SimpleChanges): void { - if (changes.activeEntryType && this.#dossier?.dossierTemplateId && this.selectedDossier?.dossierId) { - this.#onDossierChanged(this.#dossier.dossierTemplateId, this.#dossier.dossierId).then(entries => - this.#updateDiffEditorText(entries), - ); - } - - if (changes.selectedDictionaryType) { - this.#disableDiffEditor(); - this.#updateDropdownsOptions(); - } - } - async #onDossierChanged(dossierTemplateId: string, dossierId?: string) { + const selectedDictionaryByType = untracked(this.selectedDictionaryType); + const activeEntryType = untracked(this.activeEntryType); let dictionary: IDictionary; if (dossierId === 'template') { - dictionary = await this._dictionaryService.getForType(dossierTemplateId, this.selectedDictionaryType); + dictionary = await this._dictionaryService.getForType(dossierTemplateId, selectedDictionaryByType); } else { if (dossierId) { dictionary = ( await firstValueFrom( - this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId), + this._dictionaryService.loadDictionaryEntriesByType([selectedDictionaryByType], dossierTemplateId, dossierId), ).catch(() => { return [{ entries: [COMPARE_ENTRIES_ERROR], type: '' }]; }) )[0]; } else { - dictionary = this.selectedDictionaryType - ? await this._dictionaryService.getForType(this.currentDossierTemplateId, this.selectedDictionaryType) + dictionary = selectedDictionaryByType + ? await this._dictionaryService.getForType(this.currentDossierTemplateId(), selectedDictionaryByType) : { entries: [COMPARE_ENTRIES_ERROR], type: '' }; } } const activeEntries = - this.activeEntryType === DictionaryEntryTypes.ENTRY || this.hint + activeEntryType === DictionaryEntryTypes.ENTRY || this.hint() ? [...dictionary.entries] - : this.activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE + : activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE ? [...dictionary.falsePositiveEntries] : [...dictionary.falseRecommendationEntries]; return activeEntries.join('\n'); } #updateDropdownsOptions(updateSelectedDossierTemplate = true) { + const currentDossierTemplateId = untracked(this.currentDossierTemplateId); + const currentDossierId = untracked(this.currentDossierId); if (updateSelectedDossierTemplate) { - this.currentDossierTemplateId = this.initialDossierTemplateId ?? this.currentDossierTemplateId; + this.currentDossierTemplateId.set(this.initialDossierTemplateId ?? currentDossierTemplateId); this.dossierTemplates = this.currentDossierTemplateId ? this.#templatesWithCurrentEntityType : this._dossierTemplatesService.all; if (!this.currentDossierTemplateId) { this.dossierTemplates = [this.selectDossierTemplate, ...this.dossierTemplates]; } - this.selectedDossierTemplate = this.dossierTemplates.find(t => t.id === this.currentDossierTemplateId); + this.selectedDossierTemplate = this.dossierTemplates.find(t => t.id === currentDossierTemplateId); } this.dossiers = this._activeDossiersService.all.filter( - d => d.dossierTemplateId === this.currentDossierTemplateId && d.id !== this.currentDossierId, + d => d.dossierTemplateId === currentDossierTemplateId && d.id !== currentDossierId, ); const templateDictionary = { id: 'template', dossierId: 'template', dossierName: 'Template Dictionary', - dossierTemplateId: this.currentDossierTemplateId, + dossierTemplateId: currentDossierTemplateId, } as Dossier; this.dossiers.push(templateDictionary); } diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index ff626ba01..dca4e4a93 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -234,9 +234,12 @@ export class FileUploadService extends GenericService impleme if (event.status < 300) { uploadFile.progress = 100; uploadFile.completed = true; - if (isCsv(uploadFile) || isZip(uploadFile)) { + if (isCsv(uploadFile)) { this._toaster.success(_('file-upload.type.csv')); } + if (isZip(uploadFile)) { + this._toaster.success(_('file-upload.type.zip')); + } } else { uploadFile.completed = true; uploadFile.error = { diff --git a/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts b/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts index 00a784754..5ba7e9f9b 100644 --- a/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts @@ -1,17 +1,16 @@ -import { GenericService, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui'; -import { Dossier, DossierStats, IDossierChanges } from '@red/domain'; -import { forkJoin, Observable, of, Subscription, throwError, timer } from 'rxjs'; -import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators'; -import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { GenericService, ROOT_CHANGES_KEY } from '@iqser/common-ui'; +import { Dossier, DossierStats, IChangesDetails } from '@red/domain'; +import { forkJoin, Observable, Subscription, timer } from 'rxjs'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { NGXLogger } from 'ngx-logger'; import { ActiveDossiersService } from './active-dossiers.service'; import { ArchivedDossiersService } from './archived-dossiers.service'; import { inject, Injectable, OnDestroy } from '@angular/core'; import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service'; import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; -import { List } from '@iqser/common-ui/lib/utils'; import { Router } from '@angular/router'; import { filterEventsOnPages } from '@utils/operators'; +import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; @Injectable({ providedIn: 'root' }) export class DossiersChangesService extends GenericService implements OnDestroy { @@ -19,40 +18,39 @@ export class DossiersChangesService extends GenericService implements O readonly #activeDossiersService = inject(ActiveDossiersService); readonly #archivedDossiersService = inject(ArchivedDossiersService); readonly #dashboardStatsService = inject(DashboardStatsService); + readonly #dossierStatsService = inject(DossierStatsService); readonly #logger = inject(NGXLogger); readonly #router = inject(Router); protected readonly _defaultModelPath = 'dossier'; - loadOnlyChanged(): Observable { - const removeIfNotFound = (id: string) => - catchError((error: unknown) => { - if (error instanceof HttpErrorResponse && error.status === HttpStatusCode.NotFound) { - this.#activeDossiersService.remove(id); - this.#archivedDossiersService.remove(id); - return of([]); - } - return throwError(() => error); - }); + loadOnlyChanged(): Observable { + const load = (changes: IChangesDetails) => this.#load(changes.dossierChanges.map(d => d.dossierId)); - const load = (changes: IDossierChanges) => - changes.map(change => this.#load(change.dossierId).pipe(removeIfNotFound(change.dossierId))); + const loadStats = (change: IChangesDetails) => { + const dossierStatsToLoad = new Set(); + change.dossierChanges.forEach(dossierChange => dossierStatsToLoad.add(dossierChange.dossierId)); + change.fileChanges.forEach(fileChange => dossierStatsToLoad.add(fileChange.dossierId)); + return this.#dossierStatsService.getFor(Array.from(dossierStatsToLoad)); + }; return this.hasChangesDetails$().pipe( tap(changes => this.#logger.info('[DOSSIERS_CHANGES] Found changes', changes)), switchMap(dossierChanges => - forkJoin([...load(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe(map(() => dossierChanges)), + forkJoin([load(dossierChanges), loadStats(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe( + map(() => dossierChanges), + ), ), ); } - hasChangesDetails$(): Observable { + hasChangesDetails$(): Observable { const body = { value: this._lastCheckedForChanges.get(ROOT_CHANGES_KEY) }; const dateBeforeRequest = new Date().toISOString(); this.#logger.info('[DOSSIERS_CHANGES] Check with Last Checked Date', body.value); - return this._post(body, `${this._defaultModelPath}/changes/details`).pipe( - filter(changes => changes.length > 0), + return this._post(body, `${this._defaultModelPath}/changes/details/v2`).pipe( + filter(changes => changes.dossierChanges.length > 0 || changes.fileChanges.length > 0), tap(() => this._lastCheckedForChanges.set(ROOT_CHANGES_KEY, dateBeforeRequest)), tap(() => this.#logger.info('[DOSSIERS_CHANGES] Save Last Checked Date value', dateBeforeRequest)), ); @@ -75,17 +73,27 @@ export class DossiersChangesService extends GenericService implements O this.#subscription.unsubscribe(); } - #load(id: string): Observable { - const queryParams: List = [{ key: 'includeArchived', value: true }]; - return super._getOne([id], this._defaultModelPath, queryParams).pipe( - map(entity => new Dossier(entity)), - switchMap((dossier: Dossier) => { - if (dossier.isArchived) { - this.#activeDossiersService.remove(dossier.id); - return this.#archivedDossiersService.updateDossier(dossier); - } - this.#archivedDossiersService.remove(dossier.id); - return this.#activeDossiersService.updateDossier(dossier); + getByIds(ids: string[]) { + return super._post>({ value: ids }, `${this._defaultModelPath}/by-id`); + } + + #load(ids: string[]): Observable { + return this.getByIds(ids).pipe( + map(entity => { + return Object.values(entity).map(dossier => new Dossier(dossier)); + }), + map((dossiers: Dossier[]) => { + const archivedDossiers = dossiers.filter(dossier => dossier.isArchived); + const deletedDossiers = dossiers.filter(dossier => dossier.isSoftDeleted); + const activeDossiers = dossiers.filter(dossier => !dossier.isArchived && !dossier.isSoftDeleted); + + archivedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id)); + activeDossiers.forEach(dossier => this.#archivedDossiersService.remove(dossier.id)); + deletedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id)); + + this.#activeDossiersService.updateDossiers(activeDossiers); + this.#archivedDossiersService.updateDossiers(archivedDossiers); + return dossiers; }), ); } diff --git a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts index aea9fda45..bca71afb3 100644 --- a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts @@ -1,5 +1,5 @@ import { EntitiesService, Toaster } from '@iqser/common-ui'; -import { Dossier, DossierStats, IDossier, IDossierChanges, IDossierRequest } from '@red/domain'; +import { Dossier, DossierFileChanges, DossierStats, IChangesDetails, IDossier, IDossierRequest } from '@red/domain'; import { Observable, of, Subject } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { inject } from '@angular/core'; @@ -17,7 +17,7 @@ export abstract class DossiersService extends EntitiesService protected readonly _toaster = inject(Toaster); protected readonly _entityClass = Dossier; protected abstract readonly _defaultModelPath: string; - readonly dossierFileChanges$ = new Subject(); + readonly dossierFileChanges$ = new Subject(); abstract readonly routerPath: string; createOrUpdate(dossier: IDossierRequest): Observable { @@ -52,7 +52,18 @@ export abstract class DossiersService extends EntitiesService return this._dossierStatsService.getFor([dossier.id]); } - emitFileChanges(dossierChanges: IDossierChanges): void { - dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId)); + updateDossiers(dossier: Dossier[]): void { + dossier.forEach(d => this.replace(d)); + } + + emitFileChanges(changes: IChangesDetails): void { + const changeModel: DossierFileChanges = {}; + changes.fileChanges.forEach(change => { + if (!changeModel[change.dossierId]) { + changeModel[change.dossierId] = []; + } + changeModel[change.dossierId].push(change.fileId); + }); + this.dossierFileChanges$.next(changeModel); } } diff --git a/apps/red-ui/src/app/services/files/files.service.ts b/apps/red-ui/src/app/services/files/files.service.ts index 59db98b39..87e8047cc 100644 --- a/apps/red-ui/src/app/services/files/files.service.ts +++ b/apps/red-ui/src/app/services/files/files.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { EntitiesService, isArray, QueryParam } from '@iqser/common-ui'; import { List, mapEach } from '@iqser/common-ui/lib/utils'; -import { ApproveResponse, File, IFile } from '@red/domain'; +import { ApproveResponse, Dossier, DossierFileChanges, File, IFile } from '@red/domain'; import { UserService } from '@users/user.service'; import { NGXLogger } from 'ngx-logger'; import { firstValueFrom } from 'rxjs'; @@ -27,8 +27,35 @@ export class FilesService extends EntitiesService { super(); } + loadByIds(dossierFileChanges: DossierFileChanges) { + const filesByDossier$ = super + ._post<{ value: Record }>({ value: dossierFileChanges }, `${this._defaultModelPath}/by-id`) + .pipe( + map(response => { + const filesByDossier = response.value; + const result: Record = {}; + for (const key of Object.keys(filesByDossier)) { + result[key] = filesByDossier[key].map(file => new File(file, this._userService.getName(file.assignee))); + result[key].forEach(file => this._logger.info('[FILE] Loaded', file)); + } + return result; + }), + ); + return filesByDossier$.pipe( + tap(files => { + for (const key of Object.keys(files)) { + const notDeletedFiles = files[key].filter(file => !file.deleted); + const deletedFiles = files[key].filter(file => file.deleted); + this._filesMapService.replace(key, notDeletedFiles); + deletedFiles.map(file => file.id).forEach(id => this._filesMapService.delete(key, id)); + } + }), + ); + } + /** Reload dossier files + stats. */ loadAll(dossierId: string) { + console.log('loadAll'); const files$ = this.getFor(dossierId).pipe( mapEach(file => new File(file, this._userService.getName(file.assignee))), tap(file => this._logger.info('[FILE] Loaded', file)), diff --git a/apps/red-ui/src/app/services/global-error-handler.service.ts b/apps/red-ui/src/app/services/global-error-handler.service.ts index 9316c8502..dc55a1302 100644 --- a/apps/red-ui/src/app/services/global-error-handler.service.ts +++ b/apps/red-ui/src/app/services/global-error-handler.service.ts @@ -1,7 +1,7 @@ import { ErrorHandler, Inject, Injectable, Injector } from '@angular/core'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { Toaster } from '@iqser/common-ui'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { genericErrorTranslations } from '@translations/generic-error-translations'; @Injectable() export class GlobalErrorHandler extends ErrorHandler { @@ -18,7 +18,7 @@ export class GlobalErrorHandler extends ErrorHandler { if (err.error.message) { toaster.rawError(err.error.message); } else if ([400, 403, 404, 409, 500].includes(err.status)) { - toaster.rawError(_(`generic-errors.${err.status}`)); + toaster.error(genericErrorTranslations[err.status]); } } } diff --git a/apps/red-ui/src/app/translations/generic-error-translations.ts b/apps/red-ui/src/app/translations/generic-error-translations.ts new file mode 100644 index 000000000..efa66d811 --- /dev/null +++ b/apps/red-ui/src/app/translations/generic-error-translations.ts @@ -0,0 +1,9 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +export const genericErrorTranslations: { [key: number]: string } = { + 400: _('generic-errors.400'), + 403: _('generic-errors.403'), + 404: _('generic-errors.404'), + 409: _('generic-errors.409'), + 500: _('generic-errors.500'), +}; diff --git a/apps/red-ui/src/assets/i18n/redact/de.json b/apps/red-ui/src/assets/i18n/redact/de.json index a0c19ea50..d95b11301 100644 --- a/apps/red-ui/src/assets/i18n/redact/de.json +++ b/apps/red-ui/src/assets/i18n/redact/de.json @@ -224,7 +224,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -986,7 +987,7 @@ "download-file-disabled": "Download: Sie müssen Genehmiger im Dossier sein und die initiale Verarbeitung {count, plural, one{der Datei} other{der Dateien}} muss abgeschlossen sein.", "file-listing": { "file-entry": { - "file-error": "Reanalyse erforderlich", + "file-error": "Reanalyse erforderlich {errorCode, select, RULES_EXECUTION_TIMEOUT{(Zeitlimit für Regeln)} LOCKED_RULES{(Regeln gesperrt)} other{}}", "file-pending": "Ausstehend ..." } }, @@ -1462,7 +1463,7 @@ "save": { "error": "Erstellung der Datei-Attribute fehlgeschlagen.", "label": "Attribute speichern", - "success": "{count} Datei-{count, plural, one{Attribut} other{Attribute}} erfolgreich erstellt!" + "success": "{count} Datei-{count, plural, one{Attribut} other{Attribute}} erfolgreich erstellt." }, "search": { "placeholder": "Nach Spaltennamen suchen..." @@ -1631,7 +1632,8 @@ }, "file-upload": { "type": { - "csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert." + "csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert.", + "zip": "" } }, "filter-menu": { @@ -1719,6 +1721,13 @@ }, "title": "SMTP-Konto konfigurieren" }, + "generic-errors": { + "400": "Die gesendete Anfrage ist ungültig.", + "403": "Der Zugriff auf die angeforderte Ressource ist nicht erlaubt.", + "404": "Die angeforderte Ressource konnte nicht gefunden werden.", + "409": "Die Anfrage ist mit dem aktuellen Zustand nicht vereinbar.", + "500": "Der Server ist auf eine unerwartete Bedingung gestoßen, die ihn daran hindert, die Anfrage zu erfüllen." + }, "help-button": { "disable": "Hilfemodus deaktivieren", "enable": "Hilfemodus aktivieren" @@ -2093,6 +2102,8 @@ "processing-status": { "ocr": "OCR", "pending": "Ausstehend", + "pending-locked-rules": "Ausstehend (Regeln gesperrt)", + "pending-timeout": "Ausstehend (Zeitlimit für Regeln)", "processed": "Verarbeitet", "processing": "Verarbeitung läuft" }, @@ -2106,7 +2117,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -2202,7 +2214,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -2222,7 +2235,7 @@ "label": "In diesem Kontext aus Dossier entfernen" }, "in-document": { - "description": "{isImage, select, image{Das Bild} other{Der Begriff}} wird auf keiner Seite dieses Dokuments automatisch geschwärzt.", + "description": "{isImage, select, image{{number, plural, one{Das Bild} other{Die Bilder}}} other{{number, plural, one{Der Begriff} other{Die Begriffe}}}} werden auf keiner Seite dieses Dokuments automatisch geschwärzt.\n", "label": "Aus Dokument entfernen" }, "in-dossier": { @@ -2359,13 +2372,13 @@ }, "roles": { "inactive": "Inaktiv", - "manager-admin": "Manager & Admin", + "manager-admin": "{count, plural, one{Manager & Admin} other{Manager & Admins}}", "no-role": "Keine Rolle definiert", - "red-admin": "Anwendungsadmin", - "red-manager": "Manager", + "red-admin": "{count, plural, one{Anwendungsadmin} other{Anwendungsadmins}}", + "red-manager": "{count, plural, one{Manager} other{Manager}}", "red-user": "Benutzer", - "red-user-admin": "Benutzeradmin", - "regular": "regulärer Benutzer" + "red-user-admin": "{count, plural, one{Benutzeradmin} other{Benutzeradmins}}", + "regular": "{number, plural, one{{regulärer Benutzer}} other{reguläre Benutzer}}" }, "search-screen": { "cols": { diff --git a/apps/red-ui/src/assets/i18n/redact/en.json b/apps/red-ui/src/assets/i18n/redact/en.json index 443fad141..70f700417 100644 --- a/apps/red-ui/src/assets/i18n/redact/en.json +++ b/apps/red-ui/src/assets/i18n/redact/en.json @@ -224,7 +224,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", @@ -986,7 +987,7 @@ "download-file-disabled": "To download, ensure you are an approver in the dossier, and the {count, plural, one{file has undergone} other{files have undergone}} initial processing.", "file-listing": { "file-entry": { - "file-error": "Re-processing required", + "file-error": "Re-processing required {errorCode, select, RULES_EXECUTION_TIMEOUT{(Rules timeout)} LOCKED_RULES{(Rules locked)} other{}}", "file-pending": "Pending..." } }, @@ -1631,7 +1632,8 @@ }, "file-upload": { "type": { - "csv": "File attributes were imported successfully from uploaded CSV file." + "csv": "File attributes were imported successfully from uploaded CSV file.", + "zip": "The zip file has been uploaded successfully!" } }, "filter-menu": { @@ -1719,6 +1721,13 @@ }, "title": "Configure SMTP account" }, + "generic-errors": { + "400": "The sent request is invalid.", + "403": "Access to the requested resource is not allowed.", + "404": "The requested resource could not be found.", + "409": "The request is incompatible with the current state.", + "500": "The server encountered an unexpected condition that prevented it from fulfilling the request." + }, "help-button": { "disable": "Disable help mode", "enable": "Enable help mode" @@ -2093,6 +2102,8 @@ "processing-status": { "ocr": "OCR", "pending": "Pending", + "pending-locked-rules": "Pending (Rules locked)", + "pending-timeout": "Pending (Rules timeout)", "processed": "Processed", "processing": "Processing" }, @@ -2106,7 +2117,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", @@ -2184,14 +2196,14 @@ "content": { "options": { "multiple-pages": { - "description": "Remove redaction on a range of pages", + "description": "Remove {length, plural, one{redaction} other {redactions}} on a range of pages", "extraOptionDescription": "Minus(-) for range and comma(,) for enumeration", "extraOptionLabel": "Pages", "extraOptionPlaceholder": "e.g. 1-20,22,32", "label": "Remove on multiple pages" }, "only-this-page": { - "description": "Remove redaction only at this position in this document", + "description": "Remove {length, plural, one{redaction} other {redactions}} only at this position in this document", "label": "Remove only on this page" } } @@ -2202,7 +2214,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", @@ -2222,7 +2235,7 @@ "label": "Remove from dossier in this context" }, "in-document": { - "description": "Do not auto-redact the selected {isImage, select, image{image} other{term}} on any page of this document.", + "description": "Do not auto-redact the selected {isImage, select, image{{number, plural, one{image} other{images}}} other{{number, plural, one{term} other{terms}}}} on any page of this document.\n", "label": "Remove from document" }, "in-dossier": { @@ -2359,13 +2372,13 @@ }, "roles": { "inactive": "Inactive", - "manager-admin": "Manager & admin", + "manager-admin": "{count, plural, one{Manager & admin} other{Manager & admin}}", "no-role": "No role defined", "red-admin": "Application admin", - "red-manager": "Manager", + "red-manager": "{count, plural, one{Manager} other{Managers}}", "red-user": "User", - "red-user-admin": "Users admin", - "regular": "Regular" + "red-user-admin": "{count, plural, one{User admin} other{User admin}}", + "regular": "{count, plural, one{regular} other{regular}}" }, "search-screen": { "cols": { diff --git a/apps/red-ui/src/assets/i18n/scm/de.json b/apps/red-ui/src/assets/i18n/scm/de.json index e54cbf50a..383dcfc33 100644 --- a/apps/red-ui/src/assets/i18n/scm/de.json +++ b/apps/red-ui/src/assets/i18n/scm/de.json @@ -224,7 +224,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -986,7 +987,7 @@ "download-file-disabled": "Download: Sie müssen Genehmiger im Dossier sein und die initiale Verarbeitung {count, plural, one{der Datei} other{der Dateien}} muss abgeschlossen sein.", "file-listing": { "file-entry": { - "file-error": "Reanalyse erforderlich", + "file-error": "Reanalyse erforderlich {errorCode, select, RULES_EXECUTION_TIMEOUT{(Zeitlimit für Regeln)} LOCKED_RULES{(Regeln gesperrt)} other{}}", "file-pending": "Ausstehend ..." } }, @@ -1384,7 +1385,7 @@ }, "file": { "action": "Zurück zum Dossier", - "label": "Diese Datei wurde gelöscht!" + "label": "Diese Datei wurde gelöscht." } }, "file-preview": { @@ -1631,7 +1632,8 @@ }, "file-upload": { "type": { - "csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert." + "csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert.", + "zip": "" } }, "filter-menu": { @@ -1719,6 +1721,13 @@ }, "title": "SMTP-Konto konfigurieren" }, + "generic-errors": { + "400": "", + "403": "", + "404": "", + "409": "", + "500": "" + }, "help-button": { "disable": "Hilfemodus deaktivieren", "enable": "Hilfemodus aktivieren" @@ -2093,6 +2102,8 @@ "processing-status": { "ocr": "OCR", "pending": "Ausstehend", + "pending-locked-rules": "Ausstehend (Regeln gesperrt)", + "pending-timeout": "Ausstehend (Zeitlimit für Regeln)", "processed": "Verarbeitet", "processing": "Verarbeitung läuft" }, @@ -2106,7 +2117,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -2202,7 +2214,8 @@ "dialog": { "actions": { "cancel": "Abbrechen", - "save": "Speichern" + "save": "Speichern", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Kommentar", @@ -2284,7 +2297,7 @@ } } }, - "invalid-upload": "Ungültiges Upload-Format ausgewählt! Unterstützt werden Dokumente im .xlsx- und im .docx-Format", + "invalid-upload": "Ungültiges Upload-Format ausgewählt. Unterstützte Formate: .xlsx- und .docx", "multi-file-report": "(Mehrere Dateien)", "report-documents": "Berichtsvorlagen", "setup": "Dieser Platzhalter wird durch die Nummer der Seite ersetzt, auf der sich die Schwärzung befindet.", diff --git a/apps/red-ui/src/assets/i18n/scm/en.json b/apps/red-ui/src/assets/i18n/scm/en.json index b2990274c..d781a982a 100644 --- a/apps/red-ui/src/assets/i18n/scm/en.json +++ b/apps/red-ui/src/assets/i18n/scm/en.json @@ -224,7 +224,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", @@ -986,7 +987,7 @@ "download-file-disabled": "To download, ensure you are an approver in the dossier, and the {count, plural, one{file has undergone} other{files have undergone}} initial processing.", "file-listing": { "file-entry": { - "file-error": "Re-processing required", + "file-error": "Re-processing required {errorCode, select, RULES_EXECUTION_TIMEOUT{(Rules timeout)} LOCKED_RULES{(Rules locked)} other{}}", "file-pending": "Pending..." } }, @@ -1631,7 +1632,8 @@ }, "file-upload": { "type": { - "csv": "File attributes were imported successfully from uploaded CSV file." + "csv": "File attributes were imported successfully from uploaded CSV file.", + "zip": "The zip file has been uploaded successfully!" } }, "filter-menu": { @@ -1719,6 +1721,13 @@ }, "title": "Configure SMTP Account" }, + "generic-errors": { + "400": "The sent request is not valid.", + "403": "Access to the requested resource is not allowed.", + "404": "The requested resource could not be found.", + "409": "The request is incompatible with the current state.", + "500": "The server encountered an unexpected condition that prevented it from fulfilling the request." + }, "help-button": { "disable": "Disable help mode", "enable": "Enable help mode" @@ -2093,6 +2102,8 @@ "processing-status": { "ocr": "OCR", "pending": "Pending", + "pending-locked-rules": "Pending (Rules locked)", + "pending-timeout": "Pending (Rules timeout)", "processed": "Processed", "processing": "Processing" }, @@ -2106,7 +2117,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", @@ -2202,7 +2214,8 @@ "dialog": { "actions": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "save-and-remember": "Save and remember my choice" }, "content": { "comment": "Comment", diff --git a/apps/red-ui/src/assets/icons/general/redaction-preview.svg b/apps/red-ui/src/assets/icons/general/redaction-preview.svg index e84ae4550..f5afa6030 100644 --- a/apps/red-ui/src/assets/icons/general/redaction-preview.svg +++ b/apps/red-ui/src/assets/icons/general/redaction-preview.svg @@ -6,7 +6,7 @@ #redaction-preview-svg.st0 { fill-rule: evenodd; clip-rule: evenodd; - fill: #868E96; + fill: #000; } diff --git a/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts index 7a9a2e4ea..61dd2e548 100644 --- a/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts +++ b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts @@ -1,22 +1,6 @@ -import { - isProcessingStatuses, - OCR_STATES, - PENDING_STATES, - PROCESSED_STATES, - PROCESSING_STATES, - ProcessingFileStatus, -} from '../files/types'; +import { isProcessingStatuses, OCR_STATES, PENDING_STATES, PROCESSED_STATES, PROCESSING_STATES, ProcessingFileStatus } from '../files'; import { IDossierStats } from './dossier-stats'; -import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types'; - -export const ProcessingTypes = { - pending: 'pending', - ocr: 'ocr', - processing: 'processing', - processed: 'processed', -} as const; - -export type ProcessingType = keyof typeof ProcessingTypes; +import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus, ProcessingType } from './types'; export type ProcessingStats = Record; diff --git a/libs/red-domain/src/lib/dossier-stats/types.ts b/libs/red-domain/src/lib/dossier-stats/types.ts index b57ca4ecd..23696ad44 100644 --- a/libs/red-domain/src/lib/dossier-stats/types.ts +++ b/libs/red-domain/src/lib/dossier-stats/types.ts @@ -1,4 +1,20 @@ -import { ProcessingFileStatus, WorkflowFileStatus } from '../files/types'; +import { ProcessingFileStatus, WorkflowFileStatus } from '../files'; export type FileCountPerWorkflowStatus = { [key in WorkflowFileStatus]?: number }; export type FileCountPerProcessingStatus = { [key in ProcessingFileStatus]?: number }; + +export const ProcessingTypes = { + pending: 'pending', + ocr: 'ocr', + processing: 'processing', + processed: 'processed', +} as const; + +export type ProcessingType = keyof typeof ProcessingTypes; + +export const PendingTypes = { + lockedRules: 'lockedRules', + timeout: 'timeout', +} as const; + +export type PendingType = keyof typeof PendingTypes; diff --git a/libs/red-domain/src/lib/dossiers/dossier-changes.ts b/libs/red-domain/src/lib/dossiers/dossier-changes.ts index a63406f4c..625671215 100644 --- a/libs/red-domain/src/lib/dossiers/dossier-changes.ts +++ b/libs/red-domain/src/lib/dossiers/dossier-changes.ts @@ -1,11 +1,19 @@ export interface IDossierChange { readonly dossierChanges: boolean; readonly dossierId: string; - readonly fileChanges: boolean; + readonly lastUpdated: string; +} + +export interface IFileChange extends IDossierChange { + readonly fileId: string; } export type IDossierChanges = readonly IDossierChange[]; +export type IFileChanges = readonly IFileChange[]; export interface IChangesDetails { readonly dossierChanges: IDossierChanges; + readonly fileChanges: IFileChanges; } + +export type DossierFileChanges = Record; diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts index 5146f18fb..eae5d8af1 100644 --- a/libs/red-domain/src/lib/files/file.model.ts +++ b/libs/red-domain/src/lib/files/file.model.ts @@ -1,10 +1,12 @@ import { Entity } from '@iqser/common-ui'; -import { ProcessingType, ProcessingTypes } from '../dossier-stats/dossier-stats.model'; -import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '../dossiers/constants'; -import { FileAttributes } from '../file-attributes/file-attributes'; -import { StatusSorter } from '../shared/sorters/status-sorter'; +import { PendingType, PendingTypes, ProcessingType, ProcessingTypes } from '../dossier-stats'; +import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '../dossiers'; +import { FileAttributes } from '../file-attributes'; +import { StatusSorter } from '../shared'; import { IFile } from './file'; import { + FileErrorCode, + FileErrorCodes, isFullProcessingStatuses, isProcessingStatuses, OCR_STATES, @@ -81,6 +83,8 @@ export class File extends Entity implements IFile { readonly canBeOCRed: boolean; readonly processingType: ProcessingType; + readonly errorCode?: FileErrorCode; + readonly pendingType?: PendingType; constructor( file: IFile, @@ -157,6 +161,8 @@ export class File extends Entity implements IFile { file.fileAttributes && file.fileAttributes.attributeIdToValue ? file.fileAttributes : { attributeIdToValue: {} }; this.processingType = this.#processingType; + this.errorCode = this.isError ? file.fileErrorInfo?.errorCode : undefined; + this.pendingType = this.processingType === ProcessingTypes.pending ? this.#pendingType : undefined; } get deleted(): boolean { @@ -189,6 +195,16 @@ export class File extends Entity implements IFile { return ProcessingTypes.processed; } + get #pendingType(): PendingType | undefined { + if (this.errorCode === FileErrorCodes.LOCKED_RULES) { + return PendingTypes.lockedRules; + } + if (this.errorCode === FileErrorCodes.RULES_EXECUTION_TIMEOUT) { + return PendingTypes.timeout; + } + return undefined; + } + isPageExcluded(page: number): boolean { return this.excludedPages.includes(page); } diff --git a/libs/red-domain/src/lib/files/file.ts b/libs/red-domain/src/lib/files/file.ts index 1a06145ea..1c0d3c9fa 100644 --- a/libs/red-domain/src/lib/files/file.ts +++ b/libs/red-domain/src/lib/files/file.ts @@ -2,7 +2,7 @@ * Object containing information on a specific file. */ import { FileAttributes } from '../file-attributes'; -import { ProcessingFileStatus, WorkflowFileStatus } from './types'; +import { FileErrorInfo, ProcessingFileStatus, WorkflowFileStatus } from './types'; export interface IFile { /** @@ -147,4 +147,5 @@ export interface IFile { readonly fileManipulationDate: string | null; readonly redactionModificationDate: string | null; readonly lastManualChangeDate?: string; + readonly fileErrorInfo?: FileErrorInfo; } diff --git a/libs/red-domain/src/lib/files/super-types.ts b/libs/red-domain/src/lib/files/super-types.ts index 93ab235a0..9fae4894a 100644 --- a/libs/red-domain/src/lib/files/super-types.ts +++ b/libs/red-domain/src/lib/files/super-types.ts @@ -27,8 +27,8 @@ function resolveRedactionType(entry: IEntityLogEntry, hint = false) { const redaction = hint ? SuperTypes.Hint : SuperTypes.Redaction; const manualRedaction = hint ? SuperTypes.ManualHint : SuperTypes.ManualRedaction; - - if (!entry.engines.length) { + const isRedactedImageHint = entry.state === EntryStates.APPLIED && entry.entryType === EntityTypes.IMAGE_HINT; + if (!entry.engines.length && !isRedactedImageHint) { return entry.state === EntryStates.PENDING && entry.dictionaryEntry ? redaction : manualRedaction; } return redaction; diff --git a/libs/red-domain/src/lib/files/types.ts b/libs/red-domain/src/lib/files/types.ts index 2a3844918..b189186c6 100644 --- a/libs/red-domain/src/lib/files/types.ts +++ b/libs/red-domain/src/lib/files/types.ts @@ -96,3 +96,18 @@ export const PROCESSING_STATES: ProcessingFileStatus[] = [ export const PROCESSED_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.PROCESSED]; export const OCR_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.OCR_PROCESSING, ProcessingFileStatuses.OCR_PROCESSING_QUEUED]; + +export const FileErrorCodes = { + RULES_EXECUTION_TIMEOUT: 'RULES_EXECUTION_TIMEOUT', + LOCKED_RULES: 'LOCKED_RULES', +} as const; + +export type FileErrorCode = keyof typeof FileErrorCodes; + +export interface FileErrorInfo { + cause: string; + queue: string; + service: string; + timestamp: string; + errorCode?: FileErrorCode; +} diff --git a/libs/red-domain/src/lib/shared/index.ts b/libs/red-domain/src/lib/shared/index.ts index 8406e4be7..7c5c49665 100644 --- a/libs/red-domain/src/lib/shared/index.ts +++ b/libs/red-domain/src/lib/shared/index.ts @@ -13,3 +13,4 @@ export * from './app-config'; export * from './system-preferences'; export * from './component-rules'; export * from './editor.types'; +export * from './rules.model'; diff --git a/libs/red-domain/src/lib/shared/rules.model.ts b/libs/red-domain/src/lib/shared/rules.model.ts new file mode 100644 index 000000000..85b7c2d05 --- /dev/null +++ b/libs/red-domain/src/lib/shared/rules.model.ts @@ -0,0 +1,23 @@ +import { IRules } from '@red/domain'; +import { Entity } from '@iqser/common-ui'; + +export class Rules extends Entity implements IRules { + readonly id: string; + readonly routerLink: string; + readonly searchKey: string; + readonly dossierTemplateId: string; + readonly ruleFileType?: 'ENTITY' | 'COMPONENT'; + readonly rules?: string; + readonly dryRun?: boolean; + readonly timeoutDetected?: boolean; + + constructor(rules: IRules) { + super(rules); + this.id = rules.dossierTemplateId; + this.dossierTemplateId = rules.dossierTemplateId; + this.ruleFileType = rules.ruleFileType; + this.rules = rules.rules; + this.dryRun = rules.dryRun; + this.timeoutDetected = rules.timeoutDetected; + } +} diff --git a/libs/red-domain/src/lib/shared/rules.ts b/libs/red-domain/src/lib/shared/rules.ts index 5a8316b1b..431bb3427 100644 --- a/libs/red-domain/src/lib/shared/rules.ts +++ b/libs/red-domain/src/lib/shared/rules.ts @@ -5,7 +5,7 @@ export interface IRules { /** * The DossierTemplate ID for these rules */ - dossierTemplateId?: string; + dossierTemplateId: string; /** * The file type to be retrieved/saved under, defaults to ENTITY */