diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/abstract_view.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js | 440 |
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; + +}); |
