WIP on warning the user before leaving the page with unsaved changes

This commit is contained in:
Valentin 2021-12-15 10:37:48 +02:00
parent 4c076ffdaa
commit 54c4995f61
11 changed files with 175 additions and 23 deletions

View File

@ -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<AddEditDictionaryDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
@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)));
}

View File

@ -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<AddEditDossierTemplateDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
@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

View File

@ -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<AddEditFileAttributeDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
@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 {

View File

@ -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<EditColorDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: IEditColorData,
) {
super();
super(_injector, _dialogRef);
this._dossierTemplateId = data.dossierTemplateId;
this._initialColor = data.colors[data.colorKey];

View File

@ -65,5 +65,5 @@
</div>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (click)="close()"></iqser-circle-button>
</section>

View File

@ -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<EditDossierDialogComponent>,
private readonly _loadingService: LoadingService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<EditDossierDialogComponent>,
@Inject(MAT_DIALOG_DATA)
private readonly _data: {
dossierId: string;
section?: Section;
},
) {
super();
super(_injector, _dialogRef);
this.navItems = [
{
key: 'dossierInfo',

View File

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

View File

@ -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<DialogType> {
protected readonly _config: DialogConfig<DialogType> = {
confirm: {
component: ConfirmationDialogComponent,
dialogConfig: { disableClose: false },
},
};
constructor(protected readonly _dialog: MatDialog) {
super(_dialog);
}
openDialog(cb: any): MatDialogRef<unknown> {
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,
);
}
}

View File

@ -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<FormDialogComponent>,
// ) {
// 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;
// }
// }

View File

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

View File

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