RED-6973: Made attribute editing work on 3.6.3.

This commit is contained in:
Nicoleta Panaghiu 2023-07-18 17:56:49 +03:00
parent b2156dbabd
commit f05146558c
13 changed files with 362 additions and 126 deletions

View File

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

View File

@ -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<DossierOverviewScreenComponent> = () => !inject(FileAttributesService).isEditingAttribute;

View File

@ -0,0 +1,71 @@
<ng-container *ngIf="configService.listingMode$ | async as mode">
<div (click)="editFileAttribute($event)" [ngClass]="{ 'workflow-attribute': mode === 'workflow' }" class="file-attribute">
<div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value">
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
<span
*ngIf="!isDate; else date"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
[matTooltip]="fileAttributeValue"
>
{{ fileAttributeValue || '-' }}</span
>
<ng-template #date>
<span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }">
{{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }}</span
>
</ng-template>
</div>
<ng-container
*ngIf="
(fileAttributesService.isEditingAttribute === false || isInEditMode) &&
!file.isInitialProcessing &&
permissionsService.canEditFileAttributes(file, dossier)
"
>
<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' }"
class="action-buttons edit-button"
>
<div [ngClass]="{ 'workflow-edit-icon': mode === 'workflow' }" class="edit-icon">
<mat-icon [svgIcon]="'iqser:edit'"></mat-icon>
</div>
</div>
</ng-container>
</div>
<ng-template #input>
<div [ngClass]="{ 'workflow-edit-input': mode === 'workflow' }" class="edit-input">
<form (ngSubmit)="form.valid && save()" [formGroup]="form">
<iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keydown.escape)="close()"
[formControlName]="fileAttribute.id"
[id]="fileAttribute.id"
[ngClass]="{ 'workflow-input': mode === 'workflow' }"
[type]="fileAttribute.type"
></iqser-dynamic-input>
<iqser-circle-button
(action)="save()"
[disabled]="disabled"
[icon]="'iqser:check'"
[size]="mode === 'workflow' ? 15 : 34"
class="save"
></iqser-circle-button>
<iqser-circle-button
(action)="close($event)"
[icon]="'iqser:close'"
[size]="mode === 'workflow' ? 15 : 34"
></iqser-circle-button>
</form>
</div>
</ng-template>
</ng-container>
<ng-container *ngIf="shouldClose$ | async"></ng-container>

View File

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

View File

@ -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<File>,
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<void> {
$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);
}
}

View File

@ -1,43 +0,0 @@
<div class="file-attribute">
<span *ngIf="!isDate; else date" class="clamp-3"> {{ fileAttributeValue || '-' }}</span>
<ng-template #date>
<span class="clamp-3"> {{ fileAttributeValue ? (fileAttributeValue | date : 'd MMM yyyy') : '-' }}</span>
</ng-template>
<ng-container *ngIf="((fileAttributesService.isEditingFileAttribute$ | async) === false || isInEditMode) && !file.isInitialProcessing">
<div *ngIf="!isInEditMode; else input" class="action-buttons edit-button">
<iqser-circle-button
(action)="editFileAttribute($event)"
*ngIf="permissionsService.canEditFileAttributes(file, dossier)"
[disabled]="!fileAttribute.editable"
[icon]="'iqser:edit'"
[tooltip]="'file-attribute.actions.edit' | translate"
[iqserHelpMode]="'edit-file-attributes'"
id="edit-attribute-button"
></iqser-circle-button>
</div>
<ng-template #input>
<div class="edit-input" stopPropagation>
<form (submit)="save()" [formGroup]="form">
<iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keydown.escape)="close()"
[formControlName]="fileAttribute.id"
[id]="fileAttribute.id"
[type]="fileAttribute.type"
></iqser-dynamic-input>
<iqser-circle-button
(action)="save($event)"
[disabled]="disabled"
[icon]="'iqser:check'"
class="save"
></iqser-circle-button>
<iqser-circle-button (action)="close($event)" [icon]="'iqser:close'"></iqser-circle-button>
</form>
</div>
</ng-template>
</ng-container>
</div>

View File

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

View File

@ -71,6 +71,7 @@
*ngIf="!file.isProcessing"
[dossier]="dossier"
[file]="file"
(click)="$event.stopPropagation()"
class="mr-4"
type="dossier-overview-list"
></redaction-file-actions>

View File

@ -14,7 +14,7 @@
</div>
<div *ngFor="let config of displayedAttributes" class="small-label mt-4">
<b> {{ file.fileAttributes.attributeIdToValue[config.id] || '-' }} </b>
<redaction-file-attribute [file]="file" [dossier]="dossier" [fileAttribute]="config"></redaction-file-attribute>
</div>
<redaction-file-workload [file]="file"></redaction-file-workload>

View File

@ -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 = [
{

View File

@ -1,4 +1,4 @@
<div *ngIf="isDossierOverviewList && (fileAttributesService.isEditingFileAttribute$ | async) === false" class="action-buttons">
<div *ngIf="isDossierOverviewList && !fileAttributesService.isEditingAttribute" class="action-buttons">
<ng-container *ngTemplateOutlet="actions"></ng-container>
<redaction-processing-indicator *ngIf="showStatusBar" [file]="file"></redaction-processing-indicator>

View File

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

View File

@ -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<Record<string, IFileAttributesConfig>>;
@ -14,8 +14,27 @@ export class FileAttributesService extends EntitiesService<IFileAttributeConfig,
protected readonly _entityClass = FileAttributeConfig;
readonly fileAttributesConfig$ = new BehaviorSubject<FileAttributesConfigMap>({});
readonly isEditingFileAttribute$ = new BehaviorSubject(false);
readonly openAttributeEdits$ = new BehaviorSubject([]);
readonly isEditingFileAttribute$: Observable<boolean> = 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.
*/