From 60f83977508684aedb6bd1b7dddb4772db2a05fc Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 3 May 2017 18:58:43 +0200 Subject: [PATCH] Dialog to notify user about unsaved changes when editing content. Closes #34 --- src/Squidex/app/features/content/module.ts | 3 ++ .../pages/content/content-page.component.html | 22 +++++++++- .../pages/content/content-page.component.ts | 40 +++++++++++++++++-- .../angular/can-deactivate.guard.spec.ts | 27 +++++++++++++ .../framework/angular/can-deactivate.guard.ts | 21 ++++++++++ .../angular/markdown-editor.component.ts | 6 ++- .../angular/rich-editor.component.ts | 6 ++- .../app/framework/angular/stars.component.ts | 18 +++++---- src/Squidex/app/framework/declarations.ts | 3 +- src/Squidex/app/framework/module.ts | 2 + 10 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts create mode 100644 src/Squidex/app/framework/angular/can-deactivate.guard.ts diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index d584fbc0d..778298514 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -9,6 +9,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { + CanDeactivateGuard, HistoryComponent, ResolveAppLanguagesGuard, ResolveContentGuard, @@ -43,6 +44,7 @@ const routes: Routes = [ { path: 'new', component: ContentPageComponent, + canDeactivate: [CanDeactivateGuard], children: [ { path: 'assets', @@ -52,6 +54,7 @@ const routes: Routes = [ }, { path: ':contentId', component: ContentPageComponent, + canDeactivate: [CanDeactivateGuard], resolve: { content: ResolveContentGuard }, diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 2332c8bad..5b149bfe7 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -55,4 +55,24 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 7cd540ed0..a555754b7 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -9,7 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Location } from '@angular/common'; import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { Observable, Subject, Subscription } from 'rxjs'; import { ContentCreated, @@ -21,8 +21,11 @@ import { AppComponentBase, AppLanguageDto, AppsStoreService, + CanComponentDeactivate, ContentDto, ContentsService, + fadeAnimation, + ModalView, MessageBus, NotificationService, NumberFieldPropertiesDto, @@ -35,14 +38,19 @@ import { @Component({ selector: 'sqx-content-page', styleUrls: ['./content-page.component.scss'], - templateUrl: './content-page.component.html' + templateUrl: './content-page.component.html', + animations: [ + fadeAnimation + ] }) -export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { private contentDeletedSubscription: Subscription; private version: Version = new Version(''); + private cancelPromise: Subject | null = null; public schema: SchemaDetailsDto; + public cancelDialog = new ModalView(); public contentFormSubmitted = false; public contentForm: FormGroup; public contentData: any = null; @@ -91,6 +99,28 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, }); } + public canDeactivate(): Observable | Promise | boolean { + if (!this.contentForm.dirty) { + return true; + } else { + this.cancelDialog.show(); + + return this.cancelPromise = new Subject(); + } + } + + public confirmLeave() { + this.cancelDialog.hide(); + this.cancelPromise.next(true); + this.cancelPromise = null; + } + + public cancelLeave() { + this.cancelDialog.hide(); + this.cancelPromise.next(false); + this.cancelPromise = null; + } + public saveAndPublish() { this.saveContent(true); } @@ -149,6 +179,8 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } private enable() { + this.contentForm.markAsPristine(); + for (const field of this.schema.fields.filter(f => !f.isDisabled)) { const fieldForm = this.contentForm.controls[field.name]; @@ -207,6 +239,8 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } private populateForm(content: ContentDto) { + this.contentForm.markAsPristine(); + if (!content) { this.contentData = undefined; this.contentId = undefined; diff --git a/src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts b/src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts new file mode 100644 index 000000000..431cb5ee6 --- /dev/null +++ b/src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts @@ -0,0 +1,27 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { CanDeactivateGuard } from './can-deactivate.guard'; + +describe('CanDeactivateGuard', () => { + it('should call component', () => { + let called = false; + + const component = { + canDeactivate: () => { + called = true; + + return true; + } + }; + + const result = new CanDeactivateGuard().canDeactivate(component); + + expect(result).toBeTruthy(); + expect(called).toBeTruthy(); + }); +}); diff --git a/src/Squidex/app/framework/angular/can-deactivate.guard.ts b/src/Squidex/app/framework/angular/can-deactivate.guard.ts new file mode 100644 index 000000000..45d153a9a --- /dev/null +++ b/src/Squidex/app/framework/angular/can-deactivate.guard.ts @@ -0,0 +1,21 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { CanDeactivate } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; + +export interface CanComponentDeactivate { + canDeactivate: () => Observable | Promise | boolean; +} + +@Injectable() +export class CanDeactivateGuard implements CanDeactivate { + public canDeactivate(component: CanComponentDeactivate) { + return component.canDeactivate ? component.canDeactivate() : true; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/markdown-editor.component.ts b/src/Squidex/app/framework/angular/markdown-editor.component.ts index e194e015c..b06f9494b 100644 --- a/src/Squidex/app/framework/angular/markdown-editor.component.ts +++ b/src/Squidex/app/framework/angular/markdown-editor.component.ts @@ -81,7 +81,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI this.simplemde.codemirror.on('change', () => { const value = this.simplemde.value(); - this.changeCallback(value); + if (this.value !== value) { + this.value = value; + + this.changeCallback(value); + } }); this.simplemde.codemirror.on('blur', () => { diff --git a/src/Squidex/app/framework/angular/rich-editor.component.ts b/src/Squidex/app/framework/angular/rich-editor.component.ts index 4711b9b64..d43668144 100644 --- a/src/Squidex/app/framework/angular/rich-editor.component.ts +++ b/src/Squidex/app/framework/angular/rich-editor.component.ts @@ -75,7 +75,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, self.tinyEditor.on('change', () => { const value = editor.getContent(); - self.changeCallback(value); + if (this.value !== value) { + this.value = value; + + self.changeCallback(value); + } }); self.tinyEditor.on('blur', () => { diff --git a/src/Squidex/app/framework/angular/stars.component.ts b/src/Squidex/app/framework/angular/stars.component.ts index f53cf3fe4..545ac2494 100644 --- a/src/Squidex/app/framework/angular/stars.component.ts +++ b/src/Squidex/app/framework/angular/stars.component.ts @@ -92,11 +92,13 @@ export class StarsComponent implements ControlValueAccessor { return; } - this.value = null; - this.stars = 0; + if (this.value !== null) { + this.value = null; + this.stars = 0; - this.changeCallback(this.value); - this.touchedCallback(); + this.changeCallback(this.value); + this.touchedCallback(); + } return false; } @@ -106,10 +108,12 @@ export class StarsComponent implements ControlValueAccessor { return; } - this.value = this.stars = value; + if (this.value !== value) { + this.value = this.stars = value; - this.changeCallback(this.value); - this.touchedCallback(); + this.changeCallback(this.value); + this.touchedCallback(); + } return false; } diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 3dd5c2837..d2893cea4 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -7,7 +7,7 @@ export * from './angular/animations'; export * from './angular/autocomplete.component'; -export * from './angular/validators'; +export * from './angular/can-deactivate.guard'; export * from './angular/cloak.directive'; export * from './angular/control-errors.component'; export * from './angular/copy.directive'; @@ -38,6 +38,7 @@ export * from './angular/tag-editor.component'; export * from './angular/title.component'; export * from './angular/toggle.component'; export * from './angular/user-report.component'; +export * from './angular/validators'; export * from './configurations'; export * from './services/clipboard.service'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 26c737449..e3ed7e04a 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -13,6 +13,7 @@ import { RouterModule } from '@angular/router'; import { AutocompleteComponent, + CanDeactivateGuard, ClipboardService, CloakDirective, ControlErrorsComponent, @@ -153,6 +154,7 @@ export class SqxFrameworkModule { return { ngModule: SqxFrameworkModule, providers: [ + CanDeactivateGuard, ClipboardService, LocalStoreService, MessageBus,