From a53335fdb2fe6e9232bde66f06e9499bece452d4 Mon Sep 17 00:00:00 2001 From: fwi-odoo Date: Tue, 25 Apr 2017 13:55:38 +0200 Subject: [PATCH] [FIX] web, document: adapt toolbar to the new views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix the toolbar behaviour since there is no more dataset in the widget. - Improve the communication beetwen the controller and the toolbar. - Move the code linked to the document upload in the right file. - Adapt the tests to mock the search_read done by ´document´ addon - Adapt the mock server to be able to give a custom toolbar action --- addons/document/static/src/js/document.js | 178 +++++++++- addons/web/static/src/js/chrome/sidebar.js | 311 +++++++++--------- .../src/js/views/basic/basic_controller.js | 39 +++ .../src/js/views/form/form_controller.js | 25 +- .../web/static/src/js/views/form/form_view.js | 2 +- .../src/js/views/list/list_controller.js | 41 ++- .../web/static/src/js/views/list/list_view.js | 1 + addons/web/static/src/xml/base.xml | 8 +- .../web/static/tests/helpers/mock_server.js | 5 +- addons/web/static/tests/helpers/test_utils.js | 2 +- addons/web/static/tests/views/form_tests.js | 140 ++++++-- addons/web/static/tests/views/list_tests.js | 38 +++ 12 files changed, 558 insertions(+), 232 deletions(-) diff --git a/addons/document/static/src/js/document.js b/addons/document/static/src/js/document.js index 05885839469..5fc69141583 100644 --- a/addons/document/static/src/js/document.js +++ b/addons/document/static/src/js/document.js @@ -1,33 +1,183 @@ -odoo.define('document.document', function(require) { +odoo.define('document.document', function (require) { "use strict"; var core = require('web.core'); +var Dialog = require('web.Dialog'); +var framework = require('web.framework'); var Sidebar = require('web.Sidebar'); var _t = core._t; Sidebar.include({ - init : function(){ + /** + * @override + */ + init : function (parent, options) { this._super.apply(this, arguments); - var view = this.getParent(); - if (view.fields_view && view.fields_view.type === "form") { + if (options.viewType === "form") { this.sections.splice(1, 0, { 'name' : 'files', 'label' : _t('Attachment(s)'), }); this.items.files = []; } + this.fileuploadId = _.uniqueId('oe_fileupload'); + $(window).on(this.fileuploadId, this._onFileUploaded.bind(this)); }, - on_attachments_loaded: function(attachments) { + /** + * Get the attachment linked to the record when the toolbar started + * + * @override + */ + start: function () { + var _super = this._super.bind(this); + this._updateAttachments().then(_super); + }, + /** + * @override + */ + destroy: function () { + $(window).off(this.fileuploadId); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * @override + */ + updateEnv: function (env) { + this.env = env; + this._updateAttachments().then(this._redraw.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Process the attachments then rerender the toolbar + * + * @private + * @param {Object} attachments + */ + _processAttachments: function (attachments) { //to display number in name if more then one attachment which has same name. - var self = this; _.chain(attachments) - .groupBy(function(attachment) { return attachment.name; }) - .each(function(attachment){ - if(attachment.length > 1) - _.map(attachment, function(attachment, i){ - attachment.name = _.str.sprintf(_t("%s (%s)"), attachment.name, i+1); - }); - }); - self._super(attachments); + .groupBy(function (attachment) { return attachment.name; }) + .each(function (attachment) { + if (attachment.length > 1) { + _.map(attachment, function (attachment, i) { + attachment.name = _.str.sprintf(_t("%s (%s)"), attachment.name, i+1); + }); + } + }); + _.each(attachments,function (a) { + a.label = a.name; + if (a.type === "binary") { + a.url = '/web/content/' + a.id + '?download=true'; + } + }); + this.items.files = attachments; }, + /** + * @private + * @override + */ + _redraw: function () { + this._super.apply(this, arguments); + this.$('.o_sidebar_add_attachment .o_form_binary_form') + .change(this._onAddAttachment.bind(this)); + this.$('.o_sidebar_delete_attachment') + .click(this._onDeleteAttachment.bind(this)); + }, + /** + * Update the attachments to be displayed in the attachment section + * of the toolbar + * + * @private + */ + _updateAttachments: function () { + var activeId = this.env.activeIds[0]; + if (!activeId) { + this.items.files = []; + return $.when(); + } else { + var domain = [ + ['res_model', '=', this.env.model], + ['res_id', '=', activeId], + ['type', 'in', ['binary', 'url']] + ]; + var fields = ['name', 'url', 'type', + 'create_uid', 'create_date', 'write_uid', 'write_date']; + return this._rpc({ + model: 'ir.attachment', + method: 'search_read', + context: this.env.context, + domain: domain, + fields: fields, + }).then(this._processAttachments.bind(this)); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Method triggered when user click on 'add attachment' and select a file + * + * @private + * @param {Event} event + */ + _onAddAttachment: function (event) { + var $event = $(event.target); + if ($event.val() !== '') { + var $binaryForm = this.$('form.o_form_binary_form'); + $binaryForm.submit(); + $binaryForm.find('input[type=file]').prop('disabled', true); + $binaryForm.find('button').prop('disabled', true).find('img, span').toggle(); + this.$('.o_sidebar_add_attachment a').text(_t('Uploading...')); + framework.blockUI(); + } + }, + /** + * Method triggered when user delete an attachment + * + * @private + * @param {Event} event + */ + _onDeleteAttachment: function (event) { + event.preventDefault(); + var self = this; + var $event = $(event.currentTarget); + var options = { + confirm_callback: function () { + self._rpc({ + model: 'ir.attachment', + method: 'unlink', + args: [parseInt($event.attr('data-id'), 10)], + }) + .then(self._updateAttachments.bind(self)) + .then(self._redraw.bind(self)); + } + }; + Dialog.confirm(this, _t("Do you really want to delete this attachment ?"), options); + }, + /** + * Handler called when the upload is done + * + * @private + */ + _onFileUploaded: function () { + var attachments = Array.prototype.slice.call(arguments, 1); + var uploadErrors = _.filter(attachments, function (attachment) { + return attachment.error; + }); + if (uploadErrors.length) { + this.do_warn(_t('Uploading Error'), uploadErrors[0].error); + } + this._updateAttachments().then(this._redraw.bind(this)); + framework.unblockUI(); + } }); }); diff --git a/addons/web/static/src/js/chrome/sidebar.js b/addons/web/static/src/js/chrome/sidebar.js index 3cd17334e9e..bf9e3668c35 100644 --- a/addons/web/static/src/js/chrome/sidebar.js +++ b/addons/web/static/src/js/chrome/sidebar.js @@ -3,9 +3,6 @@ odoo.define('web.Sidebar', function (require) { var Context = require('web.Context'); var core = require('web.core'); -var data = require('web.data'); -var Dialog = require('web.Dialog'); -var framework = require('web.framework'); var pyeval = require('web.pyeval'); var Widget = require('web.Widget'); @@ -13,12 +10,25 @@ var QWeb = core.qweb; var _t = core._t; var Sidebar = Widget.extend({ - init: function(parent, options) { - var self = this; - this._super(parent); + events: { + "click .dropdown-menu li a": "_onDropdownClicked" + }, + /** + * @override + * + * @param {Object} options + * @param {Object} options.items + * @param {Object} options.sections + * @param {Object} options.env + * @param {Object} options.actions + * + */ + init: function (parent, options) { + this._super.apply(this, arguments); this.options = _.defaults(options || {}, { 'editable': true }); + this.env = options.env; this.sections = options.sections || [ {name: 'print', label: _t('Print')}, {name: 'other', label: _t('Action')}, @@ -27,51 +37,37 @@ var Sidebar = Widget.extend({ print: [], other: [], }; - this.fileupload_id = _.uniqueId('oe_fileupload'); - $(window).on(this.fileupload_id, function() { - var args = [].slice.call(arguments).slice(1); - self.do_attachement_update(self.dataset, self.model_id,args); - framework.unblockUI(); - }); + if (options.actions) { + this._addToolbarActions(options.actions); + } }, - start: function() { - var self = this; - this._super(this); + /** + * @override + */ + start: function () { + this._super.apply(this, arguments); this.$el.addClass('btn-group'); - this.redraw(); - this.$el.on('click','.dropdown-menu li a', function(event) { - var section = $(this).data('section'); - var index = $(this).data('index'); - var item = self.items[section][index]; - if (item.callback) { - item.callback.apply(self, [item]); - } else if (item.action) { - self.on_item_action_clicked(item); - } else if (item.url) { - return true; - } - event.preventDefault(); - }); + this._redraw(); }, - destroy: function() { - $(window).off(this.fileupload_id); - return this._super.apply(this, arguments); - }, - redraw: function() { - this.$el.html(QWeb.render('Sidebar', {widget: this})); - // Hides Sidebar sections when item list is empty - this.$('.o_dropdown').each(function() { - if (!$(this).find('li').length) { - $(this).hide(); - } - }); - this.$("[title]").tooltip({ - delay: { show: 500, hide: 0} - }); - this.$('.o_sidebar_add_attachment .o_form_binary_form').change(this.on_attachment_changed); - this.$('.o_sidebar_delete_attachment').click(this.on_attachment_delete); + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Update the env for the sidebar then rerender it. + * + * @param {Object} env + */ + updateEnv: function (env) { + this.env = env; + this._redraw(); }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** * For each item added to the section: * @@ -86,7 +82,7 @@ var Sidebar = Widget.extend({ * function to call when the item is clicked in the sidebar, called * with the item descriptor as its first argument (so information * can be stored as additional keys on the object passed to - * ``add_items``) + * ``_addItems``) * * ``classname`` (optional) * ``@class`` set on the sidebar serialization of the item @@ -94,135 +90,126 @@ var Sidebar = Widget.extend({ * ``title`` (optional) * will be set as the item's ``@title`` (tooltip) * - * @param {String} section_code + * @private + * @param {String} sectionCode * @param {Array<{label, action | callback[, classname][, title]}>} items */ - add_items: function(section_code, items) { + _addItems: function (sectionCode, items) { if (items) { - this.items[section_code].unshift.apply(this.items[section_code],items); - this.redraw(); + this.items[sectionCode].unshift.apply(this.items[sectionCode], items); } }, - add_toolbar: function(toolbar) { + /** + * Method that will add the custom actions to the toolbar + * + * @private + * @param {Object} toolbarActions + */ + _addToolbarActions: function (toolbarActions) { var self = this; - _.each(['print','action','relate'], function(type) { - var items = toolbar[type]; - if (items) { - var actions = _.map(items, function (item) { - return { - label: item.name, - action: item, - }; - }); - self.add_items(type === 'print' ? 'print' : 'other', actions); - } - }); - }, - on_item_action_clicked: function(item) { - var self = this; - self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) { - var ids = self.getParent().getSelectedIds(); - var domain; - if (self.getParent().getActiveDomain) { - domain = self.getParent().getActiveDomain(); - } - else { - domain = $.Deferred().resolve(undefined); - } - if (ids.length === 0) { - new Dialog(this, {title: _t("Warning"), size: 'medium', $content: $("
").html(_t("You must choose at least one record."))}).open(); - return false; - } - var dataset = self.getParent().dataset; - var active_ids_context = { - active_id: ids[0], - active_ids: ids, - active_model: dataset.model, - }; - - $.when(domain).done(function (domain) { - if (domain !== undefined) { - active_ids_context.active_domain = domain; + _.each(['print','action','relate'], function (type) { + if (type in toolbarActions) { + var actions = toolbarActions[type]; + if (actions && actions.length) { + var items = _.map(actions, function (action) { + return { + label: action.name, + action: action, + }; + }); + self._addItems(type === 'print' ? 'print' : 'other', items); } - var c = pyeval.eval('context', - new Context( - sidebar_eval_context, active_ids_context)); - - self._rpc({ - route: '/web/action/load', - params: { - action_id: item.action.id, - context: new Context( - dataset.get_context(), active_ids_context).eval() - }, - }) - .done(function(result) { - result.context = new Context( - result.context || {}, active_ids_context) - .set_eval_context(c); - result.flags = result.flags || {}; - result.flags.new_window = true; - self.do_action(result, { - on_close: function() { - // reload view - self.getParent().reload(); - }, - }); - }); - }); - }); - }, - do_attachement_update: function(dataset, model_id, args) { - this.dataset = dataset; - this.model_id = model_id; - var upload_error = _.filter(args, function(attachment) {return attachment.error;}); - if (upload_error.length) { - this.do_warn(_t('Uploading Error'), upload_error[0].error); - } - if (!model_id) { - this.on_attachments_loaded([]); - } else { - var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ]; - var ds = new data.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom); - ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded); - } - }, - on_attachments_loaded: function(attachments) { - _.each(attachments,function(a) { - a.label = a.name; - if(a.type === "binary") { - a.url = '/web/content/' + a.id + '?download=true'; } }); - this.items.files = attachments; - this.redraw(); - }, - on_attachment_changed: function(e) { - var $e = $(e.target); - if ($e.val() !== '') { - this.$('form.o_form_binary_form').submit(); - $e.parent().find('input[type=file]').prop('disabled', true); - $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); - this.$('.o_sidebar_add_attachment a').text(_t('Uploading...')); - framework.blockUI(); + if ('other' in toolbarActions) { + this._addItems('other', toolbarActions.other); } }, - on_attachment_delete: function(e) { - e.preventDefault(); - e.stopPropagation(); + /** + * Performs the action for the item clicked after getting the data + * necessary with a trigger up + * + * @private + * @param {Object} item + */ + _onItemActionClicked: function (item) { var self = this; - var $e = $(e.currentTarget); - var options = { - confirm_callback: function () { - new data.DataSet(this, 'ir.attachment') - .unlink([parseInt($e.attr('data-id'), 10)]) - .done(function() { - self.do_attachement_update(self.dataset, self.model_id); + this.trigger_up('sidebar_data_asked', { + callback: function (env) { + self.env = env; + var activeIdsContext = { + active_id: env.activeIds[0], + active_ids: env.activeIds, + active_model: env.model, + }; + if (env.domain) { + activeIdsContext.active_domain = env.domain; + } + + var context = pyeval.eval('context', new Context(env.context, activeIdsContext)); + self._rpc({ + route: '/web/action/load', + params: { + action_id: item.action.id, + context: context, + }, + }).done(function (result) { + result.context = new Context( + result.context || {}, activeIdsContext) + .set_eval_context(context); + result.flags = result.flags || {}; + result.flags.new_window = true; + self.do_action(result, { + on_close: function () { + self.trigger_up('reload'); + }, }); + }); } - }; - Dialog.confirm(this, _t("Do you really want to delete this attachment ?"), options); - } + }); + }, + /** + * Method that renders the sidebar when there is a data update + * + * @private + */ + _redraw: function () { + this.$el.html(QWeb.render('Sidebar', {widget: this})); + + // Hides Sidebar sections when item list is empty + this.$('.o_dropdown').each(function () { + if (!$(this).find('li').length) { + $(this).hide(); + } + }); + this.$("[title]").tooltip({ + delay: { show: 500, hide: 0} + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Method triggered when the user clicks on a toolbar dropdown + * + * @private + * @param {MouseEvent} event + */ + _onDropdownClicked: function (event) { + var section = $(event.currentTarget).data('section'); + var index = $(event.currentTarget).data('index'); + var item = this.items[section][index]; + if (item.callback) { + item.callback.apply(this, [item]); + } else if (item.action) { + this._onItemActionClicked(item); + } else if (item.url) { + return true; + } + event.preventDefault(); + }, }); return Sidebar; 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 33f6727987e..87e8d1a6664 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -19,6 +19,7 @@ var _t = core._t; var BasicController = AbstractController.extend(FieldManagerMixin, { custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { reload: '_onReload', + sidebar_data_asked: '_onSidebarDataAsked' }), /** * @override @@ -126,6 +127,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { self._setMode('readonly', recordID); }); }, + /** + * Method that will be overriden by the views with the ability to have selected ids + * + * @returns [] + */ + getSelectedIds: function () { + return []; + }, /** * @override */ @@ -287,6 +296,19 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { doIt(); } }, + /** + * Returns the new sidebar env + * + * @private + * @return {Object} the new sidebar env + */ + _getSidebarEnv: function () { + return { + context: this.model.get(this.handle).getContext(), + activeIds: this.getSelectedIds(), + model: this.modelName, + }; + }, /** * Used by list and kanban views to determine whether or not to display * the no content helper (if there is no data in the state to display) @@ -389,9 +411,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { /** * Helper method, to get the current environment variables from the model * and notifies the component chain (by bubbling an event up) + * + * @private */ _updateEnv: function () { var env = this.model.get(this.handle, {env: true}); + if (this.sidebar) { + var sidebarEnv = this._getSidebarEnv(); + this.sidebar.updateEnv(sidebarEnv); + } this.trigger_up('env_updated', env); }, /** @@ -457,6 +485,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { this.reload({fieldNames: data.fieldNames}); } }, + /** + * Handler used to get all the data necessary when a custom action is + * performed through the sidebar. + * + * @private + * @param {OdooEvent} event + */ + _onSidebarDataAsked: function (event) { + var sidebarEnv = this._getSidebarEnv(); + event.data.callback(sidebarEnv); + }, }); return BasicController; 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 e3b129c7915..f15bdfa5382 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -21,6 +21,9 @@ var FormController = BasicController.extend({ }), /** * @override + * + * @param {boolean} params.hasSidebar + * @param {Object} params.toolbarActions */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); @@ -29,7 +32,7 @@ var FormController = BasicController.extend({ this.footerToButtons = params.footerToButtons; this.defaultButtons = params.defaultButtons; this.hasSidebar = params.hasSidebar; - this.toolbar = params.toolbar; + this.toolbarActions = params.toolbarActions || {}; }, //-------------------------------------------------------------------------- @@ -71,13 +74,12 @@ var FormController = BasicController.extend({ * Returns the current res_id, wrapped in a list. This is only used by the * sidebar (and the debugmanager) * - * @todo This should be private. Need to change sidebar code + * @override * * @returns {number[]} either [current res_id] or [] */ getSelectedIds: function () { var env = this.model.get(this.handle, {env: true}); - // FIX ME : fix sidebar widget return env.currentId ? [env.currentId] : []; }, /** @@ -137,12 +139,6 @@ var FormController = BasicController.extend({ **/ renderSidebar: function ($node) { if (!this.sidebar && this.hasSidebar) { - this.sidebar = new Sidebar(this, { - editable: this.is_action_enabled('edit') - }); - if (this.toolbar) { - this.sidebar.add_toolbar(this.toolbar); - } var otherItems = []; if (this.is_action_enabled('delete')) { otherItems.push({ @@ -156,7 +152,16 @@ var FormController = BasicController.extend({ callback: this._onDuplicateRecord.bind(this), }); } - this.sidebar.add_items('other', otherItems); + this.sidebar = new Sidebar(this, { + editable: this.is_action_enabled('edit'), + viewType: 'form', + env: { + context: this.model.get(this.handle).getContext(), + activeIds: this.getSelectedIds(), + model: this.modelName, + }, + actions: _.extend(this.toolbarActions, {other: otherItems}), + }); this.sidebar.appendTo($node); // Show or hide the sidebar according to the view mode diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js index 4fff1a74ee4..aa6bdf69755 100644 --- a/addons/web/static/src/js/views/form/form_view.js +++ b/addons/web/static/src/js/views/form/form_view.js @@ -30,7 +30,7 @@ var FormView = BasicView.extend({ this.loadParams.parentID = params.parentID; this.controllerParams.hasSidebar = params.sidebar; - this.controllerParams.toolbar = false; + this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.footerToButtons = params.footer_to_buttons; if ('action' in params && 'flags' in params.action) { this.controllerParams.footerToButtons = params.action.flags.footer_to_buttons; 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 50f47a73fc0..f2b15cca349 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -30,16 +30,15 @@ var ListController = BasicController.extend({ * @constructor * @override * @param {Object} params - * @param {Object} params * @param {boolean} params.editable * @param {boolean} params.hasSidebar - * @param {boolean} params.hasToolbar + * @param {Object} params.toolbarActions * @param {boolean} params.noLeaf */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.hasSidebar = params.hasSidebar; - this.hasToolbar = params.hasToolbar; + this.toolbarActions = params.toolbarActions || {}; this.editable = params.editable; this.noLeaf = params.noLeaf; this.selectedRecords = []; // there is no selected record by default @@ -72,7 +71,7 @@ var ListController = BasicController.extend({ * 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 sidebar. The full mechanism is wrong, + * @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... * @@ -99,7 +98,7 @@ var ListController = BasicController.extend({ * Returns the list of currently selected res_ids (with the check boxes on * the left) * - * @todo This should be private. Need to change sidebar code + * @override * * @returns {number[]} list of res_ids */ @@ -135,12 +134,6 @@ var ListController = BasicController.extend({ */ renderSidebar: function ($node) { if (this.hasSidebar && !this.sidebar) { - this.sidebar = new Sidebar(this, { - editable: this.is_action_enabled('edit') - }); - if (this.hasToolbar) { - this.sidebar.add_toolbar(this.fields_view.toolbar); - } var other = [{ label: _t("Export"), callback: this._onExportData.bind(this) @@ -160,8 +153,16 @@ var ListController = BasicController.extend({ label: _t('Delete'), callback: this._onDeleteSelectedRecords.bind(this) }); + this.sidebar = new Sidebar(this, { + editable: this.is_action_enabled('edit'), + env: { + context: this.model.get(this.handle).getContext(), + activeIds: this.getSelectedIds(), + model: this.modelName, + }, + actions: _.extend(this.toolbarActions, {other: other}), + }); } - this.sidebar.add_items('other', other); this.sidebar.appendTo($node); this._toggleSidebar(); @@ -232,6 +233,15 @@ var ListController = BasicController.extend({ var state = this.model.get(this.handle); return this.renderer.confirmSave(state, id); }, + /** + * @override + * @private + */ + _getSidebarEnv: function() { + var env = this._super.apply(this, arguments); + var record = this.model.get(this.handle); + return _.extend(env, {domain: record.getDomain()}); + }, /** * Allows to change the mode of a single row. * @@ -380,6 +390,13 @@ var ListController = BasicController.extend({ this.selectedRecords = event.data.selection; this._toggleSidebar(); }, + /** + * @override + */ + _onSidebarDataAsked: function (event) { + var env = this._getSidebarEnv(); + event.data.callback(env); + }, /** * Called when clicking on 'Archive' or 'Unarchive' in the sidebar. * 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 7b597c712bc..61612282bf5 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -41,6 +41,7 @@ var ListView = BasicView.extend({ this.controllerParams.editable = arch.attrs.editable; this.controllerParams.hasSidebar = params.sidebar; + this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.activeActions.delete = true; this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf; this.controllerParams.mode = mode; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index fec8dd8c7ee..6ffe8fca2db 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -241,12 +241,12 @@
  • - + /web/binary/upload_attachment - - - + + + Add...
  • diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index cbd1f1b1e1f..60024292edd 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -61,12 +61,15 @@ var MockServer = Class.extend({ * @param {string} model a model name (that should be in this.data) * @returns {Object} an object with 2 keys: arch and fields */ - fieldsViewGet: function (arch, model) { + fieldsViewGet: function (arch, model, toolbar) { var fields = $.extend(true, {}, this.data[model].fields); var fvg = this._fieldsViewGet(arch, model, fields); var fields_views = {}; fields_views[fvg.type] = fvg; data_manager.processViews(fields_views, fields); + if (toolbar) { + fvg.toolbar = toolbar; + } return fields_views[fvg.type]; }, /** diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 1c04548a218..516971f96fe 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -126,7 +126,7 @@ function createAsyncView(params) { // add mock environment: mock server, session, fieldviewget, ... var mockServer = addMockEnvironment(widget, params); - var viewInfo = mockServer.fieldsViewGet(params.arch, params.model); + var viewInfo = mockServer.fieldsViewGet(params.arch, params.model, params.toolbar); // create the view var viewOptions = { diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 4b6f43add2b..9295f0fa236 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1082,6 +1082,12 @@ QUnit.module('Views', { '', viewOptions: {sidebar: true}, res_id: 1, + mockRPC: function(route, args) { + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); + }, }); assert.ok(!form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible'); @@ -1221,6 +1227,12 @@ QUnit.module('Views', { '', res_id: 1, viewOptions: {sidebar: true}, + mockRPC: function(route, args) { + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); + }, }); assert.strictEqual(form.get('title'), 'first record', @@ -1668,6 +1680,12 @@ QUnit.module('Views', { arch: '
    ', res_id: 1, viewOptions: {sidebar: true}, + mockRPC: function(route, args) { + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); + }, }); form.$buttons.find('.o_form_button_edit').click(); @@ -2009,6 +2027,12 @@ QUnit.module('Views', { sidebar: true, }, res_id: 1, + mockRPC: function(route, args) { + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); + }, }); assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1'); @@ -2048,6 +2072,9 @@ QUnit.module('Views', { }, res_id: 1, mockRPC: function (route, args) { + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } assert.step(args.method); return this._super.apply(this, arguments); } @@ -3598,47 +3625,106 @@ QUnit.module('Views', { }); QUnit.test('onchanges are applied before checking if it can be saved', function (assert) { - assert.expect(4); + assert.expect(4); - this.data.partner.onchanges.foo = function (obj) {}; - this.data.partner.fields.foo.required = true; + this.data.partner.onchanges.foo = function (obj) {}; + this.data.partner.fields.foo.required = true; - var def = $.Deferred(); + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
    ' + + '' + + '' + + '' + + '
    ', + res_id: 2, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + assert.step(args.method); + if (args.method === 'onchange') { + return def.then(function () { + return result; + }); + } + return result; + }, + intercepts: { + warning: function () { + assert.step('warning'); + }, + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="foo"]').val('').trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + + def.resolve(); + + assert.verifySteps(['read', 'onchange', 'warning']) + form.destroy(); + }); + + QUnit.test('display toolbar', function (assert) { + assert.expect(7); var form = createView({ View: FormView, model: 'partner', data: this.data, + res_id: 1, arch: '
    ' + - '' + - '' + - '' + + '' + '
    ', - res_id: 2, - mockRPC: function (route, args) { - var result = this._super.apply(this, arguments); - assert.step(args.method); - if (args.method === 'onchange') { - return def.then(function () { - return result; - }); - } - return result; + toolbar: { + action: [{ + model_name: 'partner', + name: 'Action partner', + type: 'ir.actions.server', + usage: 'ir_actions_server', + }], + print: [], }, - intercepts: { - warning: function () { - assert.step('warning'); - }, + viewOptions: { + sidebar: true, + }, + mockRPC: function(route, args) { + if (route === '/web/action/load') { + assert.strictEqual(args.context.active_id, 1, + "the active_id shoud be 1."); + assert.deepEqual(args.context.active_ids, [1], + "the active_ids should be an array with 1 inside."); + return $.when({}); + } + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); }, }); - form.$buttons.find('.o_form_button_edit').click(); - form.$('input[name="foo"]').val('').trigger("input"); - form.$buttons.find('.o_form_button_save').click(); + var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn'); + assert.strictEqual($dropdowns.length, 3, + "there should be 3 dropdowns (print, attachment, action) in the toolbar."); + var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[2].children; + assert.strictEqual($actions.length, 3, + "there should be 3 actions"); + var $customAction = $('.o_web_client .o_control_panel .btn-group .dropdown-menu li a')[2]; + assert.strictEqual($customAction.text.trim(), 'Action partner', + "the custom action should have 'Action partner' as name"); + testUtils.intercept(form, 'do_action', function (event) { + var context = event.data.action.context.__contexts[1]; + assert.strictEqual(context.active_id, 1, + "the active_id shoud be 1."); + assert.deepEqual(context.active_ids, [1], + "the active_ids should be an array with 1 inside."); + }); + $customAction.click(); - def.resolve(); - - assert.verifySteps(['read', 'onchange', 'warning']) form.destroy(); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 4a8c87be598..0b754c2b5bb 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -574,6 +574,9 @@ QUnit.module('Views', { viewOptions: {sidebar: true}, arch: '', mockRPC: function (route) { + if (route === '/web/dataset/call_kw/ir.attachment/search_read') { + return $.when([]); + } assert.step(route); return this._super.apply(this, arguments); }, @@ -1693,6 +1696,41 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('display toolbar', function (assert) { + assert.expect(3); + + var list = createView({ + View: ListView, + model: 'event', + data: this.data, + arch: '', + toolbar: { + action: [{ + model_name: 'event', + name: 'Action event', + type: 'ir.actions.server', + usage: 'ir_actions_server', + }], + print: [], + }, + viewOptions: { + sidebar: true, + }, + }); + + var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn'); + assert.strictEqual($dropdowns.length, 2, + "there should be 2 dropdowns in the toolbar."); + var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[1].children; + assert.strictEqual($actions.length, 3, + "there should be 3 actions"); + var $customAction = $('.o_web_client .o_control_panel .btn-group .dropdown-menu li a')[2]; + assert.strictEqual($customAction.text.trim(), 'Action event', + "the custom action should have 'Action event' as name"); + + list.destroy(); + }); + QUnit.test('edit list line after line deletion', function (assert) { assert.expect(5);