[MERGE] web,*: selection in list views

*account

This commit allows the user to apply actions to all records
matching the current domain in list views. It also ensures that
the user is aware of the set of records actually selected.

The number of selected records is now displayed in the control
panel, and the user can, with an additional click, select all
records matching the current domain instead of the ones of the
current page.

Task 2185145

closes odoo/odoo#48305

Related: odoo/enterprise#9537
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-03-30 20:44:04 +02:00
committed by GitHub
17 changed files with 800 additions and 241 deletions
+1 -3
View File
@@ -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'))
+3 -2
View File
@@ -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
@@ -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';
@@ -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();
}
@@ -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<string>} 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<string>} 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',
@@ -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);
},
});
@@ -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) :
@@ -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)
@@ -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<array[]>} 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 = $('<div>');
} 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();
},
/**
@@ -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]);
+14
View File
@@ -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;
}
}
+13
View File
@@ -859,9 +859,22 @@
</div>
</t>
<t t-name="ListView.selection">
<div class="o_list_selection_box">
<t t-if="isDomainSelected">All <t t-esc="nbTotal"/> selected</t>
<t t-else="">
<t t-esc="nbSelected"/> selected
<a t-if="isPageSelected and nbTotal > nbSelected" href="#" class="o_list_select_domain">
<i class="fa fa-arrow-right"/> Select all <t t-esc="nbTotal"/>
</a>
</t>
</div>
</t>
<t t-name="ListView.confirmModal">
<main role="alert">
<p>
<t t-if="changes.isDomainSelected">This update will only consider the records of the current page.<br/><br/></t>
<t t-if="changes.nbRecords != changes.nbValidRecords">
Among the <t t-raw="changes.nbRecords"/> selected records,
<t t-raw="changes.nbValidRecords"/> are valid for this update.<br/>
@@ -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');
@@ -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));
@@ -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});
+436 -8
View File
@@ -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: '<tree><field name="foo"/><field name="bar"/></tree>',
});
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: '<tree limit="3"><field name="foo"/><field name="bar"/></tree>',
});
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', {
'</tree>',
});
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: '<tree limit="2"><field name="foo"/></tree>',
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: '<tree limit="2"><field name="foo"/></tree>',
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: '<tree limit="2"><field name="foo"/></tree>',
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: '<tree limit="2"><field name="foo"/></tree>',
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: '<tree limit="2"><field name="foo"/></tree>',
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: `
<tree multi_edit="1" limit="2">
<field name="id"/>
<field name="int_field"/>
</tree>`,
});
// 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: '<tree>' +
'<field name="foo"/>' +
'<field name="m2o" optional="hide"/>' +
'</tree>',
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);
@@ -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: '<tree limit="2"><field name="foo"/></tree>',
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();
},
},