/* * Axelor Business Solutions * * Copyright (C) 2005-2019 Axelor (). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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 . */ (function() { "use strict"; var ui = angular.module('axelor.ui'); var equals = angular.equals, forEach = angular.forEach, isArray = angular.isArray, isObject = angular.isObject, isDate = angular.isDate; function dummyEquals(a, b) { if (a === b) return true; if (a === null || b === null) return false; if (a !== a && b !== b) return true; // NaN === NaN var keys = _.keys(a).filter(function (k) { return k.indexOf('$') === 0; }); if (keys.length === 0) { return true; } for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (!equals(a[k], b[k])) { return false; } } return true; } function updateValues(source, target, itemScope, formScope) { if (equals(source, target) && dummyEquals(source, target) && (!source || !source.$force)) { return; } // handle json records if (source && formScope && formScope._model === 'com.axelor.meta.db.MetaJsonRecord') { if (source.attrs || source.id) { source = source.id > 0 ? _.pick(source, 'jsonModel', 'name', 'attrs', 'id', 'version') : _.pick(source, 'jsonModel', 'name', 'attrs'); } var values = source.attrs ? _.extend({}, JSON.parse(source.attrs)) : source; var fix = function (rec) { if (!rec) return rec; if (_.isArray(rec)) return _.map(rec, fix); if (rec.id > 0 && (rec.version || rec.attrs)) { rec = _.pick(rec, 'id', 'name', 'selected'); if (!rec.selected) delete rec.selected; } return rec; }; _.each(values, function (v, k) { values[k] = fix(v); }); // if called from form fields if (itemScope && itemScope.updateJsonValues) { return itemScope.updateJsonValues(values); } // onNew or onSave from main form var current = target && target.attrs ? JSON.parse(target.attrs) : {}; if (source.attrs || !source.jsonModel) { source.attrs = JSON.stringify(_.extend({}, current, values)); } } else if (itemScope && itemScope.updateJsonValues) { return itemScope.updateJsonValues(source); } function compact(value) { if (!value) return value; if (value.version === undefined) return value; if (!value.id) return value; var res = _.extend(value); res.$version = res.version; res.version = undefined; return res; } var changed = false; forEach(source, function(value, key) { var dest; var newValue = value; var oldValue = target[key]; if (oldValue === newValue) { return; } if (isArray(value)) { dest = target[key] || []; newValue = _.map(value, function(item) { var found = _.find(dest, function(v){ return item.id && v.id === item.id; }); if (_.has(item, "version") && item.id) item.$fetched = true; if (found) { var found_ = _.extend({}, found); var changed_ = updateValues(item, found_); changed = changed || changed_; return changed_ ? found_ : found; } return item; }); } else if (isObject(value) && !isDate(value)) { dest = target[key] || {}; if (dest.id === value.id) { if (_.isNumber(dest.version)) { dest = _.extend({}, dest); changed = updateValues(value, dest, itemScope, formScope) || changed; } else { dest.$updatedValues = value; if (formScope) { formScope.$broadcast('on:check-nested-values', value); } } } else { dest = compact(value); } newValue = dest; } if (!equals(oldValue, newValue)) { changed = true; target[key] = newValue; } }); if (target && changed) { target.$dirty = true; } return changed; } function handleError(scope, item, message) { if (!item) { return; } var ctrl = item.data('$ngModelController'); if (!ctrl) { return; } if (ctrl.$doReset) { ctrl.$doReset(); } if (!message) { ctrl.$doReset = null; return; } var e = $('').text(message); var p = item.parent('.form-item'); if (item.children(':first').is(':input,.input-append,.picker-input')) { p.append(e); } else { p.prepend(e); } var clear = scope.$on('on:edit', function(){ ctrl.$doReset(); }); function cleanUp(items) { var idx = items.indexOf(ctrl.$doReset); if (idx > -1) { items.splice(idx, 1); } } ctrl.$doReset = function(value) { cleanUp(ctrl.$viewChangeListeners); cleanUp(ctrl.$formatters); ctrl.$setValidity('invalid', true); ctrl.$doReset = null; e.remove(); clear(); return value; }; if (!item.hasClass('readonly')) { ctrl.$setValidity('invalid', false); } ctrl.$viewChangeListeners.push(ctrl.$doReset); ctrl.$formatters.push(ctrl.$doReset); } function ActionHandler($scope, ViewService, options) { if (!options || !options.action) throw 'No action provided.'; this.canSave = options.canSave; this.name = options.name; this.prompt = options.prompt; this.action = options.action; this.element = options.element || $(); this.scope = $scope; this.ws = ViewService; this.viewType = $scope.viewType; } ActionHandler.prototype = { constructor: ActionHandler, onLoad : function() { return this.handle(); }, onNew: function() { return this.handle(); }, onSave: function() { var self = this; return this._fireBeforeSave().then(function() { return self.handle(); }); }, onTabSelect: function(unblocked) { return this.onSelect.apply(this, arguments); }, onSelect: function(unblocked) { var self = this; var blockUI = this._blockUI; if (unblocked) { this._blockUI = angular.noop; } function reset() { self._blockUI = blockUI; } var promise = this.handle(); promise.then(reset, reset); return promise; }, onClick: function(event) { var self = this; var prompt = this._getPrompt(); if (prompt) { var deferred = this.ws.defer(), promise = deferred.promise; axelor.dialogs.confirm(prompt, function(confirmed){ if (confirmed) { self._fireBeforeSave().then(function() { self.handle().then(deferred.resolve, deferred.reject); }); } else { self.scope.$timeout(deferred.reject); } }, { yesNo: false }); return promise; } return this._fireBeforeSave().then(function() { return self.handle(); }); }, onChange: function(event) { return this.handle({ wait: 100 }); }, _getPrompt: function () { var prompt = this.prompt; var itemScope = this.element.scope(); if (_.isFunction(itemScope.attr) && !this.element.is('[ui-slick-grid]')) { prompt = itemScope.attr('prompt') || prompt; } return _.isString(prompt) ? prompt : null; }, _getContext: function() { var scope = this.scope, context = scope.getContext ? scope.getContext() : scope.record, viewParams = scope._viewParams || {}; context = _.extend({}, viewParams.context, context); if (context._model === undefined) { context._model = scope._model; } // include button name as _signal (used by workflow engine) if (this.element.is("button,a.button-item,li.action-item")) { context._signal = this.element.attr('name') || this.element.attr('x-name'); } return context; }, _getRootFormElement: function () { var formElement = $(this.element).parents('form[ui-form]:last'); if (formElement.length === 0) { formElement = this._getFormElement(); } return formElement; }, _getFormElement: function () { var elem = $(this.element); var formElement = elem; if (formElement.is('form')) { return formElement; } formElement = elem.data('$editorForm') || elem.parents('form:first'); if (!formElement || !formElement.get(0)) { // toolbar button formElement = this.element.parents('.form-view:first').find('form:first'); } if (formElement.length === 0) { formElement = this.element; } return formElement; }, handle: function(options) { var that = this; var action = this.action.trim(); var deferred = this.ws.defer(); var all = this.scope.$actionPromises || []; var pending = all.slice(); var opts = _.extend({}, options); all.push(deferred.promise); this.scope.waitForActions(function () { var promise = that._handleAction(action); function done() { setTimeout(function () { var i = all.indexOf(deferred.promise); if (i > -1) { all.splice(i, 1); } }, 10); } promise.then(done, done); promise.then(deferred.resolve, deferred.reject); }, opts.wait || 10, pending); return deferred.promise; }, _blockUI: function() { // block the entire ui (auto unblocks when actions are complete) _.delay(axelor.blockUI, 100); }, _fireBeforeSave: function() { var scope = this._getRootFormElement().scope(); var event = scope.$broadcast('on:before-save', scope.record); var deferred = this.ws.defer(); if (event.defaultPrevented) { if (event.error) { axelor.dialogs.error(event.error); } setTimeout(function() { deferred.reject(event.error); }); } else { scope.$timeout(function() { scope.ajaxStop(function() { deferred.resolve(); }, 100); }, 50); } return deferred.promise; }, _checkVersion: function() { var self = this; var scope = this.scope; var deferred = this.ws.defer(); if (scope.checkVersion) { scope.checkVersion(function (verified) { if (verified) { return deferred.resolve(); } axelor.dialogs.error( _t("The record has been updated or delete by another action.")); deferred.reject(); }); } else { deferred.resolve(); } return deferred.promise; }, _handleNew: function() { var self = this; var scope = this.scope; var deferred = this.ws.defer(); if (scope.onNew) { return scope.onNew(); } if (scope.editRecord) { scope.editRecord(null); deferred.resolve(); } else { deferred.reject(); } return deferred.promise; }, _handleSave: function(validateOnly) { if (validateOnly) { return this.__handleSave(validateOnly); } var self = this; var deferred = this.ws.defer(); this._checkVersion().then(function () { self.__handleSave().then(deferred.resolve, deferred.reject); }, deferred.reject); return deferred.promise; }, __handleSave: function(validateOnly) { var self = this; var scope = this.scope; var id = (scope.record||{}).id; var o2mPopup = scope._isPopup && (scope.$parent.field||{}).serverType === "one-to-many"; if (o2mPopup && !validateOnly && this.name == 'onLoad' && (!id || id < 0)) { var deferred = this.ws.defer(); var msg = _t("The {0}={1} event can't call 'save' action on unsaved o2m item.", this.name, this.action); deferred.reject(msg); console.error(msg); return deferred.promise; } return this._fireBeforeSave().then(function() { return self.__doHandleSave(validateOnly); }); }, __doHandleSave: function(validateOnly) { this._blockUI(); // save should be done on root form scope only var rootForm = this._getRootFormElement(); var scope = rootForm.is('[ui-view-grid]') ? this.scope : rootForm.scope(); var deferred = this.ws.defer(); if (scope.isValid && !scope.isValid()) { if (scope.showErrorNotice) { scope.showErrorNotice(); } else { axelor.notify.error(_t('Please correct the invalid form values.'), { title: _t('Validation error') }); } deferred.reject(); return deferred.promise; } if (validateOnly || (scope.isDirty && !scope.isDirty())) { deferred.resolve(); return deferred.promise; } function doEdit(rec) { var params = scope._viewParams || {}; scope.editRecord(rec); if (params.$viewScope) { params.$viewScope.updateRoute(); } deferred.resolve(); } function doSave(values) { var ds = scope._dataSource; ds.save(values).success(function(rec, page) { if (scope.doRead) { return scope.doRead(rec.id).success(doEdit); } return ds.read(rec.id).success(doEdit); }); } var values = _.extend({ _original: scope.$$original }, scope.record); if (scope.onSave) { scope.onSave({ values: values, callOnSave: false, wait: false }).then(deferred.resolve, deferred.reject); } else { doSave(values); } this._invalidateContext = true; return deferred.promise; }, _closeView: function (scope) { if (scope.onOK) { return scope.onOK(); } var tab = scope._viewParams || scope.selectedTab; if (scope.closeTab) { scope.closeTab(tab); } else if (scope.$parent) { this._closeView(scope.$parent); } }, _isSameViewType: function () { return this.viewType === this.scope.viewType; }, _handleAction: function(action) { this._blockUI(); var self = this, scope = this.scope, context = this._getContext(), deferred = this.ws.defer(); if (!this._isSameViewType()) { deferred.reject(); return deferred.promise; } function resolveLater() { deferred.resolve(); return deferred.promise; } function chain(items) { var first = _.first(items); if (first === undefined) { return resolveLater(); } return self._handleSingle(first).then(function(pending) { if (_.isString(pending) && pending.trim().length) { return self._handleAction(pending); } var _deferred = self.ws.defer(); scope.$timeout(function () { scope.ajaxStop(function() { _deferred.resolve(); }); }); return _deferred.promise.then(function () { return chain(_.rest(items)); }); }); } if (!action) { return resolveLater(); } action = action.replace(/(^\s*,?\s*)|(\s*,?\s*$)/, ''); var pattern = /,\s*(sync)\s*(,|$)/; if (pattern.test(action)) { var which = pattern.exec(action)[1]; axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the first action.', which)); deferred.reject(); return deferred.promise; } pattern = /(^sync\s*,\s*)|(^sync$)/; if (pattern.test(action)) { action = action.replace(pattern, ''); return this._fireBeforeSave().then(function() { return self._handleAction(action); }); } pattern = /(^|,)\s*(new)\s*,/; if (pattern.test(action)) { var which = pattern.exec(action)[2]; axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the last action.', which)); deferred.reject(); return deferred.promise; } pattern = /(^|,)\s*(close)\s*,/; if (pattern.test(action)) { axelor.dialogs.error(_t('Invalid use of "{0}" action, must be the last action.', pattern.exec(action)[2])); deferred.reject(); return deferred.promise; } if (action === 'close') { this._closeView(scope); deferred.resolve(); return deferred.promise; } if (action === 'new') { return this._handleNew(); } if (action === 'validate') { return this._handleSave(true); } if (action === 'save') { return this._handleSave(); } if (this._invalidateContext) { context = this._getContext(); this._invalidateContext = false; } var model = context._model || scope._model; var data = scope.getActionData ? scope.getActionData(context) : null; if (data && context._signal) { data._signal = context._signal; } var promise = this.ws.action(action, model, context, data).then(function(response){ var resp = response.data, data = resp.data || []; if (resp.errors) { data.splice(0, 0, { errors: resp.errors }); } return chain(data); }); promise.then(deferred.resolve, deferred.reject); return deferred.promise; }, _handleSingle: function(data) { var deferred = this.ws.defer(); if (!data || data.length === 0) { deferred.resolve(); return deferred.promise; } if (!this._isSameViewType()) { deferred.reject(); return deferred.promise; } var self = this, scope = this.scope, formElement = this._getFormElement(), formScope = formElement.data('$scope') || scope, rootForm = this._getRootFormElement(), rootScope = rootForm.is('[ui-view-grid]') ? scope : rootForm.scope(); function doReload(pending) { self._invalidateContext = true; var promise = _.isFunction(rootScope.reload) ? rootScope.reload() : scope.reload(); if (promise) { promise.then(function(){ deferred.resolve(pending); }, deferred.reject); } else { deferred.resolve(pending); } return deferred.promise; } if (data.exportFile) { (function () { var link = "ws/files/data-export/" + data.exportFile; var frame = $('