diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-26 16:39:30 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-26 16:39:30 +0100 |
| commit | ff9c9341d6ed4e9ab18a0aa02e79441999d658f1 (patch) | |
| tree | 47ffd01e071d3783cff6013cfc4b6c3d8e47a841 /addons/web/static | |
| parent | bc84fdf71b15ad419f8693861aea682ee751f85c (diff) | |
| parent | fcb972db9dc778eeaf7d6b6ba3638c78d93998ad (diff) | |
[MERGE] forward port branch saas-12.2 up to fcb972db9dc
Diffstat (limited to 'addons/web/static')
27 files changed, 403 insertions, 76 deletions
diff --git a/addons/web/static/lib/moment/locale/es.js b/addons/web/static/lib/moment/locale/es.js index eb9e0809dfe..6d1f5907968 100644 --- a/addons/web/static/lib/moment/locale/es.js +++ b/addons/web/static/lib/moment/locale/es.js @@ -8,21 +8,30 @@ typeof define === 'function' && define.amd ? define(['../moment'], factory) : factory(global.moment) }(this, (function (moment) { 'use strict'; +var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'), + monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); - -var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'); -var monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); +var monthsParse = [/^ene/i, /^feb/i, /^mar/i, /^abr/i, /^may/i, /^jun/i, /^jul/i, /^ago/i, /^sep/i, /^oct/i, /^nov/i, /^dic/i]; +var monthsRegex = /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i; var es = moment.defineLocale('es', { months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'), monthsShort : function (m, format) { - if (/-MMM-/.test(format)) { + if (!m) { + return monthsShortDot; + } else if (/-MMM-/.test(format)) { return monthsShort[m.month()]; } else { return monthsShortDot[m.month()]; } }, - monthsParseExact : true, + monthsRegex : monthsRegex, + monthsShortRegex : monthsRegex, + monthsStrictRegex : /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i, + monthsShortStrictRegex : /^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i, + monthsParse : monthsParse, + longMonthsParse : monthsParse, + shortMonthsParse : monthsParse, weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'), weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'), weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'), @@ -57,6 +66,7 @@ var es = moment.defineLocale('es', { future : 'en %s', past : 'hace %s', s : 'unos segundos', + ss : '%d segundos', m : 'un minuto', mm : '%d minutos', h : 'una hora', diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index bd794323995..60b80a7f439 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -273,8 +273,7 @@ var py = {}; var Special = '[:;.,`@]'; var Funny = group(Operator, Bracket, Special); - var ContStr = group("([uU])?'([^']*)'", '([uU])?"([^"]*)"'); - + var ContStr = group("([uU])?'([^\n'\\\\]*(?:\\\\.[^\n'\\\\]*)*)'", '([uU])?"([^\n"\\\\]*(?:\\\\.[^\n"\\\\]*)*)"'); var PseudoToken = Whitespace + group(Number, Funny, ContStr, Name); var number_pattern = new RegExp('^' + Number + '$'); @@ -282,9 +281,6 @@ var py = {}; var name_pattern = new RegExp('^' + Name + '$'); var strip = new RegExp('^' + Whitespace); return function tokenize(s) { - s = s - .replace(/\\\"/g, '\\u0022') // for double quote - .replace(/\\\'/g, '\\u0027'); // for single quote var max=s.length, tokens = [], start, end; // /g flag makes repeated exec() have memory var pseudoprog = new RegExp(PseudoToken, 'g'); @@ -313,9 +309,6 @@ var py = {}; } else if (string_pattern.test(token)) { var m = string_pattern.exec(token); var value = (m[3] !== undefined ? m[3] : m[5]); - value - .replace(/\\u0022/g, '"') - .replace(/\\u0027/g, "'"); tokens.push(create(symbols['(string)'], { unicode: !!(m[2] || m[4]), value: value diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js index 34dd24b2be6..ef828b5dd5c 100644 --- a/addons/web/static/src/js/core/abstract_storage_service.js +++ b/addons/web/static/src/js/core/abstract_storage_service.js @@ -13,6 +13,17 @@ var AbstractStorageService = AbstractService.extend({ // this abstraction storage: null, + /** + * @override + */ + destroy: function () { + // storage can be permanent or transient, destroy transient ones + if ((this.storage || {}).destroy) { + this.storage.destroy(); + } + this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index 9bcd66864e5..296ab5c3089 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -415,7 +415,15 @@ function _formatAST(ast, lbp) { case "(number)": return String(ast.value); case "(string)": - return "'" + ast.value + "'"; + // ast.value is a string that may contain a mix of single and double quotes. + // But we need to return a string that represents the string ast.value. + // We use for that JSON.stringify. It will return a string with quotes correctly + // escaped. For instance, for x = `""` JSON.stringify(x) is `"\"\""`. + // But this means that if we pass several times here, escapes will be + // done again and again, leading to a profusion of backslashes that we don't want. + // For fun consider y = `\\` and y = JSON.stringify(y). + // This is why we first deescape particular characters in ast.value. + return JSON.stringify(ast.value.replace(/(\\(['"\\]))/g,"\$2")); case "(constant)": return ast.value; case "(name)": diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index cf64de72eef..44e09933a25 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -303,7 +303,17 @@ var utils = { var d2 = Math.pow(10, decimals); var val = _t('kMGTPE'); var symbol = ''; - for (var i = val.length - 1 ; i > 0 ; i--) { + var numberMagnitude = number.toExponential().split('e')[1]; + // the case numberMagnitude >= 21 corresponds to a number + // better expressed in the scientific format. + if (numberMagnitude >= 21) { + // we do not use number.toExponential(decimals) because we want to + // avoid the possible useless O decimals: 1e.+24 prefered to 1.0e+24 + number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2; + // formatterCallback seems useless here. + return number + 'e' + numberMagnitude; + } + for (var i = val.length; i > 0 ; i--) { var s = Math.pow(10, i * 3); if (s <= number / Math.pow(10, minDigits - 1)) { number = Math.round(number * d2 / s) / d2; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index dcfdc982b7d..c6df3885a86 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2914,6 +2914,7 @@ var AceEditor = DebouncedField.extend({ if (this.aceEditor) { this.aceEditor.destroy(); } + this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- 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 f58fbc93c4f..e59dbfb1036 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 @@ -927,10 +927,21 @@ var ControlPanelModel = mvc.Model.extend({ } var sort = JSON.parse(favorite.sort); var orderedBy = sort.map(function (order) { - var orderTerms = order.split(' '); + var fieldName; + var asc; + var sqlNotation = order.split(' '); + if (sqlNotation.length > 1) { + // regex: \fieldName (asc|desc)?\ + fieldName = sqlNotation[0]; + asc = sqlNotation[1] === 'asc'; + } else { + // legacy notation -- regex: \-?fieldName\ + fieldName = order[0] === '-' ? order.slice(1) : order; + asc = order[0] === '-' ? false : true; + } return { - name: orderTerms[0], - asc: orderTerms.length === 2 && orderTerms[1] === 'asc', + name: fieldName, + asc: asc, }; }); return { diff --git a/addons/web/static/src/js/views/control_panel/search/filter_menu.js b/addons/web/static/src/js/views/control_panel/search/filter_menu.js index 70a242dd0f8..04f333b37c4 100644 --- a/addons/web/static/src/js/views/control_panel/search/filter_menu.js +++ b/addons/web/static/src/js/views/control_panel/search/filter_menu.js @@ -89,8 +89,6 @@ var FilterMenu = DropdownMenu.extend({ domain: Domain.prototype.arrayToString(preFilter.attrs.domain), }; }); - // TO DO intercepts 'new_filters' and decide what to do whith filters - // rewrite web.search_filters? this.trigger_up('new_filters', {filters: filters}); _.invoke(this.propositions, 'destroy'); this.propositions = []; diff --git a/addons/web/static/src/js/views/control_panel/search/search_filters.js b/addons/web/static/src/js/views/control_panel/search/search_filters.js index 4703995ac7c..80a9019c02d 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_filters.js +++ b/addons/web/static/src/js/views/control_panel/search/search_filters.js @@ -171,15 +171,6 @@ var Char = Field.extend({ {value: "∃", text: _lt("is set")}, {value: "∄", text: _lt("is not set")} ], - get_domain: function (field, operator) { - switch (operator.value) { - case '∃': return [[field.name, '!=', false]]; - case '∄': return [[field.name, '=', false]]; - // does not work in all cases - // default: return "[('" + field.name + "', '" + operator.value + "', '" + this.get_value() + "')]"; - default: return [[field.name, operator.value, this.get_value()]]; - } - }, get_value: function () { return this.$el.val(); } @@ -251,7 +242,7 @@ var DateTime = Field.extend({ _create_new_widget: function (name) { this[name] = new (this._get_widget_class())(this); return this[name].appendTo(this.$el).then((function () { - this[name].setValue(moment(new Date())); + this[name].setValue(moment()); }).bind(this)); }, _get_widget_class: function () { 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 549fa56eefa..9353301ca09 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -21,6 +21,7 @@ var CHART_TYPES = ['pie', 'bar', 'line']; // hide top legend when too many items for device size var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class)); +var SPLIT_THRESHOLD = config.device.isMobile ? Infinity : 20; return AbstractRenderer.extend({ className: "o_graph_renderer", @@ -456,7 +457,11 @@ return AbstractRenderer.extend({ } } - var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'}); + var $svgContainer = $('<div/>', { class: 'o_graph_svg_container'}); + // Split the tooltip into columns for large data because some portion goes out off the screen. + if (data.length >= SPLIT_THRESHOLD) { + $svgContainer.addClass('o_tooltip_split_in_columns'); + } this.$el.append($svgContainer); var svg = d3.select($svgContainer[0]).append('svg'); svg.datum(data); 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 4c37b823639..3f195c1ea24 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -237,7 +237,7 @@ var PivotModel = AbstractModel.extend({ [ this.data.timeRangeDescription.toString(), this.data.comparisonTimeRangeDescription.toString(), - 'Variation' + _t('Variation') ], makeMeasure ) @@ -443,8 +443,10 @@ var PivotModel = AbstractModel.extend({ self._updateMainGroupBys(old_row, self.data.main_row); } - self._updateTree(old_col.root, self.data.main_col.root); - self._updateMainGroupBys(old_col, self.data.main_col); + if (!('pivot_column_groupby' in (params.context || {}))) { + self._updateTree(old_col.root, self.data.main_col.root); + self._updateMainGroupBys(old_col, self.data.main_col); + } }); }, /** 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 813dc5d38b3..1a3c451f064 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -7,6 +7,7 @@ var field_utils = require('web.field_utils'); var config = require('web.config'); var QWeb = core.qweb; +var _t = core._t; var PivotRenderer = AbstractRenderer.extend({ tagName: 'table', @@ -173,7 +174,7 @@ var PivotRenderer = AbstractRenderer.extend({ $thead.append($row); } if (this.compare) { - var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, 'Variation']; + var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, _t('Variation')]; var dataTypes = ['data', 'comparisonData', 'variation']; $row = $('<tr>'); for (i = 0; i < 3 * nbrCols; i++) { diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index 5642e656dea..0645e00544e 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -744,6 +744,10 @@ .o_field_widget { width: 50%; flex: 0 0 auto; + + &.o_field_many2manytags > .o_field_widget { + flex: 1 0 50px; + } } button.btn-link:first-child { padding-left: 0; diff --git a/addons/web/static/src/scss/graph_view.scss b/addons/web/static/src/scss/graph_view.scss index 71ec5fbca9c..e67edbd3cb5 100644 --- a/addons/web/static/src/scss/graph_view.scss +++ b/addons/web/static/src/scss/graph_view.scss @@ -24,6 +24,35 @@ // ligther area color fill-opacity: 0.1; } + .nv-lineChart .nv-x.nv-axis .nv-axisMaxMin-x { + &.nv-axisMin-x text { + text-anchor: start !important; + } + &.nv-axisMax-x text { + text-anchor: end !important; + } + } + } + + /* Use less than 50% of the screen area for tooltip and show legends in 3 column of 33% each */ + &.o_tooltip_split_in_columns .nvtooltip { + max-width: 48%; + tbody { + display: flex; + flex-flow: wrap; + tr { + width: 33%; + td { + display: inline-block; + &.key { + min-width: 160px; + max-width: 70%; + overflow-x: hidden; + text-overflow: ellipsis; + } + } + } + } } } } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 43d8f6900e8..8126fcf11c4 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -347,7 +347,7 @@ <t t-raw="item.label"/> </a> </t> - <div class="dropdown-menu" role="menu"> + <div class="dropdown-menu o_dropdown_menu" role="menu"> <t t-foreach="widget.items[section.name]" t-as="item"> <t t-if="section.name == 'files'"> <t t-set="item.title"> diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 291084402a9..4b8a3a79751 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -3133,7 +3133,7 @@ QUnit.module('ActionManager', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, "[('bar', '=', 1)]", + assert.deepEqual(filter.domain, `[("bar", "=", 1)]`, "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/chrome/user_menu_tests.js b/addons/web/static/tests/chrome/user_menu_tests.js index 600ac125dd4..0151afdf6d6 100644 --- a/addons/web/static/tests/chrome/user_menu_tests.js +++ b/addons/web/static/tests/chrome/user_menu_tests.js @@ -25,6 +25,7 @@ QUnit.module('chrome', {}, function () { "should have a 'Shortcuts' item"); userMenu.destroy(); + parent.destroy(); }); }); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index be5760fc6c7..fba8017cf01 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -1149,7 +1149,7 @@ QUnit.module('core', function () { assert.checkAST("1.4", "float value"); assert.checkAST("-12", "negative integer value"); assert.checkAST("True", "boolean"); - assert.checkAST("'some string'", "a string"); + assert.checkAST(`"some string"`, "a string"); assert.checkAST("None", "None"); }); @@ -1157,8 +1157,8 @@ QUnit.module('core', function () { assert.expect(3); assert.checkAST("{}", "empty dictionary"); - assert.checkAST("{'a': 1}", "dictionary with a single key"); - assert.checkAST("d['a']", "get a value in a dictionary"); + assert.checkAST(`{"a": 1}`, "dictionary with a single key"); + assert.checkAST(`d["a"]`, "get a value in a dictionary"); }); QUnit.test("list", function (assert) { @@ -1225,14 +1225,14 @@ QUnit.module('core', function () { QUnit.test("strftime", function (assert) { assert.expect(3); - assert.checkAST("time.strftime('%Y')", "strftime with year"); - assert.checkAST("time.strftime('%Y') + '-01-30'", "strftime with year"); - assert.checkAST("time.strftime('%Y-%m-%d %H:%M:%S')", "strftime with year"); + assert.checkAST(`time.strftime("%Y")`, "strftime with year"); + assert.checkAST(`time.strftime("%Y") + "-01-30"`, "strftime with year"); + assert.checkAST(`time.strftime("%Y-%m-%d %H:%M:%S")`, "strftime with year"); }); QUnit.test("context_today", function (assert) { assert.expect(1); - assert.checkAST("context_today().strftime('%Y-%m-%d')", "context today call"); + assert.checkAST(`context_today().strftime("%Y-%m-%d")`, "context today call"); }); @@ -1250,17 +1250,10 @@ QUnit.module('core', function () { assert.checkAST('(a - b).days', "substraction and .days"); assert.checkAST('a + day == date(2002, 3, 3)'); - var expr = "[('type', '=', 'in'), ('day', '<=', time.strftime('%Y-%m-%d')), ('day', '>', (context_today() - datetime.timedelta(days = 15)).strftime('%Y-%m-%d'))]"; + var expr = `[("type", "=", "in"), ("day", "<=", time.strftime("%Y-%m-%d")), ("day", ">", (context_today() - datetime.timedelta(days = 15)).strftime("%Y-%m-%d"))]`; assert.checkAST(expr); }); - QUnit.test("parse escaped quoted strings", function (assert) { - assert.expect(2); - - assert.checkAST(`'\"'`, "a string containing \""); - assert.checkAST(`'\'`, "a string containing \'"); - }); - QUnit.module('pyutils (_normalizeDomain)'); QUnit.assert.checkNormalization = function (domain, normalizedDomain) { @@ -1278,23 +1271,23 @@ QUnit.module('core', function () { assert.expect(3); assert.checkNormalization("[]"); - assert.checkNormalization("[('a', '=', 1)]"); - assert.checkNormalization("['!', ('a', '=', 1)]"); + assert.checkNormalization(`[("a", "=", 1)]`); + assert.checkNormalization(`["!", ("a", "=", 1)]`); }); QUnit.test("properly add the & in a non normalized domain", function (assert) { assert.expect(1); assert.checkNormalization( - "[('a', '=', 1), ('b', '=', 2)]", - "['&', ('a', '=', 1), ('b', '=', 2)]" + `[("a", "=", 1), ("b", "=", 2)]`, + `["&", ("a", "=", 1), ("b", "=", 2)]` ); }); QUnit.test("normalize domain with ! operator", function (assert) { assert.expect(1); assert.checkNormalization( - "['!', ('a', '=', 1), ('b', '=', 2)]", - "['&', '!', ('a', '=', 1), ('b', '=', 2)]" + `["!", ("a", "=", 1), ("b", "=", 2)]`, + `["&", "!", ("a", "=", 1), ("b", "=", 2)]` ); }); diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js index 3dd0a54b5e3..368f36c2fc6 100644 --- a/addons/web/static/tests/core/util_tests.js +++ b/addons/web/static/tests/core/util_tests.js @@ -67,6 +67,26 @@ QUnit.module('core', {}, function () { assert.strictEqual("MzGzHz", caseSensetiveCharacters); }); + QUnit.test('human_number', function (assert) { + assert.expect(13); + + var human_number = utils.human_number; + + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1020000, 2, 2), '1020k'); + assert.strictEqual(human_number(10200000, 2, 2), '10.2M'); + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1002, 2, 1), '1k'); + assert.strictEqual(human_number(101, 2, 1), '101'); + assert.strictEqual(human_number(64.2, 2, 1), '64'); + assert.strictEqual(human_number(1e+18), '1E'); + assert.strictEqual(human_number(1e+21, 2, 1), '1e+21'); + assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22'); + assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22'); + assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43'); + assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43'); + }); + }); }); diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js index 834033a1769..0eb4192c4a1 100644 --- a/addons/web/static/tests/core/widget_tests.js +++ b/addons/web/static/tests/core/widget_tests.js @@ -24,6 +24,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy('exec'); fn(); assert.ok(w.executed, 'should execute the named method in the right context'); + w.destroy(); }); QUnit.test('proxy (String)(*args)', function (assert) { @@ -39,6 +40,7 @@ QUnit.module('core', {}, function () { fn(42); assert.ok(w.executed, "should execute the named method in the right context"); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); QUnit.test('proxy (String), include', function (assert) { @@ -59,6 +61,7 @@ QUnit.module('core', {}, function () { fn(); assert.strictEqual(w.executed, 2, "should be lazily resolved"); + w.destroy(); }); QUnit.test('proxy (Function)', function (assert) { @@ -69,6 +72,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function () { this.executed = true; }); fn(); assert.ok(w.executed, "should set the function's context (like Function#bind)"); + w.destroy(); }); QUnit.test('proxy (Function)(*args)', function (assert) { @@ -79,6 +83,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); @@ -99,6 +104,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element"); assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute"); assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content")); + widget.destroy(); }); QUnit.test('no template, custom tag', function (assert) { @@ -111,6 +117,7 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual(widget.el.nodeName, 'UL', "should have generated the custom element tag"); + widget.destroy(); }); QUnit.test('no template, @id', function (assert) { @@ -124,6 +131,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.attributes.length, 1, "should have one attribute"); assert.hasAttrValue(widget.$el, 'id', 'foo', "should have generated the id attribute"); assert.strictEqual(widget.el.id, 'foo', "should also be available via property"); + widget.destroy(); }); QUnit.test('no template, @className', function (assert) { @@ -136,6 +144,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.className, 'oe_some_class', "should have the right property"); assert.hasAttrValue(widget.$el, 'class', 'oe_some_class', "should have the right attribute"); + widget.destroy(); }); QUnit.test('no template, bunch of attributes', function (assert) { @@ -166,6 +175,7 @@ QUnit.module('core', {}, function () { assert.hasAttrValue(widget.$el, 'clark', 'gable'); assert.hasAttrValue(widget.$el, 'spoiler', 'snape kills dumbledore'); + widget.destroy(); }); QUnit.test('template', function (assert) { @@ -193,6 +203,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'OL'); assert.strictEqual(widget.$el.children().length, 5); assert.strictEqual(widget.el.textContent, '01234'); + widget.destroy(); }); QUnit.test('repeated', function (assert) { @@ -219,6 +230,8 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual($fix.find('p').text(), '36', "DOM fixture should use new value"); assert.strictEqual(widget.$el.text(), '36', "should set new value"); + }).always(function () { + widget.destroy(); }); }); @@ -257,6 +270,7 @@ QUnit.module('core', {}, function () { assert.ok(widget.$('li:eq(3)').is(widget.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); + widget.destroy(); }); @@ -285,6 +299,7 @@ QUnit.module('core', {}, function () { for(var i=0; i<3; ++i) { assert.ok(a[i], "should pass test " + i); } + widget.destroy(); }); QUnit.test('undelegate', function (assert) { @@ -310,6 +325,7 @@ QUnit.module('core', {}, function () { widget.$('li').click(); assert.ok(!clicked, "undelegate should unbind events delegated"); assert.ok(newclicked, "undelegate should only unbind events it created"); + widget.destroy(); }); QUnit.module('Widget, and async stuff'); @@ -321,7 +337,8 @@ QUnit.module('core', {}, function () { return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen()); }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead)", function (assert) { @@ -355,7 +372,8 @@ QUnit.module('core', {}, function () { var widget = new (Widget.extend({})); return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen(), true) }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead, true)", function (assert) { @@ -445,4 +463,4 @@ QUnit.module('core', {}, function () { }); -});
\ No newline at end of file +}); diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 27dbaeeaa44..2ecc655db7e 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -199,6 +199,11 @@ function addMockEnvironment(widget, params) { // widget is destroyed, at the end of each test to avoid collisions core.bus.trigger('clear_cache'); + _(services).chain() + .compact() // services can be defined but null (e.g. ajax) + .reject(function (s) { return s.isDestroyed(); }) + .invoke('destroy'); + DebouncedField.prototype.DEBOUNCE = initialDebounceValue; dom.DEBOUNCE = initialDOMDebounceValue; if (params.debounce === false) { diff --git a/addons/web/static/tests/views/abstract_controller_tests.js b/addons/web/static/tests/views/abstract_controller_tests.js index 73bc41d650b..f9045645bc6 100644 --- a/addons/web/static/tests/views/abstract_controller_tests.js +++ b/addons/web/static/tests/views/abstract_controller_tests.js @@ -80,6 +80,8 @@ odoo.define("base.abstract_controller_tests", function(require) { testUtils.dom.click(view.$(".descr")); testUtils.dom.click(view.$(".descr2")); assert.verifySteps(["a1", "a2", "method", "method", "descr", "descr2"]); + + view.destroy(); } ); } diff --git a/addons/web/static/tests/views/control_panel_tests.js b/addons/web/static/tests/views/control_panel_tests.js index ee191e4d3a9..f2654e55e1b 100644 --- a/addons/web/static/tests/views/control_panel_tests.js +++ b/addons/web/static/tests/views/control_panel_tests.js @@ -415,5 +415,86 @@ QUnit.module('Views', { controlPanel.destroy(); }); + + QUnit.test('load filter', async function (assert) { + assert.expect(1); + + var controlPanel = await createControlPanel({ + model: 'partner', + arch: "<search></search>", + data: this.data, + searchMenuTypes: ['filter'], + intercepts: { + load_filters: function (ev) { + ev.data.on_success([ + { + user_id: [2,"Mitchell Admin"], + name: 'sorted filter', + id: 5, + context: {}, + sort: "[\"foo\", \"-bar\"]", + domain: "[('user_id', '=', uid)]", + } + ]); + } + } + }); + + _.each(controlPanel.exportState().filters, function (filter) { + if (filter.type === 'favorite') { + assert.deepEqual(filter.orderedBy, + [{ + name: 'foo', + asc: true, + }, { + name: 'bar', + asc: false, + }], + 'the filter should have the right orderedBy values'); + } + }); + + controlPanel.destroy(); + }); + + QUnit.test('save filter', async function (assert) { + assert.expect(1); + + var controlPanel = await createControlPanel({ + model: 'partner', + arch: "<search></search>", + data: this.data, + searchMenuTypes: ['filter'], + intercepts: { + create_filter: function (ev) { + assert.strictEqual(ev.data.filter.sort, "[\"foo\",\"bar desc\"]", + 'The right format for the string "sort" should be sent to the server'); + }, + get_controller_query_params: function (ev) { + ev.data.callback({ + orderedBy: [ + { + name: 'foo', + asc: true, + }, { + name: 'bar', + asc: false, + } + ] + }); + } + } + }); + + controlPanel._onNewFavorite({ + data: { + description: 'Morbier', + type: 'favorite', + }, + stopPropagation: function () {return;} + }); + + controlPanel.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 7af2892ac33..0e7e7ff0a2d 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -1007,6 +1007,95 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('Unload Filter, reset display, load another filter', function (assert) { + assert.expect(18); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + // Equivalent to unload the filter + var reloadParams = { + context: {}, + }; + pivot.reload(reloadParams); + // collapse all headers + pivot.$('.o_pivot_header_cell_opened').click(); + pivot.$('.o_pivot_header_cell_opened').click(); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed').length, 1, + 'The column should not be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 0, + 'There should not be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 0, + 'There should not be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed').length, 1, + 'The row should not be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 0, + 'There should not be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 0, + 'There should not be a row "xpad"'); + + // Equivalent to load another filter + reloadParams = { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }; + pivot.reload(reloadParams); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); @@ -1441,7 +1530,7 @@ QUnit.module('Views', { '<field name="foo" type="measure"/>' + '</pivot>', 'partner,false,search': '<search></search>', - }, + } }); actionManager.doAction({ @@ -1512,6 +1601,12 @@ QUnit.module('Views', { ]; checkCellValues(results); + $('.o_search_options button:contains("Favorites")').click(); + var $favorites = $('.dropdown-menu.o_favorites_menu'); + $favorites.find('a.o_save_search').click(); + $favorites.find('.o_input').val('Fav').trigger('input'); + $favorites.find('button').click(); + unpatchDate(); actionManager.destroy(); }); diff --git a/addons/web/static/tests/views/search_filter_menu_tests.js b/addons/web/static/tests/views/search_filter_menu_tests.js index e907d70111e..92d49cc23ae 100644 --- a/addons/web/static/tests/views/search_filter_menu_tests.js +++ b/addons/web/static/tests/views/search_filter_menu_tests.js @@ -1,9 +1,10 @@ odoo.define('web.search_filters_menu_tests', function (require) { "use strict"; +var Domain = require('web.Domain'); var FilterMenu = require('web.FilterMenu'); +var pyUtils = require('web.py_utils'); var testUtils = require('web.test_utils'); -var Domain = require('web.Domain'); function createFilterMenu(filters, fields, params) { params = params || {}; @@ -134,33 +135,70 @@ QUnit.module('FilterMenu', { filterMenu.destroy(); }); - QUnit.skip('commit search with an extended proposition with field char does not cause a crash', function (assert) { - assert.expect(0); + QUnit.test('commit search with an extended proposition with field char does not cause a crash', function (assert) { + assert.expect(6); this.fields = {many2one_field: {string: "Trululu", type: "many2one", relation: 'partner', selectable: true, searchable: true}}; + var expectedDomains = [ + [['many2one_field','ilike', `a`]], + [['many2one_field','ilike', `"a"`]], + [['many2one_field','ilike', `'a'`]], + [['many2one_field','ilike', `'`]], + [['many2one_field','ilike', `"`]], + [['many2one_field','ilike', `\\`]], + ]; + var filterMenu = createFilterMenu([], this.fields, { intercepts: { new_filters: function (ev) { var filter = ev.data.filters[0]; - Domain.prototype.stringToArray(filter.domain); + // this step combine a tokenization/parsing followed by a string formatting + var domain = pyUtils.assembleDomains([filter.domain]); + domain = Domain.prototype.stringToArray(domain); + assert.deepEqual(domain, expectedDomains.shift()); }, } }); - // open menu dropdown and custom filter submenu, select trululu field and enter string "a", then click apply + // open menu dropdown and custom filter submenu, select trululu field and enter string `a`, then click apply testUtils.dom.click(filterMenu.$('span.fa-filter')); testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); - testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), "a"); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `a`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + + // open custom filter submenu, select trululu field and enter string `"a"`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `"a"`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + // open custom filter submenu, select trululu field and enter string `'a'`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `'a'`); testUtils.dom.click(filterMenu.$('.o_apply_filter')); + // open custom filter submenu, select trululu field and enter string `'`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `'`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); - // open custom filter submenu, select trululu field and enter string "'a'", then click apply + // open custom filter submenu, select trululu field and enter string `"`, then click apply testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); - testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), '"a"'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `"`); testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + // open custom filter submenu, select trululu field and enter string `\`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `\\`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + filterMenu.destroy(); }); }); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index 87484bf855d..4916834e1bc 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -750,10 +750,10 @@ QUnit.module('Search View', { create_filter: function (ev) { assert.equal( ev.data.filter.domain, - "['&', " + - "('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d')), " + - "('date_field', '<', (context_today() + relativedelta(days = 1)).strftime('%Y-%m-%d'))"+ - "]"); + `["&", ` + + `("date_field", ">=", (context_today() + relativedelta()).strftime("%Y-%m-%d")), ` + + `("date_field", "<", (context_today() + relativedelta(days = 1)).strftime("%Y-%m-%d"))`+ + `]`); }, }, }); @@ -841,7 +841,7 @@ QUnit.module('Search View', { data: this.data, intercepts: { create_filter: function (ev) { - assert.ok(ev.data.filter.domain === "[['foo', 'ilike', 'a']]"); + assert.ok(ev.data.filter.domain === `[["foo", "ilike", "a"]]`); }, }, }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 1df70b03acc..27275e7fd80 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -443,7 +443,7 @@ QUnit.module('Views', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, "[('bar', '=', True)]", + assert.deepEqual(filter.domain, `[("bar", "=", True)]`, "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); |
