diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-20 17:33:22 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-20 17:34:45 +0200 |
| commit | 64e43808b79badb5869a8d73cc1cc3f3b3aa8e12 (patch) | |
| tree | 9a22d5f6c03073a4d10faedea8df23a5211e8b8f /addons/web/static/src/js/views | |
| parent | 944891ced081cda3b8b3db5faf3b7ac23b6b4ce8 (diff) | |
| parent | 58a83d12228fbbd4e9dde542e133d264204ee0f3 (diff) | |
[MERGE] forward port branch saas-12.5 up to 58a83d12228
Diffstat (limited to 'addons/web/static/src/js/views')
19 files changed, 351 insertions, 100 deletions
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index cb83301c14d..43a485fb009 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, { @@ -519,6 +521,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); 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/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 { 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/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js index 677f0733bc5..bdde2827e96 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 @@ -50,6 +50,7 @@ var ControlPanelRenderer = Renderer.extend({ this.$subMenus = null; this.action = params.action; this.displaySearchMenu = true; + this.isMobile = config.device.isMobile; this.menusSetup = false; this.searchMenuTypes = params.searchMenuTypes || []; this.subMenus = {}; 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 23852abcf53..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 @@ -9,9 +9,9 @@ if (!config.device.isMobile) { } ControlPanelRenderer.include({ - template:'MobileControlPanel', 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', @@ -49,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/js/views/control_panel/search/search_bar.js b/addons/web/static/src/js/views/control_panel/search/search_bar.js index 3f58fcdce2a..42b156beed7 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', }), /** @@ -32,6 +34,7 @@ var SearchBar = Widget.extend({ this.autoCompleteSources = []; this.searchFacets = []; + this._isInputComposing = false; }, /** * @override @@ -200,10 +203,27 @@ 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 917d42b7e41..22d957c030b 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; }, //-------------------------------------------------------------------------- @@ -130,6 +133,20 @@ var SearchFacet = Widget.extend({ /** * @private + * @param {CompositionEvent} ev + */ + _onCompositionend: function (ev) { + this._isComposing = false; + }, + /** + * @private + * @param {CompositionEvent} ev + */ + _onCompositionstart: function (ev) { + this._isComposing = true; + }, + /** + * @private */ _onFacetRemove: function () { this.trigger_up('facet_removed', {group: this.facet}); @@ -139,6 +156,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/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; 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..4e0060fa5ff 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,10 @@ 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, + }); }, //-------------------------------------------------------------------------- @@ -134,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); } @@ -339,6 +402,31 @@ var ListController = BasicController.extend({ }); }, /** + * 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 */ @@ -371,6 +459,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) { @@ -388,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) { @@ -399,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) - .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); @@ -413,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); + }); }); }, /** @@ -587,6 +684,28 @@ var ListController = BasicController.extend({ 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 * * @param {OdooEvent} ev @@ -614,6 +733,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. * * @private @@ -643,15 +770,16 @@ 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 = () => { - 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); }; + // disable onchanges as we'll save directly + ev.data.notifyChange = false; } this._super.apply(this, arguments); }, @@ -678,6 +806,29 @@ var ListController = BasicController.extend({ 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 * rerender the view. 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..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(); }, @@ -320,6 +322,17 @@ ListRenderer.include({ 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. * @@ -339,9 +352,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 @@ -358,6 +369,7 @@ ListRenderer.include({ } if ($row.prop('rowIndex') - 1 === this.currentRow) { this.currentRow = null; + this._enableRecordSelectors(); } // destroy widgets first @@ -452,7 +464,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 @@ -494,6 +510,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. @@ -543,7 +560,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. @@ -558,6 +582,12 @@ ListRenderer.include({ } }, /** + * @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 * to the displayed records. We want to freeze widths s.t. it doesn't @@ -581,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 @@ -639,6 +678,7 @@ ListRenderer.include({ return fixedWidths[type] || '1'; }, /** + * @private * * @returns {integer} */ @@ -1195,7 +1235,10 @@ ListRenderer.include({ return new Promise(function (resolve) { self.trigger_up('edit_line', { recordId: recordId, - onSuccess: resolve, + onSuccess: function () { + self._disableRecordSelectors(); + resolve(); + }, }); }); }); @@ -1386,6 +1429,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); @@ -1420,6 +1464,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. * 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]; }); 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') { 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 = []; } diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js index 32bfda12594..4f8c42325db 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. @@ -279,8 +288,9 @@ var SearchPanel = Widget.extend({ }); _.map(values, function (value) { var value = category.values[value.id]; - 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 = _.filter(_.map(values, function (value) { @@ -293,7 +303,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 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, { |
