Browse Source

Dialog to notify user about unsaved changes when editing content. Closes #34

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
60f8397750
  1. 3
      src/Squidex/app/features/content/module.ts
  2. 22
      src/Squidex/app/features/content/pages/content/content-page.component.html
  3. 40
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  4. 27
      src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts
  5. 21
      src/Squidex/app/framework/angular/can-deactivate.guard.ts
  6. 6
      src/Squidex/app/framework/angular/markdown-editor.component.ts
  7. 6
      src/Squidex/app/framework/angular/rich-editor.component.ts
  8. 18
      src/Squidex/app/framework/angular/stars.component.ts
  9. 3
      src/Squidex/app/framework/declarations.ts
  10. 2
      src/Squidex/app/framework/module.ts

3
src/Squidex/app/features/content/module.ts

@ -9,6 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { import {
CanDeactivateGuard,
HistoryComponent, HistoryComponent,
ResolveAppLanguagesGuard, ResolveAppLanguagesGuard,
ResolveContentGuard, ResolveContentGuard,
@ -43,6 +44,7 @@ const routes: Routes = [
{ {
path: 'new', path: 'new',
component: ContentPageComponent, component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard],
children: [ children: [
{ {
path: 'assets', path: 'assets',
@ -52,6 +54,7 @@ const routes: Routes = [
}, { }, {
path: ':contentId', path: ':contentId',
component: ContentPageComponent, component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard],
resolve: { resolve: {
content: ResolveContentGuard content: ResolveContentGuard
}, },

22
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -55,4 +55,24 @@
</sqx-panel> </sqx-panel>
</form> </form>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<div class="modal" *sqxModalView="cancelDialog" [@fade]>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Unsaved changes</h4>
</div>
<div class="modal-body">
You have unsaved changes, do you want to close the current content view?`
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="confirmLeave()">Yes</button>
<button type="button" class="btn btn-success" (click)="cancelLeave()">No</button>
</div>
</div>
</div>
</div>

40
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 { Location } from '@angular/common';
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Observable, Subject, Subscription } from 'rxjs';
import { import {
ContentCreated, ContentCreated,
@ -21,8 +21,11 @@ import {
AppComponentBase, AppComponentBase,
AppLanguageDto, AppLanguageDto,
AppsStoreService, AppsStoreService,
CanComponentDeactivate,
ContentDto, ContentDto,
ContentsService, ContentsService,
fadeAnimation,
ModalView,
MessageBus, MessageBus,
NotificationService, NotificationService,
NumberFieldPropertiesDto, NumberFieldPropertiesDto,
@ -35,14 +38,19 @@ import {
@Component({ @Component({
selector: 'sqx-content-page', selector: 'sqx-content-page',
styleUrls: ['./content-page.component.scss'], 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 contentDeletedSubscription: Subscription;
private version: Version = new Version(''); private version: Version = new Version('');
private cancelPromise: Subject<boolean> | null = null;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public cancelDialog = new ModalView();
public contentFormSubmitted = false; public contentFormSubmitted = false;
public contentForm: FormGroup; public contentForm: FormGroup;
public contentData: any = null; public contentData: any = null;
@ -91,6 +99,28 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy,
}); });
} }
public canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
if (!this.contentForm.dirty) {
return true;
} else {
this.cancelDialog.show();
return this.cancelPromise = new Subject<boolean>();
}
}
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() { public saveAndPublish() {
this.saveContent(true); this.saveContent(true);
} }
@ -149,6 +179,8 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy,
} }
private enable() { private enable() {
this.contentForm.markAsPristine();
for (const field of this.schema.fields.filter(f => !f.isDisabled)) { for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
const fieldForm = this.contentForm.controls[field.name]; const fieldForm = this.contentForm.controls[field.name];
@ -207,6 +239,8 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy,
} }
private populateForm(content: ContentDto) { private populateForm(content: ContentDto) {
this.contentForm.markAsPristine();
if (!content) { if (!content) {
this.contentData = undefined; this.contentData = undefined;
this.contentId = undefined; this.contentId = undefined;

27
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();
});
});

21
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<boolean> | Promise<boolean> | boolean;
}
@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
public canDeactivate(component: CanComponentDeactivate) {
return component.canDeactivate ? component.canDeactivate() : true;
}
}

6
src/Squidex/app/framework/angular/markdown-editor.component.ts

@ -81,7 +81,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.simplemde.codemirror.on('change', () => { this.simplemde.codemirror.on('change', () => {
const value = this.simplemde.value(); const value = this.simplemde.value();
this.changeCallback(value); if (this.value !== value) {
this.value = value;
this.changeCallback(value);
}
}); });
this.simplemde.codemirror.on('blur', () => { this.simplemde.codemirror.on('blur', () => {

6
src/Squidex/app/framework/angular/rich-editor.component.ts

@ -75,7 +75,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
self.tinyEditor.on('change', () => { self.tinyEditor.on('change', () => {
const value = editor.getContent(); const value = editor.getContent();
self.changeCallback(value); if (this.value !== value) {
this.value = value;
self.changeCallback(value);
}
}); });
self.tinyEditor.on('blur', () => { self.tinyEditor.on('blur', () => {

18
src/Squidex/app/framework/angular/stars.component.ts

@ -92,11 +92,13 @@ export class StarsComponent implements ControlValueAccessor {
return; return;
} }
this.value = null; if (this.value !== null) {
this.stars = 0; this.value = null;
this.stars = 0;
this.changeCallback(this.value); this.changeCallback(this.value);
this.touchedCallback(); this.touchedCallback();
}
return false; return false;
} }
@ -106,10 +108,12 @@ export class StarsComponent implements ControlValueAccessor {
return; return;
} }
this.value = this.stars = value; if (this.value !== value) {
this.value = this.stars = value;
this.changeCallback(this.value); this.changeCallback(this.value);
this.touchedCallback(); this.touchedCallback();
}
return false; return false;
} }

3
src/Squidex/app/framework/declarations.ts

@ -7,7 +7,7 @@
export * from './angular/animations'; export * from './angular/animations';
export * from './angular/autocomplete.component'; export * from './angular/autocomplete.component';
export * from './angular/validators'; export * from './angular/can-deactivate.guard';
export * from './angular/cloak.directive'; export * from './angular/cloak.directive';
export * from './angular/control-errors.component'; export * from './angular/control-errors.component';
export * from './angular/copy.directive'; export * from './angular/copy.directive';
@ -38,6 +38,7 @@ export * from './angular/tag-editor.component';
export * from './angular/title.component'; export * from './angular/title.component';
export * from './angular/toggle.component'; export * from './angular/toggle.component';
export * from './angular/user-report.component'; export * from './angular/user-report.component';
export * from './angular/validators';
export * from './configurations'; export * from './configurations';
export * from './services/clipboard.service'; export * from './services/clipboard.service';

2
src/Squidex/app/framework/module.ts

@ -13,6 +13,7 @@ import { RouterModule } from '@angular/router';
import { import {
AutocompleteComponent, AutocompleteComponent,
CanDeactivateGuard,
ClipboardService, ClipboardService,
CloakDirective, CloakDirective,
ControlErrorsComponent, ControlErrorsComponent,
@ -153,6 +154,7 @@ export class SqxFrameworkModule {
return { return {
ngModule: SqxFrameworkModule, ngModule: SqxFrameworkModule,
providers: [ providers: [
CanDeactivateGuard,
ClipboardService, ClipboardService,
LocalStoreService, LocalStoreService,
MessageBus, MessageBus,

Loading…
Cancel
Save