Skip to content

Commit

Permalink
editor: confirmation message when leaving
Browse files Browse the repository at this point in the history
A confirmation message is displayed if the user leaves
the form without saving it.

* Removes spinner on editor.
* Changes the visiblity of the property in the editor to allow
  abstracting the class.
* Closes rero/rero-ils#2104.

Co-Authored-by: Bertrand Zuchuat <bertrand.zuchuat@rero.ch>
  • Loading branch information
Garfield-fr committed Jun 28, 2023
1 parent 3d7d36e commit b44a817
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* RERO angular core
* Copyright (C) 2023 RERO
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Component, HostListener} from "@angular/core";

/**
* Doc: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*
* The beforeunload event is fired when the window, the document and its resources
* are about to be unloaded. The document is still visible and the event is still
* cancelable at this point.
*/
@Component({ template: '' })
export abstract class AbstractCanDeactivateComponent {

abstract canDeactivate: boolean;

@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
if (!this.canDeactivate) {
$event.returnValue = true;
}
}

/**
* Can deactivate changed on editor
* @param activate - boolean
*/
canDeactivateChanged(activate: boolean): void {
this.canDeactivate = activate;
}
}
4 changes: 4 additions & 0 deletions projects/rero/ng-core/src/lib/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { TranslateLoader } from './translate/translate-loader';
import { MenuComponent } from './widget/menu/menu.component';
import { SortListComponent } from './widget/sort-list/sort-list.component';
import { AutofocusDirective } from './directives/autofocus.directive';
import { ComponentCanDeactivateGuard } from './guard/component-can-deactivate.guard';

@NgModule({
declarations: [
Expand Down Expand Up @@ -106,6 +107,9 @@ import { AutofocusDirective } from './directives/autofocus.directive';
NgVarDirective,
MarkdownPipe,
AutofocusDirective
],
providers: [
ComponentCanDeactivateGuard
]
})
export class CoreModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* RERO angular core
* Copyright (C) 2023 RERO
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed } from '@angular/core/testing';

import { TranslateModule } from '@ngx-translate/core';
import { BsModalService, ModalModule } from 'ngx-bootstrap/modal';
import { Observable, of } from 'rxjs';
import { AbstractCanDeactivateComponent } from '../component/abstract-can-deactivate.component';
import { DialogService } from '../dialog/dialog.service';
import { ComponentCanDeactivateGuard } from './component-can-deactivate.guard';

export class MockComponent extends AbstractCanDeactivateComponent {
canDeactivate: boolean = true;
}

describe('ComponentCanDeactivateGuard', () => {
let guard: ComponentCanDeactivateGuard;
let component: MockComponent;

const dialogServiceSpy = jasmine.createSpyObj('DialogService', ['show']);
dialogServiceSpy.show.and.returnValue(of(false));

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
ModalModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
MockComponent,
ComponentCanDeactivateGuard,
{ provide: DialogService, useValue: dialogServiceSpy },
BsModalService
]
});
guard = TestBed.inject(ComponentCanDeactivateGuard);
component = TestBed.inject(MockComponent);
});

it('should be created', () => {
expect(guard).toBeTruthy();
});

it('should return a boolean if confirmation is not required.', () => {
expect(guard.canDeactivate(component)).toBeTrue();
});

it('should return an observable on a boolean value.', () => {
component.canDeactivate = false;
const obs = guard.canDeactivate(component) as Observable<boolean>;
obs.subscribe((value: boolean) => expect(value).toBeFalse());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* RERO angular core
* Copyright (C) 2023 RERO
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { AbstractCanDeactivateComponent } from '../component/abstract-can-deactivate.component';
import { DialogService } from '../dialog/dialog.service';

/**
* When this guard is configured, it intercepts the form output without
* saving or undoing changes.
*
* Route definition configuration
* { path: 'foo/bar', canDeactivate: [ ComponentCanDeactivateGuard ] },
*
* Custom editor component configuration
* class FooComponent AbstractCanDeactivateComponent {
* canDeactivate: boolean = false;
* ...
* }
*
* Template configuration (add output canDeactivateChange)
* <ng-core-editor
* (canDeactivateChange)="canDeactivateChanged($event)"
* ...
* ></ng-core-editor>
*/

@Injectable()
export class ComponentCanDeactivateGuard implements CanDeactivate<AbstractCanDeactivateComponent> {
/**
* Constructor
* @param _translateService - TranslateService
* @param _dialogService - DialogService
*/
constructor(
protected translateService: TranslateService,
protected dialogService: DialogService
) {}

/**
* Displays a confirmation modal if the user leaves the form without
* saving or canceling
* @param component - AbstractCanDeactivateComponent
* @returns Observable<boolean> or boolean
*/
canDeactivate(component: AbstractCanDeactivateComponent): Observable<boolean> | boolean {
if (!component.canDeactivate) {
return this.dialogService.show({
ignoreBackdropClick: false,
initialState: {
title: this.translateService.instant('Quit the page'),
body: this.translateService.instant(
'Do you want to quit the page? The changes made so far will be lost.'
),
confirmButton: true,
confirmTitleButton: this.translateService.instant('Quit'),
cancelTitleButton: this.translateService.instant('Stay')
}
});
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* RERO angular core
* Copyright (C) 2020 RERO
* Copyright (C) 2020-2023 RERO
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
Expand All @@ -15,6 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
Expand All @@ -23,7 +24,6 @@ import { RecordUiService } from '../record-ui.service';
import { RecordModule } from '../record.module';
import { RecordService } from '../record.service';
import { EditorComponent } from './editor.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

const recordUiServiceSpy = jasmine.createSpyObj('RecordUiService', [
'getResourceConfig',
Expand All @@ -45,21 +45,20 @@ recordUiServiceSpy.types = [
}
];

const route = {
params: of({ type: 'documents' }),
snapshot: {
params: { type: 'documents' },
data: {
types: [
{
key: 'documents',
}
],
showSearchInput: true,
adminMode: of({ message: '', can: true })
}
},
queryParams: of({})
const routeSpy = jasmine.createSpyObj('ActivatedRoute', ['']);
routeSpy.params = of({ type: 'documents' });
routeSpy.queryParams = of({});
routeSpy.snapshot = {
params: { type: 'documents' },
data: {
types: [
{
key: 'documents',
}
],
showSearchInput: true,
adminMode: of({ message: '', can: true })
}
};

const recordService = jasmine.createSpyObj('RecordService', ['getSchemaForm']);
Expand Down Expand Up @@ -87,7 +86,7 @@ describe('EditorComponent', () => {
TranslateService,
{ provide: RecordService, useValue: recordService },
{ provide: RecordUiService, useValue: recordUiServiceSpy },
{ provide: ActivatedRoute, useValue: route }
{ provide: ActivatedRoute, useValue: routeSpy }
]
})
.compileComponents();
Expand Down
Loading

0 comments on commit b44a817

Please sign in to comment.