RED-10139: removed getters from template and component refactoring.

for dossier overview.
This commit is contained in:
Nicoleta Panaghiu 2024-10-17 18:02:05 +03:00
parent 4bf2e79c57
commit b7ff80ecac
24 changed files with 671 additions and 636 deletions

View File

@ -1,9 +1,11 @@
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress>
<redaction-expandable-file-actions
[actions]="buttons"
[buttonType]="buttonType"
[maxWidth]="maxWidth"
[tooltipPosition]="IqserTooltipPositions.above"
>
</redaction-expandable-file-actions>
</ng-container>
@if (selectedFiles().length) {
<ng-container (longPress)="forceReanalysisAction($event)" redactionLongPress>
<redaction-expandable-file-actions
[actions]="buttons"
[buttonType]="buttonType()"
[maxWidth]="maxWidth()"
[tooltipPosition]="IqserTooltipPositions.above"
>
</redaction-expandable-file-actions>
</ng-container>
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Component, input, OnChanges } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CircleButtonType, CircleButtonTypes } from '@iqser/common-ui';
import { Action, ActionTypes, Dossier, File, ProcessingFileStatuses } from '@red/domain';
@ -8,14 +8,13 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { BulkActionsService } from '../../services/bulk-actions.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { IqserTooltipPositions } from '@common-ui/utils';
import { NgIf } from '@angular/common';
@Component({
selector: 'redaction-dossier-overview-bulk-actions [dossier] [selectedFiles]',
templateUrl: './dossier-overview-bulk-actions.component.html',
styleUrls: ['./dossier-overview-bulk-actions.component.scss'],
standalone: true,
imports: [LongPressDirective, ExpandableFileActionsComponent, NgIf],
imports: [LongPressDirective, ExpandableFileActionsComponent],
})
export class DossierOverviewBulkActionsComponent implements OnChanges {
#analysisForced: boolean;
@ -37,10 +36,10 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
#toggleAnalysisTooltip: string;
#allFilesAreExcluded: boolean;
#canMoveToSameState: boolean;
@Input() dossier: Dossier;
@Input() selectedFiles: File[];
@Input() buttonType: CircleButtonType = CircleButtonTypes.default;
@Input() maxWidth: number;
readonly dossier = input<Dossier>();
readonly selectedFiles = input<File[]>();
readonly buttonType = input<CircleButtonType>(CircleButtonTypes.default);
readonly maxWidth = input<number>();
buttons: Action[];
readonly IqserTooltipPositions = IqserTooltipPositions;
@ -50,12 +49,12 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
private readonly _bulkActionsService: BulkActionsService,
) {}
private get _buttons(): Action[] {
get #buttons(): Action[] {
const actions: Action[] = [
{
id: 'delete-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.delete(this.selectedFiles),
action: () => this._bulkActionsService.delete(this.selectedFiles()),
tooltip: _('dossier-overview.bulk.delete'),
icon: 'iqser:trash',
show: this.#canDelete,
@ -63,7 +62,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'assign-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assign(this.selectedFiles),
action: () => this._bulkActionsService.assign(this.selectedFiles()),
tooltip: this.#assignTooltip,
icon: 'red:assign',
show: this.#canAssign,
@ -71,7 +70,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'assign-files-to-me-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assignToMe(this.selectedFiles),
action: () => this._bulkActionsService.assignToMe(this.selectedFiles()),
tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me',
show: this.#canAssignToSelf,
@ -79,7 +78,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'to-new-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToNew(this.selectedFiles),
action: () => this._bulkActionsService.setToNew(this.selectedFiles()),
tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo',
show: this.#canSetToNew,
@ -87,7 +86,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'to-under-approval-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval',
show: this.#canSetToUnderApproval,
@ -95,7 +94,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'to-under-review-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles),
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles()),
tooltip: _('dossier-overview.under-review'),
icon: 'red:undo',
show: this.#canSetToUnderReview,
@ -103,14 +102,14 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'download-files-btn',
type: ActionTypes.downloadBtn,
show: !this.selectedFiles.some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed),
files: this.selectedFiles,
dossier: this.dossier,
show: !this.selectedFiles().some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed),
files: this.selectedFiles(),
dossier: this.dossier(),
},
{
id: 'approve-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.approve(this.selectedFiles),
action: () => this._bulkActionsService.approve(this.selectedFiles()),
disabled: !this.#canApprove,
tooltip: this.#canApprove ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved',
@ -119,7 +118,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'set-under-approval-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo',
show: this.#canUndoApproval,
@ -127,7 +126,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'ocr-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.ocr(this.selectedFiles),
action: () => this._bulkActionsService.ocr(this.selectedFiles()),
tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr',
show: this.#canOcr,
@ -135,17 +134,17 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'reanalyse-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.reanalyse(this.selectedFiles),
action: () => this._bulkActionsService.reanalyse(this.selectedFiles()),
tooltip: _('dossier-overview.bulk.reanalyse'),
icon: 'iqser:refresh',
show:
this.#canReanalyse &&
(this.#analysisForced || this.#canEnableAutoAnalysis || this.selectedFiles.every(file => file.isError)),
(this.#analysisForced || this.#canEnableAutoAnalysis || this.selectedFiles().every(file => file.isError)),
},
{
id: 'stop-automatic-analysis-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis',
show: this.#canDisableAutoAnalysis,
@ -153,7 +152,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'start-automatic-analysis-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.start-auto-analysis'),
icon: 'red:enable-analysis',
show: this.#canEnableAutoAnalysis,
@ -161,7 +160,7 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
id: 'toggle-analysis-btn',
type: ActionTypes.toggle,
action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles, !this.#allFilesAreExcluded),
action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles(), !this.#allFilesAreExcluded),
tooltip: this.#toggleAnalysisTooltip,
checked: !this.#allFilesAreExcluded,
show: this.#canToggleAnalysis,
@ -172,56 +171,56 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
}
ngOnChanges() {
this._setup();
this.#setup();
}
forceReanalysisAction($event: LongPressEvent) {
this.#analysisForced = !$event.touchEnd && this._userPreferenceService.isIqserDevMode;
this._setup();
this.#setup();
}
private _setup() {
if (!this.selectedFiles.length) {
#setup() {
if (!this.selectedFiles().length) {
return;
}
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles().reduce(
(acc, file) => acc && (file.isUnderReview || file.isNew),
true,
);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreApproved = this.selectedFiles.reduce((acc, file) => acc && file.isApproved, true);
this.#allFilesAreExcluded = this.selectedFiles.reduce((acc, file) => acc && file.excluded, true);
const allFilesAreUnderApproval = this.selectedFiles().reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreApproved = this.selectedFiles().reduce((acc, file) => acc && file.isApproved, true);
this.#allFilesAreExcluded = this.selectedFiles().reduce((acc, file) => acc && file.excluded, true);
this.#canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval || allFilesAreApproved;
this.#canAssign =
this.#canMoveToSameState &&
(this._permissionsService.canAssignUser(this.selectedFiles, this.dossier) ||
this._permissionsService.canUnassignUser(this.selectedFiles, this.dossier));
this.#canAssignToSelf = this.#canMoveToSameState && this._permissionsService.canAssignToSelf(this.selectedFiles, this.dossier);
(this._permissionsService.canAssignUser(this.selectedFiles(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.selectedFiles(), this.dossier()));
this.#canAssignToSelf = this.#canMoveToSameState && this._permissionsService.canAssignToSelf(this.selectedFiles(), this.dossier());
this.#canDelete = this._permissionsService.canSoftDeleteFile(this.selectedFiles, this.dossier);
this.#canDelete = this._permissionsService.canSoftDeleteFile(this.selectedFiles(), this.dossier());
this.#canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles, this.dossier);
this.#canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles(), this.dossier());
this.#canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis(this.selectedFiles(), this.dossier());
this.#canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis(this.selectedFiles(), this.dossier());
this.#canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.selectedFiles, this.dossier);
this.#canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.selectedFiles(), this.dossier());
this.#canOcr = this._permissionsService.canOcrFile(this.selectedFiles, this.dossier);
this.#canOcr = this._permissionsService.canOcrFile(this.selectedFiles(), this.dossier());
this.#canSetToNew = this._permissionsService.canSetToNew(this.selectedFiles, this.dossier);
this.#canSetToNew = this._permissionsService.canSetToNew(this.selectedFiles(), this.dossier());
this.#canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles, this.dossier);
this.#canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles(), this.dossier());
this.#canSetToUnderApproval = this._permissionsService.canSetUnderApproval(this.selectedFiles, this.dossier);
this.#canSetToUnderApproval = this._permissionsService.canSetUnderApproval(this.selectedFiles(), this.dossier());
this.#isReadyForApproval = this._permissionsService.isReadyForApproval(this.selectedFiles, this.dossier);
this.#isReadyForApproval = this._permissionsService.isReadyForApproval(this.selectedFiles(), this.dossier());
this.#canApprove = this._permissionsService.canBeApproved(this.selectedFiles, this.dossier);
this.#canApprove = this._permissionsService.canBeApproved(this.selectedFiles(), this.dossier());
this.#canUndoApproval = this._permissionsService.canUndoApproval(this.selectedFiles, this.dossier);
this.#canUndoApproval = this._permissionsService.canUndoApproval(this.selectedFiles(), this.dossier());
this.#assignTooltip = allFilesAreUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
@ -229,6 +228,6 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
? _('file-preview.toggle-analysis.enable')
: _('file-preview.toggle-analysis.disable');
this.buttons = this._buttons;
this.buttons = this.#buttons;
}
}

View File

@ -1,81 +1,97 @@
<ng-container *ngIf="dossierStats$ | async as stats">
@if (dossierStats$ | async; as stats) {
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate : { count: stats.numberOfFiles } }}</span>
<span>{{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: stats.numberOfFiles } }}</span>
</div>
<div *ngIf="stats.numberOfProcessingFiles">
<mat-icon svgIcon="red:reanalyse"></mat-icon>
<span>{{
'dossier-overview.dossier-details.stats.processing-documents' | translate : { count: stats.numberOfProcessingFiles }
}}</span>
</div>
@if (stats.numberOfProcessingFiles) {
<div>
<mat-icon svgIcon="red:reanalyse"></mat-icon>
<span>{{
'dossier-overview.dossier-details.stats.processing-documents' | translate: { count: stats.numberOfProcessingFiles }
}}</span>
</div>
}
<div>
<mat-icon svgIcon="red:user"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.people' | translate : { count: dossier.memberIds.length } }}</span>
<span>{{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier().memberIds.length } }}</span>
</div>
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate : { count: stats.numberOfPages | number } }}</span>
<span>{{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }}</span>
</div>
<div *ngIf="dossier.date | date : 'd MMM yyyy' as date">
<mat-icon svgIcon="iqser:calendar"></mat-icon>
<span [innerHTML]="'dossier-overview.dossier-details.stats.created-on' | translate : { date }"></span>
</div>
@if (dossier().date | date: 'd MMM yyyy'; as date) {
<div>
<mat-icon svgIcon="iqser:calendar"></mat-icon>
<span [innerHTML]="'dossier-overview.dossier-details.stats.created-on' | translate: { date }"></span>
</div>
}
<div *ngIf="dossier.dueDate | date : 'd MMM yyyy' as dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'dossier-overview.dossier-details.stats.due-date' | translate : { date: dueDate }"></span>
</div>
@if (dossier().dueDate | date: 'd MMM yyyy'; as dueDate) {
<div>
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'dossier-overview.dossier-details.stats.due-date' | translate: { date: dueDate }"></span>
</div>
}
<div>
<mat-icon svgIcon="red:template"></mat-icon>
<span>{{ dossierTemplateName }} </span>
</div>
<div
(click)="openEditDossierDialog('dossierDictionary')"
*ngIf="!isDocumine"
[attr.help-mode-key]="'edit_dossier_dossier_dictionary'"
class="link-property"
>
<mat-icon svgIcon="red:dictionary"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
</div>
@if (!isDocumine) {
<div
(click)="openEditDossierDialog('dossierDictionary')"
[attr.help-mode-key]="'edit_dossier_dossier_dictionary'"
class="link-property"
>
<mat-icon svgIcon="red:dictionary"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.dictionary' | translate }} </span>
</div>
}
<!--TODO: Navigate to trash with filter on click?-->
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<span>{{ 'dossier-overview.dossier-details.stats.deleted' | translate : { count: stats.numberOfSoftDeletedFiles } }}</span>
<span>{{ 'dossier-overview.dossier-details.stats.deleted' | translate: { count: stats.numberOfSoftDeletedFiles } }}</span>
</div>
</ng-container>
}
<ng-container *ngIf="dossierAttributes?.length">
<div
(click)="attributesExpanded = true"
*ngIf="!attributesExpanded"
[attr.help-mode-key]="'edit_dossier_dossier_attributes'"
class="all-caps-label show-attributes"
>
{{ 'dossier-overview.dossier-details.attributes.expand' | translate : { count: dossierAttributes.length } }}
</div>
<div *ngIf="attributesExpanded" [attr.help-mode-key]="'edit_dossier_dossier_attributes'" class="attributes">
<div (click)="openEditDossierDialog('dossierAttributes')" *ngFor="let attr of dossierAttributes" class="link-property">
<mat-icon svgIcon="red:attribute"></mat-icon>
<span *ngIf="!attr.value"> {{ attr.label + ': -' }}</span>
<span *ngIf="attr.value && attr.type === 'DATE'"> {{ attr.label + ': ' + (attr.value | date : 'd MMM yyyy') }}</span>
<span *ngIf="attr.value && attr.type === 'IMAGE'">
{{ attr.label + ': ' + ('dossier-overview.dossier-details.attributes.image-uploaded' | translate) }}</span
>
<span *ngIf="attr.value && (attr.type === 'TEXT' || attr.type === 'NUMBER')"> {{ attr.label + ': ' + attr.value }}</span>
@if (dossierAttributes()?.length) {
@if (!attributesExpanded) {
<div
(click)="attributesExpanded = true"
[attr.help-mode-key]="'edit_dossier_dossier_attributes'"
class="all-caps-label show-attributes"
>
{{ 'dossier-overview.dossier-details.attributes.expand' | translate: { count: dossierAttributes().length } }}
</div>
} @else {
<div [attr.help-mode-key]="'edit_dossier_dossier_attributes'" class="attributes">
@for (attr of dossierAttributes(); track attr.id) {
<div (click)="openEditDossierDialog('dossierAttributes')" class="link-property">
<mat-icon svgIcon="red:attribute"></mat-icon>
@if (!attr.value) {
<span> {{ attr.label + ': -' }}</span>
}
@if (attr.value && attr.type === 'DATE') {
<span> {{ attr.label + ': ' + (attr.value | date: 'd MMM yyyy') }}</span>
}
@if (attr.value && attr.type === 'IMAGE') {
<span> {{ attr.label + ': ' + ('dossier-overview.dossier-details.attributes.image-uploaded' | translate) }}</span>
}
@if (attr.value && (attr.type === 'TEXT' || attr.type === 'NUMBER')) {
<span> {{ attr.label + ': ' + attr.value }}</span>
}
</div>
}
<div (click)="attributesExpanded = false" class="all-caps-label hide-attributes">
{{ 'dossier-overview.dossier-details.attributes.show-less' | translate }}
<div (click)="attributesExpanded = false" class="all-caps-label hide-attributes">
{{ 'dossier-overview.dossier-details.attributes.show-less' | translate }}
</div>
</div>
</div>
</ng-container>
}
}

