diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-05-06 12:27:51 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-05-06 12:27:51 +0200 |
| commit | a97037c6d442f186a798322a6eff586a3a118ef3 (patch) | |
| tree | 930cf70045eb8ec16323ccab3c4f6023aedb83bb /addons/web/static | |
| parent | e5fc29564532e3c11a48b8c98a69675580d2c572 (diff) | |
| parent | 78b3b650b0ddcb5fb53fda505e9e61510a1364fc (diff) | |
[MERGE] forward port branch saas-12.2 up to 78b3b650b0d
Diffstat (limited to 'addons/web/static')
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 15 | ||||
| -rw-r--r-- | addons/web/static/src/js/public/public_root.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/tools/debug_manager.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_model.js | 13 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/calendar/calendar_model.js | 3 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/date_picker.js | 11 | ||||
| -rw-r--r-- | addons/web/static/src/scss/bootstrap_overridden.scss | 2 | ||||
| -rw-r--r-- | addons/web/static/src/scss/fonts.scss | 30 | ||||
| -rw-r--r-- | addons/web/static/src/scss/layout_clean.scss | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/views/calendar_tests.js | 63 | ||||
| -rw-r--r-- | addons/web/static/tests/views/kanban_model_tests.js | 64 | ||||
| -rw-r--r-- | addons/web/static/tests/views/list_tests.js | 44 | ||||
| -rw-r--r-- | addons/web/static/tests/views/search_view_tests.js | 31 |
13 files changed, 272 insertions, 12 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 5919f100bcc..e546eb02be0 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -802,7 +802,7 @@ var FieldBoolean = AbstractField.extend({ * @returns {jQuery} the focusable checkbox input */ getFocusableElement: function () { - return this.$input || $(); + return this.mode === 'readonly' ? $() : this.$input; }, /** * A boolean field is always set since false is a valid value. @@ -1032,7 +1032,13 @@ var FieldFloatToggle = AbstractField.extend({ * @returns {float} The current formatted value */ _getDisplayedValue: function () { - return parseFloat(this._formatValue(this.value)); + // this.value is a plain float + // Matches what is in Database + var usrFormatValue = this._formatValue(this.value); + // usrFormatValue is string + // contains a float represented in a user specific format + // the float is the fraction by [this.factor] of this.value + return field_utils.parse['float'](usrFormatValue); }, /** * Formats the HTML input tag for edit mode and stores selection status. @@ -1059,7 +1065,7 @@ var FieldFloatToggle = AbstractField.extend({ * one is not in the range, the next value of the closest one will be chosen. * * @private - * @returns {number} The next formatted value in the range + * @returns {number} The next value in the range */ _nextValue: function () { var range = this.nodeOptions.range; @@ -1088,7 +1094,8 @@ var FieldFloatToggle = AbstractField.extend({ if (this.mode === 'edit') { ev.stopPropagation(); // only stop propagation in edit mode var next_val = this._nextValue(); - this._setValue(next_val.toString()); // will be parsed in _setValue + next_val = field_utils.format['float'](next_val); + this._setValue(next_val); // will be parsed in _setValue } }, diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js index 5d0483ad886..31b39e97abb 100644 --- a/addons/web/static/src/js/public/public_root.js +++ b/addons/web/static/src/js/public/public_root.js @@ -294,8 +294,8 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, { var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit'); _.each($buttons, function (btn) { var $btn = $(btn); - $btn.attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text()); - $btn.button('loading'); + $btn.html('<i class="fa fa-spinner fa-spin"></i> ' + $btn.text()); + $btn.prop('disabled', true); }); }, /** diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js index 54e98bbb87e..e5eff8d7fc1 100644 --- a/addons/web/static/src/js/tools/debug_manager.js +++ b/addons/web/static/src/js/tools/debug_manager.js @@ -436,6 +436,7 @@ DebugManager.include({ var fields = state.fields; var fieldsInfo = state.fieldsInfo.form; var fieldNamesInView = state.getFieldNames(); + var fieldNamesOnlyOnView = ['message_attachment_count']; var fieldsValues = state.data; var modifierDatas = {}; _.each(fieldNamesInView, function (fieldName) { @@ -444,6 +445,7 @@ DebugManager.include({ }); }); this.fields = _.chain(fieldNamesInView) + .difference(fieldNamesOnlyOnView) .map(function (fieldName) { var modifierData = modifierDatas[fieldName]; var invisibleOrReadOnly; 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 804e6d6d1c9..2a22828b8d8 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -564,9 +564,16 @@ var BasicModel = AbstractModel.extend({ } } else if (field.type === 'one2many' || field.type === 'many2many') { if (options.raw) { - relDataPoint = this.localData[data[fieldName]]; - relDataPoint = this._applyX2ManyOperations(relDataPoint); - data[fieldName] = relDataPoint.res_ids; + if (typeof data[fieldName] === 'string') { + relDataPoint = this.localData[data[fieldName]]; + relDataPoint = this._applyX2ManyOperations(relDataPoint); + data[fieldName] = relDataPoint.res_ids; + } else { + // no datapoint has been created yet (because the loading of relational + // data has been batched, and hasn't started yet), so the value is still + // the list of ids in the relation + data[fieldName] = data[fieldName] || []; + } } else { data[fieldName] = this.get(data[fieldName]) || []; } 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 1bcf4fbb298..ba338a96e4d 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -60,10 +60,11 @@ return AbstractModel.extend({ end.add(-1, 'days'); } + var isDateEvent = this.fields[this.mapping.date_start].type === 'date'; // An "allDay" event without the "all_day" option is not considered // as a 24h day. It's just a part of the day (by default: 7h-19h). if (event.allDay) { - if (!this.mapping.all_day) { + if (!this.mapping.all_day && !isDateEvent) { if (event.r_start) { start.hours(event.r_start.hours()) .minutes(event.r_start.minutes()) diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 75c52ce81a7..3aeb1d2f6fb 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -258,6 +258,17 @@ var DateWidget = Widget.extend({ */ _onKeydown: function (ev) { if (ev.which === $.ui.keyCode.ESCAPE) { + this.__libInput++; + this.$el.datetimepicker('hide'); + this.__libInput--; + } + }, + /** + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ESCAPE) { if (this.__isOpen) { // we don't want any other effects than closing the datepicker, // like leaving the edition of a row in editable list view diff --git a/addons/web/static/src/scss/bootstrap_overridden.scss b/addons/web/static/src/scss/bootstrap_overridden.scss index c1f8cbb44d3..50fde1fa6c2 100644 --- a/addons/web/static/src/scss/bootstrap_overridden.scss +++ b/addons/web/static/src/scss/bootstrap_overridden.scss @@ -61,7 +61,7 @@ $component-active-bg: $gray-200 !default; // // Font, line-height, and color for body text, headings, and more. -$font-family-sans-serif: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif !default; +$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif !default; $font-size-base: $o-font-size-base !default; $line-height-base: $o-line-height-base !default; diff --git a/addons/web/static/src/scss/fonts.scss b/addons/web/static/src/scss/fonts.scss index bfabf4fe6fc..0a8793c9a18 100644 --- a/addons/web/static/src/scss/fonts.scss +++ b/addons/web/static/src/scss/fonts.scss @@ -4,6 +4,36 @@ $lato-font-path: '../fonts/lato'; @mixin lato-font($type, $weight, $style) { + // Cyrillic: U+0400-04FF, U+0500-052F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0400-04FF, U+0500-052F; + } + // Hebrew: U+0590-05FF, U+FB1D-FB4F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0590-05FF, U+FB1D-FB4F; + } + // Arabic: U+0600-06FF, U+0750-077F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF; + } @font-face { font-family: 'Lato'; src: url('#{$lato-font-path}/Lato-#{$type}-webfont.eot'); diff --git a/addons/web/static/src/scss/layout_clean.scss b/addons/web/static/src/scss/layout_clean.scss index 910abc763a4..0a8f4b065af 100644 --- a/addons/web/static/src/scss/layout_clean.scss +++ b/addons/web/static/src/scss/layout_clean.scss @@ -1,5 +1,5 @@ .o_clean_footer, .o_clean_header, .o_report_layout_clean { - font-family: 'Lato'; + font-family: 'Lato', 'Noto'; color: #000; } .o_clean_header { diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 01f431589fc..94200b95f5e 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -2322,6 +2322,69 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('timzeone does not affect calendar with date field', async function (assert) { + assert.expect(8); + + var calendar = await createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar date_start="start_date" mode="month">'+ + '<field name="name"/>'+ + '<field name="start_date"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + mockRPC: function (route, args) { + if (args.method === "create") { + assert.strictEqual(args.args[0].start_date, "2016-12-20 00:00:00"); + } + if (args.method === "write") { + assert.step(args.args[1].start_date); + } + return this._super(route, args); + }, + session: { + getTZOffset: function () { + return 120; // 2 hours timezone + }, + }, + }); + + // Create event (on 20 december) + var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)'); + testUtils.triggerMouseEvent($cell, "mousedown"); + testUtils.triggerMouseEvent($cell, "mouseup"); + await testUtils.nextTick(); + var $input = $('.modal-body input:first'); + await testUtils.fields.editInput($input, "An event"); + await testUtils.dom.click($('.modal button.btn:contains(Create)')); + await testUtils.nextTick(); + + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "12/20/2016") + + // Move event to another day (on 27 november) + testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').first() + ); + assert.verifySteps(["2016-11-27 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "11/27/2016") + + // Move event to last day (on 7 january) + await testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').last() + ); + assert.verifySteps(["2016-11-27 00:00:00", "2017-01-07 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017") + + calendar.destroy(); + }); + QUnit.test('form_view_id attribute works (for creating events)', async function (assert) { assert.expect(1); diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js index e69b08aa3a8..af27cadad88 100644 --- a/addons/web/static/tests/views/kanban_model_tests.js +++ b/addons/web/static/tests/views/kanban_model_tests.js @@ -302,6 +302,70 @@ QUnit.module('Views', { }) }); + + QUnit.test('call get (raw: true) before loading x2many data', function (assert) { + // Sometimes, get can be called on a datapoint that is currently being + // reloaded, and thus in a partially updated state (e.g. in a kanban + // view, the user interacts with the searchview, and before the view is + // fully reloaded, it clicks on CREATE). Ideally, this shouldn't happen, + // but with the sync API of get, we can't change that easily. So at most, + // we can ensure that it doesn't crash. Moreover, sensitive functions + // requesting the state for more precise information that, e.g., the + // count, can do that in the mutex to ensure that the state isn't + // currently being reloaded. + // In this test, we have a grouped kanban view with a one2many, whose + // relational data is loaded in batch, once for all groups. We call get + // when the search_read for the first group has returned, but not the + // second (and thus, the read of the one2many hasn't started yet). + // Note: this test can be removed as soon as search_reads are performed + // alongside read_group. + assert.expect(2); + + this.data.partner.records[1].product_ids = [37, 41]; + this.params.fieldsInfo = { + kanban: { + product_ids: { + fieldsInfo: { + default: { display_name: {}, color: {} }, + }, + relatedFields: this.data.product.fields, + viewType: 'default', + }, + }, + }; + this.params.viewType = 'kanban'; + this.params.groupedBy = ['foo']; + + var block; + var def = testUtils.makeTestPromise(); + var model = createModel({ + Model: KanbanModel, + data: this.data, + mockRPC: function (route) { + var result = this._super.apply(this, arguments); + if (route === '/web/dataset/search_read' && block) { + block = false; + return Promise.all([def]).then(_.constant(result)); + } + return result; + }, + }); + + model.load(this.params).then(function (handle) { + block = true; + model.reload(handle, {}); + + var state = model.get(handle, {raw: true}); + assert.strictEqual(state.count, 2); + + def.resolve(); + + state = model.get(handle, {raw: true}); + assert.strictEqual(state.count, 2); + }); + + model.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index f10771a8e66..80e293a5126 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -4683,6 +4683,50 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('readonly boolean in editable list is readonly', function (assert) { + assert.expect(6); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="foo"/>' + + '<field name="bar" attrs="{\'readonly\': [(\'foo\', \'!=\', \'yop\')]}"/>' + + '</tree>', + }); + + // clicking on disabled checkbox with active row does not work + var $disabledCell = list.$('.o_data_row:eq(1) .o_data_cell:last-child'); + testUtils.dom.click($disabledCell.prev()); + assert.containsOnce($disabledCell, ':disabled:checked'); + var $disabledLabel = $disabledCell.find('.custom-control-label'); + testUtils.dom.click($disabledLabel); + assert.containsOnce($disabledCell, ':checked', + "clicking disabled checkbox did not work" + ); + assert.ok( + $(document.activeElement).is('input[type="text"]'), + "disabled checkbox is not focused after click" + ); + + // clicking on enabled checkbox with active row toggles check mark + var $enabledCell = list.$('.o_data_row:eq(0) .o_data_cell:last-child'); + testUtils.dom.click($enabledCell.prev()); + assert.containsOnce($enabledCell, ':checked:not(:disabled)'); + var $enabledLabel = $enabledCell.find('.custom-control-label'); + testUtils.dom.click($enabledLabel); + assert.containsNone($enabledCell, ':checked', + "clicking enabled checkbox worked and unchecked it" + ); + assert.ok( + $(document.activeElement).is('input[type="checkbox"]'), + "enabled checkbox is focused after click" + ); + + list.destroy(); + }); + QUnit.test('grouped list with async widget', async function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index 5980ab9211b..64c7f351e6a 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -732,6 +732,37 @@ QUnit.module('Search View', { actionManager.destroy(); }); + QUnit.test('Filter with JSON-parsable domain works', function (assert) { + assert.expect(1); + + var domain = [['foo' ,'=', 'Gently Weeps']]; + var xml_domain = JSON.stringify(domain); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + assert.deepEqual(args.domain, domain, + 'A JSON parsable xml domain should be handled just like any other'); + } + return this._super.apply(this, arguments); + } + }); + + this.archs['partner,5,search'] = + '<search>'+ + '<filter string="Foo" name="gently_weeps" domain="' + _.escape(xml_domain) + '" />' + + '</search>'; + this.actions[0].search_view_id = [5, 'search']; + this.actions[0].context = {search_default_gently_weeps: true}; + + actionManager.doAction(1); + + actionManager.destroy(); + }); + QUnit.module('Favorites Menu'); QUnit.test('dynamic filters are saved dynamic', async function (assert) { |
