Merge branch 'master' into feature/RED-10115

# Conflicts:
#	apps/red-ui/src/app/modules/file-preview/dialogs/edit-redaction-dialog/edit-redaction-dialog.component.ts
This commit is contained in:
maverickstuder 2024-11-15 14:06:43 +01:00
commit ecc0f6d8c7
65 changed files with 793 additions and 353 deletions

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -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<Dictionary> implements OnInit {

View File

@ -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<IRules> {
export class RulesService extends EntitiesService<IRules, Rules> {
readonly currentTemplateRules = toSignal(
this.all$.pipe(
filter(all => !!all.length),
map(rules => rules[0]),
distinctUntilChanged(
(prev, curr) =>
prev.rules === curr.rules &&
prev.timeoutDetected === curr.timeoutDetected &&
prev.dossierTemplateId === curr.dossierTemplateId,
),
),
);
protected readonly _defaultModelPath = 'rules';
download(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') {
@ -17,6 +31,6 @@ export class RulesService extends GenericService<IRules> {
}
getFor<R = IRules>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {
return super.getFor(entityId, queryParams);
return super.getFor<R>(entityId, queryParams).pipe(tap(rules => this.setEntities([rules as Rules])));
}
}

View File

@ -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)),

View File

@ -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'"
></iqser-progress-bar>
</div>

View File

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

View File

@ -21,7 +21,9 @@ import {
DossierAttributeWithValue,
DossierStats,
File,
FileErrorCodes,
IDossierRequest,
PendingTypes,
ProcessingTypes,
StatusSorter,
User,
@ -74,6 +76,7 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
#currentChartSubtitleIndex = 0;
readonly #dossierId = getParam(DOSSIER_ID);
protected readonly circleButtonTypes = CircleButtonTypes;
protected readonly PendingTypes = PendingTypes;
@Input() dossierAttributes: DossierAttributeWithValue[];
@Output() readonly toggleCollapse = new EventEmitter();
editingOwner = false;
@ -153,6 +156,9 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
}
#calculateStatusConfig(stats: DossierStats): ProgressBarConfigModel[] {
const files = this._filesMapService.get(this.#dossierId);
const numberOfRulesLockedFiles = files.filter(file => 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<DossierDetailsCont
count: stats.processingStats.pending,
icon: 'red:reanalyse',
},
{
id: PendingTypes.lockedRules,
label: _('processing-status.pending-locked-rules'),
total: stats.numberOfFiles,
count: numberOfRulesLockedFiles,
icon: 'red:reanalyse',
},
{
id: PendingTypes.timeout,
label: _('processing-status.pending-timeout'),
total: stats.numberOfFiles,
count: numberOfTimeoutFiles,
icon: 'red:reanalyse',
},
{
id: ProcessingTypes.ocr,
label: _('processing-status.ocr'),

View File

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

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, EventEmitter, input, OnInit, Output } from '@angular/core';
import {
ActionConfig,
CircleButtonComponent,
@ -25,12 +25,12 @@ import { Router } from '@angular/router';
import { Roles } from '@users/roles';
import { SortingService } from '@iqser/common-ui/lib/sorting';
import { List, some } from '@iqser/common-ui/lib/utils';
import { ComponentLogService } from '@services/files/component-log.service';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FileDownloadBtnComponent } from '@shared/components/buttons/file-download-btn/file-download-btn.component';
import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-selection.component';
import { RulesService } from '../../../admin/services/rules.service';
@Component({
selector: 'redaction-dossier-overview-screen-header [dossier] [upload]',
@ -50,9 +50,10 @@ import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-sel
MatMenu,
MatMenuItem,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierOverviewScreenHeaderComponent implements OnInit {
@Input() dossier: Dossier;
readonly dossier = input<Dossier>();
@Output() readonly upload = new EventEmitter<void>();
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
@ -60,6 +61,9 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
readonly downloadFilesDisabled$: Observable<boolean>;
readonly downloadComponentLogsDisabled$: Observable<boolean>;
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'];

View File

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

View File

@ -24,6 +24,7 @@ import {
FileAttributeConfigType,
FileAttributeConfigTypes,
IFileAttributeConfig,
PendingType,
ProcessingType,
StatusSorter,
User,
@ -184,6 +185,7 @@ export class ConfigService {
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctProcessingTypes = new Set<ProcessingType>();
const allDistinctPendingTypes = new Set<PendingType>();
const dynamicFilters = new Map<string, { type: FileAttributeConfigType; filterValue: Set<string> }>();
@ -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<string>; type: FileAttributeConfigType }, filterKey: string) => {
const id = filterKey.split(':')[0];
const key = filterKey.split(':')[1];

View File

@ -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<Fil
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => 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]))),
);
}

