diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-01-28 13:58:11 +0000 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-01-28 13:58:11 +0000 |
| commit | cf52a04979d7634908c2f0d8eb6a83b7dd614f64 (patch) | |
| tree | 2f8c25f3fab45268400c4ac6addfa15f1f2d37f0 /addons/web/static | |
| parent | cd1f57e9ded2e8ca81d7290e4b5b95a8776045c6 (diff) | |
| parent | d3b8422c9c44557256836bdc0da5480f553eec4d (diff) | |
[MERGE] forward port branch saas-12.1 up to d3b8422c9c4
closes odoo/odoo#30614
Diffstat (limited to 'addons/web/static')
25 files changed, 551 insertions, 144 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index ea8fdc24a18..7082f26e69d 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -205,6 +205,10 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi }); } } else { + // ignore Chrome video internal error: https://crbug.com/809574 + if (!error && message === 'ResizeObserver loop limit exceeded') { + return; + } var traceback = error ? error.stack : ''; crash_manager.show_error({ type: _t("Odoo Client Error"), diff --git a/addons/web/static/src/js/chrome/apps_menu.js b/addons/web/static/src/js/chrome/apps_menu.js index f6535ed764c..eb7faaa971b 100644 --- a/addons/web/static/src/js/chrome/apps_menu.js +++ b/addons/web/static/src/js/chrome/apps_menu.js @@ -41,6 +41,9 @@ var AppsMenu = Widget.extend({ * Open the first app in the list of apps */ openFirstApp: function () { + if (!this._apps.length) { + return + } var firstApp = this._apps[0]; this._openApp(firstApp); }, diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index bbdee83c4f9..ec99f4fa0f6 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -363,7 +363,7 @@ function formatPercentage(value, field, options) { if (options.humanReadable && options.humanReadable(value * 100)) { return result + "%"; } - return parseFloat(result) + "%"; + return (parseFloat(result) + "%").replace('.', _t.database.parameters.decimal_point); } /** * Returns a string representing the value of the selection. diff --git a/addons/web/static/src/js/fields/special_fields.js b/addons/web/static/src/js/fields/special_fields.js index 5057b9de33f..e9dcbc592a3 100644 --- a/addons/web/static/src/js/fields/special_fields.js +++ b/addons/web/static/src/js/fields/special_fields.js @@ -122,7 +122,8 @@ var FieldTimezoneMismatch = FieldSelection.extend({ }); var FieldReportLayout = relational_fields.FieldMany2One.extend({ - supportedFieldTypes: ['many2one', 'selection'], + // this widget is not generic, so we disable its studio use + // supportedFieldTypes: ['many2one', 'selection'], events: _.extend({}, relational_fields.FieldMany2One.prototype.events, { 'click img': '_onImgClicked', }), diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index e1da816c8e5..31aa178dc1b 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -22,6 +22,7 @@ var map_title ={ var CrashManager = core.Class.extend({ init: function() { this.active = true; + this.isConnected = true; }, enable: function () { this.active = true; @@ -29,8 +30,29 @@ var CrashManager = core.Class.extend({ disable: function () { this.active = false; }, - rpc_error: function(error) { + handleLostConnection: function () { var self = this; + if (!this.isConnected) { + // already handled, nothing to do. This can happen when several + // rpcs are done in parallel and fail because of a lost connection. + return; + } + this.isConnected = false; + var delay = 2000; + core.bus.trigger('connection_lost'); + + setTimeout(function checkConnection() { + ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () { + core.bus.trigger('connection_restored'); + self.isConnected = true; + }).fail(function () { + // exponential backoff, with some jitter + delay = (delay * 1.5) + 500*Math.random(); + setTimeout(checkConnection, delay); + }); + }, delay); + }, + rpc_error: function(error) { if (!this.active) { return; } @@ -38,16 +60,7 @@ var CrashManager = core.Class.extend({ return; } if (error.code === -32098) { - core.bus.trigger('connection_lost'); - this.connection_lost = true; - var timeinterval = setInterval(function() { - var options = {shadow: true}; - ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () { - clearInterval(timeinterval); - core.bus.trigger('connection_restored'); - self.connection_lost = false; - }); - }, 2000); + this.handleLostConnection(); return; } var handler = core.crash_registry.get(error.data.name, true); 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 7f578da077d..f26a9cbe737 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -38,6 +38,10 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { FieldManagerMixin.init.call(this, this.model); this.mode = params.mode || 'readonly'; this.handle = this.initialState.id; + // savingDef is used to ensure that we always wait for pending save + // operations to complete before checking if there are changes to + // discard when discardChanges is called + this.savingDef = $.when(); }, /** * @override @@ -102,14 +106,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { return true; }, /** - * Waits for the mutex to be unlocked and then calls _.discardChanges. + * Waits for the mutex to be unlocked and for changes to be saved, then + * calls _.discardChanges. * This ensures that the confirm dialog isn't displayed directly if there is * a pending 'write' rpc. * * @see _.discardChanges */ discardChanges: function (recordID, options) { - return this.mutex.exec(function () {}) + return $.when(this.mutex.getUnlockedDef(), this.savingDef) .then(this._discardChanges.bind(this, recordID || this.handle, options)); }, /** @@ -183,11 +188,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // mutex-protected as commitChanges function of x2m has to be aware of // all final changes made to a row. var self = this; - return this.mutex.getUnlockedDef().then(function () { + this.savingDef = this.mutex.getUnlockedDef().then(function () { return self.renderer.commitChanges(recordID || self.handle).then(function () { return self.mutex.exec(self._saveRecord.bind(self, recordID, options)); }); }); + return this.savingDef; }, /** * @override diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js index 67b1a4c64f8..809fb0608b8 100644 --- a/addons/web/static/src/js/views/control_panel/control_panel_model.js +++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js @@ -646,8 +646,18 @@ var ControlPanelModel = mvc.Model.extend({ */ _getFilterContext: function (filter) { var context = filter.context || {}; + + // for <field> nodes, a dynamic context (like context="{'field1': self}") + // should set {'field1': [value1, value2]} in the context + if (filter.type === 'field' && filter.attrs.context) { + context = pyUtils.eval('context', filter.attrs.context, { + self: _.map(filter.autoCompleteValues, function (autoCompleteValue) { + return autoCompleteValue.value; + }), + }); + } // the following code aims to restore this: - // https://github.com/odoo/odoo/blob/master/addons/web/static/src/js/views/search/search_inputs.js#L498 + // https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/js/views/search/search_inputs.js#L498 // this is required for the helpdesk tour to pass // this seems weird to only do that for m2o fields, but a test fails if // we do it for other fields (my guess being that the test should simply @@ -656,7 +666,7 @@ var ControlPanelModel = mvc.Model.extend({ if (this.fields[filter.attrs.name].type === 'many2one') { var value = filter.defaultValue; // the following if required to make the main_flow_tour pass (see - // https://github.com/odoo/odoo/blob/master/addons/web/static/src/js/views/search/search_inputs.js#L461) + // https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/js/views/search/search_inputs.js#L461) if (_.isArray(filter.defaultValue)) { value = filter.defaultValue[0]; } 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 343730cc7a4..bc61e92a89a 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -130,8 +130,10 @@ var FieldManagerMixin = { if ('offset' in data) { params.offset = data.offset; } - this.model.reload(data.id, params).then(function (db_id) { - data.on_success(self.model.get(db_id)); + this.mutex.exec(function () { + return self.model.reload(data.id, params).then(function (db_id) { + data.on_success(self.model.get(db_id)); + }); }); }, /** 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 38c2f02351e..856882a303b 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -215,6 +215,10 @@ return AbstractRenderer.extend({ margin: {left: 80, bottom: 100, top: 80, right: 0}, delay: 100, transition: 10, + controlLabels: { + 'grouped': _t('Grouped'), + 'stacked': _t('Stacked'), + }, showLegend: _.size(data) <= MAX_LEGEND_LENGTH, showXAxis: true, showYAxis: true, 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 15c804f5a8e..b51c32aa770 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -213,15 +213,17 @@ var KanbanController = BasicController.extend({ */ _onAddColumn: function (ev) { var self = this; - this.model.createGroup(ev.data.value, this.handle).then(function () { - var state = self.model.get(self.handle, {raw: true}); - var ids = _.pluck(state.data, 'res_id').filter(_.isNumber); - return self._resequenceColumns(ids); - }).then(function () { - return self.update({}, {reload: false}); - }).then(function () { - self._updateButtons(); - self.renderer.quickCreateToggleFold(); + this.mutex.exec(function () { + return self.model.createGroup(ev.data.value, self.handle).then(function () { + var state = self.model.get(self.handle, {raw: true}); + var ids = _.pluck(state.data, 'res_id').filter(_.isNumber); + return self._resequenceColumns(ids); + }).then(function () { + return self.update({}, {reload: false}); + }).then(function () { + self._updateButtons(); + self.renderer.quickCreateToggleFold(); + }); }); }, /** @@ -274,11 +276,16 @@ var KanbanController = BasicController.extend({ * @private */ _onButtonNew: function () { + var self = this; var state = this.model.get(this.handle, {raw: true}); var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state); if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) { - // Activate the quick create in the first column - this.renderer.addQuickCreate(); + // activate the quick create in the first column when the mutex is + // unlocked, to ensure that there is no pending re-rendering that + // would remove it (e.g. if we are currently adding a new column) + this.mutex.getUnlockedDef().then(function () { + self.renderer.addQuickCreate(); + }); } else if (this.on_create && this.on_create !== 'quick_create') { // Execute the given action this.do_action(this.on_create, { diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index b7f2dd62b28..8619448e793 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -143,7 +143,7 @@ KanbanRenderer.include({ onSuccess: function () { // update the columns and tabs positions (optionally with an animation) var updateFunc = animate ? 'animate' : 'css'; - self.$('.o_kanban_mobile_tab, .o_kanban_group').removeClass('o_current'); + self.$('.o_kanban_mobile_tab').removeClass('o_current'); _.each(self.widgets, function (column, index) { var columnID = column.id || column.db_id; var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); @@ -157,7 +157,6 @@ KanbanRenderer.include({ } else if (index === moveToIndex) { $column[updateFunc]({left: '0%'}); $tab[updateFunc]({left: '50%'}); - $column.addClass('o_current'); $tab.addClass('o_current'); } else if (index < moveToIndex) { $column.css({left: '-100%'}); 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 fd5429d7c3e..dfa7d272a49 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -71,15 +71,9 @@ var ListController = BasicController.extend({ // TODO: this method should be synchronous... var self = this; if (this.$('thead .o_list_record_selector input').prop('checked')) { - var searchData = this.searchView.build_search_data(); - var userContext = this.getSession().user_context; - var results = pyUtils.eval_domains_and_contexts({ - domains: searchData.domains, - contexts: [userContext].concat(searchData.contexts), - group_by_seq: searchData.groupbys || [] - }); + var searchQuery = this._controlPanel ? this._controlPanel.getSearchQuery() : {}; var record = self.model.get(self.handle, {raw: true}); - return $.when(record.getDomain().concat(results.domain || [])); + return $.when(record.getDomain().concat(searchQuery.domain || [])); } else { return $.Deferred().resolve(); } diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 5a6423fbe34..4c37b823639 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -315,13 +315,13 @@ var PivotModel = AbstractModel.extend({ if (!this.data.has_data) { return { has_data: false, - colGroupBys: this.data.main_col.groupbys, - rowGroupBys: this.data.main_row.groupbys, + colGroupBys: this.data.main_col.root.groupbys, + rowGroupBys: this.data.main_row.root.groupbys, measures: this.data.measures, }; } return { - colGroupBys: this.data.main_col.groupbys, + colGroupBys: this.data.main_col.root.groupbys, context: this.data.context, domain: this.data.domain, compare: this.data.compare, @@ -331,7 +331,7 @@ var PivotModel = AbstractModel.extend({ mainColWidth: this.data.main_col.width, measures: this.data.measures, rows: !isRaw && this._computeRows(), - rowGroupBys: this.data.main_row.groupbys, + rowGroupBys: this.data.main_row.root.groupbys, sortedColumn: this.data.sorted_column, }; }, @@ -432,22 +432,19 @@ var PivotModel = AbstractModel.extend({ return this._loadData(); } - var old_row_root = this.data.main_row.root; - var old_col_root = this.data.main_col.root; + var old_row = this.data.main_row; + var old_col = this.data.main_col; return this._loadData().then(function () { - var new_groupby_length; if (!('groupBy' in params) && !('pivot_row_groupby' in (params.context || {}))) { // we only update the row groupbys according to the old groupbys // if we don't have the key 'groupBy' in params. In that case, // we want to have the full open state for the groupbys. - self._updateTree(old_row_root, self.data.main_row.root); - new_groupby_length = self._getHeaderDepth(self.data.main_row.root) - 1; - self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length); + self._updateTree(old_row.root, self.data.main_row.root); + self._updateMainGroupBys(old_row, self.data.main_row); } - self._updateTree(old_col_root, self.data.main_col.root); - new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1; - self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length); + self._updateTree(old_col.root, self.data.main_col.root); + self._updateMainGroupBys(old_col, self.data.main_col); }); }, /** @@ -1052,9 +1049,6 @@ var PivotModel = AbstractModel.extend({ } } - this.data.main_row.groupbys = rowGroupBys; - this.data.main_col.groupbys = colGroupBys; - main_row_header.other_root = main_col_header; main_col_header.other_root = main_row_header; @@ -1124,6 +1118,19 @@ var PivotModel = AbstractModel.extend({ } }, /** + * Updates the groupBys that the main[row | col] holds + * + * @private + * @param {Object} old: The main abstract header before reload + * @param {Object} main: The main abstract header after reload + * + */ + _updateMainGroupBys: function (old, main) { + var new_groupby_length = this._getHeaderDepth(main.root) - 1; + var new_groupby_list = old.root.groupbys.slice(0, new_groupby_length); + main.root.groupbys = new_groupby_list; + }, + /** * @param {Object} old_tree * @param {Object} new_tree */ diff --git a/addons/web/static/src/js/widgets/pie_chart.js b/addons/web/static/src/js/widgets/pie_chart.js index 30944d69584..1054360b3fd 100644 --- a/addons/web/static/src/js/widgets/pie_chart.js +++ b/addons/web/static/src/js/widgets/pie_chart.js @@ -31,6 +31,7 @@ var PieChart = Widget.extend({ Domain.prototype.stringToArray(modifiers.domain || '[]')); var arch = qweb.render('web.PieChart', { modifiers: modifiers, + title: node.attrs.title || modifiers.title || modifiers.measure, }); this.subViewParams = { diff --git a/addons/web/static/src/scss/bootstrap_overridden.scss b/addons/web/static/src/scss/bootstrap_overridden.scss index b81146a811f..c1f8cbb44d3 100644 --- a/addons/web/static/src/scss/bootstrap_overridden.scss +++ b/addons/web/static/src/scss/bootstrap_overridden.scss @@ -66,11 +66,11 @@ $font-family-sans-serif: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif $font-size-base: $o-font-size-base !default; $line-height-base: $o-line-height-base !default; -$h1-font-size: $o-font-size-base * 2.0 !default; -$h2-font-size: $o-font-size-base * 1.5 !default; -$h3-font-size: $o-font-size-base * 1.3 !default; -$h4-font-size: $o-font-size-base * 1.2 !default; -$h5-font-size: $o-font-size-base * 1.1 !default; +$h1-font-size: $font-size-base * 2.0 !default; +$h2-font-size: $font-size-base * 1.5 !default; +$h3-font-size: $font-size-base * 1.3 !default; +$h4-font-size: $font-size-base * 1.2 !default; +$h5-font-size: $font-size-base * 1.1 !default; $headings-font-weight: bold !default; diff --git a/addons/web/static/src/scss/kanban_view_mobile.scss b/addons/web/static/src/scss/kanban_view_mobile.scss index 1c5a9eab531..b74ae8aaea5 100644 --- a/addons/web/static/src/scss/kanban_view_mobile.scss +++ b/addons/web/static/src/scss/kanban_view_mobile.scss @@ -36,9 +36,6 @@ margin-left: 0; // override the margin-left: -1px of the desktop mode border: none; - &.o_current { - position: initial; - } .o_kanban_header { display: none; } diff --git a/addons/web/static/src/xml/pie_chart.xml b/addons/web/static/src/xml/pie_chart.xml index 5c3411bc882..d45bef7af6f 100644 --- a/addons/web/static/src/xml/pie_chart.xml +++ b/addons/web/static/src/xml/pie_chart.xml @@ -1,7 +1,7 @@ <template> <t t-name="web.PieChart"> - <graph type="pie" t-att-title="modifiers.title || modifiers.measure"> + <graph type="pie" t-att-title="title"> <field t-att-name="modifiers.groupby.split(':')[0]" t-att-interval="modifiers.groupby.split(':')[1]"/> <field t-if="modifiers.measure" t-att-name="modifiers.measure" type="measure"/> </graph> diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js index 6b7cda13264..940f5674780 100644 --- a/addons/web/static/tests/fields/field_utils_tests.js +++ b/addons/web/static/tests/fields/field_utils_tests.js @@ -135,7 +135,9 @@ QUnit.test('format binary', function (assert) { }); QUnit.test('format percentage', function (assert) { - assert.expect(9); + assert.expect(11); + + var originalParameters = _.clone(core._t.database.parameters); assert.strictEqual(fieldUtils.format.percentage(0), '0%'); assert.strictEqual(fieldUtils.format.percentage(0.5), '50%'); @@ -151,6 +153,16 @@ QUnit.test('format percentage', function (assert) { assert.strictEqual(fieldUtils.format.percentage(50, null, {humanReadable: function (val) {return true;}}), '5k%' ); + + _.extend(core._t.database.parameters, { + grouping: [3, 0], + decimal_point: ',', + thousands_sep: '.' + }); + assert.strictEqual(fieldUtils.format.percentage(0.125), '12,5%'); + assert.strictEqual(fieldUtils.format.percentage(0.666666), '66,67%'); + + core._t.database.parameters = originalParameters; }); QUnit.test('parse float', function(assert) { @@ -262,7 +274,9 @@ QUnit.test('parse monetary', function(assert) { }); QUnit.test('parse percentage', function(assert) { - assert.expect(7); + assert.expect(9); + + var originalParameters = _.clone(core._t.database.parameters); assert.strictEqual(fieldUtils.parse.percentage(""), 0); assert.strictEqual(fieldUtils.parse.percentage("0"), 0); @@ -271,6 +285,18 @@ QUnit.test('parse percentage', function(assert) { assert.strictEqual(fieldUtils.parse.percentage("1"), 1); assert.strictEqual(fieldUtils.parse.percentage("2%"), 0.02); assert.strictEqual(fieldUtils.parse.percentage("100%"), 1); + + _.extend(core._t.database.parameters, { + grouping: [3, 0], + decimal_point: ',', + thousands_sep: '.' + }); + + assert.strictEqual(fieldUtils.parse.percentage("0,04"), 0.04); + assert.strictEqual(fieldUtils.parse.percentage("6,02%"), 0.0602); + + core._t.database.parameters = originalParameters; + }) }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 15999ca952e..463effad124 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -150,6 +150,61 @@ QUnit.module('relational_fields', { } }, function () { + QUnit.test('search more pager is reset when doing a new search', function (assert) { + assert.expect(6); + for(var i = 10; i < 180; i++) { + this.data.partner.records.push({ + id: i, + display_name: "Partner " + i, + }); + } + + function stringToEvent ($element, string) { + for (var i = 0; i < string.length; i++) { + var keyAscii = string.charCodeAt(i); + $element.val($element.val()+string[i]); + $element.trigger($.Event('keyup', {which: keyAscii, keyCode:keyAscii})); + } + $element.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER, keyCode:$.ui.keyCode.ENTER})); + } + + this.data.partner.fields.datetime.searchable = true; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="trululu"/>' + + '</group>' + + '</sheet>' + + '</form>', + archs: { + 'partner,false,list': '<tree><field name="display_name"/></tree>', + 'partner,false,search': '<search><field name="datetime"/><field name="display_name"/></search>', + }, + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); + form.$('.o_field_many2one input').click(); + $dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click(); + $('.modal .o_pager_next').click(); + + assert.strictEqual($('.o_pager_limit').text(), "1173", "there should be 173 records"); + assert.strictEqual($('.o_pager_value').text(), "181-160", "should display the second page"); + assert.strictEqual($('tr.o_data_row').length, 80, "should display 80 record"); + + stringToEvent($('.modal .o_searchview_input'), 'first'); + + assert.strictEqual($('.o_pager_limit').text(), "11", "there should be 1 record"); + assert.strictEqual($('.o_pager_value').text(), "11-1", "should display the first page"); + assert.strictEqual($('tr.o_data_row').length, 1, "should display 1 record"); + form.destroy(); + }); + QUnit.test('x2many default_order multiple fields', function (assert) { assert.expect(7); @@ -446,6 +501,48 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('quickly switch between pages in one2many list', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1, 2, 3]; + + var readDefs = [$.when(), $.Deferred(), $.Deferred()]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree limit="1">' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'read') { + var recordID = args.args[0][0]; + return $.when(readDefs[recordID - 1]).then(_.constant(result)); + } + return result; + }, + res_id: 1, + }); + + form.$('.o_field_widget[name=turtles] .o_pager_next').click(); + form.$('.o_field_widget[name=turtles] .o_pager_next').click(); + + readDefs[1].resolve(); + + assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'donatello'); + + readDefs[2].resolve(); + + assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'raphael'); + + form.destroy(); + }); + QUnit.test('many2many read, field context is properly sent', function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index e1cb3277dba..e7d425b997e 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1634,12 +1634,6 @@ QUnit.module('Views', { '</form>', viewOptions: {hasSidebar: true}, res_id: 1, - mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, }); assert.isVisible(form.sidebar); @@ -1782,12 +1776,6 @@ QUnit.module('Views', { '</form>', res_id: 1, viewOptions: {hasSidebar: true}, - mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, }); assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record', @@ -1822,9 +1810,6 @@ QUnit.module('Views', { assert.strictEqual(args.kwargs.context.hey, 'hoy', "should have send the correct context"); } - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } return this._super.apply(this, arguments); }, }); @@ -1847,12 +1832,6 @@ QUnit.module('Views', { '</form>', res_id: 1, viewOptions: {hasSidebar: true}, - mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, }); assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record', @@ -2394,12 +2373,6 @@ QUnit.module('Views', { arch: '<form string="Partners"><field name="foo"></field></form>', res_id: 1, viewOptions: {hasSidebar: true}, - mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, }); testUtils.form.clickEdit(form); testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); @@ -2736,12 +2709,6 @@ QUnit.module('Views', { hasSidebar: true, }, res_id: 1, - mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } - return this._super.apply(this, arguments); - }, }); assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1'); @@ -2781,9 +2748,6 @@ QUnit.module('Views', { }, res_id: 1, mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } assert.step(args.method); return this._super.apply(this, arguments); } @@ -4945,9 +4909,6 @@ QUnit.module('Views', { "the active_ids should be an array with 1 inside."); return $.when({}); } - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } return this._super.apply(this, arguments); }, }); @@ -6224,9 +6185,6 @@ QUnit.module('Views', { res_id: 1, viewOptions: {hasSidebar: true}, mockRPC: function (route, args) { - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } var result = this._super.apply(this, arguments); if (args.method === 'copy') { return result.then(function (id) { @@ -6878,9 +6836,6 @@ QUnit.module('Views', { if (args.method === 'unlink') { assert.step('unlink'); } - if (args.method === 'search_read' && args.model === 'ir.attachment') { - return $.when([]); - } return this._super.apply(this, arguments); }, res_id: 1, @@ -7189,6 +7144,69 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('call canBeRemoved while saving', function (assert) { + assert.expect(10); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.display_name = obj.foo === 'trigger onchange' ? 'changed' : 'default'; + }, + }; + + var onchangeDef; + var createDef = $.Deferred(); + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="display_name"/><field name="foo"/></form>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(onchangeDef).then(_.constant(result)); + } + if (args.method === 'create') { + return $.when(createDef).then(_.constant(result)); + } + return result; + }, + }); + + // edit foo to trigger a delayed onchange + onchangeDef = $.Deferred(); + testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange'); + + assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default'); + + // save (will wait for the onchange to return), and will be delayed as well + testUtils.dom.click(form.$buttons.find('.o_form_button_save')); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default'); + + // simulate a click on the breadcrumbs to leave the form view + form.canBeRemoved(); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default'); + + // unlock the onchange + onchangeDef.resolve(); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'changed'); + + // unlock the create + createDef.resolve(); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); + assert.strictEqual(form.$('.o_field_widget[name=display_name]').text(), 'changed'); + assert.containsNone(document.body, '.modal', + "should not display the 'Changes will be discarded' dialog"); + + form.destroy(); + }); + QUnit.module('FormViewTABMainButtons'); QUnit.test('using tab in an empty required string field should not move to the next field',function(assert) { diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js index a9eec223143..98e65d1ea9c 100644 --- a/addons/web/static/tests/views/kanban_mobile_tests.js +++ b/addons/web/static/tests/views/kanban_mobile_tests.js @@ -56,7 +56,7 @@ QUnit.module('Views', { QUnit.module('KanbanView Mobile'); QUnit.test('mobile grouped rendering', function (assert) { - assert.expect(11); + assert.expect(9); var kanban = createView({ View: KanbanView, @@ -75,8 +75,6 @@ QUnit.module('Views', { assert.containsN(kanban, '.o_kanban_group', 2, "should have 2 columns" ); assert.hasClass(kanban.$('.o_kanban_mobile_tab:first'),'o_current', "first tab is the active tab with class 'o_current'"); - assert.hasClass(kanban.$('.o_kanban_group:first'),'o_current', - "first column is the active column with class 'o_current'"); assert.containsN(kanban, '.o_kanban_group:first > div.o_kanban_record', 2, "there are 2 records in active tab"); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) > div.o_kanban_record').length, 0, @@ -91,8 +89,6 @@ QUnit.module('Views', { kanban.$('.o_kanban_mobile_tab:nth(1)').trigger('click'); assert.hasClass(kanban.$('.o_kanban_mobile_tab:nth(1)'),'o_current', "second tab is now active with class 'o_current'"); - assert.hasClass(kanban.$('.o_kanban_group:nth(1)'),'o_current', - "second column is now active with class 'o_current'"); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) > div.o_kanban_record').length, 2, "the 2 records of the second group have now been loaded"); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 6702a9bee37..47af910083d 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -2110,6 +2110,67 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('quick create record while adding a new column', function (assert) { + assert.expect(10); + + var def = $.Deferred(); + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['product_id'], + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'name_create' && args.model === 'product') { + return def.then(_.constant(result)); + } + return result; + }, + }); + + assert.containsN(kanban, '.o_kanban_group', 2); + assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2); + + // add a new column + assert.containsOnce(kanban, '.o_column_quick_create'); + assert.isNotVisible(kanban.$('.o_column_quick_create input')); + + testUtils.dom.click(kanban.$('.o_quick_create_folded')); + + assert.isVisible(kanban.$('.o_column_quick_create input')); + + testUtils.fields.editInput(kanban.$('.o_column_quick_create input'), 'new column'); + testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); + + assert.containsN(kanban, '.o_kanban_group', 2); + + // click to add a new record + testUtils.dom.click(kanban.$buttons.find('.o-kanban-button-new')); + + // should wait for the column to be created (and view to be re-rendered + // before opening the quick create + assert.containsNone(kanban, '.o_kanban_quick_create'); + + // unlock column creation + def.resolve(); + + assert.containsN(kanban, '.o_kanban_group', 3); + assert.containsOnce(kanban, '.o_kanban_quick_create'); + + // quick create record in first column + testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record'); + testUtils.dom.click(kanban.$('.o_kanban_quick_create .o_kanban_add')); + + assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3); + + kanban.destroy(); + }); + QUnit.test('many2many_tags in kanban views', function (assert) { assert.expect(12); @@ -2580,6 +2641,45 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('prevent drag and drop if grouped by date/datetime field', function (assert) { + assert.expect(5); + + this.data.partner.records[0].date = '2017-01-08'; + this.data.partner.records[1].date = '2017-01-09'; + this.data.partner.records[2].date = '2017-02-08'; + this.data.partner.records[3].date = '2017-02-10'; + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="bar"/>' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates></kanban>', + groupBy: ['date:month'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, + "1st column should contain 2 records of January month"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, + "2nd column should contain 2 records of February month"); + + // drag&drop a record in another column + var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); + var $group = kanban.$('.o_kanban_group:nth-child(2)'); + testUtils.dragAndDrop($record, $group); + + // should not drag&drop record + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 2, + "Should remain same records in first column(2 records)"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, + "Should remain same records in 2nd column(2 record)"); + kanban.destroy(); + }); + QUnit.test('kanban view with default_group_by', function (assert) { assert.expect(7); this.data.partner.records.product_id = 1; diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 930fadbbb7b..7af2892ac33 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -923,6 +923,90 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('correctly remove pivot_ keys from the context', function (assert) { + assert.expect(5); + + this.data.partner.fields.amount = {string: "Amount", type: "float"}; + + // Equivalent to loading with default filter + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="date" interval="day" type="col"/>' + + '<field name="amount" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + // Equivalent to unload the filter + var reloadParams = { + context: {}, + }; + pivot.reload(reloadParams); + + assert.deepEqual(pivot.getOwnedQueryParams(), { + context: { + pivot_column_groupby: ['customer'], + pivot_measures: ['foo'], + pivot_row_groupby: ['product_id'], + }, + }, "context should be correct"); + + // Let's get rid of the rows groupBy + pivot.$('tbody .o_pivot_header_cell_opened').click(); + + assert.deepEqual(pivot.getOwnedQueryParams(), { + context: { + pivot_column_groupby: ['customer'], + pivot_measures: ['foo'], + pivot_row_groupby: [], + }, + }, "context should be correct"); + + // And now, get rid of the col groupby + pivot.$('thead .o_pivot_header_cell_opened').click(); + + assert.deepEqual(pivot.getOwnedQueryParams(), { + context: { + pivot_column_groupby: [], + pivot_measures: ['foo'], + pivot_row_groupby: [], + }, + }, "context should be correct"); + + pivot.$('tbody .o_pivot_header_cell_closed').click(); + pivot.$('.o_pivot_field_menu .dropdown-item[data-field="product_id"]:first').click(); + + assert.deepEqual(pivot.getOwnedQueryParams(), { + context: { + pivot_column_groupby: [], + pivot_measures: ['foo'], + pivot_row_groupby: ['product_id'], + }, + }, "context should be correct"); + + pivot.$('thead .o_pivot_header_cell_closed').click(); + pivot.$('.o_pivot_field_menu .dropdown-item[data-field="customer"]:first').click(); + + assert.deepEqual(pivot.getOwnedQueryParams(), { + context: { + pivot_column_groupby: ['customer'], + pivot_measures: ['foo'], + pivot_row_groupby: ['product_id'], + }, + }, "context should be correct"); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index f4b0163ce07..0714a5157e5 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -113,6 +113,13 @@ QUnit.module('Search View', { type: 'ir.actions.act_window', views: [[false, 'pivot']], search_view_id: [8, 'search'], + }, { + id: 11, + name: 'Partners Action 11', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [8, 'search'], } ]; @@ -187,7 +194,7 @@ QUnit.module('Search View', { '<field name="foo"/>' + '<field name="date_field"/>' + '<field name="birthday"/>' + - '<field name="bar"/>' + + '<field name="bar" context="{\'bar\': self}"/>' + '<field name="float_field"/>' + '<filter string="Date Field Filter" name="positive" date="date_field"/>' + '<filter string="Date Field Groupby" name="coolName" context="{\'group_by\': \'date_field:day\'}"/>' + @@ -992,6 +999,59 @@ QUnit.module('Search View', { actionManager.destroy(); }); + QUnit.test('select an autocomplete field with `context` key', function (assert) { + assert.expect(9); + + var searchRead = 0; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + debug: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + if (searchRead === 1) { + assert.deepEqual(args.domain, [["bar", "=", 1]]); + assert.deepEqual(args.context, {'bar': [1]}); + } else if (searchRead === 2) { + assert.deepEqual(args.domain, ["|", ["bar", "=", 1], ["bar", "=", 2]]); + assert.deepEqual(args.context, { 'bar': [1, 2] }); + } + searchRead++; + } + return this._super.apply(this, arguments); + }, + }); + + actionManager.doAction(11); + assert.strictEqual(searchRead, 1, "there should be 1 search_read"); + + // 'r' key to filter on bar "First Record" + $('.o_searchview_input').trigger($.Event('keypress', { which: 82, keyCode: 82 })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER })); + + assert.strictEqual($('.o_searchview_input_container .o_facet_values').eq(0).text().trim(), "First record", + "the autocompletion facet should be correct"); + assert.strictEqual(searchRead, 2, "there should be 2 search_read"); + + // 'r' key to filter on bar "Second Record" + $('.o_searchview_input').trigger($.Event('keypress', { which: 82, keyCode: 82 })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN })); + $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER })); + + assert.strictEqual($('.o_searchview_input_container .o_facet_values').eq(0).text().trim(), "First record or Second record", + "the autocompletion facet should be correct"); + assert.strictEqual(searchRead, 3, "there should be 3 search_read"); + + actionManager.destroy(); + }); + QUnit.module('TimeRangeMenu'); QUnit.test('time range menu stays hidden', function (assert) { diff --git a/addons/web/static/tests/widgets/data_export_tests.js b/addons/web/static/tests/widgets/data_export_tests.js index 40b59157b02..51c82ef1ded 100644 --- a/addons/web/static/tests/widgets/data_export_tests.js +++ b/addons/web/static/tests/widgets/data_export_tests.js @@ -36,7 +36,7 @@ QUnit.module('widgets', { QUnit.test('exporting all data in list view', function (assert) { - assert.expect(7); + assert.expect(6); var blockUI = framework.blockUI; var unblockUI = framework.unblockUI; @@ -94,16 +94,6 @@ QUnit.module('widgets', { }, }); - list.searchView = { - build_search_data: function () { - assert.step('build_search_data'); - return { - contexts: [], - domains: [], - groupbys: [], - }; - }, - }; testUtils.dom.click(list.$('thead th.o_list_record_selector input')); testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); testUtils.dom.click(list.sidebar.$('a:contains(Export)')); @@ -122,7 +112,6 @@ QUnit.module('widgets', { framework.blockUI = blockUI; framework.unblockUI = unblockUI; assert.verifySteps([ - 'build_search_data', 'block UI', '/web/export/csv', 'unblock UI', @@ -130,7 +119,7 @@ QUnit.module('widgets', { }); QUnit.test('saving fields list when exporting data', function (assert) { - assert.expect(6); + assert.expect(5); var create = data.DataSet.prototype.create; @@ -172,17 +161,6 @@ QUnit.module('widgets', { }, }); - list.searchView = { - build_search_data: function () { - assert.step('build_search_data'); - return { - contexts: [], - domains: [], - groupbys: [], - }; - }, - }; - // Open the export modal testUtils.dom.click(list.$('thead th.o_list_record_selector input')); testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); @@ -202,7 +180,7 @@ QUnit.module('widgets', { testUtils.dom.click($('.modal a:contains(Save fields list)')); testUtils.fields.editInput($('.modal .o_save_list > input'), 'fields list'); testUtils.dom.click($('.modal .o_save_list > button')); - assert.verifySteps(['build_search_data', 'create'], + assert.verifySteps(['create'], "create should have been called"); // Close the modal and destroy list |
