summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-08-02 12:31:05 +0200
committerChristophe Simonis <chs@odoo.com>2017-08-02 12:31:05 +0200
commitfcfb4782b6d93348bd2028fef9da335f6a953c05 (patch)
tree2705dbbf0deabf19c80faae27ed15eb47883b04d /addons/web/static/src
parentc1d22b6c3c605d894130251c4417b684d7cf1b54 (diff)
parenta445548f07fbf8d883f4b02e18a35f4b324c6961 (diff)
[MERGE] forward port branch saas-17 up to a445548f07f
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js14
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js33
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js32
-rw-r--r--addons/web/static/src/js/services/crash_manager.js2
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js27
-rw-r--r--addons/web/static/src/js/views/abstract_renderer.js2
-rw-r--r--addons/web/static/src/js/views/abstract_view.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js25
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js38
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js24
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js67
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js3
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js15
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js10
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js4
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js24
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js106
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js5
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js25
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js15
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js15
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js22
-rw-r--r--addons/web/static/src/less/views.less3
23 files changed, 292 insertions, 221 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index fa2f37a123f..9b484354544 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -65,12 +65,10 @@ var AbstractField = Widget.extend({
* @constructor
* @param {Widget} parent
* @param {string} name The field name defined in the model
- * @param {Object} record A record object (result of the get method of
+ * @param {Object} record A record object (result of the get method of
* a basic model)
* @param {Object} [options]
* @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
- * @param {string} [options.standalone=false] this flag indicates that the
- * field does not have a controller to delegate to
*/
init: function (parent, name, record, options) {
this._super(parent);
@@ -160,10 +158,6 @@ var AbstractField = Widget.extend({
// calls to the format (resp. parse) function.
this.formatOptions = {};
this.parseOptions = {};
-
- // standalone is a flag indicating that the field does not have a
- // controller to delegate actions to
- this.standalone = options.standalone;
},
/**
* When a field widget is appended to the DOM, its start method is called,
@@ -379,6 +373,11 @@ var AbstractField = Widget.extend({
* @private
* @param {any} value
* @param {Object} [options]
+ * @param {boolean} [options.doNotSetDirty=false] if true, the basic model
+ * will not consider that this field is dirty, even though it was changed.
+ * Please do not use this flag unless you really need it. Our only use
+ * case is currently the pad widget, which does a _setValue in the
+ * renderEdit method.
* @param {boolean} [options.forceChange=false] if true, the change event will be
* triggered even if the new value is the same as the old one
* @returns {Deferred}
@@ -407,6 +406,7 @@ var AbstractField = Widget.extend({
dataPointID: this.dataPointID,
changes: changes,
viewType: this.viewType,
+ doNotSetDirty: options && options.doNotSetDirty,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 2f41f81f932..2de5a73802d 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -151,6 +151,9 @@ var DebouncedField = AbstractField.extend({
});
var InputField = DebouncedField.extend({
+ custom_events: _.extend({}, DebouncedField.prototype.custom_events, {
+ field_changed: '_onFieldChanged',
+ }),
events: _.extend({}, DebouncedField.prototype.events, {
'input': '_onInput',
'change': '_onChange',
@@ -168,6 +171,12 @@ var InputField = DebouncedField.extend({
if (this.mode === 'edit') {
this.tagName = 'input';
}
+ // We need to know if the widget is dirty (i.e. if the user has changed
+ // the value, and those changes haven't been acknowledged yet by the
+ // environment), to prevent erasing that new value on a reset (e.g.
+ // coming by an onchange on another field)
+ this.isDirty = false;
+ this.lastChangeEvent = undefined;
},
//--------------------------------------------------------------------------
@@ -183,14 +192,21 @@ var InputField = DebouncedField.extend({
return this.$input || $();
},
/**
- * Do not re-render this field if it was the origin of the onchange call.
- * FIXME: make the onchange work on itself without disturbing the user typing
+ * Re-renders the widget if it isn't dirty. The widget is dirty if the user
+ * changed the value, and that change hasn't been acknowledged yet by the
+ * environment. For example, another field with an onchange has been updated
+ * and this field is updated before the onchange returns. Two '_setValue'
+ * are done (this is sequential), the first one returns and this widget is
+ * reset. However, it has pending changes, so we don't re-render.
*
* @override
*/
reset: function (record, event) {
this._reset(record, event);
- if (event && event.target === this) {
+ if (!event || event === this.lastChangeEvent) {
+ this.isDirty = false;
+ }
+ if (this.isDirty || (event && event.target === this)) {
return $.when();
} else {
return this._render();
@@ -266,6 +282,16 @@ var InputField = DebouncedField.extend({
this._doAction();
},
/**
+ * Listens to events 'field_changed' to keep track of the last event that
+ * has been trigerred. This allows to detect that all changes have been
+ * acknowledged by the environment.
+ *
+ * @param {OdooEvent} event 'field_changed' event
+ */
+ _onFieldChanged: function (event) {
+ this.lastChangeEvent = event;
+ },
+ /**
* Called when the user is typing text -> By default this only calls a
* debounced method to notify the outside world of the changes.
* @see _doDebouncedAction
@@ -273,6 +299,7 @@ var InputField = DebouncedField.extend({
* @private
*/
_onInput: function () {
+ this.isDirty = true;
this._doDebouncedAction();
},
/**
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 1935e5804dd..64049d8b1ec 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -223,25 +223,21 @@ var FieldMany2One = AbstractField.extend({
_quickCreate: function (name) {
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
- var action = function () {
- return self._rpc({
- model: self.field.relation,
- method: 'name_create',
- args: [name],
- context: self.record.getContext(self.recordParams),
- }).then(function (result) {
- if (self.mode === "edit") {
- self.reinitialize({id: result[0], display_name: result[1]});
- }
- }, slowCreate);
- };
-
if (this.nodeOptions.quick_create) {
- if (this.standalone) {
- action();
- } else {
- this.trigger_up('mutexify', {action: action});
- }
+ this.trigger_up('mutexify', {
+ action: function () {
+ return self._rpc({
+ model: self.field.relation,
+ method: 'name_create',
+ args: [name],
+ context: self.record.getContext(self.recordParams),
+ }).then(function (result) {
+ if (self.mode === "edit") {
+ self.reinitialize({id: result[0], display_name: result[1]});
+ }
+ }, slowCreate);
+ },
+ });
} else {
slowCreate();
}
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index 9d3b9fa63b8..533cac6f3ad 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -207,7 +207,7 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
}
});
-core.crash_registry.add('openerp.exceptions.RedirectWarning', RedirectWarningHandler);
+core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
return CrashManager;
});
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index b84f6ec1951..3bdf8ab08ca 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -32,7 +32,6 @@ var AbstractController = Widget.extend({
* @param {string} params.modelName
* @param {any} [params.handle] a handle that will be given to the model (some id)
* @param {any} params.initialState the initialState
- * @param {string} [params.noContentHelp]
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
@@ -42,7 +41,6 @@ var AbstractController = Widget.extend({
this.handle = params.handle;
this.activeActions = params.activeActions;
this.initialState = params.initialState;
- this.noContentHelp = params.noContentHelp;
},
/**
* Simply renders and updates the url.
@@ -224,31 +222,6 @@ var AbstractController = Widget.extend({
this.trigger_up('push_state', state || {});
},
/**
- * Hide or show the nocontent helper. For this, it also remove the renderer
- * from the dom, and reattach it when necessary.
- *
- * This method is a helper for controllers that want to display a help
- * message when no content is available. It is suggested to override
- * _update to call this method.
- *
- * @private
- * @param {boolean} hasNoContent
- */
- _toggleNoContentHelper: function (hasNoContent) {
- if (hasNoContent && this.noContentHelp) {
- this.renderer.$el.detach();
- var $msg = $('<div>')
- .addClass('oe_view_nocontent')
- .html(this.noContentHelp);
- this.$el.html($msg);
- } else {
- if (!document.contains(this.renderer.el)) {
- this.$('div.oe_view_nocontent').remove();
- this.$el.append(this.renderer.$el);
- }
- }
- },
- /**
* This method is called after each update or when the start method is
* completed.
*
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 7d10f05eab8..d7eb5002f2d 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -15,11 +15,13 @@ return Widget.extend({
* @param {Widget} parent
* @param {any} state
* @param {Object} params
+ * @param {string} [params.noContentHelp]
*/
init: function (parent, state, params) {
this._super(parent);
this.state = state;
this.arch = params.arch;
+ this.noContentHelp = params.noContentHelp;
},
/**
* The rendering can be asynchronous (but it is not encouraged). The start
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 4c43a692601..8de118b29b7 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -78,6 +78,7 @@ var AbstractView = Class.extend({
init: function (viewInfo, params) {
this.rendererParams = {
arch: viewInfo.arch,
+ noContentHelp: params.action && params.action.help,
};
this.controllerParams = {
@@ -88,7 +89,6 @@ var AbstractView = Class.extend({
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true,
},
- noContentHelp: params.action && params.action.help,
};
this.loadParams = {
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index cc72816a85d..3a47f65ab03 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -8,7 +8,6 @@ odoo.define('web.BasicController', function (require) {
*/
var AbstractController = require('web.AbstractController');
-var concurrency = require('web.concurrency');
var core = require('web.core');
var Dialog = require('web.Dialog');
var FieldManagerMixin = require('web.FieldManagerMixin');
@@ -19,7 +18,6 @@ var _t = core._t;
var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
discard_changes: '_onDiscardChanges',
- mutexify: '_onMutexify',
reload: '_onReload',
sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
@@ -39,7 +37,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin.init.call(this, this.model);
this.handle = params.initialState.id;
this.mode = params.mode || 'readonly';
- this.mutex = new concurrency.Mutex();
},
/**
* @override
@@ -238,7 +235,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var recordID = record.data.id;
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
- context: record.getContext({additionalContext: attrs.context}),
+ context: record.getContext({additionalContext: attrs.context || {}}),
}),
model: record.model,
res_ids: [recordID],
@@ -362,17 +359,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
};
},
/**
- * Used by list and kanban views to determine whether or not to display
- * the no content helper (if there is no data in the state to display)
- *
- * @private
- * @param {Object} state
- * @returns {boolean}
- */
- _hasContent: function (state) {
- return state.count !== 0;
- },
- /**
* Helper function to display a warning that some fields have an invalid
* value. This is used when a save operation cannot be completed.
*
@@ -538,15 +524,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin._onFieldChanged.apply(this, arguments);
},
/**
- * @private
- * @param {OdooEvent} ev
- * @param {function} ev.data.action the function to execute in the mutex
- */
- _onMutexify: function (ev) {
- ev.stopPropagation(); // prevent other controllers from handling this request
- this.mutex.exec(ev.data.action);
- },
- /**
* When a reload event triggers up, we need to reload the full view.
* For example, after a form view dialog saved some data.
*
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 8f679207f00..04fe384afdc 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -628,12 +628,12 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} record_id
* @param {Object} changes a map field => new value
- * @param {string} [viewType] current viewType. If not set, we will assume
- * main viewType from the record
+ * @param {Object} [options] will be transferred to the applyChange method
+ * @see _applyChange
* @returns {string[]} list of changed fields
*/
- notifyChanges: function (record_id, changes, viewType) {
- return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType));
+ notifyChanges: function (record_id, changes, options) {
+ return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
},
/**
* Reload all data for a given resource
@@ -956,23 +956,30 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} recordID
* @param {Object} changes
- * @param {string} [viewType] current viewType. If not set, we will assume
+ * @param {Object} [options]
+ * @param {boolean} [options.doNotSetDirty=false] if this flag is set to
+ * true, then we will not tag the record as dirty. This should be avoided
+ * for most situations.
+ * @param {string} [options.viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Deferred}
*/
- _applyChange: function (recordID, changes, viewType) {
+ _applyChange: function (recordID, changes, options) {
var self = this;
var record = this.localData[recordID];
var field;
var defs = [];
+ options = options || {};
record._changes = record._changes || {};
- record._isDirty = true;
+ if (!options.doNotSetDirty) {
+ record._isDirty = true;
+ }
// apply changes to local data
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
- defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType));
+ defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType));
} else if (field.type === 'many2one' || field.type === 'reference') {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
@@ -993,7 +1000,7 @@ var BasicModel = AbstractModel.extend({
}
var onchangeDef;
if (onChangeFields.length) {
- onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) {
+ onchangeDef = self._performOnChange(record, onChangeFields, options.viewType).then(function (result) {
delete record._warning;
return _.keys(changes).concat(Object.keys(result && result.value || {}));
});
@@ -2196,7 +2203,7 @@ var BasicModel = AbstractModel.extend({
// remove readonly fields from the list of changes
if (!withReadonly && fieldName in changes || fieldName in commands) {
var editionViewType = record._editionViewType[fieldName] || viewType;
- if (this._isFieldReadonly(record, fieldName, editionViewType)) {
+ if (this._isFieldProtected(record, fieldName, editionViewType)) {
delete changes[fieldName];
continue;
}
@@ -2555,9 +2562,10 @@ var BasicModel = AbstractModel.extend({
return context;
},
/**
- * Returns true if the field is readonly (checking first in the modifiers,
- * and if there is no readonly modifier, checking the readonly attribute of
- * the field).
+ * Returns true if the field is protected against changes, looking for a
+ * readonly modifier unless there is a force_save modifier (checking first
+ * in the modifiers, and if there is no readonly modifier, checking the
+ * readonly attribute of the field).
*
* @private
* @param {Object} record an element from the localData
@@ -2566,12 +2574,12 @@ var BasicModel = AbstractModel.extend({
* main viewType from the record
* @returns {boolean}
*/
- _isFieldReadonly: function (record, fieldName, viewType) {
+ _isFieldProtected: function (record, fieldName, viewType) {
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
if (fieldInfo) {
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}");
var modifiers = this._evalModifiers(record, rawModifiers);
- return modifiers.readonly;
+ return modifiers.readonly && !fieldInfo.force_save;
} else {
return false;
}
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 8bbf9ae45a4..e040a5fa7f0 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -318,6 +318,16 @@ var BasicRenderer = AbstractRenderer.extend({
return _.findWhere(this.allModifiersData, {node: node});
},
/**
+ * Used by list and kanban renderers to determine whether or not to display
+ * the no content helper (if there is no data in the state to display)
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasContent: function () {
+ return this.state.count !== 0;
+ },
+ /**
* Registers or updates the modifiers data associated to the given node.
* This method is quiet complex as it handles all the needs of the basic
* renderers:
@@ -487,6 +497,20 @@ var BasicRenderer = AbstractRenderer.extend({
return widget;
},
/**
+ * Renders the nocontent helper.
+ *
+ * This method is a helper for renderers that want to display a help
+ * message when no content is available.
+ *
+ * @private
+ */
+ _renderNoContentHelper: function () {
+ var $msg = $('<div>')
+ .addClass('oe_view_nocontent')
+ .html(this.noContentHelp);
+ this.$el.html($msg);
+ },
+ /**
* Actual rendering. Supposed to be overridden by concrete renderers.
* The basic responsabilities of _renderView are:
* - use the xml arch of the view to render a jQuery representation
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index 73868a93fdb..d2b0c02c524 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -302,44 +302,38 @@ return AbstractModel.extend({
// Private
//--------------------------------------------------------------------------
+ /**
+ * Converts this.data.filters into a domain
+ *
+ * @private
+ * @returns {Array}
+ */
_getFilterDomain: function () {
- var domain = [];
+ // List authorized values for every field
+ // fields with an active 'all' filter are skipped
+ var authorizedValues = {};
+
_.each(this.data.filters, function (filter) {
+ // Skip 'all' filters because they do not affect the domain
if (filter.all) return;
- var nb = {};
- var not = {};
- var contains = {};
+
+ // Loop over subfilters to complete authorizedValues
_.each(filter.filters, function (f) {
- if (!contains[filter.fieldName]) {
- contains[filter.fieldName] = [];
- not[filter.fieldName] = [];
- nb[filter.fieldName] = 0;
- }
- if (f.value === 'all' && f.active) {
- contains[filter.fieldName].all = true;
- } else {
- nb[filter.fieldName]++;
- }
+ if (!authorizedValues[filter.fieldName])
+ authorizedValues[filter.fieldName] = [];
- if (f.active && contains[filter.fieldName].indexOf(f.value) === -1) {
- contains[filter.fieldName].push(f.value);
- }
- if (!f.active && not[filter.fieldName].indexOf(f.value) === -1) {
- not[filter.fieldName].push(f.value);
+ if (f.active) {
+ authorizedValues[filter.fieldName].push(f.value);
}
});
-
- for (var field in contains) {
- if (contains[field].all || contains[field].length === nb[field]) {
- continue;
- }
- if (contains[field].length) {
- domain.push([field, 'in', contains[field]]);
- } else if (not[field].length && not[field].length !== nb[field]) {
- domain.push([field, 'not in', not[field]]);
- }
- }
});
+
+ // Compute the domain
+ var domain = [];
+ for (var field in authorizedValues) {
+ domain.push([field, 'in', authorizedValues[field]]);
+ }
+
return domain;
},
/**
@@ -393,19 +387,16 @@ return AbstractModel.extend({
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
+
return $.when.apply($, defs).then(function () {
- var eventsDef = $.Deferred().resolve([]);
- var filterDomain = self._getFilterDomain();
- if (filterDomain.length || _.isEmpty(self.data.filters)) {
- eventsDef = self._rpc({
+ return self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
- domain: self.data.domain.concat(self._getRangeDomain()).concat(filterDomain)
- });
- }
- return eventsDef.then(function (events) {
+ domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
+ })
+ .then(function (events) {
self._parseServerData(events);
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index b61236131c6..4f5719063ff 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -330,11 +330,12 @@ return AbstractRenderer.extend({
}
element.find('.fc-content .fc-time').text(display_hour);
},
- height: 'auto',
+ height: 'parent',
unselectAuto: false,
});
this.$calendar.fullCalendar(fc_options);
+ window.dispatchEvent(new Event('resize'));
},
/**
* Initialize the mini calendar in the sidebar
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 422af121d96..1ee7bf4a501 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -9,11 +9,13 @@ odoo.define('web.FieldManagerMixin', function (require) {
*/
var BasicModel = require('web.BasicModel');
+var concurrency = require('web.concurrency');
var FieldManagerMixin = {
custom_events: {
field_changed: '_onFieldChanged',
load: '_onLoad',
+ mutexify: '_onMutexify',
},
/**
* A FieldManagerMixin can be initialized with an instance of a basicModel.
@@ -23,6 +25,7 @@ var FieldManagerMixin = {
*/
init: function (model) {
this.model = model || new BasicModel(this);
+ this.mutex = new concurrency.Mutex();
},
//--------------------------------------------------------------------------
@@ -43,7 +46,8 @@ var FieldManagerMixin = {
*/
_applyChanges: function (dataPointID, changes, event) {
var self = this;
- return this.model.notifyChanges(dataPointID, changes, event.data.viewType)
+ var options = _.pick(event.data, 'viewType', 'doNotSetDirty');
+ return this.model.notifyChanges(dataPointID, changes, options)
.then(function (result) {
if (event.data.force_save) {
return self.model.save(dataPointID).then(function () {
@@ -129,6 +133,15 @@ var FieldManagerMixin = {
data.on_success(self.model.get(db_id));
});
},
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} ev.data.action the function to execute in the mutex
+ */
+ _onMutexify: function (ev) {
+ ev.stopPropagation(); // prevent other field managers from handling this request
+ this.mutex.exec(ev.data.action);
+ },
};
return FieldManagerMixin;
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 35194b313ad..1d788ac2c05 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -115,18 +115,20 @@ var FormRenderer = BasicRenderer.extend({
});
},
/**
- * Disable stat buttons so that they can't be clicked anymore
+ * Disable statusbar buttons and stat buttons so that they can't be clicked anymore
*
*/
disableButtons: function () {
- this.$('.oe_button_box button').attr('disabled', true);
+ this.$('.o_statusbar_buttons button, .oe_button_box button')
+ .attr('disabled', true);
},
/**
- * Enable stat buttons so they can be clicked again
+ * Enable statusbar buttons and stat buttons so they can be clicked again
*
*/
enableButtons: function () {
- this.$('.oe_button_box button').removeAttr('disabled');
+ this.$('.o_statusbar_buttons button, .oe_button_box button')
+ .removeAttr('disabled');
},
/**
* returns the active tab pages for each notebook
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 8a7f5da6d5d..50e9f224697 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -114,13 +114,13 @@ return AbstractModel.extend({
* @returns {Deferred}
*/
_loadGraph: function () {
- var fields = _.map(this.chart.groupedBy, function (groupBy) {
+ var groupedBy = this.chart.groupedBy.length ? this.chart.groupedBy : this.initialGroupBys;
+ var fields = _.map(groupedBy, function (groupBy) {
return groupBy.split(':')[0];
});
if (this.chart.measure !== '__count__') {
fields = fields.concat(this.chart.measure);
}
- var groupedBy = this.chart.groupedBy.length ? this.chart.groupedBy : this.initialGroupBys;
return this._rpc({
model: this.modelName,
method: 'read_group',
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2592e3d96ec..2cf6a94285e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -81,19 +81,6 @@ var KanbanController = BasicController.extend({
return this.renderer.updateRecord(this.model.get(id));
},
/**
- * The nocontent helper should be displayed in kanban:
- * - ungrouped: if there is no records
- * - grouped: if there is no groups and no column quick create
- *
- * @override
- * @private
- */
- _hasContent: function (state) {
- return this._super.apply(this, arguments) ||
- this.createColumnEnabled ||
- (state.groupedBy.length && state.data.length);
- },
- /**
* The column quick create should be displayed in kanban iff grouped by an
* m2o field and group_create action enabled.
*
@@ -138,17 +125,6 @@ var KanbanController = BasicController.extend({
.toggleClass('btn-default', createMuted);
}
},
- /**
- * @override
- * @param {Object} state
- * @returns {Deferred}
- */
- _update: function (state) {
- var hasNoContent = !this._hasContent(state);
- this.$el.toggleClass('o_kanban_nocontent', hasNoContent);
- this._toggleNoContentHelper(hasNoContent);
- return this._super.apply(this, arguments);
- },
//--------------------------------------------------------------------------
// Handlers
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index ea2f0270017..0bbc7aa2698 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -100,6 +100,8 @@ var KanbanRenderer = BasicRenderer.extend({
viewType: 'kanban',
});
this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb });
+
+ this._setState(state);
},
//--------------------------------------------------------------------------
@@ -167,12 +169,32 @@ var KanbanRenderer = BasicRenderer.extend({
record.update(recordState);
}
},
+ /**
+ * @override
+ */
+ updateState: function (state) {
+ this._setState(state);
+ return this._super.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
+ * The nocontent helper should be displayed in kanban:
+ * - ungrouped: if there is no records
+ * - grouped: if there is no groups and no column quick create
+ *
+ * @override
+ * @private
+ */
+ _hasContent: function () {
+ return this._super.apply(this, arguments) ||
+ this.createColumnEnabled ||
+ (this.state.groupedBy.length && this.state.data.length);
+ },
+ /**
* Renders empty invisible divs in a document fragment.
*
* @private
@@ -193,28 +215,6 @@ var KanbanRenderer = BasicRenderer.extend({
*/
_renderGrouped: function (fragment) {
var self = this;
- var groupByFieldAttrs = this.state.fields[this.state.groupedBy[0]];
- var groupByFieldInfo = this.state.fieldsInfo.kanban[this.state.groupedBy[0]];
- // Deactivate the drag'n'drop if the groupedBy field:
- // - is a date or datetime since we group by month or
- // - is readonly
- var draggable = true;
- if (groupByFieldAttrs) {
- if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
- draggable = false;
- } else if (groupByFieldAttrs.readonly !== undefined) {
- draggable = !(groupByFieldAttrs.readonly);
- }
- }
- var groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
- var grouped_by_field = groupedByM2O && groupByFieldAttrs.relation;
- var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
- this.columnOptions = _.extend(this.columnOptions, {
- draggable: draggable,
- group_by_tooltip: groupByTooltip,
- grouped_by_m2o: groupedByM2O,
- relation: grouped_by_field,
- });
// Render columns
_.each(this.state.data, function (group) {
@@ -228,7 +228,7 @@ var KanbanRenderer = BasicRenderer.extend({
}
});
- if (groupedByM2O) {
+ if (this.groupedByM2O) {
// Enable column sorting
this.$el.sortable({
axis: 'x',
@@ -249,7 +249,7 @@ var KanbanRenderer = BasicRenderer.extend({
});
// Enable column quickcreate
- if (this.columnOptions.group_creatable) {
+ if (this.createColumnEnabled) {
this.quickCreate = new ColumnQuickCreate(this);
this.quickCreate.appendTo(fragment);
}
@@ -280,20 +280,62 @@ var KanbanRenderer = BasicRenderer.extend({
_renderView: function () {
var oldWidgets = this.widgets;
this.widgets = [];
- var isGrouped = !!this.state.groupedBy.length;
- this.$el.toggleClass('o_kanban_grouped', isGrouped);
- this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
this.$el.empty();
- var fragment = document.createDocumentFragment();
- if (isGrouped) {
- this._renderGrouped(fragment);
+ var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
+ this.$el.toggleClass('o_kanban_nocontent', displayNoContentHelper);
+ if (displayNoContentHelper) {
+ // display the no content helper if there is no data to display
+ this._renderNoContentHelper();
} else {
- this._renderUngrouped(fragment);
+ var isGrouped = !!this.state.groupedBy.length;
+ this.$el.toggleClass('o_kanban_grouped', isGrouped);
+ this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
+ var fragment = document.createDocumentFragment();
+ // render the kanban view
+ if (isGrouped) {
+ this._renderGrouped(fragment);
+ } else {
+ this._renderUngrouped(fragment);
+ }
+ this.$el.append(fragment);
}
- this.$el.append(fragment);
+
return this._super.apply(this, arguments).then(_.invoke.bind(_, oldWidgets, 'destroy'));
},
+ /**
+ * Sets the current state and updates some internal attributes accordingly.
+ *
+ * @private
+ * @param {Object} state
+ */
+ _setState: function (state) {
+ this.state = state;
+
+ var groupByFieldAttrs = state.fields[state.groupedBy[0]];
+ var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]];
+ // Deactivate the drag'n'drop if the groupedBy field:
+ // - is a date or datetime since we group by month or
+ // - is readonly
+ var draggable = true;
+ if (groupByFieldAttrs) {
+ if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
+ draggable = false;
+ } else if (groupByFieldAttrs.readonly !== undefined) {
+ draggable = !(groupByFieldAttrs.readonly);
+ }
+ }
+ this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
+ var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation;
+ var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
+ this.columnOptions = _.extend(this.columnOptions, {
+ draggable: draggable,
+ group_by_tooltip: groupByTooltip,
+ grouped_by_m2o: this.groupedByM2O,
+ relation: grouped_by_field,
+ });
+ this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
+ },
});
return KanbanRenderer;
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 2ec9cf3181e..3b74f140e4d 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -188,7 +188,6 @@ var ListController = BasicController.extend({
position: self.editable,
});
}).then(function (recordID) {
- self._toggleNoContentHelper(false);
var state = self.model.get(self.handle);
self.renderer.updateState(state, {});
self.renderer.editRecord(recordID);
@@ -283,12 +282,10 @@ var ListController = BasicController.extend({
},
/**
* @override
- * @param {Object} state
* @returns {Deferred}
*/
- _update: function (state) {
+ _update: function () {
this.selectedRecords = [];
- this._toggleNoContentHelper(!this._hasContent(state));
this._toggleSidebar();
return this._super.apply(this, arguments);
},
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 08b9e7f1602..3cd93bb6e8b 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -13,6 +13,7 @@ odoo.define('web.EditableListRenderer', function (require) {
*/
var core = require('web.core');
var ListRenderer = require('web.ListRenderer');
+var utils = require('web.utils');
var _t = core._t;
@@ -153,12 +154,32 @@ ListRenderer.include({
*/
setRowMode: function (recordID, mode) {
var self = this;
- var rowIndex = _.findIndex(this.state.data, {id: recordID});
+
+ // find the record and its row index (handles ungrouped and grouped cases
+ // as even if the grouped list doesn't support edition, it may contain
+ // a widget allowing the edition in readonly (e.g. priority), so it
+ // should be able to update a record as well)
+ var record;
+ var rowIndex;
+ if (this.state.groupedBy.length) {
+ rowIndex = -1;
+ var count = 0;
+ utils.traverse_records(this.state, function (r) {
+ if (r.id === recordID) {
+ record = r;
+ rowIndex = count;
+ }
+ count++;
+ });
+ } else {
+ rowIndex = _.findIndex(this.state.data, {id: recordID});
+ record = this.state.data[rowIndex];
+ }
+
if (rowIndex < 0) {
return $.when();
}
var editMode = (mode === 'edit');
- var record = this.state.data[rowIndex];
this.currentRow = editMode ? rowIndex : null;
var $row = this.$('.o_data_row:nth(' + rowIndex + ')');
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 802f2f03a09..295266db276 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -30,7 +30,6 @@ var FIELD_CLASSES = {
};
var ListRenderer = BasicRenderer.extend({
- className: 'table-responsive',
events: {
'click tbody tr': '_onRowClicked',
'click tbody .o_list_record_selector': '_onSelectRecord',
@@ -577,12 +576,24 @@ var ListRenderer = BasicRenderer.extend({
_renderView: function () {
var self = this;
+ this.$el
+ .removeClass('table-responsive')
+ .empty();
+
// destroy the previously instantiated pagers, if any
_.invoke(this.pagers, 'destroy');
this.pagers = [];
+ // display the no content helper if there is no data to display
+ if (!this._hasContent() && this.noContentHelp) {
+ this._renderNoContentHelper();
+ return this._super();
+ }
+
var $table = $('<table>').addClass('o_list_view table table-condensed table-striped');
- this.$el.empty().append($table);
+ this.$el
+ .addClass('table-responsive')
+ .append($table);
var is_grouped = !!this.state.groupedBy.length;
this._computeAggregates();
$table.toggleClass('o_list_view_grouped', is_grouped);
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index 78640a97124..731183e7efe 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -170,21 +170,8 @@ var PivotController = AbstractController.extend({
/**
* @private
*/
- _update: function (state) {
+ _update: function () {
this._updateButtons();
- var hasNoContent = !state.has_data || !state.measures.length;
-
- if (hasNoContent) {
- if (!this.$('.oe_view_nocontent').length) {
- this.renderer.$el.detach();
- this.$el.append(QWeb.render('PivotView.nodata'));
- }
- } else {
- if (!document.contains(this.renderer.el)) {
- this.$('div.oe_view_nocontent').remove();
- this.$el.append(this.renderer.$el);
- }
- }
return this._super.apply(this, arguments);
},
/**
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index a37e65b104b..d279d59d4b1 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -2,8 +2,11 @@ odoo.define('web.PivotRenderer', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
+var core = require('web.core');
var field_utils = require('web.field_utils');
+var QWeb = core.qweb;
+
var PivotRenderer = AbstractRenderer.extend({
tagName: 'table',
className: 'table-hover table-condensed table-bordered',
@@ -16,14 +19,31 @@ var PivotRenderer = AbstractRenderer.extend({
//--------------------------------------------------------------------------
/**
+ * Used to determine whether or not to display the no content helper.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasContent: function () {
+ return this.state.has_data && this.state.measures.length;
+ },
+ /**
* @override
* @private
* @returns {Deferred}
*/
_render: function () {
- if (!this.state.has_data) {
+ if (!this._hasContent()) {
+ // display the nocontent helper
+ this.replaceElement(QWeb.render('PivotView.nodata'));
return this._super.apply(this, arguments);
}
+
+ if (!this.$el.is('table')) {
+ // coming from the no content helper, so the root element has to be
+ // re-rendered before rendering and appending its content
+ this.renderElement();
+ }
var $fragment = $(document.createDocumentFragment());
var $table = $('<table>').appendTo($fragment);
var $thead = $('<thead>').appendTo($table);
diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less
index 102d50e7590..876ad01a6e7 100644
--- a/addons/web/static/src/less/views.less
+++ b/addons/web/static/src/less/views.less
@@ -4,6 +4,9 @@
*/
// No content helper
+.o_view_manager_content > div {
+ height: 100%;
+}
.oe_view_nocontent {
a {
cursor: pointer;