diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
| commit | 6c948c36bbf72dd896e0bff03aa7fbddadcbb0cd (patch) | |
| tree | f8620905c011735d8a3c340c46d1417a54a75972 /addons/web/static/src | |
| parent | 48503fcf08eac6fed12e080d4f501f9bbc8d257d (diff) | |
| parent | 197474e5b9ad43ab2d587a98136a1a809ce9a2e7 (diff) | |
[MERGE] forward port branch saas-12.5 up to 197474e5b9a
Diffstat (limited to 'addons/web/static/src')
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> |
