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:
commit
ecc0f6d8c7
@ -258,6 +258,7 @@ export const appModuleFactory = (config: AppConfig) => {
|
||||
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
disableTooltipInteractivity: true,
|
||||
showDelay: 1,
|
||||
},
|
||||
},
|
||||
BaseDatePipe,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
[routerLink]="dict.routerLink"
|
||||
[tooltip]="'entities-listing.action.edit' | translate"
|
||||
icon="iqser:edit"
|
||||
iqserStopPropagation
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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])));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -45,3 +45,7 @@
|
||||
iqser-progress-bar:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.indent {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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'"
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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]))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
getConfig,
|
||||
HelpModeService,
|
||||
IqserAllowDirective,
|
||||
IqserDialog,
|
||||
IqserPermissionsService,
|
||||
isIqserDevMode,
|
||||
LoadingService,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 => ({
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -169,7 +169,7 @@ export interface RemoveRedactionResult {
|
||||
applyToAllDossiers?: boolean;
|
||||
bulkLocal?: boolean;
|
||||
pageNumbers?: number[];
|
||||
position: IEntityLogEntryPosition;
|
||||
positions: IEntityLogEntryPosition[];
|
||||
}
|
||||
|
||||
export type RemoveAnnotationResult = RemoveRedactionResult;
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
};
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 |
@ -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>;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[]>;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -13,3 +13,4 @@ export * from './app-config';
|
||||
export * from './system-preferences';
|
||||
export * from './component-rules';
|
||||
export * from './editor.types';
|
||||
export * from './rules.model';
|
||||
|
||||
23
libs/red-domain/src/lib/shared/rules.model.ts
Normal file
23
libs/red-domain/src/lib/shared/rules.model.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user