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() {
return this.data.files.reduce((prev, file) => prev && this.permissionsService.canUnassignUser(file), true);
return this.permissionsService.canUnassignUser(this.data.files);
}
/** Initialize the form with:

View File

@ -1,78 +1,4 @@
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress>
<iqser-circle-button
(action)="delete()"
*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>
<redaction-expandable-file-actions [actions]="buttons" [buttonType]="buttonType" [maxWidth]="maxWidth" [tooltipPosition]="'above'">
</redaction-expandable-file-actions>
</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 { Dossier, File } from '@red/domain';
import { FileAssignService } from '../../../../shared/services/file-assign.service';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { CircleButtonTypes, ConfirmationDialogInput, LoadingService, Required } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { Action, ActionTypes, Dossier, File } from '@red/domain';
import { CircleButtonType, CircleButtonTypes, Required } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { FilesService } from '@services/entity-services/files.service';
import { BulkActionsService } from '../../services/bulk-actions.service';
@Component({
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'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierOverviewBulkActionsComponent {
readonly circleButtonTypes = CircleButtonTypes;
export class DossierOverviewBulkActionsComponent implements OnChanges {
@Input() @Required() dossier: Dossier;
@Input() @Required() selectedFiles: File[];
@Input() buttonType: CircleButtonType = CircleButtonTypes.dark;
@Input() maxWidth: number;
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(
private readonly _dialogService: DossiersDialogService,
private readonly _fileManagementService: FileManagementService,
private readonly _reanalysisService: ReanalysisService,
private readonly _permissionsService: PermissionsService,
private readonly _fileAssignService: FileAssignService,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _filesService: FilesService,
private readonly _bulkActionsService: BulkActionsService,
) {}
get allSelectedFilesCanBeAssignedIntoSameState() {
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
(acc, file) => acc && (file.isUnderReview || file.isNew),
true,
);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
private get _buttons(): Action[] {
return [
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.delete(this.selectedFiles),
tooltip: _('dossier-overview.bulk.delete'),
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() {
return (
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');
ngOnChanges() {
this._setup();
}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
this._setup();
}
delete() {
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(
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();
},
private _setup() {
if (this.selectedFiles.length) {
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
(acc, file) => acc && (file.isUnderReview || file.isNew),
true,
);
} else {
this._loadingService.start();
await this._filesService
.setApprovedFor(
this.selectedFiles.map(f => f.id),
this.dossier.id,
)
.toPromise();
this._loadingService.stop();
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
this._canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
this.canAssign =
this._canMoveToSameState &&
this.selectedFiles.reduce(
(acc, file) => (acc && this._permissionsService.canAssignUser(file)) || this._permissionsService.canUnassignUser(file),
true,
);
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';
@Component({
selector: 'redaction-file-workload-column',
templateUrl: './file-workload-column.component.html',
styleUrls: ['./file-workload-column.component.scss'],
selector: 'redaction-file-workload',
templateUrl: './file-workload.component.html',
styleUrls: ['./file-workload.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileWorkloadColumnComponent {
export class FileWorkloadComponent {
@Input() file: File;
constructor(private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
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({
selector: 'redaction-view-mode-selection',
@ -12,5 +13,10 @@ export class ViewModeSelectionComponent {
readonly listingModes = ListingModes;
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>
<div class="details-wrapper">
<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 }}
</div>
@ -13,5 +13,29 @@
</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>

View File

@ -1,34 +1,60 @@
@use 'common-mixins';
.workflow-item {
padding: 10px;
padding: 10px 10px 8px 10px;
> div {
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-actions {
margin-top: 10px;
display: none;
&:hover .filename {
text-decoration: underline;
}
&: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 { File } from '@red/domain';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { File, IFileAttributeConfig } from '@red/domain';
import { Debounce, Required } from '@iqser/common-ui';
@Component({
selector: 'redaction-workflow-item',
@ -7,6 +8,25 @@ import { File } from '@red/domain';
styleUrls: ['./workflow-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkflowItemComponent {
@Input() file: File;
export class WorkflowItemComponent implements OnInit {
@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,
ListingMode,
ListingModes,
LoadingService,
NestedFilter,
TableColumnConfig,
WorkflowConfig,
} from '@iqser/common-ui';
import { File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
import { FileAssignService } from '../../shared/services/file-assign.service';
import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
@ -24,9 +22,9 @@ import { annotationFilterChecker, RedactionFilterSorter } from '@utils/index';
import { workloadTranslations } from '../../translations/workload-translations';
import * as moment from 'moment';
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 noop from 'lodash/noop';
import { BulkActionsService } from './services/bulk-actions.service';
@Injectable()
export class ConfigService {
@ -34,15 +32,12 @@ export class ConfigService {
private readonly _listingMode$ = new BehaviorSubject<ListingMode>(ListingModes.table);
constructor(
private readonly _fileAssignService: FileAssignService,
private readonly _filesService: FilesService,
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
private readonly _permissionsService: PermissionsService,
private readonly _translateService: TranslateService,
private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService,
private readonly _appConfigService: AppConfigService,
private readonly _bulkActionsService: BulkActionsService,
) {
this.listingMode$ = this._listingMode$.asObservable();
}
@ -63,34 +58,45 @@ export class ConfigService {
{
label: workflowFileStatusTranslations[WorkflowFileStatuses.NEW],
key: WorkflowFileStatuses.NEW,
enterFn: this._unassignFn,
enterPredicate: (file: File) => this._permissionsService.canUnassignUser(file),
enterFn: noop,
enterPredicate: () => false,
color: '#D3D5DA',
entities: new BehaviorSubject([]),
},
{
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_REVIEW],
enterFn: this._underReviewFn,
enterPredicate: (file: File) =>
this._permissionsService.canSetUnderReview(file) ||
this._permissionsService.canAssignToSelf(file) ||
this._permissionsService.canAssignUser(file),
enterFn: async (files: File[]) => {
if (files[0].workflowStatus === WorkflowFileStatuses.UNDER_APPROVAL) {
await this._bulkActionsService.backToUnderReview(files);
} else {
await this._bulkActionsService.assignToMe(files);
}
},
enterPredicate: (files: File[]) =>
this._permissionsService.canSetUnderReview(files) ||
this._permissionsService.canAssignToSelf(files) ||
this._permissionsService.canAssignUser(files),
key: WorkflowFileStatuses.UNDER_REVIEW,
color: '#FDBD00',
entities: new BehaviorSubject([]),
},
{
label: workflowFileStatusTranslations[WorkflowFileStatuses.UNDER_APPROVAL],
enterFn: this._underApprovalFn,
enterPredicate: (file: File) =>
this._permissionsService.canSetUnderApproval(file) || this._permissionsService.canUndoApproval(file),
enterFn: (files: File[]) => this._bulkActionsService.setToUnderApproval(files),
enterPredicate: (files: File[]) =>
this._permissionsService.canSetUnderApproval(files) || this._permissionsService.canUndoApproval(files),
key: WorkflowFileStatuses.UNDER_APPROVAL,
color: '#374C81',
entities: new BehaviorSubject([]),
},
{
label: workflowFileStatusTranslations[WorkflowFileStatuses.APPROVED],
enterFn: this._approveFn,
enterPredicate: (file: File) => this._permissionsService.isReadyForApproval(file) && file.canBeApproved,
enterFn: (files: File[]) => this._bulkActionsService.approve(files),
enterPredicate: (files: File[]) =>
this._permissionsService.isReadyForApproval(files) && this._permissionsService.canBeApproved(files),
key: WorkflowFileStatuses.APPROVED,
color: '#48C9F7',
entities: new BehaviorSubject([]),
},
],
};
@ -323,7 +329,7 @@ export class ConfigService {
_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[] {
const recentPeriod = this._appConfigService.values.RECENT_PERIOD_IN_HOURS;
@ -361,31 +367,4 @@ export class ConfigService {
private _openEditDossierDialog($event: MouseEvent, dossierId: string) {
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 { ConfigService } from './config.service';
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 { WorkflowItemComponent } from './components/workflow-item/workflow-item.component';
import { ScreenHeaderComponent } from './components/screen-header/screen-header.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 { AddedColumnComponent } from './components/table-item/added-column/added-column.component';
import { BulkActionsService } from './services/bulk-actions.service';
const routes: Routes = [
{
@ -36,7 +37,7 @@ const routes: Routes = [
DossierOverviewBulkActionsComponent,
DossierDetailsComponent,
DossierDetailsStatsComponent,
FileWorkloadColumnComponent,
FileWorkloadComponent,
TableItemComponent,
FileStatsComponent,
WorkflowItemComponent,
@ -45,7 +46,7 @@ const routes: Routes = [
FileNameColumnComponent,
AddedColumnComponent,
],
providers: [ConfigService],
providers: [ConfigService, BulkActionsService],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule],
})
export class DossierOverviewModule {}

View File

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

View File

@ -123,7 +123,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
}
disabledFn = (file: File) => file.excluded;
lastOpenedFn = (file: File) => this._userPreferenceService.getLastOpenedFileForDossier(file.dossierId) === file.id;
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 {
display: flex;
height: 100%;

View File

@ -48,13 +48,13 @@ export class UserManagementComponent implements OnChanges {
private get _assignOrChangeReviewerTooltip(): string {
return this.file.assignee
? this.translateService.instant('file-preview.change-reviewer')
: this.translateService.instant('file-preview.assign-reviewer');
? this.translateService.instant(_('file-preview.change-reviewer'))
: this.translateService.instant(_('file-preview.assign-reviewer'));
}
private get _assignTooltip(): string {
return this.file.isUnderApproval
? this.translateService.instant('dossier-overview.assign-approver')
? this.translateService.instant(_('dossier-overview.assign-approver'))
: this._assignOrChangeReviewerTooltip;
}
@ -68,9 +68,9 @@ export class UserManagementComponent implements OnChanges {
}
ngOnChanges() {
this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.file, this.dossier);
this.canAssignUser = this.permissionsService.canAssignUser(this.file, this.dossier);
this.canUnassignUser = this.permissionsService.canUnassignUser(this.file, this.dossier);
this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.file);
this.canAssignUser = this.permissionsService.canAssignUser(this.file);
this.canUnassignUser = this.permissionsService.canUnassignUser(this.file);
this.canAssignOrUnassign = this.canAssignUser || this.canUnassignUser;
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 { TranslateService } from '@ngx-translate/core';
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 { DossiersService } from '@services/entity-services/dossiers.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 { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { ReanalysisService } from '../../../../services/reanalysis.service';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -78,7 +78,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
hideSkipped = false;
displayPdfViewer = false;
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
@ViewChild('fileActions') fileActions: FileActionsComponent;
readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>;
@ -117,6 +116,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
@ -241,7 +241,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const file = this._filesMapService.get(this.dossierId, this.fileId);
if (file?.analysisRequired) {
await this.fileActions.reanalyseFile();
await this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, true).toPromise();
}
this.displayPdfViewer = true;

View File

@ -16,153 +16,12 @@
</ng-container>
<ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress>
<div *ngIf="file" class="file-actions" iqserHelpMode="document-features">
<iqser-circle-button
(action)="openDocument()"
*ngIf="showOpenDocument"
<div class="file-actions" iqserHelpMode="document-features">
<redaction-expandable-file-actions
[actions]="buttons"
[buttonType]="buttonType"
[maxWidth]="maxWidth"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.open-document' | translate"
[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>
></redaction-expandable-file-actions>
</div>
</ng-template>

View File

@ -3,9 +3,7 @@
.file-actions {
display: flex;
overflow-y: auto;
color: variables.$grey-1;
@include common-mixins.no-scroll-bar;
> *:not(:last-child) {
margin-right: 2px;
@ -27,14 +25,6 @@ iqser-status-bar {
margin-left: 2px;
}
mat-slide-toggle {
height: 34px;
width: 34px;
line-height: 33px;
margin-left: 8px;
margin-right: 5px;
}
.spinning-icon {
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 { File } from '@red/domain';
import { Action, ActionTypes, File } from '@red/domain';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import {
AutoUnsubscribe,
@ -25,6 +37,7 @@ import { Router } from '@angular/router';
import { ExcludedPagesService } from '../../../screens/file-preview-screen/services/excluded-pages.service';
import { tap } from 'rxjs/operators';
import { DocumentInfoService } from '../../../screens/file-preview-screen/services/document-info.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
@Component({
selector: 'redaction-file-actions',
@ -36,8 +49,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@Input() file: File;
@Input() @Required() file: File;
@Input() @Required() type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
@Input() maxWidth: number;
@Output() readonly ocredFile = new EventEmitter<void>();
toggleTooltip?: string;
@ -56,16 +70,20 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
canToggleAnalysis: boolean;
showStatusBar: boolean;
showOpenDocument: boolean;
showReanalyseFilePreview: boolean;
showReanalyseDossierOverview: boolean;
analysisForced: boolean;
isDossierOverview = false;
isDossierOverviewList = false;
isDossierOverviewWorkflow = false;
isFilePreview = false;
tooltipPosition: IqserTooltipPosition;
buttons: Action[];
@ViewChild(ExpandableFileActionsComponent) _expandableActionsComponent: ExpandableFileActionsComponent;
constructor(
@Optional() readonly excludedPagesService: ExcludedPagesService,
@Optional() readonly documentInfoService: DocumentInfoService,
@Optional() private readonly _excludedPagesService: ExcludedPagesService,
@Optional() private readonly _documentInfoService: DocumentInfoService,
private readonly _permissionsService: PermissionsService,
private readonly _dossiersService: DossiersService,
private readonly _dialogService: DossiersDialogService,
@ -82,6 +100,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
super();
}
@HostBinding('class.keep-visible') get expanded() {
return this._expandableActionsComponent?.expanded;
}
private get _toggleTooltip(): string {
if (!this.currentUser.isManager) {
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');
}
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() {
this._dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this._setup()));
}
@ -98,57 +230,6 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
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) {
$event.stopPropagation();
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();
this._loadingService.start();
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();
}
async setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) {
await this._fileAssignService.assignReviewer($event, this.file, ignoreDialogChanges);
private async _setFileUnderReview($event: MouseEvent) {
await this._fileAssignService.assignReviewer($event, this.file, true);
}
async toggleAnalysis() {
private async _toggleAnalysis() {
this._loadingService.start();
await this._reanalysisService.toggleAnalysis(this.file.dossierId, this.file.fileId, !this.file.excluded).toPromise();
this._loadingService.stop();
}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
}
private _setup() {
this.isDossierOverviewList = this.type === 'dossier-overview-list';
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.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() {

View File

@ -3,7 +3,7 @@
[disabled]="disabled || !canDownloadFiles"
[tooltipClass]="tooltipClass"
[tooltipPosition]="tooltipPosition"
[tooltip]="tooltip"
[tooltip]="tooltip | translate: { count: this.files.length }"
[type]="type"
icon="iqser:download"
></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 { File } from '@red/domain';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { CircleButtonType, CircleButtonTypes, List, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { CircleButtonType, CircleButtonTypes, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export type MenuState = 'OPEN' | 'CLOSED';
@ -14,28 +13,25 @@ export type MenuState = 'OPEN' | 'CLOSED';
styleUrls: ['./file-download-btn.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileDownloadBtnComponent {
@Input() files: List<File>;
export class FileDownloadBtnComponent implements OnChanges {
@Input() files: File[];
@Input() tooltipPosition: 'above' | 'below' | 'before' | 'after' = 'above';
@Input() type: CircleButtonType = CircleButtonTypes.default;
@Input() tooltipClass: string;
@Input() disabled = false;
tooltip: string;
canDownloadFiles: boolean;
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
) {}
get canDownloadFiles() {
return this.files.length > 0 && this.files.reduce((acc, file) => acc && this._permissionsService.canDownloadFiles(file), true);
}
get tooltip() {
return this.canDownloadFiles
? this._translateService.instant('dossier-overview.download-file')
: this._translateService.instant('dossier-overview.download-file-disabled', { count: this.files.length });
ngOnChanges(): void {
this.canDownloadFiles = this._permissionsService.canDownloadFiles(this.files);
this.tooltip = this.canDownloadFiles ? _('dossier-overview.download-file') : _('dossier-overview.download-file-disabled');
}
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 { TeamMembersComponent } from './components/team-members/team-members.component';
import { EditorComponent } from './components/editor/editor.component';
import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -39,6 +40,7 @@ const components = [
AssignUserDropdownComponent,
TypeFilterComponent,
TeamMembersComponent,
ExpandableFileActionsComponent,
...buttons,
];

View File

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

View File

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

View File

@ -728,7 +728,6 @@
},
"ocr-file": "OCR Document",
"ocr-performed": "OCR was performed for this file.",
"open-document": "Open Document",
"quick-filters": {
"assigned-to-me": "Assigned to me",
"assigned-to-others": "Assigned to others",
@ -1653,5 +1652,13 @@
},
"title": "Watermark"
},
"workflow": {
"selection": {
"all": "All",
"count": "{count} selected",
"none": "None",
"select": "Select"
}
},
"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 hasNone: boolean;
readonly isNew: boolean;
// readonly isUnassigned: boolean;
readonly isError: boolean;
readonly isProcessing: 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 './colors';
export * from './view-mode';
export * from './expandable-file-actions';

View File

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

154
yarn.lock
View File

@ -225,7 +225,7 @@
tslib "^2.3.0"
yargs "^17.2.1"
"@angular/compiler@13.0.2":
"@angular/compiler@13.0.2", "@angular/compiler@^13.0.2":
version "13.0.2"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.0.2.tgz#5bc1bfc1931f1ff2813f8fff8b8ceaa57b47d717"
integrity sha512-EvIFT8y5VNICrnPgiamv/z9hfQ7KjLCM52g4ssXGCeGPVj58OEfslEc3jO4BCJG7xuLm7dCuSRV0pBlJNTSYFg==
@ -1283,6 +1283,23 @@
"@babel/helper-validator-identifier" "^7.15.7"
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":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -1295,22 +1312,6 @@
dependencies:
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":
version "0.8.0"
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"
uri-js "^4.2.2"
ansi-align@^3.0.0:
ansi-align@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
@ -2816,6 +2817,13 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
dependencies:
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:
version "0.1.1"
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"
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:
version "0.1.0"
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"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
boxen@^5.0.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==
boxen@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d"
integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==
dependencies:
ansi-align "^3.0.0"
ansi-align "^3.0.1"
camelcase "^6.2.0"
chalk "^4.1.0"
cli-boxes "^2.2.1"
string-width "^4.2.2"
type-fest "^0.20.2"
widest-line "^3.1.0"
wrap-ansi "^7.0.0"
chalk "^4.1.2"
cli-boxes "^3.0.0"
string-width "^5.0.1"
type-fest "^2.5.0"
widest-line "^4.0.1"
wrap-ansi "^8.0.1"
brace-expansion@^1.1.7:
version "1.1.11"
@ -3747,7 +3760,7 @@ chalk@^3.0.0:
ansi-styles "^4.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"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
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"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-boxes@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
cli-boxes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
cli-cursor@^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"
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:
version "2.0.16"
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"
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -6628,7 +6641,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
gettext-parser@^4.0.4:
gettext-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-4.2.0.tgz#9327140f76b122d44f0e8cb9338fd855667d9434"
integrity sha512-aMgPyjC9W5Mz9tbFU8DcQ7GYMXoFWq633kaWGt4imlcpBWzDIWk7HY7nCSZTCJxyjRaLq9L/NEjMKkZ9gR630Q==
@ -6677,7 +6690,7 @@ glob@7.1.4:
once "^1.3.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"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
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"
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:
version "2.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -12145,6 +12163,15 @@ string-width@^2.1.1:
is-fullwidth-code-point "^2.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:
version "3.1.3"
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:
ansi-regex "^5.0.1"
strip-ansi@^7.0.0:
strip-ansi@^7.0.0, strip-ansi@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
@ -12324,7 +12351,7 @@ supports-color@^8.0.0, supports-color@^8.1.1:
dependencies:
has-flag "^4.0.0"
supports-hyperlinks@^2.0.0:
supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb"
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"
yallist "^4.0.0"
terminal-link@^2.0.0, terminal-link@^2.1.1:
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
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"
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:
version "1.4.5"
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"
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:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -13321,12 +13366,12 @@ wide-align@^1.1.2:
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
widest-line@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
dependencies:
string-width "^4.0.0"
string-width "^5.0.1"
wildcard@^2.0.0:
version "2.0.0"
@ -13371,6 +13416,15 @@ wrap-ansi@^7.0.0:
string-width "^4.1.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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"