From 7af5fb31ec7a3ef4bfce1756aa01824cba30e592 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 27 Mar 2020 10:25:00 +0000 Subject: [PATCH] [IMP] web: selection in list views 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 --- addons/web/models/ir_http.py | 5 +- .../static/src/js/components/action_menus.js | 31 ++-- .../src/js/views/list/list_controller.js | 101 ++++++++--- addons/web/static/src/scss/list_view.scss | 14 ++ addons/web/static/src/xml/base.xml | 12 ++ .../tests/components/action_menus_tests.js | 2 - .../web/static/tests/helpers/mock_server.js | 17 ++ addons/web/static/tests/views/list_tests.js | 160 +++++++++++++++++- 8 files changed, 291 insertions(+), 51 deletions(-) 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/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index a2096a59f34..0e2e207a125 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -24,6 +24,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 +55,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: () => {}, @@ -128,20 +130,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 +157,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 +183,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(); }, //-------------------------------------------------------------------------- @@ -354,9 +363,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, }); }, /** @@ -484,7 +493,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); @@ -499,6 +508,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 @@ -690,6 +724,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'. @@ -699,7 +742,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/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..ee72f282337 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -859,6 +859,18 @@
+ +
+ All selected + + selected + + Select all + + +
+
+

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/list_tests.js b/addons/web/static/tests/views/list_tests.js index 852c74d922b..cf0736f86f1 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(); }); @@ -2865,7 +2940,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 +2953,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 +4335,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 +4395,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();