PageRenderTime 68ms CodeModel.GetById 7ms RepoModel.GetById 0ms app.codeStats 0ms

/addons/web/static/src/js/chrome/view_manager.js

https://bitbucket.org/testbenchtech/odoo
JavaScript | 667 lines | 533 code | 28 blank | 106 comment | 114 complexity | f7062c8de6dcda7c4d84b957d80a894e MD5 | raw file
  1. odoo.define('web.ViewManager', function (require) {
  2. "use strict";
  3. var Context = require('web.Context');
  4. var ControlPanelMixin = require('web.ControlPanelMixin');
  5. var core = require('web.core');
  6. var data = require('web.data');
  7. var data_manager = require('web.data_manager');
  8. var dom = require('web.dom');
  9. var pyeval = require('web.pyeval');
  10. var SearchView = require('web.SearchView');
  11. var view_registry = require('web.view_registry');
  12. var Widget = require('web.Widget');
  13. var QWeb = core.qweb;
  14. var _t = core._t;
  15. var ViewManager = Widget.extend(ControlPanelMixin, {
  16. className: "o_view_manager_content",
  17. custom_events: {
  18. execute_action: function(event) {
  19. var data = event.data;
  20. this.do_execute_action(data.action_data, data.env, data.on_closed)
  21. .then(data.on_success, data.on_fail);
  22. },
  23. search: function(event) {
  24. var d = event.data;
  25. _.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
  26. this.active_view.controller.reload(_.extend({offset: 0}, this.env));
  27. },
  28. switch_view: function(event) {
  29. if ('res_id' in event.data) {
  30. this.env.currentId = event.data.res_id;
  31. }
  32. var options = {};
  33. if (event.data.view_type === 'form' && !this.env.currentId) {
  34. options.mode = 'edit';
  35. } else if (event.data.mode) {
  36. options.mode = event.data.mode;
  37. }
  38. this.switch_mode(event.data.view_type, options);
  39. },
  40. env_updated: function(event) {
  41. _.extend(this.env, event.data);
  42. },
  43. push_state: function(event) {
  44. this.do_push_state(event.data);
  45. },
  46. get_controller_context: '_onGetControllerContext',
  47. switch_to_previous_view: '_onSwitchToPreviousView',
  48. },
  49. /**
  50. * Called each time the view manager is attached into the DOM
  51. */
  52. on_attach_callback: function() {
  53. this.is_in_DOM = true;
  54. var controller = this.active_view && this.active_view.controller;
  55. if (controller && this.active_view.controller.on_attach_callback) {
  56. this.active_view.controller.on_attach_callback();
  57. }
  58. },
  59. /**
  60. * Called each time the view manager is detached from the DOM
  61. */
  62. on_detach_callback: function() {
  63. this.is_in_DOM = false;
  64. var controller = this.active_view && this.active_view.controller;
  65. if (controller && controller.on_detach_callback) {
  66. this.active_view.controller.on_detach_callback();
  67. }
  68. },
  69. /**
  70. * @param {Object} [dataset]
  71. * @param {Array} [views] List of [view_id, view_type[, fields_view]]
  72. * @param {Object} [flags] various boolean describing UI state
  73. */
  74. init: function(parent, dataset, views, flags, options) {
  75. var self = this;
  76. this._super(parent);
  77. this.action = options && options.action || {};
  78. this.action_manager = options && options.action_manager;
  79. this.flags = flags || {};
  80. this.dataset = dataset;
  81. this.view_order = [];
  82. this.views = {};
  83. this.view_stack = []; // used for breadcrumbs
  84. this.active_view = null;
  85. this.registry = view_registry;
  86. this.title = this.action.name;
  87. _.each(views, function (view) {
  88. var view_type = view[1] || view.view_type;
  89. var View = self.registry.get(view_type); //.prototype.config.Controller;
  90. if (!View) {
  91. console.error("View type", "'"+view_type+"'", "is not present in the view registry.");
  92. return;
  93. }
  94. var view_label = View.prototype.display_name;
  95. var view_descr = {
  96. accesskey: View.prototype.accesskey,
  97. button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
  98. controller: null,
  99. fields_view: view[2] || view.fields_view,
  100. icon: View.prototype.icon,
  101. label: view_label,
  102. mobile_friendly: View.prototype.mobile_friendly,
  103. multi_record: View.prototype.multi_record,
  104. options: _.extend({
  105. action: self.action,
  106. limit: self.action.limit,
  107. views: self.action.views,
  108. }, self.flags, self.flags[view_type], view.options),
  109. searchable: View.prototype.searchable,
  110. title: self.title,
  111. type: view_type,
  112. view_id: view[0] || view.view_id,
  113. };
  114. self.view_order.push(view_descr);
  115. self.views[view_type] = view_descr;
  116. });
  117. this.first_view = this.views[options && options.view_type]; // view to open first
  118. this.default_view = this.get_default_view();
  119. // env registers properties shared between views
  120. this.env = {
  121. modelName: this.dataset.model,
  122. ids: this.dataset.ids.length ? this.dataset.ids : undefined,
  123. currentId: this.dataset.ids.length ? this.dataset.ids[this.dataset.index] : undefined,
  124. domain: undefined,
  125. context: this.dataset.context,
  126. groupBy: undefined,
  127. };
  128. },
  129. willStart: function () {
  130. var views_def;
  131. var first_view_to_display = this.first_view || this.default_view;
  132. if (!first_view_to_display.fields_view || (this.flags.search_view && !this.search_fields_view)) {
  133. views_def = this.load_views();
  134. }
  135. return $.when(this._super(), views_def);
  136. },
  137. /**
  138. * @return {Deferred} initial view and search view (if any) loading promise
  139. */
  140. start: function() {
  141. var self = this;
  142. var _super = this._super.bind(this, arguments);
  143. var def;
  144. if (this.flags.search_view) {
  145. def = this.setup_search_view().then(function() {
  146. // udpate domain, context and groupby in the env
  147. var d = self.searchview.build_search_data();
  148. _.extend(self.env, self._process_search_data(d.domains, d.contexts, d.groupbys));
  149. });
  150. }
  151. return $.when(def).then(function() {
  152. var defs = [];
  153. defs.push(_super());
  154. if (self.flags.views_switcher) {
  155. self.render_switch_buttons();
  156. }
  157. // If a non multi-record first_view is given, switch to it but first push the default_view
  158. // to the view_stack to complete the breadcrumbs
  159. if (self.first_view && !self.first_view.multi_record && self.default_view.multi_record) {
  160. self.view_stack.push(self.default_view);
  161. }
  162. var view_to_load = self.first_view || self.default_view;
  163. var options = _.extend({}, view_to_load.options);
  164. defs.push(self.switch_mode(view_to_load.type, options));
  165. return $.when.apply($, defs);
  166. }).then(function() {
  167. if (self.flags.on_load) {
  168. self.flags.on_load(self);
  169. }
  170. core.bus.on('clear_uncommitted_changes', self, function(chain_callbacks) {
  171. chain_callbacks(function() {
  172. if (self.active_view.controller) {
  173. return self.active_view.controller.discardChanges(undefined, {
  174. readonlyIfRealDiscard: true,
  175. });
  176. }
  177. });
  178. });
  179. });
  180. },
  181. /**
  182. * Loads all missing field_views of views in this.views and the search view.
  183. *
  184. * @return {Deferred}
  185. */
  186. load_views: function () {
  187. var self = this;
  188. var views = [];
  189. _.each(this.views, function (view) {
  190. if (!view.fields_view) {
  191. views.push([view.view_id, view.type]);
  192. }
  193. });
  194. var options = {
  195. action_id: this.action.id,
  196. toolbar: this.flags.sidebar,
  197. };
  198. if (this.flags.search_view && !this.search_fields_view) {
  199. options.load_filters = true;
  200. var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
  201. views.push([searchview_id || false, 'search']);
  202. }
  203. var params = {
  204. model: this.dataset.model,
  205. context: this.dataset.get_context(),
  206. views_descr: views,
  207. };
  208. return data_manager.load_views(params, options).then(function (fields_views) {
  209. _.each(fields_views, function (fields_view, view_type) {
  210. if (view_type === 'search') {
  211. self.search_fields_view = fields_view;
  212. } else {
  213. self.views[view_type].fields_view = fields_view;
  214. }
  215. });
  216. });
  217. },
  218. /**
  219. * Returns the default view with the following fallbacks:
  220. *
  221. * - use the default_view defined in the flags, if any
  222. * - use the first view in the view_order
  223. *
  224. * @returns {Object} the default view
  225. */
  226. get_default_view: function() {
  227. return this.views[this.flags.default_view || this.view_order[0].type];
  228. },
  229. switch_mode: function(view_type, view_options) {
  230. var self = this;
  231. var view = this.views[view_type];
  232. if (!view || this.currently_switching) {
  233. return $.Deferred().reject();
  234. } else {
  235. this.currently_switching = true; // prevent overlapping switches
  236. }
  237. var old_view = this.active_view;
  238. // Ensure that the fields_view has been loaded
  239. var views_def;
  240. if (!view.fields_view) {
  241. views_def = this.load_views();
  242. }
  243. return $.when(views_def).then(function () {
  244. if (view.multi_record) {
  245. self.view_stack = [];
  246. } else if (self.view_stack.length > 0 && !(_.last(self.view_stack).multi_record)) {
  247. // Replace the last view by the new one if both are mono_record
  248. self.view_stack.pop();
  249. }
  250. self.view_stack.push(view);
  251. self.active_view = view;
  252. if (!view.loaded) {
  253. view_options = _.extend({}, view.options, view_options, self.env);
  254. view.loaded = $.Deferred();
  255. self.create_view(view, view_options).then(function(controller) {
  256. view.controller = controller;
  257. view.$fragment = $('<div>');
  258. controller.appendTo(view.$fragment).done(function() {
  259. // Remove the unnecessary outer div
  260. view.$fragment = view.$fragment.contents();
  261. view.loaded.resolve();
  262. });
  263. }).fail(view.loaded.reject.bind(view.loaded));
  264. } else {
  265. view.loaded = view.loaded.then(function() {
  266. view_options = _.extend({}, view_options, self.env);
  267. return view.controller.reload(view_options);
  268. });
  269. }
  270. return $.when(view.loaded)
  271. .then(function() {
  272. self._display_view(old_view);
  273. self.trigger('switch_mode', view_type, view_options);
  274. }).fail(function(e) {
  275. if (!(e && e.code === 200 && e.data.exception_type)) {
  276. self.do_warn(_t("Error"), view.label + _t(" view couldn't be loaded"));
  277. }
  278. // Restore internal state
  279. self.active_view = old_view;
  280. self.view_stack.pop();
  281. });
  282. }).always(function () {
  283. self.currently_switching = false;
  284. });
  285. },
  286. _display_view: function (old_view) {
  287. var view_controller = this.active_view.controller;
  288. var view_fragment = this.active_view.$fragment;
  289. // Prepare the ControlPanel content and update it
  290. var view_control_elements = this.render_view_control_elements();
  291. var cp_status = {
  292. active_view_selector: '.o_cp_switch_' + this.active_view.type,
  293. breadcrumbs: this.action_manager && this.action_manager.get_breadcrumbs(),
  294. cp_content: _.extend({}, this.searchview_elements, view_control_elements),
  295. hidden: this.flags.headless,
  296. searchview: this.searchview,
  297. search_view_hidden: !this.active_view.searchable || this.active_view.searchview_hidden,
  298. };
  299. this.update_control_panel(cp_status);
  300. // Detach the old view and store it
  301. if (old_view && old_view !== this.active_view) {
  302. // Store the scroll position
  303. if (this.action_manager && this.action_manager.webclient) {
  304. old_view.controller.setScrollTop(this.action_manager.webclient.getScrollTop());
  305. }
  306. // Do not detach ui-autocomplete elements to let jquery-ui garbage-collect them
  307. var $to_detach = this.$el.contents().not('.ui-autocomplete');
  308. old_view.$fragment = dom.detach([{widget: old_view.controller}], {$to_detach: $to_detach});
  309. }
  310. // If the user switches from a multi-record to a mono-record view,
  311. // the action manager should be scrolled to the top.
  312. if (old_view && old_view.controller.multi_record === true && view_controller.multi_record === false) {
  313. view_controller.setScrollTop(0);
  314. }
  315. // Append the view fragment to this.$el
  316. dom.append(this.$el, view_fragment, {
  317. in_DOM: this.is_in_DOM,
  318. callbacks: [{widget: view_controller}],
  319. });
  320. },
  321. create_view: function(view_descr, view_options) {
  322. var self = this;
  323. var arch = view_descr.fields_view.arch;
  324. var View = this.registry.get(arch.attrs.js_class || view_descr.type);
  325. var params = _.extend({}, view_options, {userContext: this.getSession().user_context});
  326. if (view_descr.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline' || this.action.target === 'fullscreen') ||
  327. (view_options && (view_options.mode === 'edit' || view_options.context.form_view_initial_mode)))) {
  328. params.mode = params.initial_mode || 'edit';
  329. }
  330. view_descr.searchview_hidden = View.prototype.searchview_hidden;
  331. var view = new View(view_descr.fields_view, params);
  332. return view.getController(this).then(function(controller) {
  333. controller.on('history_back', this, function() {
  334. if (self.action_manager) self.action_manager.trigger('history_back');
  335. });
  336. controller.on("change:title", this, function() {
  337. if (self.action_manager && !self.flags.headless) {
  338. var breadcrumbs = self.action_manager.get_breadcrumbs();
  339. self.update_control_panel({breadcrumbs: breadcrumbs}, {clear: false});
  340. }
  341. });
  342. return controller;
  343. });
  344. },
  345. select_view: function (index) {
  346. var viewType = this.view_stack[index].type;
  347. var viewOptions = {};
  348. if (viewType === 'form') {
  349. // reload form views in readonly, except for inline actions (i.e.
  350. // settings views) that stay in edit
  351. viewOptions.mode = this.action.target === 'inline' ? 'edit' : 'readonly';
  352. }
  353. return this.switch_mode(viewType, viewOptions);
  354. },
  355. /**
  356. * Renders the switch buttons for multi- and mono-record views and adds
  357. * listeners on them, but does not append them to the DOM
  358. * Sets switch_buttons.$mono and switch_buttons.$multi to send to the ControlPanel
  359. */
  360. render_switch_buttons: function() {
  361. var self = this;
  362. // Partition the views according to their multi-/mono-record status
  363. var views = _.partition(this.view_order, function(view) {
  364. return view.multi_record === true;
  365. });
  366. var multi_record_views = views[0];
  367. var mono_record_views = views[1];
  368. // Inner function to render and prepare switch_buttons
  369. var _render_switch_buttons = function(views) {
  370. if (views.length > 1) {
  371. var $switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: views}));
  372. // Create bootstrap tooltips
  373. _.each(views, function(view) {
  374. $switch_buttons.filter('.o_cp_switch_' + view.type).tooltip();
  375. });
  376. // Add onclick event listener
  377. $switch_buttons.filter('button').click(_.debounce(function(event) {
  378. var view_type = $(event.target).data('view-type');
  379. self.switch_mode(view_type);
  380. }, 200, true));
  381. return $switch_buttons;
  382. }
  383. };
  384. // Render switch buttons but do not append them to the DOM as this will
  385. // be done later, simultaneously to all other ControlPanel elements
  386. this.switch_buttons = {};
  387. this.switch_buttons.$multi = _render_switch_buttons(multi_record_views);
  388. this.switch_buttons.$mono = _render_switch_buttons(mono_record_views);
  389. },
  390. /**
  391. * Renders the control elements (buttons, sidebar, pager) of the current view.
  392. * Fills this.active_view.control_elements dictionnary with the rendered
  393. * elements and the adequate view switcher, to send to the ControlPanel.
  394. * Warning: it should be called before calling do_show on the view as the
  395. * sidebar is extended to listen on the load_record event triggered as soon
  396. * as do_show is done (the sidebar should thus be instantiated before).
  397. */
  398. render_view_control_elements: function() {
  399. if (!this.active_view.control_elements) {
  400. var view_controller = this.active_view.controller;
  401. var $buttons = this.flags.$buttons;
  402. var elements = {};
  403. if (!this.flags.headless) {
  404. elements = {
  405. $buttons: $("<div>"),
  406. $sidebar: $("<div>"),
  407. $pager: $("<div>"),
  408. };
  409. }
  410. view_controller.renderButtons($buttons ? $buttons.empty() : elements.$buttons);
  411. view_controller.renderSidebar(elements.$sidebar);
  412. view_controller.renderPager(elements.$pager);
  413. // Remove the unnecessary outer div
  414. elements = _.mapObject(elements, function($node) {
  415. return $node && $node.contents();
  416. });
  417. // Use the adequate view switcher (mono- or multi-record)
  418. if (this.switch_buttons) {
  419. if (this.active_view.multi_record) {
  420. elements.$switch_buttons = this.switch_buttons.$multi;
  421. } else {
  422. elements.$switch_buttons = this.switch_buttons.$mono;
  423. }
  424. }
  425. // Store the rendered elements in the active_view to allow restoring them later
  426. this.active_view.control_elements = elements;
  427. }
  428. return this.active_view.control_elements;
  429. },
  430. /**
  431. * Sets up the current viewmanager's search view.
  432. * Sets $searchview and $searchview_buttons in searchview_elements to send to the ControlPanel
  433. */
  434. setup_search_view: function() {
  435. var self = this;
  436. var search_defaults = {};
  437. var context = this.action.context || [];
  438. _.each(context, function (value, key) {
  439. var match = /^search_default_(.*)$/.exec(key);
  440. if (match) {
  441. search_defaults[match[1]] = value;
  442. }
  443. });
  444. var options = {
  445. hidden: this.flags.search_view === false,
  446. disable_custom_filters: this.flags.search_disable_custom_filters,
  447. $buttons: $("<div>"),
  448. action: this.action,
  449. search_defaults: search_defaults,
  450. };
  451. // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will
  452. // be done later, simultaneously to all other ControlPanel elements
  453. this.searchview = new SearchView(this, this.dataset, this.search_fields_view, options);
  454. return $.when(this.searchview.appendTo($("<div>"))).done(function() {
  455. self.searchview_elements = {};
  456. self.searchview_elements.$searchview = self.searchview.$el;
  457. self.searchview_elements.$searchview_buttons = self.searchview.$buttons.contents();
  458. });
  459. },
  460. _process_search_data: function(domains, contexts, groupbys) {
  461. // var controller = this.active_view.controller; // the correct view must be loaded here
  462. var action_context = this.action.context || {};
  463. var view_context = {}; //controller.get_context();
  464. var results = pyeval.eval_domains_and_contexts({
  465. domains: [this.action.domain || []].concat(domains || []),
  466. contexts: [action_context, view_context].concat(contexts || []),
  467. group_by_seq: groupbys || [],
  468. eval_context: this.getSession().user_context,
  469. });
  470. if (results.error) {
  471. throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
  472. JSON.stringify(results.error)));
  473. }
  474. // FORWARDPORT THIS UP TO SAAS-11.1 ONLY, NOT LATER
  475. var groupby = results.group_by.length ? results.group_by : action_context.group_by;
  476. if (_.isString(groupby)) {
  477. groupby = [groupby];
  478. }
  479. return {
  480. context: results.context,
  481. domain: results.domain,
  482. groupBy: groupby || [],
  483. };
  484. },
  485. do_push_state: function(state) {
  486. if (this.action_manager) {
  487. state.view_type = this.active_view.type;
  488. this.action_manager.do_push_state(state);
  489. }
  490. },
  491. do_load_state: function(state) {
  492. var stateChanged = false;
  493. if ('id' in state && state.id !== '' && state.id !== this.env.currentId) {
  494. this.env.currentId = state.id;
  495. stateChanged = true;
  496. }
  497. if (state.view_type && state.view_type !== this.active_view.type) {
  498. stateChanged = true;
  499. }
  500. if (stateChanged) {
  501. return this.switch_mode(state.view_type);
  502. }
  503. },
  504. /**
  505. * Fetches and executes the action identified by ``action_data``.
  506. *
  507. * @param {Object} action_data the action descriptor data
  508. * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
  509. * @param {String} [action_data.special=null] special action handlers, closes the dialog if set
  510. * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
  511. * @param {Object} [action_data.context=null] additional action context, to add to the current context
  512. * @param {string} [action_data.effect] if given, a visual effect (a
  513. * rainbowman by default) will be displayed when the action is complete,
  514. * with the string (evaluated) given as options.
  515. * @param {Object} env
  516. * @param {string} env.model the model of the record(s) triggering the action
  517. * @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
  518. * @param {integer} [env.currentID] the id of the record triggering the action
  519. * @param {Object} [env.context] a context to pass to the action
  520. * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
  521. */
  522. do_execute_action: function (action_data, env, on_closed) {
  523. var self = this;
  524. var result_handler = on_closed || function () {};
  525. var context = new Context(env.context, action_data.context || {});
  526. // OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
  527. var recordID = env.currentID || null;
  528. // response handler
  529. var handler = function (action) {
  530. // show effect if button have effect attribute
  531. // Rainbowman can be displayed from two places : from attribute on a button, or from python.
  532. // Code below handles the first case i.e 'effect' attribute on button.
  533. var effect = false;
  534. if (action_data.effect) {
  535. effect = pyeval.py_eval(action_data.effect);
  536. };
  537. if (action && action.constructor === Object) {
  538. // filter out context keys that are specific to the current action.
  539. // Wrong default_* and search_default_* values will no give the expected result
  540. // Wrong group_by values will simply fail and forbid rendering of the destination view
  541. var ncontext = new Context(
  542. _.object(_.reject(_.pairs(self.env.context), function(pair) {
  543. return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
  544. }))
  545. );
  546. ncontext.add(action_data.context || {});
  547. ncontext.add({active_model: env.model});
  548. if (recordID) {
  549. ncontext.add({
  550. active_id: recordID,
  551. active_ids: [recordID],
  552. });
  553. }
  554. ncontext.add(action.context || {});
  555. action.context = ncontext;
  556. // In case effect data is returned from python and also there is rainbow
  557. // attribute on button, priority is given to button attribute
  558. action.effect = effect || action.effect;
  559. return self.do_action(action, {
  560. on_close: result_handler,
  561. });
  562. } else {
  563. // If action doesn't return anything, but have effect
  564. // attribute on button, display rainbowman
  565. self.do_action({"type":"ir.actions.act_window_close", 'effect': effect});
  566. return result_handler();
  567. }
  568. };
  569. if (action_data.special) {
  570. return handler({"type":"ir.actions.act_window_close"});
  571. } else if (action_data.type === "object") {
  572. var args = recordID ? [[recordID]] : [env.resIDs];
  573. if (action_data.args) {
  574. try {
  575. // Warning: quotes and double quotes problem due to json and xml clash
  576. // Maybe we should force escaping in xml or do a better parse of the args array
  577. var additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
  578. args = args.concat(additional_args);
  579. } catch(e) {
  580. console.error("Could not JSON.parse arguments", action_data.args);
  581. }
  582. }
  583. args.push(context);
  584. var dataset = new data.DataSet(this, env.model, env.context);
  585. return dataset.call_button(action_data.name, args).then(handler);
  586. } else if (action_data.type === "action") {
  587. return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
  588. active_model: env.model,
  589. active_ids: env.resIDs,
  590. active_id: recordID,
  591. })).then(handler);
  592. }
  593. },
  594. destroy: function () {
  595. if (this.control_elements) {
  596. if (this.control_elements.$switch_buttons) {
  597. this.control_elements.$switch_buttons.off();
  598. }
  599. }
  600. return this._super.apply(this, arguments);
  601. },
  602. //--------------------------------------------------------------------------
  603. // Handlers
  604. //--------------------------------------------------------------------------
  605. // DO NOT FORWARDPORT THIS
  606. /**
  607. * Handles a context request: provides to the caller the context of the
  608. * active controller.
  609. *
  610. * @private
  611. * @param {OdooEvent} ev
  612. * @param {function} ev.data.callback used to send the requested context
  613. */
  614. _onGetControllerContext: function (ev) {
  615. ev.stopPropagation();
  616. var controller = this.active_view && this.active_view.controller;
  617. var context = controller && controller.getContext();
  618. ev.data.callback(context);
  619. },
  620. /**
  621. * This handler is probably called by a sub form view when the user discards
  622. * its value. The usual result of this is that we switch back to the
  623. * previous view (the first in our action stack). We do this in a really
  624. * stupid way: we trigger a 'history_back' event, and the action manager
  625. * will call this very view manager to activate the previous view.
  626. * @todo: directly switch to previous view
  627. *
  628. * @private
  629. */
  630. _onSwitchToPreviousView: function () {
  631. this.trigger_up('history_back');
  632. }
  633. });
  634. return ViewManager;
  635. });