diff options
Diffstat (limited to 'addons/web/static/src')
16 files changed, 209 insertions, 28 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 2fb0521ffb5..fbb6d0e7c6b 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1028,6 +1028,21 @@ var FieldPhone = FieldEmail.extend({ }, /** + * Remove possibly present ­ characters when saving number + * + * @override + * @private + */ + _setValue: function (value, options) { + // NOT NEEDED AS OF SAAS-11.3 + if (value) { + // remove possibly pasted ­ characters + value = value.replace(/\u00AD/g, ''); + } + return this._super(value, options); + }, + + /** * Phone fields are clickable in readonly on small screens ~= on phones. * This can be overriden by call-capable modules to display a clickable * link in different situations, like always regardless of screen size, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 353da62706f..a977147460a 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1292,12 +1292,27 @@ var FieldOne2Many = FieldX2Many.extend({ // we don't want interference with the components upstream. ev.stopPropagation(); + var self = this; var id = ev.data.id; - // trigger an empty 'UPDATE' operation when the user clicks on 'Save' in - // the dialog, to notify the main record that a subrecord of this - // relational field has changed (those changes will be already stored on - // that subrecord, thanks to the 'Save'). - var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {}); + var onSaved = function (record, hasChanged) { + if (!hasChanged) { + return; + } + if (_.some(self.value.data, {id: record.id})) { + // the record already exists in the relation, so trigger an + // empty 'UPDATE' operation when the user clicks on 'Save' in + // the dialog, to notify the main record that a subrecord of + // this relational field has changed (those changes will be + // already stored on that subrecord, thanks to the 'Save'). + self._setValue({ operation: 'UPDATE', id: record.id }); + } else { + // the record isn't in the relation yet, so add it ; this can + // happen if the user clicks on 'Save & New' in the dialog (the + // opened record will be updated, and other records will be + // created) + self._setValue({ operation: 'ADD', id: record.id }); + } + }; this._openFormDialog({ id: id, on_saved: onSaved, diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 1007aa6c720..ff7ab5a43bb 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -102,6 +102,18 @@ var AbstractController = Widget.extend(ControlPanelMixin, { } return this._super.apply(this, arguments); }, + /** + * Called each time the controller is attached into the DOM. + */ + on_attach_callback: function () { + this.renderer.on_attach_callback(); + }, + /** + * Called each time the controller is detached from the DOM. + */ + on_detach_callback: function () { + this.renderer.on_detach_callback(); + }, //-------------------------------------------------------------------------- // Public diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js index 935fcc6643e..2394fb31a64 100644 --- a/addons/web/static/src/js/views/abstract_renderer.js +++ b/addons/web/static/src/js/views/abstract_renderer.js @@ -36,6 +36,14 @@ return Widget.extend({ this.$el.addClass(this.arch.attrs.class); return $.when(this._render(), this._super()); }, + /** + * Called each time the renderer is attached into the DOM. + */ + on_attach_callback: function () {}, + /** + * Called each time the renderer is detached from the DOM. + */ + on_detach_callback: function () {}, //-------------------------------------------------------------------------- // Public 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 b17562348f7..de9001c328a 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -3167,6 +3167,9 @@ var BasicModel = AbstractModel.extend({ * @param {Object} [options] * @param {string[]} [options.fieldNames] the fields to fetch for a record * @param {boolean} [options.onlyGroups=false] + * @param {boolean} [options.keepEmptyGroups=false] if set, the groups not + * present in the read_group anymore (empty groups) will stay in the + * datapoint (used to mimic the kanban renderer behaviour for example) * @returns {Deferred} */ _load: function (dataPoint, options) { @@ -3319,9 +3322,29 @@ var BasicModel = AbstractModel.extend({ */ _makeDefaultRecord: function (modelName, params) { var self = this; + + var determineExtraFields = function() { + // Fields that are present in the originating view, that need to be initialized + // Hence preventing their value to crash when getting back to the originating view + var parentRecord = self.localData[params.parentID]; + + var originView = parentRecord && parentRecord.fieldsInfo; + if (!originView || !originView[parentRecord.viewType]) + return []; + + var fieldsFromOrigin = _.filter(Object.keys(originView[parentRecord.viewType]), + function(fieldname) { + return params.fields[fieldname] !== undefined; + }); + + return fieldsFromOrigin; + } + var fieldNames = Object.keys(params.fieldsInfo[params.viewType]); var fields_key = _.without(fieldNames, '__last_update'); + var extraFields = determineExtraFields(); + return this._rpc({ model: modelName, method: 'default_get', @@ -3339,7 +3362,7 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }); - return self.applyDefaultValues(record.id, result) + return self.applyDefaultValues(record.id, result, {fieldNames: _.union(fieldNames, extraFields)}) .then(function () { var def = $.Deferred(); self._performOnChange(record, fields_key).always(function () { @@ -3603,6 +3626,8 @@ var BasicModel = AbstractModel.extend({ } else if (_.contains(['one2many', 'many2many'], fieldType)) { var x2mCommands = commands[0][2][fieldName]; defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands)); + } else { + r._changes[fieldName] = self._parseServerValue(field, r._changes[fieldName]); } } } @@ -3801,6 +3826,16 @@ var BasicModel = AbstractModel.extend({ defs.push(self._load(newGroup, options)); } }); + if (options && options.keepEmptyGroups) { + // Find the groups that were available in a previous + // readGroup but are not there anymore. + // Note that these groups are put after existing groups so + // the order is not conserved. A sort *might* be useful. + var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data); + _.each(emptyGroupsIDs, function (groupID) { + list.data.push(groupID); + }); + } return $.when.apply($, defs).then(function () { // generate the res_ids of the main list, being the concatenation // of the fetched res_ids in each group 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 6467e4695c6..92890766b34 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -253,7 +253,10 @@ return AbstractModel.extend({ * @param {Moment} start */ setDate: function (start) { - this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; + // keep highlight/target_date in localtime + this.data.highlight_date = this.data.target_date = start.clone(); + // set dates in UTC with timezone applied manually + this.data.start_date = this.data.end_date = start; this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); switch (this.data.scale) { diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index ea56c841c86..4c73fde121b 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -99,6 +99,7 @@ var FormController = BasicController.extend({ * @todo convert to new style */ on_attach_callback: function () { + this._super.apply(this, arguments); this.autofocus(); }, /** diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 8ce6ec14aba..1fbd6572a9a 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -42,6 +42,30 @@ return AbstractRenderer.extend({ nv.utils.offWindowResize(this.to_remove); this._super(); }, + /** + * The graph view uses the nv(d3) lib to render the graph. This lib requires + * that the rendering is done directly into the DOM (so that it can correctly + * compute positions). However, the views are always rendered in fragments, + * and appended to the DOM once ready (to prevent them from flickering). We + * here use the on_attach_callback hook, called when the widget is attached + * to the DOM, to perform the rendering. This ensures that the rendering is + * always done in the DOM. + * + * @override + */ + on_attach_callback: function () { + this._super.apply(this, arguments); + this.isInDOM = true; + this._renderGraph(); + }, + /** + * @override + */ + on_detach_callback: function () { + this._super.apply(this, arguments); + this.isInDOM = false; + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -50,10 +74,9 @@ return AbstractRenderer.extend({ * Render the chart. * * Note that This method is synchronous, but the actual rendering is done - * asynchronously (in a setTimeout). The reason for that is that nvd3/d3 - * needs to be in the DOM to correctly render itself. So, we trick Odoo by - * returning immediately, then wait a tiny interval before actually - * displaying the data. + * asynchronously. The reason for that is that nvd3/d3 needs to be in the + * DOM to correctly render itself. So, we trick Odoo by returning + * immediately, then we render the chart when the widget is in the DOM. * * @override * @private @@ -76,21 +99,15 @@ return AbstractRenderer.extend({ description: _t("Try to add some records, or make sure that " + "there is no active filter in the search bar."), })); - } else { - var self = this; - setTimeout(function () { - self.$el.empty(); - var chart = self['_render' + _.str.capitalize(self.state.mode) + 'Chart'](); - if (chart && chart.tooltip.chartContainer) { - self.to_remove = chart.update; - nv.utils.onWindowResize(chart.update); - chart.tooltip.chartContainer(self.el); - } - }, 0); + } else if (this.isInDOM) { + // only render the graph if the widget is already in the DOM (this + // happens typically after an update), otherwise, it will be + // rendered when the widget will be attached to the DOM (see + // 'on_attach_callback') + this._renderGraph(); } return this._super.apply(this, arguments); }, - /** * Helper function to set up data properly for the multiBarChart model in * nvd3. @@ -327,6 +344,21 @@ return AbstractRenderer.extend({ chart(svg); return chart; }, + /** + * Renders the graph according to its type. This function must be called + * when the renderer is in the DOM (for nvd3 to render the graph correctly). + * + * @private + */ + _renderGraph: function () { + this.$el.empty(); + var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](); + if (chart && chart.tooltip.chartContainer) { + this.to_remove = chart.update; + nv.utils.onWindowResize(chart.update); + chart.tooltip.chartContainer(this.el); + } + }, }); }); 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 8a779becdea..9c227fba2ac 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -10,6 +10,7 @@ odoo.define('web.KanbanController', function (require) { var BasicController = require('web.BasicController'); var Context = require('web.Context'); var core = require('web.core'); +var Domain = require('web.Domain'); var view_dialogs = require('web.view_dialogs'); var _t = core._t; @@ -189,10 +190,57 @@ var KanbanController = BasicController.extend({ resIDs: record.res_ids, }, on_closed: function () { + var recordModel = self.model.localData[record.id]; + var group = self.model.localData[recordModel.parentID]; + var parent = self.model.localData[group.parentID]; + self.model.reload(record.id).then(function (db_id) { var data = self.model.get(db_id); var kanban_record = event.target; kanban_record.update(data); + + // Check if we still need to display the record. Some fields of the domain are + // not guaranteed to be in data. This is for example the case if the action + // contains a domain on a field which is not in the Kanban view. Therefore, + // we need to handle multiple cases based on 3 variables: + // domInData: all domain fields are in the data + // activeInDomain: 'active' is already in the domain + // activeInData: 'active' is available in the data + + var domain = (parent ? parent.domain : group.domain) || []; + var domInData = _.every(domain, function (d) { + return d[0] in data.data; + }); + var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1; + var activeInData = 'active' in data.data; + + // Case # | domInData | activeInDomain | activeInData + // 1 | true | true | true => no domain change + // 2 | true | true | false => not possible + // 3 | true | false | true => add active in domain + // 4 | true | false | false => no domain change + // 5 | false | true | true => no evaluation + // 6 | false | true | false => no evaluation + // 7 | false | false | true => replace domain + // 8 | false | false | false => no evaluation + + // There are 3 cases which cannot be evaluated since we don't have all the + // necessary information. The complete solution would be to perform a RPC in + // these cases, but this is out of scope. A simpler one is to do a try / catch. + + if (domInData && !activeInDomain && activeInData) { + domain = domain.concat([['active', '=', true]]); + } else if (!domInData && !activeInDomain && activeInData) { + domain = [['active', '=', true]]; + } + try { + var visible = new Domain(domain).compute(data.evalContext); + } catch (e) { + return; + } + if (!visible) { + kanban_record.destroy(); + } }); }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 15d6284cfad..411d4cc7b14 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -419,7 +419,10 @@ var KanbanModel = BasicModel.extend({ while (element) { if (element.progressBar) { return def.then(function (data) { - return self._load(element, {onlyGroups: true}).then(function () { + return self._load(element, { + keepEmptyGroups: true, + onlyGroups: true, + }).then(function () { return data; }); }); diff --git a/addons/web/static/src/less/datepicker.less b/addons/web/static/src/less/datepicker.less index 99c909efacc..fdf3a585b57 100644 --- a/addons/web/static/src/less/datepicker.less +++ b/addons/web/static/src/less/datepicker.less @@ -17,11 +17,12 @@ .o_datepicker_input { width: 100%; + cursor: pointer; } .o_datepicker_button { .o-position-absolute(2px, 4px); - cursor: pointer; + pointer-events: none; // let click events go through the underlying input &:after { .o-caret-down; } diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less index ee9f1d9556a..ff6fe9eb20b 100644 --- a/addons/web/static/src/less/fields.less +++ b/addons/web/static/src/less/fields.less @@ -185,7 +185,7 @@ display: inline-block; padding: 0; margin: 0; - vertical-align: middle; + vertical-align: baseline; > .o_priority_star { display: inline-block; font-size: 1.35em; diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 60a3671caa2..c3cbe2e3494 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -248,6 +248,10 @@ border-left-color: @gray-lighter; } } + + .caret { + margin-top: -2px; + } } .o-status-more { diff --git a/addons/web/static/src/less/kanban_column_progressbar.less b/addons/web/static/src/less/kanban_column_progressbar.less index 86fe0853de2..f4cffd6c097 100644 --- a/addons/web/static/src/less/kanban_column_progressbar.less +++ b/addons/web/static/src/less/kanban_column_progressbar.less @@ -91,6 +91,7 @@ margin-left: 3%; color: @headings-color; text-align: right; + white-space: nowrap; .o-transform-origin(right, center); &.o_kanban_grow { diff --git a/addons/web/static/src/less/kanban_dashboard.less b/addons/web/static/src/less/kanban_dashboard.less index adf67f31bf0..947b78739f7 100644 --- a/addons/web/static/src/less/kanban_dashboard.less +++ b/addons/web/static/src/less/kanban_dashboard.less @@ -223,6 +223,7 @@ } .o_favorite { + top: 3px; left: 0; right: auto; } diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index c6d81293fbf..73bd50e1966 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -37,12 +37,14 @@ .user-select(none); // Prevent unwanted selection while sorting &::after { - margin-left: 6px; - position: absolute; font-family: FontAwesome; content: "\f0d7"; opacity: 0; } + &:not(:empty)::after { + margin-left: 6px; + position: absolute; + } &.o-sort-up { cursor: n-resize; &::after { |
