summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-13 18:07:59 +0200
committerChristophe Simonis <chs@odoo.com>2019-09-13 18:07:59 +0200
commit6c948c36bbf72dd896e0bff03aa7fbddadcbb0cd (patch)
treef8620905c011735d8a3c340c46d1417a54a75972 /addons/web/static/src
parent48503fcf08eac6fed12e080d4f501f9bbc8d257d (diff)
parent197474e5b9ad43ab2d587a98136a1a809ce9a2e7 (diff)
[MERGE] forward port branch saas-12.5 up to 197474e5b9a
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/chrome/action_manager_report.js5
-rw-r--r--addons/web/static/src/js/core/abstract_storage_service.js13
-rw-r--r--addons/web/static/src/js/core/dialog.js72
-rw-r--r--addons/web/static/src/js/core/local_storage.js10
-rw-r--r--addons/web/static/src/js/core/ram_storage.js14
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js58
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js126
-rw-r--r--addons/web/static/src/js/fields/special_fields.js4
-rw-r--r--addons/web/static/src/js/public/public_crash_manager.js5
-rw-r--r--addons/web/static/src/js/services/crash_manager.js29
-rw-r--r--addons/web/static/src/js/tools/test_menus.js4
-rw-r--r--addons/web/static/src/js/views/abstract_view.js11
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js53
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js17
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_popover.js5
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js7
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_renderer.js2
-rw-r--r--addons/web/static/src/js/views/control_panel/search/groupby_menu.js2
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_bar.js8
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js46
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js6
-rw-r--r--addons/web/static/src/js/views/form/form_view.js8
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js3
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column_progressbar.js32
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js8
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js2
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js177
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js7
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js3
-rw-r--r--addons/web/static/src/js/views/search_panel.js10
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js26
-rw-r--r--addons/web/static/src/js/widgets/auto_complete.js12
-rw-r--r--addons/web/static/src/js/widgets/data_export.js6
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js5
-rw-r--r--addons/web/static/src/js/widgets/translation_dialog.js172
-rw-r--r--addons/web/static/src/scss/form_view.scss19
-rw-r--r--addons/web/static/src/scss/form_view_extra.scss5
-rw-r--r--addons/web/static/src/scss/kanban_column_progressbar.scss1
-rw-r--r--addons/web/static/src/scss/kanban_view.scss5
-rw-r--r--addons/web/static/src/scss/list_view.scss43
-rw-r--r--addons/web/static/src/scss/modal.scss4
-rw-r--r--addons/web/static/src/scss/search_panel.scss17
-rw-r--r--addons/web/static/src/scss/search_view.scss1
-rw-r--r--addons/web/static/src/scss/secondary_variables.scss10
-rw-r--r--addons/web/static/src/scss/translation_dialog.scss10
-rw-r--r--addons/web/static/src/scss/ui.scss8
-rw-r--r--addons/web/static/src/scss/web_calendar.scss34
-rw-r--r--addons/web/static/src/scss/webclient.scss3
-rw-r--r--addons/web/static/src/xml/kanban.xml4
-rw-r--r--addons/web/static/src/xml/translation_dialog.xml23
51 files changed, 862 insertions, 294 deletions
diff --git a/addons/web/static/src/js/chrome/action_manager_report.js b/addons/web/static/src/js/chrome/action_manager_report.js
index 416cad6a942..61dbdbbcc96 100644
--- a/addons/web/static/src/js/chrome/action_manager_report.js
+++ b/addons/web/static/src/js/chrome/action_manager_report.js
@@ -51,10 +51,11 @@ ActionManager.include({
url: '/report/download',
data: {
data: JSON.stringify([url, type]),
+ context: JSON.stringify(session.user_context),
},
success: resolve,
- error: () => {
- this.call('crash_manager', 'rpc_error', ...arguments);
+ error: (error) => {
+ self.call('crash_manager', 'rpc_error', error);
reject();
},
complete: framework.unblockUI,
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 ef828b5dd5c..5d724f87911 100644
--- a/addons/web/static/src/js/core/abstract_storage_service.js
+++ b/addons/web/static/src/js/core/abstract_storage_service.js
@@ -45,6 +45,19 @@ var AbstractStorageService = AbstractService.extend({
return val ? JSON.parse(val) : defaultValue;
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return this.storage.key(index);
+ },
+ /**
+ * @return {integer}
+ */
+ length: function () {
+ return this.storage.length;
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index 6d02c23664e..cd76ec770bb 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -21,6 +21,7 @@ var Dialog = Widget.extend({
xmlDependencies: ['/web/static/src/xml/dialog.xml'],
custom_events: _.extend({}, Widget.prototype.custom_events, {
focus_control_button: '_onFocusControlButton',
+ close_dialog: '_onCloseDialog',
}),
events: _.extend({}, Widget.prototype.events, {
'keydown .modal-footer button': '_onFooterButtonKeyDown',
@@ -157,33 +158,7 @@ var Dialog = Widget.extend({
* @param {Object[]} buttons - @see init
*/
set_buttons: function (buttons) {
- var self = this;
- this.$footer.empty();
- _.each(buttons, function (buttonData) {
- var $button = dom.renderButton({
- attrs: {
- class: buttonData.classes || (buttons.length > 1 ? 'btn-secondary' : 'btn-primary'),
- disabled: buttonData.disabled,
- },
- icon: buttonData.icon,
- text: buttonData.text,
- });
- $button.on('click', function (e) {
- var def;
- if (buttonData.click) {
- def = buttonData.click.call(self, e);
- }
- if (buttonData.close) {
- self.onForceClose = false;
- Promise.resolve(def).then(self.close.bind(self)).guardedCatch(self.close.bind(self));
- }
- });
- if (self.technical) {
- self.$footer.append($button);
- } else {
- self.$footer.prepend($button);
- }
- });
+ this._setButtonsTo(this.$footer, buttons);
},
set_title: function (title, subtitle) {
@@ -303,10 +278,53 @@ var Dialog = Widget.extend({
_focusOnClose: function() {
return false;
},
+ /**
+ * Render and set the given buttons into a target element
+ *
+ * @private
+ * @param {jQueryElement} $target The destination of the rendered buttons
+ * @param {Array} buttons The array of buttons to render
+ */
+ _setButtonsTo($target, buttons) {
+ var self = this;
+ $target.empty();
+ _.each(buttons, function (buttonData) {
+ var $button = dom.renderButton({
+ attrs: {
+ class: buttonData.classes || (buttons.length > 1 ? 'btn-secondary' : 'btn-primary'),
+ disabled: buttonData.disabled,
+ },
+ icon: buttonData.icon,
+ text: buttonData.text,
+ });
+ $button.on('click', function (e) {
+ var def;
+ if (buttonData.click) {
+ def = buttonData.click.call(self, e);
+ }
+ if (buttonData.close) {
+ self.onForceClose = false;
+ Promise.resolve(def).then(self.close.bind(self)).guardedCatch(self.close.bind(self));
+ }
+ });
+ if (self.technical) {
+ $target.append($button);
+ } else {
+ $target.prepend($button);
+ }
+ });
+ },
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
+ * @private
+ */
+ _onCloseDialog: function (ev) {
+ ev.stopPropagation();
+ this.close();
+ },
+ /**
* Moves the focus to the first button primary in the footer of the dialog
*
* @private
diff --git a/addons/web/static/src/js/core/local_storage.js b/addons/web/static/src/js/core/local_storage.js
index 0ae98498b31..ecc0cb7b78f 100644
--- a/addons/web/static/src/js/core/local_storage.js
+++ b/addons/web/static/src/js/core/local_storage.js
@@ -35,12 +35,10 @@ try {
var newValue = e.originalEvent.newValue;
try {
JSON.parse(newValue);
- if (localStorage.getItem(key) === newValue) {
- storage.trigger('storage', {
- key: key,
- newValue: newValue,
- });
- }
+ storage.trigger('storage', {
+ key: key,
+ newValue: newValue,
+ });
} catch (error) {}
});
return storage;
diff --git a/addons/web/static/src/js/core/ram_storage.js b/addons/web/static/src/js/core/ram_storage.js
index 9bf0fc29f93..abad86c4538 100644
--- a/addons/web/static/src/js/core/ram_storage.js
+++ b/addons/web/static/src/js/core/ram_storage.js
@@ -31,6 +31,7 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
*/
clear: function () {
this.storage = Object.create(null);
+ this.length = 0;
},
/**
* Returns the value associated with a given key in the storage
@@ -42,11 +43,21 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
return this.storage[key];
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return _.keys(this.storage)[index];
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
*/
removeItem: function (key) {
+ if (key in this.storage) {
+ this.length--;
+ }
delete this.storage[key];
this.trigger('storage', {key: key, newValue: null});
},
@@ -58,6 +69,9 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
* @param {string} value
*/
setItem: function (key, value) {
+ if (!(key in this.storage)) {
+ this.length++;
+ }
this.storage[key] = value;
this.trigger('storage', {key: key, newValue: value});
},
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index a782e128bc7..da46488cffa 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -40,13 +40,9 @@ var TranslatableFieldMixin = {
* @returns {jQuery}
*/
_renderTranslateButton: function () {
- if (_t.database.multi_lang && this.field.translate && this.res_id) {
+ if (_t.database.multi_lang && this.field.translate) {
var lang = _t.database.parameters.code.split('_')[0].toUpperCase();
- return $('<button>', {
- type: 'button',
- 'class': 'o_field_translate fa fa-globe btn btn-link',
- 'data-code': lang,
- })
+ return $(`<span class="o_field_translate btn btn-link">${lang}</span>`)
.on('click', this._onTranslate.bind(this));
}
return $();
@@ -59,10 +55,16 @@ var TranslatableFieldMixin = {
/**
* open the translation view for the current field
*
+ * @param {MouseEvent} ev
* @private
*/
- _onTranslate: function () {
- this.trigger_up('translate', {fieldName: this.name, id: this.dataPointID});
+ _onTranslate: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('translate', {
+ fieldName: this.name,
+ id: this.dataPointID,
+ isComingFromTranslationAlert: false,
+ });
},
};
@@ -213,7 +215,9 @@ var InputField = DebouncedField.extend({
if (!event || event === this.lastChangeEvent) {
this.isDirty = false;
}
- if (this.isDirty || (event && event.target === this && event.data.changes[this.name] === this.value)) {
+ if (this.isDirty || (event && event.target === this &&
+ event.data.changes &&
+ event.data.changes[this.name] === this.value)) {
if (this.attrs.decorations) {
// if a field is modified, then it could have triggered an onchange
// which changed some of its decorations. Since we bypass the
@@ -498,7 +502,10 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
if (this.field.size && this.field.size > 0) {
this.$el.attr('maxlength', this.field.size);
}
- this.$el = this.$el.add(this._renderTranslateButton());
+ if (this.field.translate) {
+ this.$el = this.$el.add(this._renderTranslateButton());
+ this.$el.addClass('o_field_translate');
+ }
return def;
},
/**
@@ -1327,8 +1334,10 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
start: function () {
if (this.mode === 'edit') {
dom.autoresize(this.$el, this.autoResizeOptions);
-
- this.$el = this.$el.add(this._renderTranslateButton());
+ if (this.field.translate) {
+ this.$el = this.$el.add(this._renderTranslateButton());
+ this.$el.addClass('o_field_translate');
+ }
}
return this._super();
},
@@ -1380,7 +1389,7 @@ var HandleWidget = AbstractField.extend({
description: _lt("Handle"),
noLabel: true,
className: 'o_row_handle fa fa-arrows ui-sortable-handle',
- widthInList: '32px',
+ widthInList: '33px',
tagName: 'span',
supportedFieldTypes: ['integer'],
@@ -1822,8 +1831,11 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
if(this.nodeOptions.zoom) {
var unique = this.recordData.__last_update;
- var url = this._getImageUrl(this.model, this.res_id, 'image', unique);
+ var url = this._getImageUrl(this.model, this.res_id, 'image_1920', unique);
var $img;
+ var imageField = _.find(Object.keys(this.recordData), function(o) {
+ return o.startsWith('image_');
+ });
if(this.nodeOptions.background)
{
@@ -1844,7 +1856,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
$img = this.$('img');
}
- if(this.recordData.image) {
+ if(this.recordData[imageField]) {
$img.attr('data-zoom', 1);
$img.attr('data-zoom-image', url);
@@ -1867,6 +1879,17 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
},
});
+var KanbanFieldBinaryImage = FieldBinaryImage.extend({
+ // In kanban views, there is a weird logic to determine whether or not a
+ // click on a card should open the record in a form view. This logic checks
+ // if the clicked element has click handlers bound on it, and if so, does
+ // not open the record (assuming that the click will be handle by someone
+ // else). In the case of this widget, there are clicks handler but they
+ // only apply in edit mode, which is never the case in kanban views, so we
+ // simply remove them.
+ events: {},
+});
+
var FieldBinaryFile = AbstractFieldBinary.extend({
description: _lt("File"),
template: 'FieldBinaryFile',
@@ -1941,7 +1964,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
'download': true,
'data': utils.is_bin_size(this.value) ? null : this.value,
},
- error: () => this.call('crash_manager', 'rpc_error', ...arguments),
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
url: '/web/content',
});
ev.stopPropagation();
@@ -3266,6 +3289,7 @@ return {
FieldPdfViewer: FieldPdfViewer,
AbstractFieldBinary: AbstractFieldBinary,
FieldBinaryImage: FieldBinaryImage,
+ KanbanFieldBinaryImage: KanbanFieldBinaryImage,
FieldBoolean: FieldBoolean,
BooleanToggle: BooleanToggle,
FieldChar: FieldChar,
@@ -3278,7 +3302,7 @@ return {
FieldFloatTime: FieldFloatTime,
FieldFloatFactor: FieldFloatFactor,
FieldFloatToggle: FieldFloatToggle,
- FieldPercentage : FieldPercentage,
+ FieldPercentage: FieldPercentage,
FieldInteger: FieldInteger,
FieldMonetary: FieldMonetary,
FieldPercentPie: FieldPercentPie,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 942e6fc094a..9adb578639f 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -39,6 +39,7 @@ registry
.add('CopyClipboardText', basic_fields.TextCopyClipboard)
.add('CopyClipboardChar', basic_fields.CharCopyClipboard)
.add('image', basic_fields.FieldBinaryImage)
+ .add('kanban.image', basic_fields.KanbanFieldBinaryImage)
.add('binary', basic_fields.FieldBinaryFile)
.add('pdf_viewer', basic_fields.FieldPdfViewer)
.add('monetary', basic_fields.FieldMonetary)
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index c539a3ce12d..b2d3379aef1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -402,6 +402,52 @@ var FieldMany2One = AbstractField.extend({
};
},
/**
+ * @private
+ * @param {Object} values
+ * @param {string} search_val
+ * @param {Object} domain
+ * @param {Object} context
+ * @returns {Object}
+ */
+ _manageSearchMore: function (values, search_val, domain, context) {
+ var self = this;
+ values = values.slice(0, this.limit);
+ values.push({
+ label: _t("Search More..."),
+ action: function () {
+ var prom;
+ if (search_val !== '') {
+ prom = self._rpc({
+ model: self.field.relation,
+ method: 'name_search',
+ kwargs: {
+ name: search_val,
+ args: domain,
+ operator: "ilike",
+ limit: self.SEARCH_MORE_LIMIT,
+ context: context,
+ },
+ });
+ }
+ Promise.resolve(prom).then(function (results) {
+ var dynamicFilters;
+ if (results) {
+ var ids = _.map(results, function (x) {
+ return x[0];
+ });
+ dynamicFilters = [{
+ description: _.str.sprintf(_t('Quick search: %s'), search_val),
+ domain: [['id', 'in', ids]],
+ }];
+ }
+ self._searchCreatePopup("search", false, {}, dynamicFilters);
+ });
+ },
+ classname: 'o_m2o_dropdown_option',
+ });
+ return values;
+ },
+ /**
* Listens to events 'field_changed' to keep track of the last event that
* has been trigerred. This allows to detect that all changes have been
* acknowledged by the environment.
@@ -552,40 +598,7 @@ var FieldMany2One = AbstractField.extend({
// search more... if more results than limit
if (values.length > self.limit) {
- values = values.slice(0, self.limit);
- values.push({
- label: _t("Search More..."),
- action: function () {
- var prom;
- if (search_val !== '') {
- prom = self._rpc({
- model: self.field.relation,
- method: 'name_search',
- kwargs: {
- name: search_val,
- args: domain,
- operator: "ilike",
- limit: self.SEARCH_MORE_LIMIT,
- context: context,
- },
- });
- }
- Promise.resolve(prom).then(function (results) {
- var dynamicFilters;
- if (results) {
- var ids = _.map(results, function (x) {
- return x[0];
- });
- dynamicFilters = [{
- description: _.str.sprintf(_t('Quick search: %s'), search_val),
- domain: [['id', 'in', ids]],
- }];
- }
- self._searchCreatePopup("search", false, {}, dynamicFilters);
- });
- },
- classname: 'o_m2o_dropdown_option',
- });
+ values = self._manageSearchMore(values, search_val, domain, context);
}
var create_enabled = self.can_create && !self.nodeOptions.no_create;
// quick create
@@ -1768,21 +1781,13 @@ var FieldMany2Many = FieldX2Many.extend({
supportedFieldTypes: ['many2many'],
//--------------------------------------------------------------------------
- // Handlers
+ // Public
//--------------------------------------------------------------------------
-
/**
- * Opens a SelectCreateDialog.
- *
- * @override
- * @private
- * @param {OdooEvent|MouseEvent} ev this event comes either from the 'Add
- * record' link in the list editable renderer, or from the 'Create' button
- * in the kanban view
+ * Opens a SelectCreateDialog
*/
- _onAddRecord: function (ev) {
+ onAddRecordOpenDialog: function () {
var self = this;
- ev.stopPropagation();
var domain = this.record.getDomain({fieldName: this.name});
new dialogs.SelectCreateDialog(this, {
@@ -1808,6 +1813,25 @@ var FieldMany2Many = FieldX2Many.extend({
}
}).open();
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Opens a SelectCreateDialog.
+ *
+ * @override
+ * @private
+ * @param {OdooEvent|MouseEvent} ev this event comes either from the 'Add
+ * record' link in the list editable renderer, or from the 'Create' button
+ * in the kanban view
+ */
+ _onAddRecord: function (ev) {
+ ev.stopPropagation();
+ this.onAddRecordOpenDialog();
+ },
+
/**
* Intercepts the 'open_record' event to edit its data and lets it bubble up
* to the form view.
@@ -2552,7 +2576,7 @@ var FieldSelection = AbstractField.extend({
description: _lt("Selection"),
template: 'FieldSelection',
specialData: "_fetchSpecialRelation",
- supportedFieldTypes: ['selection', 'many2one'],
+ supportedFieldTypes: ['selection'],
events: _.extend({}, AbstractField.prototype.events, {
'change': '_onChange',
}),
@@ -2605,10 +2629,14 @@ var FieldSelection = AbstractField.extend({
*/
_renderEdit: function () {
this.$el.empty();
+ var required = this.attrs.modifiersValue && this.attrs.modifiersValue.required;
for (var i = 0 ; i < this.values.length ; i++) {
+ var disabled = required && this.values[i][0] === false;
+
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
- text: this.values[i][1]
+ text: this.values[i][1],
+ style: disabled ? "display: none" : "",
}));
}
var value = this.value;
@@ -2647,9 +2675,7 @@ var FieldSelection = AbstractField.extend({
return v[0] === false && v[1] === '';
});
}
- if (!this.attrs.modifiersValue || !this.attrs.modifiersValue.required) {
- this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
- }
+ this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/special_fields.js b/addons/web/static/src/js/fields/special_fields.js
index 7b8e53095e1..cfadd9f101a 100644
--- a/addons/web/static/src/js/fields/special_fields.js
+++ b/addons/web/static/src/js/fields/special_fields.js
@@ -51,7 +51,7 @@ var FieldTimezoneMismatch = FieldSelection.extend({
* @private
*/
_renderDateTimeTimezone: function () {
- if (!this.mismatch) {
+ if (!this.mismatch || !this.$option.html()) {
return;
}
var offset = this.recordData.tz_offset.match(/([+-])([0-9]{2})([0-9]{2})/);
@@ -76,7 +76,7 @@ var FieldTimezoneMismatch = FieldSelection.extend({
var value = this.$el.val();
var $span = $('<span class="fa fa-exclamation-triangle o_tz_warning"/>');
- if (this.$option) {
+ if (this.$option && this.$option.html()) {
this.$option.html(this.$option.html().split(' ')[0]);
}
diff --git a/addons/web/static/src/js/public/public_crash_manager.js b/addons/web/static/src/js/public/public_crash_manager.js
index a352340db7a..8eae270f23a 100644
--- a/addons/web/static/src/js/public/public_crash_manager.js
+++ b/addons/web/static/src/js/public/public_crash_manager.js
@@ -14,12 +14,11 @@ const PublicCrashManager = CrashManager.extend({
* @override
*/
_displayWarning(message, title, options) {
- this.displayNotification({
- ...options,
+ this.displayNotification(Object.assign({}, options, {
title,
message,
sticky: true,
- });
+ }));
},
});
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index e08f87c8fcb..5f78b6b84dc 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -146,20 +146,6 @@ var CrashManager = AbstractService.extend({
disable: function () {
active = false;
},
- /**
- * @return {Object}
- */
- getMapTitle() {
- return {
- access_denied: _lt("Access Denied"),
- access_error: _lt("Access Error"),
- except_orm: _lt("Global Business Error"),
- missing_error: _lt("Missing Record"),
- user_error: _lt("User Error"),
- validation_error: _lt("Validation Error"),
- warning: _lt("Warning"),
- };
- },
handleLostConnection: function () {
var self = this;
if (!this.isConnected) {
@@ -188,7 +174,15 @@ var CrashManager = AbstractService.extend({
if (!document.body) {
return;
}
- var map_title = this.getMapTitle();
+ var map_title = {
+ access_denied: _lt("Access Denied"),
+ access_error: _lt("Access Error"),
+ except_orm: _lt("Global Business Error"),
+ missing_error: _lt("Missing Record"),
+ user_error: _lt("User Error"),
+ validation_error: _lt("Validation Error"),
+ warning: _lt("Warning"),
+ };
if (!active) {
return;
}
@@ -317,10 +311,9 @@ var CrashManager = AbstractService.extend({
* @param {Object} options
*/
_displayWarning: function (message, title, options) {
- return new WarningDialog(this, {
- ...options,
+ return new WarningDialog(this, Object.assign({}, options, {
title,
- }, {
+ }), {
message,
}).open();
},
diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js
index 82a4ed26c0e..df089c01fcf 100644
--- a/addons/web/static/src/js/tools/test_menus.js
+++ b/addons/web/static/src/js/tools/test_menus.js
@@ -8,6 +8,7 @@
var viewUpdateCount = 0;
var testedApps;
var testedMenus;
+ var blackListedMenus = ['base.menu_theme_store', 'base.menu_third_party'];
function createWebClientHooks() {
var AbstractController = odoo.__DEBUG__.services['web.AbstractController'];
@@ -126,6 +127,7 @@
if (testedMenus.indexOf(element.dataset.menuXmlid) >= 0) return Promise.resolve(); // Avoid infinite loop
console.log("Testing menu", element.innerText.trim(), " ", element.dataset.menuXmlid);
testedMenus.push(element.dataset.menuXmlid);
+ if (blackListedMenus.includes(element.dataset.menuXmlid)) return Promise.resolve(); // Skip black listed menus
var startActionCount = clientActionCount;
_click($(element));
var isModal = false;
@@ -203,7 +205,7 @@
function testFilters() {
var filterProm = Promise.resolve();
// var $filters = $('div.o_control_panel div.btn-group.o_dropdown > ul.o_filters_menu > li:not(.o_add_custom_filter)');
- var $filters = $('.o_filters_menu > .o_menu_item');
+ var $filters = $('.o_filters_menu > .o_menu_item:not(.d-none)');
console.log("Testing " + $filters.length + " filters");
var filter_ids = _.compact(_.map($filters, function(f) { return f.dataset.id}));
filter_ids.forEach(function(filter_id){
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index fc8e73136d8..d581f84308a 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -205,11 +205,11 @@ var AbstractView = Factory.extend({
if (this.withSearchPanel) {
var spProto = this.config.SearchPanel.prototype;
var viewInfo = this.controlPanelParams.viewInfo;
- var sections = spProto.computeSearchPanelParams(viewInfo, this.viewType);
- if (sections) {
- this.searchPanelParams.sections = sections;
+ var searchPanelParams = spProto.computeSearchPanelParams(viewInfo, this.viewType);
+ if (searchPanelParams.sections) {
+ this.searchPanelParams.sections = searchPanelParams.sections;
this.rendererParams.withSearchPanel = true;
- spDef = Promise.resolve(cpDef).then(this._createSearchPanel.bind(this, parent));
+ spDef = Promise.resolve(cpDef).then(this._createSearchPanel.bind(this, parent, searchPanelParams));
}
}
@@ -283,7 +283,7 @@ var AbstractView = Factory.extend({
* @param {Widget} parent
* @returns {Promise<SearchPanel>} resolved when the searchPanel is ready
*/
- _createSearchPanel: async function (parent) {
+ _createSearchPanel: async function (parent, params) {
var defaultValues = {};
Object.keys(this.loadParams.context).forEach((key) => {
let match = /^searchpanel_default_(.*)$/.exec(key);
@@ -299,6 +299,7 @@ var AbstractView = Factory.extend({
searchDomain: controlPanelDomain,
sections: this.searchPanelParams.sections,
state: this.searchPanelParams.state,
+ classes: params.classes || [],
});
this.controllerParams.searchPanel = searchPanel;
this.controllerParams.controlPanelDomain = controlPanelDomain;
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 2163f074cb0..be0b9a00bf3 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -12,6 +12,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var FieldManagerMixin = require('web.FieldManagerMixin');
var Pager = require('web.Pager');
+var TranslationDialog = require('web.TranslationDialog');
var _t = core._t;
@@ -332,6 +333,23 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return this.renderer.confirmChange(state, id, fields, e);
},
/**
+ * Ask the user to confirm he wants to save the record
+ * @private
+ */
+ _confirmSaveNewRecord: function () {
+ var self = this;
+ var def = new Promise(function (resolve, reject) {
+ var message = _t("You need to save this new record before editing the translation. Do you want to proceed?");
+ var dialog = Dialog.confirm(self, message, {
+ title: _t("Warning"),
+ confirm_callback: resolve.bind(self, true),
+ cancel_callback: reject,
+ });
+ dialog.on('closed', self, reject);
+ });
+ return def;
+ },
+ /**
* Delete records (and ask for confirmation if necessary)
*
* @param {string[]} ids list of local record ids
@@ -789,28 +807,35 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private
* @param {OdooEvent} ev
*/
- _onTranslate: function (ev) {
+ _onTranslate: async function (ev) {
ev.stopPropagation();
- var self = this;
- var record = this.model.get(ev.data.id, {raw: true});
- this._rpc({
+
+ if (this.model.isNew(ev.data.id)) {
+ await this._confirmSaveNewRecord();
+ var updatedFields = await this.saveRecord(ev.data.id, { stayInEdit: true });
+ await this._confirmChange(ev.data.id, updatedFields, ev);
+ }
+ var record = this.model.get(ev.data.id, { raw: true });
+ var result = await this._rpc({
route: '/web/dataset/call_button',
params: {
model: 'ir.translation',
method: 'translate_fields',
args: [record.model, record.res_id, ev.data.fieldName],
- kwargs: {context: record.getContext()},
+ kwargs: { context: record.getContext() },
}
- }).then(function (result) {
- self.do_action(result, {
- on_reverse_breadcrumb: function () {
- if (!_.isEmpty(self.renderer.alertFields)) {
- self.renderer.displayTranslationAlert();
- }
- return false;
- },
- });
});
+
+ this.translationDialog = new TranslationDialog(this, {
+ domain: result.domain,
+ searchName: result.context.search_default_name,
+ fieldName: record.fieldsInfo[record.viewType][ev.data.fieldName].name,
+ userLanguageValue: ev.target.value || '',
+ dataPointID: record.id,
+ isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert,
+ isText: record.fields[ev.data.fieldName].type === 'text',
+ });
+ return this.translationDialog.open();
},
});
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 f7005a11b9a..2d239520dfb 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -678,13 +678,14 @@ var BasicModel = AbstractModel.extend({
*/
getName: function (id) {
var record = this.localData[id];
+ var returnValue = '';
if (record._changes && 'display_name' in record._changes) {
- return record._changes.display_name;
+ returnValue = record._changes.display_name;
}
- if ('display_name' in record.data) {
- return record.data.display_name;
+ else if ('display_name' in record.data) {
+ returnValue = record.data.display_name;
}
- return _t("New");
+ return returnValue || _t("New");
},
/**
* Returns true if a record is dirty. A record is considered dirty if it has
@@ -2067,9 +2068,11 @@ var BasicModel = AbstractModel.extend({
if (field.onChange) {
hasOnchange = true;
}
- _.each(fieldInfo.views, function (view) {
- generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
- });
+ if (field.type === 'one2many' || field.type === 'many2many') {
+ _.each(fieldInfo.views, function (view) {
+ generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
+ });
+ }
});
}
return hasOnchange ? specs : false;
diff --git a/addons/web/static/src/js/views/calendar/calendar_popover.js b/addons/web/static/src/js/views/calendar/calendar_popover.js
index 8c34ed87ba5..e6e5518d752 100644
--- a/addons/web/static/src/js/views/calendar/calendar_popover.js
+++ b/addons/web/static/src/js/views/calendar/calendar_popover.js
@@ -123,7 +123,10 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
*/
_onClickPopoverEdit: function (ev) {
ev.preventDefault();
- this.trigger_up('edit_event', {id: this.event.id});
+ this.trigger_up('edit_event', {
+ id: this.event.id,
+ title: this.event.record.display_name,
+ });
},
/**
* @private
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index a8e12094e97..757835e694d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -699,7 +699,7 @@ return AbstractRenderer.extend({
} else {
context.eventDate.date = isSameDayEvent ? start.clone().format('dddd, LL') : start.clone().format('LL') + ' - ' + end.clone().format('LL');
}
-
+
if (eventData.record.allday && isSameDayEvent) {
context.eventDate.duration = _t("All day");
} else if (eventData.record.allday && !isSameDayEvent) {
@@ -772,7 +772,10 @@ return AbstractRenderer.extend({
*/
_onEditEvent: function (event) {
this._unselectEvent();
- this.trigger_up('openEvent', {_id: event.data.id});
+ this.trigger_up('openEvent', {
+ _id: event.data.id,
+ title: event.data.title,
+ });
},
/**
* @private
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
index 9c519695d7d..677f0733bc5 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
@@ -292,9 +292,7 @@ var ControlPanelRenderer = Renderer.extend({
context: this.context,
facets: this.state.facets,
fields: this.state.fields,
- filters: this.state.filters,
filterFields: this.state.filterFields,
- groupBys: this.state.groupBys,
});
return this.searchBar.appendTo(this.$('.o_searchview')).then(function () {
if (oldSearchBar) {
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
index a41bd1c23b5..348e2e8fced 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
@@ -39,7 +39,7 @@ var GroupByMenu = DropdownMenu.extend({
// determines list of options used by groupBys of type 'date'
this.groupableFields = [];
_.each(fields, function (field, name) {
- if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) {
+ if (field.sortable && name !== "id" && _.contains(GROUPABLE_TYPES, field.type)) {
self.groupableFields.push(_.extend({}, field, {
name: name,
}));
diff --git a/addons/web/static/src/js/views/control_panel/search/search_bar.js b/addons/web/static/src/js/views/control_panel/search/search_bar.js
index d4efab663d4..3f58fcdce2a 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_bar.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_bar.js
@@ -28,9 +28,7 @@ var SearchBar = Widget.extend({
this.facets = params.facets;
this.fields = params.fields;
- this.filters = params.filters;
this.filterFields = params.filterFields;
- this.groupBys = params.groupBys;
this.autoCompleteSources = [];
this.searchFacets = [];
@@ -167,12 +165,6 @@ var SearchBar = Widget.extend({
self.autoCompleteSources.push(new (Obj) (self, filter, field, self.context));
}
});
- _.each(this.filters, function (filter) {
- self.autoCompleteSources.push(new (registry.get('filter'))(self, filter));
- });
- _.each(this.groupBys, function (filter) {
- self.autoCompleteSources.push(new (registry.get('groupby'))(self, filter));
- });
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index c80f939ddee..bd31cddd50d 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -20,8 +20,6 @@ var FormController = BasicController.extend({
toggle_column_order: '_onToggleColumnOrder',
focus_control_button: '_onFocusControlButton',
form_dialog_discarded: '_onFormDialogDiscarded',
- swipe_left: '_onSwipeLeft',
- swipe_right: '_onSwipeRight',
}),
/**
* @override
@@ -188,7 +186,7 @@ var FormController = BasicController.extend({
var self = this;
if (this.hasSidebar) {
var otherItems = [];
- if (this.archiveEnabled) {
+ if (this.archiveEnabled && this.initialState.data.active !== undefined) {
var classname = "o_sidebar_item_archive" + (this.initialState.data.active ? "" : " o_hidden")
otherItems.push({
label: _t("Archive"),
@@ -579,7 +577,14 @@ var FormController = BasicController.extend({
return;
}
- def.then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
+ // Kind of hack for FormViewDialog: button on footer should trigger the dialog closing
+ // if the `close` attribute is set
+ def.then(function () {
+ self._enableButtons();
+ if (attrs.close) {
+ self.trigger_up('close_dialog');
+ }
+ }).guardedCatch(this._enableButtons.bind(this));
},
/**
* Called when the user wants to create a new record -> @see createRecord
@@ -626,7 +631,9 @@ var FormController = BasicController.extend({
* @private
*/
_onEdit: function () {
- this._setMode('edit');
+ // wait for potential pending changes to be saved (done with widgets
+ // allowing to edit in readonly)
+ this.mutex.getUnlockedDef().then(this._setMode.bind(this, 'edit'));
},
/**
* This method is called when someone tries to freeze the order, most likely
@@ -680,7 +687,7 @@ var FormController = BasicController.extend({
* @private
* @param {OdooEvent} ev
*/
- _onOpenOne2ManyRecord: function (ev) {
+ _onOpenOne2ManyRecord: async function (ev) {
ev.stopPropagation();
var data = ev.data;
var record;
@@ -688,6 +695,9 @@ var FormController = BasicController.extend({
record = this.model.get(data.id, {raw: true});
}
+ // Sync with the mutex to wait for potential onchanges
+ await this.model.mutex.getUnlockedDef();
+
new dialogs.FormViewDialog(this, {
context: data.context,
domain: data.domain,
@@ -740,30 +750,6 @@ var FormController = BasicController.extend({
this.saveRecord().then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
},
/**
- * Called when user swipes left. Move to next record.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSwipeLeft: function (ev) {
- ev.stopPropagation();
- if (this.pager) {
- this.pager.next();
- }
- },
- /**
- * Called when user swipes right. Move to previous record.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSwipeRight: function (ev) {
- ev.stopPropagation();
- if (this.pager) {
- this.pager.previous();
- }
- },
- /**
* This method is called when someone tries to sort a column, most likely
* in a x2many list view
*
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index decea8867fc..e1395eebb9a 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -1083,7 +1083,11 @@ var FormRenderer = BasicRenderer.extend({
*/
_onTranslate: function (ev) {
ev.preventDefault();
- this.trigger_up('translate', {fieldName: ev.target.name, id: this.state.id});
+ this.trigger_up('translate', {
+ fieldName: ev.target.name,
+ id: this.state.id,
+ isComingFromTranslationAlert: true,
+ });
},
/**
* remove alert fields of record from alertFields object
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 0703d9713d5..13d73cf37c2 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -20,7 +20,6 @@ var FormView = BasicView.extend({
multi_record: false,
withSearchBar: false,
searchMenuTypes: [],
- jsLibs: [],
viewType: 'form',
/**
* @override
@@ -48,9 +47,6 @@ var FormView = BasicView.extend({
this.controllerParams.mode = mode;
this.rendererParams.mode = mode;
- if (config.device.isMobile) {
- this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js');
- }
},
//--------------------------------------------------------------------------
@@ -127,6 +123,10 @@ var FormView = BasicView.extend({
var refinedContext = _.pick(self.loadParams.context, function (value, key) {
return key.indexOf('_view_ref') === -1;
});
+ // Specify the main model to prevent access rights defined in the context
+ // (e.g. create: 0) to apply to subviews. We use here the same logic as
+ // the one applied by the server for inline views.
+ refinedContext.base_model_name = self.controllerParams.modelName;
defs.push(parent.loadViews(
field.relation,
new Context(context, self.userContext, refinedContext).eval(),
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index f8125675d4e..c3ad22f6ced 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -48,6 +48,9 @@ var GraphView = AbstractView.extend({
this.arch.children.forEach(function (field) {
var fieldName = field.attrs.name;
+ if (fieldName === "id") {
+ return;
+ }
var interval = field.attrs.interval;
if (interval) {
fieldName = fieldName + ':' + interval;
diff --git a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
index 6a9caa99a39..3e9197d3fa2 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
@@ -1,9 +1,12 @@
odoo.define('web.KanbanColumnProgressBar', function (require) {
'use strict';
-var Widget = require('web.Widget');
+const core = require('web.core');
var session = require('web.session');
var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+const _t = core._t;
var KanbanColumnProgressBar = Widget.extend({
template: 'KanbanView.ColumnProgressBar',
@@ -28,7 +31,9 @@ var KanbanColumnProgressBar = Widget.extend({
// <progressbar/> attributes
this.fieldName = columnState.progressBarValues.field;
- this.colors = columnState.progressBarValues.colors;
+ this.colors = _.extend({}, columnState.progressBarValues.colors, {
+ __false: 'muted', // color to use for false value
+ });
this.sumField = columnState.progressBarValues.sum_field;
// Previous progressBar state
@@ -55,7 +60,7 @@ var KanbanColumnProgressBar = Widget.extend({
this.$bars = {};
_.each(this.colors, function (val, key) {
- self.$bars[val] = self.$('.bg-' + val + '-full');
+ self.$bars[key] = self.$(`.progress-bar[data-filter=${key}]`);
});
this.$counter = this.$('.o_kanban_counter_side');
this.$number = this.$counter.find('b');
@@ -77,18 +82,22 @@ var KanbanColumnProgressBar = Widget.extend({
// current use of progressbars
var subgroupCounts = {};
+ let allSubgroupCount = 0;
_.each(self.colors, function (val, key) {
var subgroupCount = self.columnState.progressBarValues.counts[key] || 0;
if (self.activeFilter === key && subgroupCount === 0) {
self.activeFilter = false;
}
subgroupCounts[key] = subgroupCount;
+ allSubgroupCount += subgroupCount;
});
+ subgroupCounts.__false = self.columnState.count - allSubgroupCount;
self.groupCount = self.columnState.count;
self.subgroupCounts = subgroupCounts;
self.prevTotalCounterValue = self.totalCounterValue;
self.totalCounterValue = self.sumField ? (self.columnState.aggregateValues[self.sumField] || 0) : self.columnState.count;
+
self._notifyState();
self._render();
});
@@ -121,7 +130,7 @@ var KanbanColumnProgressBar = Widget.extend({
});
this.trigger_up('tweak_column_records', {
callback: function ($el, recordData) {
- var categoryValue = recordData[self.fieldName];
+ var categoryValue = recordData[self.fieldName] ? recordData[self.fieldName] : '__false';
_.each(self.colors, function (val, key) {
$el.removeClass('oe_kanban_card_' + val);
});
@@ -134,8 +143,9 @@ var KanbanColumnProgressBar = Widget.extend({
// Display and animate the progress bars
var barNumber = 0;
var barMinWidth = 6; // In %
+ const selection = self.columnState.fields[self.fieldName].selection;
_.each(self.colors, function (val, key) {
- var $bar = self.$bars[val];
+ var $bar = self.$bars[key];
var count = self.subgroupCounts && self.subgroupCounts[key] || 0;
if (!$bar) {
@@ -143,7 +153,14 @@ var KanbanColumnProgressBar = Widget.extend({
}
// Adapt tooltip
- $bar.attr('data-original-title', count + ' ' + key);
+ let value;
+ if (selection) { // progressbar on a field of type selection
+ const option = selection.find(option => option[0] === key);
+ value = option && option[1] || _t('Other');
+ } else {
+ value = key;
+ }
+ $bar.attr('data-original-title', count + ' ' + value);
$bar.tooltip({
delay: 0,
trigger: 'hover',
@@ -181,7 +198,8 @@ var KanbanColumnProgressBar = Widget.extend({
end = 0;
_.each(self.columnState.data, function (record) {
var recordData = record.data;
- if (self.activeFilter === recordData[self.fieldName]) {
+ if (self.activeFilter === recordData[self.fieldName] ||
+ (self.activeFilter === '__false' && !recordData[self.fieldName])) {
end += parseFloat(recordData[self.sumField]);
}
});
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 701180a357f..0c4213c60b7 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -70,12 +70,14 @@ var KanbanRecord = Widget.extend({
* Called each time the record is attached to the DOM.
*/
on_attach_callback: function () {
+ this.isInDOM = true;
_.invoke(this.subWidgets, 'on_attach_callback');
},
/**
* Called each time the record is detached from the DOM.
*/
on_detach_callback: function () {
+ this.isInDOM = false;
_.invoke(this.subWidgets, 'on_detach_callback');
},
@@ -256,6 +258,9 @@ var KanbanRecord = Widget.extend({
// a widget already exists for that field, so reset it with the new state
widget.reset(self.state);
$field.replaceWith(widget.$el);
+ if (self.isInDOM && widget.on_attach_callback) {
+ widget.on_attach_callback();
+ }
}
} else {
self._processField($field, field_name);
@@ -342,6 +347,9 @@ var KanbanRecord = Widget.extend({
*/
_render: function () {
this.defs = [];
+ // call 'on_detach_callback' on each subwidget as they will be removed
+ // from the DOM at the next line
+ _.invoke(this.subWidgets, 'on_detach_callback');
this._replaceElement(this.qweb.render('kanban-box', this.qweb_context));
this.$el.addClass('o_kanban_record').attr("tabindex", 0);
this.$el.attr('role', 'article');
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 1e109d7c0b8..5ec42156000 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -622,6 +622,7 @@ var ListController = BasicController.extend({
_onEditGroupClicked: function (ev) {
ev.stopPropagation();
this.do_action({
+ context: {create: false},
type: 'ir.actions.act_window',
views: [[false, 'form']],
res_model: ev.data.record.model,
@@ -704,6 +705,7 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onToggleGroup: function (ev) {
+ ev.stopPropagation();
var self = this;
this.model
.toggleGroup(ev.data.group.id)
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index a11b326991b..d9cc1115917 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -99,6 +99,8 @@ ListRenderer.include({
this.currentRow = null;
this.currentFieldIndex = null;
this.allRecordsIds = null; // flat array of records ids used by navigation
+ this.isResizing = false;
+ this.eventListeners = [];
},
/**
* @override
@@ -111,6 +113,18 @@ ListRenderer.include({
return this._super();
},
/**
+ * Overriden to unbind all attached listeners
+ *
+ * @override
+ */
+ destroy: function () {
+ this.eventListeners.forEach(listener => {
+ const { type, el, callback, options } = listener;
+ el.removeEventListener(type, callback, options);
+ });
+ return this._super.apply(this, arguments);
+ },
+ /**
* The list renderer needs to know if it is in the DOM, and to be notified
* when it is attached to the DOM to properly compute column widths.
*
@@ -493,7 +507,9 @@ ListRenderer.include({
*/
updateState: function (state, params) {
this.columnWidths = false;
- if (params.keepWidths) {
+ // There are some cases where a record is added to an invisible list
+ // e.g. set a quotation template with optionnal products
+ if (params.keepWidths && this.$el.is(':visible')) {
this.columnWidths = this.$('thead th').toArray().map(function (th) {
return $(th).outerWidth();
});
@@ -512,6 +528,23 @@ ListRenderer.include({
//--------------------------------------------------------------------------
/**
+ * Used to bind event listeners so that they can be unbound when the list
+ * is destroyed.
+ * There is no reverse method (list._removeEventListener) because there is
+ * no issue with removing an non-existing listener.
+ *
+ * @private
+ * @param {string} type event name
+ * @param {EventTarget} el event target
+ * @param {Function} callback callback function to attach
+ * @param {Object} options event listener options
+ */
+ _addEventListener: function (type, el, callback, options) {
+ el.addEventListener(type, callback, options);
+ this.eventListeners.push({ type, el, callback, options });
+ },
+
+ /**
* Destroy all field widgets corresponding to a record. Useful when we are
* removing a useless row.
*
@@ -540,17 +573,21 @@ ListRenderer.include({
* @private
*/
_freezeColumnWidths: function () {
- if (!this._hasVisibleRecords(this.state) && !this.columnWidths) {
- // there is no record nor widths to restore -> don't force column's
- // widths w.r.t. their label
+ if (!this.columnWidths && (!this._hasVisibleRecords(this.state) || !this.$el.is(':visible'))) {
+ // there is no record nor widths to restore or the list is not visible
+ // -> don't force column's widths w.r.t. their label
return;
}
- var $thead = this.$('thead');
- $thead.find('th').each((index, th) => {
- var $th = $(th);
- $th.css('width', this.columnWidths ? this.columnWidths[index] : $th.outerWidth() + 'px');
+ const table = this.el.getElementsByTagName('table')[0];
+ const thead = table.getElementsByTagName('thead')[0];
+
+ // Freeze each th width according to their size in auto layout
+ [...thead.getElementsByTagName('th')].forEach((th, index) => {
+ th.style.width = `${this.columnWidths ? this.columnWidths[index] : th.offsetWidth}px`;
});
- this.$('table').css('table-layout', 'fixed');
+
+ // Set the table layout to fixed
+ table.style.tableLayout = 'fixed';
},
/**
* Returns the width of a column according the 'width' attribute set in the
@@ -973,6 +1010,24 @@ ListRenderer.include({
return $thead;
},
/**
+ * Overriden to add a resize handle in editable list column headers.
+ * Only applies to headers containing text.
+ *
+ * @override
+ * @private
+ */
+ _renderHeaderCell: function () {
+ const $th = this._super.apply(this, arguments);
+ if (this.editable && $th[0].innerHTML.length && this._hasVisibleRecords(this.state)) {
+ const resizeHandle = document.createElement('span');
+ resizeHandle.classList = 'o_resize';
+ resizeHandle.onclick = this._onClickResize.bind(this);
+ resizeHandle.onmousedown = this._onStartResize.bind(this);
+ $th.append(resizeHandle);
+ }
+ return $th;
+ },
+ /**
* Editable rows are possibly extended with a trash icon on their right, to
* allow deleting the corresponding record.
* For many2many editable lists, the trash bin is replaced by X.
@@ -1122,6 +1177,7 @@ ListRenderer.include({
if (rowIndex === this.currentRow) {
return Promise.resolve();
}
+ this._freezeColumnWidths();
var recordId = this._getRecordID(rowIndex);
// To select a row, the currently selected one must be unselected first
var self = this;
@@ -1209,10 +1265,20 @@ ListRenderer.include({
var $td = $(event.currentTarget);
var $tr = $td.parent();
var rowIndex = $tr.prop('rowIndex') - 1;
- var fieldIndex = Math.max($tr.find('.o_data_cell').not('.o_list_button').index($td), 0);
+ var fieldIndex = Math.max($tr.find('.o_field_cell').index($td), 0);
this._selectCell(rowIndex, fieldIndex, {event: event});
},
/**
+ * We want to override any default mouse behaviour when clicking on the resize handles
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickResize: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ },
+ /**
* We need to manually unselect row, because no one else would do it
*/
_onEmptyRowClick: function () {
@@ -1372,11 +1438,100 @@ ListRenderer.include({
* @private
*/
_onSortColumn: function () {
- if (this.currentRow === null) {
+ if (this.currentRow === null && !this.isResizing) {
this._super.apply(this, arguments);
}
},
/**
+ * Handles the resize feature on the column headers
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onStartResize: function (ev) {
+ // Only triggered by left mouse button
+ if (ev.which !== 1) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.isResizing = true;
+
+ const table = this.el.getElementsByTagName('table')[0];
+ const th = ev.target.closest('th');
+ // Freeze the whole table width
+ this._freezeColumnWidths();
+ table.style.width = `${table.offsetWidth}px`;
+ const thPosition = [...th.parentNode.children].indexOf(th);
+ const resizingColumnElements = [...table.getElementsByTagName('tr')]
+ .filter(tr => tr.children.length === th.parentNode.children.length)
+ .map(tr => tr.children[thPosition]);
+ const optionalDropdown = this.el.getElementsByClassName('o_optional_columns')[0];
+ const initialX = ev.pageX;
+ const initialWidth = th.offsetWidth;
+ const initialTableWidth = table.offsetWidth;
+ const initialDropdownX = optionalDropdown ? optionalDropdown.offsetLeft : null;
+ const resizeStoppingEvents = [
+ 'keydown',
+ 'mousedown',
+ 'mouseup',
+ ];
+
+ // Apply classes to table and selected column
+ table.classList.add('o_resizing');
+ resizingColumnElements.forEach(el => el.classList.add('o_column_resizing'));
+
+ // Mousemove event : resize header
+ const resizeHeader = ev => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const delta = ev.pageX - initialX;
+ const newWidth = Math.max(10, initialWidth + delta);
+ const tableDelta = newWidth - initialWidth;
+ th.style.width = `${newWidth}px`;
+ table.style.width = `${initialTableWidth + tableDelta}px`;
+ if (optionalDropdown) {
+ optionalDropdown.style.left = `${initialDropdownX + tableDelta}px`;
+ }
+ };
+ this._addEventListener('mousemove', window, resizeHeader);
+
+ // Mouse or keyboard events : stop resize
+ const stopResize = ev => {
+ // Ignores the initial 'left mouse button down' event in order
+ // to not instantly remove the listener
+ if (ev.type === 'mousedown' && ev.which === 1) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ // We need a small timeout to not trigger a click on column header
+ clearTimeout(this.resizeTimeout);
+ this.resizeTimeout = setTimeout(() => {
+ this.isResizing = false;
+ }, 100);
+ window.removeEventListener('mousemove', resizeHeader);
+ table.classList.remove('o_resizing');
+ resizingColumnElements.forEach(el => el.classList.remove('o_column_resizing'));
+ resizeStoppingEvents.forEach(stoppingEvent => {
+ window.removeEventListener(stoppingEvent, stopResize);
+ });
+
+ // we remove the focus to make sure that the there is no focus inside
+ // the tr. If that is the case, there is some css to darken the whole
+ // thead, and it looks quite weird with the small css hover effect.
+ document.activeElement.blur();
+ };
+ // We have to listen to several events to properly stop the resizing function. Those are:
+ // - mousedown (e.g. pressing right click)
+ // - mouseup : logical flow of the resizing feature (drag & drop)
+ // - keydown : (e.g. pressing 'Alt' + 'Tab' or 'Windows' key)
+ resizeStoppingEvents.forEach(stoppingEvent => {
+ this._addEventListener(stoppingEvent, window, stopResize);
+ });
+ },
+ /**
* Unselect the row before adding the optional column to the listview
*
* @override
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 632cdfb64c7..94609544b10 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -34,7 +34,6 @@ var FIELD_CLASSES = {
};
var ListRenderer = BasicRenderer.extend({
- className: 'o_list_view',
events: {
"mousedown": "_onMouseDown",
"click .o_optional_columns_dropdown .dropdown-item": "_onToggleOptionalColumn",
@@ -386,6 +385,7 @@ var ListRenderer = BasicRenderer.extend({
if (node.tag === 'button') {
tdClassName += ' o_list_button';
} else if (node.tag === 'field') {
+ tdClassName += ' o_field_cell';
var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type];
if (typeClass) {
tdClassName += (' ' + typeClass);
@@ -745,6 +745,9 @@ var ListRenderer = BasicRenderer.extend({
var isNodeSorted = order[0] && order[0].name === name;
var field = this.state.fields[name];
var $th = $('<th>');
+ if (name) {
+ $th.attr('data-name', name);
+ }
if (node.attrs.class) {
if (node.attrs.class.indexOf('oe_edit_only') !== -1) {
$th.addClass('oe_edit_only');
@@ -764,7 +767,6 @@ var ListRenderer = BasicRenderer.extend({
}
}
$th.text(description)
- .attr('data-name', name)
.attr('tabindex', -1)
.toggleClass('o-sort-down', isNodeSorted ? !order[0].asc : false)
.toggleClass('o-sort-up', isNodeSorted ? order[0].asc : false)
@@ -902,6 +904,7 @@ var ListRenderer = BasicRenderer.extend({
// display the no content helper if there is no data to display
var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
+ this.$el.toggleClass('o_list_view', !displayNoContentHelper);
if (displayNoContentHelper) {
// destroy the previously instantiated pagers, if any
_.invoke(oldPagers, 'destroy');
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index 7875dcef52e..47eb5ad49fe 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -150,7 +150,7 @@ var PivotController = AbstractController.extend({
url: '/web/pivot/export_xls',
data: {data: JSON.stringify(table)},
complete: framework.unblockUI,
- error: () => this.call('crash_manager', 'rpc_error', ...arguments),
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
/**
@@ -340,6 +340,7 @@ var PivotController = AbstractController.extend({
* @param {OdooEvent} ev
*/
_onOpenView: function (ev) {
+ ev.stopPropagation();
var context = ev.data.context;
var group = ev.data.group;
var domain = this.model._getGroupDomain(group);
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js
index f09ef071f79..32bfda12594 100644
--- a/addons/web/static/src/js/views/search_panel.js
+++ b/addons/web/static/src/js/views/search_panel.js
@@ -99,6 +99,7 @@ var SearchPanel = Widget.extend({
this.defaultValues = params.defaultValues || {};
this.fields = params.fields;
this.model = params.model;
+ this.className = params.classes.concat(['o_search_panel']).join(' ');
this.searchDomain = params.searchDomain;
},
/**
@@ -157,6 +158,7 @@ var SearchPanel = Widget.extend({
*/
computeSearchPanelParams: function (viewInfo, viewType) {
var searchPanelSections;
+ var classes;
if (viewInfo) {
var arch = viewUtils.parseArch(viewInfo.arch);
viewType = viewType === 'list' ? 'tree' : viewType;
@@ -167,13 +169,19 @@ var SearchPanel = Widget.extend({
if (attrs.view_types) {
viewTypes = attrs.view_types.split(',');
}
+ if (attrs.class) {
+ classes = attrs.class.split(' ');
+ }
if (viewTypes.indexOf(viewType) !== -1) {
searchPanelSections = _processSearchPanelNode(node, viewInfo.fields);
}
}
});
}
- return searchPanelSections;
+ return {
+ sections: searchPanelSections,
+ classes: classes,
+ }
},
/**
* Export the current state (categories and filters) of the searchpanel.
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index beead650622..130173a16fc 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -155,13 +155,7 @@ var FormViewDialog = ViewDialog.extend({
var multi = options.disable_multiple_selection;
if (!multi && this.deletable) {
- options.buttons.push({
- text: _t("Remove"),
- classes: 'btn-secondary ' + oBtnRemove,
- click: function() {
- self._remove().then(self.close.bind(self));
- }
- });
+ this._setRemoveButtonOption(options, oBtnRemove);
}
}
}
@@ -276,6 +270,24 @@ var FormViewDialog = ViewDialog.extend({
return self.on_saved(record, !!changedFields.length);
});
},
+
+ /**
+ * Set the "remove" button into the options' buttons list
+ *
+ * @private
+ * @param {Object} options The options object to modify
+ * @param {string} btnClasses The classes for the remove button
+ */
+ _setRemoveButtonOption(options, btnClasses) {
+ const self = this;
+ options.buttons.push({
+ text: _t("Remove"),
+ classes: 'btn-secondary ' + btnClasses,
+ click: function() {
+ self._remove().then(self.close.bind(self));
+ }
+ });
+ },
});
/**
diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js
index 4da54cf8b5c..1bb00be8d38 100644
--- a/addons/web/static/src/js/widgets/auto_complete.js
+++ b/addons/web/static/src/js/widgets/auto_complete.js
@@ -140,12 +140,22 @@ return Widget.extend({
var $item = self.make_list_item(result).appendTo($list);
result.$el = $item;
});
+ // IE9 doesn't support addEventListener with option { once: true }
+ this.el.onmousemove = function (ev) {
+ self.$('li').each(function (index, li) {
+ li.onmouseenter = self.focus_element.bind(self, $(li));
+ });
+ var targetFocus = ev.target.tagName === 'LI' ?
+ ev.target :
+ ev.target.closest('li');
+ self.focus_element($(targetFocus));
+ self.el.onmousemove = null;
+ };
this.show();
},
make_list_item: function (result) {
var self = this;
var $li = $('<li>')
- .hover(function () {self.focus_element($li);})
.mousedown(function (ev) {
if (ev.button === 0) { // left button
self.select(ev, {item: {facet: result.facet}});
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index 964e03e4aa1..070b1927c5f 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -178,7 +178,7 @@ var DataExport = Dialog.extend({
})
},
complete: framework.unblockUI,
- error: () => this.call('crash_manager', 'rpc_error', ...arguments),
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
/**
@@ -647,7 +647,9 @@ var DataExport = Dialog.extend({
}
var matchItems = this.$(".o_tree_column").filter(function () {
- return this.innerText.toUpperCase().indexOf(searchText) >= 0;
+ var title = this.getAttribute('title');
+ return this.innerText.toUpperCase().indexOf(searchText) >= 0
+ || title && title.toUpperCase().indexOf(searchText) >= 0;
}).parent();
this.$(".o_export_tree_item").hide();
if (matchItems.length) {
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 5b4480838fe..ac8e83c8e30 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -92,8 +92,8 @@ var DateWidget = Widget.extend({
* set datetime value
*/
changeDatetime: function () {
+ var oldValue = this.getValue();
if (this.isValid()) {
- var oldValue = this.getValue();
this._setValueFromUi();
var newValue = this.getValue();
var hasChanged = !oldValue !== !newValue;
@@ -110,6 +110,9 @@ var DateWidget = Widget.extend({
}
this.trigger("datetime_changed");
}
+ } else {
+ var formattedValue = oldValue ? this._formatClient(oldValue) : null;
+ this.$input.val(formattedValue);
}
},
/**
diff --git a/addons/web/static/src/js/widgets/translation_dialog.js b/addons/web/static/src/js/widgets/translation_dialog.js
new file mode 100644
index 00000000000..39d841a48d0
--- /dev/null
+++ b/addons/web/static/src/js/widgets/translation_dialog.js
@@ -0,0 +1,172 @@
+odoo.define('web.TranslationDialog', function (require) {
+ 'use strict';
+
+ var core = require('web.core');
+ var Dialog = require('web.Dialog');
+ var session = require('web.session');
+
+ var _t = core._t;
+
+ var TranslationDialog = Dialog.extend({
+ xmlDependencies: (Dialog.prototype.xmlDependencies || [])
+ .concat(['/web/static/src/xml/translation_dialog.xml']),
+ template: 'TranslationDialog',
+
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {Object} [options]
+ * @param {string} [options.domain] the domain needed to get the translation terms
+ * @param {string} [options.fieldName] the name of the field currently translated (from the model of the form view)
+ * @param {string} [options.searchName] the name of the actual field that is the reference for translation (in the form of model,field)
+ * @param {string} [options.userLanguageValue] the value of the translation in the language of the user, as seen in the from view (might be empty)
+ * @param {string} [options.dataPointID] the data point id of the record for which we do the translations
+ * @param {boolean} [options.isComingFromTranslationAlert] the initiator of the dialog, might be a link on a field or the translation alert on top of the form
+ * @param {boolean} [options.isText] is the field a text field (multiline) or char (single line)
+ *
+ */
+ init: function (parent, options) {
+ options = options || {};
+
+ this.fieldName = options.fieldName;
+ this.domain = options.domain;
+ this.searchName = options.searchName;
+ this.userLanguageValue = options.userLanguageValue;
+ this.domain.push(['name', "=", `${this.searchName}`]);
+ this.dataPointID = options.dataPointID;
+ this.isComingFromTranslationAlert = options.isComingFromTranslationAlert;
+ this.currentInterfaceLanguage = session.user_context.lang;
+ this.isText = options.isText;
+
+ this._super(parent, _.extend({
+ size: 'large',
+ title: _t('Translate: ') + `${this.fieldName}`,
+ buttons: [
+ { text: _t('Save'), classes: 'btn-primary', close: true, click: this._onSave.bind(this) },
+ { text: _t('Discard'), close: true },
+ ],
+ }, options));
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ return Promise.all([
+ this._super(),
+ this._loadLanguages().then((l) => {
+ this.languages = l;
+ }),
+ this._loadTranslations().then((t) => {
+ this.translations = t;
+ })
+ ]).then(() => {
+ this.data = this.translations.map((term) => {
+ let relatedLanguage = this.languages.find((language) => language[0] === term.lang);
+ return {
+ id: term.id,
+ lang: term.lang,
+ langName: relatedLanguage[1],
+ // we set the translation value coming from the database, except for the language
+ // the user is currently utilizing. Then we set the translation value coming
+ // from the value of the field in the form
+ value: (term.lang === this.currentInterfaceLanguage &&
+ !this.isComingFromTranslationAlert) ?
+ this.userLanguageValue : term.value || ''
+ };
+ });
+ this.data.sort((left, right) => left.langName < right.langName ? -1 : 1);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Load the translation terms for the installed language, for the current model and res_id
+ * @private
+ */
+ _loadTranslations: function () {
+ return this._rpc({
+ model: 'ir.translation',
+ method: 'search_read',
+ fields: ['lang', 'value'],
+ domain: this.domain,
+ });
+ },
+ /**
+ * Load the installed languages long names and code
+ *
+ * The result of the call is put in cache on the prototype of this dialog.
+ * If any new language is installed, a full page refresh will happen,
+ * so there is no need invalidate it.
+ * @private
+ */
+ _loadLanguages: function () {
+ if (TranslationDialog.prototype.installedLanguagesCache)
+ return Promise.resolve(TranslationDialog.prototype.installedLanguagesCache);
+
+ return this._rpc({
+ model: 'res.lang',
+ method: 'get_installed',
+ fields: ['code', 'name', 'iso_code'],
+ }).then((installedLanguages) => {
+ TranslationDialog.prototype.installedLanguagesCache = installedLanguages;
+ return TranslationDialog.prototype.installedLanguagesCache
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * Save all the terms that have been updated
+ * @private
+ * @returns a promise that is resolved when all the save have occured
+ */
+ _onSave: function () {
+ var updatedTerm = {};
+ var updateFormViewField;
+
+ this.el.querySelectorAll('input[type=text],textarea').forEach((t) => {
+ var initialValue = this.data.find((d) => d.id == t.dataset.id);
+ if (initialValue.value !== t.value) {
+ updatedTerm[t.dataset.id] = t.value;
+
+ if (initialValue.lang === this.currentInterfaceLanguage) {
+ // when the user has changed the term for the language he is
+ // using in the interface, this change should be reflected
+ // in the form view
+ var changes = {};
+ changes[this.fieldName] = updatedTerm[initialValue.id];
+ updateFormViewField = {
+ dataPointID: this.dataPointID,
+ changes: changes,
+ doNotSetDirty: false,
+ };
+ }
+ }
+ });
+
+ // updatedTerm only contains the id and values of the terms that
+ // have been updated by the user
+ var saveUpdatedTermsProms = Object.keys(updatedTerm).map((id) => {
+ var writeTranslation = {
+ model: 'ir.translation',
+ method: 'write',
+ context: this.context,
+ args: [[parseInt(id, 10)], { value: updatedTerm[id] }]
+ };
+ return this._rpc(writeTranslation);
+ });
+ return Promise.all(saveUpdatedTermsProms).then(() => {
+ // we might have to update the value of the field on the form
+ // view that opened the translation dialog
+ if (updateFormViewField) {
+ this.trigger_up('field_changed', updateFormViewField);
+ }
+ });
+ }
+ });
+
+ return TranslationDialog;
+});
diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss
index 532d3ab3de4..2744f1b1080 100644
--- a/addons/web/static/src/scss/form_view.scss
+++ b/addons/web/static/src/scss/form_view.scss
@@ -575,14 +575,19 @@
}
// Translate icon
- .o_field_translate {
- padding: 0;
- cursor: pointer;
- width: 35px !important;
- vertical-align: top;
- &::before { // seems to be useful for some old browser
+ span.o_field_translate {
+ padding: 0 $o-form-spacing-unit 0 0 !important;
+ vertical-align: top;
position: relative;
- content: attr(data-code);
+ margin-left: -35px;
+ width: 35px !important; // important is usefull for textarea
+ display: inline-block;
+ text-align: right;
+ border: none; // usefull for textarea
+ }
+ input, textarea {
+ &.o_field_translate {
+ padding-right: 25px;
}
}
diff --git a/addons/web/static/src/scss/form_view_extra.scss b/addons/web/static/src/scss/form_view_extra.scss
index 77b713784fe..b3157b0ace6 100644
--- a/addons/web/static/src/scss/form_view_extra.scss
+++ b/addons/web/static/src/scss/form_view_extra.scss
@@ -80,11 +80,6 @@
font-weight: bold;
}
- // Translate icon
- .o_field_translate {
- margin-left: -35px;
- }
-
// Specific style classes
.o_group.o_inner_group.oe_subtotal_footer {
.oe_subtotal_footer_separator {
diff --git a/addons/web/static/src/scss/kanban_column_progressbar.scss b/addons/web/static/src/scss/kanban_column_progressbar.scss
index d70644f7e4a..bfea16934c5 100644
--- a/addons/web/static/src/scss/kanban_column_progressbar.scss
+++ b/addons/web/static/src/scss/kanban_column_progressbar.scss
@@ -110,6 +110,7 @@
@include o-kanban-css-filter(success, theme-color('success'));
@include o-kanban-css-filter(warning, theme-color('warning'));
@include o-kanban-css-filter(danger, theme-color('danger'));
+ @include o-kanban-css-filter(muted, theme-color('dark'));
&.o_kanban_group_show {
display: flex;
diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss
index 34c71ebb551..18c96fb6171 100644
--- a/addons/web/static/src/scss/kanban_view.scss
+++ b/addons/web/static/src/scss/kanban_view.scss
@@ -336,6 +336,10 @@
.oe_kanban_details {
width: 100%;
+
+ // Useful for the class 'o_text_overflow'
+ min-width: 0;
+
ul {
margin-bottom: $o-kanban-inner-hmargin*0.5;;
padding-left: 0;
@@ -414,7 +418,6 @@
&:hover .o_kanban_config, .o_kanban_config.show {
visibility: visible;
- margin-left: $o-kanban-inside-hgutter;
}
.o_kanban_config {
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index cb989a2c861..0d29c3cec21 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -27,7 +27,9 @@
}
}
tbody > tr > td:not(.o_list_record_selector) {
- @include o-text-overflow(table-cell);
+ &:not(.o_handle_cell) {
+ @include o-text-overflow(table-cell);
+ }
&.o_list_number {
text-align: right;
@@ -37,9 +39,9 @@
&.o_list_many2one {
white-space: pre-wrap;
}
- &.o_list_text {
- word-break: break-word;
- }
+ &.o_list_text {
+ word-break: break-word;
+ }
}
tfoot {
@@ -180,10 +182,28 @@
//--------------------------------------------------------------------------
&.o_editable_list {
+ &.o_resizing tr > :not(.o_column_resizing) {
+ opacity: 0.5;
+ }
+
&.o_empty_list {
table-layout: fixed;
}
+ th {
+ position: relative;
+ }
+
+ span.o_resize {
+ bottom: 0;
+ cursor: col-resize;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 10px;
+ z-index: 10;
+ }
+
.o_list_record_remove_header {
width: 32px;
}
@@ -200,7 +220,7 @@
}
}
- .o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) {
+ .o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier):not(.o_invisible_modifier) {
position: relative; // for o_field_translate
background-color: white;
.o_input {
@@ -237,9 +257,16 @@
@include o-position-absolute(0, 0);
}
}
- > .o_field_translate {
- @include o-position-absolute(0, 5px);
- width: 16px; // to have some space on the right
+ > input.o_field_translate, textarea.o_field_translate {
+ padding-right: 25px;
+ + span.o_field_translate {
+ margin-left: -35px;
+ padding: 0px 1px;
+ text-align: right;
+ width: 35px;
+ vertical-align: top;
+ font-size: 12px;
+ }
}
> .o_row_handle {
visibility: hidden; // hide sequence when editing
diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss
index 52c3100df31..f33aec59431 100644
--- a/addons/web/static/src/scss/modal.scss
+++ b/addons/web/static/src/scss/modal.scss
@@ -23,6 +23,10 @@
float: right;
}
}
+
+ .o_view_nocontent {
+ position: unset;
+ }
}
.modal-footer {
diff --git a/addons/web/static/src/scss/search_panel.scss b/addons/web/static/src/scss/search_panel.scss
index deac73754d1..0c7b2266a25 100644
--- a/addons/web/static/src/scss/search_panel.scss
+++ b/addons/web/static/src/scss/search_panel.scss
@@ -1,4 +1,5 @@
// ------- View with SearchPanel -------
+$o-searchpanel-active-bg: rgba(108, 193, 237, 0.3);
$o-searchpanel-p: $o-horizontal-padding;
$o-searchpanel-p-small: $o-horizontal-padding*0.5;
$o-searchpanel-p-tiny: $o-searchpanel-p-small*0.5;
@@ -31,10 +32,12 @@ $o-searchpanel-filter-default-color: #D59244;
}
.o_search_panel_label {
+ display: flex;
cursor: pointer;
user-select: none;
.o_toggle_fold {
+ margin-left: auto;
padding: 3px;
}
}
@@ -53,16 +56,22 @@ $o-searchpanel-filter-default-color: #D59244;
}
span.o_search_panel_label_title {
color: $headings-color;
- @include o-text-overflow(inline-block, calc(100% - 22px));
+ @include o-text-overflow;
}
header.active {
- background-color: $list-group-action-active-bg;
+ background-color: $o-searchpanel-active-bg;
}
}
.o_search_panel_category_value {
+ cursor: pointer;
header {
- margin-left: -$o-searchpanel-p-tiny;
- padding-left: $o-searchpanel-p-tiny;
+ margin-top: -4px;
+ margin-bottom: -4px;
+ padding: 4px 7px;
+
+ .o_toggle_fold {
+ padding-left: 11px;
+ }
}
.o_search_panel_category_value {
position: relative;
diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss
index 22baf6d0a0e..e2f44fbf88f 100644
--- a/addons/web/static/src/scss/search_view.scss
+++ b/addons/web/static/src/scss/search_view.scss
@@ -82,7 +82,6 @@
}
&.o-expand, &.o-expanded { // caret
@include o-position-absolute($left: 6px);
- padding: 3px;
}
&.o-expand:before {
@include o-caret-right;
diff --git a/addons/web/static/src/scss/secondary_variables.scss b/addons/web/static/src/scss/secondary_variables.scss
index 5b9d6ab239d..9580a278745 100644
--- a/addons/web/static/src/scss/secondary_variables.scss
+++ b/addons/web/static/src/scss/secondary_variables.scss
@@ -9,6 +9,16 @@ $o-list-group-header-color: lighten($o-brand-lightsecondary, 10%) !default;
$o-colors: lighten(#000, 46.7%), #F06050, #F4A460, #F7CD1F, #6CC1ED, #814968,
#EB7E7F, #2C8397, #475577, #D6145F, #30C381, #9365B8 !default;
+$o-colors-secondary: #aa4b6b, #30C381, #97743a, #F7CD1F, #4285F4, #8E24AA,
+ #D6145F, #173e43, #348F50, #AA3A38, #795548, #5e0231,
+ #6be585, #999966, #e9d362, #b56969, #bdc3c7, #649173 !default;
+
+// UI custom colors, complete list
+$o-colors-complete: join(
+ set-nth(set-nth($o-colors, 1, #134E5E), 6, #3daec3),
+ $o-colors-secondary
+)!default;
+
$o-sheet-cancel-hpadding: $o-horizontal-padding !default;
$o-sheet-cancel-bpadding: $o-horizontal-padding !default;
diff --git a/addons/web/static/src/scss/translation_dialog.scss b/addons/web/static/src/scss/translation_dialog.scss
new file mode 100644
index 00000000000..477f9371454
--- /dev/null
+++ b/addons/web/static/src/scss/translation_dialog.scss
@@ -0,0 +1,10 @@
+// translation dialog
+.o_translation_dialog {
+ .o_language_current {
+ font-weight: bold;
+ }
+
+ .row {
+ margin-bottom: 9px;
+ }
+}
diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss
index 44d639805be..9ab1729a4ac 100644
--- a/addons/web/static/src/scss/ui.scss
+++ b/addons/web/static/src/scss/ui.scss
@@ -83,3 +83,11 @@
.o_debounce_disabled {
pointer-events: none;
}
+
+span.o_force_ltr {
+ display: inline-block;
+}
+.o_force_ltr, .o_field_phone {
+ /*rtl:ignore*/
+ direction: ltr;
+}
diff --git a/addons/web/static/src/scss/web_calendar.scss b/addons/web/static/src/scss/web_calendar.scss
index cf0da7c8abd..ed99c4fcd6f 100644
--- a/addons/web/static/src/scss/web_calendar.scss
+++ b/addons/web/static/src/scss/web_calendar.scss
@@ -581,38 +581,8 @@ $o-cw-filter-avatar-size: 20px;
}
// =============== Generate color classes ===============
-// These colors are specifically designed for calendar
-// =============== =============== ======================
-$o-cw-colors: (
- #5e0231, // 1:
- #97743a, // 2:
- #F4A460, // 3:
- #F7CD1F, // 4:
- #6CC1ED, // 5:
- #4285F4, // 6: Default User
- #8E24AA, // 7:
- #2C8397, // 8:
- #475577, // 9:
- #D6145F, // 10:
- #30C381, // 11:
- #9365B8, // 12:
- #aa4b6b, // 13:
- #348F50, // 14:
- #AA3A38, // 15:
- #173e43, // 16:
- #134E5E, // 17:
- #795548, // 18:
- #6be585, // 19:
- #999966, // 20:
- #e9d362, // 21:
- #b56969, // 22:
- #bdc3c7, // 23:
- #649173, // 24:
-);
-
-
-@for $i from 1 to length($o-cw-colors) + 1 {
- $color: nth($o-cw-colors, $i);
+@for $i from 1 through length($o-colors-complete) {
+ $color: nth($o-colors-complete, $i);
.o_calendar_view .fc-view {
.fc-event.o_calendar_color_#{$i} {
diff --git a/addons/web/static/src/scss/webclient.scss b/addons/web/static/src/scss/webclient.scss
index fca898f262a..d41d1fb0ba3 100644
--- a/addons/web/static/src/scss/webclient.scss
+++ b/addons/web/static/src/scss/webclient.scss
@@ -78,6 +78,9 @@ div.o_boolean_toggle.custom-control.custom-checkbox {
.bg-info-full {
background-color: theme-color('info');
}
+.bg-muted-full {
+ background-color: #dee2e6;
+}
.o_web_accesskey_overlay {
font-family: $font-family-sans-serif;
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index aadda505b44..77d2d319d70 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -243,9 +243,9 @@
<t t-set="value" t-value="category.values[valueId]"/>
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-id="value.id" t-att-data-category-id="category.id">
<header t-att-class="'list-group-item-action' + (value.id === category.activeValueId ? ' active' : '')">
- <label t-att-for="value.display_name" class="o_search_panel_label mb0 d-block">
- <i t-if="value.childrenIds.length" t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (value.folded ? 'fa-caret-left' : 'fa-caret-down')"/>
+ <label t-att-for="value.display_name" class="o_search_panel_label mb0">
<span class="o_search_panel_label_title"><t t-esc="value.display_name"/></span>
+ <i t-if="value.childrenIds.length" t-attf-class="fa fa-fw o_toggle_fold {{value.folded ? 'fa-caret-left' : 'fa-caret-down'}}"/>
</label>
</header>
<ul t-if="!value.folded" class="list-group d-block">
diff --git a/addons/web/static/src/xml/translation_dialog.xml b/addons/web/static/src/xml/translation_dialog.xml
new file mode 100644
index 00000000000..32e2a036e45
--- /dev/null
+++ b/addons/web/static/src/xml/translation_dialog.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+ <div t-name="TranslationDialog" class="o_translation_dialog">
+ <div class="row" t-foreach="widget.data" t-as="term">
+ <div t-att-class="widget.currentInterfaceLanguage == term.lang ? 'col-12 col-lg-3 o_language_current' : 'col-12 col-lg-3'">
+ <t t-esc="term.langName"></t>
+ </div>
+ <div class="col-12 col-lg-9 translation">
+ <input t-if="!widget.isText" type="text"
+ class="o_field_char o_input"
+ t-att-value="term.value"
+ t-att-data-id="term.id"/>
+
+ <textarea t-if="widget.isText"
+ t-esc="term.value"
+ t-att-data-id="term.id"
+ rows="5"
+ class="o_field_text o_field_translate o_field_widget o_input">
+ </textarea>
+ </div>
+ </div>
+ </div>
+</templates>