From 54c4995f6129629e3ddf9ae33e1304e801b160e6 Mon Sep 17 00:00:00 2001 From: Valentin Date: Wed, 15 Dec 2021 10:37:48 +0200 Subject: [PATCH] WIP on warning the user before leaving the page with unsaved changes --- .../add-edit-dictionary-dialog.component.ts | 10 +-- ...-edit-dossier-template-dialog.component.ts | 12 ++-- ...dd-edit-file-attribute-dialog.component.ts | 11 ++-- .../edit-color-dialog.component.ts | 10 +-- .../edit-dossier-dialog.component.html | 2 +- .../edit-dossier-dialog.component.ts | 10 +-- .../shared/dialog/base-dialog.component.ts | 65 +++++++++++++++++++ .../dialog/confirmation-dialog.service.ts | 36 ++++++++++ .../shared/dialog/form-dialog.component.ts | 33 ++++++++++ .../src/app/modules/shared/shared.module.ts | 2 + apps/red-ui/src/assets/i18n/en.json | 7 ++ 11 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 apps/red-ui/src/app/modules/shared/dialog/base-dialog.component.ts create mode 100644 apps/red-ui/src/app/modules/shared/dialog/confirmation-dialog.service.ts create mode 100644 apps/red-ui/src/app/modules/shared/dialog/form-dialog.component.ts diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts index 737d96915..7af17f9e4 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Observable } from 'rxjs'; -import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-ui'; +import { shareDistinctLast, Toaster } from '@iqser/common-ui'; import { TranslateService } from '@ngx-translate/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { AppStateService } from '@state/app-state.service'; @@ -12,6 +12,7 @@ import { Dictionary, IDictionary } from '@red/domain'; import { UserService } from '@services/user.service'; import { map } from 'rxjs/operators'; import { HttpStatusCode } from '@angular/common/http'; +import { BaseDialogComponent } from '../../../shared/dialog/base-dialog.component'; @Component({ selector: 'redaction-add-edit-dictionary-dialog', @@ -39,11 +40,12 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent { private readonly _appStateService: AppStateService, private readonly _translateService: TranslateService, private readonly _dictionaryService: DictionaryService, - private readonly _dialogRef: MatDialogRef, + protected readonly _injector: Injector, + protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string }, ) { - super(); + super(_injector, _dialogRef); this.hasColor$ = this._colorEmpty$; this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value))); } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts index 39025d390..9755a2839 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, Injector } from '@angular/core'; import { AppStateService } from '@state/app-state.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -7,10 +7,11 @@ import { Moment } from 'moment'; import { applyIntervalConstraints } from '@utils/date-inputs-utils'; import { downloadTypesTranslations } from '../../../../translations/download-types-translations'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; -import { BaseDialogComponent, Toaster } from '@iqser/common-ui'; +import { Toaster } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DownloadFileType, IDossierTemplate } from '@red/domain'; import { HttpStatusCode } from '@angular/common/http'; +import { BaseDialogComponent } from '../../../shared/dialog/base-dialog.component'; @Component({ templateUrl: './add-edit-dossier-template-dialog.component.html', @@ -34,10 +35,11 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent { private readonly _toaster: Toaster, private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _formBuilder: FormBuilder, - public dialogRef: MatDialogRef, + protected readonly _injector: Injector, + protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) readonly dossierTemplate: IDossierTemplate, ) { - super(); + super(_injector, _dialogRef); this.hasValidFrom = !!this.dossierTemplate?.validFrom; this.hasValidTo = !!this.dossierTemplate?.validTo; @@ -97,7 +99,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent { await this._dossierTemplatesService.createOrUpdate(dossierTemplate).toPromise(); await this._dossierTemplatesService.loadAll().toPromise(); await this._appStateService.loadDictionaryData(); - this.dialogRef.close(true); + this._dialogRef.close(true); } catch (error: any) { const message = error.status === HttpStatusCode.Conflict diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component.ts index 7346bf384..2ad60ad14 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { fileAttributeTypesTranslations } from '../../translations/file-attribute-types-translations'; import { FileAttributesService } from '@services/entity-services/file-attributes.service'; -import { BaseDialogComponent } from '@iqser/common-ui'; +import { BaseDialogComponent } from '../../../shared/dialog/base-dialog.component'; @Component({ selector: 'redaction-add-edit-file-attribute-dialog', @@ -26,7 +26,8 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent { constructor( private readonly _formBuilder: FormBuilder, private readonly _fileAttributesService: FileAttributesService, - public dialogRef: MatDialogRef, + protected readonly _injector: Injector, + protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { fileAttribute: IFileAttributeConfig; @@ -35,7 +36,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent { numberOfFilterableAttrs: number; }, ) { - super(); + super(_injector, _dialogRef); this.canSetDisplayed = data.numberOfDisplayedAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.displayedInFileList; this.canSetFilterable = data.numberOfFilterableAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.filterable; this.form = this._getForm(this.fileAttribute); @@ -69,7 +70,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent { editable: !this.form.get('readonly').value, ...this.form.getRawValue(), }; - this.dialogRef.close(fileAttribute); + this._dialogRef.close(fileAttribute); } private _getForm(fileAttribute: IFileAttributeConfig): FormGroup { diff --git a/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts index 519eb2dd2..2af506f39 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts @@ -1,12 +1,13 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, Injector } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DefaultColorType, IColors } from '@red/domain'; -import { BaseDialogComponent, Toaster } from '@iqser/common-ui'; +import { Toaster } from '@iqser/common-ui'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { defaultColorsTranslations } from '../../translations/default-colors-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DictionaryService } from '@shared/services/dictionary.service'; +import { BaseDialogComponent } from '../../../shared/dialog/base-dialog.component'; interface IEditColorData { colors: IColors; @@ -30,11 +31,12 @@ export class EditColorDialogComponent extends BaseDialogComponent { private readonly _dictionaryService: DictionaryService, private readonly _toaster: Toaster, private readonly _translateService: TranslateService, - private readonly _dialogRef: MatDialogRef, + protected readonly _injector: Injector, + protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) readonly data: IEditColorData, ) { - super(); + super(_injector, _dialogRef); this._dossierTemplateId = data.dossierTemplateId; this._initialColor = data.colors[data.colorKey]; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html index 8b9f90c77..2500c6cd4 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html @@ -65,5 +65,5 @@ - + diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts index 87235becf..864d98dcb 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Injector, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Dossier } from '@red/domain'; import { EditDossierGeneralInfoComponent } from './general-info/edit-dossier-general-info.component'; import { EditDossierDownloadPackageComponent } from './download-package/edit-dossier-download-package.component'; import { EditDossierSectionInterface } from './edit-dossier-section.interface'; -import { BaseDialogComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; +import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component'; import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component'; @@ -14,6 +14,7 @@ import { DossiersService } from '@services/entity-services/dossiers.service'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { EditDossierTeamComponent } from './edit-dossier-team/edit-dossier-team.component'; +import { BaseDialogComponent } from '@shared/dialog/base-dialog.component'; type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'members' | 'dossierAttributes' | 'deletedDocuments'; @@ -39,15 +40,16 @@ export class EditDossierDialogComponent extends BaseDialogComponent { private readonly _toaster: Toaster, private readonly _dossiersService: DossiersService, private readonly _changeRef: ChangeDetectorRef, - private readonly _dialogRef: MatDialogRef, private readonly _loadingService: LoadingService, + protected readonly _injector: Injector, + protected readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private readonly _data: { dossierId: string; section?: Section; }, ) { - super(); + super(_injector, _dialogRef); this.navItems = [ { key: 'dossierInfo', diff --git a/apps/red-ui/src/app/modules/shared/dialog/base-dialog.component.ts b/apps/red-ui/src/app/modules/shared/dialog/base-dialog.component.ts new file mode 100644 index 000000000..efdd9ba5a --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/dialog/base-dialog.component.ts @@ -0,0 +1,65 @@ +import { Directive, HostListener, Injector, OnInit } from '@angular/core'; +import { AutoUnsubscribe, IqserEventTarget } from '../../../../../../../libs/common-ui/src'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ConfirmationDialogService } from '@shared/dialog/confirmation-dialog.service'; + +@Directive() +/** + * Extend this component when you want to submit the form after pressing enter. + * + * This could be done by adding properties (submit)="save()" on the form and type="submit" on the save button. + * However, some components (e.g. redaction-select, color picker) don't set focus on the input after choosing a value. + * Also, other components (e.g. dropdown select) trigger a different action on enter, instead of submit. + * + * Make sure to remove property type="submit" from the save button and the (submit)="save()" property from the form + * (otherwise the save request will be triggered twice). + * */ +export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnInit { + abstract changed: boolean; + abstract valid: boolean; + abstract disabled: boolean; + + private readonly _dialogService: ConfirmationDialogService = this._injector.get(ConfirmationDialogService); + + private _waitingForConfirmation = false; + + constructor(protected readonly _injector: Injector, protected readonly _dialogRef: MatDialogRef) { + super(); + } + + abstract save(): void; + + ngOnInit(): void { + this.addSubscription = this._dialogRef.backdropClick().subscribe(() => { + this.close(); + }); + } + + close(): void { + if (this.changed) { + this._waitingForConfirmation = true; + const dialogRef = this._dialogService.openDialog(() => this._dialogRef.close()); + dialogRef + .afterClosed() + .toPromise() + .then(() => (this._waitingForConfirmation = false)); + return; + } + this._dialogRef.close(); + } + + @HostListener('window:keydown.Enter', ['$event']) + onEnter(event: KeyboardEvent): void { + const node = (event.target as IqserEventTarget).localName; + if (this.valid && !this.disabled && this.changed && node !== 'textarea') { + this.save(); + } + } + + @HostListener('window:keydown.Escape', ['$event']) + onEscape(): void { + if (!this._waitingForConfirmation) { + this.close(); + } + } +} diff --git a/apps/red-ui/src/app/modules/shared/dialog/confirmation-dialog.service.ts b/apps/red-ui/src/app/modules/shared/dialog/confirmation-dialog.service.ts new file mode 100644 index 000000000..508bb40a4 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/dialog/confirmation-dialog.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { ConfirmationDialogComponent, ConfirmationDialogInput, DialogConfig, DialogService, TitleColors } from '@iqser/common-ui'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +type DialogType = 'confirm'; + +@Injectable() +export class ConfirmationDialogService extends DialogService { + protected readonly _config: DialogConfig = { + confirm: { + component: ConfirmationDialogComponent, + dialogConfig: { disableClose: false }, + }, + }; + + constructor(protected readonly _dialog: MatDialog) { + super(_dialog); + } + + openDialog(cb: any): MatDialogRef { + return super.openDialog( + 'confirm', + null, + new ConfirmationDialogInput({ + title: _('confirmation-dialog.unsaved-changes.title'), + question: _('confirmation-dialog.unsaved-changes.question'), + details: _('confirmation-dialog.unsaved-changes.details'), + confirmationText: _('confirmation-dialog.unsaved-changes.confirmation-text'), + denyText: _('confirmation-dialog.unsaved-changes.deny-text'), + titleColor: TitleColors.WARN, + }), + cb, + ); + } +} diff --git a/apps/red-ui/src/app/modules/shared/dialog/form-dialog.component.ts b/apps/red-ui/src/app/modules/shared/dialog/form-dialog.component.ts new file mode 100644 index 000000000..63072f58b --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/dialog/form-dialog.component.ts @@ -0,0 +1,33 @@ +// import { BaseDialogComponent } from './base-dialog.component'; +// import { Injector, OnInit } from '@angular/core'; +// import { MatDialogRef } from '@angular/material/dialog'; +// import { FormGroup } from '@angular/forms'; +// +// export abstract class FormDialogComponent extends BaseDialogComponent implements OnInit { +// +// abstract readonly form: FormGroup; +// private _hasChange = false; +// +// constructor( +// protected readonly _injector: Injector, +// protected readonly _dialogRef: MatDialogRef, +// ) { +// super(_injector, _dialogRef); +// } +// +// get changed(): boolean { +// return this._hasChange; +// } +// +// onFormGroupValueChange() { +// const initialValue = this.form.value; +// this.createGroupForm.valueChanges.subscribe(value => { +// this.hasChange = Object.keys(initialValue).some(key => this.form.value[key] != +// initialValue[key]); +// }); +// } +// +// get valid(): boolean { +// return this.form.valid; +// } +// } diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index 4f606ceb9..81e3e3122 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -26,6 +26,7 @@ import { TypeFilterComponent } from './components/type-filter/type-filter.compon import { TeamMembersComponent } from './components/team-members/team-members.component'; import { EditorComponent } from './components/editor/editor.component'; import { ExpandableFileActionsComponent } from './components/expandable-file-actions/expandable-file-actions.component'; +import { ConfirmationDialogService } from '@shared/dialog/confirmation-dialog.service'; const buttons = [FileDownloadBtnComponent, UserButtonComponent]; @@ -69,6 +70,7 @@ const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, Rea }, }, }, + ConfirmationDialogService, ], }) export class SharedModule {} diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index aeec13bf8..4b1931c00 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -436,6 +436,13 @@ "question": "Are you sure you want to delete {filesCount, plural, one{this document} other{these documents}}?", "title": "Delete {filesCount, plural, one{{fileName}} other{Selected Documents}}" }, + "unsaved-changes": { + "confirmation-text": "Save and Leave", + "deny-text": "DISCARD CHANGES", + "details": "If you leave the tab without saving, all the unsaved changes will be lost.", + "question": "Are you sure you want to leave the tab? You have unsaved changes.", + "title": "You have unsaved changes" + }, "upload-report-template": { "alternate-confirmation-text": "Upload as multi-file report", "confirmation-text": "Upload as single-file report",