PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/lrm-lujosramirez/odoo10-lrm
JavaScript | 503 lines | 387 code | 31 blank | 85 comment | 95 complexity | 45dba2929d831756497305ba5702a700 MD5 | raw file
Possible License(s): LGPL-3.0, BSD-3-Clause, WTFPL, Apache-2.0
  1. odoo.define('web.ViewManager', function (require) {
  2. "use strict";
  3. var ControlPanelMixin = require('web.ControlPanelMixin');
  4. var core = require('web.core');
  5. var data_manager = require('web.data_manager');
  6. var framework = require('web.framework');
  7. var Model = require('web.DataModel');
  8. var pyeval = require('web.pyeval');
  9. var SearchView = require('web.SearchView');
  10. var Widget = require('web.Widget');
  11. var QWeb = core.qweb;
  12. var _t = core._t;
  13. var ViewManager = Widget.extend(ControlPanelMixin, {
  14. className: "o_view_manager_content",
  15. custom_events: {
  16. get_controller_context: '_onGetControllerContext',
  17. },
  18. /**
  19. * Called each time the view manager is attached into the DOM
  20. */
  21. on_attach_callback: function() {
  22. this.is_in_DOM = true;
  23. if (this.active_view && this.active_view.controller.on_attach_callback) {
  24. this.active_view.controller.on_attach_callback();
  25. }
  26. },
  27. /**
  28. * Called each time the view manager is detached from the DOM
  29. */
  30. on_detach_callback: function() {
  31. this.is_in_DOM = false;
  32. if (this.active_view && this.active_view.controller.on_detach_callback) {
  33. this.active_view.controller.on_detach_callback();
  34. }
  35. },
  36. /**
  37. * @param {Object} [dataset]
  38. * @param {Array} [views] List of [view_id, view_type[, fields_view]]
  39. * @param {Object} [flags] various boolean describing UI state
  40. */
  41. init: function(parent, dataset, views, flags, options) {
  42. var self = this;
  43. this._super(parent);
  44. this.action = options && options.action || {};
  45. this.action_manager = options && options.action_manager;
  46. this.flags = flags || {};
  47. this.dataset = dataset;
  48. this.view_order = [];
  49. this.views = {};
  50. this.view_stack = []; // used for breadcrumbs
  51. this.active_view = null;
  52. this.registry = core.view_registry;
  53. this.title = this.action.name;
  54. _.each(views, function (view) {
  55. var view_type = view[1] || view.view_type;
  56. var View = self.registry.get(view_type);
  57. if (!View) {
  58. console.error("View type", "'"+view_type+"'", "is not present in the view registry.");
  59. return;
  60. }
  61. var view_label = View.prototype.display_name;
  62. var view_descr = {
  63. accesskey: View.prototype.accesskey,
  64. button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
  65. controller: null,
  66. fields_view: view[2] || view.fields_view,
  67. icon: View.prototype.icon,
  68. label: view_label,
  69. mobile_friendly: View.prototype.mobile_friendly,
  70. multi_record: View.prototype.multi_record,
  71. options: view.options || {},
  72. require_fields: View.prototype.require_fields,
  73. title: self.title,
  74. type: view_type,
  75. view_id: view[0] || view.view_id,
  76. };
  77. self.view_order.push(view_descr);
  78. self.views[view_type] = view_descr;
  79. });
  80. this.first_view = this.views[options && options.view_type]; // view to open first
  81. this.default_view = this.get_default_view();
  82. },
  83. willStart: function () {
  84. var views_def;
  85. var first_view_to_display = this.first_view || this.default_view;
  86. if (!first_view_to_display.fields_view || (this.flags.search_view && !this.search_fields_view)) {
  87. views_def = this.load_views(first_view_to_display.require_fields);
  88. }
  89. return $.when(this._super(), views_def);
  90. },
  91. /**
  92. * @return {Deferred} initial view and search view (if any) loading promise
  93. */
  94. start: function() {
  95. var self = this;
  96. _.each(this.views, function (view) {
  97. view.options = _.extend({
  98. action: self.action,
  99. }, self.flags, self.flags[view.type], view.options);
  100. });
  101. if (this.flags.search_view) {
  102. this.search_view_loaded = this.setup_search_view();
  103. }
  104. if (this.flags.views_switcher) {
  105. this.render_switch_buttons();
  106. }
  107. // If a non multi-record first_view is given, switch to it but first push the default_view
  108. // to the view_stack to complete the breadcrumbs
  109. if (this.first_view && !this.first_view.multi_record && this.default_view.multi_record) {
  110. this.default_view.controller = this.create_view(this.default_view);
  111. this.view_stack.push(this.default_view);
  112. }
  113. var view_to_load = this.first_view || this.default_view;
  114. var options = this.flags[view_to_load] && this.flags[view_to_load].options;
  115. var main_view_loaded = this.switch_mode(view_to_load.type, options);
  116. return $.when(this._super(), main_view_loaded, this.search_view_loaded);
  117. },
  118. /**
  119. * Loads all missing field_views of views in this.views and the search view.
  120. *
  121. * @param {Boolean} [load_fields] whether or not to load the fields as well
  122. * @return {Deferred}
  123. */
  124. load_views: function (load_fields) {
  125. var self = this;
  126. var views = [];
  127. _.each(this.views, function (view) {
  128. if (!view.fields_view) {
  129. views.push([view.view_id, view.type]);
  130. }
  131. });
  132. var options = {
  133. action_id: this.action.id,
  134. load_fields: load_fields,
  135. toolbar: this.flags.sidebar,
  136. };
  137. if (this.flags.search_view && !this.search_fields_view) {
  138. options.load_filters = true;
  139. var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
  140. views.push([searchview_id || false, 'search']);
  141. }
  142. return data_manager.load_views(this.dataset, views, options).then(function (fields_views) {
  143. _.each(fields_views, function (fields_view, view_type) {
  144. if (view_type === 'search') {
  145. self.search_fields_view = fields_view;
  146. } else {
  147. self.views[view_type].fields_view = fields_view;
  148. }
  149. });
  150. });
  151. },
  152. /**
  153. * Returns the default view with the following fallbacks:
  154. * - use the default_view defined in the flags, if any
  155. * - use the first view in the view_order
  156. *
  157. * @returns {Object} the default view
  158. */
  159. get_default_view: function() {
  160. return this.views[this.flags.default_view || this.view_order[0].type];
  161. },
  162. switch_mode: function(view_type, view_options) {
  163. var self = this;
  164. var view = this.views[view_type];
  165. var old_view = this.active_view;
  166. if (!view || this.currently_switching) {
  167. return $.Deferred().reject();
  168. } else {
  169. this.currently_switching = true; // prevent overlapping switches
  170. }
  171. // Ensure that the fields_view has been loaded
  172. var views_def;
  173. if (!view.fields_view) {
  174. views_def = this.load_views(view.require_fields);
  175. }
  176. return $.when(views_def).then(function () {
  177. if (view.multi_record) {
  178. self.view_stack = [];
  179. } else if (self.view_stack.length > 0 && !(_.last(self.view_stack).multi_record)) {
  180. // Replace the last view by the new one if both are mono_record
  181. self.view_stack.pop();
  182. }
  183. self.view_stack.push(view);
  184. self.active_view = view;
  185. if (!view.loaded) {
  186. if (!view.controller) {
  187. view.controller = self.create_view(view, view_options);
  188. }
  189. view.$fragment = $('<div>');
  190. view.loaded = view.controller.appendTo(view.$fragment).done(function () {
  191. // Remove the unnecessary outer div
  192. view.$fragment = view.$fragment.contents();
  193. self.trigger("controller_inited", view.type, view.controller);
  194. });
  195. }
  196. self.active_search = $.Deferred();
  197. // Call do_search on the searchview to compute domains, contexts and groupbys
  198. if (self.search_view_loaded &&
  199. self.flags.auto_search &&
  200. view.controller.searchable !== false) {
  201. $.when(self.search_view_loaded, view.loaded).done(function() {
  202. self.searchview.do_search();
  203. });
  204. } else {
  205. self.active_search.resolve();
  206. }
  207. return $.when(view.loaded, self.active_search, self.search_view_loaded)
  208. .then(function() {
  209. return self._display_view(view_options, old_view).then(function() {
  210. self.trigger('switch_mode', view_type, view_options);
  211. });
  212. }).fail(function(e) {
  213. if (!(e && e.code === 200 && e.data.exception_type)) {
  214. self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
  215. }
  216. // Restore internal state
  217. self.active_view = old_view;
  218. self.view_stack.pop();
  219. });
  220. }).always(function () {
  221. self.currently_switching = false;
  222. });
  223. },
  224. _display_view: function (view_options, old_view) {
  225. var self = this;
  226. var view_controller = this.active_view.controller;
  227. var view_fragment = this.active_view.$fragment;
  228. var view_control_elements = this.render_view_control_elements();
  229. // Show the view
  230. return $.when(view_controller.do_show(view_options)).done(function () {
  231. // Prepare the ControlPanel content and update it
  232. var cp_status = {
  233. active_view_selector: '.o_cp_switch_' + self.active_view.type,
  234. breadcrumbs: self.action_manager && self.action_manager.get_breadcrumbs(),
  235. cp_content: _.extend({}, self.searchview_elements, view_control_elements),
  236. hidden: self.flags.headless,
  237. searchview: self.searchview,
  238. search_view_hidden: view_controller.searchable === false || view_controller.searchview_hidden,
  239. };
  240. self.update_control_panel(cp_status);
  241. // Detach the old view and store it
  242. if (old_view && old_view !== self.active_view) {
  243. // Store the scroll position
  244. if (self.action_manager && self.action_manager.webclient) {
  245. old_view.controller.set_scrollTop(self.action_manager.webclient.get_scrollTop());
  246. }
  247. // Do not detach ui-autocomplete elements to let jquery-ui garbage-collect them
  248. var $to_detach = self.$el.contents().not('.ui-autocomplete');
  249. old_view.$fragment = framework.detach([{widget: old_view.controller}], {$to_detach: $to_detach});
  250. }
  251. // If the user switches from a multi-record to a mono-record view,
  252. // the action manager should be scrolled to the top.
  253. if (old_view && old_view.controller.multi_record === true && view_controller.multi_record === false) {
  254. view_controller.set_scrollTop(0);
  255. }
  256. // Append the view fragment to self.$el
  257. framework.append(self.$el, view_fragment, {
  258. in_DOM: self.is_in_DOM,
  259. callbacks: [{widget: view_controller}],
  260. });
  261. });
  262. },
  263. create_view: function(view, view_options) {
  264. var self = this;
  265. var js_class = view.fields_view.arch.attrs && view.fields_view.arch.attrs.js_class;
  266. var View = this.registry.get(js_class || view.type);
  267. var options = _.clone(view.options);
  268. if (view.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline') ||
  269. (view_options && view_options.mode === 'edit'))) {
  270. options.initial_mode = options.initial_mode || 'edit';
  271. }
  272. var controller = new View(this, this.dataset, view.fields_view, options);
  273. controller.on('switch_mode', this, this.switch_mode.bind(this));
  274. controller.on('history_back', this, function () {
  275. if (self.action_manager) self.action_manager.trigger('history_back');
  276. });
  277. controller.on("change:title", this, function() {
  278. if (self.action_manager && !self.flags.headless) {
  279. var breadcrumbs = self.action_manager.get_breadcrumbs();
  280. self.update_control_panel({breadcrumbs: breadcrumbs}, {clear: false});
  281. }
  282. });
  283. return controller;
  284. },
  285. select_view: function (index) {
  286. var view_type = this.view_stack[index].type;
  287. return this.switch_mode(view_type);
  288. },
  289. /**
  290. * Renders the switch buttons for multi- and mono-record views and adds
  291. * listeners on them, but does not append them to the DOM
  292. * Sets switch_buttons.$mono and switch_buttons.$multi to send to the ControlPanel
  293. */
  294. render_switch_buttons: function() {
  295. var self = this;
  296. // Partition the views according to their multi-/mono-record status
  297. var views = _.partition(this.view_order, function(view) {
  298. return view.multi_record === true;
  299. });
  300. var multi_record_views = views[0];
  301. var mono_record_views = views[1];
  302. // Inner function to render and prepare switch_buttons
  303. var _render_switch_buttons = function(views) {
  304. if (views.length > 1) {
  305. var $switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: views}));
  306. // Create bootstrap tooltips
  307. _.each(views, function(view) {
  308. $switch_buttons.filter('.o_cp_switch_' + view.type).tooltip();
  309. });
  310. // Add onclick event listener
  311. $switch_buttons.filter('button').click(_.debounce(function(event) {
  312. var view_type = $(event.target).data('view-type');
  313. self.switch_mode(view_type);
  314. }, 200, true));
  315. return $switch_buttons;
  316. }
  317. };
  318. // Render switch buttons but do not append them to the DOM as this will
  319. // be done later, simultaneously to all other ControlPanel elements
  320. this.switch_buttons = {};
  321. this.switch_buttons.$multi = _render_switch_buttons(multi_record_views);
  322. this.switch_buttons.$mono = _render_switch_buttons(mono_record_views);
  323. },
  324. /**
  325. * Renders the control elements (buttons, sidebar, pager) of the current view.
  326. * Fills this.active_view.control_elements dictionnary with the rendered
  327. * elements and the adequate view switcher, to send to the ControlPanel.
  328. * Warning: it should be called before calling do_show on the view as the
  329. * sidebar is extended to listen on the load_record event triggered as soon
  330. * as do_show is done (the sidebar should thus be instantiated before).
  331. */
  332. render_view_control_elements: function() {
  333. if (!this.active_view.control_elements) {
  334. var view_controller = this.active_view.controller;
  335. var $buttons = this.flags.$buttons;
  336. var elements = {};
  337. if (!this.flags.headless) {
  338. elements = {
  339. $buttons: $("<div>"),
  340. $sidebar: $("<div>"),
  341. $pager: $("<div>"),
  342. };
  343. }
  344. view_controller.render_buttons($buttons ? $buttons.empty() : elements.$buttons);
  345. view_controller.render_sidebar(elements.$sidebar);
  346. view_controller.render_pager(elements.$pager);
  347. // Remove the unnecessary outer div
  348. elements = _.mapObject(elements, function($node) {
  349. return $node && $node.contents();
  350. });
  351. // Use the adequate view switcher (mono- or multi-record)
  352. if (this.switch_buttons) {
  353. if (this.active_view.multi_record) {
  354. elements.$switch_buttons = this.switch_buttons.$multi;
  355. } else {
  356. elements.$switch_buttons = this.switch_buttons.$mono;
  357. }
  358. }
  359. // Store the rendered elements in the active_view to allow restoring them later
  360. this.active_view.control_elements = elements;
  361. }
  362. return this.active_view.control_elements;
  363. },
  364. /**
  365. * Sets up the current viewmanager's search view.
  366. * Sets $searchview and $searchview_buttons in searchview_elements to send to the ControlPanel
  367. */
  368. setup_search_view: function() {
  369. var self = this;
  370. if (this.searchview) {
  371. this.searchview.destroy();
  372. }
  373. var search_defaults = {};
  374. var context = this.action.context || [];
  375. _.each(context, function (value, key) {
  376. var match = /^search_default_(.*)$/.exec(key);
  377. if (match) {
  378. search_defaults[match[1]] = value;
  379. }
  380. });
  381. var options = {
  382. hidden: this.flags.search_view === false,
  383. disable_custom_filters: this.flags.search_disable_custom_filters,
  384. $buttons: $("<div>"),
  385. action: this.action,
  386. search_defaults: search_defaults,
  387. };
  388. // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will
  389. // be done later, simultaneously to all other ControlPanel elements
  390. this.searchview = new SearchView(this, this.dataset, this.search_fields_view, options);
  391. this.searchview.on('search_data', this, this.search.bind(this));
  392. return $.when(this.searchview.appendTo($("<div>"))).done(function() {
  393. self.searchview_elements = {};
  394. self.searchview_elements.$searchview = self.searchview.$el;
  395. self.searchview_elements.$searchview_buttons = self.searchview.$buttons.contents();
  396. });
  397. },
  398. /**
  399. * Executed on event "search_data" thrown by the SearchView
  400. */
  401. search: function(domains, contexts, groupbys) {
  402. var self = this;
  403. var controller = this.active_view.controller; // the correct view must be loaded here
  404. var action_context = this.action.context || {};
  405. var view_context = controller.get_context();
  406. pyeval.eval_domains_and_contexts({
  407. domains: [this.action.domain || []].concat(domains || []),
  408. contexts: [action_context, view_context].concat(contexts || []),
  409. group_by_seq: groupbys || []
  410. }).done(function (results) {
  411. if (results.error) {
  412. self.active_search.resolve();
  413. throw new Error(
  414. _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
  415. JSON.stringify(results.error)));
  416. }
  417. self.dataset._model = new Model(
  418. self.dataset.model, results.context, results.domain);
  419. var groupby = results.group_by.length ?
  420. results.group_by :
  421. action_context.group_by;
  422. if (_.isString(groupby)) {
  423. groupby = [groupby];
  424. }
  425. if (!controller.grouped && !_.isEmpty(groupby)){
  426. self.dataset.set_sort([]);
  427. }
  428. $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
  429. self.active_search.resolve();
  430. });
  431. });
  432. },
  433. do_push_state: function(state) {
  434. if (this.action_manager) {
  435. state.view_type = this.active_view.type;
  436. this.action_manager.do_push_state(state);
  437. }
  438. },
  439. do_load_state: function(state, warm) {
  440. if (state.view_type && state.view_type !== this.active_view.type) {
  441. this.switch_mode(state.view_type, true);
  442. }
  443. this.active_view.controller.do_load_state(state, warm);
  444. },
  445. destroy: function () {
  446. if (this.control_elements) {
  447. if (this.control_elements.$switch_buttons) {
  448. this.control_elements.$switch_buttons.off();
  449. }
  450. }
  451. return this._super.apply(this, arguments);
  452. },
  453. //--------------------------------------------------------------------------
  454. // Handlers
  455. //--------------------------------------------------------------------------
  456. // ONLY FORWARDPORT THIS COMMIT UP TO SAAS-15
  457. /**
  458. * Handles a context request: provides to the caller the context of the
  459. * active view.
  460. *
  461. * @private
  462. * @param {OdooEvent} ev
  463. * @param {function} ev.data.callback used to send the requested context
  464. */
  465. _onGetControllerContext: function (ev) {
  466. var controller = this.active_view && this.active_view.controller;
  467. var context = controller && controller.get_context();
  468. ev.data.callback(context);
  469. },
  470. });
  471. return ViewManager;
  472. });