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 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',
update_filters: '_onUpdateFilters',
search: '_onSearch',
switch_view: '_onSwitchView',
navigation_move: '_onNavigationMove',
}),
//--------------------------------------------------------------------------
// 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 = {
clear_breadcrumbs: true,
pushState: false,
};
if (state.action) {
var currentController = this.getCurrentController();
var currentAction = currentController && this.actions[currentController.actionID];
if (currentAction && currentAction.id === state.action &&
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 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']],
};
} else if (state.model && state.view_type) {
// this is a window action on a multi-record view, so restore it
// from the session storage
var storedAction = this.call('session_storage', 'getItem', 'current_action');
var lastAction = JSON.parse(storedAction || '{}');
if (lastAction.res_model === state.model) {
action = lastAction;
options.viewType = state.view_type;
}
}
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) {
// 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: $('
'),
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} resolved with the created controller
*/
_createViewController: function (action, viewType, viewOptions, options) {
var self = this;
var viewDescr = _.findWhere(action.views, {type: viewType});
if (!viewDescr) {
// the requested view type isn't specified in the action (e.g.
// action with list view only, user clicks on a row in the list, it
// tries to switch to form view)
return $.Deferred().reject();
}
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 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 {
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];
},
/**
* Destroys the controllers and search view of a given action of type
* 'ir.actions.act_window'.
*
* @private
* @param {Object} action
*/
_destroyWindowAction: function (action) {
var self = this;
_.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._keepSearchView) {
action.searchView.destroy();
}
},
/**
* 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.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.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];
}
} else {
firstView = 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 (mainView && !mainView.isMobileFriendly) {
mainView = self._findMobileView(views, mainView.multiRecord) || mainView;
}
}
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 (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));
});
},
/**
* Helper function to find the first mobile-friendly view, if any.
*
* @private
* @param {Array} views an array of views
* @param {boolean} multiRecord set to true iff we search for a multiRecord
* view
* @returns {Object|undefined} a mobile-friendly view of the requested
* multiRecord type, undefined if there is no such view
*/
_findMobileView: function (views, multiRecord) {
return _.findWhere(views, {
isMobileFriendly: true,
multiRecord: multiRecord,
});
},
/**
* 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...
*
* @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 parsedXML = new DOMParser().parseFromString(fieldsView.arch, "text/xml");
var key = parsedXML.documentElement.getAttribute('js_class');
var View = view_registry.get(key || viewType);
if (View) {
views.push({
accessKey: View.prototype.accessKey || View.prototype.accesskey,
displayName: View.prototype.display_name,
fieldsView: fieldsView,
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);
},
/**
* 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.
*
* @override
* @private
*/
_removeAction: function (actionID) {
var action = this.actions[actionID];
if (action.type === 'ir.actions.act_window') {
delete this.actions[action.jsID];
this._destroyWindowAction(action);
} 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.state() === 'rejected') {
// if the controllerDef is rejected, it probably means that the js
// code or the requests made to the server crashed. In that case,
// if we reuse the same deferred, then the switch to the view is
// definitely blocked. We want to use a new controller, even though
// it is very likely that it will recrash again. At least, it will
// give more feedback to the user, and it could happen that one
// record crashes, but not another.
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 $.when(controller.widget.willRestore()).then(function () {
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; // pyUtils handles null value, not undefined
var def = $.Deferred();
// determine the action to execute according to the actionData
if (actionData.special) {
def = $.when({type: 'ir.actions.act_window_close', infos: 'special'});
} 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(pyUtils.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 = pyUtils.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);
});
},
/**
* 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 || {});
},
/**
* 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 mainly from the control panel when the focus should be given to a controller
*
* @param {OdooEvent} event
* @private
*/
_onNavigationMove : function(event) {
switch(event.data.direction) {
case 'down' :
var currentController = this.getCurrentController().widget;
currentController.giveFocus();
event.stopPropagation();
break;
}
},
/**
* 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);
}
},
});
});