summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/abstract_view.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/views/abstract_view.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/views/abstract_view.js')
-rw-r--r--addons/web/static/src/js/views/abstract_view.js336
1 files changed, 184 insertions, 152 deletions
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 024216b5619..75de2be9446 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -23,14 +23,16 @@ odoo.define('web.AbstractView', function (require) {
* in most case discarded.
*/
-var ajax = require('web.ajax');
-var Class = require('web.Class');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
-var utils = require('web.utils');
+var ControlPanelView = require('web.ControlPanelView');
+var mvc = require('web.mvc');
+var viewUtils = require('web.viewUtils');
-var AbstractView = Class.extend({
+var Factory = mvc.Factory;
+
+var AbstractView = Factory.extend({
// name displayed in view switchers
display_name: '',
// indicates whether or not the view is mobile-friendly
@@ -40,22 +42,20 @@ var AbstractView = Class.extend({
// multi_record is used to distinguish views displaying a single record
// (e.g. FormView) from those that display several records (e.g. ListView)
multi_record: true,
- // determine if a search view should be displayed in the control panel and
- // allowed to interact with the view. Currently, the only not searchable
- // views are the form view and the diagram view.
- searchable: true,
// viewType is the type of the view, like 'form', 'kanban', 'list'...
viewType: undefined,
- // if searchable, this flag determines if the search view will display a
- // groupby menu or not. This is useful for the views which do not support
- // grouping data.
- groupable: true,
- enableTimeRangeMenu: false,
- config: {
+ // determines if a search bar is available
+ withSearchBar: true,
+ // determines the search menus available and their orders
+ searchMenuTypes: ['filter', 'groupBy', 'favorite'],
+ // determines if a control panel should be instantiated
+ withControlPanel: true,
+ // determines the MVC components to use
+ config: _.extend({}, Factory.prototype.config, {
Model: AbstractModel,
Renderer: AbstractRenderer,
Controller: AbstractController,
- },
+ }),
/**
* The constructor function is supposed to set 3 variables: rendererParams,
@@ -65,33 +65,32 @@ var AbstractView = Class.extend({
* @constructs AbstractView
*
* @param {Object} viewInfo
- * @param {Object} viewInfo.arch
+ * @param {Object|string} viewInfo.arch
* @param {Object} viewInfo.fields
* @param {Object} viewInfo.fieldsInfo
* @param {Object} params
- * @param {string} params.modelName The actual model name
- * @param {Object} params.context
- * @param {number} [params.count]
+ * @param {string} [params.modelName]
+ * @param {Object} [params.action={}]
+ * @param {Object} [params.context={}]
* @param {string} [params.controllerID]
- * @param {string[]} params.domain
- * @param {string[][]} params.timeRange
- * @param {string[][]} params.comparisonTimeRange
- * @param {string} params.timeRangeDescription
- * @param {string} params.comparisonTimeRangeDescription
- * @param {boolean} params.compare
- * @param {string[]} params.groupBy
+ * @param {number} [params.count]
* @param {number} [params.currentId]
- * @param {boolean} params.isEmbedded
+ * @param {string} [params.controllerState]
+ * @param {string} [params.displayName]
+ * @param {Array[]} [params.domain=[]]
* @param {number[]} [params.ids]
- * @param {boolean} [params.withControlPanel]
- * @param {boolean} [params.action.flags.headless]
- * @param {string} [params.action.display_name]
- * @param {string} [params.action.name]
- * @param {string} [params.action.help]
- * @param {string} [params.action.jsID]
- * @param {boolean} [params.action.views]
+ * @param {boolean} [params.isEmbedded=false]
+ * @param {Object} [params.searchQuery={}]
+ * @param {Object} [params.searchQuery.context={}]
+ * @param {Array[]} [params.searchQuery.domain=[]]
+ * @param {string[]} [params.searchQuery.groupBy=[]]
+ * @param {Object} [params.userContext={}]
+ * @param {boolean} [params.withControlPanel=true]
*/
init: function (viewInfo, params) {
+ var action = params.action || {};
+ params = _.defaults(params, this._extractParamsFromAction(action));
+
// in general, the fieldsView has to be processed by the View (e.g. the
// arch is a string that needs to be parsed) ; the only exception is for
// inline form views inside form views, as they are processed alongside
@@ -103,89 +102,51 @@ var AbstractView = Class.extend({
} else {
this.fieldsView = viewInfo;
}
- this.fields = this.fieldsView.viewFields;
this.arch = this.fieldsView.arch;
- // the boolean parameter 'isEmbedded' determines if the view should be considered
- // as a subview. For now this is only used by the graph controller that appends a
- // 'Group By' button beside the 'Measures' button when the graph view is embedded.
+ this.fields = this.fieldsView.viewFields;
+ this.userContext = params.userContext || {};
+ this.withControlPanel = params.withControlPanel;
+
+ // the boolean parameter 'isEmbedded' determines if the view should be
+ // considered as a subview. For now this is only used by the graph
+ // controller that appends a 'Group By' button beside the 'Measures'
+ // button when the graph view is embedded.
var isEmbedded = params.isEmbedded || false;
this.rendererParams = {
arch: this.arch,
isEmbedded: isEmbedded,
- noContentHelp: params.action && params.action.help,
+ noContentHelp: params.noContentHelp,
};
- var timeRangeMenuData = params.context.timeRangeMenuData;
- var timeRange = [];
- var comparisonTimeRange = [];
- var compare = false;
- var timeRangeDescription = "";
- var comparisonTimeRangeDescription = "";
- if (this.enableTimeRangeMenu && timeRangeMenuData) {
- timeRange = timeRangeMenuData.timeRange;
- comparisonTimeRange = timeRangeMenuData.comparisonTimeRange;
- compare = comparisonTimeRange.length > 0;
- timeRangeDescription = timeRangeMenuData.timeRangeDescription;
- comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription;
- this.rendererParams.timeRangeDescription = timeRangeDescription;
- this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
- }
-
this.controllerParams = {
- modelName: params.modelName,
+ actionViews: params.actionViews,
activeActions: {
edit: this.arch.attrs.edit ? JSON.parse(this.arch.attrs.edit) : true,
create: this.arch.attrs.create ? JSON.parse(this.arch.attrs.create) : true,
delete: this.arch.attrs.delete ? JSON.parse(this.arch.attrs.delete) : true,
duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true,
},
- groupable: this.groupable,
- enableTimeRangeMenu: this.enableTimeRangeMenu,
- isEmbedded: isEmbedded,
- controllerID: params.controllerID,
bannerRoute: this.arch.attrs.banner_route,
+ controllerID: params.controllerID,
+ displayName: params.displayName,
+ isEmbedded: isEmbedded,
+ isMultiRecord: this.multi_record,
+ modelName: params.modelName,
+ viewType: this.viewType,
};
- // AAB: these params won't be necessary as soon as the ControlPanel will
- // be instantiated by the View
- this.controllerParams.displayName = params.action && (params.action.display_name || params.action.name);
- this.controllerParams.isMultiRecord = this.multi_record;
- this.controllerParams.searchable = this.searchable;
- this.controllerParams.searchView = params.action && params.action.searchView;
- this.controllerParams.searchViewHidden = this.searchview_hidden; // AAB: use searchable instead where it is used?
- this.controllerParams.actionViews = params.action ? params.action.views : [];
- this.controllerParams.viewType = this.viewType;
- this.controllerParams.withControlPanel = true;
- if (params.action && params.action.flags) {
- this.controllerParams.withControlPanel = !params.action.flags.headless;
- } else if ('withControlPanel' in params) {
- this.controllerParams.withControlPanel = params.withControlPanel;
- }
-
- var groupBy = params.groupBy;
- if (typeof groupBy === 'string') {
- groupBy = [groupBy];
- }
+ var controllerState = params.controllerState || {};
+ var currentId = controllerState.currentId || params.currentId;
this.loadParams = {
context: params.context,
count: params.count || ((this.controllerParams.ids !== undefined) &&
this.controllerParams.ids.length) || 0,
domain: params.domain,
- timeRange: timeRange,
- timeRangeDescription: timeRangeDescription,
- comparisonTimeRange: comparisonTimeRange,
- comparisonTimeRangeDescription: comparisonTimeRangeDescription,
- compare: compare,
- groupedBy: groupBy,
modelName: params.modelName,
- res_id: params.currentId,
- res_ids: params.ids,
- orderedBy: params.context ? params.context.orderedBy : [],
+ res_id: currentId,
+ res_ids: controllerState.resIds || params.ids || (currentId ? [currentId] : undefined),
};
- if (params.modelName) {
- this.loadParams.modelName = params.modelName;
- }
// default_order is like:
// 'name,id desc'
// but we need it like:
@@ -197,8 +158,23 @@ var AbstractView = Class.extend({
return {name: order[0], asc: order[1] !== 'desc'};
});
}
+ if (params.searchQuery) {
+ this._updateMVCParams(params.searchQuery);
+ }
- this.userContext = params.userContext;
+ this.controlPanelParams = {
+ action: action,
+ activateDefaultFavorite: params.activateDefaultFavorite,
+ breadcrumbs: params.breadcrumbs,
+ context: this.loadParams.context,
+ domain: this.loadParams.domain,
+ modelName: params.modelName,
+ searchMenuTypes: params.searchMenuTypes,
+ state: controllerState.cpState,
+ viewInfo: params.controlPanelFieldsView,
+ withBreadcrumbs: params.withBreadcrumbs,
+ withSearchBar: params.withSearchBar,
+ };
},
//--------------------------------------------------------------------------
@@ -206,69 +182,48 @@ var AbstractView = Class.extend({
//--------------------------------------------------------------------------
/**
- * Main method of the view class. Create a controller, and make sure that
- * data and libraries are loaded.
- *
- * There is a unusual thing going in this method with parents: we create
- * renderer/model with parent as parent, then we have to reassign them at
- * the end to make sure that we have the proper relationships. This is
- * necessary to solve the problem that the controller need the model and the
- * renderer to be instantiated, but the model need a parent to be able to
- * load itself, and the renderer needs the data in its constructor.
- *
- * @param {Widget} parent The parent of the resulting Controller (most
- * likely an action manager)
- * @returns {Deferred} The deferred resolves to a controller
+ * @override
*/
getController: function (parent) {
var self = this;
- // check if a model already exists, as if not, one will be created and
- // we'll have to set the controller as its parent
- var alreadyHasModel = !!this.model;
- return $.when(this._loadData(parent), ajax.loadLibs(this)).then(function () {
- var state = self.model.get(arguments[0]);
- var renderer = self.getRenderer(parent, state);
- var Controller = self.Controller || self.config.Controller;
- var controllerParams = _.extend({
- initialState: state,
- }, self.controllerParams);
- var controller = new Controller(parent, self.model, renderer, controllerParams);
- renderer.setParent(controller);
-
- if (!alreadyHasModel) {
- // if we have a model, it already has a parent. Otherwise, we
- // set the controller, so the rpcs from the model actually work
- self.model.setParent(controller);
+ var def;
+ if (this.withControlPanel) {
+ def = this._createControlPanel(parent);
+ }
+ var _super = this._super.bind(this);
+ return $.when(def).then(function (controlPanel) {
+ if (controlPanel) {
+ var searchQuery = controlPanel.getSearchQuery();
+ self._updateMVCParams(searchQuery);
}
- return controller;
+ // get the parent of the model if it already exists, as _super will
+ // set the new controller as parent, which we don't want
+ var modelParent = self.model && self.model.getParent();
+ return _super(parent).done(function (controller) {
+ if (controlPanel) {
+ controlPanel.setParent(controller);
+ }
+ if (modelParent) {
+ // if we already add a model, restore its parent
+ self.model.setParent(modelParent);
+ }
+ });
+
});
},
/**
- * Returns the view model or create an instance of it if none
+ * Ensures that only one instance of AbstractModel is created
*
- * @param {Widget} parent the parent of the model, if it has to be created
- * @return {Object} instance of the view model
+ * @override
*/
- getModel: function (parent) {
+ getModel: function () {
if (!this.model) {
- var Model = this.config.Model;
- this.model = new Model(parent);
+ this.model = this._super.apply(this, arguments);
}
return this.model;
},
/**
- * Returns the a new view renderer instance
- *
- * @param {Widget} parent the parent of the model, if it has to be created
- * @param {Object} state the information related to the rendered view
- * @return {Object} instance of the view renderer
- */
- getRenderer: function (parent, state) {
- var Renderer = this.config.Renderer;
- return new Renderer(parent, state, this.rendererParams);
- },
- /**
- * this is useful to customize the actual class to use before calling
+ * This is useful to customize the actual class to use before calling
* createView.
*
* @param {Controller} Controller
@@ -282,16 +237,61 @@ var AbstractView = Class.extend({
//--------------------------------------------------------------------------
/**
- * Load initial data from the model
+ * Instantiates and starts a ControlPanelController.
*
* @private
- * @param {Widget} parent the parent of the model
- * @returns {Deferred<*>} a deferred that resolves to whatever the model
- * decide to return
+ * @param {Widget} parent
+ * @returns {ControlPanelController}
*/
- _loadData: function (parent) {
- var model = this.getModel(parent);
- return model.load(this.loadParams);
+ _createControlPanel: function (parent) {
+ var self = this;
+ var controlPanelView = new ControlPanelView(this.controlPanelParams);
+ return controlPanelView.getController(parent).then(function (controlPanel) {
+ self.controllerParams.controlPanel = controlPanel;
+ return controlPanel.appendTo(document.createDocumentFragment()).then(function () {
+ return controlPanel;
+ });
+ });
+ },
+ /**
+ * @private
+ * @param {Object} [action]
+ * @param {Object} [action.context || {}]
+ * @param {boolean} [action.context.no_breadcrumbs=false]
+ * @param {integer} [action.context.active_id]
+ * @param {integer[]} [action.context.active_ids]
+ * @param {Object} [action.controlPanelFieldsView]
+ * @param {string} [action.display_name]
+ * @param {Array[]} [action.domain=[]]
+ * @param {string} [action.help]
+ * @param {integer} [action.id]
+ * @param {integer} [action.limit]
+ * @param {string} [action.name]
+ * @param {string} [action.res_model]
+ * @param {string} [action.target]
+ * @returns {Object}
+ */
+ _extractParamsFromAction: function (action) {
+ action = action || {};
+ var context = action.context || {};
+ var inline = action.target === 'inline';
+ return {
+ actionId: action.id || false,
+ actionViews: action.views || [],
+ activateDefaultFavorite: !context.active_id && !context.active_ids,
+ context: action.context || {},
+ controlPanelFieldsView: action.controlPanelFieldsView,
+ currentId: action.res_id ? action.res_id : undefined, // load returns 0
+ displayName: action.display_name || action.name,
+ domain: action.domain || [],
+ limit: action.limit,
+ modelName: action.res_model,
+ noContentHelp: action.help,
+ searchMenuTypes: inline ? [] : this.searchMenuTypes,
+ withBreadcrumbs: 'no_breadcrumbs' in context ? !context.no_breadcrumbs : true,
+ withControlPanel: this.withControlPanel,
+ withSearchBar: inline ? false : this.withSearchBar,
+ };
},
/**
* Processes a fieldsView. In particular, parses its arch.
@@ -303,12 +303,44 @@ var AbstractView = Class.extend({
*/
_processFieldsView: function (fieldsView) {
var fv = _.extend({}, fieldsView);
- var doc = $.parseXML(fv.arch).documentElement;
- var stripWhitespaces = doc.nodeName.toLowerCase() !== 'kanban';
- fv.arch = utils.xml_to_json(doc, stripWhitespaces);
+ fv.arch = viewUtils.parseArch(fv.arch);
fv.viewFields = _.defaults({}, fv.viewFields, fv.fields);
return fv;
},
+ /**
+ * Hook to update the renderer, controller and load params with the result
+ * of a search (i.e. a context, a domain and a groupBy).
+ *
+ * @private
+ * @param {Object} searchQuery
+ * @param {Object} searchQuery.context
+ * @param {Object} [searchQuery.context.timeRangeMenuData={}]
+ * @param {Array[]} [searchQuery.context.timeRangeMenuData.comparisonTimeRange=[]]
+ * @param {string} [searchQuery.context.timeRangeMenuData.comparisonTimeRangeDescription='']
+ * @param {string} [searchQuery.context.timeRangeMenuData.timeRangeDescription='']
+ * @param {Array[]} [searchQuery.context.timeRangeMenuData.timeRange=[]]
+ * @param {Array[]} searchQuery.domain
+ * @param {string[]} searchQuery.groupBy
+ */
+ _updateMVCParams: function (searchQuery) {
+ var timeRangeMenuData = searchQuery.context.timeRangeMenuData || {};
+ var comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
+ var comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription || '';
+ var timeRangeDescription = timeRangeMenuData.timeRangeDescription || '';
+ this.loadParams = _.extend(this.loadParams, {
+ compare: comparisonTimeRange.length > 0,
+ comparisonTimeRange: comparisonTimeRange,
+ comparisonTimeRangeDescription: comparisonTimeRangeDescription,
+ context: searchQuery.context,
+ domain: searchQuery.domain,
+ groupedBy: searchQuery.groupBy,
+ timeRange: timeRangeMenuData.timeRange || [],
+ timeRangeDescription: timeRangeMenuData.timeRangeDescription || '',
+ });
+ this.loadParams.orderedBy = searchQuery.orderedBy ? searchQuery.orderedBy : this.loadParams.orderedBy;
+ this.rendererParams.timeRangeDescription = timeRangeDescription;
+ this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
+ },
});
return AbstractView;