Pull request #347: VM/RED-3332

Merge in RED/ui from VM/RED-3332 to master

* commit '32408542afcbc60ab05e1f6262ba4598a6e62325':
  shown confirmation message for enable/disable auto-analysis
  updated and called replace file method from files map service instead of reloading all files from backend
  used bulk api for both requests(toggle one file or bulk files)
  - split check for can disable or enable auto analysis in two different methods - split request method for toggle auto analysis in two methods, one for a single file and another for bulk
  fix for reanalyze request when is triggered by user
  added back "reanalyse" button when automatic analysis is disabled
  WIP on enabling/disabling automatic analysis process for a document
This commit is contained in:
Valentin-Gabriel Mihai 2022-02-08 16:37:51 +01:00
commit 3870186413
18 changed files with 207 additions and 29 deletions

View File

@ -25,6 +25,8 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
canAssign: boolean;
canDelete: boolean;
canReanalyse: boolean;
canDisableAutoAnalysis: boolean;
canEnableAutoAnalysis: boolean;
canOcr: boolean;
canSetToUnderReview: boolean;
canSetToUnderApproval: boolean;
@ -112,7 +114,21 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
action: () => this._bulkActionsService.reanalyse(this.selectedFiles),
tooltip: _('dossier-overview.bulk.reanalyse'),
icon: 'iqser:refresh',
show: this.canReanalyse && this.analysisForced,
show: this.canReanalyse && (this.analysisForced || this.canEnableAutoAnalysis),
},
{
type: ActionTypes.circleBtn,
action: $event => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
tooltip: _('dossier-overview.disable-auto-analysis'),
icon: 'red:stop',
show: this.canDisableAutoAnalysis,
},
{
type: ActionTypes.circleBtn,
action: $event => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
tooltip: _('dossier-overview.enable-auto-analysis'),
icon: 'red:play',
show: this.canEnableAutoAnalysis,
},
].filter(btn => btn.show);
}
@ -149,6 +165,10 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
this.canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles);
this.canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis(this.selectedFiles);
this.canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis(this.selectedFiles);
this.canOcr = this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true);
this.canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles) && !isWorkflow;

View File

@ -78,8 +78,16 @@ export class BulkActionsService {
async reanalyse(files: File[]) {
this._loadingService.start();
const fileIds = files.filter(file => file.analysisRequired).map(file => file.fileId);
await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier(fileIds, files[0].dossierId));
const fileIds = files.map(file => file.fileId);
await firstValueFrom(
this._reanalysisService.reanalyzeFilesForDossier(fileIds, files[0].dossierId, { force: true, triggeredByUser: true }),
);
this._loadingService.stop();
}
async toggleAutomaticAnalysis(files: File[]) {
this._loadingService.start();
await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(files[0].dossierId, files));
this._loadingService.stop();
}

View File

@ -31,12 +31,18 @@
</ng-template>
<div class="right-content">
<div *ngIf="state.isReadonly$ | async" class="justify-center read-only d-flex">
<div *ngIf="state.isReadonly$ | async" class="justify-center banner read-only d-flex">
<div class="flex-center">
<mat-icon class="primary-white" svgIcon="red:read-only"></mat-icon>
<span class="read-only-text" translate="readonly"></span>
</div>
</div>
<div *ngIf="file.excludedFromAutomaticAnalysis" class="justify-center banner disabled-auto-analysis d-flex">
<div class="flex-center">
<mat-icon class="primary-white" svgIcon="red:denied"></mat-icon>
<span class="disabled-auto-analysis-text" translate="disabled-auto-analysis"></span>
</div>
</div>
<div *ngIf="multiSelectActive$ | async" class="multi-select">
<div class="selected-wrapper">

View File

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

View File

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

View File

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

View File

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

View File

@ -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<File> {
return this._entityChanged$.pipe(

View File

@ -31,7 +31,7 @@ export class FilesService extends EntitiesService<File, IFile> {
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])),
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M500,10C229.4,10,10,229.4,10,500c0,270.6,219.4,490,490,490c270.6,0,490-219.4,490-490C990,229.4,770.6,10,500,10z M500,876.9c-208.2,0-376.9-168.8-376.9-376.9c0-83.3,27.3-160.1,73.2-222.6l526.3,526.3C660.1,849.6,583.3,876.9,500,876.9z M802.6,724L276,197.3c62.6-46.5,140-74.2,224-74.2c208.2,0,376.9,168.7,376.9,376.9C876.9,584,849.1,661.3,802.6,724z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M990,56c0-25.4-20.6-46-46-46H56c-25.4,0-46,20.6-46,46V944c0,25.4,20.6,46,46,46H944c25.4,0,46-20.6,46-46L990,56L990,56z M898.2,882.8c0,8.5-6.9,15.3-15.3,15.3H117.2c-8.5,0-15.3-6.9-15.3-15.3V117.2c0-8.5,6.9-15.3,15.3-15.3h765.7c8.5,0,15.3,6.9,15.3,15.3V882.8z"/><path d="M362.8,662.5c0,5.6,2.8,11.1,8,14.3c5.1,3.2,11.3,3.3,16.3,0.7l313.3-156.6c5.5-2.8,9.3-8.4,9.3-15c0-6.6-3.8-12.3-9.3-15L387.1,334.2c-5-2.5-11.2-2.4-16.3,0.7c-5.1,3.2-8,8.7-8,14.3L362.8,662.5L362.8,662.5z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256zm296-80v160c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V176c0-8.8 7.2-16 16-16h160c8.8 0 16 7.2 16 16z"/></svg>
<!--
Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->

After

Width:  |  Height:  |  Size: 512 B

View File

@ -16,6 +16,7 @@ export class File extends Entity<IFile> 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<IFile> 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;

View File

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