View File

@ -113,11 +113,13 @@ export class BulkActionsService {
async approve(files: File[]): Promise<void> {
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)

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
HostListener,
OnDestroy,
OnInit,
signal,
TemplateRef,
untracked,
viewChild,
@ -107,8 +108,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
const file = this.state.file();
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
});
protected displayedAnnotations = new Map<number, AnnotationWrapper[]>();
readonly activeAnnotations = computed(() => this.displayedAnnotations.get(this.pdf.currentPage()) || []);
protected displayedAnnotations = signal(new Map<number, AnnotationWrapper[]>());
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;

View File

@ -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 {

View File

@ -104,6 +104,14 @@
>
</iqser-icon-button>
<iqser-icon-button
(action)="saveAndRemember()"
[disabled]="disabled"
[label]="'add-hint.dialog.actions.save-and-remember' | translate"
[submit]="true"
[type]="iconButtonTypes.dark"
/>
<div class="all-caps-label cancel" mat-dialog-close [translate]="'add-hint.dialog.actions.cancel'"></div>
</div>
</form>

View File

@ -189,6 +189,15 @@ export class AddHintDialogComponent extends IqserDialogComponent<AddHintDialogCo
});
}
async saveAndRemember() {
const option = this.form.controls.option?.value;
await this._userPreferences.saveAddHintDefaultOption(option?.value ?? SystemDefaults.ADD_HINT_DEFAULT);
if (option?.additionalCheck) {
await this._userPreferences.saveAddHintDefaultExtraOption(option.additionalCheck.checked);
}
this.save();
}
#setDictionaries() {
this.dictionaries = this._dictionaryService.getAddHintDictionaries(
this.#dossier.dossierId,

View File

@ -215,11 +215,7 @@ export class EditRedactionDialogComponent
const value = this.form.value;
const initialReason: LegalBasisOption = this.initialFormValue.reason;
const initialLegalBasis = initialReason?.technicalName ?? '';
const pageNumbers = parseSelectedPageNumbers(
this.form.get('option').value?.additionalInput?.value,
this.data.file,
this.data.annotations[0],
);
const pageNumbers = parseSelectedPageNumbers(this.form.get('option').value?.additionalInput?.value, this.data.file);
const position = parseRectanglePosition(this.annotations[0]);
this.close({

View File

@ -126,6 +126,13 @@
[type]="iconButtonTypes.primary"
/>
<iqser-icon-button
(action)="saveAndRemember()"
[disabled]="!form.valid"
[label]="'redact-text.dialog.actions.save-and-remember' | translate"
[type]="iconButtonTypes.dark"
/>
<div [translate]="'redact-text.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
</div>
</form>

View File

@ -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) {

View File

@ -42,6 +42,16 @@
>
</iqser-icon-button>
@if (!allRectangles) {
<iqser-icon-button
(action)="saveAndRemember()"
[disabled]="disabled"
[label]="'remove-redaction.dialog.actions.save-and-remember' | translate"
[submit]="true"
[type]="iconButtonTypes.dark"
/>
}
<div [translate]="'remove-redaction.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
<iqser-help-button *deny="roles.getRss"></iqser-help-button>

View File

@ -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<RectangleRedactOption | RemoveRedactionOption>[] = this.#allRectangles
? getRectangleRedactOptions('remove')
readonly options: DetailsRadioOption<RectangleRedactOption | RemoveRedactionOption>[] = 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<RectangleRedactOption | RemoveRedactionOption> {
return this.options.find(o => o.value === option);
}

View File

@ -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<IRemoveRedactionRequest>).length > maximumNumberEntries) {
const requests = body as List<IRemoveRedactionRequest>;
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<IRemoveRedactionRequest> | IBulkLocalRemoveRequest {
): List<IRemoveRedactionRequest | IBulkLocalRemoveRequest> {
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 => ({

View File

@ -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),
);
}

View File

@ -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]);
}
});

View File

@ -102,19 +102,25 @@ export const getRedactOrHintOptions = (
return options;
};
export const getRectangleRedactOptions = (action: 'add' | 'edit' | 'remove' = 'add'): DetailsRadioOption<RectangleRedactOption>[] => {
export const getRectangleRedactOptions = (
action: 'add' | 'edit' | 'remove' = 'add',
data?: RemoveRedactionData,
): DetailsRadioOption<RectangleRedactOption>[] => {
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: {

View File

@ -169,7 +169,7 @@ export interface RemoveRedactionResult {
applyToAllDossiers?: boolean;
bulkLocal?: boolean;
pageNumbers?: number[];
position: IEntityLogEntryPosition;
positions: IEntityLogEntryPosition[];
}
export type RemoveAnnotationResult = RemoveRedactionResult;

View File

@ -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 [];
}

View File

@ -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;
}

View File

@ -13,8 +13,7 @@ import Annotation = Core.Annotations.Annotation;
export class ReadableRedactionsService {
readonly active$: Observable<boolean>;
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<boolean>(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');
}
}
}

