odoo.define('web.ViewManager', function (require) { "use strict"; var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var data = require('web.data'); var framework = require('web.framework'); var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); var SearchView = require('web.SearchView'); var Widget = require('web.Widget'); var QWeb = core.qweb; var _t = core._t; var ViewManager = Widget.extend(ControlPanelMixin, { template: "ViewManager", /** * @param {Object} [dataset] null object (... historical reasons) * @param {Array} [views] List of [view_id, view_type] * @param {Object} [flags] various boolean describing UI state */ init: function(parent, dataset, views, flags, action, options) { if (action) { flags = action.flags || {}; if (!('auto_search' in flags)) { flags.auto_search = action.auto_search !== false; } this.action = action; this.action_manager = parent; 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; } views = action.views; } var self = this; this._super(parent); this.flags = flags || {}; this.dataset = dataset; this.view_order = []; this.views = {}; this.view_stack = []; // used for breadcrumbs this.active_view = null; this.registry = core.view_registry; this.title = this.action && this.action.name; this.is_in_DOM = false; // used to know if the view manager is attached in the DOM _.each(views, function (view) { var view_type = view[1] || view.view_type; var View = core.view_registry.get(view_type, true); if (!View) { console.error("View type", "'"+view[1]+"'", "is not present in the view registry."); return; } var view_label = View.prototype.display_name; var view_descr = { controller: null, options: view.options || {}, view_id: view[0] || view.view_id, type: view_type, label: view_label, embedded_view: view.embedded_view, title: self.title, button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}), multi_record: View.prototype.multi_record, accesskey: View.prototype.accesskey, icon: View.prototype.icon, }; self.view_order.push(view_descr); self.views[view_type] = view_descr; }); if (options && options.state && options.state.view_type) { var view_type = options.state.view_type; var view_descr = this.views[view_type]; this.default_view = view_descr && view_descr.multi_record ? view_type : undefined; } // Listen to event 'switch_view' indicating that the VM must now display view wiew_type this.on('switch_view', this, function(view_type) { if (view_type === 'form' && this.active_view && this.active_view.type === 'form') { this._display_view(view_type); } else { this.switch_mode(view_type); } }); }, /** * @returns {jQuery.Deferred} initial view loading promise */ start: function() { var self = this; var default_view = this.get_default_view(); var default_options = this.flags[default_view] && this.flags[default_view].options; this._super(); var views_ids = {}; _.each(this.views, function (view) { views_ids[view.type] = view.view_id; view.options = _.extend({ action : self.action, action_views_ids : views_ids, }, self.flags, self.flags[view.type], view.options); view.$container = self.$(".oe-view-manager-view-" + view.type); }); this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current')); this.control_elements = {}; if (this.flags.search_view) { this.search_view_loaded = this.setup_search_view(); } if (this.flags.views_switcher) { this.render_switch_buttons(); } // Switch to the default_view to load it var main_view_loaded = this.switch_mode(default_view, null, default_options); return $.when(main_view_loaded, this.search_view_loaded); }, get_default_view: function() { return this.default_view || this.flags.default_view || this.view_order[0].type; }, switch_mode: function(view_type, no_store, view_options) { var self = this; var view = this.views[view_type]; var old_view = this.active_view; if (!view) { return $.Deferred().reject(); } if (view.multi_record) { this.view_stack = []; } else if (this.view_stack.length > 0 && !(_.last(this.view_stack).multi_record)) { // Replace the last view by the new one if both are mono_record this.view_stack.pop(); } this.view_stack.push(view); // Hide active view (at first rendering, there is no view to hide) if (this.active_view && this.active_view !== view) { if (this.active_view.controller) this.active_view.controller.do_hide(); if (this.active_view.$container) this.active_view.$container.hide(); } this.active_view = view; if (!view.created) { view.created = this.create_view.bind(this)(view, view_options); } // Call do_search on the searchview to compute domains, contexts and groupbys if (this.search_view_loaded && this.flags.auto_search && view.controller.searchable !== false) { this.active_search = $.Deferred(); $.when(this.search_view_loaded, view.created).done(function() { self.searchview.do_search(); }); } return $.when(view.created, this.active_search).then(function () { return self._display_view(view_options, old_view).then(function () { self.trigger('switch_mode', view_type, no_store, view_options); }); }); }, _display_view: function (view_options, old_view) { var self = this; var view_controller = this.active_view.controller; var view_fragment = this.active_view.$fragment; var view_control_elements = this.render_view_control_elements(); // Show the view this.active_view.$container.show(); return $.when(view_controller.do_show(view_options)).done(function () { // Prepare the ControlPanel content and update it var cp_status = { active_view_selector: '.oe-cp-switch-' + self.active_view.type, breadcrumbs: self.action_manager && self.action_manager.get_breadcrumbs(), cp_content: _.extend({}, self.control_elements, view_control_elements), hidden: self.flags.headless, searchview: self.searchview, search_view_hidden: view_controller.searchable === false || view_controller.searchview_hidden, }; self.update_control_panel(cp_status); // Detach the old view but not ui-autocomplete elements to let // jquery-ui garbage-collect them if (old_view) { old_view.$container.contents().not('.ui-autocomplete').detach(); } // Append the view fragment to its $container framework.append(self.active_view.$container, view_fragment, self.is_in_DOM); }); }, create_view: function(view, view_options) { var self = this; var View = this.registry.get(view.type); var options = _.clone(view.options); var view_loaded = $.Deferred(); if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline')) || (view_options && view_options.mode === 'edit'))) { options.initial_mode = options.initial_mode || 'edit'; } var controller = new View(this, this.dataset, view.view_id, options); view.controller = controller; view.$fragment = $('
'); if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); } controller.on('switch_mode', this, this.switch_mode.bind(this)); controller.on('history_back', this, function () { if (self.action_manager) self.action_manager.trigger('history_back'); }); controller.on("change:title", this, function() { if (self.action_manager && !self.flags.headless) { var breadcrumbs = self.action_manager.get_breadcrumbs(); self.update_control_panel({breadcrumbs: breadcrumbs}, {clear: false}); } }); controller.on('view_loaded', this, function () { view_loaded.resolve(); }); // render the view in a fragment so that it is appended in the view's // $container only when it's ready return $.when(controller.appendTo(view.$fragment), view_loaded).done(function () { // Remove the unnecessary outer div view.$fragment = view.$fragment.contents(); self.trigger("controller_inited", view.type, controller); }); }, select_view: function (index) { var view_type = this.view_stack[index].type; this.view_stack.splice(index); return this.switch_mode(view_type); }, /** * Renders the switch buttons and adds listeners on them but does not append them to the DOM * Sets $switch_buttons in control_elements to send to the ControlPanel * @param {Object} [src] the source requesting the switch_buttons * @param {Array} [views] the array of views */ render_switch_buttons: function() { if (this.flags.views_switcher && this.view_order.length > 1) { var self = this; // Render switch buttons but do not append them to the DOM as this will // be done later, simultaneously to all other ControlPanel elements this.control_elements.$switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: self.view_order})); // Create bootstrap tooltips _.each(this.views, function(view) { self.control_elements.$switch_buttons.siblings('.oe-cp-switch-' + view.type).tooltip(); }); // Add onclick event listener this.control_elements.$switch_buttons.siblings('button').click(function(event) { var view_type = $(event.target).data('view-type'); self.switch_mode(view_type); }); } }, /** * Renders the control elements (buttons, sidebar, pager) of the current view * This must be done when active_search is resolved (for KanbanViews) * Fills this.active_view.control_elements dictionnary with the rendered * elements and the adequate view switcher, to send to the ControlPanel * Warning: it should be called before calling do_show on the view as the * sidebar is extended to listen on the load_record event triggered as soon * as do_show is done (the sidebar should thus be instantiated before) */ render_view_control_elements: function() { if (!this.active_view.control_elements) { var view_controller = this.active_view.controller; var elements = {}; if (!this.flags.headless) { elements = { $buttons: !this.flags.footer_to_buttons ? $("
") : undefined, $sidebar: $("
"), $pager: $("
"), }; } view_controller.render_buttons(elements.$buttons); view_controller.render_sidebar(elements.$sidebar); view_controller.render_pager(elements.$pager); // Remove the unnecessary outer div elements = _.mapObject(elements, function($node) { return $node && $node.contents(); }); // Store the rendered elements in the active_view to allow restoring them later this.active_view.control_elements = elements; } return this.active_view.control_elements; }, /** * @returns {Number|Boolean} the view id of the given type, false if not found */ get_view_id: function(view_type) { return this.views[view_type] && this.views[view_type].view_id || false; }, /** * Sets up the current viewmanager's search view. * Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel * * @param {Number|false} view_id the view to use or false for a default one * @returns {jQuery.Deferred} search view startup deferred */ setup_search_view: function() { var self = this; if (this.searchview) { this.searchview.destroy(); } var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; var search_defaults = {}; var context = this.action ? this.action.context : []; _.each(context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); if (match) { search_defaults[match[1]] = value; } }); var options = { hidden: this.flags.search_view === false, disable_custom_filters: this.flags.search_disable_custom_filters, $buttons: $("
"), action: this.action, }; // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will // be done later, simultaneously to all other ControlPanel elements this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options); this.searchview.on('search_data', this, this.search.bind(this)); return $.when(this.searchview.appendTo($("
"))).done(function() { self.control_elements.$searchview = self.searchview.$el; self.control_elements.$searchview_buttons = self.searchview.$buttons.contents(); }); }, /** * Executed on event "search_data" thrown by the SearchView */ search: function(domains, contexts, groupbys) { var self = this; var controller = this.active_view.controller; // the correct view must be loaded here var action_context = this.action.context || {}; var view_context = controller.get_context(); pyeval.eval_domains_and_contexts({ domains: [this.action.domain || []].concat(domains || []), contexts: [action_context, view_context].concat(contexts || []), group_by_seq: groupbys || [] }).done(function (results) { if (results.error) { self.active_search.resolve(); throw new Error( _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); } self.dataset._model = new Model( self.dataset.model, results.context, results.domain); var groupby = results.group_by.length ? results.group_by : action_context.group_by; if (_.isString(groupby)) { groupby = [groupby]; } if (!controller.grouped && !_.isEmpty(groupby)){ self.dataset.set_sort([]); } $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() { self.active_search.resolve(); }); }); }, do_push_state: function(state) { if (this.action_manager) { state.view_type = this.active_view.type; this.action_manager.do_push_state(state); } }, do_load_state: function(state, warm) { if (state.view_type && state.view_type !== this.active_view.type) { // warning: this code relies on the fact that switch_mode has an immediate side // effect (setting the 'active_view' to its new value) AND an async effect (the // view is created/loaded). So, the next statement (do_load_state) is executed // on the new view, after it was initialized, but before it is fully loaded and // in particular, before the do_show method is called. this.switch_mode(state.view_type, true); } this.active_view.controller.do_load_state(state, warm); }, }); return ViewManager; });