diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-08-02 12:31:05 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-08-02 12:31:05 +0200 |
| commit | fcfb4782b6d93348bd2028fef9da335f6a953c05 (patch) | |
| tree | 2705dbbf0deabf19c80faae27ed15eb47883b04d /addons/web/static/src | |
| parent | c1d22b6c3c605d894130251c4417b684d7cf1b54 (diff) | |
| parent | a445548f07fbf8d883f4b02e18a35f4b324c6961 (diff) | |
[MERGE] forward port branch saas-17 up to a445548f07f
Diffstat (limited to 'addons/web/static/src')
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; |