View File

@ -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 {

View File

@ -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);
}

View File

@ -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'),

View File

@ -54,8 +54,8 @@
[currentDossierTemplateId]="dossier.dossierTemplateId"
[hint]="selectedDictionary.hint"
[initialEntries]="entriesToDisplay || []"
[selectedDictionaryTypeLabel]="selectedDictionary.label"
[selectedDictionaryType]="selectedDictionary.type"
[activeDictionary]="selectedDictionary"
[withFloatingActions]="false"
>
<ng-container slot="typeSwitch">

View File

@ -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,

View File

@ -24,8 +24,8 @@ export class FileDownloadBtnComponent implements OnChanges {
readonly tooltipPosition = input<'above' | 'below' | 'before' | 'after'>('above');
readonly type = input<CircleButtonType>(CircleButtonTypes.default);
readonly tooltipClass = input<string>();
readonly disabled = input<boolean>(false);
readonly singleFileDownload = input<boolean>(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;

View File

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

View File

@ -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<string[]>();
export class DictionaryManagerComponent implements OnInit {
readonly type = input<DictionaryType>('dictionary');
readonly entityType = input<string>();
readonly currentDossierId = input<string>();
readonly currentDossierTemplateId = model<string>();
readonly withFloatingActions = input(true, { transform: booleanAttribute });
readonly initialEntries = input.required<List>();
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<Dictionary>();
readonly selectedDictionaryType = model<string>('dossier_redaction');
readonly activeEntryType = input<DictionaryEntryType>(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);
}

View File

@ -234,9 +234,12 @@ export class FileUploadService extends GenericService<IFileUploadResult> 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 = {

View File

@ -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<Dossier> implements OnDestroy {
@ -19,40 +18,39 @@ export class DossiersChangesService extends GenericService<Dossier> 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<IDossierChanges> {
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<IChangesDetails> {
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<string>();
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<IDossierChanges> {
hasChangesDetails$(): Observable<IChangesDetails> {
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<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.length > 0),
return this._post<IChangesDetails>(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<Dossier> implements O
this.#subscription.unsubscribe();
}
#load(id: string): Observable<DossierStats[]> {
const queryParams: List<QueryParam> = [{ 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<Record<string, Dossier>>({ value: ids }, `${this._defaultModelPath}/by-id`);
}
#load(ids: string[]): Observable<Dossier[]> {
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;
}),
);
}

View File

@ -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<IDossier, Dossier>
protected readonly _toaster = inject(Toaster);
protected readonly _entityClass = Dossier;
protected abstract readonly _defaultModelPath: string;
readonly dossierFileChanges$ = new Subject<string>();
readonly dossierFileChanges$ = new Subject<DossierFileChanges>();
abstract readonly routerPath: string;
createOrUpdate(dossier: IDossierRequest): Observable<Dossier> {
@ -52,7 +52,18 @@ export abstract class DossiersService extends EntitiesService<IDossier, Dossier>
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);
}
}

View File

@ -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<IFile, File> {
super();
}
loadByIds(dossierFileChanges: DossierFileChanges) {
const filesByDossier$ = super
._post<{ value: Record<string, IFile[]> }>({ value: dossierFileChanges }, `${this._defaultModelPath}/by-id`)
.pipe(
map(response => {
const filesByDossier = response.value;
const result: Record<string, File[]> = {};
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)),

View File

@ -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]);
}
}
}

View File

@ -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'),
};

View File

@ -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": {

View File

@ -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": {

View File

@ -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.",

View File

@ -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",

View File

@ -6,7 +6,7 @@
#redaction-preview-svg.st0 {
fill-rule: evenodd;
clip-rule: evenodd;
fill: #868E96;
fill: #000;
}
</style>

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 747 B

View File

@ -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<ProcessingType, number>;

View File

@ -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;

View File

@ -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<string, string[]>;

View File

@ -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<IFile> 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<IFile> 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<IFile> 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);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -13,3 +13,4 @@ export * from './app-config';
export * from './system-preferences';
export * from './component-rules';
export * from './editor.types';
export * from './rules.model';

View File

@ -0,0 +1,23 @@
import { IRules } from '@red/domain';
import { Entity } from '@iqser/common-ui';
export class Rules extends Entity<IRules> 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;
}
}

View File

@ -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
*/