summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/chrome
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2018-12-06 12:39:06 +0000
committerGéry Debongnie <ged@odoo.com>2018-12-07 13:16:06 +0000
commit40dd121938552d3f88289d2e5fcd1d57a0dd01bb (patch)
tree4830c8ffdea1e10d35c7ddf096f536bd8999240f /addons/web/static/src/js/chrome
parent48a50b4bc011fc6dfc82a673f921fad941dc79cf (diff)
[REF] web: move ControlPanel inside controllers/actions
This branch introduces a large-scale reorganization of the component tree generated by the web client. The short version is that now, the control panel is a child of the view controller and no longer a sibling. Graphically (and simplified), we go from this: webClient / | \ ... ... actionManager / \ controlPanel viewController / \ to this: webClient / | \ ... ... actionManager | viewController / | \ ControlPanelController / \ CPRenderer CPModel The motivation is that this work moves the code where it should be. Before this commit, it was kind of weird to have code in the controllers to render buttons outside of their root node (in renderButtons). Also, the action manager had to take care of coordinating search view states between view transitions. So, this work simplifies the code. It also makes it easier to extend. We see day after day that Odoo needs to take care of more complex UI needs, and in many cases, these needs were quite difficult to implement (we prefer spaghettis in our plates, not in our code). The changes in this branch should open the way to implement these features. For example, - it will now be easy to add the possibility of views (for example, the search view or a new ControlPanel view) to add custom buttons (of type action or object) in the control panel. - Another need will be to serialize/ restore the state of the search view across action boundaries (needed by the dashboard). - Another example is the possibility for views to customize easily the presence/absence of sub menus (filters/groupbys/favorites/ time range/...) - Another need is an easier way for views/client action to customize their control panel. This commit also contains a large rewrite of the search view (so it is more inline with our architecture and easier to maintain). Part of task 1893568 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Martin Geubelle <mge@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/chrome')
-rw-r--r--addons/web/static/src/js/chrome/abstract_action.js144
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js17
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js169
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js484
-rw-r--r--addons/web/static/src/js/chrome/action_mixin.js182
-rw-r--r--addons/web/static/src/js/chrome/control_panel.js286
6 files changed, 495 insertions, 787 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_action.js b/addons/web/static/src/js/chrome/abstract_action.js
index c4755338bf0..226802ba76b 100644
--- a/addons/web/static/src/js/chrome/abstract_action.js
+++ b/addons/web/static/src/js/chrome/abstract_action.js
@@ -2,82 +2,126 @@ odoo.define('web.AbstractAction', function (require) {
"use strict";
/**
- * We define here the generic notion of Action (from the point of view of the
- * web client). In short, an action is a widget which controls the main part
- * of the screen (everything below the control panel).
- *
- * More precisely, the action manager is the component that coordinates a stack
- * of actions. Whenever the user navigates in the interface, switches views,
- * open different menus, the action manager creates/updates/destroys special
- * widgets which are actions. These actions need to answer to a standardised
- * API, which is the reason for this AbstractAction class.
- *
- * In practice, most actions are view controllers (coming from a
- * ir.action.act_window). However, some actions are 'client actions'. They
- * also need to be an AbstractAction for a better cooperation with the action
- * manager.
+ * We define here the AbstractAction widget, which implements the ActionMixin.
+ * All client actions must extend this widget.
*
* @module web.AbstractAction
*/
+var ActionMixin = require('web.ActionMixin');
+var ControlPanelView = require('web.ControlPanelView');
var Widget = require('web.Widget');
-var AbstractAction = Widget.extend({
+var AbstractAction = Widget.extend(ActionMixin, {
+ config: {
+ ControlPanelView: ControlPanelView,
+ },
/**
- * Called each time the action is attached into the DOM.
+ * If this flag is set to true, the client action will create a control
+ * panel whenever it is created.
+ *
+ * @type boolean
*/
- on_attach_callback: function () {},
+ hasControlPanel: false,
+
/**
- * Called each time the action is detached from the DOM.
+ * If true, this flag indicates that the client action should automatically
+ * fetch the <arch> of a search view (or control panel view). Note that
+ * to do that, it also needs a specific modelName.
+ *
+ * For example, the Discuss application adds the following line in its
+ * constructor:
+ *
+ * this.controlPanelParams.modelName = 'mail.message';
+ *
+ * @type boolean
*/
- on_detach_callback: function () {},
+ loadControlPanel: false,
+
/**
- * Called by the action manager when action is restored (typically, when the
- * user clicks on the action in the breadcrumb)
+ * A client action might want to use a search bar in its control panel, or
+ * it could choose not to use it.
+ *
+ * Note that it only makes sense if hasControlPanel is set to true.
*
- * @returns {Deferred|undefined}
+ * @type boolean
*/
- willRestore: function () {},
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
+ withSearchBar: false,
/**
- * In some situations, we need confirmation from the controller that the
- * current state can be destroyed without prejudice to the user. For
- * example, if the user has edited a form, maybe we should ask him if we
- * can discard all his changes when we switch to another action. In that
- * case, the action manager will call this method. If the returned
- * deferred is succesfully resolved, then we can destroy the current action,
- * otherwise, we need to stop.
+ * This parameter can be set to customize the available sub menus in the
+ * controlpanel (Filters/Group By/Favorites). This is basically a list of
+ * the sub menus that we want to use.
+ *
+ * Note that it only makes sense if hasControlPanel is set to true.
+ *
+ * For example, set ['filter', 'favorite'] to enable the Filters and
+ * Favorites menus.
*
- * @returns {Deferred} resolved if the action can be removed, rejected otherwise
+ * @type string[]
*/
- canBeRemoved: function () {
- return $.when();
- },
+ searchMenuTypes: [],
+
/**
- * This function is called when the current context (~state) of the action
- * should be known. For instance, if the action is a view controller,
- * this may be useful to reinstantiate the view in the same state.
+ * @override
+ *
+ * @param {Widget} parent
+ * @param {Object} action
+ * @param {Object} [options]
*/
- getContext: function () {
+ init: function (parent, action, options) {
+ this._super(parent);
+ this._title = action.display_name || action.name;
+ this.controlPanelParams = {
+ actionId: action.id,
+ context: action.context,
+ breadcrumbs: options && options.breadcrumbs || [],
+ title: this.getTitle(),
+ viewId: action.search_view_id && action.search_view_id[0],
+ withSearchBar: this.withSearchBar,
+ searchMenuTypes: this.searchMenuTypes,
+ };
},
/**
- * Gives the focus to the action
+ * The willStart method is actually quite complicated if the client action
+ * has a controlPanel, because it needs to prepare it.
+ *
+ * @override
*/
- giveFocus: function () {
+ willStart: function () {
+ var self = this;
+ if (this.hasControlPanel) {
+ var params = this.controlPanelParams;
+ var def;
+ if (this.loadControlPanel) {
+ def = this
+ .loadFieldView(params.modelName, params.context || {}, params.viewId, 'search')
+ .then(function (fieldsView) {
+ params.viewInfo = {
+ arch: fieldsView.arch,
+ fields: fieldsView.fields,
+ };
+ });
+ }
+ return $.when(def).then(function () {
+ var controlPanelView = new self.config.ControlPanelView(params);
+ return controlPanelView.getController(self).then(function (controlPanel) {
+ self._controlPanel = controlPanel;
+ return self._controlPanel.appendTo(document.createDocumentFragment());
+ });
+ });
+ }
+ return $.when();
},
/**
- * Renders the buttons to append, in most cases, to the control panel (in
- * the bottom left corner). When the action is rendered in a dialog, those
- * buttons might be moved to the dialog's footer.
- *
- * @param {jQuery Node} $node
+ * @override
*/
- renderButtons: function ($node) {
+ start: function () {
+ if (this._controlPanel) {
+ this._controlPanel.$el.prependTo(this.$el);
+ }
+ return this._super.apply(this, arguments);
},
});
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 80711fd24ed..ea8fdc24a18 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -60,10 +60,11 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
},
load_filters: function (event) {
return data_manager
- .load_filters(event.data.dataset, event.data.action_id)
+ .load_filters(event.data)
.then(event.data.on_success);
},
create_filter: '_onCreateFilter',
+ delete_filter: '_onDeleteFilter',
push_state: '_onPushState',
show_effect: '_onShowEffect',
// session
@@ -218,7 +219,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
this.action_manager = new ActionManager(this, session.user_context);
var fragment = document.createDocumentFragment();
return this.action_manager.appendTo(fragment).then(function () {
- dom.append(self.$('.o_main_content'), fragment, {
+ dom.append(self.$el, fragment, {
in_DOM: true,
callbacks: [{widget: self.action_manager}],
});
@@ -368,6 +369,18 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
.then(e.data.on_success);
},
/**
+ * @private
+ * @param {OdooEvent} e
+ * @param {Object} e.data.filter the filter description
+ * @param {function} e.data.on_success called when the RPC succeeds with its
+ * returned value as argument
+ */
+ _onDeleteFilter: function (e) {
+ data_manager
+ .delete_filter(e.data.filterId)
+ .then(e.data.on_success);
+ },
+ /**
* Displays a warning in a dialog or with the notification service
*
* @private
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 18bbb6a3681..c798189c0a0 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -10,10 +10,8 @@ odoo.define('web.ActionManager', function (require) {
*/
var AbstractAction = require('web.AbstractAction');
-var Bus = require('web.Bus');
var concurrency = require('web.concurrency');
var Context = require('web.Context');
-var ControlPanel = require('web.ControlPanel');
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
@@ -24,7 +22,7 @@ var Widget = require('web.Widget');
var _t = core._t;
var ActionManager = Widget.extend({
- className: 'o_content',
+ className: 'o_action_manager',
custom_events: {
breadcrumb_clicked: '_onBreadcrumbClicked',
history_back: '_onHistoryBack',
@@ -64,17 +62,6 @@ var ActionManager = Widget.extend({
this.currentDialogController = null;
},
/**
- * @override
- */
- start: function () {
- // AAB: temporarily instantiate a unique main ControlPanel used by
- // controllers in the controllerStack
- this.controlPanel = new ControlPanel(this);
- var def = this.controlPanel.insertBefore(this.$el);
-
- return $.when(def, this._super.apply(this, arguments));
- },
- /**
* Called each time the action manager is attached into the DOM.
*/
on_attach_callback: function () {
@@ -266,14 +253,6 @@ var ActionManager = Widget.extend({
if (controller.scrollPosition) {
this.trigger_up('scrollTo', controller.scrollPosition);
}
-
- if (!controller.widget.need_control_panel) {
- this.controlPanel.do_hide();
- } else {
- this.controlPanel.update({
- breadcrumbs: this._getBreadcrumbs(),
- }, {clear: false});
- }
},
/**
* Closes the current dialog, if any. Because we listen to the 'closed'
@@ -330,19 +309,12 @@ var ActionManager = Widget.extend({
return this._executeActionInDialog(action, options);
}
+ var controller = self.controllers[action.controllerID];
return this.clearUncommittedChanges()
.then(function () {
- var controller = self.controllers[action.controllerID];
- var widget = controller.widget;
- // AAB: this will be moved to the Controller
- if (widget.need_control_panel) {
- // set the ControlPanel bus on the controller to allow it to
- // communicate its status
- widget.set_cp_bus(self.controlPanel.get_bus());
- }
return self.dp.add(self._startController(controller));
})
- .then(function (controller) {
+ .then(function () {
if (self.currentDialogController) {
self._closeDialog({ silent: true });
}
@@ -358,7 +330,7 @@ var ActionManager = Widget.extend({
}
// update the internal state and the DOM
- self._pushController(controller, options);
+ self._pushController(controller);
// toggle the fullscreen mode for actions in target='fullscreen'
self._toggleFullscreen();
@@ -387,12 +359,6 @@ var ActionManager = Widget.extend({
var self = this;
var controller = this.controllers[action.controllerID];
var widget = controller.widget;
- // AAB: this will be moved to the Controller
- if (widget.need_control_panel) {
- // set the ControlPanel bus on the controller to allow it to
- // communicate its status
- widget.set_cp_bus(new Bus());
- }
return this._startController(controller).then(function (controller) {
var prevDialogOnClose;
@@ -472,20 +438,18 @@ var ActionManager = Widget.extend({
}
var controllerID = _.uniqueId('controller_');
+
+ var index = this._getControllerStackIndex(options);
+ options.breadcrumbs = this._getBreadcrumbs(this.controllerStack.slice(0, index));
options.controllerID = controllerID;
+ var widget = new ClientAction(this, action, options);
var controller = {
actionID: action.jsID,
+ index: index,
jsID: controllerID,
- widget: new ClientAction(this, action, options),
+ title: widget.getTitle(),
+ widget: widget,
};
- // AAB: TODO: simplify this with AbstractAction (implement a getTitle
- // function that returns action.name by default, and that can be
- // overriden in client actions and view controllers)
- Object.defineProperty(controller, 'title', {
- get: function () {
- return controller.widget.get('title') || action.display_name || action.name;
- },
- });
this.controllers[controllerID] = controller;
action.controllerID = controllerID;
return this._executeAction(action, options).done(function () {
@@ -586,34 +550,52 @@ var ActionManager = Widget.extend({
return $.when();
},
/**
- * Returns a description of the current stack of controllers, used to render
- * the breadcrumbs. It is an array of Objects with keys 'title' (what to
- * display in the breadcrumbs) and 'controllerID' (the ID of the
- * corresponding controller, used to restore it when this part of the
+ * Returns a description of the controllers in the given controller stack.
+ * It is used to render the breadcrumbs. It is an array of Objects with keys
+ * 'title' (what to display in the breadcrumbs) and 'controllerID' (the ID
+ * of the corresponding controller, used to restore it when this part of the
* breadcrumbs is clicked).
- * Ignores the content of the stack of controllers if the action of the
- * last controller of the stack is flagged with 'no_breadcrumbs', indicating
- * that the breadcrumbs should not be displayed for that action.
*
* @private
+ * @param {string[]} controllerStack
* @returns {Object[]}
*/
- _getBreadcrumbs: function () {
+ _getBreadcrumbs: function (controllerStack) {
var self = this;
- var currentController = this.getCurrentController();
- var noBreadcrumbs = !currentController ||
- this.actions[currentController.actionID].context.no_breadcrumbs;
- if (noBreadcrumbs) {
- return [];
- }
- return _.map(this.controllerStack, function (controllerID) {
+ return _.map(controllerStack, function (controllerID) {
return {
- title: self.controllers[controllerID].title,
controllerID: controllerID,
+ title: self.controllers[controllerID].title,
};
});
},
/**
+ * Returns the index where a controller should be inserted in the controller
+ * stack according to the given options. By default, a controller is pushed
+ * on the top of the stack.
+ *
+ * @private
+ * @param {options} [options.clear_breadcrumbs=false] if true, insert at
+ * index 0 and remove all other controllers
+ * @param {options} [options.index=null] if given, that index is returned
+ * @param {options} [options.replace_last_action=false] if true, replace the
+ * last controller of the stack
+ * @returns {integer} index
+ */
+ _getControllerStackIndex: function (options) {
+ var index;
+ if ('index' in options) {
+ index = options.index;
+ } else if (options.clear_breadcrumbs) {
+ index = 0;
+ } else if (options.replace_last_action) {
+ index = this.controllerStack.length - 1;
+ } else {
+ index = this.controllerStack.length;
+ }
+ return index;
+ },
+ /**
* Returns an object containing information about the given controller, like
* its title, its action's id, the active_id and active_ids of the action...
*
@@ -625,7 +607,7 @@ var ActionManager = Widget.extend({
var controller = this.controllers[controllerID];
var action = this.actions[controller.actionID];
var state = {
- title: controller.title,
+ title: controller.widget.getTitle(),
};
if (action.id) {
state.action = action.id;
@@ -706,41 +688,28 @@ var ActionManager = Widget.extend({
* @param {Object} controller
* @param {string} controller.jsID
* @param {Widget} controller.widget
- * @param {Object} [options]
- * @param {Object} [options.clear_breadcrumbs=false] if true, destroys all
- * controllers from the controller stack before adding the given one
- * @param {Object} [options.replace_last_action=false] if true, replaces the
- * last controller of the controller stack by the given one
- * @param {integer} [options.index] if given, pushes the controller at that
- * position in the controller stack, and destroys the controllers with an
- * higher index
- */
- _pushController: function (controller, options) {
- options = options || {};
+ * @param {integer} controller.index the controller is pushed at that
+ * position in the controller stack and controllers with an higher index
+ * are destroyed
+ */
+ _pushController: function (controller) {
var self = this;
// detach the current controller
this._detachCurrentController();
- // empty the controller stack or replace the last controller as requested,
- // destroy the removed controllers and push the new controller to the stack
- var toDestroy;
- if (options.clear_breadcrumbs) {
- toDestroy = this.controllerStack;
- this.controllerStack = [];
- } else if (options.replace_last_action && this.controllerStack.length > 0) {
- toDestroy = [this.controllerStack.pop()];
- } else if (options.index !== undefined) {
- toDestroy = this.controllerStack.splice(options.index);
- // reject from the list of controllers to destroy the one that we are
- // currently pushing, or those linked to the same action as the one
- // linked to the controller that we are pushing
- toDestroy = _.reject(toDestroy, function (controllerID) {
- return controllerID === controller.jsID ||
- self.controllers[controllerID].actionID === controller.actionID;
- });
- }
+ // push the new controller to the stack at the given position, and
+ // destroy controllers with an higher index
+ var toDestroy = this.controllerStack.slice(controller.index);
+ // reject from the list of controllers to destroy the one that we are
+ // currently pushing, or those linked to the same action as the one
+ // linked to the controller that we are pushing
+ toDestroy = _.reject(toDestroy, function (controllerID) {
+ return controllerID === controller.jsID ||
+ self.controllers[controllerID].actionID === controller.actionID;
+ });
this._removeControllers(toDestroy);
+ this.controllerStack = this.controllerStack.slice(0, controller.index);
this.controllerStack.push(controller.jsID);
// append the new controller to the DOM
@@ -861,7 +830,7 @@ var ActionManager = Widget.extend({
return $.when(def).then(function () {
return $.when(controller.widget.do_show()).then(function () {
var index = _.indexOf(self.controllerStack, controllerID);
- self._pushController(controller, {index: index});
+ self._pushController(controller, index);
});
});
},
@@ -878,19 +847,7 @@ var ActionManager = Widget.extend({
* @returns {Deferred<Object>} resolved with the controller when it is ready
*/
_startController: function (controller) {
- var self = this;
var fragment = document.createDocumentFragment();
- // AAB: change this logic to stop using the properties mixin
- controller.widget.on("change:title", this, function () {
- if (self.getCurrentController() !== controller) {
- return;
- }
- var action = self.actions[controller.actionID];
- if (!action.flags || !action.flags.headless) {
- var breadcrumbs = self._getBreadcrumbs();
- self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false});
- }
- });
return controller.widget.appendTo(fragment).then(function () {
return controller;
});
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index e66ae801bc1..1a688ffa6b2 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -10,21 +10,12 @@ var ActionManager = require('web.ActionManager');
var config = require('web.config');
var Context = require('web.Context');
var core = require('web.core');
-var data = require('web.data'); // this will be removed at some point
var pyUtils = require('web.py_utils');
-var SearchView = require('web.SearchView');
var view_registry = require('web.view_registry');
-var _t = core._t;
-
ActionManager.include({
custom_events: _.extend({}, ActionManager.prototype.custom_events, {
- env_updated: '_onEnvUpdated',
execute_action: '_onExecuteAction',
- get_controller_context: '_onGetControllerContext',
- navigation_move: '_onNavigationMove',
- update_filters: '_onUpdateFilters',
- search: '_onSearch',
switch_view: '_onSwitchView',
}),
@@ -33,6 +24,26 @@ ActionManager.include({
//--------------------------------------------------------------------------
/**
+ * Override to handle the case of lazy-loaded controllers, which may be the
+ * last controller in the stack, but which should not be considered as
+ * current controller as they don't have an alive widget.
+ *
+ * Note: this function assumes that there can be at most one lazy loaded
+ * controller in the stack
+ *
+ * @override
+ */
+ getCurrentController: function () {
+ var currentController = this._super.apply(this, arguments);
+ var action = currentController && this.actions[currentController.actionID];
+ if (action && action.type === 'ir.actions.act_window' && !currentController.widget) {
+ var lastControllerID = this.controllerStack.pop();
+ currentController = this._super.apply(this, arguments);
+ this.controllerStack.push(lastControllerID);
+ }
+ return currentController;
+ },
+ /**
* Overrides to handle the case where an 'ir.actions.act_window' has to be
* loaded.
*
@@ -59,9 +70,9 @@ ActionManager.include({
currentAction.type === 'ir.actions.act_window') {
// the action to load is already the current one, so update it
this._closeDialog(true); // there may be a currently opened dialog, close it
- currentAction.env.currentId = state.id;
+ var viewOptions = {currentId: state.id};
var viewType = state.view_type || currentController.viewType;
- return this._switchController(currentAction, viewType);
+ return this._switchController(currentAction, viewType, viewOptions);
} else if (!core.action_registry.contains(state.action)) {
// the action to load isn't the current one, so execute it
var context = {};
@@ -114,59 +125,6 @@ ActionManager.include({
//--------------------------------------------------------------------------
/**
- * Instantiates a search view for a given action, and starts it so that it
- * is ready to be appended to the DOM.
- *
- * @private
- * @param {Object} action
- * @returns {Deferred} resolved with the search view when it is ready
- */
- _createSearchView: function (action) {
- // if requested, keep the searchview of the current action instead of
- // creating a new one
- if (action._keepSearchView) {
- var currentAction = this.getCurrentAction();
- if (currentAction) {
- action.searchView = currentAction.searchView;
- action.env = currentAction.env; // make those actions share the same env
- return $.when(currentAction.searchView);
- } else {
- // there is not searchview to keep, so reset the flag to false
- // to ensure that the one that will be created will be correctly
- // destroyed
- action._keepSearchView = false;
- }
- }
-
- // AAB: temporarily create a dataset, until the SearchView is refactored
- // and stops using it
- var dataset = new data.DataSetSearch(this, action.res_model, action.context, action.domain);
- if (action.res_id) {
- dataset.ids.push(action.res_id);
- dataset.index = 0;
- }
-
- // find 'search_default_*' keys in actions's context
- var searchDefaults = {};
- _.each(action.context, function (value, key) {
- var match = /^search_default_(.*)$/.exec(key);
- if (match) {
- searchDefaults[match[1]] = value;
- }
- });
- var searchView = new SearchView(this, dataset, action.searchFieldsView, {
- $buttons: $('<div>'),
- action: action,
- disable_custom_filters: action.flags.disableCustomFilters,
- search_defaults: searchDefaults,
- });
-
- return searchView.appendTo(document.createDocumentFragment()).then(function () {
- action.searchView = searchView;
- return searchView;
- });
- },
- /**
* Instantiates the controller for a given action and view type, and adds it
* to the list of controllers in the action.
*
@@ -181,6 +139,9 @@ ActionManager.include({
* @param {Object} [viewOptions] dict of options passed to the initialization
* of the controller's widget
* @param {Object} [options]
+ * @param {string} [options.controllerID=false] when the controller has
+ * previously been lazy-loaded, we want to keep its jsID when loading it
+ * @param {integer} [options.index=0] the controller's index in the stack
* @param {boolean} [options.lazy=false] set to true to differ the
* initialization of the controller's widget
* @returns {Deferred<Object>} resolved with the created controller
@@ -195,31 +156,36 @@ ActionManager.include({
return $.Deferred().reject();
}
- var controllerID = _.uniqueId('controller_');
+ options = options || {};
+ var index = options.index || 0;
+ var controllerID = options.controllerID || _.uniqueId('controller_');
var controller = {
actionID: action.jsID,
className: 'o_act_window', // used to remove the padding in dialogs
+ index: index,
jsID: controllerID,
viewType: viewType,
};
Object.defineProperty(controller, 'title', {
get: function () {
// handle the case where the widget is lazy loaded
- return controller.widget ? controller.widget.getTitle() : (action.display_name || action.name);
+ return controller.widget ?
+ controller.widget.getTitle() :
+ (action.display_name || action.name);
},
});
this.controllers[controllerID] = controller;
- if (!options || !options.lazy) {
+ if (!options.lazy) {
// build the view options from different sources
- viewOptions = _.extend({
+ var flags = action.flags || {};
+ viewOptions = _.extend({}, flags, flags[viewType], viewOptions, {
action: action,
- limit: action.limit,
- }, action.flags, action.flags[viewType], viewOptions, action.env);
- // pass the controllerID to the views as an hook for further
- // communication with trigger_up (e.g. for 'env_updated' event)
- viewOptions = _.extend(viewOptions, { controllerID: controllerID });
-
+ breadcrumbs: this._getBreadcrumbs(this.controllerStack.slice(0, index)),
+ // pass the controllerID to the views as an hook for further
+ // communication with trigger_up
+ controllerID: controllerID,
+ });
var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
var def = $.Deferred();
action.controllers[viewType] = def;
@@ -263,9 +229,6 @@ ActionManager.include({
// its reference is removed
controllerDef.reject();
});
- if (action.searchView && !action._keepSearchView) {
- action.searchView.destroy();
- }
},
/**
* Executes actions of type 'ir.actions.act_window'.
@@ -280,75 +243,64 @@ ActionManager.include({
*/
_executeWindowAction: function (action, options) {
var self = this;
-
- if (action.context.active_id || action.context.active_ids) {
- // we assume that when 'active_id' or 'active_ids' is used in the
- // context, we are in a 'related' action, so we disable the
- // searchview's default custom filters
- action.context.search_disable_custom_filters = true;
- }
- action.flags = this._generateActionFlags(action);
-
return this.dp.add(this._loadViews(action)).then(function (fieldsViews) {
var views = self._generateActionViews(action, fieldsViews);
action._views = action.views; // save the initial attribute
action.views = views;
- if (fieldsViews.search) {
- action.searchFieldsView = fieldsViews.search;
- }
- action.env = self._generateActionEnv(action, options);
+ action.controlPanelFieldsView = fieldsViews.search;
action.controllers = {};
- // select the first view to display, and optionally the main view
- // which will be lazyloaded
- var firstView = options.viewType && _.findWhere(views, {type: options.viewType});
- var mainView;
- if (firstView) {
- if (!firstView.multiRecord && views[0].multiRecord) {
- mainView = views[0];
+ // select the current view to display, and optionally the main view
+ // of the action which will be lazyloaded
+ var curView = options.viewType && _.findWhere(views, {type: options.viewType});
+ var lazyView;
+ if (curView) {
+ if (!curView.multiRecord && views[0].multiRecord) {
+ lazyView = views[0];
}
} else {
- firstView = views[0];
+ curView = views[0];
}
// use mobile-friendly view by default in mobile, if possible
if (config.device.isMobile) {
- if (!firstView.isMobileFriendly) {
- firstView = self._findMobileView(views, firstView.multiRecord) || firstView;
+ if (!curView.isMobileFriendly) {
+ curView = self._findMobileView(views, curView.multiRecord) || curView;
}
- if (mainView && !mainView.isMobileFriendly) {
- mainView = self._findMobileView(views, mainView.multiRecord) || mainView;
+ if (lazyView && !lazyView.isMobileFriendly) {
+ lazyView = self._findMobileView(views, lazyView.multiRecord) || lazyView;
}
}
- var def;
- if (action.flags.hasSearchView) {
- def = self._createSearchView(action).then(function (searchView) {
- // udpate domain, context and groupby in the env
- var searchData = searchView.build_search_data();
- _.extend(action.env, self._processSearchData(action, searchData));
- });
+ var lazyViewDef;
+ if (lazyView) {
+ // if the main view is lazy-loaded, its (lazy-loaded) controller is inserted
+ // into the controller stack (so that breadcrumbs can be correctly computed),
+ // so we force clear_breadcrumbs to false so that it won't be removed when the
+ // current controller will be inserted afterwards
+ options.clear_breadcrumbs = false;
+ // this controller being lazy-loaded, this call is actually sync
+ lazyViewDef = self._createViewController(action, lazyView.type, {}, {lazy: true})
+ .then(function (lazyLoadedController) {
+ self.controllerStack.push(lazyLoadedController.jsID);
+ });
}
- return $.when(def).then(function () {
- var defs = [];
- defs.push(self._createViewController(action, firstView.type));
- if (mainView) {
- defs.push(self._createViewController(action, mainView.type, {}, {lazy: true}));
- }
- return self.dp.add($.when.apply($, defs));
- }).then(function (controller, lazyLoadedController) {
- action.controllerID = controller.jsID;
- return self._executeAction(action, options).done(function () {
- if (lazyLoadedController) {
- // controller should be placed just before the current one
- var index = self.controllerStack.length - 1;
- self.controllerStack.splice(index, 0, lazyLoadedController.jsID);
- self.controlPanel.update({
- breadcrumbs: self._getBreadcrumbs(),
- }, {clear: false});
- }
- });
- }).fail(self._destroyWindowAction.bind(self, action));
+ return self.dp.add($.when(lazyViewDef))
+ .then(function () {
+ var viewOptions = {
+ controllerState: options.controllerState,
+ currentId: options.resID,
+ };
+ var curViewDef = self._createViewController(action, curView.type, viewOptions, {
+ index: self._getControllerStackIndex(options),
+ });
+ return self.dp.add(curViewDef);
+ })
+ .then(function (controller) {
+ action.controllerID = controller.jsID;
+ return self._executeAction(action, options);
+ })
+ .fail(self._destroyWindowAction.bind(self, action));
});
},
/**
@@ -368,49 +320,6 @@ ActionManager.include({
});
},
/**
- * Generates the initial environment of a given action, which is a dict
- * containing information like the model name, the domain, the context...
- * That information is shared between the controllers of the corresponding
- * action.
- *
- * @private
- * @param {Object} action
- * @param {Object} options
- * @param {integer} [options.resID]
- * @returns {Object}
- */
- _generateActionEnv: function (action, options) {
- var resID = options.resID || action.res_id;
- return {
- modelName: action.res_model,
- ids: resID ? [resID] : undefined,
- currentId: resID || undefined,
- domain: [],
- context: action.context || {},
- groupBy: [],
- };
- },
- /**
- * Generates the flags of a given action.
- *
- * @private
- * @param {Object} action
- * @returns {Object}
- */
- _generateActionFlags: function (action) {
- var popup = action.target === 'new';
- var inline = action.target === 'inline';
- var form = action.views[0][1] === 'form';
- return _.defaults({}, action.flags, {
- disableCustomFilters: action.context && action.context.search_disable_custom_filters,
- footerToButtons: popup,
- hasSearchView: !(popup && form) && !inline,
- hasSidebar: !popup && !inline,
- headless: (popup || inline) && form,
- mode: (popup || inline || action.target === 'fullscreen') && 'edit',
- });
- },
- /**
* Generate the description of the views of a given action. For each view,
* it generates a dict with information like the fieldsView, the view type,
* the Widget to use...
@@ -483,12 +392,14 @@ ActionManager.include({
* @returns {Deferred}
*/
_loadViews: function (action) {
+ var inDialog = action.target === 'new';
+ var inline = action.target === 'inline';
var options = {
action_id: action.id,
- toolbar: action.flags.hasSidebar,
+ toolbar: inDialog && inline,
};
var views = action.views.slice();
- if (action.flags.hasSearchView) {
+ if (!inline && !(inDialog && action.views[0][1] === 'form')) {
options.load_filters = true;
var searchviewID = action.search_view_id && action.search_view_id[0];
views.push([searchviewID || false, 'search']);
@@ -496,60 +407,6 @@ ActionManager.include({
return this.loadViews(action.res_model, action.context, views, options);
},
/**
- * Overrides to handle the 'keepSearchView' option. If set to true, the
- * search view of the current action will be re-used in the new action, i.e.
- * the environment (domain, context, groupby) will be shared between both
- * actions.
- *
- * @override
- */
- _preprocessAction: function (action, options) {
- this._super.apply(this, arguments);
- if (action.type === 'ir.actions.act_window' && options.keepSearchView) {
- action._keepSearchView = true;
- }
- },
- /**
- * Processes the search data sent by the search view.
- *
- * @private
- * @param {Object} action
- * @param {Object} searchData
- * @param {Object} [searchData.contexts=[]]
- * @param {Object} [searchData.domains=[]]
- * @param {Object} [searchData.groupbys=[]]
- * @returns {Object} an object with keys 'context', 'domain', 'groupBy'
- */
- _processSearchData: function (action, searchData) {
- var contexts = searchData.contexts;
- var domains = searchData.domains;
- var groupbys = searchData.groupbys;
- var action_context = action.context || {};
- var results = pyUtils.eval_domains_and_contexts({
- domains: [action.domain || []].concat(domains || []),
- contexts: [action_context].concat(contexts || []),
- group_by_seq: groupbys || [],
- eval_context: this.userContext,
- });
- var groupBy = results.group_by.length ?
- results.group_by :
- (action.context.group_by || []);
- groupBy = (typeof groupBy === 'string') ? [groupBy] : groupBy;
-
- if (results.error) {
- throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
- JSON.stringify(results.error)));
- }
-
- var context = _.omit(results.context, 'time_ranges');
-
- return {
- context: context,
- domain: results.domain,
- groupBy: groupBy,
- };
- },
- /**
* Overrides to handle the case of 'ir.actions.act_window' actions, i.e.
* destroys all controllers associated to the given action, and its search
* view.
@@ -598,8 +455,8 @@ ActionManager.include({
return this._super.apply(this, arguments);
},
/**
- * Handles the switch from a controller to another inside the same window
- * action.
+ * Handles the switch from a controller to another (either inside the same
+ * window action, or from a window action to another using the breadcrumbs).
*
* @private
* @param {Object} controller the controller to switch to
@@ -608,18 +465,55 @@ ActionManager.include({
*/
_switchController: function (action, viewType, viewOptions) {
var self = this;
+ var view = _.findWhere(action.views, {type: viewType});
+ if (!view) {
+ // can't switch to an unknown view
+ return $.Deferred().reject();
+ }
+ var currentController = this.getCurrentController();
+ var index;
+ if (currentController.actionID !== action.jsID) {
+ // the requested controller is from another action, so we went back
+ // to a previous action using the breadcrumbs
+ var controller = _.findWhere(this.controllers, {
+ actionID: action.jsID,
+ viewType: viewType,
+ });
+ index = _.indexOf(this.controllerStack, controller.jsID);
+ } else {
+ // the requested controller is from the same action as the current
+ // one, so we either
+ // 1) go one step back from a mono record view to a multi record
+ // one using the breadcrumbs
+ // 2) or we switched from a view to another using the view
+ // switcher
+ // 3) or we opened a record from a multi record view
+ if (view.multiRecord) {
+ // cases 1) and 2) (with multi record views): replace the first
+ // controller linked to the same action in the stack
+ index = _.findIndex(this.controllerStack, function (controllerID) {
+ return self.controllers[controllerID].actionID === action.jsID;
+ });
+ } else if (!_.findWhere(action.views, {type: currentController.viewType}).multiRecord) {
+ // case 2) (with mono record views): replace the last
+ // controller by the new one if they are from the same action
+ // and if they both are mono record
+ index = this.controllerStack.length - 1;
+ } else {
+ // case 3): insert the controller on the top of the controller
+ // stack
+ index = this.controllerStack.length;
+ }
+ }
- var newController = function () {
+ var newController = function (controllerID) {
+ var options = {
+ controllerID: controllerID,
+ index: index,
+ };
return self
- ._createViewController(action, viewType, viewOptions)
+ ._createViewController(action, viewType, viewOptions, options)
.then(function (controller) {
- // AAB: this will be moved to the Controller
- var widget = controller.widget;
- if (widget.need_control_panel) {
- // set the ControlPanel bus on the controller to allow it to
- // communicate its status
- widget.set_cp_bus(self.controlPanel.get_bus());
- }
return self._startController(controller);
});
};
@@ -637,15 +531,9 @@ ActionManager.include({
} else {
controllerDef = controllerDef.then(function (controller) {
if (!controller.widget) {
- // lazy loaded -> load it now
- return newController().done(function (newController) {
- // replace the old controller (without widget) by the new one
- var index = self.controllerStack.indexOf(controller.jsID);
- self.controllerStack[index] = newController.jsID;
- delete self.controllers[controller.jsID];
- });
+ // lazy loaded -> load it now (with same jsID)
+ return newController(controller.jsID);
} else {
- viewOptions = _.extend(viewOptions || {}, action.env);
return $.when(controller.widget.willRestore()).then(function () {
return controller.widget.reload(viewOptions).then(function () {
return controller;
@@ -656,22 +544,7 @@ ActionManager.include({
}
return this.dp.add(controllerDef).then(function (controller) {
- var view = _.findWhere(action.views, {type: viewType});
- var currentController = self.getCurrentController();
- var index;
- if (currentController.actionID !== action.jsID) {
- index = _.indexOf(self.controllerStack, controller.jsID);
- } else if (view.multiRecord) {
- // remove other controllers linked to the same action from the stack
- index = _.findIndex(self.controllerStack, function (controllerID) {
- return self.controllers[controllerID].actionID === action.jsID;
- });
- } else if (!_.findWhere(action.views, {type: currentController.viewType}).multiRecord) {
- // replace the last controller by the new one if they are from the
- // same action and if they both are mono record
- index = self.controllerStack.length - 1;
- }
- return self._pushController(controller, {index: index});
+ return self._pushController(controller);
});
},
@@ -680,18 +553,6 @@ ActionManager.include({
//--------------------------------------------------------------------------
/**
- * @private
- * @param {OdooEvent} ev
- * @param {string} ev.data.controllerID
- * @param {Object} ev.data.env
- */
- _onEnvUpdated: function (ev) {
- ev.stopPropagation();
- var controller = this.controllers[ev.data.controllerID];
- var action = this.actions[controller.actionID];
- _.extend(action.env, ev.data.env);
- },
- /**
* Handler for event 'execute_action', which is typically called when a
* button is clicked. The button may be of type 'object' (call a given
* method of a given model) or 'action' (execute a given action).
@@ -803,67 +664,6 @@ ActionManager.include({
});
},
/**
- * Handles a context request: provides to the caller the context of the
- * current controller.
- *
- * @private
- * @param {OdooEvent} ev
- * @param {function} ev.data.callback used to send the requested context
- */
- _onGetControllerContext: function (ev) {
- ev.stopPropagation();
- var currentController = this.getCurrentController();
- var context = currentController && currentController.widget.getContext();
- ev.data.callback(context || {});
- },
- /**
- * Called mainly from the control panel when the focus should be given to a
- * controller
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onNavigationMove : function (ev) {
- switch(ev.data.direction) {
- case 'down' :
- var currentController = this.getCurrentController().widget;
- currentController.giveFocus();
- ev.stopPropagation();
- break;
- }
- },
- /**
- * Handles a request to add/remove search view filters.
- *
- * @param {OdooEvent} ev
- * @param {string} ev.data.controllerID
- * @param {Array[Object]} [ev.data.newFilters]
- * @param {Array[Object]} [ev.data.filtersToRemove]
- * @param {function} ev.data.callback called with the added filters as arg
- */
- _onUpdateFilters: function (ev) {
- var controller = this.controllers[ev.data.controllerID];
- var action = this.actions[controller.actionID];
- var data = ev.data;
- var addedFilters = action.searchView.updateFilters(data.newFilters, data.filtersToRemove);
- data.callback(addedFilters);
- },
- /**
- * Called when there is a change in the search view, so the current action's
- * environment needs to be updated with the new domain, context and groupby.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSearch: function (ev) {
- ev.stopPropagation();
- // AAB: the id of the correct controller should be given in data
- var currentController = this.getCurrentController();
- var action = this.actions[currentController.actionID];
- _.extend(action.env, this._processSearchData(action, ev.data));
- currentController.widget.reload(_.extend({offset: 0}, action.env));
- },
- /**
* @private
* @param {OdooEvent} ev
* @param {string} ev.data.controllerID the id of the controller that
@@ -882,13 +682,11 @@ ActionManager.include({
// only switch to the requested view if the controller that
// triggered the request is the current controller
var action = this.actions[currentController.actionID];
- if ('res_id' in ev.data) {
- action.env.currentId = ev.data.res_id;
- }
- var options = {};
- if (viewType === 'form' && !action.env.currentId) {
- options.mode = 'edit';
- } else if (ev.data.mode) {
+ var options = {
+ controllerState: currentController.widget.exportState(),
+ currentId: ev.data.res_id,
+ };
+ if (ev.data.mode) {
options.mode = ev.data.mode;
}
this._switchController(action, viewType, options);
diff --git a/addons/web/static/src/js/chrome/action_mixin.js b/addons/web/static/src/js/chrome/action_mixin.js
new file mode 100644
index 00000000000..099244592ad
--- /dev/null
+++ b/addons/web/static/src/js/chrome/action_mixin.js
@@ -0,0 +1,182 @@
+odoo.define('web.ActionMixin', function (require) {
+"use strict";
+
+/**
+ * We define here the ActionMixin, the generic notion of action (from the point
+ * of view of the web client). In short, an action is a widget which controls
+ * the main part of the screen (everything below the navbar).
+ *
+ * More precisely, the action manager is the component that coordinates a stack
+ * of actions. Whenever the user navigates in the interface, switches views,
+ * open different menus, the action manager creates/updates/destroys special
+ * widgets which implements the ActionMixin. These actions need to answer to a
+ * standardised API, which is the reason for this mixin.
+ *
+ * In practice, most actions are view controllers (coming from an
+ * ir.action.act_window). However, some actions are 'client actions'. They
+ * also need to implement the ActionMixin for a better cooperation with the
+ * action manager.
+ *
+ * @module web.ActionMixin
+ */
+
+var core = require('web.core');
+
+var ActionMixin = {
+ template: 'Action',
+
+ /**
+ * The action mixin assumes that it is rendered with the 'Action' template.
+ * This template has a special zone ('.o_content') where the content should
+ * be added. Actions that want to automatically render a template there
+ * should define the contentTemplate key. In short, client actions should
+ * probably define a contentTemplate key, and not a template key.
+ */
+ contentTemplate: null,
+
+ /**
+ * If an action wants to use a control panel, it will be created and
+ * registered in this _controlPanel key (the widget). The way this control
+ * panel is created is up to the implementation (so, view controllers or
+ * client actions may have different needs).
+ *
+ * Note that most of the time, this key should be set by the framework, not
+ * by the code of the client action.
+ */
+ _controlPanel: null,
+
+ /**
+ * String containing the title of the client action (which may be needed to
+ * display in the breadcrumbs zone of the control panel).
+ *
+ * @see _setTitle
+ */
+ _title: '',
+
+ /**
+ * @override
+ */
+ renderElement: function () {
+ this._super.apply(this, arguments);
+ if (this.contentTemplate) {
+ var content = core.qweb.render(this.contentTemplate, {widget: this});
+ this.$('.o_content').append(content);
+ }
+ },
+ /**
+ * Called each time the action is attached into the DOM.
+ */
+ on_attach_callback: function () {},
+ /**
+ * Called each time the action is detached from the DOM.
+ */
+ on_detach_callback: function () {},
+ /**
+ * Called by the action manager when action is restored (typically, when the
+ * user clicks on the action in the breadcrumb)
+ *
+ * @returns {Deferred|undefined}
+ */
+ willRestore: function () {},
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * In some situations, we need confirmation from the controller that the
+ * current state can be destroyed without prejudice to the user. For
+ * example, if the user has edited a form, maybe we should ask him if we
+ * can discard all his changes when we switch to another action. In that
+ * case, the action manager will call this method. If the returned
+ * deferred is succesfully resolved, then we can destroy the current action,
+ * otherwise, we need to stop.
+ *
+ * @returns {Deferred} resolved if the action can be removed, rejected
+ * otherwise
+ */
+ canBeRemoved: function () {
+ return $.when();
+ },
+ /**
+ * This function is called when the current state of the action
+ * should be known. For instance, if the action is a view controller,
+ * this may be useful to reinstantiate a view in the same state.
+ * Typically the state can (and should) be encoded in a query object of
+ * the form
+ * {
+ * context: {...},
+ * groupBy: [...],
+ * domain = [...],
+ * orderedBy = [...],
+ * }
+ * where the context key can contain many information.
+ * This method is mainly called during the creation of a custom filter.
+ *
+ * @returns {Object}
+ */
+ getOwnedQueryParams: function () {
+ return {};
+ },
+ /**
+ * Returns a title that may be displayed in the breadcrumb area. For
+ * example, the name of the record (for a form view). This is actually
+ * important for the action manager: this is the way it is able to give
+ * the proper titles for other actions.
+ *
+ * @returns {string}
+ */
+ getTitle: function () {
+ return this._title;
+ },
+ /**
+ * Gives the focus to the action
+ */
+ giveFocus: function () {
+ },
+ /**
+ * Renders the buttons to append, in most cases, to the control panel (in
+ * the bottom left corner). When the action is rendered in a dialog, those
+ * buttons might be moved to the dialog's footer.
+ *
+ * @param {jQuery Node} $node
+ */
+ renderButtons: function ($node) {
+ },
+ /**
+ * This is the main method to customize the controlpanel content.
+ *
+ * @see updateContents method in ControlPanelRenderer for more info
+ *
+ * @param {Object} [status]
+ * @param {string} [status.title]
+ * @param {Object} [options]
+ * @param {boolean} [options.clear]
+ */
+ updateControlPanel: function (status, options) {
+ if (this._controlPanel) {
+ status = status || {};
+ status.title = status.title || this.getTitle();
+ this._controlPanel.updateContents(status, options || {});
+ }
+ },
+ // TODO: add hooks methods:
+ // - onRestoreHook (on_reverse_breadcrumbs)
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} title
+ */
+ _setTitle: function (title) {
+ this._title = title;
+ this.updateControlPanel({title: this._title}, {clear: false});
+ },
+};
+
+return ActionMixin;
+
+});
diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js
deleted file mode 100644
index 5d0601820d0..00000000000
--- a/addons/web/static/src/js/chrome/control_panel.js
+++ /dev/null
@@ -1,286 +0,0 @@
-odoo.define('web.ControlPanelMixin', function (require) {
-"use strict";
-
-/**
- * Mixin allowing widgets to communicate with the ControlPanel. Widgets needing a
- * ControlPanel should use this mixin and call update_control_panel(cp_status) where
- * cp_status contains information for the ControlPanel to update itself.
- *
- * Note that the API is slightly awkward. Hopefully we will improve this when
- * we get the time to refactor the control panel.
- *
- * For example, here is what a typical client action would need to do to add
- * support for a control panel with some buttons::
- *
- * var ControlPanelMixin = require('web.ControlPanelMixin');
- *
- * var SomeClientAction = Widget.extend(ControlPanelMixin, {
- * ...
- * start: function () {
- * this._renderButtons();
- * this._updateControlPanel();
- * ...
- * },
- * do_show: function () {
- * ...
- * this._updateControlPanel();
- * },
- * _renderButtons: function () {
- * this.$buttons = $(QWeb.render('SomeTemplate.Buttons'));
- * this.$buttons.on('click', ...);
- * },
- * _updateControlPanel: function () {
- * this.update_control_panel({
- * cp_content: {
- * $buttons: this.$buttons,
- * },
- * });
- */
-var ControlPanelMixin = {
- need_control_panel: true,
- /**
- * @param {web.Bus} [cp_bus] Bus to communicate with the ControlPanel
- */
- set_cp_bus: function(cp_bus) {
- this.cp_bus = cp_bus;
- },
- /**
- * Triggers 'update' on the cp_bus to update the ControlPanel according to cp_status
- * @param {Object} [cp_status] see web.ControlPanel.update() for a description
- * @param {Object} [options] see web.ControlPanel.update() for a description
- */
- update_control_panel: function(cp_status, options) {
- if (this.cp_bus) {
- this.cp_bus.trigger("update", cp_status || {}, options || {});
- }
- },
-};
-
-return ControlPanelMixin;
-
-});
-
-odoo.define('web.ControlPanel', function (require) {
-"use strict";
-
-var Bus = require('web.Bus');
-var data = require('web.data');
-var Widget = require('web.Widget');
-
-var ControlPanel = Widget.extend({
- template: 'ControlPanel',
- /**
- * @param {String} [template] the QWeb template to render the ControlPanel.
- * By default, the template 'ControlPanel' will be used
- */
- init: function(parent, template) {
- this._super(parent);
- if (template) {
- this.template = template;
- }
-
- this.bus = new Bus(this);
- // the updateIndex is used to prevent concurrent updates of the control
- // panel depending on asynchronous code to be executed in the wrong order
- this.bus.updateIndex = 0;
- this.bus.on("update", this, this.update);
- },
- /**
- * Renders the control panel and creates a dictionnary of its exposed elements
- * @return {jQuery.Deferred}
- */
- start: function() {
- // Exposed jQuery nodesets
- this.nodes = {
- $buttons: this.$('.o_cp_buttons'),
- $pager: this.$('.o_cp_pager'),
- $searchview: this.$('.o_cp_searchview'),
- $searchview_buttons: this.$('.o_search_options'),
- $sidebar: this.$('.o_cp_sidebar'),
- $switch_buttons: this.$('.o_cp_switch_buttons'),
- };
-
- // Prevent the search dropdowns to close when clicking inside them
- this.$el.on('click.bs.dropdown', '.o_search_options .dropdown-menu', function (e) {
- e.stopPropagation();
- });
-
- // By default, hide the ControlPanel and remove its contents from the DOM
- this._toggle_visibility(false);
-
- return this._super();
- },
- /**
- * @return {Object} the Bus the ControlPanel is listening on
- */
- get_bus: function() {
- return this.bus;
- },
- /**
- * Updates the content and displays the ControlPanel
- * @param {Object} [status.active_view] the current active view
- * @param {Array} [status.breadcrumbs] the breadcrumbs to display (see _render_breadcrumbs() for
- * precise description)
- * @param {Object} [status.cp_content] dictionnary containing the new ControlPanel jQuery elements
- * @param {Boolean} [status.hidden] true if the ControlPanel should be hidden
- * @param {openerp.web.SearchView} [status.searchview] the searchview widget
- * @param {Boolean} [status.search_view_hidden] true if the searchview is hidden, false otherwise
- * @param {Boolean} [options.clear] set to true to clear from control panel
- * elements that are not in status.cp_content
- */
- update: function(status, options) {
- this.bus.updateIndex++;
-
- this._toggle_visibility(!status.hidden);
-
- // Don't update the ControlPanel in headless mode as the views have
- // inserted themselves the buttons where they want, so inserting them
- // again in the ControlPanel will remove them from where they should be
- if (!status.hidden) {
- options = _.defaults({}, options, {
- clear: true, // clear control panel by default
- });
- var new_cp_content = status.cp_content || {};
-
- // Render the breadcrumbs
- if (status.breadcrumbs) {
- this.$('.breadcrumb').html(this._render_breadcrumbs(status.breadcrumbs));
- }
-
- // Detach control_panel old content and attach new elements
- var toDetach = this.nodes;
- if (status.searchview && this.searchview === status.searchview) {
- // If the searchview is the same as before, don't detach it s.t.
- // we don't loose any floating content, nor the focus
- toDetach = _.omit(toDetach, '$searchview');
- new_cp_content = _.omit(new_cp_content, '$searchview');
- }
- if (options.clear) {
- this._detach_content(toDetach);
- // Show the searchview buttons area, which might have been hidden by
- // the searchview, as client actions may insert elements into it
- this.nodes.$searchview_buttons.show();
- } else {
- this._detach_content(_.pick(toDetach, _.keys(new_cp_content)));
- }
- this._attach_content(new_cp_content);
- if (options.clear || status.searchview) {
- this.searchview = status.searchview;
- }
-
- // Update the searchview and switch buttons
- if (status.searchview || options.clear) {
- this._update_search_view(status.searchview, status.search_view_hidden, status.groupable, status.enableTimeRangeMenu);
- }
- if (status.active_view_selector) {
- this._update_switch_buttons(status.active_view_selector);
- }
- }
- },
- /**
- * Private function that hides (or shows) the ControlPanel in headless (resp. non-headless) mode
- * Also detaches or attaches its contents to clean the DOM
- * @param {Boolean} [visible] true to show the control panel, false to hide it
- */
- _toggle_visibility: function(visible) {
- this.do_toggle(visible);
- if (!visible && !this.$content) {
- this.$content = this.$el.contents().detach();
- } else if (this.$content) {
- this.$content.appendTo(this.$el);
- this.$content = null;
- }
- },
- /**
- * Private function that detaches the content of the ControlPanel
- * @param {Object} [elements_to_detach] subset of this.nodes to detach
- */
- _detach_content: function(elements_to_detach) {
- _.each(elements_to_detach, function($nodeset) {
- $nodeset.contents().detach();
- });
- },
- /**
- * Private function that attaches content to the ControlPanel
- * @param {Object} [content] dictionnary of jQuery elements to attach, whose keys
- * are jQuery nodes identifiers in this.nodes
- */
- _attach_content: function(content) {
- var self = this;
- _.each(content, function($nodeset, $element) {
- if ($nodeset && self.nodes[$element]) {
- self.nodes[$element].append($nodeset);
- }
- });
- },
- /**
- * Private function that removes active class on all switch-buttons and adds
- * it to the one of the active view
- * @param {Object} [active_view_selector] the selector of the div to activate
- */
- _update_switch_buttons: function(active_view_selector) {
- _.each(this.nodes.$switch_buttons.find('button'), function(button) {
- $(button).removeClass('active');
- });
- this.$(active_view_selector).addClass('active');
- },
- /**
- * Private function that renders the breadcrumbs
- * @param {Array} [breadcrumbs] list of objects containing the following keys:
- * - action: the action to execute when clicking on this part of the breadcrumbs
- * - index: the index in the breadcrumbs (starting at 0)
- * - title: what to display in the breadcrumbs
- * @return {Array} list of breadcrumbs' li jQuery elements
- */
- _render_breadcrumbs: function (breadcrumbs) {
- var self = this;
- return breadcrumbs.map(function (bc, index) {
- return self._render_breadcrumbs_li(bc, index, breadcrumbs.length);
- });
- },
- /**
- * Private function that renders a breadcrumbs' li Jquery element
- */
- _render_breadcrumbs_li: function (bc, index, length) {
- var self = this;
- var is_last = (index === length-1);
- var li_content = bc.title && _.escape(bc.title.trim()) || data.noDisplayContent;
- var $bc = $('<li>', {class: 'breadcrumb-item'})
- .append(is_last ? li_content : $('<a>', {href: '#'}).html(li_content))
- .toggleClass('active', is_last);
- if (!is_last) {
- $bc.click(function (ev) {
- ev.preventDefault();
- self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
- });
- }
- return $bc;
- },
- /**
- * Private function that updates the SearchView's visibility and extend the
- * breadcrumbs area if the SearchView is not visible
- *
- * @private
- * @param {SearchView} [searchview] the searchview Widget
- * @param {boolean} [isHidden] visibility of the searchview
- * @param {boolean} [groupable] visibility of the groupable menu (only
- * relevant if searchview is visible)
- */
- _update_search_view: function (searchview, isHidden, groupable, enableTimeRangeMenu) {
- if (searchview) {
- searchview.toggle_visibility(!isHidden);
- if (groupable !== undefined){
- searchview.groupby_menu.do_toggle(groupable);
- }
- if (enableTimeRangeMenu !== undefined){
- searchview.displayTimeRangeMenu(enableTimeRangeMenu);
- }
- }
- this.nodes.$searchview.toggle(!isHidden);
- this.$el.toggleClass('o_breadcrumb_full', !!isHidden);
- },
-});
-
-return ControlPanel;
-
-});