View File

@ -1,5 +1,5 @@
import { AsyncPipe, DecimalPipe, NgForOf, NgIf } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { AsyncPipe, DecimalPipe } from '@angular/common';
import { Component, input, OnInit, untracked } from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { getConfig, largeDialogConfig } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
@ -17,11 +17,11 @@ import { DossiersDialogService } from '../../../shared-dossiers/services/dossier
templateUrl: './dossier-details-stats.component.html',
styleUrls: ['./dossier-details-stats.component.scss'],
standalone: true,
imports: [MatIcon, NgIf, AsyncPipe, TranslateModule, DatePipe, DecimalPipe, NgForOf],
imports: [MatIcon, AsyncPipe, TranslateModule, DatePipe, DecimalPipe],
})
export class DossierDetailsStatsComponent implements OnInit {
@Input() dossierAttributes: DossierAttributeWithValue[];
@Input() dossier: Dossier;
readonly dossierAttributes = input<DossierAttributeWithValue[]>();
readonly dossier = input<Dossier>();
attributesExpanded = false;
dossierTemplateName: string;
@ -36,14 +36,16 @@ export class DossierDetailsStatsComponent implements OnInit {
) {}
ngOnInit() {
this.dossierStats$ = this._dossierStatsService.watch$(this.dossier.id);
this.dossierTemplateName = this._dossierTemplatesService.find(this.dossier.dossierTemplateId)?.name || '-';
const dossier = untracked(this.dossier);
this.dossierStats$ = this._dossierStatsService.watch$(dossier.id);
this.dossierTemplateName = this._dossierTemplatesService.find(dossier.dossierTemplateId)?.name || '-';
}
openEditDossierDialog(section: string): void {
const data = { dossierId: this.dossier.id, section };
const dossier = untracked(this.dossier);
const data = { dossierId: dossier.id, section };
this._dialogService.open(EditDossierDialogComponent, data, { ...largeDialogConfig, width: '98vw', maxWidth: '98vw' }, async () => {
await firstValueFrom(this._filesService.loadAll(this.dossier.id));
await firstValueFrom(this._filesService.loadAll(dossier.id));
});
}
}

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="componentContext$ | async as ctx">
@if (componentContext$ | async; as ctx) {
<div class="collapsed-wrapper">
<ng-container *ngTemplateOutlet="collapsible; context: { action: 'expand', tooltip: (expandTooltip | translate) }"></ng-container>
<div class="all-caps-label" translate="dossier-details.title"></div>
@ -14,7 +14,7 @@
<div class="mt-24">
<div class="all-caps-label" translate="dossier-details.owner"></div>
<div class="mt-12 flex">
<ng-container *ngIf="!editingOwner; else editOwner">
@if (!editingOwner) {
<iqser-initials-avatar [user]="ctx.dossier?.ownerId" [withName]="true" color="gray" size="large"></iqser-initials-avatar>
<iqser-circle-button
@ -25,7 +25,14 @@
[tooltip]="'dossier-details.edit-owner' | translate"
class="ml-14"
></iqser-circle-button>
</ng-container>
} @else {
<redaction-assign-user-dropdown
(cancel)="editingOwner = false"
(save)="editingOwner = false; assignOwner($event, ctx.dossier)"
[options]="managers()"
[value]="ctx.dossier?.ownerId"
></redaction-assign-user-dropdown>
}
</div>
</div>
@ -38,69 +45,63 @@
></redaction-team-members>
</div>
<ng-container *ngIf="ctx.dossierStats as stats">
<div *ngIf="stats.hasFiles" class="mt-24">
<redaction-donut-chart
(subtitleChanged)="onSubtitleChanged($event)"
[config]="chartConfig"
[filterKey]="'statusFilters'"
[helpModeKey]="'dashboard_in_dossier'"
[radius]="63"
[strokeWidth]="15"
[subtitles]="[
'dossier-overview.dossier-details.charts.documents-in-dossier' | translate,
'dossier-overview.dossier-details.charts.pages-in-dossier' | translate,
]"
direction="row"
></redaction-donut-chart>
</div>
<div *ngIf="ctx.statusConfig as statusConfig" class="mt-24">
<div class="all-caps-label mb-8" translate="dossier-details.document-status"></div>
<iqser-progress-bar
*ngFor="let config of statusConfig"
[attr.help-mode-key]="'dashboard_in_dossier'"
[config]="config"
filterKey="processingTypeFilters"
></iqser-progress-bar>
</div>
<div
*ngIf="stats.hasFiles && !isDocumine && ctx.needsWorkFilters as filters"
[attr.help-mode-key]="'dashboard_in_dossier'"
class="mt-32 legend pb-32"
>
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
*ngFor="let filter of filters"
[class.active]="filter.checked"
>
<redaction-type-filter [dossierTemplateId]="ctx.dossier?.dossierTemplateId" [filter]="filter"></redaction-type-filter>
@if (ctx.dossierStats; as stats) {
@if (stats.hasFiles) {
<div class="mt-24">
<redaction-donut-chart
(subtitleChanged)="onSubtitleChanged($event)"
[config]="chartConfig"
[filterKey]="'statusFilters'"
[helpModeKey]="'dashboard_in_dossier'"
[radius]="63"
[strokeWidth]="15"
[subtitles]="[
'dossier-overview.dossier-details.charts.documents-in-dossier' | translate,
'dossier-overview.dossier-details.charts.pages-in-dossier' | translate,
]"
direction="row"
></redaction-donut-chart>
</div>
</div>
}
@if (ctx.statusConfig; as statusConfig) {
<div class="mt-24">
<div class="all-caps-label mb-8" translate="dossier-details.document-status"></div>
@for (config of statusConfig; track config.id) {
<iqser-progress-bar
[attr.help-mode-key]="'dashboard_in_dossier'"
[config]="config"
filterKey="processingTypeFilters"
></iqser-progress-bar>
}
</div>
}
@if (stats.hasFiles && !isDocumine && ctx.needsWorkFilters; as filters) {
<div [attr.help-mode-key]="'dashboard_in_dossier'" class="mt-32 legend pb-32">
@for (filter of filters; track filter.id) {
<div (click)="filterService.toggleFilter('needsWorkFilters', filter.id)" [class.active]="filter.checked">
<redaction-type-filter
[dossierTemplateId]="ctx.dossier?.dossierTemplateId"
[filter]="filter"
></redaction-type-filter>
</div>
}
</div>
}
<div [class.mt-24]="!stats.hasFiles" class="pb-32">
<redaction-dossier-details-stats
[dossierAttributes]="dossierAttributes"
[dossierAttributes]="dossierAttributes()"
[dossier]="ctx.dossier"
></redaction-dossier-details-stats>
</div>
</ng-container>
<div *ngIf="ctx.dossier?.description as description" class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div>
<div class="mt-8">{{ description }}</div>
</div>
<ng-template #editOwner>
<redaction-assign-user-dropdown
(cancel)="editingOwner = false"
(save)="editingOwner = false; assignOwner($event, ctx.dossier)"
[options]="managers"
[value]="ctx.dossier?.ownerId"
></redaction-assign-user-dropdown>
</ng-template>
</ng-container>
}
@if (ctx.dossier?.description; as description) {
<div class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div>
<div class="mt-8">{{ description }}</div>
</div>
}
}
<ng-template #collapsible let-action="action" let-tooltip="tooltip">
<iqser-circle-button

