updated PendingChangesGuard and used it for user profile and notifications preferences screens

This commit is contained in:
Valentin 2022-01-23 16:00:30 +02:00
parent d30c270d72
commit 3c697c643c
9 changed files with 55 additions and 60 deletions

View File

@ -1,40 +1,32 @@
import { CanDeactivate } from '@angular/router'; import { CanDeactivate } from '@angular/router';
import { Directive, HostListener, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { ConfirmationDialogService } from '../../../../../libs/common-ui/src/lib/dialog/confirmation-dialog.service';
import { FormGroup } from '@angular/forms';
import { ConfirmOptions } from '../../../../../libs/common-ui/src';
export interface ComponentCanDeactivate { export interface ComponentCanDeactivate {
hasChanges: boolean; changed: boolean;
} form: FormGroup;
save: () => Promise<void>;
@Directive()
export abstract class ComponentHasChanges implements ComponentCanDeactivate {
abstract hasChanges: boolean;
protected constructor(protected _translateService: TranslateService) {}
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
if (this.hasChanges) {
// This message will be displayed in IE/Edge
$event.returnValue = this._translateService.instant('pending-changes-guard');
}
}
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> { export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
constructor(private readonly _translateService: TranslateService) {} constructor(private _dialogService: ConfirmationDialogService) {}
canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> { canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
// if there are no pending changes, just allow deactivation; else confirm first if (component.changed) {
return !component.hasChanges const dialogRef = this._dialogService.openDialog({ disableConfirm: component.form.invalid });
? true return dialogRef.afterClosed().pipe(
: // NOTE: this warning message will only be shown when navigating elsewhere map(result => {
// within your angular app; if (result === ConfirmOptions.CONFIRM) {
// when navigating away from your angular app, component.save();
// the browser will show a generic warning message }
// see http://stackoverflow.com/a/42207299/7307355 return !!result;
confirm(this._translateService.instant('pending-changes-guard')); }),
);
}
return true;
} }
} }

View File

@ -1,4 +1,4 @@
<form (submit)="save()" [formGroup]="formGroup"> <form (submit)="save()" [formGroup]="form">
<div class="dialog-content"> <div class="dialog-content">
<div *ngFor="let category of notificationCategories"> <div *ngFor="let category of notificationCategories">
<div class="iqser-input-group header w-full"> <div class="iqser-input-group header w-full">
@ -36,7 +36,7 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="formGroup.invalid" color="primary" mat-flat-button type="submit"> <button [disabled]="form.invalid" color="primary" mat-flat-button type="submit">
{{ 'user-profile-screen.actions.save' | translate }} {{ 'user-profile-screen.actions.save' | translate }}
</button> </button>
</div> </div>

View File

