diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index 0033f3247..d4e59469e 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -23,6 +23,7 @@ import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIE import { DossierFilesGuard } from '@guards/dossier-files-guard'; import { WebViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard'; import { ROLES } from '@users/roles'; +import { editAttributeGuard } from '@guards/edit-attribute.guard'; const dossierTemplateIdRoutes: IqserRoutes = [ { @@ -40,6 +41,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [ { path: `:${DOSSIER_ID}`, canActivate: [CompositeRouteGuard, IqserPermissionsGuard], + canDeactivate: [editAttributeGuard], data: { routeGuards: [DossierFilesGuard], breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier], diff --git a/apps/red-ui/src/app/guards/edit-attribute.guard.ts b/apps/red-ui/src/app/guards/edit-attribute.guard.ts new file mode 100644 index 000000000..6c76b26d7 --- /dev/null +++ b/apps/red-ui/src/app/guards/edit-attribute.guard.ts @@ -0,0 +1,6 @@ +import { CanDeactivateFn } from '@angular/router'; +import { inject } from '@angular/core'; +import { FileAttributesService } from '@services/entity-services/file-attributes.service'; +import { DossierOverviewScreenComponent } from '../modules/dossier-overview/screen/dossier-overview-screen.component'; + +export const editAttributeGuard: CanDeactivateFn = () => !inject(FileAttributesService).isEditingAttribute; diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.html new file mode 100644 index 000000000..28cb398fb --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.html @@ -0,0 +1,71 @@ + +
+
+ {{ fileAttribute.label }}: + + {{ fileAttributeValue || '-' }} + + + {{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }} + +
+ + +
+
+ +
+
+
+
+ + +
+
+ + + + + +
+
+
+
+ + diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.scss b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.scss new file mode 100644 index 000000000..a4410d2b2 --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.scss @@ -0,0 +1,191 @@ +.file-attribute { + height: 100%; + width: 100%; + display: flex; + align-items: center; + + &.workflow-attribute { + padding: 2px; + position: relative; + } + + .value { + z-index: 1; + } + + .workflow-value { + display: flex; + width: 90%; + + b { + text-transform: uppercase; + padding-right: 5px; + white-space: nowrap; + } + + span { + word-break: unset; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .edit-icon { + display: none; + } + + .help-mode-button { + background-color: var(--iqser-grey-6); + width: 90%; + height: 50%; + border-radius: 4px; + position: absolute; + margin-left: -10px; + } + + .edit-input { + cursor: default; + display: flex; + z-index: 2; + border: solid var(--iqser-grey-6); + border-radius: 10px; + background: var(--iqser-background); + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); + margin-left: -10px; + + &.workflow-edit-input { + justify-content: space-between; + box-shadow: none; + width: 100%; + position: absolute; + left: 0; + top: -5px; + border: none; + + form { + width: 100%; + } + + iqser-circle-button { + margin: 0 5px; + + &:nth-child(2) { + padding-left: 10px; + } + + &:last-child { + margin-right: -8px; + } + } + } + + form { + margin: 5px; + display: flex; + align-items: center; + + iqser-dynamic-input { + margin-top: 0; + } + + .workflow-input { + width: 100%; + padding-left: 2px; + + ::ng-deep .iqser-input-group { + width: 100%; + margin-top: 0; + + input { + margin-top: 0; + min-height: 14px; + line-height: 0; + padding-top: 0; + border: solid 1px gray; + font-size: 12px; + padding-left: 5px; + } + } + } + + .save { + margin-left: 7px; + } + } + } +} + +.file-attribute:hover { + &.workflow-attribute { + background-color: var(--iqser-grey-6); + border-radius: 4px; + padding: 2px; + } + + .workflow-value { + b { + white-space: nowrap; + overflow: unset; + } + + span { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .edit-button { + background-color: var(--iqser-grey-6); + width: 100%; + height: 50%; + border-radius: 4px; + position: absolute; + margin-left: -10px; + + &.workflow-edit-button { + background-color: transparent; + position: relative; + top: 0; + } + } + + .edit-icon { + z-index: 1; + background: white; + width: 23px; + height: 23px; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + right: 0; + border-radius: 50%; + margin-top: -8px; + margin-right: -8px; + + &.workflow-edit-icon { + background: none; + top: 0; + margin-right: 5px; + width: 14px; + height: 14px; + } + + mat-icon { + width: 13px; + height: 13px; + } + } +} + +.hide { + visibility: hidden; +} + +@media screen and (max-width: 1395px) { + .file-attribute .edit-input form .workflow-input { + width: 63%; + } +} diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.ts similarity index 67% rename from apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.ts rename to apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.ts index 9dd17473c..6b7e82b2a 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/file-attribute/file-attribute.component.ts @@ -1,6 +1,6 @@ -import { Component, HostListener, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, HostListener, Input, OnDestroy } from '@angular/core'; import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain'; -import { BaseFormComponent, ListingService, Toaster } from '@iqser/common-ui'; +import { BaseFormComponent, Debounce, HelpModeService, ListingService, Toaster } from '@iqser/common-ui'; import { PermissionsService } from '@services/permissions.service'; import { FormBuilder, UntypedFormGroup } from '@angular/forms'; import { FileAttributesService } from '@services/entity-services/file-attributes.service'; @@ -10,6 +10,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import dayjs from 'dayjs'; import { NavigationEnd, Router } from '@angular/router'; import { filter, map, tap } from 'rxjs/operators'; +import { ConfigService } from '../../config.service'; @Component({ selector: 'redaction-file-attribute [fileAttribute] [file] [dossier]', @@ -24,6 +25,20 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr isInEditMode = false; closedDatepicker = true; readonly #subscriptions = new Subscription(); + readonly shouldClose$ = this.fileAttributesService.isEditingFileAttribute$.pipe( + filter(value => value === true), + tap(value => { + if ( + value && + !!this.file && + !!this.fileAttribute && + (this.fileAttribute.id !== this.fileAttributesService.openAttributeEdit$.value || + this.file.fileId !== this.fileAttributesService.fileEdit$.value) + ) { + this.close(); + } + }), + ); constructor( router: Router, @@ -33,6 +48,8 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr readonly permissionsService: PermissionsService, private readonly _listingService: ListingService, readonly fileAttributesService: FileAttributesService, + readonly helpModeService: HelpModeService, + readonly configService: ConfigService, ) { super(); const sub = router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => this.close()); @@ -53,13 +70,32 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id]; } + @Debounce(50) + @HostListener('document:click') + clickOutside() { + if (this.isInEditMode && this.closedDatepicker) { + this.close(); + } + } + ngOnDestroy() { this.#subscriptions.unsubscribe(); } - async editFileAttribute($event: MouseEvent): Promise { - $event.stopPropagation(); - this.#toggleEdit(); + editFileAttribute($event: MouseEvent) { + if (!this.file.isInitialProcessing && this.permissionsService.canEditFileAttributes(this.file, this.dossier)) { + $event.stopPropagation(); + this.#toggleEdit(); + this.fileAttributesService.setFileEdit(this.file.fileId); + this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute.id); + this.fileAttributesService.openAttributeEdits$.next([ + ...this.fileAttributesService.openAttributeEdits$.value, + { + attribute: this.fileAttribute.id, + file: this.file.id, + }, + ]); + } } async save($event?: MouseEvent) { @@ -82,22 +118,24 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr this._toaster.error(_('file-attribute.update.error')); } - this.#toggleEdit(); + this.close(); } close($event?: MouseEvent): void { $event?.stopPropagation(); - if (this.isInEditMode) { this.form = this.#getForm(); this.#toggleEdit(); - } - } - - @HostListener('document:click') - clickOutside() { - if (this.isInEditMode && this.closedDatepicker) { - this.close(); + this.fileAttributesService.openAttributeEdits$.next( + this.fileAttributesService.openAttributeEdits$.value.filter( + element => + JSON.stringify(element) !== + JSON.stringify({ + attribute: this.fileAttribute.id, + file: this.file.id, + }), + ), + ); } } @@ -117,11 +155,14 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr const attrValue = fileAttributes[key]; config[key] = [dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue]; }); - return this._formBuilder.group(config); + return this._formBuilder.group(config, { + validators: control => + !control.get(this.fileAttribute.id).value?.trim().length && !this.fileAttributeValue ? { emptyString: true } : null, + }); } #formatAttributeValue(attrValue) { - return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue; + return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue.trim().replaceAll(/\s\s+/g, ' '); } #toggleEdit(): void { @@ -132,7 +173,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr } this.isInEditMode = !this.isInEditMode; - this.fileAttributesService.isEditingFileAttribute$.next(this.isInEditMode); if (this.isInEditMode) { this.#focusOnEditInput(); @@ -142,7 +182,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr #focusOnEditInput(): void { setTimeout(() => { const input = document.getElementById(this.fileAttribute.id); - input.focus(); + input?.focus(); }, 100); } } diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.html deleted file mode 100644 index 2457fa914..000000000 --- a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
- {{ fileAttributeValue || '-' }} - - {{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }} - - - -
- -
- - -
-
- - - - - -
-
-
-
-
diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.scss b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.scss deleted file mode 100644 index 5d0d3d2a4..000000000 --- a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/file-attribute/file-attribute.component.scss +++ /dev/null @@ -1,45 +0,0 @@ -.file-attribute { - height: 100%; - width: 100%; - display: flex; - align-items: center; - - .edit-button { - position: absolute; - height: 100%; - right: 10%; - width: 90%; - background: linear-gradient(to left, var(--iqser-side-nav) 20%, rgba(244, 245, 247, 0) 60%); - - iqser-circle-button { - position: absolute; - top: 50%; - left: 80%; - transform: translate(-50%, -50%); - } - } - - .edit-input { - cursor: default; - display: flex; - z-index: 1; - border: solid var(--iqser-grey-4); - border-radius: 10px; - background: var(--iqser-background); - box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); - - form { - margin: 5px; - display: flex; - align-items: center; - - iqser-dynamic-input { - margin-top: 0; - } - - .save { - margin-left: 7px; - } - } - } -} diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html index 60d8510f2..8b41e46ad 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/table-item/table-item.component.html @@ -71,6 +71,7 @@ *ngIf="!file.isProcessing" [dossier]="dossier" [file]="file" + (click)="$event.stopPropagation()" class="mr-4" type="dossier-overview-list" > diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/workflow-item/workflow-item.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/workflow-item/workflow-item.component.html index 9b743922e..3b37ea586 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/workflow-item/workflow-item.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/workflow-item/workflow-item.component.html @@ -14,7 +14,7 @@
- {{ file.fileAttributes.attributeIdToValue[config.id] || '-' }} +
diff --git a/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts b/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts index 3e2a64bfb..779e81777 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/dossier-overview.module.ts @@ -25,7 +25,7 @@ import { FileWorkloadComponent } from './components/table-item/file-workload/fil import { WorkflowItemComponent } from './components/workflow-item/workflow-item.component'; import { DossierOverviewScreenHeaderComponent } from './components/screen-header/dossier-overview-screen-header.component'; import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component'; -import { FileAttributeComponent } from './components/table-item/file-attribute/file-attribute.component'; +import { FileAttributeComponent } from './components/file-attribute/file-attribute.component'; const routes: Routes = [ { diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.html b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.html index 0fffc25b6..596dabaa8 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.html +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index 1d54670e4..55f602d65 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -142,7 +142,7 @@ export class FileActionsComponent implements OnChanges { { id: 'assign-to-me-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this._assignToMe($event), + action: () => this._assignToMe(), tooltip: _('dossier-overview.assign-me'), icon: 'red:assign-me', show: this.showAssignToSelf, @@ -189,7 +189,7 @@ export class FileActionsComponent implements OnChanges { { id: 'set-file-to-new-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this.#setToNew($event), + action: () => this.#setToNew(), tooltip: _('dossier-overview.back-to-new'), icon: 'red:undo', show: this.showSetToNew, @@ -222,7 +222,7 @@ export class FileActionsComponent implements OnChanges { { id: 'toggle-automatic-analysis-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event), + action: () => this._toggleAutomaticAnalysis(), tooltip: _('dossier-overview.stop-auto-analysis'), icon: 'red:disable-analysis', show: this.canDisableAutoAnalysis, @@ -240,7 +240,7 @@ export class FileActionsComponent implements OnChanges { { id: 'toggle-automatic-analysis-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event), + action: () => this._toggleAutomaticAnalysis(), tooltip: _('dossier-overview.start-auto-analysis'), buttonType: this.isFilePreview ? CircleButtonTypes.warn : CircleButtonTypes.default, icon: 'red:enable-analysis', @@ -257,7 +257,7 @@ export class FileActionsComponent implements OnChanges { { id: 'ocr-file-btn-' + fileId, type: ActionTypes.circleBtn, - action: ($event: MouseEvent) => this._ocrFile($event), + action: () => this._ocrFile(), tooltip: _('dossier-overview.ocr-file'), icon: 'iqser:ocr', show: this.showOCR, @@ -294,7 +294,6 @@ export class FileActionsComponent implements OnChanges { } async setFileApproved($event: MouseEvent) { - $event.stopPropagation(); if (!this.file.analysisRequired && !this.file.hasUpdates) { await this.#setFileApproved(); return; @@ -369,8 +368,7 @@ export class FileActionsComponent implements OnChanges { this._dialogService.openDialog('assignFile', $event, { targetStatus, files, withCurrentUserAsDefault, withUnassignedOption }); } - private async _assignToMe($event: MouseEvent) { - $event.stopPropagation(); + private async _assignToMe() { await this._fileAssignService.assignToMe([this.file]); } @@ -383,20 +381,17 @@ export class FileActionsComponent implements OnChanges { await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file], this.file.dossierId, params)); } - private async _toggleAutomaticAnalysis($event: MouseEvent) { - $event.stopPropagation(); + private async _toggleAutomaticAnalysis() { this._loadingService.start(); await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file.dossierId, [this.file])); this._loadingService.stop(); } private async _setFileUnderApproval($event: MouseEvent) { - $event.stopPropagation(); await this._fileAssignService.assignApprover($event, this.file, true); } - private async _ocrFile($event: MouseEvent) { - $event.stopPropagation(); + private async _ocrFile() { if (this.file.lastManualChangeDate) { const confirm = await firstValueFrom(this.#showOCRConfirmationDialog()); if (!confirm) { @@ -476,8 +471,7 @@ export class FileActionsComponent implements OnChanges { this._loadingService.stop(); } - async #setToNew($event: MouseEvent) { - $event.stopPropagation(); + async #setToNew() { this._loadingService.start(); await this._filesService.setToNew(this.file); this._loadingService.stop(); diff --git a/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts b/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts index e724e48a7..064dc1a01 100644 --- a/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts +++ b/apps/red-ui/src/app/services/entity-services/file-attributes.service.ts @@ -1,7 +1,7 @@ import { EntitiesService, List, RequiredParam, Validate } from '@iqser/common-ui'; import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, of } from 'rxjs'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { FileAttributeConfig, FileAttributes, IFileAttributeConfig, IFileAttributesConfig } from '@red/domain'; export type FileAttributesConfigMap = Readonly>; @@ -14,8 +14,27 @@ export class FileAttributesService extends EntitiesService({}); - readonly isEditingFileAttribute$ = new BehaviorSubject(false); + readonly openAttributeEdits$ = new BehaviorSubject([]); + readonly isEditingFileAttribute$: Observable = this.openAttributeEdits$.pipe( + distinctUntilChanged(), + map(value => value.length > 0), + ); + + readonly openAttributeEdit$ = new BehaviorSubject(''); + readonly fileEdit$ = new BehaviorSubject(''); + + get isEditingAttribute() { + return this.openAttributeEdits$.value.length > 0; + } + + setOpenAttributeEdit(attributeId: string) { + this.openAttributeEdit$.next(attributeId); + } + + setFileEdit(fileId: string) { + this.fileEdit$.next(fileId); + } /** * Get the file attributes that can be used at importing csv. */