odoo.define('web.AbstractController', function (require) { "use strict"; /** * The Controller class is the class coordinating the model and the renderer. * It is the C in MVC, and is what was formerly known in Odoo as a View. * * Its role is to listen to events bubbling up from the model/renderer, and call * the appropriate methods if necessary. It also render control panel buttons, * and react to changes in the search view. Basically, all interactions from * the renderer/model with the outside world (meaning server/reading in session/ * reading localstorage, ...) has to go through the controller. */ var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var AbstractAction = require('web.AbstractAction'); var QWeb = core.qweb; var AbstractController = AbstractAction.extend(ControlPanelMixin, { custom_events: { open_record: '_onOpenRecord', switch_view: '_onSwitchView', }, events: { 'click a[type="action"]': '_onActionClicked', }, /** * @constructor * @param {Widget} parent * @param {AbstractModel} model * @param {AbstractRenderer} renderer * @param {object} params * @param {string} params.modelName * @param {string} [params.controllerID] an id to ease the communication * with upstream components * @param {any} [params.handle] a handle that will be given to the model (some id) * @param {any} params.initialState the initialState * @param {boolean} params.isMultiRecord * @param {Object[]} params.actionViews * @param {string} params.viewType * @param {boolean} params.withControlPanel set to false to hide the * ControlPanel */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.model = model; this.renderer = renderer; this.modelName = params.modelName; this.handle = params.handle; this.activeActions = params.activeActions; this.controllerID = params.controllerID; this.initialState = params.initialState; // those arguments are temporary, they won't be necessary as soon as the // ControlPanel will be handled by the View this.displayName = params.displayName; this.isMultiRecord = params.isMultiRecord; this.searchable = params.searchable; this.searchView = params.searchView; this.searchViewHidden = params.searchViewHidden; this.groupable = params.groupable; this.actionViews = params.actionViews; this.viewType = params.viewType; this.withControlPanel = params.withControlPanel !== false; // override this.need_control_panel so that the ActionManager doesn't // update the control panel when it isn't visible (this is a temporary // hack that can be removed as soon as the CP'll be handled by the view) this.need_control_panel = this.withControlPanel; }, /** * Simply renders and updates the url. * * @returns {Deferred} */ start: function () { var self = this; this.$el.addClass('o_view_controller'); // render the ControlPanel elements (buttons, pager, sidebar...) this.controlPanelElements = this._renderControlPanelElements(); return $.when( this._super.apply(this, arguments), this.renderer.appendTo(this.$el) ).then(function () { self._update(self.initialState); }); }, /** * @override */ destroy: function () { if (this.$buttons) { this.$buttons.off(); } if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) { this.controlPanelElements.$switch_buttons.off(); } return this._super.apply(this, arguments); }, /** * Called each time the controller is attached into the DOM. */ on_attach_callback: function () { if (this.searchView) { this.searchView.on_attach_callback(); } this.renderer.on_attach_callback(); }, /** * Called each time the controller is detached from the DOM. */ on_detach_callback: function () { this.renderer.on_detach_callback(); }, /** * Gives the focus to the renderer */ giveFocus:function(){ this.renderer.giveFocus(); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ canBeRemoved: function () { // AAB: get rid of this option when on_hashchange mechanism is improved return this.discardChanges(undefined, {readonlyIfRealDiscard: true}); }, /** * Discards the changes made on the record associated to the given ID, or * all changes made by the current controller if no recordID is given. For * example, when the user opens the 'home' screen, the action manager calls * this method on the active view to make sure it is ok to open the home * screen (and lose all current state). * * Note that it returns a deferred, because the view could choose to ask the * user if he agrees to discard. * * @param {string} [recordID] * if not given, we consider all the changes made by the controller * @returns {Deferred} resolved if properly discarded, rejected otherwise */ discardChanges: function (recordID) { return $.when(); }, /** * Returns any special keys that may be useful when reloading the view to * get the same effect. This is necessary for saving the current view in * the favorites. For example, a graph view might want to add a key to * save the current graph type. * * @returns {Object} */ getContext: function () { return {}; }, /** * Returns a title that may be displayed in the breadcrumb area. For * example, the name of the record. * * note: this will be moved to AbstractAction * * @returns {string} */ getTitle: function () { return this.displayName; }, /** * The use of this method is discouraged. It is still snakecased, because * it currently is used in many templates, but we will move to a simpler * mechanism as soon as we can. * * @deprecated * @param {string} action type of action, such as 'create', 'read', ... * @returns {boolean} */ is_action_enabled: function (action) { return this.activeActions[action]; }, /** * Short helper method to reload the view * * @param {Object} [params] This object will simply be given to the update * @returns {Deferred} */ reload: function (params) { return this.update(params || {}); }, /** * For views that require a pager, this method will be called to allow the * controller to instantiate and render a pager. Note that in theory, the * controller can actually render whatever he wants in the pager zone. If * your view does not want a pager, just let this method empty. * * @param {jQuery Node} $node */ renderPager: function ($node) { }, /** * Same as renderPager, but for the 'sidebar' zone (the zone with the menu * dropdown in the control panel next to the buttons) * * @param {jQuery Node} $node */ renderSidebar: function ($node) { }, /** * This is the main entry point for the controller. Changes from the search * view arrive in this method, and internal changes can sometimes also call * this method. It is basically the way everything notifies the controller * that something has changed. * * The update method is responsible for fetching necessary data, then * updating the renderer and wait for the rendering to complete. * * @param {Object} params will be given to the model and to the renderer * @param {Object} [options] * @param {boolean} [options.reload=true] if true, the model will reload data * * @returns {Deferred} */ update: function (params, options) { var self = this; var shouldReload = (options && 'reload' in options) ? options.reload : true; var def = shouldReload ? this.model.reload(this.handle, params) : $.when(); return def.then(function (handle) { self.handle = handle || self.handle; // update handle if we reloaded var state = self.model.get(self.handle); var localState = self.renderer.getLocalState(); return self.renderer.updateState(state, params).then(function () { self.renderer.setLocalState(localState); self._update(state); }); }); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * This method is the way a view can notifies the outside world that * something has changed. The main use for this is to update the url, for * example with a new id. * * @private * @param {Object} [state] information that will be pushed to the outside * world */ _pushState: function (state) { this.trigger_up('push_state', { controllerID: this.controllerID, state: state || {}, }); }, /** * Renders the control elements (buttons, pager and sidebar) of the current * view. * * @private * @returns {Object} an object containing the control panel jQuery elements */ _renderControlPanelElements: function () { var elements = {}; if (this.withControlPanel) { elements = { $buttons: $('
'), $sidebar: $('
'), $pager: $('
'), }; this.renderButtons(elements.$buttons); this.renderSidebar(elements.$sidebar); this.renderPager(elements.$pager); // remove the unnecessary outer div elements = _.mapObject(elements, function($node) { return $node && $node.contents(); }); elements.$switch_buttons = this._renderSwitchButtons(); } return elements; }, /** * Renders the switch buttons and binds listeners on them. * * @private * @returns {jQuery} */ _renderSwitchButtons: function () { var self = this; var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord}); if (views.length <= 1) { return $(); } var $switchButtons = $(QWeb.render('ControlPanel.SwitchButtons', { views: views, })); // create bootstrap tooltips _.each(views, function (view) { $switchButtons.filter('.o_cp_switch_' + view.type).tooltip(); }); // add onclick event listener $switchButtons.filter('button').click(_.debounce(function (event) { var viewType = $(event.target).data('view-type'); self.trigger_up('switch_view', {view_type: viewType}); }, 200, true)); return $switchButtons; }, /** * This method is called after each update or when the start method is * completed. * * Its primary use is to be used as a hook to update all parts of the UI, * besides the renderer. For example, it may be used to enable/disable * some buttons in the control panel, such as the current graph type for a * graph view. * * @private * @param {Object} state the state given by the model * @returns {Deferred} */ _update: function (state) { // AAB: update the control panel -> this will be moved elsewhere at some point var cpContent = _.extend({}, this.controlPanelElements); if (this.searchView) { _.extend(cpContent, { $searchview: this.searchView.$el, $searchview_buttons: this.searchView.$buttons, }); } this.update_control_panel({ active_view_selector: '.o_cp_switch_' + this.viewType, cp_content: cpContent, hidden: !this.withControlPanel, searchview: this.searchView, search_view_hidden: !this.searchable || this.searchviewHidden, groupable: this.groupable, }); this._pushState(); return $.when(); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * When an Odoo event arrives requesting a record to be opened, this method * gets the res_id, and request a switch view in the appropriate mode * * Note: this method seems wrong, it relies on the model being a basic model, * to get the res_id. It should receive the res_id in the event data * @todo move this to basic controller? * * @private * @param {OdooEvent} event * @param {number} event.data.id The local model ID for the record to be * opened * @param {string} [event.data.mode='readonly'] */ _onOpenRecord: function (event) { event.stopPropagation(); var record = this.model.get(event.data.id, {raw: true}); this.trigger_up('switch_view', { view_type: 'form', res_id: record.res_id, mode: event.data.mode || 'readonly', model: this.modelName, }); }, /** * When a user clicks on an link with type="action", we need to actually * do the action. This kind of links is used a lot in no-content helpers. * * @private * @param {OdooEvent} event */ _onActionClicked: function (event) { event.preventDefault(); this.do_action(event.target.name); }, /** * Intercepts the 'switch_view' event to add the controllerID into the data, * and lets the event bubble up. * * @param {OdooEvent} event */ _onSwitchView: function (event) { event.data.controllerID = this.controllerID; }, }); return AbstractController; });