From 127be68cca0693b37a7213204c497c968d4f56db Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 10 Sep 2019 13:30:38 +0000 Subject: [FIX] web: do not disable searchpanel on button clicked Before this rev., the searchpanel was automatically disabled (not present) in actions obtained by following links or buttons (e.g. stat buttons). This rev. removes this constraint, but adds the possibiliy to disable it by setting key 'search_panel' to false in the context. Note that in this case, the active categories are never retrieved from the local storage (i.e. the last one set). If no default is given, 'All' is simply selected (no domain). Task 2069310 --- .../static/src/js/chrome/action_manager_act_window.js | 2 +- addons/web/static/src/js/views/abstract_view.js | 18 ++++++++++-------- addons/web/static/src/js/views/search_panel.js | 12 ++++++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 1a08bf59a9a..6027e21bb42 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -697,7 +697,7 @@ ActionManager.include({ }; } var options = {on_close: ev.data.on_closed}; - action.flags = _.extend({}, action.flags, {withSearchPanel: false}); + action.flags = _.extend({}, action.flags, {searchPanelDefaultNoFilter: true}); return self.doAction(action, options).then(ev.data.on_success, ev.data.on_fail); }); }, diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index d581f84308a..7dc3cc30563 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -91,8 +91,8 @@ var AbstractView = Factory.extend({ * @param {Array[]} [params.searchQuery.domain=[]] * @param {string[]} [params.searchQuery.groupBy=[]] * @param {Object} [params.userContext={}] - * @param {boolean} [params.withControlPanel=true] - * @param {boolean} [params.withSearchPanel=true] + * @param {boolean} [params.withControlPanel=AbstractView.prototype.withControlPanel] + * @param {boolean} [params.withSearchPanel=AbstractView.prototype.withSearchPanel] */ init: function (viewInfo, params) { this._super.apply(this, arguments); @@ -115,7 +115,9 @@ var AbstractView = Factory.extend({ this.fields = this.fieldsView.viewFields; this.userContext = params.userContext || {}; this.withControlPanel = this.withControlPanel && params.withControlPanel; - this.withSearchPanel = this.withSearchPanel && this.multi_record && params.withSearchPanel; + const searchPanelDisabled = 'search_panel' in params.context && !params.search_panel; + this.withSearchPanel = this.withSearchPanel && this.multi_record && + params.withSearchPanel && !searchPanelDisabled; // the boolean parameter 'isEmbedded' determines if the view should be // considered as a subview. For now this is only used by the graph @@ -187,6 +189,9 @@ var AbstractView = Factory.extend({ withSearchBar: params.withSearchBar, }; this.searchPanelParams = { + defaultNoFilter: params.searchPanelDefaultNoFilter, + fields: this.fields, + model: this.loadParams.modelName, state: controllerState.spState, }; }, @@ -292,15 +297,12 @@ var AbstractView = Factory.extend({ } }); var controlPanelDomain = this.loadParams.domain; - var searchPanel = new this.config.SearchPanel(parent, { + var spParams = _.extend({}, this.searchPanelParams, { defaultValues: defaultValues, - fields: this.fields, - model: this.loadParams.modelName, searchDomain: controlPanelDomain, - sections: this.searchPanelParams.sections, - state: this.searchPanelParams.state, classes: params.classes || [], }); + var searchPanel = new this.config.SearchPanel(parent, spParams); this.controllerParams.searchPanel = searchPanel; this.controllerParams.controlPanelDomain = controlPanelDomain; await searchPanel.appendTo(document.createDocumentFragment()); diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js index 32bfda12594..d222a5642cf 100644 --- a/addons/web/static/src/js/views/search_panel.js +++ b/addons/web/static/src/js/views/search_panel.js @@ -77,6 +77,9 @@ var SearchPanel = Widget.extend({ * @param {Object} params * @param {Object} [params.defaultValues={}] the value(s) to activate by * default, for each filter and category + * @param {boolean} [params.defaultNoFilter=false] if true, select 'All' as + * value for each category that has no value specified in defaultValues + * (instead of looking in the localStorage for the last selected value) * @param {Object} params.fields * @param {string} params.model * @param {Array[]} params.searchDomain domain coming from controlPanel @@ -97,6 +100,12 @@ var SearchPanel = Widget.extend({ this.initialState = params.state; this.scrollTop = this.initialState && this.initialState.scrollTop || null; this.defaultValues = params.defaultValues || {}; + if (params.defaultNoFilter) { + Object.keys(this.categories).forEach((categoryId) => { + var fieldName = this.categories[categoryId].fieldName; + this.defaultValues[fieldName] = this.defaultValues[fieldName] || false; + }); + } this.fields = params.fields; this.model = params.model; this.className = params.classes.concat(['o_search_panel']).join(' '); @@ -181,7 +190,7 @@ var SearchPanel = Widget.extend({ return { sections: searchPanelSections, classes: classes, - } + }; }, /** * Export the current state (categories and filters) of the searchpanel. @@ -293,7 +302,6 @@ var SearchPanel = Widget.extend({ // set active value var validValues = _.pluck(category.values, 'id').concat([false]); var value = this._getCategoryDefaultValue(category, validValues); - // if not set in localStorage either, select 'All' category.activeValueId = _.contains(validValues, value) ? value : false; // unfold ancestor values of active value to make it is visible -- cgit v1.2.3 From ac00548b28c0c384b5b1423f65405e4918101872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Thu, 12 Sep 2019 13:16:10 +0000 Subject: [IMP] web: Export list view as XLS file Purpose ======= Have a quick export feature. The current export wizard is quite complex. We want to be able to quickly export a tree view, with the group by correctly represented, depending on the fields visible in the list view. Specification ============= Add an "EXPORT (XLS)" button in the list view (a little download icon) - export only columns visible in list view - export all the records - while respecting the group by This should be a generic feature but should be applied to all the account.move.lines list views. account.view_move_line_tree_grouped_sales_purchases account.view_move_line_tree_grouped_bank_cash account.view_move_line_tree_grouped_misc Task 2031835 closes odoo/odoo#36943 Signed-off-by: Yannick Tivisse (yti) --- .../static/src/js/views/list/list_controller.js | 94 ++++++++++++++++++++++ addons/web/static/src/xml/base.xml | 3 + 2 files changed, 97 insertions(+) (limited to 'addons/web/static/src') 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 5ec42156000..dc26a750eb4 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -8,20 +8,78 @@ odoo.define('web.ListController', function (require) { */ var core = require('web.core'); +var session = require('web.session'); +var framework = require('web.framework'); var BasicController = require('web.BasicController'); var DataExport = require('web.DataExport'); var Dialog = require('web.Dialog'); +var fieldUtils = require('web.field_utils'); var Sidebar = require('web.Sidebar'); var _t = core._t; var qweb = core.qweb; +/** + * Return data required for exporting the current list to a xls file. + * Recursive if some groups are nested. + * Note: this function is also used when the records are not grouped. + * In this scenario, an artificial single group with every record in + * the list is built. + * @param {Array} groups + * @param {Array} displayedFields + * @param {Array} allFields + * @returns {Array} list of groups with the following structure: + * 'data': list of records or list of sub-groups + * 'isGrouped': true if data contains sub-groups + * 'value': title of the group + * 'count': number of records in the group + * 'aggregateValues': dict container aggregated values of fields {field: aggregatedValue, ...} + * 'hideHeader': self explanatory, used when the records are not grouped at all. + */ +var processGroups = (groups, displayedFields, allFields) => { + let formatRecord = record => { + let data = _.pick(record.data, displayedFields) + let formattedData = {} + + Object.entries(data).forEach(([field_name, value]) => { + let field = allFields[field_name] + let formattedValue = fieldUtils.format[field.type](value, field, {data: record.data, forceString: true}) + formattedData[field_name] = formattedValue.replace(' ', '') + }); + return formattedData + } + + if (groups.length && groups[0].groupedBy && groups[0].groupedBy.length) { + // Recursively process sub-group + return groups.map(group => ({ + hideHeader: !!group.hideHeader, + isGrouped: true, + value: group.value !== undefined && group.value.toString() || _t("Undefined"), + count: group.count, + aggregateValues: _.pick(group.aggregateValues, displayedFields), + data: processGroups(group.data, displayedFields, allFields), + })) + } + // process records + return groups.map(group => ({ + hideHeader: !!group.hideHeader, + isGrouped: false, + value: group.value !== undefined && group.value.toString() || _t("Undefined"), + count: group.count, + aggregateValues: _.pick(group.aggregateValues, displayedFields), + data: group.data.map(formatRecord) + })) +} + var ListController = BasicController.extend({ /** * This key contains the name of the buttons template to render on top of * the list view. It can be overridden to add buttons in specific child views. */ buttons_template: 'ListView.buttons', + events: _.extend({}, BasicController.prototype.events, { + 'click .o_list_download': '_onExportRecords', + }), custom_events: _.extend({}, BasicController.prototype.custom_events, { activate_next_widget: '_onActivateNextWidget', add_record: '_onAddRecord', @@ -50,6 +108,9 @@ var ListController = BasicController.extend({ this.noLeaf = params.noLeaf; this.selectedRecords = params.selectedRecords || []; this.multipleRecordsSavingPromise = null; + this.activeActions = _.extend({}, this.activeActions, { + download: this.renderer.arch.attrs.download ? !!JSON.parse(this.renderer.arch.attrs.download) : true, + }); }, //-------------------------------------------------------------------------- @@ -338,6 +399,31 @@ var ListController = BasicController.extend({ self._updateButtons('readonly'); }); }, + /** + * Export the current list data in a xls file. + * + * @private + */ + _downloadList() { + let groups = this.renderer.state.data + groups = this.renderer.isGrouped ? groups : [{data: groups, count: groups.length, hideHeader: true}] // Artificial single group + + let allFields = this.renderer.state.fields + let columns = this.renderer.columns.map(column => ({ + field: column.attrs.name, + aggregateValue: column.aggregate && column.aggregate.value, + string: allFields[column.attrs.name].string, + })) + + groups = processGroups(groups, columns.map(c => c.field), allFields) + + return session.get_file({ + url: '/web/list/export_xls', + data: {data: JSON.stringify({columns, groups, title: this._title})}, + complete: framework.unblockUI, + error: (error) => this.call('crash_manager', 'rpc_error', error), + }); + }, /** * @override * @private @@ -613,6 +699,14 @@ var ListController = BasicController.extend({ }); new DataExport(this, record, defaultExportFields).open(); }, + /** + * Export Records in a xls file + * + * @private + */ + _onExportRecords() { + this._downloadList() + }, /** * Opens the related form view. * diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 300aff10c00..733bd37c211 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -271,6 +271,9 @@ Create + + -- cgit v1.2.3 From 3b3d35e8b91fb8881be848665349828c64642e03 Mon Sep 17 00:00:00 2001 From: Adrien Dieudonne Date: Thu, 29 Aug 2019 12:10:07 +0000 Subject: [IMP] web: SearchView: Allow user to quickly search on mobile Currently the search panel is nice for a search with filters but that was not 100% efficient for simple research without filters. The Search is now integrated in the form view, with the ability to open the Search panel to apply filters or group by. Task ID : 1924760 --- .../control_panel/mobile_control_panel_renderer.js | 30 +++++++++++++++++++--- addons/web/static/src/xml/base.xml | 3 ++- 2 files changed, 29 insertions(+), 4 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js index bf45c95ca68..95b5c577587 100644 --- a/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js +++ b/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js @@ -10,7 +10,8 @@ if (!config.device.isMobile) { ControlPanelRenderer.include({ events:_.extend({}, ControlPanelRenderer.prototype.events, { - 'click .o_mobile_search_close, .o_mobile_search_show_result, .o_enable_searchview': '_toggleMobileSearchView', + 'click .o_mobile_search_close, .o_mobile_search_show_result, .o_toggle_searchview_full': '_toggleMobileSearchView', + 'click .o_enable_searchview': '_toggleMobileQuickSearchView', 'click': '_onOpenMobileSearchView', 'click .o_mobile_search_clear_facets': '_onEmptyAll', 'show.bs.dropdown .o_mobile_search_filter .o_dropdown': '_onDropdownToggle', @@ -48,17 +49,40 @@ ControlPanelRenderer.include({ _renderSearchBar: function () { var self = this; return this._super.apply(this, arguments).then(function () { - self.searchBar.$el.detach().insertAfter(self.$('.o_mobile_search_header')); + self._renderSearchviewInput(); }); }, + _renderSearchviewInput: function () { + if (this.$('.o_toggle_searchview_full').is(':visible') && !this.$('.o_mobile_search').is(':visible')) { + this.$('.o_toggle_searchview_full').toggleClass('btn-secondary', !!this.state.query.length); + this.searchBar.$el.detach().insertAfter(this.$('.o_mobile_search')); + } else { + this.searchBar.$el.detach().insertAfter(this.$('.o_mobile_search_header')); + } + }, /** * Toggles mobile search view screen. * * @private */ _toggleMobileSearchView: function () { - this.$('.o_enable_searchview').toggleClass('btn-secondary', !!this.state.query.length); this.$('.o_mobile_search').toggleClass('o_hidden'); + this._renderSearchviewInput(); + }, + /** + * Toggles mobile quick search view on screen. + * + * @private + */ + _toggleMobileQuickSearchView: function () { + this.$('.o_cp_searchview').toggleClass('o_searchview_quick'); + this.$('.breadcrumb').toggleClass('o_hidden', + this.$('.o_cp_searchview').hasClass('o_searchview_quick')); + this.$('.o_toggle_searchview_full') + .toggleClass('o_hidden') + .toggleClass('btn-secondary', !!this.state.query.length); + this._renderSearchviewInput(); + this.$('.o_enable_searchview').toggleClass("fa-search").toggleClass("fa-close"); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index e8d8850115e..1e131c9fc8b 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1170,7 +1170,8 @@
-
SEE RESULT
+
SEE RESULT
-- cgit v1.2.3 From ef46378cd38d6cb361f1b867a3009f3bbe3bd7c1 Mon Sep 17 00:00:00 2001 From: wan Date: Mon, 16 Sep 2019 14:23:31 +0000 Subject: [FIX] web: remove absolute position on autoresize Before this commit, it was set to position absolute, in order to make sure it is not visible. But it is not really necessary. Also, it was harmful in some situations, for example the web_ribbon is missplaced in edit mode (apparently in webkit based browsers). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue was detected on the invoice form, when it is paid and we are going in edit mode. A fix is to set the height of the dummy text area to 0 instead of putting its position to absolute and out of the screen. closes odoo/odoo#36965 Signed-off-by: Géry Debongnie (ged) --- addons/web/static/src/js/core/dom.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 7be17700f98..f260478da1e 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -108,16 +108,13 @@ var dom = { class: $textarea[0].className, }); - var direction = _t.database.parameters.direction === 'rtl' ? 'right' : 'left'; $fixedTextarea.css({ - position: 'absolute', opacity: 0, - height: 10, + height: 0, borderTopWidth: 0, borderBottomWidth: 0, padding: 0, - top: -10000, - }).css(direction, -10000); + }); $fixedTextarea.data("auto_resize", true); // The following line is necessary to prevent the scrollbar to appear -- cgit v1.2.3 From dd5f6cb8978bf33f3cb85ed335b0fb3a39686bae Mon Sep 17 00:00:00 2001 From: jbm-odoo Date: Tue, 17 Sep 2019 14:28:00 +0000 Subject: [FIX] web: Reload page after archive with action For the moment, when we click on (un)archive and if the toggle_active retrieve an action to execute, the page isn't reloaded at the end. id=2072891 closes odoo/odoo#37027 Signed-off-by: Yannick Tivisse (yti) --- addons/web/static/src/js/views/basic/basic_model.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src') 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 2d239520dfb..4b4d02c2755 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1199,7 +1199,7 @@ var BasicModel = AbstractModel.extend({ if (!_.isEmpty(action)) { return self.do_action(action, { on_close: function () { - return self.reload(parentID); + return self.trigger_up('reload'); } }); } else { @@ -1231,7 +1231,7 @@ var BasicModel = AbstractModel.extend({ if (!_.isEmpty(action)) { return self.do_action(action, { on_close: function () { - return self.reload(parentID); + return self.trigger_up('reload'); } }); } else { @@ -1263,7 +1263,7 @@ var BasicModel = AbstractModel.extend({ if (!_.isEmpty(action)) { return self.do_action(action, { on_close: function () { - return self.reload(parentID); + return self.trigger_up('reload'); } }); } else { -- cgit v1.2.3 From 29bbdc9aa5ffba5f20c374b66b0045275c67f0c0 Mon Sep 17 00:00:00 2001 From: Adrien Dieudonne Date: Tue, 17 Sep 2019 13:57:58 +0000 Subject: [IMP] web: PivotView: display grouped columns on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, only "Total" column was shown on mobile and it wasn't possible to add another one. (1) Now, we let the user to add it even if he probably will have to scroll horizontally. (2) The "Flip" button has been reintroduced like in desktop. (3) By default, the old behavior will remain the same (only one colum will be displayed) but to avoid frustrating for users who always have to add manually columns to see the same information as in desktop; you can use favorite filters. In this case, all columns will be displayed. Task ID : 1945893 opw-1935645 closes odoo/odoo#37057 Signed-off-by: Adrien Dieudonné (adr) --- .../web/static/src/js/views/pivot/pivot_controller.js | 18 +----------------- addons/web/static/src/js/views/pivot/pivot_renderer.js | 5 +---- addons/web/static/src/js/views/pivot/pivot_view.js | 1 - 3 files changed, 2 insertions(+), 22 deletions(-) (limited to 'addons/web/static/src') 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 47eb5ad49fe..733a812d328 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -13,7 +13,6 @@ odoo.define('web.PivotController', function (require) { */ var AbstractController = require('web.AbstractController'); -var config = require('web.config'); var core = require('web.core'); var framework = require('web.framework'); var session = require('web.session'); @@ -115,16 +114,6 @@ var PivotController = AbstractController.extend({ this._updateButtons(); } }, - /** - * @override - * @param {Object} params - */ - update: function (params) { - if (config.device.isMobile && params.context) { - params.context.pivot_column_groupby = []; - } - return this._super.apply(this, arguments); - }, //-------------------------------------------------------------------------- // Private @@ -218,12 +207,7 @@ var PivotController = AbstractController.extend({ .toggleClass('selected', isSelected); }); var noDataDisplayed = !state.hasData || !state.measures.length; - var $flipButton = this.$buttons.find('.o_pivot_flip_button'); - if (config.device.isMobile) { - $flipButton.toggleClass('o_hidden', true); - } else { - $flipButton.prop('disabled', noDataDisplayed); - } + this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed); this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed); this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed); }, diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index b74b408c877..3affc975225 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -125,10 +125,7 @@ var PivotRenderer = AbstractRenderer.extend({ if (rowIndex > 0) { cellParams.title = groupbyLabels[rowIndex - 1]; } - className = 'o_pivot_header_cell'; - if (!config.device.isMobile) { - className += cell.isLeaf ? '_closed' : '_opened'; - } + className = 'o_pivot_header_cell' + (cell.isLeaf ? '_closed' : '_opened'); } cellParams.class = className; diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index 78e7c68cbe6..1b878cf3530 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -108,7 +108,6 @@ var PivotView = AbstractView.extend({ this.loadParams.default_order = params.default_order || this.arch.attrs.default_order; if (config.device.isMobile) { this.loadParams.colGroupBys = []; - this.loadParams.context.pivot_column_groupby = []; } -- cgit v1.2.3 From 2cdd46b566d00aa6c8e6bb9452c3edde120ce2a3 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 6 Sep 2019 09:15:45 +0000 Subject: [FIX] web: list: multi edition: force write same value Let's assume an editable list view with a many2one field and 2 records: the first one has value 1, the second value 2. The user selects all records and wants to set their many2one to value 1. It edits the first one, sets 1 and validates the confirm dialog. Before this rev., it didn't save anything because the 'reference' record (the one used to edit), didn't actually changed, as we reset the same value. This rev. forces the write the new value on all selected records in that case. Task 2068280 --- addons/web/static/src/js/views/list/list_controller.js | 3 ++- addons/web/static/src/js/views/list/list_model.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src') 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 dc26a750eb4..cf911ef5398 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -457,6 +457,7 @@ var ListController = BasicController.extend({ */ _saveMultipleRecords: function (recordId, node, changes) { var self = this; + var fieldName = Object.keys(changes)[0]; var value = Object.values(changes)[0]; var recordIds = _.union([recordId], this.selectedRecords); var validRecordIds = recordIds.reduce(function (result, nextRecordId) { @@ -487,7 +488,7 @@ var ListController = BasicController.extend({ } Dialog.confirm(this, message, { confirm_callback: () => { - this.model.saveRecords(recordId, validRecordIds) + this.model.saveRecords(recordId, validRecordIds, fieldName) .then(() => { this._updateButtons('readonly'); const state = this.model.get(this.handle); diff --git a/addons/web/static/src/js/views/list/list_model.js b/addons/web/static/src/js/views/list/list_model.js index 900f3764b0e..65526346c8e 100644 --- a/addons/web/static/src/js/views/list/list_model.js +++ b/addons/web/static/src/js/views/list/list_model.js @@ -40,12 +40,18 @@ odoo.define('web.ListModel', function (require) { * @param {string} referenceRecordId the record datapoint used to * generate the changes to apply to recordIds * @param {string[]} recordIds a list of record datapoint ids + * @param {string} fieldName the field to write */ - saveRecords: function (referenceRecordId, recordIds) { + saveRecords: function (referenceRecordId, recordIds, fieldName) { var self = this; var referenceRecord = this.localData[referenceRecordId]; var list = this.localData[referenceRecord.parentID]; - var changes = this._generateChanges(referenceRecord, {}); + // generate all record values to ensure that we'll write something + // (e.g. 2 records selected, edit a many2one in the first one, but + // reset same value, we still want to save this value on the other + // record) + var allChanges = this._generateChanges(referenceRecord, {changesOnly: false}); + var changes = _.pick(allChanges, fieldName); var records = recordIds.map(function (recordId) { return self.localData[recordId]; }); -- cgit v1.2.3 From f15c0699062ad41a325fb94a1862502f36d21a7d Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 6 Sep 2019 09:58:34 +0000 Subject: [FIX] web: list: clicking discard doesn't trigger an onFieldChange Fixes the way records are discarded in list views Before this commit, clicking the "Discard changes" button in a list view after editing a record triggered both the "field changed" and the "discard changes" methods (resulting in 2 modals opening). Now, all fieldChanges are prevented when trying to click "Discard changes", and are re-applied if the click was unsuccessful (lifted left mouse button somewhere else). Task 2068280 --- .../static/src/js/views/list/list_controller.js | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src') 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 cf911ef5398..96f3f1cd556 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -108,6 +108,7 @@ var ListController = BasicController.extend({ this.noLeaf = params.noLeaf; this.selectedRecords = params.selectedRecords || []; this.multipleRecordsSavingPromise = null; + this.fieldChangedPrevented = false; this.activeActions = _.extend({}, this.activeActions, { download: this.renderer.arch.attrs.download ? !!JSON.parse(this.renderer.arch.attrs.download) : true, }); @@ -195,6 +196,7 @@ var ListController = BasicController.extend({ }, trigger: 'manual', }); + this.$buttons.on('mousedown', '.o_list_button_discard', this._onDiscardMousedown.bind(this)); this.$buttons.on('click', '.o_list_button_discard', this._onDiscard.bind(this)); this.$buttons.appendTo($node); } @@ -673,6 +675,28 @@ var ListController = BasicController.extend({ ev.stopPropagation(); // So that it is not considered as a row leaving this._discardChanges(); }, + /** + * Used to detect if the discard button is about to be clicked. + * Some focusout events might occur and trigger a save which + * is not always wanted when clicking "Discard". + * + * @param {MouseEvent} ev + * @private + */ + _onDiscardMousedown: function (ev) { + var self = this; + this.fieldChangedPrevented = true; + window.addEventListener('mouseup', function (mouseupEvent) { + var preventedEvent = self.fieldChangedPrevented; + self.fieldChangedPrevented = false; + // If the user starts clicking (mousedown) on the button and stops clicking + // (mouseup) outside of the button, we want to trigger the original onFieldChanged + // Event that was prevented in the meantime. + if (ev.target !== mouseupEvent.target && preventedEvent.constructor.name === 'OdooEvent') { + self._onFieldChanged(preventedEvent); + } + }, { capture: true, once: true }); + }, /** * Called when the user asks to edit a row -> Updates the controller buttons * @@ -738,7 +762,9 @@ var ListController = BasicController.extend({ ev.stopPropagation(); const recordId = ev.data.dataPointID; - if (this.renderer.inMultipleRecordEdition(recordId)) { + if (this.fieldChangedPrevented) { + this.fieldChangedPrevented = ev; + } else if (this.renderer.inMultipleRecordEdition(recordId)) { // deal with edition of multiple lines const _onSuccess = ev.data.onSuccess; ev.data.onSuccess = () => { -- cgit v1.2.3 From 6a50a125f07a7bec1e45c0b71e8e7438d02b4afa Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Mon, 9 Sep 2019 11:05:46 +0000 Subject: [FIX] web: readonly many2one in list views Let's assume an editable list with a many2one and a char, the many2one being readonly. Before this rev., if the user clicked on the many2one, the row was switched in edit more, and the many2one link was given the focus. Now, in the same scenario, the focus is given to the char field which can be edited (i.e. its input). Task 2068280 --- addons/web/static/src/js/fields/relational_fields.js | 16 ++++++++++++++++ addons/web/static/src/xml/base.xml | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index b2d3379aef1..f713342c240 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -821,6 +821,22 @@ var Many2oneBarcode = FieldMany2One.extend({ var ListFieldMany2One = FieldMany2One.extend({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * If in readonly, will never be considered as an active widget. + * + * @override + */ + activate: function () { + if (this.mode === 'readonly') { + return false; + } + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index db6ccf777be..f52818d5ed6 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -712,8 +712,8 @@ - - + +
-- cgit v1.2.3 From 6ecf325769381b495ca53f102825c5dafe9b8712 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 11 Sep 2019 08:22:52 +0000 Subject: [FIX] web: list: multi edit: do not create new row ...when validating last row by pressing ENTER. In single edition, we automatically add a new row when the user edits the last one and presses ENTER. This is something we don't want in multi edition. Task 2068280 --- addons/web/static/src/js/views/list/list_controller.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src') 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 96f3f1cd556..a78f2bc61b0 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -766,12 +766,9 @@ var ListController = BasicController.extend({ this.fieldChangedPrevented = ev; } else if (this.renderer.inMultipleRecordEdition(recordId)) { // deal with edition of multiple lines - const _onSuccess = ev.data.onSuccess; ev.data.onSuccess = () => { - Promise.resolve(_onSuccess()).then(() => { - const savedRecordsPromise = this._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes); - this.multipleRecordsSavingPromise = savedRecordsPromise; - }); + this.multipleRecordsSavingPromise = + this._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes); }; } this._super.apply(this, arguments); -- cgit v1.2.3 From 4a3315fa2245cea287bf02e1eff2516e80ed62a9 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Wed, 11 Sep 2019 12:38:34 +0000 Subject: [FIX] web: list: multi edition only between selected records Before this commit, if a record was selected and another one was edited, it was considered as multi edition. Now, the multi edition only works if the edited record is included in the selection; else it will be the only edited record (single edition). Task 2068280 --- addons/web/static/src/js/views/list/list_editable_renderer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'addons/web/static/src') 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 d9cc1115917..1aec18f6fb7 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 @@ -339,9 +339,7 @@ ListRenderer.include({ * @returns {boolean} */ inMultipleRecordEdition: function (recordId) { - const record = this._getRecord(recordId) || {}; - const recordIds = [...new Set([recordId, ...this.selection])]; - return this.editable && recordIds.length > 1 && record.res_id; + return this.editable && this.selection.length > 1 && this.selection.includes(recordId); }, /** * Removes the line associated to the given recordID (the index of the row -- cgit v1.2.3 From 3615fb3c7f1bfeb9adeec251cf33cda50f1e7324 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 12 Sep 2019 06:30:11 +0000 Subject: [FIX] web: list: disable record selectors when editing Having those inputs enabled can lead to weird situations where a record has been modified but can't be saved anymore (because the user clicked on one of those record selectors to focusout the edited row). Also we don't want the user to start multi-editing a field, then change the selection, and then validate the changes. Task 2068280 --- .../src/js/views/list/list_editable_renderer.js | 30 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src') 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 1aec18f6fb7..deb29554798 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 @@ -356,6 +356,7 @@ ListRenderer.include({ } if ($row.prop('rowIndex') - 1 === this.currentRow) { this.currentRow = null; + this._enableRecordSelectors(); } // destroy widgets first @@ -450,7 +451,11 @@ ListRenderer.include({ // Toggle selected class here so that style is applied at the end $row.toggleClass('o_selected_row', editMode); - $row.find('.o_list_record_selector input').prop('disabled', !record.res_id); + if (editMode) { + this._disableRecordSelectors(); + } else { + this._enableRecordSelectors(); + } return Promise.all(defs).then(function () { // necessary to trigger resize on fieldtexts @@ -492,6 +497,7 @@ ListRenderer.include({ onFailure: reject, }); }).then(changedFields => { + this._enableRecordSelectors(); // If any field has changed and if the list is in multiple edition, // we send a truthy boolean to _selectRow to tell it not to select // the following record. @@ -541,7 +547,14 @@ ListRenderer.include({ el.addEventListener(type, callback, options); this.eventListeners.push({ type, el, callback, options }); }, - + /** + * When editing a row, we want to disable all record selectors. + * + * @private + */ + _disableRecordSelectors: function () { + this.$('.o_list_record_selector input').attr('disabled', 'disabled'); + }, /** * Destroy all field widgets corresponding to a record. Useful when we are * removing a useless row. @@ -555,6 +568,12 @@ ListRenderer.include({ delete this.allFieldWidgets[recordID]; } }, + /** + * @private + */ + _enableRecordSelectors: function () { + this.$('.o_list_record_selector input').attr('disabled', false); + }, /** * This function freezes the column widths and forces a fixed table-layout, * once the browser has computed the optimal width of each column according @@ -637,6 +656,7 @@ ListRenderer.include({ return fixedWidths[type] || '1'; }, /** + * @private * * @returns {integer} */ @@ -1193,7 +1213,10 @@ ListRenderer.include({ return new Promise(function (resolve) { self.trigger_up('edit_line', { recordId: recordId, - onSuccess: resolve, + onSuccess: function () { + self._disableRecordSelectors(); + resolve(); + }, }); }); }); @@ -1384,6 +1407,7 @@ ListRenderer.include({ this.trigger_up('discard_changes', { recordID: ev.target.dataPointID, onSuccess: function () { + self._enableRecordSelectors(); var recordId = self._getRecordID(rowIndex); if (recordId) { var correspondingRow = self._getRow(recordId); -- cgit v1.2.3 From 38e7f6f95b54504d3f2b3d572351daead1fdd3d8 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 12 Sep 2019 08:05:59 +0000 Subject: [FIX] web: date(time) fields: trigger fieldChanged once Before this rev., the 'fieldChanged' event was triggered each time the user selected a value (a day, an hour, a minute or a second) in the datepicker. This means that if there was an onchange on the field, a lot of RPCs could have been done when a user set a datetime. In addition, in a list view with multi edition, the user was asked to save the changes at each step of the datetime selection, making the feature unusable. Task 2068280 --- addons/web/static/src/js/widgets/date_picker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index ac8e83c8e30..3f528fe7c37 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -12,7 +12,6 @@ var DateWidget = Widget.extend({ template: "web.datepicker", type_of_date: "date", events: { - 'change.datetimepicker': 'changeDatetime', 'error.datetimepicker': 'errorDatetime', 'change .o_datepicker_input': 'changeDatetime', 'click input': '_onInputClicked', @@ -257,6 +256,7 @@ var DateWidget = Widget.extend({ if (this._onScroll) { window.removeEventListener('scroll', this._onScroll, true); } + this.changeDatetime(); }, /** * Reacts to the datetimepicker being shown -- cgit v1.2.3 From 34e5271d7b05a20ac7609f2bd8a2ca1561326e94 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 12 Sep 2019 08:31:05 +0000 Subject: [FIX] web: list: multi edit input of date(time) fields The Date(Time)Fields listen to 'input' and 'change' events (like all InputFields) to keep track of changes. However, the datepicker already listens to the 'change' event. As a consequence, in multi edition in a list view, if the user changed the value of a date(time) field by directly editing the input, the fieldChanged event was triggered twice, and there were 2 dialogs asking the user if he wanted to save the change. This rev. makes the Date(Time)Fields stop listening themselves to 'input' and 'change' events, and instead completely rely on the datepicker widget. Task 2068280 --- addons/web/static/src/js/fields/basic_fields.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index da46488cffa..3dc90ea6afe 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -684,6 +684,9 @@ var FieldDate = InputField.extend({ className: "o_field_date", tagName: "span", supportedFieldTypes: ['date', 'datetime'], + // we don't need to listen on 'input' nor 'change' events because the + // datepicker widget is already listening, and will correctly notify changes + events: AbstractField.prototype.events, /** * @override -- cgit v1.2.3 From 4fba43c027fcc52066ad5149a7fd08bde1109806 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Thu, 12 Sep 2019 09:01:28 +0000 Subject: [FIX] web: lists: focus last edited cell Gave back focus to the last edited cell to increase usability with key nav. Before this commit, after saving several records with multi edition in a list, focus was lost (given to the body). Now, last edited cell is given back the focus after multi editing. --- addons/web/static/src/js/views/list/list_controller.js | 18 +++++++++++++----- .../static/src/js/views/list/list_editable_renderer.js | 11 +++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src') 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 a78f2bc61b0..32c05537439 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -477,6 +477,7 @@ var ListController = BasicController.extend({ this.model.discardChanges(recordId); return this._confirmSave(recordId).then(reject); }; + let dialog; if (validRecordIds.length > 0) { let message; if (nbInvalid === 0) { @@ -488,13 +489,13 @@ var ListController = BasicController.extend({ _t("Do you want to set the value on the %d valid selected records? (%d invalid)"), validRecordIds.length, nbInvalid); } - Dialog.confirm(this, message, { + dialog = Dialog.confirm(this, message, { confirm_callback: () => { - this.model.saveRecords(recordId, validRecordIds, fieldName) - .then(() => { + return this.model.saveRecords(recordId, validRecordIds, fieldName) + .then(async () => { this._updateButtons('readonly'); const state = this.model.get(this.handle); - this.renderer.updateState(state, {keepWidths: true}); + await this.renderer.updateState(state, {keepWidths: true}); resolve(Object.keys(changes)); }) .guardedCatch(rejectAndDiscard); @@ -502,10 +503,17 @@ var ListController = BasicController.extend({ cancel_callback: rejectAndDiscard, }); } else { - Dialog.alert(this, _t("No valid record to save"), { + dialog = Dialog.alert(this, _t("No valid record to save"), { confirm_callback: rejectAndDiscard, }); } + dialog.on('closed', this, async () => { + // we need to wait for the dialog to be actually closed, but + // the 'closed' event is triggered just before, and it prevents + // from focussing the cell + await Promise.resolve(); + this.renderer.focusCell(recordId, node); + }); }); }, /** 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 deb29554798..e2393ef61e3 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 @@ -319,6 +319,17 @@ ListRenderer.include({ var rowIndex = $row.prop('rowIndex') - 1; this._selectCell(rowIndex, 0); }, + /** + * Gives focus to a specific cell, given its row and its related column. + * + * @param {string} recordId + * @param {Object} column + */ + focusCell: function (recordId, column) { + var $row = this._getRow(recordId); + var cellIndex = this.columns.indexOf(column); + $row.find('.o_data_cell')[cellIndex].focus(); + }, /** * Returns the recordID associated to the line which is currently in edition * or null if there is no line in edition. -- cgit v1.2.3 From c7ec4bb39d5a52517acb23429a4d7fc71abbf363 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 13 Sep 2019 07:36:33 +0000 Subject: [FIX] web: list: multi edition: disable onchanges Onchanges make no sense in multi edition as they would be computed on the 'reference' record (the one use to edit), but applied on all selected records. This rev. simply disables them in that case. Task 2068280 --- addons/web/static/src/js/views/list/list_controller.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'addons/web/static/src') 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 32c05537439..e096a157225 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -778,6 +778,8 @@ var ListController = BasicController.extend({ this.multipleRecordsSavingPromise = this._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes); }; + // disable onchanges as we'll save directly + ev.data.notifyChange = false; } this._super.apply(this, arguments); }, -- cgit v1.2.3 From 708a5b656618e1bcc8034bd7feddfcb5aac6a998 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 13 Sep 2019 09:28:31 +0000 Subject: [FIX] web: list: multi edit boolean with ENTER This rev. solves the similar issue as 801c603218, but with boolean fields. Scenario: select several records, select a boolean cell, press enter to change the boolean value. Task 2068280 --- addons/web/static/src/js/fields/basic_fields.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 3dc90ea6afe..a2f4917b699 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1083,6 +1083,8 @@ var FieldBoolean = AbstractField.extend({ _onKeydown: function (ev) { switch (ev.which) { case $.ui.keyCode.ENTER: + // prevent subsequent 'click' event (see _onKeydown of AbstractField) + ev.preventDefault(); this.$input.prop('checked', !this.value); this._setValue(!this.value); return; -- cgit v1.2.3 From ff9a0b87e63fd7d2b38514d21a52cbad7702582d Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 6 Sep 2019 12:33:28 +0000 Subject: [FIX] web: list: m2o doesn't trigger fieldChange when empty Gives time to the user to enter an actual value in m2o in list views. Before this commit, emptying a many2one field (selecting all > BACKSPACE or DELETE) triggered instantly a field_change. This was an issue in multi edition as a field changed triggered instantly a save on selected records without giving time to the user to enter a new value. Now, many2ones only trigger a fieldChange when selecting a new valid value. Task 2068280 --- .../web/static/src/js/fields/relational_fields.js | 78 ++++++++++++++++++++-- addons/web/static/src/js/views/view_dialogs.js | 5 ++ 2 files changed, 79 insertions(+), 4 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index f713342c240..1646ef735a2 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -397,8 +397,10 @@ var FieldMany2One = AbstractField.extend({ kanban_view_ref: this.attrs.kanban_view_ref, on_selected: function (records) { self.reinitialize(records[0]); + }, + on_closed: function () { self.activate(); - } + }, }; }, /** @@ -650,7 +652,6 @@ var FieldMany2One = AbstractField.extend({ * in the dialog (each filter has keys 'description' and 'domain') */ _searchCreatePopup: function (view, ids, context, dynamicFilters) { - var self = this; var options = this._getSearchCreatePopupOptions(view, ids, context, dynamicFilters); return new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, options)).open(); }, @@ -820,6 +821,23 @@ var Many2oneBarcode = FieldMany2One.extend({ }); var ListFieldMany2One = FieldMany2One.extend({ + events: _.extend({}, FieldMany2One.prototype.events, { + 'focusin input': '_onInputFocusin', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + // when we empty the input, we delay the setValue to prevent from + // triggering the 'fieldChanged' event twice when the user wants set + // another m2o value ; the following attribute is used to determine when + // we skipped the setValue, s.t. we can perform it later on if the user + // didn't select another value + this.mustSetValue = false; + this.m2oDialogFocused = false; + }, //-------------------------------------------------------------------------- // Public @@ -836,6 +854,13 @@ var ListFieldMany2One = FieldMany2One.extend({ } return this._super.apply(this, arguments); }, + /** + * @override + */ + reinitialize: function () { + this.mustSetValue = false; + return this._super.apply(this, arguments); + }, //-------------------------------------------------------------------------- // Private @@ -847,6 +872,52 @@ var ListFieldMany2One = FieldMany2One.extend({ _renderReadonly: function () { this.$el.text(this.m2o_value); }, + /** + * @override + * @private + */ + _searchCreatePopup: function () { + this.m2oDialogFocused = true; + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onInputFocusin: function () { + this.m2oDialogFocused = false; + }, + /** + * In list views, we don't want to try to trigger a fieldChange when the field + * is being emptied. Instead, it will be triggered as the user leaves the field + * while it is empty. + * + * @override + * @private + */ + _onInputFocusout: function () { + this._super.apply(this, arguments); + if (!this.m2oDialogFocused && this.$input.val() === "" && this.mustSetValue) { + this.reinitialize(false); + } + }, + /** + * Prevents the triggering of an immediate _onFieldChanged when emptying the field. + * + * @override + * @private + */ + _onInputKeyup: function () { + if (this.$input.val() !== "") { + this._super.apply(this, arguments); + } else { + this.mustSetValue = true; + } + }, }); var KanbanFieldMany2One = AbstractField.extend({ @@ -2220,8 +2291,7 @@ var FieldMany2ManyTags = AbstractField.extend({ disable_multiple_selection: false, on_selected: function (records) { self.many2one.reinitialize(records); - self.many2one.activate(); - } + }, }); }; return this.many2one.appendTo(this.$el); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 130173a16fc..47e3a5309d9 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -320,6 +320,7 @@ var SelectCreateDialog = ViewDialog.extend({ this._super.apply(this, arguments); _.defaults(this.options, { initial_view: 'search' }); this.on_selected = this.options.on_selected || (function () {}); + this.on_closed = this.options.on_closed || (function () {}); this.initialIDs = this.options.initial_ids; this.viewType = 'list'; }, @@ -395,6 +396,10 @@ var SelectCreateDialog = ViewDialog.extend({ return fragment; }); }, + close: function () { + this._super.apply(this, arguments); + this.on_closed(); + }, create_edit_record: function () { var self = this; var dialog = new FormViewDialog(this, _.extend({}, this.options, { -- cgit v1.2.3 From 62452aad419c36ced9c8c634f403006809221d4e Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Tue, 17 Sep 2019 14:40:31 +0000 Subject: [FIX] web: handling of invalid formats in list views Added handling of invalid format types in multiple edition in list views. Before this commit, in multiple edition, a field that was edited with an invalid value format (e.g. a numeric field receiving a string value) didn't trigger a field_changed and no prompt was shown; the record was simply idle with an dirty value. Now, the list controller also listents for set_dirty events and automatically triggers an error modal + discards all changes when receiving one. --- .../static/src/js/views/list/list_controller.js | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'addons/web/static/src') 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 e096a157225..4e0060fa5ff 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -805,6 +805,29 @@ var ListController = BasicController.extend({ this.selectedRecords = ev.data.selection; this._toggleSidebar(); }, + /** + * If the record is set as dirty while in multiple record edition, + * we want to immediatly discard the change. + * + * @private + * @override + * @param {OdooEvent} ev + */ + _onSetDirty: function (ev) { + var self = this; + var recordId = ev.data.dataPointID; + if (this.renderer.inMultipleRecordEdition(recordId)) { + ev.stopPropagation(); + Dialog.alert(this, _t("No valid record to save"), { + confirm_callback: function () { + self.model.discardChanges(recordId); + self._confirmSave(recordId); + }, + }); + } else { + this._super.apply(this, arguments); + } + }, /** * When the user clicks on one of the sortable column headers, we need to * tell the model to sort itself properly, to update the pager and to -- cgit v1.2.3 From 8b6f3580d8bceb3f9bdf18917cb48d3407eee0aa Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Tue, 17 Sep 2019 15:26:35 +0000 Subject: [FIX] web: list selectors synchronisation Changed the way the header selector behave when the selection changes. Before this commit, the header selector was only checked when clicked and was unchecked as soon as any other record was unchecked. Now, the header selector reflects the current visible selection: if all of the other visible selectors are checked, it will be checked. Folded records are not considered visible, thus only records from active groups and from the current pager will be taken into account. Task 2068280 --- .../web/static/src/js/views/list/list_renderer.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'addons/web/static/src') 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 94609544b10..5daa33125a7 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -996,11 +996,18 @@ var ListRenderer = BasicRenderer.extend({ * @private */ _updateSelection: function () { - var $selectedRows = this.$('tbody .o_list_record_selector input:checked') - .closest('tr'); - this.selection = _.map($selectedRows, function (row) { - return $(row).data('id'); + this.selection = []; + var self = this; + var $inputs = this.$('tbody .o_list_record_selector input:visible:not(:disabled)'); + var allChecked = $inputs.length > 0; + $inputs.each(function (index, input) { + if (input.checked) { + self.selection.push($(input).closest('tr').data('id')); + } else { + allChecked = false; + } }); + this.$('thead .o_list_record_selector input').prop('checked', allChecked); this.trigger_up('selection_changed', { selection: this.selection }); this._updateFooter(); }, @@ -1186,9 +1193,6 @@ var ListRenderer = BasicRenderer.extend({ _onSelectRecord: function (ev) { ev.stopPropagation(); this._updateSelection(); - if (!$(ev.currentTarget).find('input').prop('checked')) { - this.$('thead .o_list_record_selector input').prop('checked', false); - } }, /** * @private @@ -1209,7 +1213,8 @@ var ListRenderer = BasicRenderer.extend({ if (group.count) { this.trigger_up('toggle_group', { group: group, - onSuccess: function () { + onSuccess: () => { + this._updateSelection(); // Refocus the header after re-render unless the user // already focused something else by now if (document.activeElement.tagName === 'BODY') { -- cgit v1.2.3 From 6c15557a874d92c9aaaee1d92006870c27ce22fe Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 4 Sep 2019 12:29:20 +0000 Subject: [FIX] web: editable list: re-compute widths on window resize This rev. adds a (debounced) handler to react on window resize events in editable list views, in order to recompute the (fixed) column widths. Task 2067477 --- .../src/js/views/list/list_editable_renderer.js | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src') 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 e2393ef61e3..6f21653b70c 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 @@ -19,6 +19,7 @@ var utils = require('web.utils'); var _t = core._t; ListRenderer.include({ + RESIZE_DELAY: 200, custom_events: _.extend({}, ListRenderer.prototype.custom_events, { navigation_move: '_onNavigationMove', }), @@ -109,6 +110,7 @@ ListRenderer.include({ start: function () { if (this.editable) { core.bus.on('click', this, this._onWindowClicked.bind(this)); + core.bus.on('resize', this, _.debounce(this._onResize.bind(this), this.RESIZE_DELAY)); } return this._super(); }, @@ -609,9 +611,18 @@ ListRenderer.include({ const table = this.el.getElementsByTagName('table')[0]; const thead = table.getElementsByTagName('thead')[0]; + // Set table layout auto and remove inline style to make sure that css + // rules apply (e.g. fixed width of record selector) + table.style.tableLayout = 'auto'; + [...table.getElementsByTagName('th')].forEach((th) => { + th.style.width = null; + }); + // 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`; + const thElements = [...thead.getElementsByTagName('th')]; + const thWidths = thElements.map((th) => th.offsetWidth); + thElements.forEach((th, index) => { + th.style.width = `${this.columnWidths ? this.columnWidths[index] : thWidths[index]}px`; }); // Set the table layout to fixed @@ -1452,6 +1463,15 @@ ListRenderer.include({ }); } }, + /** + * React to window resize events by recomputing the width of each column. + * + * @private + */ + _onResize: function () { + this.columnWidths = false; + this._freezeColumnWidths(); + }, /** * If the list view editable, just let the event bubble. We don't want to * open the record in this case anyway. -- cgit v1.2.3 From eafe7eae79507438482cdd0c02c97b02ac474d90 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 5 Sep 2019 13:27:02 +0000 Subject: [FIX] web: list: optional fields icon above resize handle Because of the z-index, the resize handle of the last column was over the optional fields dropdown icon, making it impossible to click on. The z-index seems unnecessary, so we remove it. Task 2067477 closes odoo/odoo#36987 Signed-off-by: VincentSchippefilt --- addons/web/static/src/scss/list_view.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 0d29c3cec21..0e62f071d42 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -201,7 +201,7 @@ right: 0; top: 0; width: 10px; - z-index: 10; + z-index: 1; // must be over the sort caret (::after pseudo-element) } .o_list_record_remove_header { @@ -294,9 +294,11 @@ } .o_optional_columns_dropdown_toggle { + cursor: pointer; width: 10px; text-align: center; line-height: 30px; + z-index: 1; // must be over the resize handle } .o_optional_columns { -- cgit v1.2.3 From 16f23e8246db4da4eb34552f360974c2a15f553b Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 19 Sep 2019 06:19:05 +0000 Subject: [FIX] web: fix m2mtags widget to be able to use 'SAVE & NEW' PURPOSE Previously when the 'SAVE & NEW' button was pressed in the 'create and edit...' menu of the many2many_tags widget was pressed, the modal window would close and the button would act exactly like the 'SAVE & CLOSE' button. This was due to the fact that whenever a new record was created and added to the field, it would reset the many2one field it uses for display, which would destroy the edit modal. This commit fixes that by not notifying the underlying many2one field of any change until the modal is closed by the user. This has the disadvantage that tags display, if visible behind the modal 'shade', will not be updated until the modal is closed, but restores the functionality of the save&new button. It also adds a test for this behaviour task-2070454 closes odoo/odoo#37114 Signed-off-by: Aaron Bohy (aab) --- addons/web/static/src/js/fields/relational_fields.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 1646ef735a2..2a56c6c93e8 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -2286,11 +2286,15 @@ var FieldMany2ManyTags = AbstractField.extend({ this.many2one._getSearchCreatePopupOptions = function (view, ids, context, dynamicFilters) { var options = _getSearchCreatePopupOptions.apply(this, arguments); var domain = this.record.getDomain({fieldName: this.name}); + var m2mRecords = []; return _.extend({}, options, { domain: domain.concat(["!", ["id", "in", self.value.res_ids]]), disable_multiple_selection: false, on_selected: function (records) { - self.many2one.reinitialize(records); + m2mRecords.push(...records); + }, + on_closed: function () { + self.many2one.reinitialize(m2mRecords); }, }); }; -- cgit v1.2.3 From b6288e54461426d7aac6cfc549cee3e90309a093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Thu, 19 Sep 2019 10:23:39 +0000 Subject: [IMP] *: remove image_64 and clean views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are too many image sizes. Since they are stored resized this takes time to generate when saving a new image, it's more rows on the attachment table, more files on the disk, ... 64px is close enough to 128px that it can be removed without a big impact on download size. It will even reduce download and number of requests when both images are displayed because now only one has to be downloaded and then benefit from cache. The difference between the two is typically around 1.5kB which is negligible these days, especially when the request overhead is around 0.5kB already, not even taking into account other factors such as latency. If a 64px image must absolutely be returned, it is still possible to pass the size parameters to the image route. But the current guideline is to handle resizing in the views when necessary. Views ===== - remove width and height attributes when existing CSS rules are overriding them (eg. `.oe_kanban_avatar` in the right context) - add CSS rules instead of width and height attributes when possible - use `object-fit: cover;` where width and height are forced to avoid distortion of non-square images - for products, use `object-fit: contain;` instead, keep ratio but without crop - add new CSS rules where the expected size was max 64px*64px before due to the image size itself - remove `img-fluid` where using size classes to avoid conflicting rules task-2060865 closes odoo/odoo#36147 Signed-off-by: Sébastien Theys (seb) --- addons/web/static/src/js/chrome/user_menu.js | 2 +- .../web/static/src/js/fields/relational_fields.js | 2 +- .../static/src/js/views/calendar/calendar_view.js | 2 +- addons/web/static/src/scss/fields.scss | 1 + addons/web/static/src/scss/kanban_view.scss | 1 + addons/web/static/src/scss/ui.scss | 38 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/chrome/user_menu.js b/addons/web/static/src/js/chrome/user_menu.js index 3b93bbd9c3b..ffc23e53273 100644 --- a/addons/web/static/src/js/chrome/user_menu.js +++ b/addons/web/static/src/js/chrome/user_menu.js @@ -46,7 +46,7 @@ var UserMenu = Widget.extend({ self.$('.oe_topbar_name').text(topbar_name); var avatar_src = session.url('/web/image', { model:'res.users', - field: 'image_64', + field: 'image_128', id: session.uid, }); $avatar.attr('src', avatar_src); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 2a56c6c93e8..b89900c9b3b 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -2381,7 +2381,7 @@ var FieldMany2ManyTagsAvatar = FieldMany2ManyTags.extend({ _getRenderTagsContext: function () { var result = this._super.apply(this, arguments); result.avatarModel = this.nodeOptions.avatarModel || this.field.relation; - result.avatarField = this.nodeOptions.avatarField || 'image_64'; + result.avatarField = this.nodeOptions.avatarField || 'image_128'; return result; }, }); diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js index 232a7316fa5..1b081316f20 100644 --- a/addons/web/static/src/js/views/calendar/calendar_view.js +++ b/addons/web/static/src/js/views/calendar/calendar_view.js @@ -105,7 +105,7 @@ var CalendarView = AbstractView.extend({ }; if (fields[fieldName].relation) { if (['res.users', 'res.partner', 'hr.employee'].indexOf(fields[fieldName].relation) !== -1) { - filters[fieldName].avatar_field = 'image_64'; + filters[fieldName].avatar_field = 'image_128'; } filters[fieldName].avatar_model = fields[fieldName].relation; } diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index b9f85f2d5c0..e3cba4e3770 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -196,6 +196,7 @@ img { height: 18px; width: 18px; + object-fit: cover; } &.o_tag_color_0 { background-color: #F1F1F1; diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss index 18c96fb6171..a46d4632c24 100644 --- a/addons/web/static/src/scss/kanban_view.scss +++ b/addons/web/static/src/scss/kanban_view.scss @@ -122,6 +122,7 @@ .oe_kanban_avatar { width: 20px; height: 20px; + object-fit: cover; margin-left: 6px; } } diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss index 9ab1729a4ac..5a46c9c01a8 100644 --- a/addons/web/static/src/scss/ui.scss +++ b/addons/web/static/src/scss/ui.scss @@ -91,3 +91,41 @@ span.o_force_ltr { /*rtl:ignore*/ direction: ltr; } + +// To fill the available space while keeping aspect ratio (crop). +// Assuming the important part of the image is its center. +.o_object_fit_cover { + object-fit: cover; +} + +.o_image_24_cover { + width: 24px; + height: 24px; + object-fit: cover; +} + +.o_image_40_cover { + width: 40px; + height: 40px; + object-fit: cover; +} + +.o_image_64_cover { + width: 64px; + height: 64px; + object-fit: cover; +} + +// Keep ratio but avoid cropping (so part of the background becomes visible). +.o_image_64_contain { + width: 64px; + height: 64px; + object-fit: contain; +} + +// When having a square is not necessary, the image will take less space if any +// of its width or height is smaller than 64px. +.o_image_64_max { + max-width: 64px; + max-height: 64px; +} -- cgit v1.2.3 From 0fdc914a67bc81d3b9fd64895a3808cb3b46baa7 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 11 Sep 2019 12:39:56 +0000 Subject: [FIX] web: search panel handles archived parent category Have a model with a parent field Set his parents like Parent 0 | -- Parent 1 | -- Parent 2 Now, put the Parent 1 as inactive Open a view with a search panel, which will fetch the parent field as categories Before this commit, it crashed, because the inner record memory did not contain a reference to the parent of Parent 2 After this commit, it doesn't crash, and the search panel just doesn't display parents categories that are archived, nor any children of those OPW 2070266 closes odoo/odoo#36699 Signed-off-by: Aaron Bohy (aab) --- addons/web/static/src/js/views/kanban/search_panel.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/kanban/search_panel.js b/addons/web/static/src/js/views/kanban/search_panel.js index d06eacbdb07..b7d2432426f 100644 --- a/addons/web/static/src/js/views/kanban/search_panel.js +++ b/addons/web/static/src/js/views/kanban/search_panel.js @@ -140,8 +140,9 @@ var SearchPanel = Widget.extend({ }); Object.keys(category.values).forEach(function (valueId) { var value = category.values[valueId]; - if (value.parentId) { - category.values[value.parentId].childrenIds.push(value.id); + var parentCategoryId = value.parentId; + if (parentCategoryId && parentCategoryId in category.values) { + category.values[parentCategoryId].childrenIds.push(value.id); } }); category.rootIds = Object.keys(category.values).filter(function (valueId) { -- cgit v1.2.3 From 057d3eba2e07fd035a826f5eeed651149ced469f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 12 Sep 2019 09:00:39 +0000 Subject: [FIX] web: use search view input with composition mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forward-port of #36688 (original: #36284) Before this commit, search view was barely usable with Japanese IME. For instance, when pressing ENTER while in composition mode and IME prompts some suggestions, it made a search with the updated input content. The expected behaviour should have been to update the input content, without making the search yet. This commit fixes the issue by turning off some actions of the search view when typing during composition mode. opw-2061590 closes odoo/odoo#36728 Signed-off-by: Alexandre Kühn (aku) --- .../src/js/views/control_panel/search/search_bar.js | 20 ++++++++++++++++++++ .../js/views/control_panel/search/search_facet.js | 20 ++++++++++++++++++++ addons/web/static/src/js/widgets/auto_complete.js | 14 ++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src') 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 6248489922c..9063b2aca50 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 @@ -9,6 +9,8 @@ var Widget = require('web.Widget'); var SearchBar = Widget.extend({ template: 'SearchView.SearchBar', events: _.extend({}, Widget.prototype.events, { + 'compositionend .o_searchview_input': '_onCompositionendInput', + 'compositionstart .o_searchview_input': '_onCompositionstartInput', 'keydown': '_onKeydown', }), /** @@ -34,6 +36,7 @@ var SearchBar = Widget.extend({ this.autoCompleteSources = []; this.searchFacets = []; + this._isInputComposing = false; }, /** * @override @@ -207,11 +210,28 @@ var SearchBar = Widget.extend({ }); } }, + /** + * @rivate + * @param {CompositionEvent} ev + */ + _onCompositionendInput: function () { + this._isInputComposing = false; + }, + /** + * @rivate + * @param {CompositionEvent} ev + */ + _onCompositionstartInput: function () { + this._isInputComposing = true; + }, /** * @private * @param {KeyEvent} e */ _onKeydown: function (e) { + if (this._isInputComposing) { + return; + } switch(e.which) { case $.ui.keyCode.LEFT: this._focusPreceding(); diff --git a/addons/web/static/src/js/views/control_panel/search/search_facet.js b/addons/web/static/src/js/views/control_panel/search/search_facet.js index ca9a7ad6225..3f18597cccf 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_facet.js +++ b/addons/web/static/src/js/views/control_panel/search/search_facet.js @@ -10,6 +10,8 @@ var SearchFacet = Widget.extend({ template: 'SearchView.SearchFacet', events: _.extend({}, Widget.prototype.events, { 'click .o_facet_remove': '_onFacetRemove', + 'compositionend': '_onCompositionend', + 'compositionstart': '_onCompositionstart', 'keydown': '_onKeydown', }), /** @@ -26,6 +28,7 @@ var SearchFacet = Widget.extend({ }); this.separator = this._getSeparator(); this.icon = this._getIcon(); + this._isComposing = false; }, //-------------------------------------------------------------------------- @@ -105,6 +108,20 @@ var SearchFacet = Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * @private + * @param {CompositionEvent} ev + */ + _onCompositionend: function (ev) { + this._isComposing = false; + }, + /** + * @private + * @param {CompositionEvent} ev + */ + _onCompositionstart: function (ev) { + this._isComposing = true; + }, /** * @private */ @@ -116,6 +133,9 @@ var SearchFacet = Widget.extend({ * @param {KeyboardEvent} ev */ _onKeydown: function (ev) { + if (this._isComposing) { + return; + } switch (ev.which) { case $.ui.keyCode.BACKSPACE: this.trigger_up('facet_removed', {group: this.facet}); diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js index 1bb00be8d38..30c38690d17 100644 --- a/addons/web/static/src/js/widgets/auto_complete.js +++ b/addons/web/static/src/js/widgets/auto_complete.js @@ -29,16 +29,23 @@ return Widget.extend({ this.searching = true; this.search_string = ''; this.current_search = null; + this._isInputComposing = false; }, start: function () { var self = this; + this.$input.on('compositionend', function (ev) { + self._isInputComposing = false; + }); + this.$input.on('compositionstart', function (ev) { + self._isInputComposing = true; + }); this.$input.on('keyup', function (ev) { - if (ev.which === $.ui.keyCode.RIGHT) { + if (ev.which === $.ui.keyCode.RIGHT && !self._isInputComposing) { self.searching = true; ev.preventDefault(); return; } - if (ev.which === $.ui.keyCode.ENTER) { + if (ev.which === $.ui.keyCode.ENTER && !self._isInputComposing) { if (self.search_string.length) { self.select_item(ev); } @@ -68,6 +75,9 @@ return Widget.extend({ } }); this.$input.on('keydown', function (ev) { + if (self._isInputComposing) { + return; + } switch (ev.which) { case $.ui.keyCode.ENTER: -- cgit v1.2.3 From 26c62cbb4401bffb72ce77fee481ca84b77a8143 Mon Sep 17 00:00:00 2001 From: Romeo Fragomeli Date: Wed, 11 Sep 2019 13:06:37 +0000 Subject: [FIX] web: unable to scroll modal's table in Safari (iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, users wasn't able to scroll vertically when there was "table-responsive" class on modal's table. In fact the scroll was effective on the body and not in the modal content. We can fix this issue by having flex elements but as this is a fix, we preferred to choose a one line fix solution. Steps to reproduce : * open Odoo in Safari (iOS) * open Sale app * create a new quotation * select a customer * select "Search More ..." * try to scroll the table vertically (BUG) Sources : https://stackoverflow.com/questions/41681251/overflow-x-scroll-inside-a-fixed-div-prevents-vertical-scrolling-on-ios#comment84970666_41693350 https://stackoverflow.com/a/57566116 opw-2042095 closes odoo/odoo#36707 closes odoo/odoo#36730 Signed-off-by: Adrien Dieudonné (adr) Signed-off-by: Adrien Dieudonné (adr) --- addons/web/static/src/scss/modal.scss | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss index 52c3100df31..8976dc1e6e7 100644 --- a/addons/web/static/src/scss/modal.scss +++ b/addons/web/static/src/scss/modal.scss @@ -92,6 +92,11 @@ } } +body.modal-open { + // Allow vertical scrolling in modals on iOS + -webkit-overflow-scrolling: touch; +} + // Temporary fix for modals which are not instantiated thanks to the Dialog // JS classes (deprecated case) (see bootstrap_overridden.scss) + Frontend. // -- cgit v1.2.3 From 416f1794426af55132a19d388a2e5693f59dfde6 Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Mon, 16 Sep 2019 08:24:40 +0000 Subject: [FIX] web: pass the context when executing an action In case you have a button of type action, the controller is doing an rpc on the model using the right method, but in this rpc, the context is not passed. This creates a translation problem (nothing is translated) since the language is not defined in the context. Task Id 2062419 Closes #36189 closes odoo/odoo#36969 Signed-off-by: Yannick Tivisse (yti) --- addons/web/static/src/js/views/abstract_controller.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 63d59d153fd..5b5197469db 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -19,6 +19,8 @@ var config = require('web.config'); var core = require('web.core'); var mvc = require('web.mvc'); +var session = require('web.session'); + var QWeb = core.qweb; var AbstractController = mvc.Controller.extend(ActionMixin, { @@ -476,6 +478,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { this.dp.add(this._rpc({ model: data.model, method: data.method, + context: session.user_context, })).then(function (action) { if (action !== undefined) { self.do_action(action, options); -- cgit v1.2.3 From fa283d3d4eb73eab34d6ae5c8af5623ab07eea49 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Mon, 2 Sep 2019 13:39:38 +0000 Subject: [FIX] web: progress bar feature The progress bar was pretty broken before this commit. > It was not possible to input the value as text > Moving the progress bar to modify value and saving crashed > modifying the max value crashed > The whole feature was ill-defined With this commit, we fix the crashed. It is now possible to write the field behind the progress bar by moving it in RO mode or by typing the input in RW mode *if it is the value we want to write on* It it is the max value that the field targets, we can do it in RO and RW mode, only with the input as text Note that this works *only if* the editable option on the widget is set to true OPW 2061846 closes odoo/odoo#36928 closes odoo/odoo#36970 Signed-off-by: Lucas Perais (lpe) Signed-off-by: Lucas Perais (lpe) --- addons/web/static/src/js/fields/basic_fields.js | 101 ++++++++++++++++-------- 1 file changed, 66 insertions(+), 35 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index b3bf09ab88d..38276373cc2 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2306,11 +2306,28 @@ var FieldProgressBar = AbstractField.extend({ if (this.recordData[this.nodeOptions.current_value]) { this.value = this.recordData[this.nodeOptions.current_value]; } - this.max_value = this.recordData[this.nodeOptions.max_value] || 100; + + // The few next lines determine if the widget can write on the record or not + this.editable_readonly = !!this.nodeOptions.editable_readonly; + // "hard" readonly this.readonly = this.nodeOptions.readonly || !this.nodeOptions.editable; - this.edit_max_value = this.nodeOptions.edit_max_value || false; + + this.canWrite = !this.readonly && ( + this.mode === 'edit' || + (this.editable_readonly && this.mode === 'readonly') || + (this.viewType === 'kanban') // Keep behavior before commit + ); + + // Boolean to toggle if we edit the numerator (value) or the denominator (max_value) + this.edit_max_value = !!this.nodeOptions.edit_max_value; + this.max_value = this.recordData[this.nodeOptions.max_value] || 100; + this.title = _t(this.attrs.title || this.nodeOptions.title) || ''; - this.edit_on_click = !this.nodeOptions.edit_max_value || false; + + // Ability to edit the field through the bar + // /!\ this feature is disabled + this.enableBarAsInput = false; + this.edit_on_click = this.enableBarAsInput && this.mode === 'readonly' && !this.edit_max_value; this.write_mode = false; }, @@ -2318,19 +2335,19 @@ var FieldProgressBar = AbstractField.extend({ var self = this; this._render_value(); - if (!this.readonly) { + if (this.canWrite) { if (this.edit_on_click) { this.$el.on('click', '.o_progress', function (e) { var $target = $(e.currentTarget); - self.value = Math.floor((e.pageX - $target.offset().left) / $target.outerWidth() * self.max_value); + var numValue = Math.floor((e.pageX - $target.offset().left) / $target.outerWidth() * self.max_value); + self.on_update(numValue); self._render_value(); - self.on_update(self.value); }); } else { this.$el.on('click', function () { if (!self.write_mode) { var $input = $('', {type: 'text', class: 'o_progressbar_value o_input'}); - $input.on('blur', _.bind(self.on_change_input, self)); + $input.on('blur', self.on_change_input.bind(self)); self.$('.o_progressbar_value').replaceWith($input); self.write_mode = true; self._render_value(); @@ -2340,24 +2357,25 @@ var FieldProgressBar = AbstractField.extend({ } return this._super(); }, + /** + * Updates the widget with value + * + * @param {Number} value + */ on_update: function (value) { - if (!isNaN(value)) { - if (this.edit_max_value) { - try { - this.max_value = this._parseValue(value); - this._isValid = true; - } catch (e) { - this._isValid = false; - } - var changes = {}; - changes[this.nodeOptions.max_value] = this.max_value; - this.trigger_up('field_changed', { - dataPointID: this.dataPointID, - changes: changes, - }); - } else { - this._setValue(value); - } + if (this.edit_max_value) { + this.max_value = value; + this._isValid = true; + var changes = {}; + changes[this.nodeOptions.max_value] = this.max_value; + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: changes, + }); + } else { + // _setValues accepts string and will parse it + var formattedValue = this._formatValue(value); + this._setValue(formattedValue); } }, on_change_input: function (e) { @@ -2365,29 +2383,42 @@ var FieldProgressBar = AbstractField.extend({ if (e.type === 'change' && !$input.is(':focus')) { return; } - if (isNaN($input.val())) { - this.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid.")); - } else { - if (e.type === 'input') { - this._render_value($input.val()); - if (parseFloat($input.val()) === 0) { + + var parsedValue; + try { + // Cover all numbers with parseFloat + parsedValue = field_utils.parse.float($input.val()); + } catch (error) { + this.do_warn(_t("Wrong value entered!"), _t("Only Integer or Float Value should be valid.")); + } + + if (parsedValue !== undefined) { + if (e.type === 'input') { // ensure what has just been typed in the input is a number + // returns NaN if not a number + this._render_value(parsedValue); + if (parsedValue === 0) { $input.select(); } - } else { + } else { // Implicit type === 'blur': we commit the value if (this.edit_max_value) { - this.max_value = $(e.target).val(); - } else { - this.value = $(e.target).val() || 0; + parsedValue = parsedValue || 100; } + var $div = $('
', {class: 'o_progressbar_value'}); this.$('.o_progressbar_value').replaceWith($div); this.write_mode = false; + this.on_update(parsedValue); this._render_value(); - this.on_update(this.edit_max_value ? this.max_value : this.value); } } }, + /** + * Renders the value + * + * @private + * @param {Number} v + */ _render_value: function (v) { var value = this.value; var max_value = this.max_value; -- cgit v1.2.3 From fa9beab3eab77ddbc9d21cd1eb70ea0fe6be5b18 Mon Sep 17 00:00:00 2001 From: oco-odoo Date: Mon, 16 Sep 2019 11:31:31 +0000 Subject: [FIX] account: dashboard: Center the empty dashboard message horizontally when the setup bar is displayed on a small screen Before that, when using a small screen (with width <= 768 px) and showing the accounting dashboard of a company without any chart of accounts installed, the icon and message prompting to install a chart of accounts was displayed in the lower left corner, instead of being horizontally centered. closes odoo/odoo#36974 X-original-commit: 4999f47d7e283c6a5799e68f2b0e1351638e17a3 Signed-off-by: Nicolas Martinelli (nim) Co-authored-by: Nicolas Lempereur --- addons/web/static/src/scss/banner.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/scss/banner.scss b/addons/web/static/src/scss/banner.scss index d7237203897..be011d70698 100644 --- a/addons/web/static/src/scss/banner.scss +++ b/addons/web/static/src/scss/banner.scss @@ -1,8 +1,9 @@ .o_has_banner { .o_view_nocontent { top: 30%; - @media screen and (max-height: map-get($grid-breakpoints, 'md')) { + @include media-breakpoint-down(sm) { position: relative; + margin: auto; } } } -- cgit v1.2.3 From 149d2a3d60e9915632d4fa51b92ac658a8beeea2 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Fri, 20 Sep 2019 09:07:48 +0000 Subject: [FIX] web: always show the graph view button "Stacked" With the actual code, some computations are done in the graph renderer to determine if it is worth to show the "Stacked" button in the graph view control panel for the data on hand. It turns out that that problem is not satisfactorily addressed and is quite hard from a usability perspective. We prefer to restore the old behavior as long as we don't have better ideas. closes odoo/odoo#37188 Signed-off-by: VincentSchippefilt --- .../static/src/js/views/graph/graph_controller.js | 2 +- .../static/src/js/views/graph/graph_renderer.js | 29 +--------------------- 2 files changed, 2 insertions(+), 29 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js index 2953b99f731..1c580a25bd6 100644 --- a/addons/web/static/src/js/views/graph/graph_controller.js +++ b/addons/web/static/src/js/views/graph/graph_controller.js @@ -177,7 +177,7 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ .find('.o_graph_button[data-mode="stack"]') .data('stacked', state.stacked) .toggleClass('active', state.stacked) - .toggleClass('o_hidden', state.mode !== 'bar' || !this.renderer.isStackable); + .toggleClass('o_hidden', state.mode !== 'bar'); _.each(this.$measureList.find('.dropdown-item'), function (item) { var $item = $(item); $item.toggleClass('selected', $item.data('field') === state.measure); diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index e24de02879c..6cb2c023d0e 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -71,8 +71,6 @@ return AbstractRenderer.extend({ this.chart = null; this.chartId = _.uniqueId('chart'); - // the bar charts are considered stackable a priori - this.isStackable = true; this.$legendTooltip = null; this.$tooltip = null; }, @@ -782,34 +780,9 @@ return AbstractRenderer.extend({ // prepare data var data = this._prepareData(dataPoints); - // The datapoints (with non zero value) can be grouped by - // their originIndex and associated labels. - // If at least one group formed in that way has two elements - // this.isStackable will be true. - var groupMemberCounts = data.labels.map(function () { - return {}; - }); - this.isStackable = false; - for (var i = 0; i < data.datasets.length; i++) { - var dataset = data.datasets[i]; - var originIndex = dataset.originIndex; - if (this.isStackable) { - break; - } - for (var j = 0; j < dataset.data.length; j++) { - if (dataset.data[j]) { - if (originIndex in groupMemberCounts[j]) { - this.isStackable = true; - break; - } else { - groupMemberCounts[j][originIndex] = 1; - } - } - } - } data.datasets.forEach(function (dataset, index) { // used when stacked - dataset.stack = self.isStackable && self.state.stacked ? self.state.origins[dataset.originIndex] : undefined; + dataset.stack = self.state.stacked ? self.state.origins[dataset.originIndex] : undefined; // set dataset color var color = self._getColor(index); dataset.backgroundColor = color; -- cgit v1.2.3