diff options
| author | Lucas Perais (lpe) <lpe@odoo.com> | 2020-07-23 08:46:39 +0000 |
|---|---|---|
| committer | Lucas Perais (lpe) <lpe@odoo.com> | 2020-07-29 08:59:42 +0000 |
| commit | 49d9771c56d9d5652eca34285ade04cc1a303718 (patch) | |
| tree | b22048a2f08d32a24b46cdae8138956ce342ef2a /addons/web/static/src/js/views | |
| parent | 97cdcc05f17eb9df0e6ca7896ace6fbf6450b356 (diff) | |
[IMP] web, base: action buttons in list's controlPanel
This commit introduces a new semantics to define action and method buttons
in the list view.
The aim of this feature is to be able to do some actions on selected records, or on the entire
records' domain with the help of buttons located in the controlPanel, much like the form view header buttons.
To define such buttons, one must add a `header` node in the list view's xml definition
```
<tree>
<header>
<button type="action|object" name="actionId|methodName" string="button's text" context="{}"/>
</header>
<field name="fieldName" />
</tree>
```
task-2246383
Diffstat (limited to 'addons/web/static/src/js/views')
4 files changed, 137 insertions, 25 deletions
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 ec7255b9e60..e1d132c498e 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -263,31 +263,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * @returns {Promise} */ _callButtonAction: function (attrs, record) { - var self = this; - var def = new Promise(function (resolve, reject) { - var reload = function () { - return self.isDestroyed() ? Promise.resolve() : self.reload(); - }; - record = record || self.model.get(self.handle); - - self.trigger_up('execute_action', { - action_data: _.extend({}, attrs, { - context: record.getContext({additionalContext: attrs.context || {}}), - }), - env: { - context: record.getContext(), - currentID: record.data.id, - model: record.model, - resIDs: record.res_ids, - }, - on_success: resolve, - on_fail: function () { - self.update({}, { reload: false }).then(reject).guardedCatch(reject); - }, - on_closed: reload, - }); + record = record || this.model.get(this.handle); + const actionData = Object.assign({}, attrs, { + context: record.getContext({additionalContext: attrs.context || {}}) }); - return this.alive(def); + const recordData = { + context: record.getContext(), + currentID: record.data.id, + model: record.model, + resIDs: record.res_ids, + }; + return this._executeButtonAction(actionData, recordData); }, /** * Called by the field manager mixin to confirm that a change just occured @@ -410,6 +396,39 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { } }, /** + * Executes the action associated with a button + * + * @private + * @param {Object} actionData: the descriptor of the action + * @param {string} actionData.type: the button's action's type, accepts "object" or "action" + * @param {string} actionData.name: the button's action's name + * either the model method's name for type "object" + * or the action's id in database, or xml_id + * @param {string} actionData.context: the action's execution context + * + * @param {Object} recordData: basic information on the current record(s) + * @param {number[]} recordData.resIDs: record ids: + * - on which an object method applies + * - that will be used as active_ids to load an action + * @param {string} recordData.model: model name + * @param {Object} recordData.context: the records' context, will be used to load + * the action, and merged into actionData.context at execution time + * + * @returns {Promise} + */ + async _executeButtonAction(actionData, recordData) { + const prom = new Promise((resolve, reject) => { + this.trigger_up('execute_action', { + action_data: actionData, + env: recordData, + on_closed: () => this.isDestroyed() ? Promise.resolve() : this.reload(), + on_success: resolve, + on_fail: () => this.update({}, { reload: false }).then(reject).guardedCatch(reject) + }); + }); + return this.alive(prom); + }, + /** * Override to add the current record ID (currentId) and the list of ids * (resIds) in the current dataPoint to the exported state. * 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 24dbc4683dd..97053682df3 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -13,6 +13,7 @@ var DataExport = require('web.DataExport'); var Dialog = require('web.Dialog'); var ListConfirmDialog = require('web.ListConfirmDialog'); var session = require('web.session'); +const viewUtils = require('web.viewUtils'); var _t = core._t; var qweb = core.qweb; @@ -44,12 +45,15 @@ var ListController = BasicController.extend({ * @param {Object} params * @param {boolean} params.editable * @param {boolean} params.hasActionMenus + * @param {Object[]} [params.headerButtons=[]]: a list of node descriptors + * for controlPanel's action buttons * @param {Object} params.toolbarActions * @param {boolean} params.noLeaf */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.hasActionMenus = params.hasActionMenus; + this.headerButtons = params.headerButtons || []; this.toolbarActions = params.toolbarActions || {}; this.editable = params.editable; this.noLeaf = params.noLeaf; @@ -138,6 +142,35 @@ var ListController = BasicController.extend({ } }, /** + * Renders (and updates) the buttons that are described inside the `header` + * node of the list view arch. Those buttons are visible when selecting some + * records. They will be appended to the controlPanel's buttons. + * + * @private + */ + _renderHeaderButtons() { + if (this.$headerButtons) { + this.$headerButtons.remove(); + this.$headerButtons = null; + } + if (!this.headerButtons.length || !this.selectedRecords.length) { + return; + } + const btnClasses = 'btn-primary btn-secondary btn-link btn-success btn-info btn-warning btn-danger'.split(' '); + let $elms = $(); + this.headerButtons.forEach(node => { + const $btn = viewUtils.renderButtonFromNode(node); + $btn.addClass('btn'); + if (!btnClasses.some(cls => $btn.hasClass(cls))) { + $btn.addClass('btn-secondary'); + } + $btn.on("click", this._onHeaderButtonClicked.bind(this, node)); + $elms = $elms.add($btn); + }); + this.$headerButtons = $elms; + this.$headerButtons.appendTo(this.$buttons); + }, + /** * Overrides to update the list of selected records * * @override @@ -589,6 +622,8 @@ var ListController = BasicController.extend({ * 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. + * Since header action buttons' display is dependent on the selection, we + * refresh them each time the selection is updated. * * @private */ @@ -607,6 +642,7 @@ var ListController = BasicController.extend({ })); this.$selectionBox.appendTo(this.$buttons); } + this._renderHeaderButtons(); }, //-------------------------------------------------------------------------- @@ -803,6 +839,44 @@ var ListController = BasicController.extend({ this._super.apply(this, arguments); }, /** + * @private + * @param {Object} node the button's node in the xml + * @returns {Promise} + */ + async _onHeaderButtonClicked(node) { + this._disableButtons(); + const state = this.model.get(this.handle); + try { + let resIds; + if (this.isDomainSelected) { + const limit = session.active_ids_limit; + resIds = await this._domainToResIds(state.getDomain(), limit); + } else { + resIds = this.getSelectedIds(); + } + // add the context of the button node (in the xml) and our custom one + // (active_ids and domain) to the action's execution context + const actionData = Object.assign({}, node.attrs, { + context: state.getContext({ additionalContext: node.attrs.context }), + }); + Object.assign(actionData.context, { + active_domain: state.getDomain(), + active_id: resIds[0], + active_ids: resIds, + active_model: state.model, + }); + // load the action with the correct context and record parameters (resIDs, model etc...) + const recordData = { + context: state.getContext(), + model: state.model, + resIDs: resIds, + }; + await this._executeButtonAction(actionData, recordData); + } finally { + this._enableButtons(); + } + }, + /** * Called when the renderer displays an editable row and the user tries to * leave it -> Saves the record associated to that line. * 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 6488a5744d9..eaa06929d35 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -366,7 +366,7 @@ var ListRenderer = BasicRenderer.extend({ }, }); _.each(this.arch.children, function (c) { - if (c.tag !== 'control' && c.tag !== 'groupby') { + if (c.tag !== 'control' && c.tag !== 'groupby' && c.tag !== 'header') { var reject = c.attrs.modifiers.column_invisible; // If there is an evaluated domain for the field we override the node // attribute to have the evaluated modifier value. diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js index abe05173bc8..5c36d01edf3 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -45,10 +45,14 @@ var ListView = BasicView.extend({ var expandGroups = !!JSON.parse(pyUtils.py_eval(this.arch.attrs.expand || "0", {'context': pyevalContext})); this.groupbys = {}; + this.headerButtons = []; this.arch.children.forEach(function (child) { if (child.tag === 'groupby') { self._extractGroup(child); } + if (child.tag === 'header') { + self._extractHeaderButtons(child); + } }); let editable = false; @@ -59,6 +63,7 @@ var ListView = BasicView.extend({ this.controllerParams.activeActions.export_xlsx = this.arch.attrs.export_xlsx ? !!JSON.parse(this.arch.attrs.export_xlsx): true; this.controllerParams.editable = editable; this.controllerParams.hasActionMenus = params.hasActionMenus; + this.controllerParams.headerButtons = this.headerButtons; this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.mode = 'readonly'; this.controllerParams.selectedRecords = selectedRecords; @@ -95,6 +100,20 @@ var ListView = BasicView.extend({ this.groupbys[node.attrs.name] = this._processFieldsView(innerView, 'groupby'); }, /** + * Extracts action buttons definitions from the <header> node of the list + * view definition + * + * @private + * @param {Object} node + */ + _extractHeaderButtons(node) { + node.children.forEach(child => { + if (child.tag === 'button' && !child.attrs.modifiers.invisible) { + this.headerButtons.push(child); + } + }); + }, + /** * @override */ _extractParamsFromAction: function (action) { |
