diff --git a/addons/account/wizard/account_resequence.py b/addons/account/wizard/account_resequence.py index b585395f5f5..bd0b9378cd4 100644 --- a/addons/account/wizard/account_resequence.py +++ b/addons/account/wizard/account_resequence.py @@ -26,9 +26,7 @@ class ReSequenceWizard(models.TransientModel): def default_get(self, fields_list): values = super(ReSequenceWizard, self).default_get(fields_list) active_move_ids = self.env['account.move'] - if self.env.context.get('select_all') and self.env.context.get('active_model') == 'account.move' and 'active_domain' in self.env.context: - active_move_ids = self.env['account.move'].search(self.env.context['active_domain']) - elif self.env.context['active_model'] == 'account.move' and 'active_ids' in self.env.context: + if self.env.context['active_model'] == 'account.move' and 'active_ids' in self.env.context: active_move_ids = self.env['account.move'].browse(self.env.context['active_ids']) if len(active_move_ids.journal_id) > 1: raise UserError(_('You can only resequence items from the same journal')) diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index 3c5839aa042..748e469ff28 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -26,7 +26,7 @@ class Http(models.AbstractModel): version_info = odoo.service.common.exp_version() user_context = request.session.get_context() if request.session.uid else {} - + IrConfigSudo = self.env['ir.config_parameter'].sudo() session_info = { "uid": request.session.uid, "is_system": user._is_system() if request.session.uid else False, @@ -40,7 +40,8 @@ class Http(models.AbstractModel): "partner_display_name": user.partner_id.display_name, "company_id": user.company_id.id if request.session.uid else None, # YTI TODO: Remove this from the user context "partner_id": user.partner_id.id if request.session.uid and user.partner_id else None, - "web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''), + "web.base.url": IrConfigSudo.get_param('web.base.url', default=''), + "active_ids_limit": int(IrConfigSudo.get_param('web.active_ids_limit', default='20000')), } if self.env.user.has_group('base.group_user'): # the following is only useful in the context of a webclient bootstrapping diff --git a/addons/web/static/src/js/components/action_menus.js b/addons/web/static/src/js/components/action_menus.js index 69a10423373..22e549a271e 100644 --- a/addons/web/static/src/js/components/action_menus.js +++ b/addons/web/static/src/js/components/action_menus.js @@ -3,7 +3,6 @@ odoo.define('web.ActionMenus', function (require) { const Context = require('web.Context'); const DropdownMenu = require('web.DropdownMenu'); - const pyUtils = require('web.py_utils'); const Registry = require('web.Registry'); const { Component } = owl; @@ -95,15 +94,25 @@ odoo.define('web.ActionMenus', function (require) { * @param {OwlEvent} ev */ async _executeAction(action) { - const activeIdsContext = { - active_id: this.props.activeIds[0], - active_ids: this.props.activeIds, - active_model: this.env.action.res_model, - select_all: this.props.selectAll || false, - }; - if (this.props.domain) { - activeIdsContext.active_domain = this.props.domain; + let activeIds = this.props.activeIds; + if (this.props.isDomainSelected) { + activeIds = await this.rpc({ + model: this.env.action.res_model, + method: 'search', + args: [this.props.domain], + kwargs: { + limit: this.env.session.active_ids_limit, + }, + }); } + const activeIdsContext = { + active_id: activeIds[0], + active_ids: activeIds, + active_model: this.env.action.res_model, + // keep active_domain in context for backward compatibility + // reasons, and to allow actions to bypass the active_ids_limit + active_domain: this.props.domain, + }; const context = new Context(this.props.context, activeIdsContext).eval(); const result = await this.rpc({ @@ -149,9 +158,10 @@ odoo.define('web.ActionMenus', function (require) { ActionMenus.components = { DropdownMenu }; ActionMenus.props = { - activeIds: { type: Array, element: [Number|String] }, // virtual IDs are strings. + activeIds: { type: Array, element: [Number | String] }, // virtual IDs are strings. context: Object, domain: { type: Array, optional: 1 }, + isDomainSelected: { type: Boolean, optional: 1 }, items: { type: Object, shape: { @@ -160,7 +170,6 @@ odoo.define('web.ActionMenus', function (require) { other: { type: Array, optional: 1 }, }, }, - selectAll: { type: Boolean, optional: 1 }, }; ActionMenus.template = 'web.ActionMenus'; diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 8b4672bc291..bbfee26e51f 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -238,6 +238,25 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { return _super(dataPointID, changes, event); }); }, + /** + * Archive the current selection + * + * @private + * @param {number[]} ids + * @param {boolean} archive + * @returns {Promise} + */ + _archive: async function (ids, archive) { + if (ids.length === 0) { + return Promise.resolve(); + } + if (archive) { + await this.model.actionArchive(ids, this.handle); + } else { + await this.model.actionUnarchive(ids, this.handle); + } + return this.update({}, {reload: false}); + }, /** * When the user clicks on a 'action button', this function determines what * should happen. @@ -327,9 +346,10 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { .then(self._onDeletedRecords.bind(self, ids)); } if (this.confirmOnDelete) { - Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), { - confirm_callback: doIt, - }); + const message = ids.length > 1 ? + _t("Are you sure you want to delete these records?") : + _t("Are you sure you want to delete this record?"); + Dialog.confirm(this, message, { confirm_callback: doIt }); } else { doIt(); } 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 a69f6878023..8dc540a8861 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -811,6 +811,15 @@ var BasicModel = AbstractModel.extend({ return dataPoint.id; }); }, + /** + * Returns the list of res_ids for a given list of local ids. + * + * @param {string[]} localIds + * @returns {integer[]} + */ + localIdsToResIds: function (localIds) { + return localIds.map(localId => this.localData[localId].res_id); + }, /** * This helper method is designed to help developpers that want to use a * field widget outside of a view. In that case, we want a way to create @@ -1286,16 +1295,13 @@ var BasicModel = AbstractModel.extend({ /** * Archive the given records * - * @param {Array} recordIDs local ids of the records to (un)archive + * @param {integer[]} resIDs ids of the records to archive * @param {string} parentID id of the parent resource to reload * @returns {Promise} resolves to the parent id */ - actionArchive: function (recordIDs, parentID) { + actionArchive: function (resIDs, parentID) { var self = this; var parent = this.localData[parentID]; - var resIDs = _.map(recordIDs, function (recordID) { - return self.localData[recordID].res_id; - }); return this._rpc({ model: parent.model, method: 'action_archive', @@ -1318,16 +1324,13 @@ var BasicModel = AbstractModel.extend({ /** * Unarchive the given records * - * @param {Array} recordIDs local ids of the records to (un)archive + * @param {integer[]} resIDs ids of the records to unarchive * @param {string} parentID id of the parent resource to reload * @returns {Promise} resolves to the parent id */ - actionUnarchive: function (recordIDs, parentID) { + actionUnarchive: function (resIDs, parentID) { var self = this; var parent = this.localData[parentID]; - var resIDs = _.map(recordIDs, function (recordID) { - return self.localData[recordID].res_id; - }); return this._rpc({ model: parent.model, method: 'action_unarchive', diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 5c54afb7e41..7147f7aee66 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -305,30 +305,6 @@ var FormController = BasicController.extend({ core.bus.trigger('DOM_updated'); return result; }, - - /** - * Archive the current selection - * - * @private - * @param {string[]} ids - * @param {boolean} archive - * @returns {Promise} - */ - _archive: function (ids, archive) { - if (ids.length === 0) { - return Promise.resolve(); - } - if (archive) { - return this.model - .actionArchive(ids, this.handle) - .then(this.update.bind(this, {}, {reload: false})); - } else { - return this.model - .actionUnarchive(ids, this.handle) - .then(this.update.bind(this, {}, {reload: false})); - } - }, - /** * Assign on the buttons save and discard additionnal behavior to facilitate * the work of the users doing input only using the keyboard @@ -676,7 +652,6 @@ var FormController = BasicController.extend({ */ _onSave: function (ev) { ev.stopPropagation(); // Prevent x2m lines to be auto-saved - var self = this; this._disableButtons(); this.saveRecord().then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this)); }, @@ -703,7 +678,8 @@ var FormController = BasicController.extend({ * @param {boolean} archive */ _toggleArchiveState: function (archive) { - this._archive([this.handle], archive); + const resIds = this.model.localIdsToResIds([this.handle]); + this._archive(resIds, archive); }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index cca0a43cb5e..a908eb0d138 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -496,7 +496,7 @@ var KanbanController = BasicController.extend({ var self = this; var archive = ev.data.archive; var column = ev.target; - var recordIds = _.pluck(column.records, 'db_id'); + var recordIds = _.pluck(column.records, 'id'); if (recordIds.length) { var prom = archive ? this.model.actionArchive(recordIds, column.db_id) : diff --git a/addons/web/static/src/js/views/list/list_confirm_dialog.js b/addons/web/static/src/js/views/list/list_confirm_dialog.js index ea0a3191f9e..a499a5debb7 100644 --- a/addons/web/static/src/js/views/list/list_confirm_dialog.js +++ b/addons/web/static/src/js/views/list/list_confirm_dialog.js @@ -22,6 +22,8 @@ const ListConfirmDialog = Dialog.extend({ * @param {Widget} parent * @param {Object} record edited record with updated value * @param {Object} changes changes registered by the list controller + * @param {Object} changes isDomainSelected true iff the user selected the + * whole domain * @param {string} changes.fieldLabel label of the changed field * @param {string} changes.fieldName technical name of the changed field * @param {number} changes.nbRecords number of records (total) 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 41dfa18c8bc..4a3972d50eb 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -12,6 +12,7 @@ var BasicController = require('web.BasicController'); var DataExport = require('web.DataExport'); var Dialog = require('web.Dialog'); var ListConfirmDialog = require('web.ListConfirmDialog'); +var session = require('web.session'); var _t = core._t; var qweb = core.qweb; @@ -24,6 +25,7 @@ var ListController = BasicController.extend({ buttons_template: 'ListView.buttons', events: _.extend({}, BasicController.prototype.events, { 'click .o_list_export_xlsx': '_onDirectExportData', + 'click .o_list_select_domain': '_onSelectDomain', }), custom_events: _.extend({}, BasicController.prototype.custom_events, { activate_next_widget: '_onActivateNextWidget', @@ -54,7 +56,8 @@ var ListController = BasicController.extend({ this.selectedRecords = params.selectedRecords || []; this.multipleRecordsSavingPromise = null; this.fieldChangedPrevented = false; - this.selectAll = false; + this.isPageSelected = false; // true iff all records of the page are selected + this.isDomainSelected = false; // true iff the user selected all records matching the domain Object.defineProperty(this, 'mode', { get: () => this.renderer.isEditable() ? 'edit' : 'readonly', set: () => {}, @@ -65,25 +68,6 @@ var ListController = BasicController.extend({ // Public //-------------------------------------------------------------------------- - /** - * Calculate the active domain of the list view. This should be done only - * if the header checkbox has been checked. This is done by evaluating the - * search results, and then adding the dataset domain (i.e. action domain). - * - * @todo This is done only for the data export. The full mechanism is wrong, - * this method should be private, most of the code in the sidebar should be - * moved to the controller, and we should not use the getParent method... - * - * @returns {Promise} a promise that resolve to the active domain - */ - getActiveDomain: function () { - var self = this; - if (this.selectAll) { - var searchQuery = this._controlPanelModel ? this._controlPanelModel.getQuery() : {}; - var record = self.model.get(self.handle, {raw: true}); - return record.getDomain().concat(searchQuery.domain || []); - } - }, /* * @override */ @@ -128,20 +112,22 @@ var ListController = BasicController.extend({ */ renderButtons: function ($node) { if (this.noLeaf || !this.hasButtons) { - return; + this.hasButtons = false; + this.$buttons = $('
'); + } else { + this.$buttons = $(qweb.render(this.buttons_template, {widget: this})); + this.$buttons.on('click', '.o_list_button_add', this._onCreateRecord.bind(this)); + this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add')); + this.$buttons.find('.o_list_button_add').tooltip({ + delay: {show: 200, hide: 0}, + title: function () { + return qweb.render('CreateButton.tooltip'); + }, + 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 = $(qweb.render(this.buttons_template, {widget: this})); - this.$buttons.on('click', '.o_list_button_add', this._onCreateRecord.bind(this)); - this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add')); - this.$buttons.find('.o_list_button_add').tooltip({ - delay: {show: 200, hide: 0}, - title: function () { - return qweb.render('CreateButton.tooltip'); - }, - 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)); if ($node) { this.$buttons.appendTo($node); } @@ -153,15 +139,20 @@ var ListController = BasicController.extend({ */ update: function (params, options) { var self = this; + let res_ids; if (options && options.keepSelection) { // filter out removed records from selection - var res_ids = this.model.get(this.handle).res_ids; + res_ids = this.model.get(this.handle).res_ids; this.selectedRecords = _.filter(this.selectedRecords, function (id) { return _.contains(res_ids, self.model.get(id).res_id); }); } else { this.selectedRecords = []; } + if (this.selectedRecords.length === 0 || this.selectedRecords.length < res_ids.length) { + this.isDomainSelected = false; + this.isPageSelected = false; + } params.selectedRecords = this.selectedRecords; return this._super.apply(this, arguments); @@ -174,16 +165,16 @@ var ListController = BasicController.extend({ * @param {string} mode either 'readonly' or 'edit' */ updateButtons: function (mode) { - if (!this.$buttons) { - return; - } - this.$buttons.toggleClass('o-editing', mode === 'edit'); - const state = this.model.get(this.handle, {raw: true}); - if (state.count) { - this.$('.o_list_export_xlsx').show(); - } else { - this.$('.o_list_export_xlsx').hide(); + if (this.hasButtons) { + this.$buttons.toggleClass('o-editing', mode === 'edit'); + const state = this.model.get(this.handle, {raw: true}); + if (state.count) { + this.$('.o_list_export_xlsx').show(); + } else { + this.$('.o_list_export_xlsx').hide(); + } } + this._updateSelectionBox(); }, //-------------------------------------------------------------------------- @@ -234,28 +225,6 @@ var ListController = BasicController.extend({ }); }).then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this)); }, - /** - * Archive the current selection - * - * @private - * @param {string[]} ids - * @param {boolean} archive - * @returns {Promise} - */ - _archive: function (ids, archive) { - if (ids.length === 0) { - return Promise.resolve(); - } - if (archive) { - return this.model - .actionArchive(ids, this.handle) - .then(this.update.bind(this, {}, {reload: false})); - } else { - return this.model - .actionUnarchive(ids, this.handle) - .then(this.update.bind(this, {}, {reload: false})); - } - }, /** * Assign on the buttons create additionnal behavior to facilitate the work of the users doing input only using the keyboard * @@ -297,6 +266,39 @@ var ListController = BasicController.extend({ return this.renderer.updateState(state, {noRender: true}) .then(this._setMode.bind(this, 'readonly', id)); }, + /** + * Deletes records matching the current domain. We limit the number of + * deleted records to the 'active_ids_limit' config parameter. + * + * @private + */ + _deleteRecordsInCurrentDomain: function () { + const doIt = async () => { + const state = this.model.get(this.handle, {raw: true}); + const resIds = await this._domainToResIds(state.getDomain(), session.active_ids_limit); + await this._rpc({ + model: this.modelName, + method: 'unlink', + args: [resIds], + context: state.getContext(), + }); + if (resIds.length === session.active_ids_limit) { + const msg = _.str.sprintf( + _t("Of the %d records selected, only the first %d have been deleted."), + state.count, resIds.length + ); + this.do_notify(_t('Warning'), msg); + } + this.reload(); + }; + if (this.confirmOnDelete) { + Dialog.confirm(this, _t("Are you sure you want to delete these records ?"), { + confirm_callback: doIt, + }); + } else { + doIt(); + } + }, /** * To improve performance, list view must not be rerendered if it is asked * to discard all its changes. Indeed, only the in-edition row needs to be @@ -319,6 +321,24 @@ var ListController = BasicController.extend({ self.updateButtons('readonly'); }); }, + /** + * Returns the ids of records matching the given domain. + * + * @private + * @param {Array[]} domain + * @param {integer} [limit] + * @returns {integer[]} + */ + _domainToResIds: function (domain, limit) { + return this._rpc({ + model: this.modelName, + method: 'search', + args: [domain], + kwargs: { + limit: limit, + }, + }); + }, /** * @returns {DataExport} the export dialog widget * @private @@ -327,8 +347,9 @@ var ListController = BasicController.extend({ let state = this.model.get(this.handle); let defaultExportFields = this.renderer.columns.filter(field => field.tag === 'field').map(field => field.attrs.name); let groupedBy = this.renderer.state.groupedBy; + const domain = this.isDomainSelected && state.getDomain(); return new DataExport(this, state, defaultExportFields, groupedBy, - this.getActiveDomain(), this.getSelectedIds()); + domain, this.getSelectedIds()); }, /** * Only display the pager when there are data to display. @@ -376,9 +397,9 @@ var ListController = BasicController.extend({ } return Object.assign(props, { items: Object.assign({}, this.toolbarActions, { other: otherActionItems }), - context: this.model.get(this.handle, { raw: true }).getContext(), + context: state.getContext(), domain: state.getDomain(), - selectAll: this.selectAll, + isDomainSelected: this.isDomainSelected, }); }, /** @@ -443,6 +464,7 @@ var ListController = BasicController.extend({ }; const record = this.model.get(recordId); const dialogChanges = { + isDomainSelected: this.isDomainSelected, fieldLabel: node.attrs.string || record.fields[fieldName].string, fieldName: node.attrs.name, nbRecords: recordIds.length, @@ -495,9 +517,26 @@ var ListController = BasicController.extend({ * * @private * @param {boolean} archive + * @returns {Promise} */ - _toggleArchiveState: function (archive) { - this._archive(this.selectedRecords, archive); + _toggleArchiveState: async function (archive) { + let resIds; + let displayNotif = false; + const state = this.model.get(this.handle, {raw: true}); + if (this.isDomainSelected) { + resIds = await this._domainToResIds(state.getDomain(), session.active_ids_limit); + displayNotif = (resIds.length === session.active_ids_limit); + } else { + resIds = this.model.localIdsToResIds(this.selectedRecords); + } + await this._archive(resIds, archive); + if (displayNotif) { + const msg = _.str.sprintf( + _t("Of the %d records selected, only the first %d have been archived/unarchived."), + state.count, resIds.length + ); + this.do_notify(_t('Warning'), msg); + } }, /** * Hide the create button in non-empty grouped editable list views, as an @@ -506,7 +545,7 @@ var ListController = BasicController.extend({ * @private */ _toggleCreateButton: function () { - if (this.$buttons) { + if (this.hasButtons) { var state = this.model.get(this.handle); var createHidden = this.renderer.isEditable() && state.groupedBy.length && state.data.length; this.$buttons.find('.o_list_button_add').toggleClass('o_hidden', !!createHidden); @@ -521,6 +560,31 @@ var ListController = BasicController.extend({ this._toggleCreateButton(); this.updateButtons('readonly'); }, + /** + * When records are selected, a box is displayed in the control panel (next + * to the buttons). It indicates the number of selected records, and allows + * the user to select the whole domain instead of the current page (when the + * page is selected). This function renders and displays this box when at + * least one record is selected. + * + * @private + */ + _updateSelectionBox() { + if (this.$selectionBox) { + this.$selectionBox.remove(); + this.$selectionBox = null; + } + if (this.selectedRecords.length) { + const state = this.model.get(this.handle, {raw: true}); + this.$selectionBox = $(qweb.render('ListView.selection', { + isDomainSelected: this.isDomainSelected, + isPageSelected: this.isPageSelected, + nbSelected: this.selectedRecords.length, + nbTotal: state.count, + })); + this.$selectionBox.appendTo(this.$buttons); + } + }, //-------------------------------------------------------------------------- // Handlers @@ -591,8 +655,12 @@ var ListController = BasicController.extend({ * * @private */ - _onDeleteSelectedRecords: function () { - this._deleteRecords(this.selectedRecords); + _onDeleteSelectedRecords: async function () { + if (this.isDomainSelected) { + this._deleteRecordsInCurrentDomain(); + } else { + this._deleteRecords(this.selectedRecords); + } }, /** * Handler called when the user clicked on the 'Discard' button. @@ -712,6 +780,15 @@ var ListController = BasicController.extend({ .then(ev.data.onSuccess) .guardedCatch(ev.data.onFailure); }, + /** + * @private + */ + _onSelectDomain: function (ev) { + ev.preventDefault(); + this.isDomainSelected = true; + this._updateSelectionBox(); + this._updateControlPanel(); + }, /** * When the current selection changes (by clicking on the checkboxes on the * left), we need to display (or hide) the 'sidebar'. @@ -721,7 +798,9 @@ var ListController = BasicController.extend({ */ _onSelectionChanged: function (ev) { this.selectedRecords = ev.data.selection; - this.selectAll = ev.data.allChecked; + this.isPageSelected = ev.data.allChecked; + this.isDomainSelected = false; + this._updateSelectionBox(); this._updateControlPanel(); }, /** 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 418c39b14e0..5cf4b022c5d 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -989,6 +989,9 @@ var ListRenderer = BasicRenderer.extend({ return _.contains(self.selection, $(el).data('id')); }); $checked_rows.find('.o_list_record_selector input').prop('checked', true); + if ($checked_rows.length === self.$('.o_data_row').length) { // all rows are checked + self.$('thead .o_list_record_selector input').prop('checked', true); + } } }); return Promise.all([this._super.apply(this, arguments), prom]); diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 2c864a25613..a9076270ed2 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -348,3 +348,17 @@ } } } + +// Selection box (next to the buttons, in ControlPanel) +$selected-box-color: #e7f2f6; +.o_list_selection_box { + display: inline-block; + padding: $btn-padding-y $btn-padding-x; + vertical-align: middle; // align with the buttons + border: 1px solid $selected-box-color; + background-color: $selected-box-color; + .o_list_select_domain { + font-weight: bold; + margin-left: 4px; + } +} diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 54ee1d035da..2b0ff6f31dd 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -859,9 +859,22 @@
+ +
+ All selected + + selected + + Select all + + +
+
+

+ This update will only consider the records of the current page.

Among the selected records, are valid for this update.
diff --git a/addons/web/static/tests/components/action_menus_tests.js b/addons/web/static/tests/components/action_menus_tests.js index f4229b30493..e17ae95ba78 100644 --- a/addons/web/static/tests/components/action_menus_tests.js +++ b/addons/web/static/tests/components/action_menus_tests.js @@ -105,7 +105,6 @@ odoo.define('web.action_menus_tests', function (require) { active_id: 23, active_ids: [23], active_model: 'hobbit', - select_all: false, // default props }; assert.deepEqual(args.context, expectedContext); assert.step('load-action'); @@ -178,7 +177,6 @@ odoo.define('web.action_menus_tests', function (require) { active_id: 23, active_ids: [23], active_model: 'hobbit', - select_all: false, // default props }; assert.deepEqual(args.context, expectedContext); assert.step('load-action'); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index a59a925306f..d59cafacd28 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -1093,6 +1093,20 @@ var MockServer = Class.extend({ } return true; }, + /** + * Simulate a 'search' operation + * + * @private + * @param {string} model + * @param {Array} args + * @param {Object} kwargs + * @param {integer} [kwargs.limit] + * @returns {integer[]} + */ + _mockSearch: function (model, args, kwargs) { + const limit = kwargs.limit || Number.MAX_VALUE; + return this._getRecords(model, args[0]).map(r => r.id).slice(0, limit); + }, /** * Simulate a 'search_count' operation * @@ -1340,6 +1354,9 @@ var MockServer = Class.extend({ case 'read_progress_bar': return Promise.resolve(this._mockReadProgressBar(args.model, args.kwargs)); + case 'search': + return Promise.resolve(this._mockSearch(args.model, args.args, args.kwargs)); + case 'search_count': return Promise.resolve(this._mockSearchCount(args.model, args.args)); diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js index 81deca04968..793a5749b46 100644 --- a/addons/web/static/tests/views/kanban_model_tests.js +++ b/addons/web/static/tests/views/kanban_model_tests.js @@ -157,7 +157,7 @@ QUnit.module('Views', { assert.strictEqual(xpadGroup.count, 1, 'xpad group has one record'); // archive the column 'xphone' - var recordIDs = _.pluck(xphoneGroup.data, 'id'); + var recordIDs = xphoneGroup.data.map(record => record.res_id); await model.actionArchive(recordIDs, xphoneGroup.id); state = model.get(resultID); xphoneGroup = _.findWhere(state.data, {res_id: 37}); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 852c74d922b..94a7a3bb156 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -1251,8 +1251,76 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('selection box is properly displayed (single page)', async function (assert) { + assert.expect(11); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + }); + + assert.containsN(list, '.o_data_row', 4); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); + + // select a record + await testUtils.dom.click(list.$('.o_data_row:first .o_list_record_selector input')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain'); + assert.strictEqual(list.$('.o_list_selection_box').text().trim(), '1 selected'); + + // select all records of first page + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain'); + assert.strictEqual(list.$('.o_list_selection_box').text().trim(), '4 selected'); + + // unselect a record + await testUtils.dom.click(list.$('.o_data_row:nth(1) .o_list_record_selector input')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain'); + assert.strictEqual(list.$('.o_list_selection_box').text().trim(), '3 selected'); + + list.destroy(); + }); + + QUnit.test('selection box is properly displayed (multi pages)', async function (assert) { + assert.expect(10); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + }); + + assert.containsN(list, '.o_data_row', 3); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); + + // select a record + await testUtils.dom.click(list.$('.o_data_row:first .o_list_record_selector input')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain'); + assert.strictEqual(list.$('.o_list_selection_box').text().trim(), '1 selected'); + + // select all records of first page + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.containsOnce(list.$('.o_list_selection_box'), '.o_list_select_domain'); + assert.strictEqual(list.$('.o_list_selection_box').text().replace(/\s+/g, ' ').trim(), + '3 selected Select all 4'); + + // select all domain + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); + assert.strictEqual(list.$('.o_list_selection_box').text().trim(), 'All 4 selected'); + + list.destroy(); + }); + QUnit.test('selection is reset on reload', async function (assert) { - assert.expect(5); + assert.expect(8); var list = await createView({ View: ListView, @@ -1264,6 +1332,7 @@ QUnit.module('Views', { '', }); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should be 32 (no record selected)"); @@ -1271,6 +1340,7 @@ QUnit.module('Views', { var $firstRowSelector = list.$('tbody .o_list_record_selector input').first(); testUtils.dom.click($firstRowSelector); assert.ok($firstRowSelector.is(':checked'), "first row should be selected"); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); assert.strictEqual(list.$('tfoot td:nth(2)').text(), '10', "total should be 10 (first record selected)"); @@ -1279,6 +1349,7 @@ QUnit.module('Views', { $firstRowSelector = list.$('tbody .o_list_record_selector input').first(); assert.notOk($firstRowSelector.is(':checked'), "first row should no longer be selected"); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should be 32 (no more record selected)"); @@ -1286,7 +1357,7 @@ QUnit.module('Views', { }); QUnit.test('selection is kept on render without reload', async function (assert) { - assert.expect(6); + assert.expect(10); var list = await createView({ View: ListView, @@ -1301,17 +1372,20 @@ QUnit.module('Views', { }); assert.containsNone(list.el, 'div.o_control_panel .o_cp_action_menus'); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); // open blip grouping and check all lines await testUtils.dom.click(list.$('.o_group_header:contains("blip (2)")')); await testUtils.dom.click(list.$('.o_data_row:first input')); assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); // open yop grouping and verify blip are still checked await testUtils.dom.click(list.$('.o_group_header:contains("yop (1)")')); assert.containsOnce(list, '.o_data_row input:checked', "opening a grouping does not uncheck others"); assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list.$('.o_cp_buttons'), '.o_list_selection_box'); // close and open blip grouping and verify blip are unchecked await testUtils.dom.click(list.$('.o_group_header:contains("blip (2)")')); @@ -1319,6 +1393,7 @@ QUnit.module('Views', { assert.containsNone(list, '.o_data_row input:checked', "opening and closing a grouping uncheck its elements"); assert.containsNone(list.el, 'div.o_control_panel .o_cp_action_menus'); + assert.containsNone(list.$('.o_cp_buttons'), '.o_list_selection_box'); list.destroy(); }); @@ -2506,6 +2581,107 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('delete all records matching the domain', async function (assert) { + assert.expect(6); + + this.data.foo.records.push({id: 5, bar: true, foo: "xxx"}); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + domain: [['bar', '=', true]], + mockRPC: function (route, args) { + if (args.method === 'unlink') { + assert.deepEqual(args.args[0], [1, 2, 3, 5]); + } + return this._super.apply(this, arguments); + }, + services: { + notification: NotificationService.extend({ + notify: function () { + throw new Error('should not display a notification'); + }, + }), + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + assert.containsNone(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsN(list, 'tbody td.o_list_record_selector', 2, "should have 2 records"); + + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + + assert.containsOnce(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list, '.o_list_selection_box .o_list_select_domain'); + + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Delete"); + + assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); + await testUtils.dom.click($('.modal-footer .btn-primary')); + + list.destroy(); + }); + + QUnit.test('delete all records matching the domain (limit reached)', async function (assert) { + assert.expect(8); + + this.data.foo.records.push({id: 5, bar: true, foo: "xxx"}); + this.data.foo.records.push({id: 6, bar: true, foo: "yyy"}); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + domain: [['bar', '=', true]], + mockRPC: function (route, args) { + if (args.method === 'unlink') { + assert.deepEqual(args.args[0], [1, 2, 3, 5]); + } + return this._super.apply(this, arguments); + }, + services: { + notification: NotificationService.extend({ + notify: function () { + assert.step('notify'); + }, + }), + }, + session: { + active_ids_limit: 4, + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + + assert.containsNone(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsN(list, 'tbody td.o_list_record_selector', 2, "should have 2 records"); + + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + + assert.containsOnce(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list, '.o_list_selection_box .o_list_select_domain'); + + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Delete"); + + assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); + await testUtils.dom.click($('.modal-footer .btn-primary')); + + assert.verifySteps(['notify']); + + list.destroy(); + }); + QUnit.test('archiving one record', async function (assert) { assert.expect(12); @@ -2552,6 +2728,113 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('archive all records matching the domain', async function (assert) { + assert.expect(6); + + // add active field on foo model and make all records active + this.data.foo.fields.active = {string: 'Active', type: 'boolean', default: true}; + this.data.foo.records.push({id: 5, bar: true, foo: "xxx"}); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + domain: [['bar', '=', true]], + mockRPC: function (route, args) { + if (args.method === 'action_archive') { + assert.deepEqual(args.args[0], [1, 2, 3, 5]); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + services: { + notification: NotificationService.extend({ + notify: function () { + throw new Error('should not display a notification'); + }, + }), + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + assert.containsNone(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsN(list, 'tbody td.o_list_record_selector', 2, "should have 2 records"); + + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + + assert.containsOnce(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list, '.o_list_selection_box .o_list_select_domain'); + + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Archive"); + + assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); + await testUtils.dom.click($('.modal-footer .btn-primary')); + + list.destroy(); + }); + + QUnit.test('archive all records matching the domain (limit reached)', async function (assert) { + assert.expect(8); + + // add active field on foo model and make all records active + this.data.foo.fields.active = {string: 'Active', type: 'boolean', default: true}; + this.data.foo.records.push({id: 5, bar: true, foo: "xxx"}); + this.data.foo.records.push({id: 6, bar: true, foo: "yyy"}); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + domain: [['bar', '=', true]], + mockRPC: function (route, args) { + if (args.method === 'action_archive') { + assert.deepEqual(args.args[0], [1, 2, 3, 5]); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + services: { + notification: NotificationService.extend({ + notify: function () { + assert.step('notify'); + }, + }), + }, + session: { + active_ids_limit: 4, + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + + assert.containsNone(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsN(list, 'tbody td.o_list_record_selector', 2, "should have 2 records"); + + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + + assert.containsOnce(list, 'div.o_control_panel .o_cp_action_menus'); + assert.containsOnce(list, '.o_list_selection_box .o_list_select_domain'); + + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Archive"); + + assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); + await testUtils.dom.click($('.modal-footer .btn-primary')); + + assert.verifySteps(['notify']); + + list.destroy(); + }); + QUnit.test('pager (ungrouped and grouped mode), default limit', async function (assert) { assert.expect(4); @@ -2865,7 +3148,7 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('list without import button', async function (assert) { + QUnit.test('list with group_by_no_leaf flag in context', async function (assert) { assert.expect(1); var list = await createView({ @@ -2878,7 +3161,7 @@ QUnit.module('Views', { } }); - assert.ok(!list.$buttons, "should not have any buttons"); + assert.containsNone(list, '.o_list_buttons', "should not have any buttons"); list.destroy(); }); @@ -4260,7 +4543,7 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('execute ActionMenus actions with correct params', async function (assert) { + QUnit.test('execute ActionMenus actions with correct params (single page)', async function (assert) { assert.expect(12); const list = await createView({ @@ -4320,9 +4603,78 @@ QUnit.module('Views', { await cpHelpers.toggleMenuItem(list, "Custom Action"); assert.verifySteps([ - '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2,3,4],"active_model":"foo","select_all":true,"active_domain":[]}}', - '{"action_id":44,"context":{"active_id":2,"active_ids":[2,3,4],"active_model":"foo","select_all":false,"active_domain":[]}}', - '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2],"active_model":"foo","select_all":false,"active_domain":[["bar","=",true]]}}', + '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2,3,4],"active_model":"foo","active_domain":[]}}', + '{"action_id":44,"context":{"active_id":2,"active_ids":[2,3,4],"active_model":"foo","active_domain":[]}}', + '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2],"active_model":"foo","active_domain":[["bar","=",true]]}}', + ]); + + list.destroy(); + }); + + QUnit.test('execute ActionMenus actions with correct params (multi pages)', async function (assert) { + assert.expect(13); + + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + toolbar: { + action: [{ + id: 44, + name: 'Custom Action', + type: 'ir.actions.server', + }], + print: [], + }, + mockRPC: function (route, args) { + if (route === '/web/action/load') { + assert.step(JSON.stringify(args)); + return Promise.resolve({}); + } + return this._super(...arguments); + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + assert.containsNone(list.el, 'div.o_control_panel .o_cp_action_menus'); + assert.containsN(list, '.o_data_row', 2); + + // select all records + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + assert.containsN(list, '.o_list_record_selector input:checked', 3); + assert.containsOnce(list, '.o_list_selection_box .o_list_select_domain'); + assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus'); + + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Custom Action"); + + // select all domain + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + assert.containsN(list, '.o_list_record_selector input:checked', 3); + + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Custom Action"); + + // add a domain + await list.reload({ domain: [['bar', '=', true]] }); + assert.containsNone(list, '.o_list_selection_box .o_list_select_domain'); + + // select all domain + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + assert.containsN(list, '.o_list_record_selector input:checked', 3); + assert.containsNone(list, '.o_list_selection_box .o_list_select_domain'); + + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, "Custom Action"); + + assert.verifySteps([ + '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2],"active_model":"foo","active_domain":[]}}', + '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2,3,4],"active_model":"foo","active_domain":[]}}', + '{"action_id":44,"context":{"active_id":1,"active_ids":[1,2,3],"active_model":"foo","active_domain":[["bar","=",true]]}}', ]); list.destroy(); @@ -5988,6 +6340,33 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('editable list view: multi edition when the domain is selected', async function (assert) { + assert.expect(1); + + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: ` + + + + `, + }); + + // select all records, and then select all domain + await testUtils.dom.click(list.$('th.o_list_record_selector input')); + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + + // edit a field + await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)')); + await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666); + + assert.ok($('.modal-body').text().includes('This update will only consider the records of the current page.')); + + list.destroy(); + }); + QUnit.test('editable list view: many2one with readonly modifier', async function (assert) { assert.expect(2); @@ -8460,6 +8839,55 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('selection is kept when optional fields are toggled', async function (assert) { + assert.expect(7); + + var RamStorageService = AbstractStorageService.extend({ + storage: new RamStorage(), + }); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '', + services: { + local_storage: RamStorageService, + }, + }); + + assert.containsN(list, 'th', 2); + + // select a record + await testUtils.dom.click(list.$('.o_data_row .o_list_record_selector input:first')); + + assert.containsOnce(list, '.o_list_record_selector input:checked'); + + // add an optional field + await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle')); + await testUtils.dom.click(list.$('div.o_optional_columns div.dropdown-item:first input')); + assert.containsN(list, 'th', 3); + + assert.containsOnce(list, '.o_list_record_selector input:checked'); + + // select all records + await testUtils.dom.click(list.$('thead .o_list_record_selector input')); + + assert.containsN(list, '.o_list_record_selector input:checked', 5); + + // remove an optional field + await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle')); + await testUtils.dom.click(list.$('div.o_optional_columns div.dropdown-item:first input')); + assert.containsN(list, 'th', 2); + + assert.containsN(list, '.o_list_record_selector input:checked', 5); + + list.destroy(); + }); + QUnit.test('change the viewType of the current action', async function (assert) { assert.expect(25); diff --git a/addons/web/static/tests/widgets/data_export_tests.js b/addons/web/static/tests/widgets/data_export_tests.js index 7abc3e93f6a..9a6f23b815b 100644 --- a/addons/web/static/tests/widgets/data_export_tests.js +++ b/addons/web/static/tests/widgets/data_export_tests.js @@ -1,10 +1,10 @@ odoo.define('web.data_export_tests', function (require) { "use strict"; +const data = require('web.data'); const framework = require('web.framework'); const ListView = require('web.ListView'); const testUtils = require('web.test_utils'); -const data = require('web.data'); const cpHelpers = testUtils.controlPanel; const createView = testUtils.createView; @@ -12,7 +12,7 @@ const createView = testUtils.createView; QUnit.module('widgets', { beforeEach: function () { this.data = { - partner: { + 'partner': { fields: { foo: {string: "Foo", type: "char"}, bar: {string: "Bar", type: "char"}, @@ -40,6 +40,38 @@ QUnit.module('widgets', { records: [], }, }; + + this.mockDataExportRPCs = function (route) { + if (route === '/web/export/formats') { + return Promise.resolve([ + {tag: 'csv', label: 'CSV'}, + {tag: 'xls', label: 'Excel'}, + ]); + } + if (route === '/web/export/get_fields') { + return Promise.resolve([ + { + field_type: "one2many", + string: "Activities", + required: false, + value: "activity_ids/id", + id: "activity_ids", + params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"}, + relation_field: "res_id", + children: true, + }, { + children: false, + field_type: 'char', + id: "foo", + relation_field: null, + required: false, + string: 'Foo', + value: "foo", + } + ]); + } + return this._super.apply(this, arguments); + }; } }, function () { @@ -66,37 +98,7 @@ QUnit.module('widgets', { viewOptions: { hasActionMenus: true, }, - mockRPC: function (route) { - if (route === '/web/export/formats') { - return Promise.resolve([ - {tag: 'csv', label: 'CSV'}, - {tag: 'xls', label: 'Excel'}, - ]); - } - if (route === '/web/export/get_fields') { - return Promise.resolve([ - { - field_type: "one2many", - string: "Activities", - required: false, - value: "activity_ids/id", - id: "activity_ids", - params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"}, - relation_field: "res_id", - children: true, - }, { - children: false, - field_type: 'char', - id: "foo", - relation_field: null, - required: false, - string: 'Foo', - value: "foo", - } - ]); - } - return this._super.apply(this, arguments); - }, + mockRPC: this.mockDataExportRPCs, session: { get_file: function (params) { assert.step(params.url); @@ -131,12 +133,68 @@ QUnit.module('widgets', { ]); }); + QUnit.test('exporting data in list view (multi pages)', async function (assert) { + assert.expect(4); + + let expectedData; + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + domain: [['id', '<', 1000]], + viewOptions: { + hasActionMenus: true, + }, + mockRPC: this.mockDataExportRPCs, + session: { + get_file: function (params) { + const data = JSON.parse(params.data.data); + assert.deepEqual({ids: data.ids, domain: data.domain}, expectedData); + params.complete(); + }, + }, + }); + + // select all records (first page) and export + expectedData = { + ids: [1, 2], + domain: [['id', '<', 1000]], + }; + await testUtils.dom.click(list.$('thead th.o_list_record_selector input')); + + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, 'Export'); + + assert.containsOnce(document.body, '.modal'); + + await testUtils.dom.click($('.modal span:contains(Export)')); + await testUtils.dom.click($('.modal span:contains(Close)')); + + // select all domain and export + expectedData = { + ids: false, + domain: [['id', '<', 1000]], + }; + await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain')); + + await cpHelpers.toggleActionMenu(list); + await cpHelpers.toggleMenuItem(list, 'Export'); + + assert.containsOnce(document.body, '.modal'); + + await testUtils.dom.click($('.modal span:contains(Export)')); + await testUtils.dom.click($('.modal span:contains(Close)')); + + list.destroy(); + }); + QUnit.test('saving fields list when exporting data', async function (assert) { assert.expect(4); var create = data.DataSet.prototype.create; - data.DataSet.prototype.create = function (data, options) { + data.DataSet.prototype.create = function () { assert.step('create'); return Promise.resolve([]); }; @@ -149,37 +207,7 @@ QUnit.module('widgets', { viewOptions: { hasActionMenus: true, }, - mockRPC: function (route) { - if (route === '/web/export/formats') { - return Promise.resolve([ - {tag: 'csv', label: 'CSV'}, - {tag: 'xls', label: 'Excel'}, - ]); - } - if (route === '/web/export/get_fields') { - return Promise.resolve([ - { - field_type: "one2many", - string: "Activities", - required: false, - value: "activity_ids/id", - id: "activity_ids", - params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"}, - relation_field: "res_id", - children: true, - }, { - children: false, - field_type: 'char', - id: "foo", - relation_field: null, - required: false, - string: 'Foo', - value: "foo", - } - ]); - } - return this._super.apply(this, arguments); - }, + mockRPC: this.mockDataExportRPCs, }); @@ -221,37 +249,7 @@ QUnit.module('widgets', { viewOptions: { hasActionMenus: true, }, - mockRPC: function (route) { - if (route === '/web/export/formats') { - return Promise.resolve([ - {tag: 'csv', label: 'CSV' }, - {tag: 'xls', label: 'Excel' }, - ]); - } - if (route === '/web/export/get_fields') { - return Promise.resolve([ - { - field_type: "one2many", - string: "Activities", - required: false, - value: "activity_ids/id", - id: "activity_ids", - params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities" }, - relation_field: "res_id", - children: true, - }, { - children: false, - field_type: 'char', - id: "foo", - relation_field: null, - required: false, - string: 'Foo', - value: "foo", - } - ]); - } - return this._super.apply(this, arguments); - }, + mockRPC: this.mockDataExportRPCs, }); @@ -291,7 +289,7 @@ QUnit.module('widgets', { }); QUnit.test('Direct export list ', async function (assert) { - assert.expect(2) + assert.expect(2); let list = await createView({ View: ListView, @@ -321,7 +319,7 @@ QUnit.module('widgets', { name: 'bar', label: 'Bar', }] - }, "should be called with correct params") + }, "should be called with correct params"); args.complete(); }, }, @@ -336,7 +334,7 @@ QUnit.module('widgets', { }); QUnit.test('Direct export grouped list ', async function (assert) { - assert.expect(2) + assert.expect(2); let list = await createView({ View: ListView, @@ -367,7 +365,7 @@ QUnit.module('widgets', { name: 'bar', label: 'Bar', }] - }, "should be called with correct params") + }, "should be called with correct params"); args.complete(); }, },