summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/chrome/action_manager_act_window.js
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2017-11-17 13:14:02 +0100
committerAaron Bohy <aab@odoo.com>2018-01-16 20:49:19 +0100
commit32b8cec5361174c92c45a4975d4e02f1fb448aa2 (patch)
tree110bd6986c2da1ed5047002b7ea40c91ea9fa5ed /addons/web/static/src/js/chrome/action_manager_act_window.js
parent9bee25a9df65f657fef0f2f32a7ad823fdc7d4e1 (diff)
[REF] web,*: ActionManager/ViewManager refactoring
This rev. is the first part (out of 3) of the refactoring of the ActionManager and ViewManager layer of the webclient. The main changes are: - there is no ViewManager anymore, its work is now handled by the ActionManager itself, but isolated in a specific file ; this eases a lot of things, as the breadcrumbs handling for example. - the ActionManager code is converted to the new coding principles and guidelines ; mainly, children widgets communicate with it by triggering events up, and not by function calls anymore. - the ActionManager layer is now testable, and a lot of tests have already been written. - the code in other addons has been adapted consequently. What's coming next: - introduce the 'AbstractAction' Widget, and make client actions and view controllers inherit from it ; this widget will implement a common API that could be used uniformly by the ActionManager (e.g. restore(), canBeLeft(), renderButtons(), getTitle()...). - move the ControlPanel handling from the ActionManager to the AbstractAction.
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.js756
1 files changed, 756 insertions, 0 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
new file mode 100644
index 00000000000..a37b37fca73
--- /dev/null
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -0,0 +1,756 @@
+odoo.define('web.ActWindowActionManager', function (require) {
+"use strict";
+
+/**
+ * The purpose of this file is to add the support of Odoo actions of type
+ * 'ir.actions.act_window' to the ActionManager.
+ */
+
+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 pyeval = require('web.pyeval');
+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',
+ search: '_onSearch',
+ switch_view: '_onSwitchView',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Overrides to handle the case where an 'ir.actions.act_window' has to be
+ * loaded.
+ *
+ * @override
+ * @param {Object} state
+ * @param {integer|string} [state.action] the ID or xml ID of the action to
+ * execute
+ * @param {integer} [state.active_id]
+ * @param {string} [state.active_ids]
+ * @param {integer} [state.id]
+ * @param {integer} [state.view_id=false]
+ * @param {string} [state.view_type]
+ */
+ loadState: function (state) {
+ var action;
+ var options = {pushState: false};
+ if (state.action) {
+ var currentController = this.getCurrentController();
+ var currentAction = currentController && this.actions[currentController.actionID];
+ if (currentAction && currentAction.id === state.action) {
+ // 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 viewType = state.view_type || currentController.viewType;
+ return this._switchController(currentAction, viewType);
+ } else if (!core.action_registry.contains(state.action)) {
+ // the action to load isn't the current one, so execute it
+ var context = {};
+ if (state.active_id) {
+ context.active_id = state.active_id;
+ }
+ if (state.active_ids) {
+ // jQuery's BBQ plugin does some parsing on values that are valid integers
+ // which means that if there's only one item, it will do parseInt() on it,
+ // otherwise it will keep the comma seperated list as string
+ context.active_ids = state.active_ids.split(',').map(function (id) {
+ return parseInt(id, 10) || id;
+ });
+ } else if (state.active_id) {
+ context.active_ids = [state.active_id];
+ }
+ context.params = state;
+ action = state.action;
+ options = _.extend(options, {
+ additional_context: context,
+ resID: state.id,
+ viewType: state.view_type,
+ });
+ }
+ } else if (state.model && state.id) {
+ action = {
+ res_model: state.model,
+ res_id: state.id,
+ type: 'ir.actions.act_window',
+ views: [[state.view_id || false, 'form']],
+ };
+ }
+ if (action) {
+ return this.doAction(action, options);
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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) {
+ // 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.
+ *
+ * @private
+ * @param {Object} action
+ * @param {AbstractController[]} action.controllers the already created
+ * controllers for this action
+ * @param {Object[]} action.views the views available for the action, each
+ * one containing its fieldsView
+ * @param {Object} action.env
+ * @param {string} viewType
+ * @param {Object} [viewOptions] dict of options passed to the initialization
+ * of the controller's widget
+ * @param {Object} [options]
+ * @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
+ */
+ _createViewController: function (action, viewType, viewOptions, options) {
+ var self = this;
+ var controllerID = _.uniqueId('controller_');
+ var controller = {
+ actionID: action.jsID,
+ className: 'o_act_window', // used to remove the padding in dialogs
+ 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);
+ },
+ });
+ this.controllers[controllerID] = controller;
+
+ if (!options || !options.lazy) {
+ // build the view options from different sources
+ viewOptions = _.extend({
+ 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 });
+
+ var viewDescr = _.findWhere(action.views, {type: viewType});
+ var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
+ var def = $.Deferred();
+ action.controllers[viewType] = def;
+ view.getController(this).then(function (widget) {
+ if (def.state() === 'rejected') {
+ // the deferred has been rejected meanwhile, meaning that
+ // the action has been removed, so simply destroy the widget
+ widget.destroy();
+ } else {
+ // AAB: change this logic to stop using the properties mixin
+ widget.on("change:title", this, function () {
+ if (!action.flags.headless) {
+ var breadcrumbs = self._getBreadcrumbs();
+ self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false});
+ }
+ });
+ controller.widget = widget;
+
+ def.resolve(controller);
+ }
+ }).fail(def.reject.bind(def));
+ def.fail(function () {
+ delete self.controllers[controllerID];
+ });
+ } else {
+ action.controllers[viewType] = $.Deferred().resolve(controller);
+ }
+
+ return action.controllers[viewType];
+ },
+ /**
+ * Executes actions of type 'ir.actions.act_window'.
+ *
+ * @private
+ * @param {Object} action the description of the action to execute
+ * @param {Array} action.views list of tuples [viewID, viewType]
+ * @param {Object} options @see doAction for details
+ * @param {integer} [options.resID] the current res ID
+ * @param {string} [options.viewType] the view to open
+ * @returns {Deferred} resolved when the action is appended to the DOM
+ */
+ _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._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.controllers = {};
+
+ // select the first view to display
+ var lazyLoadFirstView = false;
+ var firstView = options.viewType && _.findWhere(views, {type: options.viewType});
+ if (firstView) {
+ if (!firstView.multiRecord && views[0].multiRecord) {
+ lazyLoadFirstView = true;
+ }
+ } else {
+ firstView = views[0];
+ }
+ if (config.device.isMobile && !firstView.isMobileFriendly) {
+ firstView = _.findWhere(action.views, {isMobileFriendly: true}) || firstView;
+ }
+
+ 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));
+ });
+ }
+ return $.when(def).then(function () {
+ var defs = [];
+ defs.push(self._createViewController(action, firstView.type));
+ if (lazyLoadFirstView) {
+ defs.push(self._createViewController(action, views[0].type, {}, {lazy: true}));
+ }
+ return $.when.apply($, defs);
+ }).then(function (controller, lazyLoadedController) {
+ action.controllerID = controller.jsID;
+ return self._executeAction(action, options).done(function () {
+ if (lazyLoadedController) {
+ self.controllerStack.unshift(lazyLoadedController.jsID);
+ self.controlPanel.update({
+ breadcrumbs: self._getBreadcrumbs(),
+ }, {clear: false});
+ }
+ });
+ });
+ });
+ },
+ /**
+ * 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 actionGroupBy = action.context.group_by || [];
+ if (typeof actionGroupBy === 'string') {
+ actionGroupBy = [actionGroupBy];
+ }
+ var resID = options.resID || action.res_id;
+ return {
+ modelName: action.res_model,
+ ids: resID ? [resID] : undefined,
+ currentId: resID || undefined,
+ domain: [],
+ context: action.context || {},
+ groupBy: actionGroupBy,
+ };
+ },
+ /**
+ * 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 = _.str.startsWith(action.view_mode, '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...
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} fieldsViews
+ * @returns {Object}
+ */
+ _generateActionViews: function (action, fieldsViews) {
+ var views = [];
+ _.each(action.views, function (view) {
+ var viewType = view[1];
+ var fieldsView = fieldsViews[viewType];
+ var View = view_registry.get(fieldsView.arch.attrs.js_class || viewType);
+ if (View) {
+ views.push({
+ accessKey: View.prototype.accessKey,
+ fieldsView: fieldsViews[viewType],
+ icon: View.prototype.icon,
+ isMobileFriendly: View.prototype.mobile_friendly,
+ multiRecord: View.prototype.multi_record,
+ type: viewType,
+ viewID: view[0],
+ Widget: View,
+ });
+ } else {
+ console.error("View type '" + viewType + "' is not present in the view registry.");
+ }
+ });
+ return views;
+ },
+ /**
+ * Overrides to add specific information for controllers from actions of
+ * type 'ir.actions.act_window', like the res_model and the view_type.
+ *
+ * @override
+ * @private
+ */
+ _getControllerState: function (controllerID) {
+ var state = this._super.apply(this, arguments);
+ var controller = this.controllers[controllerID];
+ var action = this.actions[controller.actionID];
+ if (action.type === 'ir.actions.act_window') {
+ state.model = action.res_model;
+ state.view_type = controller.viewType;
+ }
+ return state;
+ },
+ /**
+ * Overrides to handle the 'ir.actions.act_window' actions.
+ *
+ * @override
+ * @private
+ */
+ _handleAction: function (action, options) {
+ if (action.type === 'ir.actions.act_window') {
+ return this._executeWindowAction(action, options);
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Loads the fields_views and fields for the given action.
+ *
+ * @private
+ * @param {Object} action
+ * @returns {Deferred}
+ */
+ _loadViews: function (action) {
+ var options = {
+ action_id: action.id,
+ toolbar: action.flags.hasSidebar,
+ };
+ var views = action.views.slice();
+ if (action.flags.hasSearchView) {
+ options.load_filters = true;
+ var searchviewID = action.search_view_id && action.search_view_id[0];
+ views.push([searchviewID || false, 'search']);
+ }
+ return this.loadViews(action.res_model, action.context, views, options);
+ },
+ /**
+ * 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 = pyeval.eval_domains_and_contexts({
+ domains: [action.domain || []].concat(domains || []),
+ contexts: [action_context].concat(contexts || []),
+ group_by_seq: groupbys || [],
+ eval_context: this.userContext,
+ });
+ if (results.error) {
+ throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
+ JSON.stringify(results.error)));
+ }
+ return {
+ context: results.context,
+ domain: results.domain,
+ groupBy: results.group_by,
+ };
+ },
+ /**
+ * 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.
+ *
+ * @override
+ * @private
+ */
+ _removeAction: function (actionID) {
+ var self = this;
+ var action = this.actions[actionID];
+ if (action.type === 'ir.actions.act_window') {
+ delete this.actions[action.jsID];
+ _.each(action.controllers, function (controllerDef) {
+ controllerDef.then(function (controller) {
+ delete self.controllers[controller.jsID];
+ if (controller.widget) {
+ controller.widget.destroy();
+ }
+ });
+ // reject the deferred if it is not yet resolved, so that the
+ // controller is correctly destroyed as soon as it is ready, and
+ // its reference is removed
+ controllerDef.reject();
+ });
+ if (action.searchView) {
+ action.searchView.destroy();
+ }
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
+ * Overrides to handle the case where the controller to restore is from an
+ * 'ir.actions.act_window' action. In this case, only the controllers
+ * stacked over the one to restore *that are not from the same action* are
+ * destroyed.
+ * For instance, when going back to the list controller from a form
+ * controller of the same action using the breadcrumbs, the form controller
+ * isn't destroyed, as it might be reused in the future.
+ *
+ * @override
+ * @private
+ */
+ _restoreController: function (controllerID) {
+ var self = this;
+ var controller = this.controllers[controllerID];
+ var action = this.actions[controller.actionID];
+ if (action.type === 'ir.actions.act_window') {
+ return this.clearUncommittedChanges().then(function () {
+ // AAB: this will be done directly in AbstractAction's restore
+ // function
+ var def;
+ if (action.on_reverse_breadcrumb) {
+ def = action.on_reverse_breadcrumb();
+ }
+ return $.when(def).then(function () {
+ return self._switchController(action, controller.viewType);
+ });
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Handles the switch from a controller to another inside the same window
+ * action.
+ *
+ * @private
+ * @param {Object} controller the controller to switch to
+ * @param {Object} [viewOptions]
+ * @return {Deferred} resolved when the new controller is in the DOM
+ */
+ _switchController: function (action, viewType, viewOptions) {
+ var self = this;
+
+ var newController = function () {
+ return self
+ ._createViewController(action, viewType, viewOptions)
+ .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);
+ });
+ };
+
+ var controllerDef = action.controllers[viewType];
+ if (!controllerDef) {
+ controllerDef = newController();
+ } 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];
+ });
+ } else {
+ viewOptions = _.extend(viewOptions || {}, action.env);
+ return controller.widget.reload(viewOptions).then(function () {
+ return controller;
+ });
+ }
+ });
+ }
+
+ 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});
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @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).
+ * Alternatively, the button may have the attribute 'special', and in this
+ * case an 'ir.actions.act_window_close' is executed.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {Object} ev.data.action_data typically, the html attributes of the
+ * button extended with additional information like the context
+ * @param {Object} [ev.data.action_data.special=false]
+ * @param {Object} [ev.data.action_data.type] 'object' or 'action', if set
+ * @param {Object} ev.data.env
+ * @param {function} [ev.data.on_closed]
+ * @param {function} [ev.data.on_fail]
+ * @param {function} [ev.data.on_success]
+ */
+ _onExecuteAction: function (ev) {
+ ev.stopPropagation();
+ var self = this;
+ var actionData = ev.data.action_data;
+ var env = ev.data.env;
+ var context = new Context(env.context, actionData.context || {});
+ var recordID = env.currentID || null; // pyeval handles null value, not undefined
+ var def;
+
+ // determine the action to execute according to the actionData
+ if (actionData.special) {
+ def = $.when({type: 'ir.actions.act_window_close'});
+ } else if (actionData.type === 'object') {
+ // call a Python Object method, which may return an action to execute
+ var args = recordID ? [[recordID]] : [env.resIDs];
+ if (actionData.args) {
+ try {
+ // warning: quotes and double quotes problem due to json and xml clash
+ // maybe we should force escaping in xml or do a better parse of the args array
+ var additionalArgs = JSON.parse(actionData.args.replace(/'/g, '"'));
+ args = args.concat(additionalArgs);
+ } catch (e) {
+ console.error("Could not JSON.parse arguments", actionData.args);
+ }
+ }
+ args.push(context.eval());
+ def = this._rpc({
+ route: '/web/dataset/call_button',
+ params: {
+ args: args,
+ method: actionData.name,
+ model: env.model,
+ },
+ });
+ } else if (actionData.type === 'action') {
+ // execute a given action, so load it first
+ def = this._loadAction(actionData.name, _.extend(pyeval.eval('context', context), {
+ active_model: env.model,
+ active_ids: env.resIDs,
+ active_id: recordID,
+ }));
+ }
+
+ // use the DropPrevious to prevent from executing the handler if another
+ // request (doAction, switchView...) has been done meanwhile ; execute
+ // the fail handler if the 'call_button' or 'loadAction' failed but not
+ // if the request failed due to the DropPrevious,
+ def.fail(ev.data.on_fail);
+ this.dp.add(def).then(function (action) {
+ // show effect if button have effect attribute
+ // rainbowman can be displayed from two places: from attribute on a button or from python
+ // code below handles the first case i.e 'effect' attribute on button.
+ var effect = false;
+ if (actionData.effect) {
+ effect = pyeval.py_eval(actionData.effect);
+ }
+
+ if (action && action.constructor === Object) {
+ // filter out context keys that are specific to the current action, because:
+ // - wrong default_* and search_default_* values won't give the expected result
+ // - wrong group_by values will fail and forbid rendering of the destination view
+ var ctx = new Context(
+ _.object(_.reject(_.pairs(env.context), function (pair) {
+ return pair[0].match('^(?:(?:default_|search_default_|show_).+|' +
+ '.+_view_ref|group_by|group_by_no_leaf|active_id|' +
+ 'active_ids)$') !== null;
+ }))
+ );
+ ctx.add(actionData.context || {});
+ ctx.add({active_model: env.model});
+ if (recordID) {
+ ctx.add({
+ active_id: recordID,
+ active_ids: [recordID],
+ });
+ }
+ ctx.add(action.context || {});
+ action.context = ctx;
+ // in case an effect is returned from python and there is already an effect
+ // attribute on the button, the priority is given to the button attribute
+ action.effect = effect || action.effect;
+ } else {
+ // if action doesn't return anything, but there is an effect
+ // attribute on the button, display rainbowman
+ action = {
+ effect: effect,
+ type: 'ir.actions.act_window_close',
+ };
+ }
+ var options = {on_close: ev.data.on_closed};
+ return self.doAction(action, options).then(ev.data.on_success, ev.data.on_fail);
+ });
+ },
+ /**
+ * 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
+ * triggered the event
+ * @param {string} ev.data.viewType the type of view to switch to
+ * @param {integer} [ev.data.res_id] the id of the record to open (for
+ * mono-record views)
+ * @param {mode} [ev.data.mode] the mode to open, i.e. 'edit' or 'readonly'
+ * (only relevant for form views)
+ */
+ _onSwitchView: function (ev) {
+ ev.stopPropagation();
+ var viewType = ev.data.view_type;
+ var currentController = this.getCurrentController();
+ if (currentController.jsID === ev.data.controllerID) {
+ // 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) {
+ options.mode = ev.data.mode;
+ }
+ this._switchController(action, viewType, options);
+ }
+ },
+});
+
+});