summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/chrome/action_manager_act_window.js
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/action_manager_act_window.js
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/action_manager_act_window.js')
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js484
1 files changed, 141 insertions, 343 deletions
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);