blob: f348bfba0d09510cf300868d7be68b36b9cd256a [file] [log] [blame]
/*
* Copyright (c) 2014-2018 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {EventSupport, MessageBox, MessageBoxes, objects, scout, Status} from '../index';
import * as $ from 'jquery';
/**
* Abstract base class for validation lifecycles as used for forms.
* A subclass must set the properties, in order to display messages:
* - emptyMandatoryElementsTextKey
* - invalidElementsTextKey
* - saveChangesQuestionTextKey
*
* @class
* @constructor
* @abstract
*/
export default class Lifecycle {
constructor() {
this.widget = null;
this.emptyMandatoryElementsTextKey = null;
this.emptyMandatoryElementsText = null;
this.invalidElementsTextKey = null;
this.invalidElementsText = null;
this.saveChangesQuestionTextKey = null;
this.askIfNeedSave = true;
this.askIfNeedSaveText = null; // Java: cancelVerificationText
this.events = new EventSupport();
this.handlers = {
'load': this._defaultLoad.bind(this),
'save': this._defaultSave.bind(this)
};
}
// Info: doExportXml, doImportXml, doSaveWithoutMarkerChange is not supported in Html UI
init(model) {
scout.assertParameter('widget', model.widget);
$.extend(this, model);
if (objects.isNullOrUndefined(this.emptyMandatoryElementsText)) {
this.emptyMandatoryElementsText = this.session().text(this.emptyMandatoryElementsTextKey);
}
if (objects.isNullOrUndefined(this.invalidElementsText)) {
this.invalidElementsText = this.session().text(this.invalidElementsTextKey);
}
if (objects.isNullOrUndefined(this.askIfNeedSaveText)) {
this.askIfNeedSaveText = this.session().text(this.saveChangesQuestionTextKey);
}
}
load() {
return this._load().then(function() {
this.markAsSaved();
this.events.trigger('postLoad');
}.bind(this));
}
/**
* @returns {Promise}
*/
_load() {
return this.handlers.load()
.then(function(status) {
this.events.trigger('load');
}.bind(this));
}
/**
* @returns {Promise}
*/
_defaultLoad() {
return $.resolvedPromise();
}
/**
* @returns {Promise}
*/
ok() {
// 1.) validate form
return this._whenInvalid(this._validate)
.then(function(invalid) {
if (invalid) {
return;
}
// 2.) check if save is required
if (!this.requiresSave()) {
return this.close();
}
// 3.) perform save operation
return this._whenInvalid(this._save)
.then(function(invalid) {
if (invalid) {
return;
}
this.markAsSaved();
return this.close();
}.bind(this));
}.bind(this));
}
/**
* @returns {Promise}
*/
cancel() {
var showMessageBox = this.requiresSave() && this.askIfNeedSave;
if (showMessageBox) {
return this._showYesNoCancelMessageBox(
this.askIfNeedSaveText,
this.ok.bind(this),
this.close.bind(this));
}
return this.close();
}
/**
* @returns {Promise}
*/
reset() {
this._reset();
// reload the state
return this.load().then(function() {
this.events.trigger('reset');
}.bind(this));
}
/**
* @returns {Promise}
*/
close() {
return this._close();
}
/**
* @returns {Promise}
*/
_close() {
this.events.trigger('close');
return $.resolvedPromise();
}
/**
* @returns {Promise}
*/
save() {
// 1.) validate form
return this._whenInvalid(this._validate)
.then(function(invalid) {
// 2.) invalid or form has not been changed
if (invalid || !this.requiresSave()) {
return;
}
// 3.) perform save operation
return this._whenInvalid(this._save)
.then(function(invalid) {
if (invalid) {
return;
}
this.markAsSaved();
}.bind(this));
}.bind(this));
}
/**
* @returns {Promise}
*/
_save() {
return this.handlers.save()
.then(function(status) {
this.events.trigger('save');
return status;
}.bind(this));
}
/**
* @returns {Promise}
*/
_defaultSave() {
return $.resolvedPromise();
}
markAsSaved() {
// NOP
}
/**
* Override this function to check if any data has changed and saving is required.
* @returns {boolean}
*/
requiresSave() {
return false;
}
setAskIfNeedSave(askIfNeedSave) {
this.askIfNeedSave = askIfNeedSave;
}
/**
* Helper function to deal with functions that return a Status object.
* Makes it easier to return early when that function returns an invalid status (= less code to write).
*
* @returns {Promise} If the resulting promise is resolved with "true", the life cycle is considered invalid.
* Otherwise, the life cycle is considered valid and the store/save operation continues.
* If the status returned by 'func' is absent or Status.Severity.OK, a promise resolved with
* "false" is returned. Otherwise, the promise returned by _showStatusMessageBox() is returned.
*/
_whenInvalid(func) {
return func.call(this)
.then(function(status) {
if (!status || status.severity === Status.Severity.OK) {
return $.resolvedPromise(false); // invalid=false
}
return this._showStatusMessageBox(status);
}.bind(this))
.catch(function(error) {
return this._showStatusMessageBox(errorToStatus(error));
}.bind(this));
// See ValueField#_createInvalidValueStatus, has similar code to transform error to status
function errorToStatus(error) {
if (error instanceof Status) {
return error;
}
if (typeof error === 'string') {
return Status.error({
message: error
});
}
return Status.error({
message: error.message
});
}
}
_showYesNoCancelMessageBox(message, yesAction, noAction) {
return MessageBoxes.create(this.widget)
.withSeverity(Status.Severity.WARNING)
.withHeader(message)
.withYes()
.withNo()
.withCancel()
.buildAndOpen()
.then(function(option) {
if (option === MessageBox.Buttons.YES) {
return yesAction();
} else if (option === MessageBox.Buttons.NO) {
return noAction();
}
return $.resolvedPromise();
});
}
/**
* @param status
* @returns {Promise} If the resulting promise is resolved with "true", the life cycle is considered invalid.
* Otherwise, the life cycle is considered valid and the store/save operation continues.
* By default, a promise that is resolved with "true" is returned.
*/
_showStatusMessageBox(status) {
return MessageBoxes.createOk(this.widget)
.withSeverity(status.severity)
.withBody(status.message, true)
.buildAndOpen()
.then(function() {
var invalid = (status.severity === Status.Severity.ERROR);
return $.resolvedPromise(invalid);
});
}
/**
* @returns {Promise}
*/
_validate() {
var status = this._validateElements();
if (status.isValid()) {
status = this._validateWidget();
}
return $.resolvedPromise(status);
}
/**
* Validates all elements (i.e form-fields) covered by the lifecycle and checks for missing or invalid elements.
*
* @return Status
*/
_validateElements() {
var elements = this._invalidElements();
var status = new Status();
if (elements.missingElements.length === 0 && elements.invalidElements.length === 0) {
status.severity = Status.Severity.OK;
} else {
status.severity = Status.Severity.ERROR;
status.message = this._createInvalidElementsMessageHtml(elements.missingElements, elements.invalidElements);
}
return status;
}
/**
* Validates the widget (i.e. form) associated with this lifecycle. This function is only called when there are
* no missing or invalid elements. It is used to implement an overall-validate logic which has nothing to do
* with a specific element or field. For instance you could validate if an internal member variable of a Lifecycle
* or Form is set.
*
* @return Status
*/
_validateWidget() {
return Status.ok();
}
/**
* Override this function to check for invalid elements on the parent which prevent
* saving of the parent.(eg. check if all mandatory elements contain a value)
*
* @returns Object with
* valid: (true or false)
* missingElements: Elements which should have a value
* invalidElements: Elements which have an invalid value
*/
_invalidElements() {
return {
missingElements: [],
invalidElements: []
};
}
/**
* Creates a HTML message used to display missing and invalid fields in a message box.
*/
_createInvalidElementsMessageHtml(missing, invalid) {
var $div = $('<div>'),
hasMissing = missing.length > 0,
hasInvalid = invalid.length > 0;
if (hasMissing) {
appendTitleAndList.call(this, $div, this.emptyMandatoryElementsText, missing, this._missingElementText);
}
if (hasMissing && hasInvalid) {
$div.appendElement('<br>');
}
if (hasInvalid) {
appendTitleAndList.call(this, $div, this.invalidElementsText, invalid, this._invalidElementText);
}
return $div.html();
// ----- Helper function -----
function appendTitleAndList($div, title, elements, elementTextFunc) {
$div.appendElement('<strong>').text(title);
var $ul = $div.appendElement('<ul>');
elements.forEach(function(element) {
$ul.appendElement('<li>').text(elementTextFunc.call(this, element));
}, this);
}
}
/**
* Override this function to retrieve the text of an invalid element
* @param element
* @returns {String}
*/
_invalidElementText(element) {
return '';
}
/**
* Override this function to retrieve the text of an missing mandatory element
* @param element
* @returns {String}
*/
_missingElementText(element) {
return '';
}
session() {
return this.widget.session;
}
/**
* Register a handler function for save actions.
* All handler functions must return a Status. In case of an error a Status object with severity error must be returned.
* Note: in contrast to events, handlers can control the flow of the lifecycle. They also have a return value where events have none.
* Only one handler can be registered for each type.
*/
handle(type, func) {
var supportedTypes = ['load', 'save'];
if (supportedTypes.indexOf(type) === -1) {
throw new Error('Cannot register handler for unsupported type \'' + type + '\'');
}
this.handlers[type] = func;
}
/**
* Register an event handler for the given type.
* Event handlers don't have a return value. They do not have any influence on the lifecycle flow. There can be multiple event
* handler for each type.
*/
on(type, func) {
return this.events.on(type, func);
}
off(type, func) {
return this.events.off(type, func);
}
}