diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.scss
index a3ced2806..95e122287 100644
--- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.scss
+++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/components/file-workload/file-workload.component.scss
@@ -1,13 +1,13 @@
@use 'variables';
@use 'common-mixins';
-.read-only {
+.banner {
padding: 13px 16px;
- background-color: variables.$primary;
color: variables.$white;
justify-content: space-between;
- .read-only-text {
+ .read-only-text,
+ .disabled-auto-analysis-text {
font-size: 11px;
font-weight: 600;
line-height: 14px;
@@ -33,8 +33,17 @@
}
}
+.read-only {
+ background-color: variables.$primary;
+}
+
+.disabled-auto-analysis {
+ background-color: variables.$yellow-2;
+}
+
.right-content {
flex-direction: column;
+ overflow: hidden;
.no-annotations-buttons-container {
display: flex;
diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
index cdc3da485..c01c905ed 100644
--- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
+++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
@@ -209,7 +209,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const file = await this.stateService.file;
if (file?.analysisRequired) {
- const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, true);
+ const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, { force: true });
await firstValueFrom(reanalyzeFiles);
}
diff --git a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts
index ae697a33e..e084f178c 100644
--- a/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts
+++ b/apps/red-ui/src/app/modules/dossier/shared/components/file-actions/file-actions.component.ts
@@ -30,7 +30,7 @@ import { FileAssignService } from '../../services/file-assign.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { FilesService } from '@services/entity-services/files.service';
-import { ReanalysisService } from '@services/reanalysis.service';
+import { ReanalysisService, ReanalyzeQueryParams } from '@services/reanalysis.service';
import { Router } from '@angular/router';
import { ExcludedPagesService } from '../../../screens/file-preview-screen/services/excluded-pages.service';
import { tap } from 'rxjs/operators';
@@ -62,6 +62,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
showDelete = false;
showOCR = false;
canReanalyse = false;
+ canDisableAutoAnalysis = false;
+ canEnableAutoAnalysis = false;
showUnderReview = false;
showUnderApproval = false;
showApprove = false;
@@ -182,6 +184,20 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
disabled: !this.file.canBeApproved,
show: this.showApprove,
},
+ {
+ type: ActionTypes.circleBtn,
+ action: $event => this.toggleAutomaticAnalysis($event),
+ tooltip: _('dossier-overview.disable-auto-analysis'),
+ icon: 'red:stop',
+ show: this.canDisableAutoAnalysis,
+ },
+ {
+ type: ActionTypes.circleBtn,
+ action: $event => this.toggleAutomaticAnalysis($event),
+ tooltip: _('dossier-overview.enable-auto-analysis'),
+ icon: 'red:play',
+ show: this.canEnableAutoAnalysis,
+ },
{
type: ActionTypes.circleBtn,
action: $event => this._setFileUnderApproval($event),
@@ -296,10 +312,19 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
}
private async _reanalyseFile($event?: MouseEvent) {
- if ($event) {
- $event.stopPropagation();
- }
- await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file.fileId], this.file.dossierId, true));
+ $event?.stopPropagation();
+ const params: ReanalyzeQueryParams = {
+ force: true,
+ triggeredByUser: true,
+ };
+ await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file.fileId], this.file.dossierId, params));
+ }
+
+ private async toggleAutomaticAnalysis($event: MouseEvent) {
+ $event.stopPropagation();
+ this._loadingService.start();
+ await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file.dossierId, [this.file]));
+ this._loadingService.stop();
}
private async _setFileUnderApproval($event: MouseEvent) {
@@ -345,6 +370,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
this.showDelete = this._permissionsService.canDeleteFile(this.file);
this.showOCR = this.file.canBeOCRed;
this.canReanalyse = this._permissionsService.canReanalyseFile(this.file);
+ this.canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis([this.file]);
+ this.canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis([this.file]);
this.showStatusBar = !this.file.isError && !this.file.isPending && this.isDossierOverviewList;
@@ -353,8 +380,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
(this._permissionsService.canAssignUser(this.file) || this._permissionsService.canUnassignUser(this.file)) &&
this.isDossierOverview;
- this.showReanalyseFilePreview = this.canReanalyse && this.isFilePreview && this.analysisForced;
- this.showReanalyseDossierOverview = this.canReanalyse && this.isDossierOverview && this.analysisForced;
+ this.showReanalyseFilePreview = this.canReanalyse && this.isFilePreview && (this.analysisForced || this.canEnableAutoAnalysis);
+ this.showReanalyseDossierOverview =
+ this.canReanalyse && this.isDossierOverview && (this.analysisForced || this.canEnableAutoAnalysis);
this.buttons = this._buttons;
diff --git a/apps/red-ui/src/app/modules/icons/icons.module.ts b/apps/red-ui/src/app/modules/icons/icons.module.ts
index cf4598843..7d808ccd7 100644
--- a/apps/red-ui/src/app/modules/icons/icons.module.ts
+++ b/apps/red-ui/src/app/modules/icons/icons.module.ts
@@ -24,6 +24,7 @@ export class IconsModule {
'comment-fill',
'csv',
'dictionary',
+ 'denied',
'double-chevron-right',
'enter',
'entries',
@@ -44,6 +45,7 @@ export class IconsModule {
'new-tab',
'notification',
'page',
+ 'play',
'preview',
'put-back',
'read-only',
@@ -59,6 +61,7 @@ export class IconsModule {
'secret',
'status',
'status-info',
+ 'stop',
'template',
'thumb-down',
'thumb-up',
diff --git a/apps/red-ui/src/app/services/entity-services/files-map.service.ts b/apps/red-ui/src/app/services/entity-services/files-map.service.ts
index 07936d006..372fadc42 100644
--- a/apps/red-ui/src/app/services/entity-services/files-map.service.ts
+++ b/apps/red-ui/src/app/services/entity-services/files-map.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
-import { File } from '@red/domain';
+import { File, IFile } from '@red/domain';
import { filter, startWith } from 'rxjs/operators';
import { RequiredParam, shareLast, Validate } from '@iqser/common-ui';
@@ -66,14 +66,28 @@ export class FilesMapService {
}
}
- replace(entity: File) {
- const existingFile = this.get(entity.dossierId).find(file => file.fileId === entity.fileId);
- if (existingFile.lastUpdated !== entity.lastUpdated) {
- const all = this.get(entity.dossierId).filter(file => file.fileId !== entity.fileId);
- this.set(entity.dossierId, [...all, entity]);
+ replace(entities: File[]) {
+ const dossierId = entities[0].dossierId;
+ const entityIds = entities.map(entity => entity.id);
+ let existingFiles = this.get(dossierId).filter(file => entityIds.includes(file.fileId));
+ entities = entities.filter(entity => {
+ const existingFile = existingFiles.find(existingFile => existingFile.id === entity.id);
+ return existingFile.lastUpdated !== entity.lastUpdated;
+ });
+ if (entities.length) {
+ const all = this.get(dossierId).filter(file => !entities.map(entity => entity.id).includes(file.id));
+ this.set(dossierId, [...all, ...entities]);
}
}
+ replaceFiles(files: File[], property: keyof IFile, generateValue: Function) {
+ const newFiles = files.map(
+ file =>
+ new File({ ...file, [property]: generateValue(file[property]), lastUpdated: new Date().toISOString() }, file.reviewerName),
+ );
+ this.replace(newFiles);
+ }
+
@Validate()
watch$(@RequiredParam() key: string, @RequiredParam() entityId: string): Observable
{
return this._entityChanged$.pipe(
diff --git a/apps/red-ui/src/app/services/entity-services/files.service.ts b/apps/red-ui/src/app/services/entity-services/files.service.ts
index 258c93870..f28eeb296 100644
--- a/apps/red-ui/src/app/services/entity-services/files.service.ts
+++ b/apps/red-ui/src/app/services/entity-services/files.service.ts
@@ -31,7 +31,7 @@ export class FilesService extends EntitiesService {
return super._getOne([dossierId, fileId]).pipe(
map(file => new File(file, this._userService.getNameForId(file.assignee))),
switchMap(file => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(file))),
- tap(file => this._filesMapService.replace(file)),
+ tap(file => this._filesMapService.replace([file])),
);
}
diff --git a/apps/red-ui/src/app/services/permissions.service.ts b/apps/red-ui/src/app/services/permissions.service.ts
index 4ad6f6d6b..b476bd189 100644
--- a/apps/red-ui/src/app/services/permissions.service.ts
+++ b/apps/red-ui/src/app/services/permissions.service.ts
@@ -32,6 +32,14 @@ export class PermissionsService {
return files.reduce((acc, _file) => this._canReanalyseFile(_file) && acc, true);
}
+ canEnableAutoAnalysis(files: File[]): boolean {
+ return files.reduce((acc, _file) => this._canEnableAutoAnalysis(_file) && acc, true);
+ }
+
+ canDisableAutoAnalysis(files: File[]): boolean {
+ return files.reduce((acc, _file) => this._canDisableAutoAnalysis(_file) && acc, true);
+ }
+
isFileAssignee(file: File): boolean {
return file.assignee === this._userService.currentUser.id;
}
@@ -158,6 +166,14 @@ export class PermissionsService {
return this.isReviewerOrApprover(file) || file.isNew || (file.isError && file.isNew);
}
+ private _canEnableAutoAnalysis(file: File): boolean {
+ return file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id;
+ }
+
+ private _canDisableAutoAnalysis(file: File): boolean {
+ return !file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id;
+ }
+
private _canAssignToSelf(file: File, dossier: Dossier): boolean {
const precondition = this.isDossierMember(dossier) && !this.isFileAssignee(file) && !file.isError && !file.isProcessing;
return precondition && (file.isNew || file.isUnderReview || (file.isUnderApproval && this.isApprover(dossier)));
diff --git a/apps/red-ui/src/app/services/reanalysis.service.ts b/apps/red-ui/src/app/services/reanalysis.service.ts
index 1707aca5a..a636a7b60 100644
--- a/apps/red-ui/src/app/services/reanalysis.service.ts
+++ b/apps/red-ui/src/app/services/reanalysis.service.ts
@@ -1,14 +1,27 @@
import { Injectable, Injector } from '@angular/core';
-import { GenericService, List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
-import { IPageExclusionRequest } from '@red/domain';
-import { switchMap } from 'rxjs/operators';
+import { GenericService, List, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
+import { File, IPageExclusionRequest } from '@red/domain';
+import { catchError, switchMap, tap } from 'rxjs/operators';
import { FilesService } from './entity-services/files.service';
+import { FilesMapService } from './entity-services/files-map.service';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { of } from 'rxjs';
+
+export interface ReanalyzeQueryParams {
+ force?: boolean;
+ triggeredByUser?: boolean;
+}
@Injectable({
providedIn: 'root',
})
export class ReanalysisService extends GenericService {
- constructor(protected readonly _injector: Injector, private readonly _filesService: FilesService) {
+ constructor(
+ protected readonly _injector: Injector,
+ private readonly _toaster: Toaster,
+ private readonly _filesService: FilesService,
+ private readonly _filesMapService: FilesMapService,
+ ) {
super(_injector, '');
}
@@ -23,10 +36,13 @@ export class ReanalysisService extends GenericService {
}
@Validate()
- reanalyzeFilesForDossier(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string, force?: boolean) {
+ reanalyzeFilesForDossier(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string, params?: ReanalyzeQueryParams) {
const queryParams: QueryParam[] = [];
- if (force) {
- queryParams.push({ key: 'force', value: force });
+ if (params?.force) {
+ queryParams.push({ key: 'force', value: true });
+ }
+ if (params?.triggeredByUser) {
+ queryParams.push({ key: 'triggeredByUser', value: true });
}
return this._post(fileIds, `reanalyze/${dossierId}/bulk`, queryParams).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
@@ -44,6 +60,25 @@ export class ReanalysisService extends GenericService {
);
}
+ @Validate()
+ toggleAutomaticAnalysis(@RequiredParam() dossierId: string, @RequiredParam() files: File[]) {
+ const fileIds = files.map(file => file.id);
+ const excluded = !files[0].excludedFromAutomaticAnalysis;
+ const queryParams: QueryParam[] = [{ key: 'excluded', value: excluded }];
+ return this._post(fileIds, `toggle-automatic-analysis/${dossierId}/bulk`, queryParams).pipe(
+ tap(() => this._filesMapService.replaceFiles(files, 'excludedFromAutomaticAnalysis', value => !value)),
+ tap(() =>
+ this._toaster.success(_('toggle-auto-analysis-message.success'), {
+ params: { toggleOperation: excluded ? 'Deactivated' : 'Activated' },
+ }),
+ ),
+ catchError(() => {
+ this._toaster.error(_('toggle-auto-analysis-message.error'));
+ return of({});
+ }),
+ );
+ }
+
@Validate()
ocrFiles(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string) {
return this._post(fileIds, `ocr/reanalyze/${dossierId}/bulk`).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json
index 07269c939..21819e941 100644
--- a/apps/red-ui/src/assets/i18n/de.json
+++ b/apps/red-ui/src/assets/i18n/de.json
@@ -599,6 +599,7 @@
"placeholder": "Begründung"
}
},
+ "disabled-auto-analysis": "",
"document-info": {
"save": "Dokumenteninformation speichern",
"title": "Datei-Attribute anlegen"
@@ -710,6 +711,7 @@
"delete": {
"action": "Datei löschen"
},
+ "disable-auto-analysis": "",
"dossier-details": {
"attributes": {
"expand": "{count} {count, plural, one{benutzerdefiniertes Attribut} other{benutzerdefinierte Attribute}}",
@@ -733,6 +735,7 @@
},
"download-file": "Herunterladen",
"download-file-disabled": "Nur genehmigte Dateien können heruntergeladen werden",
+ "enable-auto-analysis": "",
"file-listing": {
"file-entry": {
"file-error": "Reanalyse erforderlich",
@@ -1594,6 +1597,10 @@
"less-than-an-hour": "< 1 Stunde",
"no-time-left": "Frist für Wiederherstellung verstrichen"
},
+ "toggle-auto-analysis-message": {
+ "success": "",
+ "error": ""
+ },
"top-bar": {
"navigation-items": {
"back": "Zurück",
diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json
index 4cfab2283..642abf35c 100644
--- a/apps/red-ui/src/assets/i18n/en.json
+++ b/apps/red-ui/src/assets/i18n/en.json
@@ -607,6 +607,7 @@
"placeholder": "Reason"
}
},
+ "disabled-auto-analysis": "Automatic analysys is disabled",
"document-info": {
"save": "Save Document Info",
"title": "Introduce File Attributes"
@@ -718,6 +719,7 @@
"delete": {
"action": "Delete File"
},
+ "disable-auto-analysis": "Disable auto-analysis",
"dossier-details": {
"attributes": {
"expand": "{count} custom {count, plural, one{attribute} other{attributes}}",
@@ -741,6 +743,7 @@
},
"download-file": "Download",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be approved in order to download.",
+ "enable-auto-analysis": "Enable auto-analysis",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
@@ -1633,6 +1636,10 @@
"less-than-an-hour": "< 1 hour",
"no-time-left": "Time to restore already passed"
},
+ "toggle-auto-analysis-message": {
+ "success": "{toggleOperation} automatic processing.",
+ "error": "Something went wrong."
+ },
"top-bar": {
"navigation-items": {
"back": "Back",
diff --git a/apps/red-ui/src/assets/icons/general/denied.svg b/apps/red-ui/src/assets/icons/general/denied.svg
new file mode 100644
index 000000000..6530d96ff
--- /dev/null
+++ b/apps/red-ui/src/assets/icons/general/denied.svg
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/apps/red-ui/src/assets/icons/general/play.svg b/apps/red-ui/src/assets/icons/general/play.svg
new file mode 100644
index 000000000..1b37e3874
--- /dev/null
+++ b/apps/red-ui/src/assets/icons/general/play.svg
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/apps/red-ui/src/assets/icons/general/stop.svg b/apps/red-ui/src/assets/icons/general/stop.svg
new file mode 100644
index 000000000..db10a7bb9
--- /dev/null
+++ b/apps/red-ui/src/assets/icons/general/stop.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts
index f333045c8..d1168ffa2 100644
--- a/libs/red-domain/src/lib/files/file.model.ts
+++ b/libs/red-domain/src/lib/files/file.model.ts
@@ -16,6 +16,7 @@ export class File extends Entity implements IFile {
readonly dossierDictionaryVersion?: number;
readonly dossierId: string;
readonly excluded: boolean;
+ readonly excludedFromAutomaticAnalysis: boolean;
readonly fileAttributes?: FileAttributes;
readonly fileId: string;
readonly filename: string;
@@ -68,6 +69,7 @@ export class File extends Entity implements IFile {
this.dossierDictionaryVersion = file.dossierDictionaryVersion;
this.dossierId = file.dossierId;
this.excluded = !!file.excluded;
+ this.excludedFromAutomaticAnalysis = !!file.excludedFromAutomaticAnalysis;
this.fileAttributes = file.fileAttributes;
this.fileId = file.fileId;
this.filename = file.filename;
diff --git a/libs/red-domain/src/lib/files/file.ts b/libs/red-domain/src/lib/files/file.ts
index a93082aa8..6208d4fab 100644
--- a/libs/red-domain/src/lib/files/file.ts
+++ b/libs/red-domain/src/lib/files/file.ts
@@ -46,6 +46,10 @@ export interface IFile {
* Shows if the file was excluded from analysis.
*/
readonly excluded?: boolean;
+ /**
+ * Shows if the file was excluded from automatic analysis.
+ */
+ readonly excludedFromAutomaticAnalysis?: boolean;
/**
* Set of excluded pages for this file.
*/