PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://gitlab.com/thanhchatvn/cloud-odoo
JavaScript | 420 lines | 323 code | 34 blank | 63 comment | 79 complexity | 7b9150a04963854ba5bbd4a0a8c5cc6e MD5 | raw file
  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 = require('web.data');
  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. template: "ViewManager",
  15. /**
  16. * @param {Object} [dataset] null object (... historical reasons)
  17. * @param {Array} [views] List of [view_id, view_type]
  18. * @param {Object} [flags] various boolean describing UI state
  19. */
  20. init: function(parent, dataset, views, flags, action, options) {
  21. if (action) {
  22. flags = action.flags || {};
  23. if (!('auto_search' in flags)) {
  24. flags.auto_search = action.auto_search !== false;
  25. }
  26. this.action = action;
  27. this.action_manager = parent;
  28. dataset = new data.DataSetSearch(this, action.res_model, action.context, action.domain);
  29. if (action.res_id) {
  30. dataset.ids.push(action.res_id);
  31. dataset.index = 0;
  32. }
  33. views = action.views;
  34. }
  35. var self = this;
  36. this._super(parent);
  37. this.flags = flags || {};
  38. this.dataset = dataset;
  39. this.view_order = [];
  40. this.views = {};
  41. this.view_stack = []; // used for breadcrumbs
  42. this.active_view = null;
  43. this.registry = core.view_registry;
  44. this.title = this.action && this.action.name;
  45. this.is_in_DOM = false; // used to know if the view manager is attached in the DOM
  46. _.each(views, function (view) {
  47. var view_type = view[1] || view.view_type;
  48. var View = core.view_registry.get(view_type, true);
  49. if (!View) {
  50. console.error("View type", "'"+view[1]+"'", "is not present in the view registry.");
  51. return;
  52. }
  53. var view_label = View.prototype.display_name;
  54. var view_descr = {
  55. controller: null,
  56. options: view.options || {},
  57. view_id: view[0] || view.view_id,
  58. type: view_type,
  59. label: view_label,
  60. embedded_view: view.embedded_view,
  61. title: self.title,
  62. button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
  63. multi_record: View.prototype.multi_record,
  64. accesskey: View.prototype.accesskey,
  65. icon: View.prototype.icon,
  66. };
  67. self.view_order.push(view_descr);
  68. self.views[view_type] = view_descr;
  69. });
  70. if (options && options.state && options.state.view_type) {
  71. var view_type = options.state.view_type;
  72. var view_descr = this.views[view_type];
  73. this.default_view = view_descr && view_descr.multi_record ? view_type : undefined;
  74. }
  75. // Listen to event 'switch_view' indicating that the VM must now display view wiew_type
  76. this.on('switch_view', this, function(view_type) {
  77. if (view_type === 'form' && this.active_view && this.active_view.type === 'form') {
  78. this._display_view(view_type);
  79. } else {
  80. this.switch_mode(view_type);
  81. }
  82. });
  83. },
  84. /**
  85. * @returns {jQuery.Deferred} initial view loading promise
  86. */
  87. start: function() {
  88. var self = this;
  89. var default_view = this.get_default_view();
  90. var default_options = this.flags[default_view] && this.flags[default_view].options;
  91. this._super();
  92. var views_ids = {};
  93. _.each(this.views, function (view) {
  94. views_ids[view.type] = view.view_id;
  95. view.options = _.extend({
  96. action : self.action,
  97. action_views_ids : views_ids,
  98. }, self.flags, self.flags[view.type], view.options);
  99. view.$container = self.$(".oe-view-manager-view-" + view.type);
  100. });
  101. this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current'));
  102. this.control_elements = {};
  103. if (this.flags.search_view) {
  104. this.search_view_loaded = this.setup_search_view();
  105. }
  106. if (this.flags.views_switcher) {
  107. this.render_switch_buttons();
  108. }
  109. // Switch to the default_view to load it
  110. var main_view_loaded = this.switch_mode(default_view, null, default_options);
  111. return $.when(main_view_loaded, this.search_view_loaded);
  112. },
  113. get_default_view: function() {
  114. return this.default_view || this.flags.default_view || this.view_order[0].type;
  115. },
  116. switch_mode: function(view_type, no_store, view_options) {
  117. var self = this;
  118. var view = this.views[view_type];
  119. var old_view = this.active_view;
  120. if (!view || this.currently_switching) {
  121. return $.Deferred().reject();
  122. } else {
  123. this.currently_switching = true; // prevent overlapping switches
  124. }
  125. if (view.multi_record) {
  126. this.view_stack = [];
  127. } else if (this.view_stack.length > 0 && !(_.last(this.view_stack).multi_record)) {
  128. // Replace the last view by the new one if both are mono_record
  129. this.view_stack.pop();
  130. }
  131. this.view_stack.push(view);
  132. this.active_view = view;
  133. if (!view.created) {
  134. view.created = this.create_view.bind(this)(view, view_options);
  135. }
  136. // Call do_search on the searchview to compute domains, contexts and groupbys
  137. if (this.search_view_loaded &&
  138. this.flags.auto_search &&
  139. view.controller.searchable !== false) {
  140. this.active_search = $.Deferred();
  141. $.when(this.search_view_loaded, view.created).done(function() {
  142. self.searchview.do_search();
  143. });
  144. }
  145. var switched = $.when(view.created, this.active_search).then(function () {
  146. return self._display_view(view_options, old_view).then(function () {
  147. self.trigger('switch_mode', view_type, no_store, view_options);
  148. });
  149. });
  150. switched.fail(function(e) {
  151. if (!(e && e.code === 200 && e.data.exception_type)) {
  152. self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
  153. }
  154. // Restore internal state
  155. self.active_view = old_view;
  156. self.view_stack.pop();
  157. });
  158. switched.always(function () {
  159. self.currently_switching = false;
  160. });
  161. return switched;
  162. },
  163. _display_view: function (view_options, old_view) {
  164. var self = this;
  165. var view_controller = this.active_view.controller;
  166. var view_fragment = this.active_view.$fragment;
  167. var view_control_elements = this.render_view_control_elements();
  168. // Show the view
  169. this.active_view.$container.show();
  170. return $.when(view_controller.do_show(view_options)).done(function () {
  171. // Prepare the ControlPanel content and update it
  172. var cp_status = {
  173. active_view_selector: '.oe-cp-switch-' + self.active_view.type,
  174. breadcrumbs: self.action_manager && self.action_manager.get_breadcrumbs(),
  175. cp_content: _.extend({}, self.control_elements, view_control_elements),
  176. hidden: self.flags.headless,
  177. searchview: self.searchview,
  178. search_view_hidden: view_controller.searchable === false || view_controller.searchview_hidden,
  179. };
  180. self.update_control_panel(cp_status);
  181. if (old_view) {
  182. // Detach the old view but not ui-autocomplete elements to let
  183. // jquery-ui garbage-collect them
  184. old_view.$container.contents().not('.ui-autocomplete').detach();
  185. // Hide old view (at first rendering, there is no view to hide)
  186. if (self.active_view !== old_view) {
  187. if (old_view.controller) old_view.controller.do_hide();
  188. if (old_view.$container) old_view.$container.hide();
  189. }
  190. }
  191. // Append the view fragment to its $container
  192. framework.append(self.active_view.$container, view_fragment, self.is_in_DOM);
  193. });
  194. },
  195. create_view: function(view, view_options) {
  196. var self = this;
  197. var View = this.registry.get(view.type);
  198. var options = _.clone(view.options);
  199. var view_loaded = $.Deferred();
  200. if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline')) ||
  201. (view_options && view_options.mode === 'edit'))) {
  202. options.initial_mode = options.initial_mode || 'edit';
  203. }
  204. var controller = new View(this, this.dataset, view.view_id, options);
  205. view.controller = controller;
  206. view.$fragment = $('<div>');
  207. if (view.embedded_view) {
  208. controller.set_embedded_view(view.embedded_view);
  209. }
  210. controller.on('switch_mode', this, this.switch_mode.bind(this));
  211. controller.on('history_back', this, function () {
  212. if (self.action_manager) self.action_manager.trigger('history_back');
  213. });
  214. controller.on("change:title", this, function() {
  215. if (self.action_manager && !self.flags.headless) {
  216. var breadcrumbs = self.action_manager.get_breadcrumbs();
  217. self.update_control_panel({breadcrumbs: breadcrumbs}, {clear: false});
  218. }
  219. });
  220. controller.on('view_loaded', this, function () {
  221. view_loaded.resolve();
  222. });
  223. // render the view in a fragment so that it is appended in the view's
  224. // $container only when it's ready
  225. return $.when(controller.appendTo(view.$fragment), view_loaded).done(function () {
  226. // Remove the unnecessary outer div
  227. view.$fragment = view.$fragment.contents();
  228. self.trigger("controller_inited", view.type, controller);
  229. });
  230. },
  231. select_view: function (index) {
  232. var view_type = this.view_stack[index].type;
  233. return this.switch_mode(view_type);
  234. },
  235. /**
  236. * Renders the switch buttons and adds listeners on them but does not append them to the DOM
  237. * Sets $switch_buttons in control_elements to send to the ControlPanel
  238. * @param {Object} [src] the source requesting the switch_buttons
  239. * @param {Array} [views] the array of views
  240. */
  241. render_switch_buttons: function() {
  242. if (this.flags.views_switcher && this.view_order.length > 1) {
  243. var self = this;
  244. // Render switch buttons but do not append them to the DOM as this will
  245. // be done later, simultaneously to all other ControlPanel elements
  246. this.control_elements.$switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: self.view_order}));
  247. // Create bootstrap tooltips
  248. _.each(this.views, function(view) {
  249. self.control_elements.$switch_buttons.siblings('.oe-cp-switch-' + view.type).tooltip();
  250. });
  251. // Add onclick event listener
  252. this.control_elements.$switch_buttons.siblings('button').click(_.debounce(function(event) {
  253. var view_type = $(event.target).data('view-type');
  254. self.switch_mode(view_type);
  255. }, 200, true));
  256. }
  257. },
  258. /**
  259. * Renders the control elements (buttons, sidebar, pager) of the current view
  260. * This must be done when active_search is resolved (for KanbanViews)
  261. * Fills this.active_view.control_elements dictionnary with the rendered
  262. * elements and the adequate view switcher, to send to the ControlPanel
  263. * Warning: it should be called before calling do_show on the view as the
  264. * sidebar is extended to listen on the load_record event triggered as soon
  265. * as do_show is done (the sidebar should thus be instantiated before)
  266. */
  267. render_view_control_elements: function() {
  268. if (!this.active_view.control_elements) {
  269. var view_controller = this.active_view.controller;
  270. var elements = {};
  271. if (!this.flags.headless) {
  272. elements = {
  273. $buttons: !this.flags.footer_to_buttons ? $("<div>") : undefined,
  274. $sidebar: $("<div>"),
  275. $pager: $("<div>"),
  276. };
  277. }
  278. view_controller.render_buttons(elements.$buttons);
  279. view_controller.render_sidebar(elements.$sidebar);
  280. view_controller.render_pager(elements.$pager);
  281. // Remove the unnecessary outer div
  282. elements = _.mapObject(elements, function($node) {
  283. return $node && $node.contents();
  284. });
  285. // Store the rendered elements in the active_view to allow restoring them later
  286. this.active_view.control_elements = elements;
  287. }
  288. return this.active_view.control_elements;
  289. },
  290. /**
  291. * @returns {Number|Boolean} the view id of the given type, false if not found
  292. */
  293. get_view_id: function(view_type) {
  294. return this.views[view_type] && this.views[view_type].view_id || false;
  295. },
  296. /**
  297. * Sets up the current viewmanager's search view.
  298. * Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel
  299. *
  300. * @param {Number|false} view_id the view to use or false for a default one
  301. * @returns {jQuery.Deferred} search view startup deferred
  302. */
  303. setup_search_view: function() {
  304. var self = this;
  305. if (this.searchview) {
  306. this.searchview.destroy();
  307. }
  308. var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
  309. var search_defaults = {};
  310. var context = this.action ? this.action.context : [];
  311. _.each(context, function (value, key) {
  312. var match = /^search_default_(.*)$/.exec(key);
  313. if (match) {
  314. search_defaults[match[1]] = value;
  315. }
  316. });
  317. var options = {
  318. hidden: this.flags.search_view === false,
  319. disable_custom_filters: this.flags.search_disable_custom_filters,
  320. $buttons: $("<div>"),
  321. action: this.action,
  322. };
  323. // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will
  324. // be done later, simultaneously to all other ControlPanel elements
  325. this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
  326. this.searchview.on('search_data', this, this.search.bind(this));
  327. return $.when(this.searchview.appendTo($("<div>"))).done(function() {
  328. self.control_elements.$searchview = self.searchview.$el;
  329. self.control_elements.$searchview_buttons = self.searchview.$buttons.contents();
  330. });
  331. },
  332. /**
  333. * Executed on event "search_data" thrown by the SearchView
  334. */
  335. search: function(domains, contexts, groupbys) {
  336. var self = this;
  337. var controller = this.active_view.controller; // the correct view must be loaded here
  338. var action_context = this.action.context || {};
  339. var view_context = controller.get_context();
  340. pyeval.eval_domains_and_contexts({
  341. domains: [this.action.domain || []].concat(domains || []),
  342. contexts: [action_context, view_context].concat(contexts || []),
  343. group_by_seq: groupbys || []
  344. }).done(function (results) {
  345. if (results.error) {
  346. self.active_search.resolve();
  347. throw new Error(
  348. _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
  349. JSON.stringify(results.error)));
  350. }
  351. self.dataset._model = new Model(
  352. self.dataset.model, results.context, results.domain);
  353. var groupby = results.group_by.length ?
  354. results.group_by :
  355. action_context.group_by;
  356. if (_.isString(groupby)) {
  357. groupby = [groupby];
  358. }
  359. if (!controller.grouped && !_.isEmpty(groupby)){
  360. self.dataset.set_sort([]);
  361. }
  362. $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
  363. self.active_search.resolve();
  364. });
  365. });
  366. },
  367. do_push_state: function(state) {
  368. if (this.action_manager) {
  369. state.view_type = this.active_view.type;
  370. this.action_manager.do_push_state(state);
  371. }
  372. },
  373. do_load_state: function(state, warm) {
  374. if (state.view_type && state.view_type !== this.active_view.type) {
  375. // warning: this code relies on the fact that switch_mode has an immediate side
  376. // effect (setting the 'active_view' to its new value) AND an async effect (the
  377. // view is created/loaded). So, the next statement (do_load_state) is executed
  378. // on the new view, after it was initialized, but before it is fully loaded and
  379. // in particular, before the do_show method is called.
  380. this.switch_mode(state.view_type, true);
  381. }
  382. this.active_view.controller.do_load_state(state, warm);
  383. },
  384. });
  385. return ViewManager;
  386. });