View File

@ -1,5 +1,5 @@
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { Component, computed, input, output } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
CircleButtonComponent,
@ -55,14 +55,12 @@ interface DossierDetailsContext {
standalone: true,
imports: [
NgTemplateOutlet,
NgIf,
AsyncPipe,
CircleButtonComponent,
IqserAllowDirective,
TranslateModule,
DonutChartComponent,
IqserLoadingModule,
NgForOf,
TypeFilterComponent,
DossierDetailsStatsComponent,
AssignUserDropdownComponent,
@ -74,8 +72,8 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
#currentChartSubtitleIndex = 0;
readonly #dossierId = getParam(DOSSIER_ID);
protected readonly circleButtonTypes = CircleButtonTypes;
@Input() dossierAttributes: DossierAttributeWithValue[];
@Output() readonly toggleCollapse = new EventEmitter();
readonly dossierAttributes = input<DossierAttributeWithValue[]>();
readonly toggleCollapse = output();
editingOwner = false;
readonly roles = Roles;
readonly currentUser = getCurrentUser<User>();
@ -84,6 +82,12 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
chartConfig: DonutChartConfig[] = [];
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly IqserTooltipPositions = IqserTooltipPositions;
readonly managers = computed(() =>
this._userService
.allUsers()
.filter(u => u.isManager)
.map(u => u.id),
);
constructor(
private readonly _toaster: Toaster,
@ -113,10 +117,6 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
});
}
get managers() {
return this._userService.all.filter(u => u.isManager).map(u => u.id);
}
async assignOwner(user: User | string, dossier: Dossier) {
const owner = typeof user === 'string' ? this._userService.find(user) : user;
const dossierRequest: IDossierRequest = { ...dossier, ownerId: owner.id };

View File

@ -1,90 +1,95 @@
<ng-container *ngIf="configService.listingMode$ | async as mode">
@if (configService.listingMode$ | async; as mode) {
<div
(click)="handleFieldClick($event)"
(mousedown)="handleClick($event)"
[ngClass]="{ 'workflow-attribute': mode === 'workflow', 'file-name-column': fileNameColumn }"
[ngClass]="{ 'workflow-attribute': mode === 'workflow', 'file-name-column': fileNameColumn() }"
class="file-attribute"
>
<div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value" *ngIf="!isInEditMode || mode === 'workflow'">
<mat-icon *ngIf="!fileAttribute.editable" [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon>
<span
*ngIf="!isDate; else date"
[style.max-width]="attributeValueWidth"
[matTooltip]="fileAttributeValue"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
>
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
{{ fileAttributeValue || '-' }}
</span>
<ng-template #date>
<span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }">
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
{{ fileAttributeValue ? (fileAttributeValue | date: 'd MMM yyyy') : '-' }}
</span>
</ng-template>
</div>
<ng-container
*ngIf="
(fileAttributesService.isEditingFileAttribute() === false || isInEditMode) &&
!file.isInitialProcessing &&
permissionsService.canEditFileAttributes(file, dossier) &&
fileAttribute.editable
"
>
<div
(click)="editFileAttribute($event)"
*ngIf="!isInEditMode; else input"
[attr.help-mode-key]="'edit_file_attributes'"
[class.help-mode-button]="helpModeService.isHelpModeActive$ | async"
[ngClass]="{
'workflow-edit-button': mode === 'workflow',
'action-buttons edit-button': !fileNameColumn,
'filename-edit-button': fileNameColumn,
}"
>
<div [ngClass]="{ 'workflow-edit-icon': mode === 'workflow', 'edit-icon': !fileNameColumn }">
<mat-icon [svgIcon]="'iqser:edit'"></mat-icon>
</div>
@if (!isInEditMode || mode === 'workflow') {
<div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value">
@if (!fileAttribute().editable) {
<mat-icon [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon>
}
@if (!isDate()) {
<span
[style.max-width]="attributeValueWidth()"
[matTooltip]="value()"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
>
@if (mode === 'workflow' && !isInEditMode) {
<b> {{ fileAttribute().label }}: </b>
}
{{ value() || '-' }}
</span>
} @else {
<span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }">
@if (mode === 'workflow' && !isInEditMode) {
<b> {{ fileAttribute().label }}: </b>
}
{{ value() ? (value() | date: 'd MMM yyyy') : '-' }}
</span>
}
</div>
</ng-container>
}
@if (
(fileAttributesService.isEditingFileAttribute() === false || isInEditMode) &&
!file().isInitialProcessing &&
permissionsService.canEditFileAttributes(file(), dossier()) &&
fileAttribute().editable
) {
@if (!isInEditMode) {
<div
(click)="editFileAttribute($event)"
[attr.help-mode-key]="'edit_file_attributes'"
[class.help-mode-button]="helpModeService.isHelpModeActive$ | async"
[ngClass]="{
'workflow-edit-button': mode === 'workflow',
'action-buttons edit-button': !fileNameColumn(),
'filename-edit-button': fileNameColumn(),
}"
>
<div [ngClass]="{ 'workflow-edit-icon': mode === 'workflow', 'edit-icon': !fileNameColumn() }">
<mat-icon [svgIcon]="'iqser:edit'"></mat-icon>
</div>
</div>
} @else {
<div
[ngClass]="{ 'workflow-edit-input': mode === 'workflow', 'file-name-column-input': fileNameColumn() }"
class="edit-input"
iqserStopPropagation
>
<form [formGroup]="form">
<iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keyup.enter)="form.valid && save()"
(keydown.escape)="close()"
[style.max-width]="editFieldWidth()"
[style.min-width]="editFieldWidth()"
[formControlName]="fileAttribute().id"
[id]="fileAttribute().id"
[ngClass]="{ 'workflow-input': mode === 'workflow' || fileNameColumn(), 'file-name-input': fileNameColumn() }"
[type]="fileAttribute().type"
></iqser-dynamic-input>
<iqser-circle-button
(action)="save()"
[disabled]="disabled"
[icon]="'iqser:check'"
[size]="mode === 'workflow' || fileNameColumn() ? 15 : 34"
[ngClass]="{ 'file-name-btn': fileNameColumn() }"
class="save"
></iqser-circle-button>
<iqser-circle-button
(action)="close()"
[ngClass]="{ 'file-name-btn': fileNameColumn() }"
[icon]="'iqser:close'"
[size]="mode === 'workflow' || fileNameColumn() ? 15 : 34"
></iqser-circle-button>
</form>
</div>
}
}
</div>
<ng-template #input>
<div
[ngClass]="{ 'workflow-edit-input': mode === 'workflow', 'file-name-column-input': fileNameColumn }"
class="edit-input"
iqserStopPropagation
>
<form [formGroup]="form">
<iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keyup.enter)="form.valid && save()"
(keydown.escape)="close()"
[style.max-width]="editFieldWidth"
[style.min-width]="editFieldWidth"
[formControlName]="fileAttribute.id"
[id]="fileAttribute.id"
[ngClass]="{ 'workflow-input': mode === 'workflow' || fileNameColumn, 'file-name-input': fileNameColumn }"
[type]="fileAttribute.type"
></iqser-dynamic-input>
<iqser-circle-button
(action)="save()"
[disabled]="disabled"
[icon]="'iqser:check'"
[size]="mode === 'workflow' || fileNameColumn ? 15 : 34"
[ngClass]="{ 'file-name-btn': fileNameColumn }"
class="save"
></iqser-circle-button>
<iqser-circle-button
(action)="close()"
[ngClass]="{ 'file-name-btn': fileNameColumn }"
[icon]="'iqser:close'"
[size]="mode === 'workflow' || fileNameColumn ? 15 : 34"
></iqser-circle-button>
</form>
</div>
</ng-template>
</ng-container>
}

