Pull request #319: RED-2830

Merge in RED/ui from RED-2830 to master

* commit '02da8e5a02e7d414550900e66acecf58f1ad9692':
  File bulk actions service, permissions for multiple files, workflow done
  Updated common
  Minor updates
  Fix some stuff, break other stuff
  More workflow updates (multi drag almost done)
  Workflow bulk actions
  Reset selection on view mode change
  Some fixes, workflow bulk actions WIP
  Workflow multi select
  Update workflow UI
This commit is contained in:
Adina Teudan 2021-12-07 23:25:27 +01:00
commit b99bd42cca
38 changed files with 1049 additions and 731 deletions

View File

@ -66,7 +66,7 @@ export class AssignReviewerApproverDialogComponent {
} }
private get _canUnassignFiles() { private get _canUnassignFiles() {
return this.data.files.reduce((prev, file) => prev && this.permissionsService.canUnassignUser(file), true); return this.permissionsService.canUnassignUser(this.data.files);
} }
/** Initialize the form with: /** Initialize the form with:

View File

@ -1,78 +1,4 @@
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress> <ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress>
<iqser-circle-button <redaction-expandable-file-actions [actions]="buttons" [buttonType]="buttonType" [maxWidth]="maxWidth" [tooltipPosition]="'above'">
(action)="delete()" </redaction-expandable-file-actions>
*ngIf="canDelete"
[tooltip]="'dossier-overview.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="assign()"
*ngIf="canAssign"
[tooltip]="assignTooltip"
[type]="circleButtonTypes.dark"
icon="red:assign"
></iqser-circle-button>
<iqser-circle-button
(action)="assignToMe()"
*ngIf="canAssignToSelf"
[tooltip]="'dossier-overview.assign-me' | translate"
[type]="circleButtonTypes.dark"
icon="red:assign-me"
></iqser-circle-button>
<iqser-circle-button
(action)="setToUnderApproval()"
*ngIf="canSetToUnderApproval"
[tooltip]="'dossier-overview.under-approval' | translate"
[type]="circleButtonTypes.dark"
icon="red:ready-for-approval"
></iqser-circle-button>
<iqser-circle-button
(action)="setToUnderReview()"
*ngIf="canSetToUnderReview"
[tooltip]="'dossier-overview.under-review' | translate"
[type]="circleButtonTypes.dark"
icon="red:undo"
></iqser-circle-button>
<redaction-file-download-btn [files]="selectedFiles"></redaction-file-download-btn>
<!-- Approved-->
<iqser-circle-button
(action)="approveDocuments()"
*ngIf="isReadyForApproval"
[disabled]="!canApprove"
[tooltip]="canApprove ? ('dossier-overview.approve' | translate) : ('dossier-overview.approve-disabled' | translate)"
[type]="circleButtonTypes.dark"
icon="red:approved"
></iqser-circle-button>
<!-- Back to approval -->
<iqser-circle-button
(action)="setToUnderApproval()"
*ngIf="canUndoApproval"
[tooltip]="'dossier-overview.under-approval' | translate"
[type]="circleButtonTypes.dark"
icon="red:undo"
></iqser-circle-button>
<iqser-circle-button
(action)="ocr()"
*ngIf="canOcr"
[tooltip]="'dossier-overview.ocr-file' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:ocr"
></iqser-circle-button>
<iqser-circle-button
(action)="reanalyse()"
*ngIf="canReanalyse && analysisForced"
[tooltip]="'dossier-overview.bulk.reanalyse' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:refresh"
></iqser-circle-button>
</ng-container> </ng-container>

View File

@ -1,16 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { Dossier, File } from '@red/domain'; import { Action, ActionTypes, Dossier, File } from '@red/domain';
import { FileAssignService } from '../../../../shared/services/file-assign.service'; import { CircleButtonType, CircleButtonTypes, Required } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { CircleButtonTypes, ConfirmationDialogInput, LoadingService, Required } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { LongPressEvent } from '@shared/directives/long-press.directive'; import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service'; import { UserPreferenceService } from '@services/user-preference.service';
import { FileManagementService } from '@services/entity-services/file-management.service'; import { BulkActionsService } from '../../services/bulk-actions.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { FilesService } from '@services/entity-services/files.service';
@Component({ @Component({
selector: 'redaction-dossier-overview-bulk-actions', selector: 'redaction-dossier-overview-bulk-actions',
@ -18,204 +13,153 @@ import { FilesService } from '@services/entity-services/files.service';
styleUrls: ['./dossier-overview-bulk-actions.component.scss'], styleUrls: ['./dossier-overview-bulk-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DossierOverviewBulkActionsComponent { export class DossierOverviewBulkActionsComponent implements OnChanges {
readonly circleButtonTypes = CircleButtonTypes;
@Input() @Required() dossier: Dossier; @Input() @Required() dossier: Dossier;
@Input() @Required() selectedFiles: File[]; @Input() @Required() selectedFiles: File[];
@Input() buttonType: CircleButtonType = CircleButtonTypes.dark;
@Input() maxWidth: number;
analysisForced: boolean; analysisForced: boolean;
canAssignToSelf: boolean;
canAssign: boolean;
canDelete: boolean;
canReanalyse: boolean;
canOcr: boolean;
canSetToUnderReview: boolean;
canSetToUnderApproval: boolean;
isReadyForApproval: boolean;
canApprove: boolean;
canUndoApproval: boolean;
assignTooltip: string;
buttons: Action[];
private _canMoveToSameState: boolean;
constructor( constructor(
private readonly _dialogService: DossiersDialogService,
private readonly _fileManagementService: FileManagementService,
private readonly _reanalysisService: ReanalysisService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _fileAssignService: FileAssignService,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _filesService: FilesService, private readonly _bulkActionsService: BulkActionsService,
) {} ) {}
get allSelectedFilesCanBeAssignedIntoSameState() { private get _buttons(): Action[] {
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce( return [
(acc, file) => acc && (file.isUnderReview || file.isNew), {
true, type: ActionTypes.circleBtn,
); action: () => this._bulkActionsService.delete(this.selectedFiles),
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); tooltip: _('dossier-overview.bulk.delete'),
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval; icon: 'iqser:trash',
show: this.canDelete,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assign(this.selectedFiles),
tooltip: this.assignTooltip,
icon: 'red:assign',
show: this.canAssign,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assignToMe(this.selectedFiles),
tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me',
show: this.canAssignToSelf,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval',
show: this.canSetToUnderApproval,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles),
tooltip: _('dossier-overview.under-review'),
icon: 'red:undo',
show: this.canSetToUnderReview,
},
{
type: ActionTypes.downloadBtn,
show: true,
files: this.selectedFiles,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.approve(this.selectedFiles),
disabled: !this.canApprove,
tooltip: this.canApprove ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved',
show: this.isReadyForApproval,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo',
show: this.canUndoApproval,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.ocr(this.selectedFiles),
tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr',
show: this.canOcr,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.reanalyse(this.selectedFiles),
tooltip: _('dossier-overview.bulk.reanalyse'),
icon: 'iqser:refresh',
show: this.canReanalyse && this.analysisForced,
},
].filter(btn => btn.show);
} }
get canAssignToSelf() { ngOnChanges() {
return ( this._setup();
this.allSelectedFilesCanBeAssignedIntoSameState &&
this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canAssignToSelf(file), true)
);
}
get canAssign() {
return (
this.allSelectedFilesCanBeAssignedIntoSameState &&
this.selectedFiles.reduce(
(acc, file) => (acc && this._permissionsService.canAssignUser(file)) || this._permissionsService.canUnassignUser(file),
true,
)
);
}
get canDelete() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canDeleteFile(file), true);
}
get canReanalyse() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canReanalyseFile(file), true);
}
get canOcr() {
return this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true);
}
get canSetToUnderReview() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderReview(file), true);
}
get canSetToUnderApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderApproval(file), true);
}
get isReadyForApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.isReadyForApproval(file), true);
}
get canApprove() {
return this.selectedFiles.reduce((acc, file) => acc && file.canBeApproved, true);
}
get canUndoApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canUndoApproval(file), true);
}
get assignTooltip() {
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
return allFilesAreUnderApproval
? this._translateService.instant('dossier-overview.assign-approver')
: this._translateService.instant('dossier-overview.assign-reviewer');
} }
forceReanalysisAction($event: LongPressEvent) { forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled; this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
this._setup();
} }
delete() { private _setup() {
this._dialogService.openDialog( if (this.selectedFiles.length) {
'confirm', const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
null, (acc, file) => acc && (file.isUnderReview || file.isNew),
new ConfirmationDialogInput({ true,
title: _('confirmation-dialog.delete-file.title'),
question: _('confirmation-dialog.delete-file.question'),
}),
async () => {
this._loadingService.start();
await this._fileManagementService
.delete(
this.selectedFiles.map(item => item.fileId),
this.dossier.dossierId,
)
.toPromise();
this._loadingService.stop();
},
);
}
async setToUnderApproval() {
// If more than 1 approver - show dialog and ask who to assign
if (this.dossier.approverIds.length > 1) {
this._assignFiles('approver', true);
} else {
this._loadingService.start();
await this._filesService
.setUnderApprovalFor(
this.selectedFiles.map(f => f.id),
this.dossier.id,
this.dossier.approverIds[0],
)
.toPromise();
this._loadingService.stop();
}
}
async reanalyse() {
this._loadingService.start();
const fileIds = this.selectedFiles.filter(file => file.analysisRequired).map(file => file.fileId);
await this._reanalysisService.reanalyzeFilesForDossier(fileIds, this.dossier.id).toPromise();
this._loadingService.stop();
}
async ocr() {
this._loadingService.start();
await this._reanalysisService
.ocrFiles(
this.selectedFiles.map(f => f.fileId),
this.dossier.id,
)
.toPromise();
this._loadingService.stop();
}
async setToUnderReview() {
this._loadingService.start();
await this._filesService
.setUnderReviewFor(
this.selectedFiles.map(f => f.id),
this.dossier.id,
)
.toPromise();
this._loadingService.stop();
}
async approveDocuments(): Promise<void> {
const foundUpdatedFile = this.selectedFiles.find(file => file.hasUpdates);
if (foundUpdatedFile) {
this._dialogService.openDialog(
'confirm',
null,
new ConfirmationDialogInput({
title: _('confirmation-dialog.approve-multiple-files.title'),
question: _('confirmation-dialog.approve-multiple-files.question'),
}),
async () => {
this._loadingService.start();
await this._filesService
.setApprovedFor(
this.selectedFiles.map(f => f.id),
this.dossier.id,
)
.toPromise();
this._loadingService.stop();
},
); );
} else { const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
this._loadingService.start(); this._canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
await this._filesService
.setApprovedFor( this.canAssign =
this.selectedFiles.map(f => f.id), this._canMoveToSameState &&
this.dossier.id, this.selectedFiles.reduce(
) (acc, file) => (acc && this._permissionsService.canAssignUser(file)) || this._permissionsService.canUnassignUser(file),
.toPromise(); true,
this._loadingService.stop(); );
this.canAssignToSelf = this._canMoveToSameState && this._permissionsService.canAssignToSelf(this.selectedFiles);
this.canDelete = this._permissionsService.canDeleteFile(this.selectedFiles);
this.canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles);
this.canOcr = this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true);
this.canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles);
this.canSetToUnderApproval = this._permissionsService.canSetUnderApproval(this.selectedFiles);
this.isReadyForApproval = this._permissionsService.isReadyForApproval(this.selectedFiles);
this.canApprove = this._permissionsService.canBeApproved(this.selectedFiles);
this.canUndoApproval = this._permissionsService.canUndoApproval(this.selectedFiles);
this.assignTooltip = allFilesAreUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
this.buttons = this._buttons;
} }
} }
async assignToMe() {
await this._fileAssignService.assignToMe(this.selectedFiles);
}
assign() {
const mode = this.selectedFiles[0].isUnderApproval ? 'approver' : 'reviewer';
this._assignFiles(mode);
}
private _assignFiles(mode: 'reviewer' | 'approver', ignoreChanged = false) {
const data = { mode, files: this.selectedFiles, ignoreChanged };
this._dialogService.openDialog('assignFile', null, data);
}
} }

View File

@ -4,12 +4,12 @@ import { File } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service'; import { DossiersService } from '@services/entity-services/dossiers.service';
@Component({ @Component({
selector: 'redaction-file-workload-column', selector: 'redaction-file-workload',
templateUrl: './file-workload-column.component.html', templateUrl: './file-workload.component.html',
styleUrls: ['./file-workload-column.component.scss'], styleUrls: ['./file-workload.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FileWorkloadColumnComponent { export class FileWorkloadComponent {
@Input() file: File; @Input() file: File;
constructor(private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {} constructor(private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {}

View File

@ -17,7 +17,7 @@
<ng-container *ngIf="!file.isError"> <ng-container *ngIf="!file.isError">
<div class="cell"> <div class="cell">
<redaction-file-workload-column [file]="file"></redaction-file-workload-column> <redaction-file-workload [file]="file"></redaction-file-workload>
</div> </div>
<div class="user-column cell"> <div class="user-column cell">

View File

@ -2,7 +2,7 @@
<div class="all-caps-label" translate="view-mode.view-as"></div> <div class="all-caps-label" translate="view-mode.view-as"></div>
<iqser-circle-button <iqser-circle-button
(action)="configService.listingMode = listingModes.table" (action)="setListingMode(listingModes.table)"
[attr.aria-expanded]="mode === listingModes.table" [attr.aria-expanded]="mode === listingModes.table"
[tooltip]="'view-mode.list' | translate" [tooltip]="'view-mode.list' | translate"
[greySelected]="true" [greySelected]="true"
@ -10,7 +10,7 @@
></iqser-circle-button> ></iqser-circle-button>
<iqser-circle-button <iqser-circle-button
(action)="configService.listingMode = listingModes.workflow" (action)="setListingMode(listingModes.workflow)"
[attr.aria-expanded]="mode === listingModes.workflow" [attr.aria-expanded]="mode === listingModes.workflow"
[tooltip]="'view-mode.workflow' | translate" [tooltip]="'view-mode.workflow' | translate"
[greySelected]="true" [greySelected]="true"

View File

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ConfigService } from '../../config.service'; import { ConfigService } from '../../config.service';
import { CircleButtonTypes, ListingModes } from '@iqser/common-ui'; import { CircleButtonTypes, ListingMode, ListingModes, ListingService } from '@iqser/common-ui';
import { File } from '@red/domain';
@Component({ @Component({
selector: 'redaction-view-mode-selection', selector: 'redaction-view-mode-selection',
@ -12,5 +13,10 @@ export class ViewModeSelectionComponent {
readonly listingModes = ListingModes; readonly listingModes = ListingModes;
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
constructor(readonly configService: ConfigService) {} constructor(readonly configService: ConfigService, private readonly _listingService: ListingService<File>) {}
setListingMode(listingMode: ListingMode): void {
this.configService.listingMode = listingMode;
this._listingService.setSelected([]);
}
} }

View File

@ -1,7 +1,7 @@
<div class="workflow-item"> <div class="workflow-item">
<div> <div class="details-wrapper">
<div class="details"> <div class="details">
<div [matTooltip]="file.filename" class="filename" matTooltipPosition="above"> <div [matTooltip]="file.filename" [routerLink]="file.routerLink" class="filename pointer" matTooltipPosition="above">
{{ file.filename }} {{ file.filename }}
</div> </div>
@ -13,5 +13,29 @@
</div> </div>
</div> </div>
<redaction-file-actions *ngIf="!file.isProcessing" [file]="file" type="dossier-overview-workflow"></redaction-file-actions> <div *ngFor="let config of displayedAttributes" class="small-label mt-4">
{{ file.fileAttributes.attributeIdToValue[config.id] || '-' }}
</div>
<redaction-file-workload [file]="file"></redaction-file-workload>
<div class="file-actions">
<div
*ngIf="file.isProcessing"
class="spinning-icon mr-8"
matTooltip="{{ 'file-status.processing' | translate }}"
matTooltipPosition="above"
>
<mat-icon svgIcon="red:reanalyse"></mat-icon>
</div>
<div #actionsWrapper class="actions-wrapper">
<redaction-file-actions
*ngIf="!file.isProcessing"
[file]="file"
[maxWidth]="width"
type="dossier-overview-workflow"
></redaction-file-actions>
</div>
</div>
</div> </div>

View File

@ -1,34 +1,60 @@
@use 'common-mixins'; @use 'common-mixins';
.workflow-item { .workflow-item {
padding: 10px; padding: 10px 10px 8px 10px;
> div { &:hover .filename {
display: flex; text-decoration: underline;
justify-content: space-between;
.details {
max-width: calc(100% - 28px);
.filename {
font-weight: 600;
line-height: 18px;
@include common-mixins.line-clamp(1);
}
}
.user {
display: flex;
align-items: flex-end;
}
}
redaction-file-actions {
margin-top: 10px;
display: none;
} }
&:hover redaction-file-actions { &:hover redaction-file-actions {
display: block; display: initial;
} }
} }
.details-wrapper {
display: flex;
justify-content: space-between;
.details {
max-width: calc(100% - 28px);
.filename {
font-weight: 600;
line-height: 18px;
@include common-mixins.line-clamp(1);
}
}
.user {
display: flex;
align-items: flex-end;
}
}
redaction-file-workload {
margin-top: 10px;
display: block;
min-height: 16px;
}
.file-actions {
margin-top: 8px;
min-height: 34px;
overflow: hidden;
align-items: center;
display: flex;
}
.actions-wrapper {
overflow: hidden;
flex: 1;
}
redaction-file-actions:not(.keep-visible) {
display: none;
}
.mt-4 {
margin-top: 4px;
}

View File

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { File } from '@red/domain'; import { File, IFileAttributeConfig } from '@red/domain';
import { Debounce, Required } from '@iqser/common-ui';
@Component({ @Component({
selector: 'redaction-workflow-item', selector: 'redaction-workflow-item',
@ -7,6 +8,25 @@ import { File } from '@red/domain';
styleUrls: ['./workflow-item.component.scss'], styleUrls: ['./workflow-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class WorkflowItemComponent { export class WorkflowItemComponent implements OnInit {
@Input() file: File; @Input() @Required() file!: File;
@Input() @Required() displayedAttributes!: IFileAttributeConfig[];
width: number;
@ViewChild('actionsWrapper', { static: true }) private _actionsWrapper: ElementRef;
constructor(private readonly _changeRef: ChangeDetectorRef) {}
ngOnInit(): void {
const _observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
this._updateItemWidth(entries[0]);
});
_observer.observe(this._actionsWrapper.nativeElement);
}
@Debounce(30)
private _updateItemWidth(entry: ResizeObserverEntry): void {
this.width = entry.contentRect.width;
this._changeRef.detectChanges();
}
} }

View File

@ -7,14 +7,12 @@ import {
List, List,
ListingMode, ListingMode,
ListingModes, ListingModes,
LoadingService,
NestedFilter, NestedFilter,
TableColumnConfig, TableColumnConfig,
WorkflowConfig, WorkflowConfig,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; import { File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { workflowFileStatusTranslations } from '../../translations/file-status-translations'; import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
import { FileAssignService } from '../../shared/services/file-assign.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -24,9 +22,9 @@ import { annotationFilterChecker, RedactionFilterSorter } from '@utils/index';
import { workloadTranslations } from '../../translations/workload-translations'; import { workloadTranslations } from '../../translations/workload-translations';
import * as moment from 'moment'; import * as moment from 'moment';
import { ConfigService as AppConfigService } from '@services/config.service'; import { ConfigService as AppConfigService } from '@services/config.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesService } from '@services/entity-services/files.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import noop from 'lodash/noop';
import { BulkActionsService } from './services/bulk-actions.service';
@Injectable() @Injectable()
export class ConfigService { export class ConfigService {
@ -34,15 +32,12 @@ export class ConfigService {
private readonly _listingMode$ = new BehaviorSubject<ListingMode>(ListingModes.table); private readonly _listingMode$ = new BehaviorSubject<ListingMode>(ListingModes.table);
constructor( constructor(
private readonly _fileAssignService: FileAssignService,
private readonly _filesService: FilesService,
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _userService: UserService, private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService, private readonly _dialogService: DossiersDialogService,
private readonly _appConfigService: AppConfigService, private readonly _appConfigService: AppConfigService,
private readonly _bulkActionsService: BulkActionsService,
) { ) {
this.listingMode$ = this._listingMode$.asObservable(); this.listingMode$ = this._listingMode$.asObservable();
} }
@ -63,34 +58,45 @@ export class ConfigService {
{ {
label: workflowFileStatusTranslations[WorkflowFileStatuses.NEW], label: workflowFileStatusTranslations[WorkflowFileStatuses.NEW],
key: WorkflowFileStatuses.NEW, key: WorkflowFileStatuses.NEW,
enterFn: this._unassignFn, enterFn: noop,
enterPredicate: (file: File) => this._permissionsService.canUnassignUser(file), enterPredicate: () => false,
color: '#D3D5DA', color: '#D3D5DA',
entities: new BehaviorSubject([]),
}, },
{ {
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_REVIEW], label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_REVIEW],
enterFn: this._underReviewFn, enterFn: async (files: File[]) => {
enterPredicate: (file: File) => if (files[0].workflowStatus === WorkflowFileStatuses.UNDER_APPROVAL) {
this._permissionsService.canSetUnderReview(file) || await this._bulkActionsService.backToUnderReview(files);
this._permissionsService.canAssignToSelf(file) || } else {
this._permissionsService.canAssignUser(file), await this._bulkActionsService.assignToMe(files);
}
},
enterPredicate: (files: File[]) =>
this._permissionsService.canSetUnderReview(files) ||
this._permissionsService.canAssignToSelf(files) ||
this._permissionsService.canAssignUser(files),
key: WorkflowFileStatuses.UNDER_REVIEW, key: WorkflowFileStatuses.UNDER_REVIEW,
color: '#FDBD00', color: '#FDBD00',
entities: new BehaviorSubject([]),
}, },
{ {
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_APPROVAL], label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_APPROVAL],
enterFn: this._underApprovalFn, enterFn: (files: File[]) => this._bulkActionsService.setToUnderApproval(files),
enterPredicate: (file: File) => enterPredicate: (files: File[]) =>
this._permissionsService.canSetUnderApproval(file) || this._permissionsService.canUndoApproval(file), this._permissionsService.canSetUnderApproval(files) || this._permissionsService.canUndoApproval(files),
key: WorkflowFileStatuses.UNDER_APPROVAL, key: WorkflowFileStatuses.UNDER_APPROVAL,
color: '#374C81', color: '#374C81',
entities: new BehaviorSubject([]),
}, },
{ {
label: workflowFileStatusTranslations[WorkflowFileStatuses.APPROVED], label: workflowFileStatusTranslations[WorkflowFileStatuses.APPROVED],
enterFn: this._approveFn, enterFn: (files: File[]) => this._bulkActionsService.approve(files),
enterPredicate: (file: File) => this._permissionsService.isReadyForApproval(file) && file.canBeApproved, enterPredicate: (files: File[]) =>
this._permissionsService.isReadyForApproval(files) && this._permissionsService.canBeApproved(files),
key: WorkflowFileStatuses.APPROVED, key: WorkflowFileStatuses.APPROVED,
color: '#48C9F7', color: '#48C9F7',
entities: new BehaviorSubject([]),
}, },
], ],
}; };
@ -323,7 +329,7 @@ export class ConfigService {
_unassignedChecker = (file: File) => !file.assignee; _unassignedChecker = (file: File) => !file.assignee;
_assignedToOthersChecker = (file: File) => !file.isNew && file.assignee !== this._userService.currentUser.id; _assignedToOthersChecker = (file: File) => file.assignee && file.assignee !== this._userService.currentUser.id;
private _quickFilters(entities: File[]): NestedFilter[] { private _quickFilters(entities: File[]): NestedFilter[] {
const recentPeriod = this._appConfigService.values.RECENT_PERIOD_IN_HOURS; const recentPeriod = this._appConfigService.values.RECENT_PERIOD_IN_HOURS;
@ -361,31 +367,4 @@ export class ConfigService {
private _openEditDossierDialog($event: MouseEvent, dossierId: string) { private _openEditDossierDialog($event: MouseEvent, dossierId: string) {
this._dialogService.openDialog('editDossier', $event, { dossierId }); this._dialogService.openDialog('editDossier', $event, { dossierId });
} }
private _unassignFn = async (file: File) => {
this._loadingService.start();
await this._filesService.setUnassigned([file.fileId], file.dossierId).toPromise();
this._loadingService.stop();
};
private _underReviewFn = async (file: File) => {
await this._fileAssignService.assignReviewer(null, file, true);
};
private _underApprovalFn = async (file: File) => {
const dossier = this._dossiersService.find(file.dossierId);
if (dossier.approverIds.length > 1) {
await this._fileAssignService.assignApprover(null, file, true);
} else {
this._loadingService.start();
await this._filesService.setUnderApprovalFor([file.id], dossier.dossierId, dossier.approverIds[0]).toPromise();
this._loadingService.stop();
}
};
private _approveFn = async (file: File) => {
this._loadingService.start();
await this._filesService.setApprovedFor([file.id], file.dossierId).toPromise();
this._loadingService.stop();
};
} }

View File

@ -11,13 +11,14 @@ import { DossierDetailsStatsComponent } from './components/dossier-details-stats
import { TableItemComponent } from './components/table-item/table-item.component'; import { TableItemComponent } from './components/table-item/table-item.component';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { SharedDossiersModule } from '../../shared/shared-dossiers.module'; import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
import { FileWorkloadColumnComponent } from './components/table-item/file-workload-column/file-workload-column.component'; import { FileWorkloadComponent } from './components/table-item/file-workload/file-workload.component';
import { FileStatsComponent } from './components/file-stats/file-stats.component'; import { FileStatsComponent } from './components/file-stats/file-stats.component';
import { WorkflowItemComponent } from './components/workflow-item/workflow-item.component'; import { WorkflowItemComponent } from './components/workflow-item/workflow-item.component';
import { ScreenHeaderComponent } from './components/screen-header/screen-header.component'; import { ScreenHeaderComponent } from './components/screen-header/screen-header.component';
import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component'; import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component';
import { FileNameColumnComponent } from './components/table-item/file-name-column/file-name-column.component'; import { FileNameColumnComponent } from './components/table-item/file-name-column/file-name-column.component';
import { AddedColumnComponent } from './components/table-item/added-column/added-column.component'; import { AddedColumnComponent } from './components/table-item/added-column/added-column.component';
import { BulkActionsService } from './services/bulk-actions.service';
const routes: Routes = [ const routes: Routes = [
{ {
@ -36,7 +37,7 @@ const routes: Routes = [
DossierOverviewBulkActionsComponent, DossierOverviewBulkActionsComponent,
DossierDetailsComponent, DossierDetailsComponent,
DossierDetailsStatsComponent, DossierDetailsStatsComponent,
FileWorkloadColumnComponent, FileWorkloadComponent,
TableItemComponent, TableItemComponent,
FileStatsComponent, FileStatsComponent,
WorkflowItemComponent, WorkflowItemComponent,
@ -45,7 +46,7 @@ const routes: Routes = [
FileNameColumnComponent, FileNameColumnComponent,
AddedColumnComponent, AddedColumnComponent,
], ],
providers: [ConfigService], providers: [ConfigService, BulkActionsService],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule], imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule],
}) })
export class DossierOverviewModule {} export class DossierOverviewModule {}

View File

@ -32,16 +32,16 @@
(noDataAction)="fileInput.click()" (noDataAction)="fileInput.click()"
*ngIf="mode === listingModes.workflow" *ngIf="mode === listingModes.workflow"
[addElementIcon]="'iqser:upload'" [addElementIcon]="'iqser:upload'"
[bulkActions]="bulkActions"
[config]="workflowConfig" [config]="workflowConfig"
[itemClasses]="{ disabled: disabledFn }" [itemClasses]="{ disabled: disabledFn }"
[itemHeight]="'56px'"
[itemTemplate]="workflowItemTemplate" [itemTemplate]="workflowItemTemplate"
[noDataButtonIcon]="'iqser:upload'" [noDataButtonIcon]="'iqser:upload'"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate" [noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataIcon]="'iqser:document'" [noDataIcon]="'iqser:document'"
[noDataText]="'dossier-overview.no-data.title' | translate" [noDataText]="'dossier-overview.no-data.title' | translate"
[showNoDataButton]="true" [showNoDataButton]="true"
addElementColumn="UNASSIGNED" addElementColumn="NEW"
></iqser-workflow> ></iqser-workflow>
</div> </div>
@ -54,10 +54,12 @@
</div> </div>
</section> </section>
<ng-template #bulkActions> <ng-template #bulkActions let-maxWidth="maxWidth">
<redaction-dossier-overview-bulk-actions <redaction-dossier-overview-bulk-actions
[buttonType]="(configService.listingMode$ | async) === listingModes.table ? circleButtonTypes.dark : circleButtonTypes.primary"
[dossier]="dossier" [dossier]="dossier"
[selectedFiles]="this.listingService.selected" [maxWidth]="maxWidth"
[selectedFiles]="listingService.selectedEntities$ | async"
></redaction-dossier-overview-bulk-actions> ></redaction-dossier-overview-bulk-actions>
</ng-template> </ng-template>
@ -77,5 +79,5 @@
<input #fileInput (change)="uploadFiles($event.target['files'])" class="file-upload-input" multiple="true" type="file" /> <input #fileInput (change)="uploadFiles($event.target['files'])" class="file-upload-input" multiple="true" type="file" />
<ng-template #workflowItemTemplate let-entity="entity"> <ng-template #workflowItemTemplate let-entity="entity">
<redaction-workflow-item [file]="entity"></redaction-workflow-item> <redaction-workflow-item [displayedAttributes]="displayedAttributes" [file]="entity"></redaction-workflow-item>
</ng-template> </ng-template>

View File

@ -123,7 +123,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
} }
disabledFn = (file: File) => file.excluded; disabledFn = (file: File) => file.excluded;
lastOpenedFn = (file: File) => this._userPreferenceService.getLastOpenedFileForDossier(file.dossierId) === file.id; lastOpenedFn = (file: File) => this._userPreferenceService.getLastOpenedFileForDossier(file.dossierId) === file.id;
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {

View File

@ -0,0 +1,140 @@
import { Injectable } from '@angular/core';
import { Dossier, File } from '@red/domain';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesService } from '@services/entity-services/files.service';
import { FileAssignService } from '../../../shared/services/file-assign.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ReanalysisService } from '../../../../../services/reanalysis.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
@Injectable()
export class BulkActionsService {
constructor(
private readonly _dialogService: DossiersDialogService,
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
private readonly _filesService: FilesService,
private readonly _fileAssignService: FileAssignService,
private readonly _reanalysisService: ReanalysisService,
private readonly _fileManagementService: FileManagementService,
) {}
async setToUnderApproval(files: File[]) {
const dossier = this._getDossier(files);
// If more than 1 approver - show dialog and ask who to assign
if (dossier.approverIds.length > 1) {
this._assignFiles(files, 'approver', true);
} else {
this._loadingService.start();
await this._filesService
.setUnderApprovalFor(
files.map(f => f.id),
dossier.id,
dossier.approverIds[0],
)
.toPromise();
this._loadingService.stop();
}
}
async assignToMe(files: File[]) {
await this._fileAssignService.assignToMe(files);
}
async ocr(files: File[]) {
this._loadingService.start();
await this._reanalysisService
.ocrFiles(
files.map(f => f.fileId),
files[0].dossierId,
)
.toPromise();
this._loadingService.stop();
}
delete(files: File[]) {
this._dialogService.openDialog(
'confirm',
null,
new ConfirmationDialogInput({
title: _('confirmation-dialog.delete-file.title'),
question: _('confirmation-dialog.delete-file.question'),
}),
async () => {
this._loadingService.start();
await this._fileManagementService
.delete(
files.map(item => item.fileId),
files[0].dossierId,
)
.toPromise();
this._loadingService.stop();
},
);
}
async reanalyse(files: File[]) {
this._loadingService.start();
const fileIds = files.filter(file => file.analysisRequired).map(file => file.fileId);
await this._reanalysisService.reanalyzeFilesForDossier(fileIds, files[0].dossierId).toPromise();
this._loadingService.stop();
}
async backToUnderReview(files: File[]): Promise<void> {
this._loadingService.start();
await this._filesService
.setUnderReviewFor(
files.map(f => f.id),
files[0].dossierId,
)
.toPromise();
this._loadingService.stop();
}
async approve(files: File[]): Promise<void> {
const foundUpdatedFile = files.find(file => file.hasUpdates);
if (foundUpdatedFile) {
this._dialogService.openDialog(
'confirm',
null,
new ConfirmationDialogInput({
title: _('confirmation-dialog.approve-multiple-files.title'),
question: _('confirmation-dialog.approve-multiple-files.question'),
}),
async () => {
this._loadingService.start();
await this._filesService
.setApprovedFor(
files.map(f => f.id),
files[0].dossierId,
)
.toPromise();
this._loadingService.stop();
},
);
} else {
this._loadingService.start();
await this._filesService
.setApprovedFor(
files.map(f => f.id),
files[0].dossierId,
)
.toPromise();
this._loadingService.stop();
}
}
assign(files: File[], mode: 'reviewer' | 'approver' = files[0].isUnderApproval ? 'approver' : 'reviewer') {
this._assignFiles(files, mode);
}
private _getDossier(files: File[]): Dossier {
return this._dossiersService.find(files[0].dossierId);
}
private _assignFiles(files: File[], mode: 'reviewer' | 'approver', ignoreChanged = false) {
this._dialogService.openDialog('assignFile', null, { mode, files, ignoreChanged });
}
}

View File

@ -46,30 +46,6 @@
} }
} }
.multi-select {
min-height: 40px;
background: variables.$primary;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 0 16px;
color: variables.$white;
.selected-wrapper {
display: flex;
align-items: center;
iqser-round-checkbox .wrapper.inactive {
cursor: default;
}
.all-caps-label {
margin: 0 16px 0 8px;
opacity: 1;
}
}
}
.annotations-wrapper { .annotations-wrapper {
display: flex; display: flex;
height: 100%; height: 100%;

View File

@ -48,13 +48,13 @@ export class UserManagementComponent implements OnChanges {
private get _assignOrChangeReviewerTooltip(): string { private get _assignOrChangeReviewerTooltip(): string {
return this.file.assignee return this.file.assignee
? this.translateService.instant('file-preview.change-reviewer') ? this.translateService.instant(_('file-preview.change-reviewer'))
: this.translateService.instant('file-preview.assign-reviewer'); : this.translateService.instant(_('file-preview.assign-reviewer'));
} }
private get _assignTooltip(): string { private get _assignTooltip(): string {
return this.file.isUnderApproval return this.file.isUnderApproval
? this.translateService.instant('dossier-overview.assign-approver') ? this.translateService.instant(_('dossier-overview.assign-approver'))
: this._assignOrChangeReviewerTooltip; : this._assignOrChangeReviewerTooltip;
} }
@ -68,9 +68,9 @@ export class UserManagementComponent implements OnChanges {
} }
ngOnChanges() { ngOnChanges() {
this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.file, this.dossier); this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.file);
this.canAssignUser = this.permissionsService.canAssignUser(this.file, this.dossier); this.canAssignUser = this.permissionsService.canAssignUser(this.file);
this.canUnassignUser = this.permissionsService.canUnassignUser(this.file, this.dossier); this.canUnassignUser = this.permissionsService.canUnassignUser(this.file);
this.canAssignOrUnassign = this.canAssignUser || this.canUnassignUser; this.canAssignOrUnassign = this.canAssignUser || this.canUnassignUser;
this.canAssign = this.canAssignToSelf || this.canAssignOrUnassign; this.canAssign = this.canAssignToSelf || this.canAssignOrUnassign;

View File

@ -44,7 +44,6 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { clearStamps, stampPDFPage } from '@utils/page-stamper'; import { clearStamps, stampPDFPage } from '@utils/page-stamper';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { handleFilterDelta } from '@utils/filter-utils'; import { handleFilterDelta } from '@utils/filter-utils';
import { FileActionsComponent } from '../../shared/components/file-actions/file-actions.component';
import { FilesService } from '@services/entity-services/files.service'; import { FilesService } from '@services/entity-services/files.service';
import { DossiersService } from '@services/entity-services/dossiers.service'; import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.service'; import { FileManagementService } from '@services/entity-services/file-management.service';
@ -55,6 +54,7 @@ import { ExcludedPagesService } from './services/excluded-pages.service';
import { ViewModeService } from './services/view-mode.service'; import { ViewModeService } from './services/view-mode.service';
import { MultiSelectService } from './services/multi-select.service'; import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service'; import { DocumentInfoService } from './services/document-info.service';
import { ReanalysisService } from '../../../../services/reanalysis.service';
import Annotation = Core.Annotations.Annotation; import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet; import PDFNet = Core.PDFNet;
@ -78,7 +78,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
hideSkipped = false; hideSkipped = false;
displayPdfViewer = false; displayPdfViewer = false;
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent; @ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
@ViewChild('fileActions') fileActions: FileActionsComponent;
readonly dossierId: string; readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>; readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>; readonly dossier$: Observable<Dossier>;
@ -117,6 +116,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService, private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService, private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
readonly excludedPagesService: ExcludedPagesService, readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService, readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService, readonly multiSelectService: MultiSelectService,
@ -241,7 +241,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const file = this._filesMapService.get(this.dossierId, this.fileId); const file = this._filesMapService.get(this.dossierId, this.fileId);
if (file?.analysisRequired) { if (file?.analysisRequired) {
await this.fileActions.reanalyseFile(); await this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, true).toPromise();
} }
this.displayPdfViewer = true; this.displayPdfViewer = true;

View File

@ -16,153 +16,12 @@
</ng-container> </ng-container>
<ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress> <ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress>
<div *ngIf="file" class="file-actions" iqserHelpMode="document-features"> <div class="file-actions" iqserHelpMode="document-features">
<iqser-circle-button <redaction-expandable-file-actions
(action)="openDocument()" [actions]="buttons"
*ngIf="showOpenDocument" [buttonType]="buttonType"
[maxWidth]="maxWidth"
[tooltipPosition]="tooltipPosition" [tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.open-document' | translate" ></redaction-expandable-file-actions>
[type]="buttonType"
icon="iqser:collapse"
></iqser-circle-button>
<iqser-circle-button
(action)="openDeleteFileDialog($event)"
*ngIf="showDelete"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.delete.action' | translate"
[type]="buttonType"
icon="iqser:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="assign($event)"
*ngIf="showAssign"
[tooltipPosition]="tooltipPosition"
[tooltip]="assignTooltip | translate"
[type]="buttonType"
icon="red:assign"
></iqser-circle-button>
<iqser-circle-button
(action)="assignToMe($event)"
*ngIf="showAssignToSelf"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.assign-me' | translate"
[type]="buttonType"
icon="red:assign-me"
></iqser-circle-button>
<!-- download redacted file-->
<redaction-file-download-btn
[files]="[file]"
[tooltipClass]="'small'"
[tooltipPosition]="tooltipPosition"
[type]="buttonType"
></redaction-file-download-btn>
<iqser-circle-button
(action)="documentInfoService.toggle()"
*ngIf="documentInfoService"
[attr.aria-expanded]="documentInfoService.shown$ | async"
[tooltip]="'file-preview.document-info' | translate"
icon="red:status-info"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="excludedPagesService.toggle()"
*ngIf="excludedPagesService"
[attr.aria-expanded]="excludedPagesService.shown$ | async"
[showDot]="!!file.excludedPages?.length"
[tooltip]="'file-preview.exclude-pages' | translate"
icon="red:exclude-pages"
tooltipPosition="below"
></iqser-circle-button>
<!-- Ready for approval-->
<iqser-circle-button
(action)="setFileUnderApproval($event)"
*ngIf="showUnderApproval"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.under-approval' | translate"
[type]="buttonType"
icon="red:ready-for-approval"
></iqser-circle-button>
<!-- Back to review -->
<iqser-circle-button
(action)="setFileUnderReview($event, true)"
*ngIf="showUnderReview"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.under-review' | translate"
[type]="buttonType"
icon="red:undo"
></iqser-circle-button>
<!-- Approved-->
<iqser-circle-button
(action)="setFileApproved($event)"
*ngIf="showApprove"
[disabled]="!file.canBeApproved"
[tooltipPosition]="tooltipPosition"
[tooltip]="file.canBeApproved ? ('dossier-overview.approve' | translate) : ('dossier-overview.approve-disabled' | translate)"
[type]="buttonType"
icon="red:approved"
></iqser-circle-button>
<!-- Back to approval -->
<iqser-circle-button
(action)="setFileUnderApproval($event)"
*ngIf="showUndoApproval"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.under-approval' | translate"
[type]="buttonType"
icon="red:undo"
></iqser-circle-button>
<iqser-circle-button
(action)="ocrFile($event)"
*ngIf="showOCR"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.ocr-file' | translate"
[type]="buttonType"
icon="iqser:ocr"
></iqser-circle-button>
<!-- reanalyse file preview -->
<iqser-circle-button
(action)="reanalyseFile($event)"
*ngIf="canReanalyse && isFilePreview && analysisForced"
[tooltip]="'file-preview.reanalyse-notification' | translate"
[type]="circleButtonTypes.warn"
icon="iqser:refresh"
tooltipClass="warn small"
tooltipPosition="below"
></iqser-circle-button>
<!-- reanalyse file listing -->
<iqser-circle-button
(action)="reanalyseFile($event)"
*ngIf="canReanalyse && isDossierOverview && analysisForced"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.reanalyse.action' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:refresh"
></iqser-circle-button>
<!-- exclude from redaction -->
<div class="iqser-input-group">
<mat-slide-toggle
(change)="toggleAnalysis()"
(click)="$event.stopPropagation()"
[checked]="!file?.excluded"
[class.mr-24]="isDossierOverviewList"
[disabled]="!canToggleAnalysis"
[matTooltipPosition]="tooltipPosition"
[matTooltip]="toggleTooltip | translate"
color="primary"
></mat-slide-toggle>
</div>
</div> </div>
</ng-template> </ng-template>

View File

@ -3,9 +3,7 @@
.file-actions { .file-actions {
display: flex; display: flex;
overflow-y: auto;
color: variables.$grey-1; color: variables.$grey-1;
@include common-mixins.no-scroll-bar;
> *:not(:last-child) { > *:not(:last-child) {
margin-right: 2px; margin-right: 2px;
@ -27,14 +25,6 @@ iqser-status-bar {
margin-left: 2px; margin-left: 2px;
} }
mat-slide-toggle {
height: 34px;
width: 34px;
line-height: 33px;
margin-left: 8px;
margin-right: 5px;
}
.spinning-icon { .spinning-icon {
margin: 0 12px 0 11px; margin: 0 12px 0 11px;
} }

View File

@ -1,6 +1,18 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Optional, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostBinding,
Input,
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
ViewChild,
} from '@angular/core';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { File } from '@red/domain'; import { Action, ActionTypes, File } from '@red/domain';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service'; import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { import {
AutoUnsubscribe, AutoUnsubscribe,
@ -25,6 +37,7 @@ import { Router } from '@angular/router';
import { ExcludedPagesService } from '../../../screens/file-preview-screen/services/excluded-pages.service'; import { ExcludedPagesService } from '../../../screens/file-preview-screen/services/excluded-pages.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { DocumentInfoService } from '../../../screens/file-preview-screen/services/document-info.service'; import { DocumentInfoService } from '../../../screens/file-preview-screen/services/document-info.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
@Component({ @Component({
selector: 'redaction-file-actions', selector: 'redaction-file-actions',
@ -36,8 +49,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser; readonly currentUser = this._userService.currentUser;
@Input() file: File; @Input() @Required() file: File;
@Input() @Required() type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow'; @Input() @Required() type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
@Input() maxWidth: number;
@Output() readonly ocredFile = new EventEmitter<void>(); @Output() readonly ocredFile = new EventEmitter<void>();
toggleTooltip?: string; toggleTooltip?: string;
@ -56,16 +70,20 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
canToggleAnalysis: boolean; canToggleAnalysis: boolean;
showStatusBar: boolean; showStatusBar: boolean;
showOpenDocument: boolean; showOpenDocument: boolean;
showReanalyseFilePreview: boolean;
showReanalyseDossierOverview: boolean;
analysisForced: boolean; analysisForced: boolean;
isDossierOverview = false; isDossierOverview = false;
isDossierOverviewList = false; isDossierOverviewList = false;
isDossierOverviewWorkflow = false; isDossierOverviewWorkflow = false;
isFilePreview = false; isFilePreview = false;
tooltipPosition: IqserTooltipPosition; tooltipPosition: IqserTooltipPosition;
buttons: Action[];
@ViewChild(ExpandableFileActionsComponent) _expandableActionsComponent: ExpandableFileActionsComponent;
constructor( constructor(
@Optional() readonly excludedPagesService: ExcludedPagesService, @Optional() private readonly _excludedPagesService: ExcludedPagesService,
@Optional() readonly documentInfoService: DocumentInfoService, @Optional() private readonly _documentInfoService: DocumentInfoService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _dossiersService: DossiersService, private readonly _dossiersService: DossiersService,
private readonly _dialogService: DossiersDialogService, private readonly _dialogService: DossiersDialogService,
@ -82,6 +100,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
super(); super();
} }
@HostBinding('class.keep-visible') get expanded() {
return this._expandableActionsComponent?.expanded;
}
private get _toggleTooltip(): string { private get _toggleTooltip(): string {
if (!this.currentUser.isManager) { if (!this.currentUser.isManager) {
return _('file-preview.toggle-analysis.only-managers'); return _('file-preview.toggle-analysis.only-managers');
@ -90,6 +112,116 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
return this.file?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable'); return this.file?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
} }
private get _buttons(): Action[] {
return [
{
type: ActionTypes.circleBtn,
action: $event => this._openDeleteFileDialog($event),
tooltip: _('dossier-overview.delete.action'),
icon: 'iqser:trash',
show: this.showDelete,
},
{
type: ActionTypes.circleBtn,
action: $event => this._assign($event),
tooltip: this.assignTooltip,
icon: 'red:assign',
show: this.showAssign,
},
{
type: ActionTypes.circleBtn,
action: $event => this._assignToMe($event),
tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me',
show: this.showAssignToSelf,
},
{
type: ActionTypes.downloadBtn,
show: true,
files: [this.file],
tooltipClass: 'small',
},
{
type: ActionTypes.circleBtn,
action: () => this._documentInfoService.toggle(),
tooltip: _('file-preview.document-info'),
ariaExpanded: this._documentInfoService?.shown$,
icon: 'red:status-info',
show: !!this._documentInfoService,
},
{
type: ActionTypes.circleBtn,
action: () => this._excludedPagesService.toggle(),
tooltip: _('file-preview.exclude-pages'),
ariaExpanded: this._excludedPagesService?.shown$,
showDot: !!this.file.excludedPages?.length,
icon: 'red:exclude-pages',
show: !!this._excludedPagesService,
},
{
type: ActionTypes.circleBtn,
action: $event => this._setFileUnderApproval($event),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval',
show: this.showUnderApproval,
},
{
type: ActionTypes.circleBtn,
action: $event => this._setFileUnderReview($event),
tooltip: _('dossier-overview.under-review'),
icon: 'red:undo',
show: this.showUnderReview,
},
{
type: ActionTypes.circleBtn,
action: $event => this.setFileApproved($event),
tooltip: this.file.canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved',
disabled: !this.file.canBeApproved,
show: this.showApprove,
},
{
type: ActionTypes.circleBtn,
action: $event => this._setFileUnderApproval($event),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo',
show: this.showUndoApproval,
},
{
type: ActionTypes.circleBtn,
action: $event => this._ocrFile($event),
tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr',
show: this.showOCR,
},
{
type: ActionTypes.circleBtn,
action: $event => this._reanalyseFile($event),
tooltip: _('file-preview.reanalyse-notification'),
buttonType: CircleButtonTypes.warn,
tooltipClass: 'warn small',
icon: 'iqser:refresh',
show: this.showReanalyseFilePreview,
},
{
type: ActionTypes.circleBtn,
action: $event => this._reanalyseFile($event),
tooltip: _('dossier-overview.reanalyse.action'),
icon: 'iqser:refresh',
show: this.showReanalyseDossierOverview,
},
{
type: ActionTypes.toggle,
action: () => this._toggleAnalysis(),
disabled: !this.canToggleAnalysis,
tooltip: this.toggleTooltip,
class: { 'mr-24': this.isDossierOverviewList },
checked: !this.file.excluded,
show: true,
},
].filter(btn => btn.show);
}
ngOnInit() { ngOnInit() {
this._dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this._setup())); this._dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this._setup()));
} }
@ -98,57 +230,6 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
this._setup(); this._setup();
} }
openDocument() {
this._router.navigate([this.file.routerLink]).then();
}
openDeleteFileDialog($event: MouseEvent) {
this._dialogService.openDialog(
'confirm',
$event,
new ConfirmationDialogInput({
title: _('confirmation-dialog.delete-file.title'),
question: _('confirmation-dialog.delete-file.question'),
}),
async () => {
this._loadingService.start();
try {
const dossier = this._dossiersService.find(this.file.dossierId);
await this._fileManagementService.delete([this.file.fileId], this.file.dossierId).toPromise();
await this._router.navigate([dossier.routerLink]);
} catch (error) {
this._toaster.error(_('error.http.generic'), { params: error });
}
this._loadingService.stop();
},
);
}
assign($event: MouseEvent) {
const mode = this.file.isUnderApproval ? 'approver' : 'reviewer';
const files = [this.file];
const withCurrentUserAsDefault = true;
const withUnassignedOption = true;
this._dialogService.openDialog('assignFile', $event, { mode, files, withCurrentUserAsDefault, withUnassignedOption });
}
async assignToMe($event: MouseEvent) {
$event.stopPropagation();
await this._fileAssignService.assignToMe([this.file]);
}
async reanalyseFile($event?: MouseEvent) {
if ($event) {
$event.stopPropagation();
}
await this._reanalysisService.reanalyzeFilesForDossier([this.file.fileId], this.file.dossierId, true).toPromise();
}
async setFileUnderApproval($event: MouseEvent) {
$event.stopPropagation();
await this._fileAssignService.assignApprover($event, this.file, true);
}
async setFileApproved($event: MouseEvent) { async setFileApproved($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
if (!this.file.hasUpdates) { if (!this.file.hasUpdates) {
@ -169,7 +250,59 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
); );
} }
async ocrFile($event: MouseEvent) { forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
this._setup();
}
private _openDeleteFileDialog($event: MouseEvent) {
this._dialogService.openDialog(
'confirm',
$event,
new ConfirmationDialogInput({
title: _('confirmation-dialog.delete-file.title'),
question: _('confirmation-dialog.delete-file.question'),
}),
async () => {
this._loadingService.start();
try {
const dossier = this._dossiersService.find(this.file.dossierId);
await this._fileManagementService.delete([this.file.fileId], this.file.dossierId).toPromise();
await this._router.navigate([dossier.routerLink]);
} catch (error) {
this._toaster.error(_('error.http.generic'), { params: error });
}
this._loadingService.stop();
},
);
}
private _assign($event: MouseEvent) {
const mode = this.file.isUnderApproval ? 'approver' : 'reviewer';
const files = [this.file];
const withCurrentUserAsDefault = true;
const withUnassignedOption = true;
this._dialogService.openDialog('assignFile', $event, { mode, files, withCurrentUserAsDefault, withUnassignedOption });
}
private async _assignToMe($event: MouseEvent) {
$event.stopPropagation();
await this._fileAssignService.assignToMe([this.file]);
}
private async _reanalyseFile($event?: MouseEvent) {
if ($event) {
$event.stopPropagation();
}
await this._reanalysisService.reanalyzeFilesForDossier([this.file.fileId], this.file.dossierId, true).toPromise();
}
private async _setFileUnderApproval($event: MouseEvent) {
$event.stopPropagation();
await this._fileAssignService.assignApprover($event, this.file, true);
}
private async _ocrFile($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
this._loadingService.start(); this._loadingService.start();
await this._reanalysisService.ocrFiles([this.file.fileId], this.file.dossierId).toPromise(); await this._reanalysisService.ocrFiles([this.file.fileId], this.file.dossierId).toPromise();
@ -177,20 +310,16 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
this._loadingService.stop(); this._loadingService.stop();
} }
async setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) { private async _setFileUnderReview($event: MouseEvent) {
await this._fileAssignService.assignReviewer($event, this.file, ignoreDialogChanges); await this._fileAssignService.assignReviewer($event, this.file, true);
} }
async toggleAnalysis() { private async _toggleAnalysis() {
this._loadingService.start(); this._loadingService.start();
await this._reanalysisService.toggleAnalysis(this.file.dossierId, this.file.fileId, !this.file.excluded).toPromise(); await this._reanalysisService.toggleAnalysis(this.file.dossierId, this.file.fileId, !this.file.excluded).toPromise();
this._loadingService.stop(); this._loadingService.stop();
} }
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
}
private _setup() { private _setup() {
this.isDossierOverviewList = this.type === 'dossier-overview-list'; this.isDossierOverviewList = this.type === 'dossier-overview-list';
this.isDossierOverviewWorkflow = this.type === 'dossier-overview-workflow'; this.isDossierOverviewWorkflow = this.type === 'dossier-overview-workflow';
@ -220,7 +349,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
(this._permissionsService.canAssignUser(this.file) || this._permissionsService.canUnassignUser(this.file)) && (this._permissionsService.canAssignUser(this.file) || this._permissionsService.canUnassignUser(this.file)) &&
this.isDossierOverview; this.isDossierOverview;
this.showOpenDocument = this.file.canBeOpened && this.isDossierOverviewWorkflow; this.showReanalyseFilePreview = this.canReanalyse && this.isFilePreview && this.analysisForced;
this.showReanalyseDossierOverview = this.canReanalyse && this.isDossierOverview && this.analysisForced;
this.buttons = this._buttons;
} }
private async _setFileApproved() { private async _setFileApproved() {

View File

@ -3,7 +3,7 @@
[disabled]="disabled || !canDownloadFiles" [disabled]="disabled || !canDownloadFiles"
[tooltipClass]="tooltipClass" [tooltipClass]="tooltipClass"
[tooltipPosition]="tooltipPosition" [tooltipPosition]="tooltipPosition"
[tooltip]="tooltip" [tooltip]="tooltip | translate: { count: this.files.length }"
[type]="type" [type]="type"
icon="iqser:download" icon="iqser:download"
></iqser-circle-button> ></iqser-circle-button>

View File

@ -1,9 +1,8 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { File } from '@red/domain'; import { File } from '@red/domain';
import { FileDownloadService } from '@upload-download/services/file-download.service'; import { FileDownloadService } from '@upload-download/services/file-download.service';
import { CircleButtonType, CircleButtonTypes, List, Toaster } from '@iqser/common-ui'; import { CircleButtonType, CircleButtonTypes, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export type MenuState = 'OPEN' | 'CLOSED'; export type MenuState = 'OPEN' | 'CLOSED';
@ -14,28 +13,25 @@ export type MenuState = 'OPEN' | 'CLOSED';
styleUrls: ['./file-download-btn.component.scss'], styleUrls: ['./file-download-btn.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FileDownloadBtnComponent { export class FileDownloadBtnComponent implements OnChanges {
@Input() files: List<File>; @Input() files: File[];
@Input() tooltipPosition: 'above' | 'below' | 'before' | 'after' = 'above'; @Input() tooltipPosition: 'above' | 'below' | 'before' | 'after' = 'above';
@Input() type: CircleButtonType = CircleButtonTypes.default; @Input() type: CircleButtonType = CircleButtonTypes.default;
@Input() tooltipClass: string; @Input() tooltipClass: string;
@Input() disabled = false; @Input() disabled = false;
tooltip: string;
canDownloadFiles: boolean;
constructor( constructor(
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _fileDownloadService: FileDownloadService, private readonly _fileDownloadService: FileDownloadService,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
) {} ) {}
get canDownloadFiles() { ngOnChanges(): void {
return this.files.length > 0 && this.files.reduce((acc, file) => acc && this._permissionsService.canDownloadFiles(file), true); this.canDownloadFiles = this._permissionsService.canDownloadFiles(this.files);
} this.tooltip = this.canDownloadFiles ? _('dossier-overview.download-file') : _('dossier-overview.download-file-disabled');
get tooltip() {
return this.canDownloadFiles
? this._translateService.instant('dossier-overview.download-file')
: this._translateService.instant('dossier-overview.download-file-disabled', { count: this.files.length });
} }
async downloadFiles($event: MouseEvent) { async downloadFiles($event: MouseEvent) {

View File

@ -0,0 +1,64 @@
<ng-container *ngFor="let btn of displayedButtons">
<iqser-circle-button
(action)="btn.action($event)"
*ngIf="btn.type === 'circleBtn'"
[attr.aria-expanded]="btn.ariaExpanded && btn.ariaExpanded | async"
[disabled]="btn.disabled"
[icon]="btn.icon"
[showDot]="btn.showDot"
[tooltipClass]="btn.tooltipClass"
[tooltipPosition]="tooltipPosition"
[tooltip]="btn.tooltip | translate"
[type]="btn.buttonType || buttonType"
></iqser-circle-button>
<!-- download redacted file-->
<redaction-file-download-btn
*ngIf="btn.type === 'downloadBtn'"
[files]="btn.files"
[tooltipClass]="btn.tooltipClass"
[tooltipPosition]="tooltipPosition"
[type]="buttonType"
></redaction-file-download-btn>
<!-- exclude from redaction -->
<div *ngIf="btn.type === 'toggle'" class="iqser-input-group">
<mat-slide-toggle
(change)="btn.action()"
(click)="$event.stopPropagation()"
[checked]="btn.checked"
[disabled]="btn.disabled"
[matTooltipPosition]="tooltipPosition"
[matTooltip]="btn.tooltip | translate"
[ngClass]="btn.class"
color="primary"
></mat-slide-toggle>
</div>
</ng-container>
<iqser-circle-button
(menuClosed)="expanded = false"
(menuOpened)="expanded = true"
*ngIf="hiddenButtons.length > 0"
[attr.aria-expanded]="expanded"
[icon]="'iqser:more-actions'"
[matMenuTriggerFor]="hiddenButtonsMenu"
[type]="buttonType"
></iqser-circle-button>
<mat-menu #hiddenButtonsMenu="matMenu" class="padding-bottom-8">
<button (click)="btn.action($event)" *ngFor="let btn of hiddenButtons" [disabled]="btn.disabled" mat-menu-item>
<ng-container *ngIf="btn.type === 'circleBtn'">
<mat-icon [svgIcon]="btn.icon"></mat-icon>
{{ btn.tooltip | translate }}
</ng-container>
<ng-container *ngIf="btn.type === 'downloadBtn'">
<mat-icon svgIcon="iqser:download"></mat-icon>
{{ 'dossier-overview.download-file' | translate }}
</ng-container>
<ng-container *ngIf="btn.type === 'toggle'">
<mat-slide-toggle [checked]="btn.checked" [disabled]="btn.disabled" class="ml-0" color="primary"></mat-slide-toggle>
{{ btn.tooltip | translate }}
</ng-container>
</button>
</mat-menu>

View File

@ -0,0 +1,27 @@
:host {
display: contents;
}
mat-slide-toggle {
height: 34px;
width: 34px;
line-height: 33px;
margin-left: 8px;
margin-right: 5px;
}
.mat-menu-item {
mat-icon {
color: inherit;
width: 14px;
height: 14px;
}
&[disabled] {
color: rgba(var(--iqser-accent-rgb), 0.3);
}
}
.ml-0 {
margin-left: 0;
}

View File

@ -0,0 +1,64 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Action, ActionTypes, File } from '@red/domain';
import { CircleButtonType, IqserTooltipPosition, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { PermissionsService } from '@services/permissions.service';
@Component({
selector: 'redaction-expandable-file-actions',
templateUrl: './expandable-file-actions.component.html',
styleUrls: ['./expandable-file-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpandableFileActionsComponent implements OnChanges {
@Input() maxWidth: number;
@Input() actions: Action[];
@Input() buttonType: CircleButtonType;
@Input() tooltipPosition: IqserTooltipPosition;
displayedButtons: Action[];
hiddenButtons: Action[];
expanded = false;
constructor(
private readonly _fileDownloadService: FileDownloadService,
private readonly _toaster: Toaster,
private readonly _permissionsService: PermissionsService,
) {}
ngOnChanges(changes: SimpleChanges) {
if (changes.actions || changes.maxWidth) {
if (this.maxWidth) {
const count = Math.floor(this.maxWidth / 36) || 1;
if (count >= this.actions.length) {
this.displayedButtons = [...this.actions];
this.hiddenButtons = [];
} else {
this.displayedButtons = this.actions.slice(0, count - 1);
this.hiddenButtons = this.actions.slice(count - 1);
}
} else {
this.displayedButtons = [...this.actions];
this.hiddenButtons = [];
}
}
if (changes.actions) {
// Patch download button
const downloadBtn = this.actions.find(btn => btn.type === ActionTypes.downloadBtn);
if (downloadBtn) {
downloadBtn.action = $event => this._downloadFiles($event, downloadBtn.files);
downloadBtn.disabled = !this._permissionsService.canDownloadFiles(downloadBtn.files);
}
}
}
private async _downloadFiles($event: MouseEvent, files: File[]) {
$event.stopPropagation();
const dossierId = files[0].dossierId;
const filesIds = files.map(f => f.fileId);
await this._fileDownloadService.downloadFiles(filesIds, dossierId).toPromise();
this._toaster.info(_('download-status.queued'));
}
}

View File

@ -25,6 +25,7 @@ import { NamePipe } from './pipes/name.pipe';
import { TypeFilterComponent } from './components/type-filter/type-filter.component'; import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { TeamMembersComponent } from './components/team-members/team-members.component'; import { TeamMembersComponent } from './components/team-members/team-members.component';
import { EditorComponent } from './components/editor/editor.component'; import { EditorComponent } from './components/editor/editor.component';
import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent]; const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -39,6 +40,7 @@ const components = [
AssignUserDropdownComponent, AssignUserDropdownComponent,
TypeFilterComponent, TypeFilterComponent,
TeamMembersComponent, TeamMembersComponent,
ExpandableFileActionsComponent,
...buttons, ...buttons,
]; ];

View File

@ -22,58 +22,57 @@ export class PermissionsService {
return this.isReviewerOrApprover(file) && (file.isNew || file.isUnderReview || file.isUnderApproval); return this.isReviewerOrApprover(file) && (file.isNew || file.isUnderReview || file.isUnderApproval);
} }
canReanalyseFile(file: File): boolean { canReanalyseFile(file: File | File[]): boolean {
return this.isReviewerOrApprover(file) || file.isNew || (file.isError && file.isNew); const files = file instanceof File ? [file] : file;
return files.reduce((acc, _file) => this._canReanalyseFile(_file) && acc, true);
} }
isFileAssignee(file: File): boolean { isFileAssignee(file: File): boolean {
return file.assignee === this._userService.currentUser.id; return file.assignee === this._userService.currentUser.id;
} }
// https://jira.iqser.com/browse/RED-2787 canDeleteFile(file: File | File[]): boolean {
canDeleteFile(file: File): boolean { const files = file instanceof File ? [file] : file;
const dossier = this._getDossier(file); const dossier = this._getDossier(files[0]);
return ( return files.reduce((acc, _file) => this._canDeleteFile(_file, dossier) && acc, true);
file.isNew ||
(file.isUnderReview && !file.assignee && this.isDossierMember(dossier)) ||
(file.isUnderApproval && !file.assignee && this.isApprover(dossier)) ||
(file.assignee && !file.isApproved && (this.isFileAssignee(file) || this.isOwner(dossier)))
);
} }
canAssignToSelf(file: File, dossier = this._getDossier(file)): boolean { canAssignToSelf(file: File | File[]): boolean {
const precondition = this.isDossierMember(dossier) && !this.isFileAssignee(file) && !file.isError && !file.isProcessing; const files = file instanceof File ? [file] : file;
return precondition && (file.isNew || file.isUnderReview || (file.isUnderApproval && this.isApprover(dossier))); const dossier = this._getDossier(files[0]);
return files.reduce((acc, _file) => this._canAssignToSelf(_file, dossier) && acc, true);
} }
canAssignUser(file: File, dossier = this._getDossier(file)): boolean { canAssignUser(file: File | File[]): boolean {
const precondition = !file.isProcessing && !file.isError && !file.isApproved && this.isApprover(dossier); const files = file instanceof File ? [file] : file;
const dossier = this._getDossier(files[0]);
if (precondition) { return files.reduce((acc, _file) => this._canAssignUser(_file, dossier) && acc, true);
if ((file.isNew || file.isUnderReview) && dossier.hasReviewers) {
return true;
}
if (file.isUnderApproval && dossier.approverIds.length > 1) {
return true;
}
}
return false;
} }
canUnassignUser(file: File, dossier = this._getDossier(file)): boolean { canUnassignUser(file: File | File[]): boolean {
return (file.isUnderReview || file.isUnderApproval) && (this.isFileAssignee(file) || this.isApprover(dossier)); const files = file instanceof File ? [file] : file;
const dossier = this._getDossier(files[0]);
return files.reduce((acc, _file) => this._canUnassignUser(_file, dossier) && acc, true);
} }
canSetUnderReview(file: File, dossier = this._getDossier(file)): boolean { canSetUnderReview(file: File | File[]): boolean {
return file.isUnderApproval && this.isApprover(dossier); const files = file instanceof File ? [file] : file;
const dossier = this._getDossier(files[0]);
return this.isApprover(dossier) && files.reduce((acc, _file) => this._canSetUnderReview(_file) && acc, true);
} }
isReadyForApproval(file: File): boolean { canBeApproved(file: File | File[]): boolean {
return this.canSetUnderReview(file); const files = file instanceof File ? [file] : file;
return files.reduce((acc, _file) => this._canBeApproved(_file) && acc, true);
} }
canSetUnderApproval(file: File): boolean { isReadyForApproval(files: File | File[]): boolean {
return file.isUnderReview && this.isReviewerOrApprover(file); return this.canSetUnderReview(files);
}
canSetUnderApproval(file: File | File[]): boolean {
const files = file instanceof File ? [file] : file;
return files.reduce((acc, _file) => this._canSetUnderApproval(_file) && acc, true);
} }
isOwner(dossier: Dossier, user = this._userService.currentUser): boolean { isOwner(dossier: Dossier, user = this._userService.currentUser): boolean {
@ -97,21 +96,22 @@ export class PermissionsService {
return (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file); return (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file);
} }
canUndoApproval(file: File): boolean { canUndoApproval(file: File | File[]): boolean {
return file.isApproved && this.isApprover(this._getDossier(file)); const files = file instanceof File ? [file] : file;
const dossier = this._getDossier(files[0]);
return files.reduce((acc, _file) => this._canUndoApproval(_file, dossier) && acc, true);
} }
canMarkPagesAsViewed(file: File): boolean { canMarkPagesAsViewed(file: File): boolean {
return (file.isUnderReview || file.isUnderApproval) && this.isFileAssignee(file); return (file.isUnderReview || file.isUnderApproval) && this.isFileAssignee(file);
} }
canDownloadFiles(file: File): boolean { canDownloadFiles(files: File[]): boolean {
const dossier = this._getDossier(file); if (files.length === 0) {
if (!dossier) {
return false; return false;
} }
const dossier = this._getDossier(files[0]);
return file.isApproved && this.isApprover(dossier); return this.isApprover(dossier) && files.reduce((prev, file) => prev && file.isApproved, true);
} }
canDeleteDossier(dossier: Dossier): boolean { canDeleteDossier(dossier: Dossier): boolean {
@ -136,6 +136,59 @@ export class PermissionsService {
return (comment.user === this._userService.currentUser.id || this.isApprover(dossier)) && !file.isApproved; return (comment.user === this._userService.currentUser.id || this.isApprover(dossier)) && !file.isApproved;
} }
// https://jira.iqser.com/browse/RED-2787
private _canDeleteFile(file: File, dossier: Dossier): boolean {
return (
file.isNew ||
(file.isUnderReview && !file.assignee && this.isDossierMember(dossier)) ||
(file.isUnderApproval && !file.assignee && this.isApprover(dossier)) ||
(file.assignee && !file.isApproved && (this.isFileAssignee(file) || this.isOwner(dossier)))
);
}
private _canReanalyseFile(file: File): boolean {
return this.isReviewerOrApprover(file) || file.isNew || (file.isError && file.isNew);
}
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)));
}
private _canSetUnderApproval(file: File): boolean {
return file.isUnderReview && this.isReviewerOrApprover(file);
}
private _canUndoApproval(file: File, dossier: Dossier): boolean {
return file.isApproved && this.isApprover(dossier);
}
private _canBeApproved(file: File): boolean {
return file.canBeApproved;
}
private _canAssignUser(file: File, dossier: Dossier) {
const precondition = !file.isProcessing && !file.isError && !file.isApproved && this.isApprover(dossier);
if (precondition) {
if ((file.isNew || file.isUnderReview) && dossier.hasReviewers) {
return true;
}
if (file.isUnderApproval && dossier.approverIds.length > 1) {
return true;
}
}
return false;
}
private _canUnassignUser(file: File, dossier: Dossier) {
return (file.isUnderReview || file.isUnderApproval) && (this.isFileAssignee(file) || this.isApprover(dossier));
}
private _canSetUnderReview(file: File): boolean {
return file.isUnderApproval;
}
private _getDossier(file: File): Dossier { private _getDossier(file: File): Dossier {
return this._dossiersService.find(file.dossierId); return this._dossiersService.find(file.dossierId);
} }

View File

@ -45,8 +45,8 @@ export class ReanalysisService extends GenericService<unknown> {
} }
@Validate() @Validate()
ocrFiles(@RequiredParam() body: List, @RequiredParam() dossierId: string) { ocrFiles(@RequiredParam() fileIds: List, @RequiredParam() dossierId: string) {
return this._post(body, `ocr/reanalyze/${dossierId}/bulk`).pipe(switchMap(() => this._filesService.loadAll(dossierId))); return this._post(fileIds, `ocr/reanalyze/${dossierId}/bulk`).pipe(switchMap(() => this._filesService.loadAll(dossierId)));
} }
@Validate() @Validate()

View File

@ -728,7 +728,6 @@
}, },
"ocr-file": "OCR Document", "ocr-file": "OCR Document",
"ocr-performed": "OCR was performed for this file.", "ocr-performed": "OCR was performed for this file.",
"open-document": "Open Document",
"quick-filters": { "quick-filters": {
"assigned-to-me": "Assigned to me", "assigned-to-me": "Assigned to me",
"assigned-to-others": "Assigned to others", "assigned-to-others": "Assigned to others",
@ -1653,5 +1652,13 @@
}, },
"title": "Watermark" "title": "Watermark"
}, },
"workflow": {
"selection": {
"all": "All",
"count": "{count} selected",
"none": "None",
"select": "Select"
}
},
"yesterday": "Yesterday" "yesterday": "Yesterday"
} }

@ -1 +1 @@
Subproject commit fb53b06cfc8fa2c846e7d1a173ad6f0e19f90d02 Subproject commit 28a6b735f700351f36c2583de4b581fe5fb8106b

View File

@ -45,7 +45,6 @@ export class File extends Entity<IFile> implements IFile {
readonly hintsOnly: boolean; readonly hintsOnly: boolean;
readonly hasNone: boolean; readonly hasNone: boolean;
readonly isNew: boolean; readonly isNew: boolean;
// readonly isUnassigned: boolean;
readonly isError: boolean; readonly isError: boolean;
readonly isProcessing: boolean; readonly isProcessing: boolean;
readonly isApproved: boolean; readonly isApproved: boolean;

View File

@ -0,0 +1,27 @@
import { Observable } from 'rxjs';
import { CircleButtonType } from '@iqser/common-ui';
import { File } from '@red/domain';
export type ActionType = 'circleBtn' | 'downloadBtn' | 'toggle';
export const ActionTypes = {
circleBtn: 'circleBtn' as ActionType,
downloadBtn: 'downloadBtn' as ActionType,
toggle: 'toggle' as ActionType,
};
export interface Action {
action?: (...args: any[]) => void;
tooltip?: string;
icon?: string;
show?: boolean;
ariaExpanded?: Observable<boolean>;
showDot?: boolean;
disabled?: boolean;
buttonType?: CircleButtonType;
tooltipClass?: string;
checked?: boolean;
class?: { [key: string]: boolean };
files?: File[];
type: ActionType;
}

View File

@ -5,3 +5,4 @@ export * from './watermark';
export * from './default-color-type'; export * from './default-color-type';
export * from './colors'; export * from './colors';
export * from './view-mode'; export * from './view-mode';
export * from './expandable-file-actions';

View File

@ -69,7 +69,6 @@
"@angular/cli": "13.0.3", "@angular/cli": "13.0.3",
"@angular/compiler-cli": "13.0.2", "@angular/compiler-cli": "13.0.2",
"@angular/language-service": "13.0.2", "@angular/language-service": "13.0.2",
"@biesbjerg/ngx-translate-extract": "^7.0.4",
"@nrwl/cli": "13.2.3", "@nrwl/cli": "13.2.3",
"@nrwl/cypress": "13.2.3", "@nrwl/cypress": "13.2.3",
"@nrwl/eslint-plugin-nx": "13.2.3", "@nrwl/eslint-plugin-nx": "13.2.3",
@ -84,6 +83,7 @@
"@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0", "@typescript-eslint/parser": "4.33.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"@bartholomej/ngx-translate-extract": "^8.0.1",
"cypress": "^6.9.1", "cypress": "^6.9.1",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "^5.0.8",
"cypress-keycloak": "^1.7.0", "cypress-keycloak": "^1.7.0",

154
yarn.lock
View File

@ -225,7 +225,7 @@
tslib "^2.3.0" tslib "^2.3.0"
yargs "^17.2.1" yargs "^17.2.1"
"@angular/compiler@13.0.2": "@angular/compiler@13.0.2", "@angular/compiler@^13.0.2":
version "13.0.2" version "13.0.2"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.0.2.tgz#5bc1bfc1931f1ff2813f8fff8b8ceaa57b47d717" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.0.2.tgz#5bc1bfc1931f1ff2813f8fff8b8ceaa57b47d717"
integrity sha512-EvIFT8y5VNICrnPgiamv/z9hfQ7KjLCM52g4ssXGCeGPVj58OEfslEc3jO4BCJG7xuLm7dCuSRV0pBlJNTSYFg== integrity sha512-EvIFT8y5VNICrnPgiamv/z9hfQ7KjLCM52g4ssXGCeGPVj58OEfslEc3jO4BCJG7xuLm7dCuSRV0pBlJNTSYFg==
@ -1283,6 +1283,23 @@
"@babel/helper-validator-identifier" "^7.15.7" "@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@bartholomej/ngx-translate-extract@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@bartholomej/ngx-translate-extract/-/ngx-translate-extract-8.0.1.tgz#4d9cc6ffbc2ce7f34d88cd15b28da2f382f58f43"
integrity sha512-mf/G8Xjz865xjLekZ6U0q5g6BO9ZF++tn3bDs1bPPOAKCw2BjBmwbMOftfGOdHYrVcSTa0yl2sqafZqYa4+waA==
dependencies:
"@angular/compiler" "^13.0.2"
"@phenomnomnominal/tsquery" "^4.1.1"
boxen "^6.2.1"
colorette "^2.0.16"
flat "^5.0.2"
gettext-parser "^4.2.0"
glob "^7.2.0"
mkdirp "^1.0.4"
path "^0.12.7"
terminal-link "^3.0.0"
yargs "^17.2.1"
"@bcoe/v8-coverage@^0.2.3": "@bcoe/v8-coverage@^0.2.3":
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -1295,22 +1312,6 @@
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@biesbjerg/ngx-translate-extract@^7.0.4":
version "7.0.4"
resolved "https://registry.yarnpkg.com/@biesbjerg/ngx-translate-extract/-/ngx-translate-extract-7.0.4.tgz#98190aa798dfe78a9f33904256891e76634fd52c"
integrity sha512-33hR94Fu26LK7Z+ImW2IdZiHfOcAzyIs1CdkUXg/536z2MqxBYqPoI9Ghsk6RTEfnsGa65wMgOcDXn7Ilhp8ew==
dependencies:
"@phenomnomnominal/tsquery" "^4.1.1"
boxen "^5.0.1"
colorette "^1.2.2"
flat "^5.0.2"
gettext-parser "^4.0.4"
glob "^7.1.6"
mkdirp "^1.0.4"
path "^0.12.7"
terminal-link "^2.1.1"
yargs "^16.2.0"
"@cspotcode/source-map-consumer@0.8.0": "@cspotcode/source-map-consumer@0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
@ -2792,7 +2793,7 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0:
require-from-string "^2.0.2" require-from-string "^2.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
ansi-align@^3.0.0: ansi-align@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
@ -2816,6 +2817,13 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
dependencies: dependencies:
type-fest "^0.21.3" type-fest "^0.21.3"
ansi-escapes@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-5.0.0.tgz#b6a0caf0eef0c41af190e9a749e0c00ec04bb2a6"
integrity sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==
dependencies:
type-fest "^1.0.2"
ansi-gray@^0.1.1: ansi-gray@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251"
@ -2872,6 +2880,11 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3"
integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==
ansi-wrap@0.1.0: ansi-wrap@0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
@ -3377,19 +3390,19 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
boxen@^5.0.1: boxen@^6.2.1:
version "5.1.2" version "6.2.1"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d"
integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==
dependencies: dependencies:
ansi-align "^3.0.0" ansi-align "^3.0.1"
camelcase "^6.2.0" camelcase "^6.2.0"
chalk "^4.1.0" chalk "^4.1.2"
cli-boxes "^2.2.1" cli-boxes "^3.0.0"
string-width "^4.2.2" string-width "^5.0.1"
type-fest "^0.20.2" type-fest "^2.5.0"
widest-line "^3.1.0" widest-line "^4.0.1"
wrap-ansi "^7.0.0" wrap-ansi "^8.0.1"
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
@ -3747,7 +3760,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -3862,10 +3875,10 @@ clean-stack@^2.0.0:
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-boxes@^2.2.1: cli-boxes@^3.0.0:
version "2.2.1" version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
cli-cursor@^1.0.2: cli-cursor@^1.0.2:
version "1.0.2" version "1.0.2"
@ -4008,11 +4021,6 @@ color-support@^1.1.2, color-support@^1.1.3:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
colorette@^1.2.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
colorette@^2.0.10, colorette@^2.0.16: colorette@^2.0.10, colorette@^2.0.16:
version "2.0.16" version "2.0.16"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
@ -5173,6 +5181,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
emojis-list@^3.0.0: emojis-list@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -6628,7 +6641,7 @@ getpass@^0.1.1:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
gettext-parser@^4.0.4: gettext-parser@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-4.2.0.tgz#9327140f76b122d44f0e8cb9338fd855667d9434" resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-4.2.0.tgz#9327140f76b122d44f0e8cb9338fd855667d9434"
integrity sha512-aMgPyjC9W5Mz9tbFU8DcQ7GYMXoFWq633kaWGt4imlcpBWzDIWk7HY7nCSZTCJxyjRaLq9L/NEjMKkZ9gR630Q== integrity sha512-aMgPyjC9W5Mz9tbFU8DcQ7GYMXoFWq633kaWGt4imlcpBWzDIWk7HY7nCSZTCJxyjRaLq9L/NEjMKkZ9gR630Q==
@ -6677,7 +6690,7 @@ glob@7.1.4:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@ -7465,6 +7478,11 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-fullwidth-code-point@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88"
integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==
is-generator-fn@^2.0.0: is-generator-fn@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
@ -12128,7 +12146,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -12145,6 +12163,15 @@ string-width@^2.1.1:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0" strip-ansi "^4.0.0"
string-width@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.0.1.tgz#0d8158335a6cfd8eb95da9b6b262ce314a036ffd"
integrity sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==
dependencies:
emoji-regex "^9.2.2"
is-fullwidth-code-point "^4.0.0"
strip-ansi "^7.0.1"
string.prototype.padend@^3.0.0: string.prototype.padend@^3.0.0:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1"
@ -12212,7 +12239,7 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
dependencies: dependencies:
ansi-regex "^5.0.1" ansi-regex "^5.0.1"
strip-ansi@^7.0.0: strip-ansi@^7.0.0, strip-ansi@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
@ -12324,7 +12351,7 @@ supports-color@^8.0.0, supports-color@^8.1.1:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
supports-hyperlinks@^2.0.0: supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb"
integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==
@ -12393,7 +12420,7 @@ tar@^6.0.2, tar@^6.1.0, tar@^6.1.2:
mkdirp "^1.0.3" mkdirp "^1.0.3"
yallist "^4.0.0" yallist "^4.0.0"
terminal-link@^2.0.0, terminal-link@^2.1.1: terminal-link@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==
@ -12401,6 +12428,14 @@ terminal-link@^2.0.0, terminal-link@^2.1.1:
ansi-escapes "^4.2.1" ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0" supports-hyperlinks "^2.0.0"
terminal-link@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-3.0.0.tgz#91c82a66b52fc1684123297ce384429faf72ac5c"
integrity sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==
dependencies:
ansi-escapes "^5.0.0"
supports-hyperlinks "^2.2.0"
terser-webpack-plugin@^1.4.3: terser-webpack-plugin@^1.4.3:
version "1.4.5" version "1.4.5"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
@ -12761,6 +12796,16 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-fest@^1.0.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
type-fest@^2.5.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.8.0.tgz#39d7c9f9c508df8d6ce1cf5a966b0e6568dcc50d"
integrity sha512-O+V9pAshf9C6loGaH0idwsmugI2LxVNR7DtS40gVo2EXZVYFgz9OuNtOhgHLdHdapOEWNdvz9Ob/eeuaWwwlxA==
type-is@~1.6.17, type-is@~1.6.18: type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18" version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -13321,12 +13366,12 @@ wide-align@^1.1.2:
dependencies: dependencies:
string-width "^1.0.2 || 2 || 3 || 4" string-width "^1.0.2 || 2 || 3 || 4"
widest-line@^3.1.0: widest-line@^4.0.1:
version "3.1.0" version "4.0.1"
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
dependencies: dependencies:
string-width "^4.0.0" string-width "^5.0.1"
wildcard@^2.0.0: wildcard@^2.0.0:
version "2.0.0" version "2.0.0"
@ -13371,6 +13416,15 @@ wrap-ansi@^7.0.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.0.1.tgz#2101e861777fec527d0ea90c57c6b03aac56a5b3"
integrity sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"