From b44a817a838008750fc5880d0635ef43a8d775a0 Mon Sep 17 00:00:00 2001 From: Bertrand Zuchuat Date: Mon, 19 Jun 2023 14:56:23 +0200 Subject: [PATCH] editor: confirmation message when leaving 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 --- .../abstract-can-deactivate.component.ts | 45 +++++ projects/rero/ng-core/src/lib/core.module.ts | 4 + .../component-can-deactivate.guard.spec.ts | 67 +++++++ .../guard/component-can-deactivate.guard.ts | 80 ++++++++ .../record/editor/editor.component.spec.ts | 35 ++-- .../src/lib/record/editor/editor.component.ts | 179 +++++++++--------- projects/rero/ng-core/src/public-api.ts | 8 +- 7 files changed, 309 insertions(+), 109 deletions(-) create mode 100644 projects/rero/ng-core/src/lib/component/abstract-can-deactivate.component.ts create mode 100644 projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.spec.ts create mode 100644 projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.ts diff --git a/projects/rero/ng-core/src/lib/component/abstract-can-deactivate.component.ts b/projects/rero/ng-core/src/lib/component/abstract-can-deactivate.component.ts new file mode 100644 index 00000000..95fd6f04 --- /dev/null +++ b/projects/rero/ng-core/src/lib/component/abstract-can-deactivate.component.ts @@ -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 . + */ +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; + } +} diff --git a/projects/rero/ng-core/src/lib/core.module.ts b/projects/rero/ng-core/src/lib/core.module.ts index e29b7cfd..b1150677 100644 --- a/projects/rero/ng-core/src/lib/core.module.ts +++ b/projects/rero/ng-core/src/lib/core.module.ts @@ -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: [ @@ -106,6 +107,9 @@ import { AutofocusDirective } from './directives/autofocus.directive'; NgVarDirective, MarkdownPipe, AutofocusDirective + ], + providers: [ + ComponentCanDeactivateGuard ] }) export class CoreModule { } diff --git a/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.spec.ts b/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.spec.ts new file mode 100644 index 00000000..ed664963 --- /dev/null +++ b/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.spec.ts @@ -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 . + */ +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; + obs.subscribe((value: boolean) => expect(value).toBeFalse()); + }); +}); diff --git a/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.ts b/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.ts new file mode 100644 index 00000000..df590b22 --- /dev/null +++ b/projects/rero/ng-core/src/lib/guard/component-can-deactivate.guard.ts @@ -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 . + */ +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) + * + */ + +@Injectable() +export class ComponentCanDeactivateGuard implements CanDeactivate { + /** + * 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 or boolean + */ + canDeactivate(component: AbstractCanDeactivateComponent): Observable | 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; + } +} diff --git a/projects/rero/ng-core/src/lib/record/editor/editor.component.spec.ts b/projects/rero/ng-core/src/lib/record/editor/editor.component.spec.ts index 114e61b7..8b676a75 100644 --- a/projects/rero/ng-core/src/lib/record/editor/editor.component.spec.ts +++ b/projects/rero/ng-core/src/lib/record/editor/editor.component.spec.ts @@ -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 @@ -15,6 +15,7 @@ * along with this program. If not, see . */ 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'; @@ -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', @@ -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']); @@ -87,7 +86,7 @@ describe('EditorComponent', () => { TranslateService, { provide: RecordService, useValue: recordService }, { provide: RecordUiService, useValue: recordUiServiceSpy }, - { provide: ActivatedRoute, useValue: route } + { provide: ActivatedRoute, useValue: routeSpy } ] }) .compileComponents(); diff --git a/projects/rero/ng-core/src/lib/record/editor/editor.component.ts b/projects/rero/ng-core/src/lib/record/editor/editor.component.ts index 4de276be..e1f4552b 100644 --- a/projects/rero/ng-core/src/lib/record/editor/editor.component.ts +++ b/projects/rero/ng-core/src/lib/record/editor/editor.component.ts @@ -1,6 +1,6 @@ /* * RERO angular core - * Copyright (C) 2020-2022 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 @@ -24,11 +24,11 @@ import { TranslateService } from '@ngx-translate/core'; import { JSONSchema7 as JSONSchema7Base } from 'json-schema'; import { cloneDeep } from 'lodash-es'; import { BsModalService } from 'ngx-bootstrap/modal'; -import { NgxSpinnerService } from 'ngx-spinner'; import { ToastrService } from 'ngx-toastr'; -import { BehaviorSubject, combineLatest, Observable, of, Subscription, throwError } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription, combineLatest, of, throwError } from 'rxjs'; import { catchError, debounceTime, map, switchMap } from 'rxjs/operators'; import { ApiService } from '../../api/api.service'; +import { AbstractCanDeactivateComponent } from '../../component/abstract-can-deactivate.component'; import { Error } from '../../error/error'; import { RouteCollectionService } from '../../route/route-collection.service'; import { LoggerService } from '../../service/logger.service'; @@ -50,9 +50,9 @@ export interface JSONSchema7 extends JSONSchema7Base { // make the style global: required by JSONSchema cssClass encapsulation: ViewEncapsulation.None }) -export class EditorComponent implements OnInit, OnChanges, OnDestroy { +export class EditorComponent extends AbstractCanDeactivateComponent implements OnInit, OnChanges, OnDestroy { - // form intial values + // form initial values @Input() model: any = null; // initial values changes notification @@ -61,6 +61,12 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // editor loading state notifications @Output() loadingChange = new EventEmitter(); + // editor can deactivate change notification + @Output() canDeactivateChange = new EventEmitter(); + + // Can Deactivate + canDeactivate: boolean = false; + // angular formGroup root form: UntypedFormGroup; @@ -103,7 +109,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { error: Error; // subscribers - private _subscribers: Subscription[] = []; + private _subscribers: Subscription = new Subscription(); // Config for resource private _resourceConfig: any; @@ -150,33 +156,32 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { /** * Constructor. - * @param _formlyJsonschema Formly JSON schema. - * @param _recordService Record service. - * @param _apiService API service. - * @param _route Route. - * @param _recordUiService Record UI service. - * @param _translateService Translate service. - * @param _toastrService Toast service. - * @param _location Location. - * @param _spinner Spinner service. - * @param _modalService BsModalService. - * @param _routeCollectionService RouteCollectionService - * @param _loggerService LoggerService + * @param formlyJsonschema Formly JSON schema. + * @param recordService Record service. + * @param apiService API service. + * @param route Route. + * @param recordUiService Record UI service. + * @param translateService Translate service. + * @param toastrService Toast service. + * @param location Location. + * @param modalService BsModalService. + * @param routeCollectionService RouteCollectionService + * @param loggerService LoggerService */ constructor( - private _formlyJsonschema: FormlyJsonschema, - private _recordService: RecordService, - private _apiService: ApiService, - private _route: ActivatedRoute, - private _recordUiService: RecordUiService, - private _translateService: TranslateService, - private _toastrService: ToastrService, - private _location: Location, - private _spinner: NgxSpinnerService, - private _modalService: BsModalService, - private _routeCollectionService: RouteCollectionService, - private _loggerService: LoggerService + protected formlyJsonschema: FormlyJsonschema, + protected recordService: RecordService, + protected apiService: ApiService, + protected route: ActivatedRoute, + protected recordUiService: RecordUiService, + protected translateService: TranslateService, + protected toastrService: ToastrService, + protected location: Location, + protected modalService: BsModalService, + protected routeCollectionService: RouteCollectionService, + protected loggerService: LoggerService ) { + super(); this.form = new UntypedFormGroup({}); } @@ -227,24 +232,22 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { return of([]); }) ); - combineLatest([this._route.params, this._route.queryParams]).subscribe( + combineLatest([this.route.params, this.route.queryParams]).subscribe( ([params, queryParams]) => { // uncomment for debug // this.form.valueChanges.subscribe(v => // console.log('model', this.model, 'v', v, 'form', this.form) // ); - this._spinner.show(); this.loadingChange.emit(true); if (!params.type) { this.model = {}; this.schema = {}; - this._spinner.hide(); return; } this.recordType = params.type; - this._recordUiService.types = this._route.snapshot.data.types; + this.recordUiService.types = this.route.snapshot.data.types; - this._resourceConfig = this._recordUiService.getResourceConfig( + this._resourceConfig = this.recordUiService.getResourceConfig( this.recordType ); if (this._resourceConfig.editorSettings) { @@ -255,11 +258,11 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // If editor allowed to use a resource as a template, we need to load the configuration // of this resource and save it into `recordUIService.types` to use it when loading and saving if (this.editorSettings.template.recordType !== undefined) { - const tmplResource = this._routeCollectionService.getRoute(this.editorSettings.template.recordType); + const tmplResource = this.routeCollectionService.getRoute(this.editorSettings.template.recordType); const tmplConfiguration = tmplResource.getConfiguration(); if (tmplConfiguration.hasOwnProperty('data') && tmplConfiguration.data.hasOwnProperty('types')) { - this._recordUiService.types = this._recordUiService.types.concat(tmplConfiguration.data.types); + this.recordUiService.types = this.recordUiService.types.concat(tmplConfiguration.data.types); } } @@ -270,22 +273,22 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { this.saveAlternatives = []; if (this.editorSettings.template.saveAsTemplate) { this.saveAlternatives.push({ - label: this._translateService.instant('Save as template') + '…', + label: this.translateService.instant('Save as template') + '…', action: this._saveAsTemplate }); } this.pid = params.pid; - const schema$: Observable = this._recordService.getSchemaForm(this.recordType); + const schema$: Observable = this.recordService.getSchemaForm(this.recordType); let record$: Observable = of({ record: {}, result: null }); // load from template // If the url contains query arguments 'source' and 'pid' and 'source'=='templates' // then we need to use the data from this template as data source to fill the form. if (queryParams.source === 'templates' && queryParams.pid != null) { - record$ = this._recordService.getRecord('templates', queryParams.pid).pipe( + record$ = this.recordService.getRecord('templates', queryParams.pid).pipe( map((record: any) => { - this._toastrService.success( - this._translateService.instant('Template loaded') + this.toastrService.success( + this.translateService.instant('Template loaded') ); return { result: true, @@ -297,11 +300,11 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // If the parsed url contains a 'pid' value, then user try to edit a record. Then // we need to load this record and use it as data source to fill the form. } else if (this.pid) { - record$ = this._recordService + record$ = this.recordService .getRecord(this.recordType, this.pid) .pipe( switchMap((record: any) => { - return this._recordUiService.canUpdateRecord$(record, this.recordType).pipe( + return this.recordUiService.canUpdateRecord$(record, this.recordType).pipe( map((result) => { return { result, @@ -324,21 +327,19 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // Check permissions and set record if (data.result && data.result.can === false) { - this._toastrService.error( - this._translateService.instant('You cannot update this record'), - this._translateService.instant(this.recordType) + this.toastrService.error( + this.translateService.instant('You cannot update this record'), + this.translateService.instant(this.recordType) ); - this._location.back(); + this.location.back(); } else { this._setModel(data.record); } - this._spinner.hide(); this.loadingChange.emit(false); }, (error) => { this.error = error; - this._spinner.hide(); this.loadingChange.emit(false); } ); @@ -348,7 +349,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { /** onDestroy hook */ ngOnDestroy(): void { - this._subscribers.forEach(s => s.unsubscribe()); + this._subscribers.unsubscribe(); } /** @@ -388,7 +389,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * @param record - Record object to preprocess */ private preprocessRecord(record: any): any { - const config = this._recordUiService.getResourceConfig(this.recordType); + const config = this.recordUiService.getResourceConfig(this.recordType); if (config.preprocessRecordEditor) { return config.preprocessRecordEditor(record); @@ -401,7 +402,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * @param record - Record object to postprocess */ private postprocessRecord(record: any): any { - const config = this._recordUiService.getResourceConfig(this.recordType); + const config = this.recordUiService.getResourceConfig(this.recordType); if (config.postprocessRecordEditor) { return config.postprocessRecordEditor(record); @@ -414,7 +415,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * @param record - Record object */ private preCreateRecord(record: any): any { - const config = this._recordUiService.getResourceConfig(this.recordType); + const config = this.recordUiService.getResourceConfig(this.recordType); if (config.preCreateRecord) { return config.preCreateRecord(record); @@ -427,7 +428,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * @param record - Record object */ private preUpdateRecord(record: any): any { - const config = this._recordUiService.getResourceConfig(this.recordType); + const config = this.recordUiService.getResourceConfig(this.recordType); if (config.preUpdateRecord) { return config.preUpdateRecord(record); @@ -441,7 +442,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { */ setSchema(schema: any): void { // reorder all object properties - this.schema = orderedJsonSchema(formToWidget(schema, this._loggerService)); + this.schema = orderedJsonSchema(formToWidget(schema, this.loggerService)); this.options = {}; // remove hidden field list in case of a previous setSchema call @@ -449,7 +450,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // form configuration const fields = [ - this._formlyJsonschema.toFieldConfig(this.schema, { + this.formlyJsonschema.toFieldConfig(this.schema, { // post process JSONSChema7 to FormlyFieldConfig conversion map: (field: FormlyFieldConfig, jsonSchema: JSONSchema7) => { /**** additionnal JSONSchema configurations *******/ @@ -499,16 +500,16 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * Save the data on the server. */ submit(): void { + this._canDeactivate(); this.form.updateValueAndValidity(); if (this.form.valid === false) { - this._toastrService.error( - this._translateService.instant('The form contains errors.') + this.toastrService.error( + this.translateService.instant('The form contains errors.') ); return; } - this._spinner.show(); this.loadingChange.emit(true); let data = removeEmptyValues(this.model); @@ -526,34 +527,33 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { let recordAction$: Observable; if (this.pid != null) { - recordAction$ = this._recordService.update(this.recordType, this.pid, this.preUpdateRecord(data)).pipe( + recordAction$ = this.recordService.update(this.recordType, this.pid, this.preUpdateRecord(data)).pipe( catchError((error) => this._handleError(error)), map(record => { - return { record, action: 'update', message: this._translateService.instant('Record updated.') }; + return { record, action: 'update', message: this.translateService.instant('Record updated.') }; }) ); } else { - recordAction$ = this._recordService.create(this.recordType, this.preCreateRecord(data)).pipe( + recordAction$ = this.recordService.create(this.recordType, this.preCreateRecord(data)).pipe( catchError((error) => this._handleError(error)), map(record => { - return { record, action: 'create', message: this._translateService.instant('Record created.') }; + return { record, action: 'create', message: this.translateService.instant('Record created.') }; }) ); } recordAction$.subscribe(result => { - this._toastrService.success( - this._translateService.instant(result.message), - this._translateService.instant(this.recordType) + this.toastrService.success( + this.translateService.instant(result.message), + this.translateService.instant(this.recordType) ); - this._recordUiService.redirectAfterSave( + this.recordUiService.redirectAfterSave( result.record.id, result.record, this.recordType, result.action, - this._route + this.route ); - this._spinner.hide(); this.loadingChange.emit(true); }); } @@ -568,14 +568,13 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { // As we use `_saveAsTemplate` in a ngFor loop, the common `this` value equals to the current // loop value, not the current component. We need to pass this component as parameter of // the function to use it. - const saveAsTemplateModalRef = component._modalService.show(SaveTemplateFormComponent, { + const saveAsTemplateModalRef = component.modalService.show(SaveTemplateFormComponent, { ignoreBackdropClick: true, }); // if the modal is closed by clicking the 'save' button, the `saveEvent` is fired. // Subscribe to this event know when creating a model - component._subscribers.push(saveAsTemplateModalRef.content.saveEvent.subscribe( + component._subscribers.add(saveAsTemplateModalRef.content.saveEvent.subscribe( (data) => { - component._spinner.show(); let modelData = removeEmptyValues(component.model); modelData = component.postprocessRecord(modelData); @@ -584,28 +583,27 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { data: modelData, template_type: component.recordType, }; - const tmplConfig = component._recordUiService.getResourceConfig(component.editorSettings.template.recordType); + const tmplConfig = component.recordUiService.getResourceConfig(component.editorSettings.template.recordType); if (tmplConfig.preCreateRecord) { record = tmplConfig.preCreateRecord(record); } // create template - component._recordService.create(component.editorSettings.template.recordType, record).subscribe( + component.recordService.create(component.editorSettings.template.recordType, record).subscribe( (createdRecord) => { - component._toastrService.success( - component._translateService.instant('Record created.'), - component._translateService.instant(component.editorSettings.template.recordType) + component.toastrService.success( + component.translateService.instant('Record created.'), + component.translateService.instant(component.editorSettings.template.recordType) ); - component._recordUiService.redirectAfterSave( + component.recordUiService.redirectAfterSave( createdRecord.id, createdRecord, component.editorSettings.template.recordType, 'create', - component._route + component.route ); } ); - component._spinner.hide(); component.loadingChange.emit(true); } )); @@ -626,7 +624,8 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * Cancel editing and back to previous page */ cancel(): void { - this._location.back(); + this._canDeactivate(); + this.location.back(); } /** @@ -634,7 +633,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { */ showLoadTemplateDialog(): void { const templateResourceType = this.editorSettings.template.recordType; - this._modalService.show(LoadTemplateFormComponent, { + this.modalService.show(LoadTemplateFormComponent, { ignoreBackdropClick: true, initialState: { templateResourceType, @@ -773,7 +772,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { afterContentInit: (f: FormlyFieldConfig) => { const recordType = formOptions.remoteOptions.type; const query = formOptions.remoteOptions.query ? formOptions.remoteOptions.query : ''; - f.templateOptions.options = this._recordService + f.templateOptions.options = this.recordService .getRecords(recordType, query, 1, RecordService.MAX_REST_RESULTS_SIZE) .pipe( map((data: Record) => @@ -782,7 +781,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { label: formOptions.remoteOptions.labelField && formOptions.remoteOptions.labelField in record.metadata ? record.metadata[formOptions.remoteOptions.labelField] : record.metadata.name, - value: this._apiService.getRefEndpoint( + value: this.apiService.getRefEndpoint( recordType, record.id ) @@ -837,7 +836,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { field.validation.messages[key.replace(/Message$/, '')] = (error, f: FormlyFieldConfig) => // translate the validation messages coming from the JSONSchema // TODO: need to remove `as any` once it is fixed in ngx-formly v.5.7.2 - this._translateService.stream(msg) as any; + this.translateService.stream(msg) as any; } } @@ -864,7 +863,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { const validatorExpression = { expression: (fc: UntypedFormControl) => expressionFn(fc), // translate the validation message coming form the JSONSchema - message: this._translateService.stream(validator.message) + message: this.translateService.stream(validator.message) }; field.validators = field.validators !== undefined ? field.validators : {}; field.validators[validatorKey] = validatorExpression; @@ -896,8 +895,12 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy { * @return Observable */ private _handleError(error: { status: number, title: string }): Observable { - this._spinner.hide(); this.form.setErrors({ formError: error }); return throwError({ status: error.status, title: error.title }); } + + private _canDeactivate(activate: boolean = true) { + this.canDeactivate = activate; + this.canDeactivateChange.next(activate); + } } diff --git a/projects/rero/ng-core/src/public-api.ts b/projects/rero/ng-core/src/public-api.ts index c6bae7ac..584354b0 100644 --- a/projects/rero/ng-core/src/public-api.ts +++ b/projects/rero/ng-core/src/public-api.ts @@ -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 @@ -15,7 +15,9 @@ * along with this program. If not, see . */ +export * from './lib/ILogger'; export * from './lib/api/api.service'; +export * from './lib/component/abstract-can-deactivate.component'; export * from './lib/core-config.service'; export * from './lib/core.module'; export * from './lib/dialog/dialog.component'; @@ -24,7 +26,7 @@ export * from './lib/directives/autofocus.directive'; export * from './lib/directives/ng-var.directive'; export * from './lib/error/error'; export * from './lib/error/error.component'; -export * from './lib/ILogger'; +export * from './lib/guard/component-can-deactivate.guard'; export * from './lib/menu/menu-factory'; export * from './lib/menu/menu-item'; export * from './lib/menu/menu-item-interface'; @@ -44,6 +46,7 @@ export * from './lib/record/detail/detail.component'; export * from './lib/record/editor/editor.component'; export * from './lib/record/editor/extensions'; export * from './lib/record/editor/type/array-type/array-type.component'; +export * from './lib/record/editor/type/date-time-picker-type.component'; export * from './lib/record/editor/type/datepicker-type.component'; export * from './lib/record/editor/type/multischema/multischema.component'; export * from './lib/record/editor/type/object-type/object-type.component'; @@ -51,7 +54,6 @@ export * from './lib/record/editor/type/password-generator-type.component'; export * from './lib/record/editor/type/remote-typeahead/remote-typeahead.component'; export * from './lib/record/editor/type/remote-typeahead/remote-typeahead.service'; export * from './lib/record/editor/type/switch/switch.component'; -export * from './lib/record/editor/type/date-time-picker-type.component'; export * from './lib/record/editor/utils'; export * from './lib/record/editor/widgets/add-field-editor/add-field-editor.component'; export * from './lib/record/editor/widgets/dropdown-label-editor/dropdown-label-editor.component';