View File

@ -1,9 +1,8 @@
import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, computed, effect, HostListener, Input, OnDestroy } from '@angular/core';
import { AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common';
import { Component, computed, effect, HostListener, input, untracked } from '@angular/core';
import { AbstractControl, FormBuilder, FormsModule, ReactiveFormsModule, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { NavigationEnd, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DynamicInputComponent } from '@common-ui/inputs/dynamic-input/dynamic-input.component';
import {
@ -14,7 +13,7 @@ import {
StopPropagationDirective,
Toaster,
} from '@iqser/common-ui';
import { Debounce, log } from '@iqser/common-ui/lib/utils';
import { Debounce } from '@iqser/common-ui/lib/utils';
import { TranslateModule } from '@ngx-translate/core';
import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@ -22,9 +21,9 @@ import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { DatePipe } from '@shared/pipes/date.pipe';
import dayjs from 'dayjs';
import { firstValueFrom, Subscription } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import { firstValueFrom } from 'rxjs';
import { ConfigService } from '../../config.service';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'redaction-file-attribute',
@ -32,7 +31,6 @@ import { ConfigService } from '../../config.service';
styleUrls: ['./file-attribute.component.scss'],
standalone: true,
imports: [
NgIf,
NgClass,
MatTooltipModule,
MatIconModule,
@ -47,29 +45,34 @@ import { ConfigService } from '../../config.service';
StopPropagationDirective,
],
})
export class FileAttributeComponent extends BaseFormComponent implements OnDestroy {
readonly #subscriptions = new Subscription();
#widthFactor = window.innerWidth >= 1800 ? 0.85 : 0.7;
export class FileAttributeComponent extends BaseFormComponent {
isInEditMode = false;
closedDatepicker = true;
@Input({ required: true }) fileAttribute!: IFileAttributeConfig;
@Input({ required: true }) file!: File;
readonly fileAttribute = input.required<IFileAttributeConfig>();
readonly file = input.required<File>();
readonly dossier = input<Dossier>();
readonly fileNameColumn = input(false);
readonly width = input<number>();
readonly editFieldWidth = computed(() => (this.width() ? `${this.width() * this.#widthFactor}px` : 'unset'));
readonly attributeValueWidth = computed(() => (this.width() ? `${this.width() * 0.9}px` : 'unset'));
readonly isText = computed(() => this.fileAttribute().type === FileAttributeConfigTypes.TEXT);
readonly value = computed(() => this.file().fileAttributes.attributeIdToValue[this.fileAttribute().id]);
readonly isDate = computed(() => this.fileAttribute().type === FileAttributeConfigTypes.DATE);
readonly #selectedLength = toSignal(this._listingService.selectedLength$);
readonly #shouldClose = computed(
() =>
(this.fileAttributesService.isEditingFileAttribute() &&
this.file &&
this.fileAttribute &&
(this.fileAttribute.id !== this.fileAttributesService.openAttributeEdit() ||
this.file.fileId !== this.fileAttributesService.fileEdit())) ||
(this.fileAttribute().id !== this.fileAttributesService.openAttributeEdit() ||
this.file().fileId !== this.fileAttributesService.fileEdit())) ||
!this.fileAttributesService.openAttributeEdits().length,
);
@Input({ required: true }) dossier!: Dossier;
@Input() fileNameColumn = false;
readonlyAttrs: string[] = [];
@Input() width?: number;
#widthFactor = window.innerWidth >= 1800 ? 0.85 : 0.7;
#readonlyAttrs: string[] = [];
constructor(
router: Router,
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _filesService: FilesService,
@ -80,14 +83,11 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
readonly configService: ConfigService,
) {
super();
const sub = router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => this.close());
this.#subscriptions.add(sub);
const sub2 = this._listingService.selectedLength$.pipe(
map(selectedLength => !!selectedLength),
tap(() => this.close()),
);
this.#subscriptions.add(sub2.subscribe());
effect(() => {
if (this.#selectedLength()) {
this.close();
}
});
effect(
() => {
@ -99,30 +99,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
);
}
get editFieldWidth(): string {
return this.width ? `${this.width * this.#widthFactor}px` : 'unset';
}
get attributeValueWidth(): string {
return this.width ? `${this.width * 0.9}px` : 'unset';
}
get isDate(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
}
get isNumber(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.NUMBER;
}
get isText(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.TEXT;
}
get fileAttributeValue(): string {
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
}
@HostListener('window:resize')
onResize() {
if (window.innerWidth >= 1800) {
@ -136,7 +112,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
@HostListener('document:click', ['$event'])
clickOutside($event: MouseEvent) {
const clickCalendarCell = ($event.target as HTMLElement).classList?.contains('mat-calendar-body-cell-content');
if (this.isDate) {
if (this.isDate()) {
this.#focusOnEditInput();
}
@ -150,13 +126,9 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
$event.preventDefault();
}
ngOnDestroy() {
this.#subscriptions.unsubscribe();
}
handleFieldClick($event: MouseEvent) {
$event.preventDefault();
if (!this.fileNameColumn) {
if (!this.fileNameColumn()) {
this.editFileAttribute($event);
}
}
@ -164,34 +136,34 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
editFileAttribute($event: MouseEvent) {
$event.preventDefault();
if (
!this.file.isInitialProcessing &&
this.permissionsService.canEditFileAttributes(this.file, this.dossier) &&
this.fileAttribute.editable &&
!this.file().isInitialProcessing &&
this.permissionsService.canEditFileAttributes(this.file(), this.dossier()) &&
this.fileAttribute().editable &&
!this.isInEditMode
) {
$event.stopPropagation();
this.fileAttributesService.openAttributeEdits.update(value => [
...value,
{ attribute: this.fileAttribute.id, file: this.file.id },
{ attribute: this.fileAttribute().id, file: this.file().id },
]);
this.fileAttributesService.setFileEdit(this.file.fileId);
this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute.id);
this.fileAttributesService.setFileEdit(this.file().fileId);
this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute().id);
this.#toggleEdit();
}
}
async save() {
const rawFormValue = this.form.getRawValue();
const fileAttrValue = rawFormValue[this.fileAttribute.id];
const fileAttrValue = rawFormValue[this.fileAttribute().id];
const attributeIdToValue = {
...this.#getForm().getRawValue(),
[this.fileAttribute.id]: this.#formatAttributeValue(fileAttrValue),
[this.fileAttribute().id]: this.#formatAttributeValue(fileAttrValue),
};
try {
await firstValueFrom(
this.fileAttributesService.setFileAttributes({ attributeIdToValue }, this.file.dossierId, this.file.fileId),
this.fileAttributesService.setFileAttributes({ attributeIdToValue }, this.file().dossierId, this.file().fileId),
);
await this._filesService.reload(this.file.dossierId, this.file);
await this._filesService.reload(this.file().dossierId, this.file());
this.initialFormValue = rawFormValue;
this._toaster.success(_('file-attribute.update.success'));
} catch (e) {
@ -203,6 +175,8 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
close(): void {
if (this.isInEditMode) {
const fileAttribute = untracked(this.fileAttribute);
const file = untracked(this.file);
this.form = this.#getForm();
this.#toggleEdit();
this.fileAttributesService.openAttributeEdits.update(value =>
@ -210,8 +184,8 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
val =>
JSON.stringify(val) !==
JSON.stringify({
attribute: this.fileAttribute.id,
file: this.file.id,
attribute: fileAttribute.id,
file: file.id,
}),
),
);
@ -219,24 +193,26 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#initFileAttributes() {
const configs = this.fileAttributesService.getFileAttributeConfig(this.file.dossierTemplateId).fileAttributeConfigs;
this.readonlyAttrs = configs.filter(config => config.editable === false).map(config => config.id);
const file = untracked(this.file);
const configs = this.fileAttributesService.getFileAttributeConfig(file.dossierTemplateId).fileAttributeConfigs;
this.#readonlyAttrs = configs.filter(config => config.editable === false).map(config => config.id);
configs.forEach(config => {
if (!this.file.fileAttributes.attributeIdToValue[config.id]) {
this.file.fileAttributes.attributeIdToValue[config.id] = null;
if (!file.fileAttributes.attributeIdToValue[config.id]) {
file.fileAttributes.attributeIdToValue[config.id] = null;
}
});
}
#getForm(): UntypedFormGroup {
const config = {};
const fileAttributes = this.file.fileAttributes.attributeIdToValue;
const file = untracked(this.file);
const fileAttributes = file.fileAttributes.attributeIdToValue;
Object.keys(fileAttributes).forEach(key => {
const attrValue = fileAttributes[key];
config[key] = [
{
value: dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue,
disabled: this.readonlyAttrs.includes(key),
disabled: this.#readonlyAttrs.includes(key),
},
];
});
@ -246,19 +222,25 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#checkEmptyInput(): ValidatorFn {
const isText = untracked(this.isText);
const fileAttribute = untracked(this.fileAttribute);
const value = untracked(this.value);
return (control: AbstractControl) =>
(this.isText && !control.get(this.fileAttribute.id)?.value?.trim().length && !this.fileAttributeValue) ||
control.get(this.fileAttribute.id)?.value === this.fileAttributeValue
(isText && !control.get(fileAttribute.id)?.value?.trim().length && !this.value()) ||
control.get(fileAttribute.id)?.value === value
? { emptyString: true }
: null;
}
#checkDate(): ValidatorFn {
const isDate = untracked(this.isDate);
const fileAttribute = untracked(this.fileAttribute);
const value = untracked(this.value);
return (control: AbstractControl) => {
const expr = new RegExp('(0?[1-9]|[12][0-9]|3[01])(/|.)(0?[1-9]|1[12])(/|.)\\d{2}');
return this.isDate
? (!expr.test(control.get(this.fileAttribute.id)?.value) && control.get(this.fileAttribute.id)?.value?.length) ||
this.#formatAttributeValue(control.get(this.fileAttribute.id)?.value) === this.fileAttributeValue
return isDate
? (!expr.test(control.get(fileAttribute.id)?.value) && control.get(fileAttribute.id)?.value?.length) ||
this.#formatAttributeValue(control.get(fileAttribute.id)?.value) === value
? { invalidDate: true }
: null
: null;
@ -266,11 +248,12 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#formatAttributeValue(attrValue) {
if (this.isDate) {
const isDate = untracked(this.isDate);
if (isDate) {
return attrValue && dayjs(attrValue).format('YYYY-MM-DD');
}
if (this.isText) {
const isText = untracked(this.isText);
if (isText) {
return attrValue.trim().replaceAll(/\s\s+/g, ' ');
}
@ -292,9 +275,12 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#focusOnEditInput(): void {
if (this.isDate || this.isText) {
const isDate = untracked(this.isDate);
const isText = untracked(this.isText);
const fileAttribute = untracked(this.fileAttribute);
if (isDate || isText) {
setTimeout(() => {
const input = document.getElementById(this.fileAttribute.id) as HTMLInputElement;
const input = document.getElementById(fileAttribute.id) as HTMLInputElement;
if (!input) {
return;
}

View File

@ -1,5 +1,5 @@
<iqser-page-header
(closeAction)="router.navigate([dossier.dossiersListRouterLink])"
(closeAction)="router.navigate([dossier().dossiersListRouterLink])"
[actionConfigs]="actionConfigs"
[helpModeKey]="'document'"
[showCloseButton]="true"
@ -10,40 +10,43 @@
[attr.help-mode-key]="isDocumine ? 'dossier_download_dossier' : 'download_dossier_in_dossier'"
[buttonId]="'download-files-btn'"
[disabled]="downloadFilesDisabled$ | async"
[dossier]="dossier"
[dossier]="dossier()"
[files]="entitiesService.all$ | async"
dossierDownload
></redaction-file-download-btn>
<iqser-circle-button
(action)="downloadDossierAsCSV()"
*ngIf="permissionsService.canDownloadCsvReport(dossier)"
[attr.help-mode-key]="'download_csv'"
[disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:csv'"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
></iqser-circle-button>
@if (permissionsService.canDownloadCsvReport(dossier())) {
<iqser-circle-button
(action)="downloadDossierAsCSV()"
[attr.help-mode-key]="'download_csv'"
[disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:csv'"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
></iqser-circle-button>
}
<iqser-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:refresh'"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
></iqser-circle-button>
@if (permissionsService.displayReanalyseBtn(dossier())) {
<iqser-circle-button
(action)="reanalyseDossier()"
[disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:refresh'"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
></iqser-circle-button>
}
<iqser-circle-button
(action)="upload.emit()"
*ngIf="permissionsService.canUploadFiles(dossier)"
[attr.help-mode-key]="'upload_document'"
[buttonId]="'upload-document-btn'"
[icon]="'iqser:upload'"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"
></iqser-circle-button>
@if (permissionsService.canUploadFiles(dossier())) {
<iqser-circle-button
(action)="upload.emit()"
[attr.help-mode-key]="'upload_document'"
[buttonId]="'upload-document-btn'"
[icon]="'iqser:upload'"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"
></iqser-circle-button>
}
</ng-container>
</iqser-page-header>

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, input, OnInit, output, untracked } from '@angular/core';
import {
ActionConfig,
CircleButtonComponent,
@ -25,9 +25,8 @@ import { Router } from '@angular/router';
import { Roles } from '@users/roles';
import { SortingService } from '@iqser/common-ui/lib/sorting';
import { List, some } from '@iqser/common-ui/lib/utils';
import { ComponentLogService } from '@services/files/component-log.service';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FileDownloadBtnComponent } from '@shared/components/buttons/file-download-btn/file-download-btn.component';
import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-selection.component';
@ -44,7 +43,6 @@ import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-sel
AsyncPipe,
TranslateModule,
FileDownloadBtnComponent,
NgIf,
ViewModeSelectionComponent,
DisableStopPropagationDirective,
MatMenu,
@ -52,8 +50,8 @@ import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-sel
],
})
export class DossierOverviewScreenHeaderComponent implements OnInit {
@Input() dossier: Dossier;
@Output() readonly upload = new EventEmitter<void>();
readonly dossier = input<Dossier>();
readonly upload = output();
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
actionConfigs: List<ActionConfig>;
@ -84,13 +82,14 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
}
ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.id, this.listingService.areSomeSelected$);
this.actionConfigs = this.configService.actionConfig(this.dossier().id, this.listingService.areSomeSelected$);
}
async reanalyseDossier() {
this._loadingService.start();
try {
await this._reanalysisService.reanalyzeDossier(this.dossier, true);
const dossier = untracked(this.dossier);
await this._reanalysisService.reanalyzeDossier(dossier, true);
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
@ -101,12 +100,13 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
async downloadDossierAsCSV() {
const displayedEntities = await firstValueFrom(this.listingService.displayed$);
const entities = this.sortingService.defaultSort(displayedEntities);
const fileName = this.dossier.dossierName + '.export.csv';
const dossier = untracked(this.dossier);
const fileName = dossier.dossierName + '.export.csv';
const mapper = (file?: File) => ({
...file,
hasAnnotations: file.hasRedactions,
assignee: this._userService.getName(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId),
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier().dossierTemplateId),
});
const documineOnlyFields = ['hasAnnotations'];
const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions'];

View File

@ -1,19 +1,27 @@
<div class="needs-work">
<redaction-annotation-icon
*ngIf="file().analysisRequired"
[color]="analysisColor$ | async"
label="A"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="updated()" [color]="updatedColor$ | async" label="U" type="square"></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="file().hasRedactions"
[color]="redactionColor$ | async"
[label]="'redaction-abbreviation' | translate"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file().hasImages" [color]="imageColor$ | async" label="I" type="square"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file().hintsOnly" [color]="hintColor$ | async" label="H" type="circle"></redaction-annotation-icon>
<mat-icon *ngIf="file().hasAnnotationComments" svgIcon="red:comment"></mat-icon>
<ng-container *ngIf="noWorkloadItems()"> -</ng-container>
@if (file().analysisRequired) {
<redaction-annotation-icon [color]="analysisColor$ | async" label="A" type="square"></redaction-annotation-icon>
}
@if (updated()) {
<redaction-annotation-icon [color]="updatedColor$ | async" label="U" type="square"></redaction-annotation-icon>
}
@if (file().hasRedactions) {
<redaction-annotation-icon
[color]="redactionColor$ | async"
[label]="'redaction-abbreviation' | translate"
type="square"
></redaction-annotation-icon>
}
@if (file().hasImages) {
<redaction-annotation-icon [color]="imageColor$ | async" label="I" type="square"></redaction-annotation-icon>
}
@if (file().hintsOnly) {
<redaction-annotation-icon [color]="hintColor$ | async" label="H" type="circle"></redaction-annotation-icon>
}
@if (file().hasAnnotationComments) {
<mat-icon svgIcon="red:comment"></mat-icon>
}
@if (noWorkloadItems()) {
-
}
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, computed, input, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, input, OnInit } from '@angular/core';
import { annotationDefaultColorConfig, DefaultBasedColorType, File } from '@red/domain';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
@ -7,7 +7,7 @@ import { UserService } from '@users/user.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnnotationIconComponent } from '@shared/components/annotation-icon/annotation-icon.component';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatIcon } from '@angular/material/icon';
@ -17,7 +17,7 @@ import { MatIcon } from '@angular/material/icon';
styleUrls: ['./file-workload.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AnnotationIconComponent, AsyncPipe, TranslateModule, MatIcon, NgIf],
imports: [AnnotationIconComponent, AsyncPipe, TranslateModule, MatIcon],
})
export class FileWorkloadComponent implements OnInit {
#dossierTemplateId: string;

View File

@ -1,78 +1,94 @@
<div class="cell" [class.file-name-cell]="!fileAttributesService.isEditingFileAttribute()">
<redaction-file-name-column [dossierTemplateId]="dossierTemplateId" [file]="file" [dossier]="dossier"></redaction-file-name-column>
<redaction-file-name-column
[dossierTemplateId]="dossierTemplateId()"
[file]="file()"
[dossier]="dossier()"
></redaction-file-name-column>
</div>
<div class="cell">
<redaction-date-column [date]="file.added" [isError]="file.isError"></redaction-date-column>
<redaction-date-column [date]="file().added" [isError]="file().isError"></redaction-date-column>
</div>
<div class="cell">
<redaction-date-column [date]="file.redactionModificationDate" [isError]="file.isError"></redaction-date-column>
<redaction-date-column [date]="file().redactionModificationDate" [isError]="file().isError"></redaction-date-column>
</div>
<div *ngFor="let config of displayedAttributes" class="cell">
<redaction-file-attribute [file]="file" [dossier]="dossier" [fileAttribute]="config"></redaction-file-attribute>
</div>
@for (config of displayedAttributes(); track config.id) {
<div class="cell">
<redaction-file-attribute [file]="file()" [dossier]="dossier()" [fileAttribute]="config"></redaction-file-attribute>
</div>
}
<!-- always show A for error-->
<div *ngIf="file.isError && !isDocumine" class="cell">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
<ng-container *ngIf="!file.isError">
<div class="cell" *ngIf="!isDocumine">
<redaction-file-workload *ngIf="!file.excluded" [file]="file"></redaction-file-workload>
@if (file().isError && !isDocumine) {
<div class="cell">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
}
@if (!file().isError) {
@if (!isDocumine) {
<div class="cell">
@if (!file().excluded) {
<redaction-file-workload [file]="file()"></redaction-file-workload>
}
</div>
}
<div class="user-column cell">
<iqser-initials-avatar [user]="file.assignee" [withName]="true"></iqser-initials-avatar>
<iqser-initials-avatar [user]="file().assignee" [withName]="true"></iqser-initials-avatar>
</div>
<div class="cell">
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ file.numberOfPages }}
{{ file().numberOfPages }}
</div>
</div>
</div>
</ng-container>
}
<div [class.extend-cols]="file.isError" class="status-container cell">
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
<div [class.extend-cols]="file().isError" class="status-container cell">
@if (file().isError) {
<div class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
}
<div *ngIf="file.isUnprocessed" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
@if (file().isUnprocessed) {
<div class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
}
<div class="status-wrapper">
<ng-container *ngIf="file.isOcrProcessing; else defaultProcessing">
@if (file().isOcrProcessing) {
<redaction-ocr-progress-bar
[numberOfOCRedPages]="file.numberOfOCRedPages"
[numberOfPagesToOCR]="file.numberOfPagesToOCR"
[numberOfOCRedPages]="file().numberOfOCRedPages"
[numberOfPagesToOCR]="file().numberOfPagesToOCR"
[progressColor]="'primary'"
></redaction-ocr-progress-bar>
</ng-container>
} @else {
<redaction-processing-indicator [file]="file()"></redaction-processing-indicator>
@if (!file().isError && !file().isUnprocessed && !file().isInitialProcessing) {
<iqser-status-bar
[configs]="[
{
color: file().workflowStatus,
length: 1,
},
]"
></iqser-status-bar>
}
}
</div>
<ng-template #defaultProcessing>
<redaction-processing-indicator [file]="file"></redaction-processing-indicator>
<iqser-status-bar
*ngIf="!file.isError && !file.isUnprocessed && !file.isInitialProcessing"
[configs]="[
{
color: file.workflowStatus,
length: 1,
},
]"
></iqser-status-bar>
</ng-template>
<redaction-file-actions
*ngIf="!file.isProcessing"
[dossier]="dossier"
[file]="file"
[singleEntityAction]="true"
class="mr-4"
type="dossier-overview-list"
></redaction-file-actions>
@if (!file().isProcessing) {
<redaction-file-actions
[dossier]="dossier()"
[file]="file()"
[singleEntityAction]="true"
class="mr-4"
type="dossier-overview-list"
></redaction-file-actions>
}
</div>

View File

@ -1,11 +1,10 @@
import { Component, Input } from '@angular/core';
import { Component, input } from '@angular/core';
import { getConfig } from '@iqser/common-ui';
import { Dossier, File, IFileAttributeConfig } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { FileNameColumnComponent } from '@shared/components/file-name-column/file-name-column.component';
import { DateColumnComponent } from '../../../shared-dossiers/components/date-column/date-column.component';
import { FileAttributeComponent } from '../file-attribute/file-attribute.component';
import { NgForOf, NgIf } from '@angular/common';
import { AnnotationIconComponent } from '@shared/components/annotation-icon/annotation-icon.component';
import { FileWorkloadComponent } from './file-workload/file-workload.component';
import { InitialsAvatarComponent, IqserUsersModule } from '@common-ui/users';
@ -25,7 +24,6 @@ import { TranslateModule } from '@ngx-translate/core';
FileNameColumnComponent,
DateColumnComponent,
FileAttributeComponent,
NgIf,
AnnotationIconComponent,
FileWorkloadComponent,
IqserUsersModule,
@ -35,15 +33,14 @@ import { TranslateModule } from '@ngx-translate/core';
StatusBarComponent,
FileActionsComponent,
TranslateModule,
NgForOf,
InitialsAvatarComponent,
],
})
export class TableItemComponent {
@Input({ required: true }) file: File;
@Input({ required: true }) dossier: Dossier;
@Input({ required: true }) displayedAttributes: IFileAttributeConfig[];
@Input({ required: true }) dossierTemplateId: string;
readonly file = input.required<File>();
readonly dossier = input.required<Dossier>();
readonly displayedAttributes = input.required<IFileAttributeConfig[]>();
readonly dossierTemplateId = input.required<string>();
readonly isDocumine = getConfig().IS_DOCUMINE;

View File

@ -1,21 +1,23 @@
<div *ngIf="configService.listingMode$ | async as mode" class="view-mode-selection">
<div class="all-caps-label" translate="view-mode.view-as"></div>
@if (configService.listingMode$ | async; as mode) {
<div class="view-mode-selection">
<div class="all-caps-label" translate="view-mode.view-as"></div>
<iqser-circle-button
(action)="setListingMode(listingModes.table)"
[attr.aria-expanded]="mode === listingModes.table"
[attr.help-mode-key]="'document_list_view'"
[tooltip]="'view-mode.list' | translate"
greySelected
icon="iqser:list"
></iqser-circle-button>
<iqser-circle-button
(action)="setListingMode(listingModes.table)"
[attr.aria-expanded]="mode === listingModes.table"
[attr.help-mode-key]="'document_list_view'"
[tooltip]="'view-mode.list' | translate"
greySelected
icon="iqser:list"
></iqser-circle-button>
<iqser-circle-button
(action)="setListingMode(listingModes.workflow)"
[attr.aria-expanded]="mode === listingModes.workflow"
[attr.help-mode-key]="'workflow_view'"
[tooltip]="'view-mode.workflow' | translate"
greySelected
icon="iqser:lanes"
></iqser-circle-button>
</div>
<iqser-circle-button
(action)="setListingMode(listingModes.workflow)"
[attr.aria-expanded]="mode === listingModes.workflow"
[attr.help-mode-key]="'workflow_view'"
[tooltip]="'view-mode.workflow' | translate"
greySelected
icon="iqser:lanes"
></iqser-circle-button>
</div>
}

View File

@ -3,7 +3,7 @@ import { CircleButtonComponent, ListingMode, ListingModes, ListingService } from
import { File } from '@red/domain';
import { ConfigService } from '../../config.service';
import { TranslateModule } from '@ngx-translate/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'redaction-view-mode-selection',
@ -11,7 +11,7 @@ import { AsyncPipe, NgIf } from '@angular/common';
styleUrls: ['./view-mode-selection.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CircleButtonComponent, TranslateModule, AsyncPipe, NgIf],
imports: [CircleButtonComponent, TranslateModule, AsyncPipe],
})
export class ViewModeSelectionComponent {
readonly listingModes = ListingModes;

View File

@ -7,41 +7,44 @@
<div class="details">
<div
[attr.help-mode-key]="'workflow_view'"
[matTooltip]="file.filename"
[routerLink]="file.routerLink"
[matTooltip]="file().filename"
[routerLink]="fileRouterLink()"
class="filename pointer"
matTooltipPosition="above"
>
{{ file.filename }}
{{ file().filename }}
</div>
<redaction-file-stats [file]="file"></redaction-file-stats>
<redaction-file-stats [file]="file()"></redaction-file-stats>
</div>
<div class="user">
<iqser-initials-avatar [user]="file.assignee"></iqser-initials-avatar>
<iqser-initials-avatar [user]="file().assignee"></iqser-initials-avatar>
</div>
</div>
<div *ngFor="let config of displayedAttributes; trackBy: trackBy" class="small-label mt-8 attribute">
<redaction-file-attribute [dossier]="dossier" [fileAttribute]="config" [file]="file"></redaction-file-attribute>
</div>
@for (config of displayedAttributes(); track config.id) {
<div class="small-label mt-8 attribute">
<redaction-file-attribute [dossier]="dossier()" [fileAttribute]="config" [file]="file()"></redaction-file-attribute>
</div>
}
<redaction-file-workload [file]="file"></redaction-file-workload>
<redaction-file-workload [file]="file()"></redaction-file-workload>
<div class="file-actions overflow-visible">
<redaction-processing-indicator [file]="file" class="mr-8"></redaction-processing-indicator>
<redaction-processing-indicator [file]="file()" class="mr-8"></redaction-processing-indicator>
<div #actionsWrapper class="actions-wrapper">
<redaction-file-actions
*ngIf="!file.isProcessing"
[dossier]="dossier"
[file]="file"
[maxWidth]="width"
[singleEntityAction]="true"
iqserDisableStopPropagation
type="dossier-overview-workflow"
></redaction-file-actions>
@if (!file().isProcessing) {
<redaction-file-actions
[dossier]="dossier()"
[file]="file()"
[maxWidth]="width"
[singleEntityAction]="true"
iqserDisableStopPropagation
type="dossier-overview-workflow"
></redaction-file-actions>
}
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, computed, ElementRef, Input, OnInit, Optional, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, computed, ElementRef, input, OnInit, Optional, ViewChild } from '@angular/core';
import { DisableStopPropagationDirective, HelpModeService } from '@iqser/common-ui';
import { Debounce, trackByFactory } from '@iqser/common-ui/lib/utils';
import { Dossier, File, IFileAttributeConfig } from '@red/domain';
@ -11,7 +11,7 @@ import { FileAttributeComponent } from '../file-attribute/file-attribute.compone
import { FileWorkloadComponent } from '../table-item/file-workload/file-workload.component';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { FileActionsComponent } from '../../../shared-dossiers/components/file-actions/file-actions.component';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'redaction-workflow-item',
@ -27,10 +27,8 @@ import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
FileWorkloadComponent,
ProcessingIndicatorComponent,
FileActionsComponent,
NgIf,
DisableStopPropagationDirective,
AsyncPipe,
NgForOf,
InitialsAvatarComponent,
],
})
@ -38,9 +36,10 @@ export class WorkflowItemComponent implements OnInit {
@ViewChild('actionsWrapper', { static: true }) private _actionsWrapper: ElementRef;
width: number;
readonly trackBy = trackByFactory();
@Input({ required: true }) file: File;
@Input({ required: true }) dossier: Dossier;
@Input({ required: true }) displayedAttributes: IFileAttributeConfig[];
readonly file = input.required<File>();
readonly dossier = input.required<Dossier>();
readonly displayedAttributes = input.required<IFileAttributeConfig[]>();
readonly fileRouterLink = computed(() => this.file().routerLink);
constructor(
readonly fileAttributesService: FileAttributesService,

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="(files$ | async) && dossier$ | async as dossier">
@if ((files$ | async) && dossier$ | async; as dossier) {
<section>
<redaction-dossier-overview-screen-header
(upload)="fileInput.click()"
@ -8,60 +8,64 @@
<div class="overlay-shadow"></div>
<div class="content-inner">
<div *ngIf="listingMode$ | async as mode" [class.extended]="collapsedDetails" class="content-container">
<iqser-table
(noDataAction)="fileInput.click()"
*ngIf="mode === listingModes.table"
[bulkActions]="bulkActions"
[hasScrollButton]="true"
[headerHelpModeKey]="'document_list'"
[helpModeKey]="'document'"
[itemSize]="80"
[noDataButtonIcon]="'iqser:upload'"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataIcon]="'iqser:document'"
[noDataText]="'dossier-overview.no-data.title' | translate"
[noMatchText]="'dossier-overview.no-match.title' | translate"
[selectionEnabled]="true"
[showNoDataButton]="permissionsService.canUploadFiles(dossier)"
[tableColumnConfigs]="tableColumnConfigs"
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
[rowIdPrefix]="'file'"
[namePropertyKey]="'filename'"
></iqser-table>
@if (listingMode$ | async; as mode) {
<div [class.extended]="collapsedDetails" class="content-container">
@if (mode === listingModes.table) {
<iqser-table
(noDataAction)="fileInput.click()"
[bulkActions]="bulkActions"
[hasScrollButton]="true"
[headerHelpModeKey]="'document_list'"
[helpModeKey]="'document'"
[itemSize]="80"
[noDataButtonIcon]="'iqser:upload'"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataIcon]="'iqser:document'"
[noDataText]="'dossier-overview.no-data.title' | translate"
[noMatchText]="'dossier-overview.no-match.title' | translate"
[selectionEnabled]="true"
[showNoDataButton]="permissionsService.canUploadFiles(dossier)"
[tableColumnConfigs]="tableColumnConfigs"
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
[rowIdPrefix]="'file'"
[namePropertyKey]="'filename'"
></iqser-table>
} @else if (mode === listingModes.workflow) {
<iqser-workflow
(addElement)="fileInput.click()"
(noDataAction)="fileInput.click()"
[addElementIcon]="'iqser:upload'"
[bulkActions]="bulkActions"
[config]="workflowConfig"
[itemClasses]="{ disabled: disabledFn }"
[noDataButtonIcon]="'iqser:upload'"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataIcon]="'iqser:document'"
[noDataText]="'dossier-overview.no-data.title' | translate"
[showNoDataButton]="true"
[id]="'workflow-view'"
addElementColumn="NEW"
>
<ng-template #workflowItemTemplate let-entity="entity">
<redaction-workflow-item
[displayedAttributes]="displayedWorkflowAttributes"
[dossier]="dossier"
[file]="entity"
></redaction-workflow-item>
</ng-template>
</iqser-workflow>
}
</div>
}
<iqser-workflow
(addElement)="fileInput.click()"
(noDataAction)="fileInput.click()"
*ngIf="mode === listingModes.workflow"
[addElementIcon]="'iqser:upload'"
[bulkActions]="bulkActions"
[config]="workflowConfig"
[itemClasses]="{ disabled: disabledFn }"
[noDataButtonIcon]="'iqser:upload'"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataIcon]="'iqser:document'"
[noDataText]="'dossier-overview.no-data.title' | translate"
[showNoDataButton]="true"
[id]="'workflow-view'"
addElementColumn="NEW"
>
<ng-template #workflowItemTemplate let-entity="entity">
<redaction-workflow-item
[displayedAttributes]="displayedWorkflowAttributes"
[dossier]="dossier"
[file]="entity"
></redaction-workflow-item>
</ng-template>
</iqser-workflow>
</div>
<div *ngIf="dossierAttributes$ | async" [class.collapsed]="collapsedDetails" class="right-container">
<redaction-dossier-details
(toggleCollapse)="collapsedDetails = !collapsedDetails"
[dossierAttributes]="dossierAttributes"
></redaction-dossier-details>
</div>
@if (dossierAttributes$ | async) {
<div [class.collapsed]="collapsedDetails" class="right-container">
<redaction-dossier-details
(toggleCollapse)="collapsedDetails = !collapsedDetails"
[dossierAttributes]="dossierAttributes"
></redaction-dossier-details>
</div>
}
</div>
</section>
@ -82,7 +86,7 @@
[file]="file"
></redaction-table-item>
</ng-template>
</ng-container>
}
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [dossierTemplateId]="dossierTemplateId" [filter]="filter"></redaction-type-filter>