@ -10,6 +10,7 @@ import {
NotificationGroupsKeys, NotificationGroupsKeys,
NotificationGroupsValues, NotificationGroupsValues,
} from '@red/domain'; } from '@red/domain';
import { BaseFormComponent } from '../../../../../../../../../libs/common-ui/src/lib/form/base-form.component';
@Component({ @Component({
selector: 'redaction-notifications-screen', selector: 'redaction-notifications-screen',
@ -17,57 +18,57 @@ import {
styleUrls: ['./notifications-screen.component.scss'], styleUrls: ['./notifications-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NotificationsScreenComponent implements OnInit { export class NotificationsScreenComponent extends BaseFormComponent implements OnInit {
readonly emailNotificationScheduleTypes = EmailNotificationScheduleTypesValues; readonly emailNotificationScheduleTypes = EmailNotificationScheduleTypesValues;
readonly notificationCategories = NotificationCategoriesValues; readonly notificationCategories = NotificationCategoriesValues;
readonly notificationGroupsKeys = NotificationGroupsKeys; readonly notificationGroupsKeys = NotificationGroupsKeys;
readonly notificationGroupsValues = NotificationGroupsValues; readonly notificationGroupsValues = NotificationGroupsValues;
readonly translations = notificationsTranslations; readonly translations = notificationsTranslations;
readonly formGroup: FormGroup = this._getForm();
constructor( constructor(
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder, private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _notificationPreferencesService: NotificationPreferencesService, private readonly _notificationPreferencesService: NotificationPreferencesService,
) {} ) {
super();
}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
await this._initializeForm(); await this._initializeForm();
} }
isCategoryActive(category: string) { isCategoryActive(category: string) {
return this.formGroup.get(`${category}Enabled`).value; return this.form.get(`${category}Enabled`).value;
} }
setEmailNotificationType(type: string) { setEmailNotificationType(type: string) {
this.formGroup.get('emailNotificationType').setValue(type); this.form.get('emailNotificationType').setValue(type);
} }
getEmailNotificationType() { getEmailNotificationType() {
return this.formGroup.get('emailNotificationType').value; return this.form.get('emailNotificationType').value;
} }
isPreferenceChecked(category: string, preference: string) { isPreferenceChecked(category: string, preference: string) {
return this.formGroup.get(category).value.includes(preference); return this.form.get(category).value.includes(preference);
} }
addRemovePreference(checked: boolean, category: string, preference: string) { addRemovePreference(checked: boolean, category: string, preference: string) {
const preferences = this.formGroup.get(category).value; const preferences = this.form.get(category).value;
if (checked) { if (checked) {
preferences.push(preference); preferences.push(preference);
} else { } else {
const indexOfPreference = preferences.indexOf(preference); const indexOfPreference = preferences.indexOf(preference);
preferences.splice(indexOfPreference, 1); preferences.splice(indexOfPreference, 1);
} }
this.formGroup.get(category).setValue(preferences); this.form.get(category).setValue(preferences);
} }
async save() { async save() {
this._loadingService.start(); this._loadingService.start();
try { try {
await this._notificationPreferencesService.update(this.formGroup.value).toPromise(); await this._notificationPreferencesService.update(this.form.value).toPromise();
} catch (e) { } catch (e) {
this._toaster.error(_('notifications-screen.error.generic')); this._toaster.error(_('notifications-screen.error.generic'));
} }
@ -87,8 +88,10 @@ export class NotificationsScreenComponent implements OnInit {
private async _initializeForm() { private async _initializeForm() {
this._loadingService.start(); this._loadingService.start();
this.form = this._getForm();
const notificationPreferences = await this._notificationPreferencesService.get().toPromise(); const notificationPreferences = await this._notificationPreferencesService.get().toPromise();
this.formGroup.patchValue(notificationPreferences); this.form.patchValue(notificationPreferences);
this.initialFormValue = JSON.parse(JSON.stringify(this.form.getRawValue()));
this._loadingService.stop(); this._loadingService.stop();
} }

View File

@ -3,8 +3,9 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component'; import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
const routes = [{ path: '', component: NotificationsScreenComponent }]; const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({ @NgModule({
declarations: [NotificationsScreenComponent], declarations: [NotificationsScreenComponent],

View File

@ -9,6 +9,8 @@ import { PermissionsService } from '../../../../../services/permissions.service'
import { UserService } from '../../../../../services/user.service'; import { UserService } from '../../../../../services/user.service';
import { ConfigService } from '../../../../../services/config.service'; import { ConfigService } from '../../../../../services/config.service';
import { LanguageService } from '../../../../../i18n/language.service'; import { LanguageService } from '../../../../../i18n/language.service';
import { ConfirmationDialogService } from '../../../../../../../../../libs/common-ui/src/lib/dialog/confirmation-dialog.service';
import { BaseFormComponent } from '../../../../../../../../../libs/common-ui/src/lib/form/base-form.component';
@Component({ @Component({
selector: 'redaction-user-profile-screen', selector: 'redaction-user-profile-screen',
@ -16,8 +18,7 @@ import { LanguageService } from '../../../../../i18n/language.service';
styleUrls: ['./user-profile-screen.component.scss'], styleUrls: ['./user-profile-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class UserProfileScreenComponent implements OnInit { export class UserProfileScreenComponent extends BaseFormComponent implements OnInit {
readonly form: FormGroup = this._getForm();
changePasswordUrl: SafeResourceUrl; changePasswordUrl: SafeResourceUrl;
translations = languagesTranslations; translations = languagesTranslations;
@ -30,11 +31,12 @@ export class UserProfileScreenComponent implements OnInit {
private readonly _configService: ConfigService, private readonly _configService: ConfigService,
private readonly _languageService: LanguageService, private readonly _languageService: LanguageService,
private readonly _domSanitizer: DomSanitizer, private readonly _domSanitizer: DomSanitizer,
private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _dialogService: ConfirmationDialogService,
protected readonly _translateService: TranslateService,
) { ) {
super();
this._loadingService.start(); this._loadingService.start();
this.changePasswordUrl = this._domSanitizer.bypassSecurityTrustResourceUrl( this.changePasswordUrl = this._domSanitizer.bypassSecurityTrustResourceUrl(
`${this._configService.values.OAUTH_URL}/account/password`, `${this._configService.values.OAUTH_URL}/account/password`,
); );
@ -100,6 +102,7 @@ export class UserProfileScreenComponent implements OnInit {
private _initializeForm(): void { private _initializeForm(): void {
try { try {
this.form = this._getForm();
this._profileModel = { this._profileModel = {
email: this._userService.currentUser.email, email: this._userService.currentUser.email,
firstName: this._userService.currentUser.firstName, firstName: this._userService.currentUser.firstName,
@ -111,6 +114,7 @@ export class UserProfileScreenComponent implements OnInit {
this.form.get('email').disable(); this.form.get('email').disable();
} }
this.form.patchValue(this._profileModel, { emitEvent: false }); this.form.patchValue(this._profileModel, { emitEvent: false });
this.initialFormValue = this.form.getRawValue();
} catch (e) { } catch (e) {
} finally { } finally {
this._loadingService.stop(); this._loadingService.stop();

View File

@ -3,8 +3,9 @@ import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component'; import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
const routes = [{ path: '', component: UserProfileScreenComponent }]; const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({ @NgModule({
declarations: [UserProfileScreenComponent], declarations: [UserProfileScreenComponent],

View File

@ -3,7 +3,6 @@ import { AppStateService } from '@state/app-state.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component'; import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionaryService } from '@shared/services/dictionary.service'; import { DictionaryService } from '@shared/services/dictionary.service';
@ -16,7 +15,7 @@ import { Dictionary } from '@red/domain';
templateUrl: './dictionary-overview-screen.component.html', templateUrl: './dictionary-overview-screen.component.html',
styleUrls: ['./dictionary-overview-screen.component.scss'], styleUrls: ['./dictionary-overview-screen.component.scss'],
}) })
export class DictionaryOverviewScreenComponent extends ComponentHasChanges implements OnInit, OnDestroy { export class DictionaryOverviewScreenComponent implements OnInit, OnDestroy {
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser; readonly currentUser = this._userService.currentUser;
@ -37,9 +36,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
private readonly _dialogService: AdminDialogService, private readonly _dialogService: AdminDialogService,
protected readonly _translateService: TranslateService, protected readonly _translateService: TranslateService,
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
) { ) {}
super(_translateService);
}
get hasChanges() { get hasChanges() {
return this._dictionaryManager.editor.hasChanges; return this._dictionaryManager.editor.hasChanges;

View File

@ -3,7 +3,6 @@ import { PermissionsService } from '@services/permissions.service';
import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { RulesService } from '../../services/rules.service'; import { RulesService } from '../../services/rules.service';
@ -17,7 +16,7 @@ import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorCon
styleUrls: ['./rules-screen.component.scss'], styleUrls: ['./rules-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RulesScreenComponent extends ComponentHasChanges implements OnInit { export class RulesScreenComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly editorOptions: IStandaloneEditorConstructionOptions = { readonly editorOptions: IStandaloneEditorConstructionOptions = {
theme: 'vs', theme: 'vs',
@ -43,9 +42,7 @@ export class RulesScreenComponent extends ComponentHasChanges implements OnInit
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
protected readonly _translateService: TranslateService, protected readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
) { ) {}
super(_translateService);
}
get hasChanges(): boolean { get hasChanges(): boolean {
return this.currentLines.toString() !== this.initialLines.toString(); return this.currentLines.toString() !== this.initialLines.toString();

@ -1 +1 @@
Subproject commit 2e36f3cac77411c4cef0ec5b51165aa62a9d075c Subproject commit 867d7b089ee3d10abf42bf6957c02f7f48ffdb7f