summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/abstract_view.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/abstract_view.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/views/abstract_view.js')
-rw-r--r--addons/web/static/src/js/views/abstract_view.js440
1 files changed, 440 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
new file mode 100644
index 00000000..9440bf67
--- /dev/null
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -0,0 +1,440 @@
+odoo.define('web.AbstractView', function (require) {
+"use strict";
+
+/**
+ * This is the base class inherited by all (JS) views. Odoo JS views are the
+ * widgets used to display information in the main area of the web client
+ * (note: the search view is not a "JS view" in that sense).
+ *
+ * The abstract view role is to take a set of fields, an arch (the xml
+ * describing the view in db), and some params, and then, to create a
+ * controller, a renderer and a model. This is the classical MVC pattern, but
+ * the word 'view' has historical significance in Odoo code, so we replaced the
+ * V in MVC by the 'renderer' word.
+ *
+ * JS views are supposed to be used like this:
+ * 1. instantiate a view with some arch, fields and params
+ * 2. call the getController method on the view instance. This returns a
+ * controller (with a model and a renderer as sub widgets)
+ * 3. append the controller somewhere
+ *
+ * Note that once a controller has been instantiated, the view class is no
+ * longer useful (unless you want to create another controller), and will be
+ * in most case discarded.
+ */
+
+const ActionModel = require("web/static/src/js/views/action_model.js");
+var AbstractModel = require('web.AbstractModel');
+var AbstractRenderer = require('web.AbstractRenderer');
+var AbstractController = require('web.AbstractController');
+const ControlPanel = require('web.ControlPanel');
+const SearchPanel = require("web/static/src/js/views/search_panel.js");
+var mvc = require('web.mvc');
+var viewUtils = require('web.viewUtils');
+
+const { Component } = owl;
+
+var Factory = mvc.Factory;
+
+var AbstractView = Factory.extend({
+ // name displayed in view switchers
+ display_name: '',
+ // indicates whether or not the view is mobile-friendly
+ mobile_friendly: false,
+ // icon is the font-awesome icon to display in the view switcher
+ icon: 'fa-question',
+ // 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,
+ // viewType is the type of the view, like 'form', 'kanban', 'list'...
+ viewType: undefined,
+ // 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 if a search panel could be instantiated
+ withSearchPanel: true,
+ // determines the MVC components to use
+ config: _.extend({}, Factory.prototype.config, {
+ Model: AbstractModel,
+ Renderer: AbstractRenderer,
+ Controller: AbstractController,
+ ControlPanel,
+ SearchPanel,
+ }),
+
+ /**
+ * The constructor function is supposed to set 3 variables: rendererParams,
+ * controllerParams and loadParams. These values will be used to initialize
+ * the model, renderer and controllers.
+ *
+ * @constructs AbstractView
+ *
+ * @param {Object} viewInfo
+ * @param {Object|string} viewInfo.arch
+ * @param {Object} viewInfo.fields
+ * @param {Object} viewInfo.fieldsInfo
+ * @param {Object} params
+ * @param {string} [params.modelName]
+ * @param {Object} [params.action={}]
+ * @param {Object} [params.context={}]
+ * @param {string} [params.controllerID]
+ * @param {number} [params.count]
+ * @param {number} [params.currentId]
+ * @param {Object} [params.controllerState]
+ * @param {string} [params.displayName]
+ * @param {Array[]} [params.domain=[]]
+ * @param {Object[]} [params.dynamicFilters] transmitted to the
+ * ControlPanel
+ * @param {number[]} [params.ids]
+ * @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.useSampleModel]
+ * @param {boolean} [params.withControlPanel=AbstractView.prototype.withControlPanel]
+ * @param {boolean} [params.withSearchPanel=AbstractView.prototype.withSearchPanel]
+ */
+ init: function (viewInfo, params) {
+ this._super.apply(this, arguments);
+
+ 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
+ // the main view, but they are opened in a FormViewDialog which
+ // instantiates another FormView (unlike kanban or list subviews for
+ // which only a Renderer is instantiated)
+ if (typeof viewInfo.arch === 'string') {
+ this.fieldsView = this._processFieldsView(viewInfo);
+ } else {
+ this.fieldsView = viewInfo;
+ }
+ this.arch = this.fieldsView.arch;
+ this.fields = this.fieldsView.viewFields;
+ this.userContext = params.userContext || {};
+
+ // 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;
+
+ // The noContentHelper's message can be empty, i.e. either a real empty string
+ // or an empty html tag. In both cases, we consider the helper empty.
+ var help = params.noContentHelp || "";
+ var htmlHelp = document.createElement("div");
+ htmlHelp.innerHTML = help;
+ this.rendererParams = {
+ arch: this.arch,
+ isEmbedded: isEmbedded,
+ noContentHelp: htmlHelp.innerText.trim() ? help : "",
+ };
+
+ this.controllerParams = {
+ 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,
+ },
+ bannerRoute: this.arch.attrs.banner_route,
+ controllerID: params.controllerID,
+ displayName: params.displayName,
+ isEmbedded: isEmbedded,
+ modelName: params.modelName,
+ viewType: this.viewType,
+ };
+
+ 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,
+ modelName: params.modelName,
+ res_id: currentId,
+ res_ids: controllerState.resIds || params.ids || (currentId ? [currentId] : undefined),
+ };
+
+ const useSampleModel = 'useSampleModel' in params ?
+ params.useSampleModel :
+ !!(this.arch.attrs.sample && JSON.parse(this.arch.attrs.sample));
+
+ this.modelParams = {
+ fields: this.fields,
+ modelName: params.modelName,
+ useSampleModel,
+ };
+ if (useSampleModel) {
+ this.modelParams.SampleModel = this.config.Model;
+ }
+
+ var defaultOrder = this.arch.attrs.default_order;
+ if (defaultOrder) {
+ this.loadParams.orderedBy = _.map(defaultOrder.split(','), function (order) {
+ order = order.trim().split(' ');
+ return {name: order[0], asc: order[1] !== 'desc'};
+ });
+ }
+ if (params.searchQuery) {
+ this._updateMVCParams(params.searchQuery);
+ }
+
+ this.withControlPanel = this.withControlPanel && params.withControlPanel;
+ this.withSearchPanel = this.withSearchPanel &&
+ this.multi_record && params.withSearchPanel &&
+ !('search_panel' in params.context && !params.search_panel);
+
+ const searchModelParams = Object.assign({}, params, { action });
+ if (this.withControlPanel || this.withSearchPanel) {
+ const { arch, fields, favoriteFilters } = params.controlPanelFieldsView || {};
+ const archInfo = ActionModel.extractArchInfo({ search: arch }, this.viewType);
+ const controlPanelInfo = archInfo[this.config.ControlPanel.modelExtension];
+ const searchPanelInfo = archInfo[this.config.SearchPanel.modelExtension];
+ this.withSearchPanel = this.withSearchPanel && Boolean(searchPanelInfo);
+ Object.assign(searchModelParams, {
+ fields,
+ favoriteFilters,
+ controlPanelInfo,
+ searchPanelInfo,
+ });
+ }
+ const searchModel = this._createSearchModel(searchModelParams);
+ this.controllerParams.searchModel = searchModel;
+ if (this.controllerParams.controlPanel) {
+ this.controllerParams.controlPanel.props.searchModel = searchModel;
+ }
+ if (this.controllerParams.searchPanel) {
+ this.controllerParams.searchPanel.props.searchModel = searchModel;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @param {Object} params
+ * @param {Object} extraExtensions
+ * @returns {ActionModel}
+ */
+ _createSearchModel: function (params, extraExtensions) {
+ // Search model + common config
+ const { fields, favoriteFilters, controlPanelInfo, searchPanelInfo } = params;
+ const extensions = Object.assign({}, extraExtensions);
+ const importedState = params.controllerState || {};
+
+ // Control panel params
+ if (this.withControlPanel) {
+ // Control panel (Model)
+ const ControlPanelComponent = this.config.ControlPanel;
+ extensions[ControlPanelComponent.modelExtension] = {
+ actionId: params.action.id,
+ // control initialization
+ activateDefaultFavorite: params.activateDefaultFavorite,
+ archNodes: controlPanelInfo.children,
+ dynamicFilters: params.dynamicFilters,
+ favoriteFilters,
+ withSearchBar: params.withSearchBar,
+ };
+ this.controllerParams.withControlPanel = true;
+ // Control panel (Component)
+ const controlPanelProps = {
+ action: params.action,
+ breadcrumbs: params.breadcrumbs,
+ fields,
+ searchMenuTypes: params.searchMenuTypes,
+ view: this.fieldsView,
+ views: params.action.views && params.action.views.filter(
+ v => v.multiRecord === this.multi_record
+ ),
+ withBreadcrumbs: params.withBreadcrumbs,
+ withSearchBar: params.withSearchBar,
+ };
+ this.controllerParams.controlPanel = {
+ Component: ControlPanelComponent,
+ props: controlPanelProps,
+ };
+ }
+
+ // Search panel params
+ if (this.withSearchPanel) {
+ // Search panel (Model)
+ const SearchPanelComponent = this.config.SearchPanel;
+ extensions[SearchPanelComponent.modelExtension] = {
+ archNodes: searchPanelInfo.children,
+ };
+ this.controllerParams.withSearchPanel = true;
+ this.rendererParams.withSearchPanel = true;
+ // Search panel (Component)
+ const searchPanelProps = {
+ importedState: importedState.searchPanel,
+ };
+ if (searchPanelInfo.attrs.class) {
+ searchPanelProps.className = searchPanelInfo.attrs.class;
+ }
+ this.controllerParams.searchPanel = {
+ Component: SearchPanelComponent,
+ props: searchPanelProps,
+ };
+ }
+
+ const searchModel = new ActionModel(extensions, {
+ env: Component.env,
+ modelName: params.modelName,
+ context: Object.assign({}, this.loadParams.context),
+ domain: this.loadParams.domain || [],
+ importedState: importedState.searchModel,
+ searchMenuTypes: params.searchMenuTypes,
+ searchQuery: params.searchQuery,
+ fields,
+ });
+
+ return searchModel;
+ },
+
+ /**
+ * @override
+ */
+ getController: async function () {
+ const _super = this._super.bind(this);
+ const { searchModel } = this.controllerParams;
+ await searchModel.load();
+ this._updateMVCParams(searchModel.get("query"));
+ // get the parent of the model if it already exists, as _super will
+ // set the new controller as parent, which we don't want
+ const modelParent = this.model && this.model.getParent();
+ const [controller] = await Promise.all([
+ _super(...arguments),
+ searchModel.isReady(),
+ ]);
+ if (modelParent) {
+ // if we already add a model, restore its parent
+ this.model.setParent(modelParent);
+ }
+ return controller;
+ },
+ /**
+ * Ensures that only one instance of AbstractModel is created
+ *
+ * @override
+ */
+ getModel: function () {
+ if (!this.model) {
+ this.model = this._super.apply(this, arguments);
+ }
+ return this.model;
+ },
+ /**
+ * This is useful to customize the actual class to use before calling
+ * createView.
+ *
+ * @param {Controller} Controller
+ */
+ setController: function (Controller) {
+ this.Controller = Controller;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @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]
+ * @param {boolean} [action.useSampleModel]
+ * @returns {Object}
+ */
+ _extractParamsFromAction: function (action) {
+ action = action || {};
+ var context = action.context || {};
+ var inline = action.target === 'inline';
+ const params = {
+ 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,
+ withSearchPanel: this.withSearchPanel,
+ };
+ if ('useSampleModel' in action) {
+ params.useSampleModel = action.useSampleModel;
+ }
+ return params;
+ },
+ /**
+ * Processes a fieldsView. In particular, parses its arch.
+ *
+ * @private
+ * @param {Object} fieldsView
+ * @param {string} fieldsView.arch
+ * @returns {Object} the processed fieldsView
+ */
+ _processFieldsView: function (fieldsView) {
+ var fv = _.extend({}, fieldsView);
+ 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.timeRanges]
+ * @param {Array[]} searchQuery.domain
+ * @param {string[]} searchQuery.groupBy
+ */
+ _updateMVCParams: function (searchQuery) {
+ this.loadParams = _.extend(this.loadParams, {
+ context: searchQuery.context,
+ domain: searchQuery.domain,
+ groupedBy: searchQuery.groupBy,
+ });
+ this.loadParams.orderedBy = Array.isArray(searchQuery.orderedBy) && searchQuery.orderedBy.length ?
+ searchQuery.orderedBy :
+ this.loadParams.orderedBy;
+ if (searchQuery.timeRanges) {
+ this.loadParams.timeRanges = searchQuery.timeRanges;
+ this.rendererParams.timeRanges = searchQuery.timeRanges;
+ }
+ },
+});
+
+return AbstractView;
+
+});