View File

@ -45,7 +45,7 @@ import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '../config.service';
import { BulkActionsService } from '../services/bulk-actions.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { DossierOverviewScreenHeaderComponent } from '../components/screen-header/dossier-overview-screen-header.component';
import { TranslateModule } from '@ngx-translate/core';
import { WorkflowItemComponent } from '../components/workflow-item/workflow-item.component';
@ -64,7 +64,6 @@ import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.
AsyncPipe,
IqserListingModule,
TranslateModule,
NgIf,
WorkflowItemComponent,
DossierDetailsComponent,
DossierOverviewBulkActionsComponent,
@ -128,11 +127,11 @@ export default class DossierOverviewScreenComponent extends ListingComponent<Fil
this.#updateFileAttributes();
}
get checkedRequiredFilters(): NestedFilter[] {
get #checkedRequiredFilters(): NestedFilter[] {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters(): NestedFilter[] {
get #checkedNotRequiredFilters(): NestedFilter[] {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => !f.required && f.checked);
}
@ -270,8 +269,8 @@ export default class DossierOverviewScreenComponent extends ListingComponent<Fil
this.#fileAttributeConfigs,
this.#dossier.dossierTemplateId,
this._needsWorkFilterTemplate,
() => this.checkedRequiredFilters,
() => this.checkedNotRequiredFilters,
() => this.#checkedRequiredFilters,
() => this.#checkedNotRequiredFilters,
);
this.filterService.addFilterGroups(filterGroups, true);
}

