summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-05-06 12:27:51 +0200
committerChristophe Simonis <chs@odoo.com>2019-05-06 12:27:51 +0200
commita97037c6d442f186a798322a6eff586a3a118ef3 (patch)
tree930cf70045eb8ec16323ccab3c4f6023aedb83bb /addons/web/static
parente5fc29564532e3c11a48b8c98a69675580d2c572 (diff)
parent78b3b650b0ddcb5fb53fda505e9e61510a1364fc (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.js15
-rw-r--r--addons/web/static/src/js/public/public_root.js4
-rw-r--r--addons/web/static/src/js/tools/debug_manager.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js13
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js3
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js11
-rw-r--r--addons/web/static/src/scss/bootstrap_overridden.scss2
-rw-r--r--addons/web/static/src/scss/fonts.scss30
-rw-r--r--addons/web/static/src/scss/layout_clean.scss2
-rw-r--r--addons/web/static/tests/views/calendar_tests.js63
-rw-r--r--addons/web/static/tests/views/kanban_model_tests.js64
-rw-r--r--addons/web/static/tests/views/list_tests.js44
-rw-r--r--addons/web/static/tests/views/search_view_tests.js31
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) {