View File

@ -105,21 +105,6 @@ export class EditRedactionDialogComponent
readonly reasonStatus = formStatusToSignal(this.form.controls.reason);
readonly reasonValue = formValueToSignal(this.form.controls.reason);
readonly sectionValue = formValueToSignal(this.form.controls.section);
constructor(
private readonly _justificationsService: JustificationsService,
private readonly _dictionaryService: DictionaryService,
) {
super();
if (this.allRectangles) {
prefillPageRange(
this.data.annotations[0],
this.data.allFileAnnotations,
this.options as DetailsRadioOption<RectangleRedactOption>[],
);
}
}
readonly displayedDictionaryLabel = computed(() => {
const selectedDictionaryType = this.dictionaryType();
if (selectedDictionaryType) {
@ -142,6 +127,14 @@ export class EditRedactionDialogComponent
private readonly _dictionaryService: DictionaryService,
) {
super();
if (this.allRectangles) {
prefillPageRange(
this.data.annotations[0],
this.data.allFileAnnotations,
this.options as DetailsRadioOption<RectangleRedactOption>[],
);
}
}
get disabled() {

View File

@ -8,10 +8,10 @@
[icon]="btn.icon"
[showDot]="btn.showDot"
[tooltipClass]="btn.tooltipClass"
[tooltipPosition]="tooltipPosition"
[tooltipPosition]="tooltipPosition()"
[tooltip]="btn.tooltip | translate"
[type]="btn.buttonType || buttonType"
[attr.help-mode-key]="helpModeKey(btn)"
[type]="btn.buttonType || buttonType()"
[attr.help-mode-key]="btn.helpModeKey"
></iqser-circle-button>
<!-- download redacted file-->
@ -22,10 +22,10 @@
[dossier]="btn.dossier"
[files]="btn.files"
[tooltipClass]="btn.tooltipClass"
[tooltipPosition]="tooltipPosition"
[type]="buttonType"
[attr.help-mode-key]="helpModeKey(btn)"
[singleFileDownload]="singleEntityAction"
[tooltipPosition]="tooltipPosition()"
[type]="buttonType()"
[attr.help-mode-key]="btn.helpModeKey"
[singleFileDownload]="singleEntityAction()"
></redaction-file-download-btn>
<!-- exclude from redaction -->
@ -35,10 +35,10 @@
[checked]="btn.checked"
[disabled]="btn.disabled"
[id]="btn.id"
[matTooltipPosition]="tooltipPosition"
[matTooltipPosition]="tooltipPosition()"
[matTooltip]="btn.tooltip | translate"
[ngClass]="btn.class"
[attr.help-mode-key]="helpModeKey(btn)"
[attr.help-mode-key]="btn.helpModeKey"
color="primary"
iqserStopPropagation
></mat-slide-toggle>
@ -52,7 +52,7 @@
[attr.aria-expanded]="expanded"
[icon]="'iqser:more-actions'"
[matMenuTriggerFor]="hiddenButtonsMenu"
[type]="buttonType"
[type]="buttonType()"
buttonId="file-actions-menu-trigger-btn"
></iqser-circle-button>

View File

@ -1,4 +1,4 @@
import { booleanAttribute, Component, inject, input, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { Component, computed, effect, inject, input, viewChild } from '@angular/core';
import { Action, ActionTypes, Dossier, File } from '@red/domain';
import { CircleButtonComponent, CircleButtonType, IqserDialog, StopPropagationDirective, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -36,19 +36,20 @@ import { MatIcon } from '@angular/material/icon';
StopPropagationDirective,
],
})
export class ExpandableFileActionsComponent implements OnChanges {
@Input({ required: true }) actions: Action[];
@Input() maxWidth: number;
@Input() minWidth: number;
@Input() buttonType: CircleButtonType;
@Input() tooltipPosition: IqserTooltipPosition;
@Input() helpModeKeyPrefix: 'dossier' | 'editor';
@Input() isDossierOverviewWorkflow = false;
@Input() singleEntityAction = false;
export class ExpandableFileActionsComponent {
readonly _actions = input.required<Action[]>({ alias: 'actions' });
readonly maxWidth = input<number>();
readonly minWidth = input<number>();
readonly buttonType = input<CircleButtonType>();
readonly tooltipPosition = input<IqserTooltipPosition>();
readonly helpModeKeyPrefix = input<'dossier' | 'editor'>();
readonly isDossierOverviewWorkflow = input(false);
readonly singleEntityAction = input(false);
readonly actions = computed(() => this._actions().map(action => ({ ...action, helpModeKey: this.helpModeKey(action) })));
displayedButtons: Action[];
hiddenButtons: Action[];
expanded = false;
@ViewChild(MatMenuTrigger) readonly matMenu: MatMenuTrigger;
readonly matMenu = viewChild<MatMenuTrigger>(MatMenuTrigger);
readonly trackBy = trackByFactory();
readonly #appBaseHref = inject(APP_BASE_HREF);
@ -57,54 +58,51 @@ export class ExpandableFileActionsComponent implements OnChanges {
private readonly _toaster: Toaster,
private readonly _permissionsService: PermissionsService,
private readonly _dialog: IqserDialog,
) {}
ngOnChanges(changes: SimpleChanges) {
if (changes.actions || changes.maxWidth || changes.minWidth) {
) {
effect(() => {
let count = 0;
if (this.maxWidth) {
count = Math.floor(this.maxWidth / 36) || 1;
} else if (this.minWidth <= 850) {
count = Math.floor(this.minWidth / (this.actions.length * 15)) || 1;
if (this.minWidth <= 450) {
if (this.maxWidth()) {
count = Math.floor(this.maxWidth() / 36) || 1;
} else if (this.minWidth() <= 850) {
count = Math.floor(this.minWidth() / (this.actions().length * 15)) || 1;
if (this.minWidth() <= 450) {
this.displayedButtons = [];
this.hiddenButtons = [...this.actions];
this.hiddenButtons = [...this.actions()];
return;
}
} else {
this.displayedButtons = [...this.actions];
this.displayedButtons = [...this.actions()];
this.hiddenButtons = [];
return;
}
if (count >= this.actions.length) {
this.displayedButtons = [...this.actions];
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);
this.displayedButtons = this.actions().slice(0, count - 1);
this.hiddenButtons = this.actions().slice(count - 1);
}
}
});
if (changes.actions) {
// Patch download button
const downloadBtn = this.actions.find(btn => btn.type === ActionTypes.downloadBtn);
effect(() => {
const downloadBtn = this.actions().find(btn => btn.type === ActionTypes.downloadBtn);
if (downloadBtn) {
downloadBtn.action = () => this.#downloadFiles(downloadBtn.files, downloadBtn.dossier);
downloadBtn.disabled = !this._permissionsService.canDownloadFiles(downloadBtn.files, downloadBtn.dossier);
}
}
});
}
helpModeKey(action: Action) {
return action.helpModeKey
? `${this.helpModeKeyPrefix}${this.isDossierOverviewWorkflow ? '_workflow' : ''}_${action.helpModeKey}`
? `${this.helpModeKeyPrefix()}${this.isDossierOverviewWorkflow() ? '_workflow' : ''}_${action.helpModeKey}`
: '';
}
onButtonClick(button: Action, $event: MouseEvent) {
button.action($event);
this.matMenu.closeMenu();
this.matMenu().closeMenu();
}
async #downloadFiles(files: File[], dossier: Dossier) {

View File

@ -5,6 +5,7 @@ import { Roles } from '@users/roles';
import { of } from 'rxjs';
import { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users';
import { List } from '@iqser/common-ui/lib/utils';
import { toSignal } from '@angular/core/rxjs-interop';
@Injectable({
providedIn: 'root',
@ -12,6 +13,7 @@ import { List } from '@iqser/common-ui/lib/utils';
export class UserService extends IqserUserService<IIqserUser, User> {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = User;
readonly allUsers = toSignal(this.all$);
async loadCurrentUser(): Promise<User | undefined> {
const currentUser = await super.loadCurrentUser();