From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/web/static/src/js/chrome/action_manager.js | 939 ++++++++++++++++++++++ 1 file changed, 939 insertions(+) create mode 100644 addons/web/static/src/js/chrome/action_manager.js (limited to 'addons/web/static/src/js/chrome/action_manager.js') diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js new file mode 100644 index 00000000..6cb86030 --- /dev/null +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -0,0 +1,939 @@ +odoo.define('web.ActionManager', function (require) { +"use strict"; + +/** + * ActionManager + * + * The ActionManager is one of the centrepieces in the WebClient architecture. + * Its role is to makes sure that Odoo actions are properly started and + * coordinated. + */ + +var AbstractAction = require('web.AbstractAction'); +var concurrency = require('web.concurrency'); +var Context = require('web.Context'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var dom = require('web.dom'); +var framework = require('web.framework'); +var pyUtils = require('web.py_utils'); +var Widget = require('web.Widget'); + +var _t = core._t; +var ActionManager = Widget.extend({ + className: 'o_action_manager', + custom_events: { + breadcrumb_clicked: '_onBreadcrumbClicked', + history_back: '_onHistoryBack', + push_state: '_onPushState', + redirect: '_onRedirect', + }, + + /** + * @override + * @param {Object} [userContext={}] + */ + init: function (parent, userContext) { + this._super.apply(this, arguments); + this.userContext = userContext || {}; + + // use a DropPrevious to drop previous actions when multiple actions are + // run simultaneously + this.dp = new concurrency.DropPrevious(); + + // 'actions' is an Object that registers the actions that are currently + // handled by the ActionManager (either stacked in the current window, + // or opened in dialogs) + this.actions = {}; + + // 'controllers' is an Object that registers the alive controllers + // linked registered actions, a controller being Object with keys + // (amongst others) 'jsID' (a local identifier) and 'widget' (the + // instance of the controller's widget) + this.controllers = {}; + + // 'controllerStack' is the stack of ids of the controllers currently + // displayed in the current window + this.controllerStack = []; + + // 'currentDialogController' is the current controller opened in a + // dialog (i.e. coming from an action with target='new') + this.currentDialogController = null; + }, + /** + * Called each time the action manager is attached into the DOM. + */ + on_attach_callback: function () { + this.isInDOM = true; + var currentController = this.getCurrentController(); + if (currentController) { + currentController.widget.on_attach_callback(); + } + }, + /** + * Called each time the action manager is detached from the DOM. + */ + on_detach_callback: function () { + this.isInDOM = false; + var currentController = this.getCurrentController(); + if (currentController) { + currentController.widget.on_detach_callback(); + } + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * This function is called when the current controller is about to be + * removed from the DOM, because a new one will be pushed, or an old one + * will be restored. It ensures that the current controller can be left (for + * instance, that it has no unsaved changes). + * + * @returns {Promise} resolved if the current controller can be left, + * rejected otherwise. + */ + clearUncommittedChanges: function () { + var currentController = this.getCurrentController(); + if (currentController) { + return currentController.widget.canBeRemoved(); + } + return Promise.resolve(); + }, + /** + * This is the entry point to execute Odoo actions, given as an ID in + * database, an xml ID, a client action tag or an action descriptor. + * + * @param {number|string|Object} action the action to execute + * @param {Object} [options] + * @param {Object} [options.additional_context] additional context to be + * merged with the action's context. + * @param {boolean} [options.clear_breadcrumbs=false] set to true to clear + * the breadcrumbs history list + * @param {Function} [options.on_close] callback to be executed when the + * current action is active again (typically, if the new action is + * executed in target="new", on_close will be executed when the dialog is + * closed, if the current controller is still active) + * @param {Function} [options.on_reverse_breadcrumb] callback to be executed + * whenever an anterior breadcrumb item is clicked on + * @param {boolean} [options.pushState=true] set to false to prevent the + * ActionManager from pushing the state when the action is executed (this + * is useful when we come from a loadState()) + * @param {boolean} [options.replace_last_action=false] set to true to + * replace last part of the breadcrumbs with the action + * @return {Promise} resolved with the action when the action is + * loaded and appended to the DOM ; rejected if the action can't be + * executed (e.g. if doAction has been called to execute another action + * before this one was complete). + */ + doAction: function (action, options) { + var self = this; + options = _.defaults({}, options, { + additional_context: {}, + clear_breadcrumbs: false, + on_close: function () {}, + on_reverse_breadcrumb: function () {}, + pushState: true, + replace_last_action: false, + }); + + // build or load an action descriptor for the given action + var def; + if (_.isString(action) && core.action_registry.contains(action)) { + // action is a tag of a client action + action = { type: 'ir.actions.client', tag: action }; + } else if (_.isNumber(action) || _.isString(action)) { + // action is an id or xml id + def = this._loadAction(action, { + active_id: options.additional_context.active_id, + active_ids: options.additional_context.active_ids, + active_model: options.additional_context.active_model, + }).then(function (result) { + action = result; + }); + } + + return this.dp.add(Promise.resolve(def)).then(function () { + // action.target 'main' is equivalent to 'current' except that it + // also clears the breadcrumbs + options.clear_breadcrumbs = action.target === 'main' || + options.clear_breadcrumbs; + + self._preprocessAction(action, options); + + return self._handleAction(action, options).then(function () { + // now that the action has been executed, force its 'pushState' + // flag to 'true', as we don't want to prevent its controller + // from pushing its state if it changes in the future + action.pushState = true; + + return action; + }); + }).then(function(action) { + self.trigger_up('webclient_started'); + return action; + }); + }, + /** + * Compatibility with client actions that are still using do_push_state. + * + * @todo: convert all of them to trigger_up('push_state') instead. + * @param {Object} state + */ + do_push_state: function (state) { + this.trigger_up('push_state', {state: state}); + }, + /** + * Returns the action of the last controller in the controllerStack, i.e. + * the action of the currently displayed controller in the main window (not + * in a dialog), and null if there is no controller in the stack. + * + * @returns {Object|null} + */ + getCurrentAction: function () { + var controller = this.getCurrentController(); + return controller ? this.actions[controller.actionID] : null; + }, + /** + * Returns the last controller in the controllerStack, i.e. the currently + * displayed controller in the main window (not in a dialog), and + * null if there is no controller in the stack. + * + * @returns {Object|null} + */ + getCurrentController: function () { + var currentControllerID = _.last(this.controllerStack); + return currentControllerID ? this.controllers[currentControllerID] : null; + }, + /** + * Updates the UI according to the given state, for instance, executes a new + * action, or updates the state of the current action. + * + * @param {Object} state + * @param {integer|string} [state.action] the action to execute (given its + * id or tag for client actions) + * @returns {Promise} resolved when the UI has been updated + */ + loadState: function (state) { + var action; + if (!state.action) { + return Promise.resolve(); + } + if (_.isString(state.action) && core.action_registry.contains(state.action)) { + action = { + params: state, + tag: state.action, + type: 'ir.actions.client', + }; + } else { + action = state.action; + } + return this.doAction(action, { + clear_breadcrumbs: true, + pushState: false, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Appends the given controller to the DOM and restores its scroll position. + * Also updates the control panel. + * + * @private + * @param {Object} controller + */ + _appendController: function (controller) { + dom.append(this.$el, controller.widget.$el, { + in_DOM: this.isInDOM, + callbacks: [{widget: controller.widget}], + }); + + if (controller.scrollPosition) { + this.trigger_up('scrollTo', controller.scrollPosition); + } + }, + /** + * Closes the current dialog, if any. Because we listen to the 'closed' + * event triggered by the dialog when it is closed, this also destroys the + * embedded controller and removes the reference to the corresponding action. + * This also executes the 'on_close' handler in some cases, and may also + * provide infos for closing this dialog. + * + * @private + * @param {Object} options + * @param {Object} [options.infos] some infos related to the closing the + * dialog. + * @param {boolean} [options.silent=false] if true, the 'on_close' handler + * won't be called ; this is in general the case when the current dialog + * is closed because another action is opened, so we don't want the former + * action to execute its handler as it won't be displayed anyway + */ + _closeDialog: function (options) { + if (this.currentDialogController) { + this.currentDialogController.dialog.destroy(options); + } + }, + /** + * Detaches the current controller from the DOM and stores its scroll + * position, in case we'd come back to that controller later. + * + * @private + */ + _detachCurrentController: function () { + var currentController = this.getCurrentController(); + if (currentController) { + currentController.scrollPosition = this._getScrollPosition(); + dom.detach([{widget: currentController.widget}]); + } + }, + /** + * Executes actions for which a controller has to be appended to the DOM, + * either in the main content (target="current", by default), or in a dialog + * (target="new"). + * + * @private + * @param {Object} action + * @param {widget} action.controller a Widget instance to append to the DOM + * @param {string} [action.target="current"] set to "new" to render the + * controller in a dialog + * @param {Object} options @see doAction for details + * @returns {Promise} resolved when the controller is started and appended + */ + _executeAction: function (action, options) { + var self = this; + this.actions[action.jsID] = action; + + if (action.target === 'new') { + return this._executeActionInDialog(action, options); + } + + var controller = self.controllers[action.controllerID]; + return this.clearUncommittedChanges() + .then(function () { + return self.dp.add(self._startController(controller)); + }) + .then(function () { + if (self.currentDialogController) { + self._closeDialog({ silent: true }); + } + + // store the optional 'on_reverse_breadcrumb' handler + // AAB: store it on the AbstractAction instance, and call it + // automatically when the action is restored + if (options.on_reverse_breadcrumb) { + var currentAction = self.getCurrentAction(); + if (currentAction) { + currentAction.on_reverse_breadcrumb = options.on_reverse_breadcrumb; + } + } + + // update the internal state and the DOM + self._pushController(controller); + + // store the action into the sessionStorage so that it can be + // fully restored on F5 + self.call('session_storage', 'setItem', 'current_action', action._originalAction); + + return action; + }) + .guardedCatch(function () { + self._removeAction(action.jsID); + }); + }, + /** + * Executes actions with attribute target='new'. Such actions are rendered + * in a dialog. + * + * @private + * @param {Object} action + * @param {Object} options @see doAction for details + * @returns {Promise} resolved when the controller is rendered inside a + * dialog appended to the DOM + */ + _executeActionInDialog: function (action, options) { + var self = this; + var controller = this.controllers[action.controllerID]; + var widget = controller.widget; + + return this._startController(controller).then(function (controller) { + var prevDialogOnClose; + if (self.currentDialogController) { + prevDialogOnClose = self.currentDialogController.onClose; + self._closeDialog({ silent: true }); + } + + controller.onClose = prevDialogOnClose || options.on_close; + var dialog = new Dialog(self, _.defaults({}, options, { + buttons: [], + dialogClass: controller.className, + title: action.name, + size: action.context.dialog_size, + })); + /** + * @param {Object} [options={}] + * @param {Object} [options.infos] if provided and `silent` is + * unset, the `on_close` handler will pass this information, + * which gives some context for closing this dialog. + * @param {boolean} [options.silent=false] if set, do not call the + * `on_close` handler. + */ + dialog.on('closed', self, function (options) { + options = options || {}; + self._removeAction(action.jsID); + self.currentDialogController = null; + if (options.silent !== true) { + controller.onClose(options.infos); + } + }); + controller.dialog = dialog; + + return dialog.open().opened(function () { + self.currentDialogController = controller; + widget.setParent(dialog); + dom.append(dialog.$el, widget.$el, { + in_DOM: true, + callbacks: [{widget: controller.widget}], + }); + widget.renderButtons(dialog.$footer); + dialog.rebindButtonBehavior(); + + return action; + }); + }).guardedCatch(function () { + self._removeAction(action.jsID); + }); + }, + /** + * Executes actions of type 'ir.actions.client'. + * + * @private + * @param {Object} action the description of the action to execute + * @param {string} action.tag the key of the action in the action_registry + * @param {Object} options @see doAction for details + * @returns {Promise} resolved when the client action has been executed + */ + _executeClientAction: function (action, options) { + var self = this; + var ClientAction = core.action_registry.get(action.tag); + if (!ClientAction) { + console.error("Could not find client action " + action.tag, action); + return Promise.reject(); + } + if (!(ClientAction.prototype instanceof Widget)) { + // the client action might be a function, which is executed and + // whose returned value might be another action to execute + var next = ClientAction(this, action); + if (next) { + return this.doAction(next, options); + } + return Promise.resolve(); + } + if (!(ClientAction.prototype instanceof AbstractAction)) { + console.warn('The client action ' + action.tag + ' should be an instance of AbstractAction!'); + } + + var controllerID = _.uniqueId('controller_'); + + var index = this._getControllerStackIndex(options); + options.breadcrumbs = this._getBreadcrumbs(this.controllerStack.slice(0, index)); + options.controllerID = controllerID; + var widget = new ClientAction(this, action, options); + var controller = { + actionID: action.jsID, + index: index, + jsID: controllerID, + title: widget.getTitle(), + widget: widget, + }; + this.controllers[controllerID] = controller; + action.controllerID = controllerID; + var prom = this._executeAction(action, options); + prom.then(function () { + self._pushState(controllerID, {}); + }); + return prom; + }, + /** + * Executes actions of type 'ir.actions.act_window_close', i.e. closes the + * last opened dialog. + * + * The action may also specify an effect to display right after the close + * action (e.g. rainbow man), or provide a reason for the close action. + * This is useful for decision making for the `on_close` handler. + * + * @private + * @param {Object} action + * @param {Object} [action.effect] effect to show up, e.g. rainbow man. + * @param {Object} [action.infos] infos on performing the close action. + * Useful for providing some context for the `on_close` handler. + * @returns {Promise} resolved immediately + */ + _executeCloseAction: function (action, options) { + var result; + if (!this.currentDialogController) { + result = options.on_close(action.infos); + } + + this._closeDialog({ infos: action.infos }); + + // display some effect (like rainbowman) on appropriate actions + if (action.effect) { + this.trigger_up('show_effect', action.effect); + } + + return Promise.resolve(result); + }, + /** + * Executes actions of type 'ir.actions.server'. + * + * @private + * @param {Object} action the description of the action to execute + * @param {integer} action.id the db ID of the action to execute + * @param {Object} [action.context] + * @param {Object} options @see doAction for details + * @returns {Promise} resolved when the action has been executed + */ + _executeServerAction: function (action, options) { + var self = this; + var runDef = this._rpc({ + route: '/web/action/run', + params: { + action_id: action.id, + context: action.context || {}, + }, + }); + return this.dp.add(runDef).then(function (action) { + action = action || { type: 'ir.actions.act_window_close' }; + return self.doAction(action, options); + }); + }, + /** + * Executes actions of type 'ir.actions.act_url', i.e. redirects to the + * given url. + * + * @private + * @param {Object} action the description of the action to execute + * @param {string} action.url + * @param {string} [action.target] set to 'self' to redirect in the current page, + * redirects to a new page by default + * @param {Object} options @see doAction for details + * @returns {Promise} resolved when the redirection is done (immediately + * when redirecting to a new page) + */ + _executeURLAction: function (action, options) { + var url = action.url; + + if (action.target === 'self') { + framework.redirect(url); + return Promise.resolve(); + } else { + var w = window.open(url, '_blank'); + if (!w || w.closed || typeof w.closed === 'undefined') { + var message = _t('A popup window has been blocked. You ' + + 'may need to change your browser settings to allow ' + + 'popup windows for this page.'); + this.do_warn(false, message, true); + } + } + + options.on_close(); + + return Promise.resolve(); + }, + /** + * Returns a description of the controllers in the given controller stack. + * It is used to render the breadcrumbs. It is an array of Objects with keys + * 'title' (what to display in the breadcrumbs) and 'controllerID' (the ID + * of the corresponding controller, used to restore it when this part of the + * breadcrumbs is clicked). + * + * @private + * @param {string[]} controllerStack + * @returns {Object[]} + */ + _getBreadcrumbs: function (controllerStack) { + var self = this; + return _.map(controllerStack, function (controllerID) { + return { + controllerID: controllerID, + title: self.controllers[controllerID].title, + }; + }); + }, + /** + * Returns the index where a controller should be inserted in the controller + * stack according to the given options. By default, a controller is pushed + * on the top of the stack. + * + * @private + * @param {options} [options.clear_breadcrumbs=false] if true, insert at + * index 0 and remove all other controllers + * @param {options} [options.index=null] if given, that index is returned + * @param {options} [options.replace_last_action=false] if true, replace the + * last controller of the stack + * @returns {integer} index + */ + _getControllerStackIndex: function (options) { + var index; + if ('index' in options) { + index = options.index; + } else if (options.clear_breadcrumbs) { + index = 0; + } else if (options.replace_last_action) { + index = this.controllerStack.length - 1; + } else { + index = this.controllerStack.length; + } + return index; + }, + /** + * Returns an object containing information about the given controller, like + * its title, its action's id, the active_id and active_ids of the action... + * + * @private + * @param {string} controllerID + * @returns {Object} + */ + _getControllerState: function (controllerID) { + var controller = this.controllers[controllerID]; + var action = this.actions[controller.actionID]; + var state = { + title: controller.widget.getTitle(), + }; + if (action.id) { + state.action = action.id; + } else if (action.type === 'ir.actions.client') { + state.action = action.tag; + var params = _.pick(action.params, function (v) { + return _.isString(v) || _.isNumber(v); + }); + state = _.extend(params || {}, state); + } + if (action.context) { + var active_id = action.context.active_id; + if (active_id) { + state.active_id = active_id; + } + var active_ids = action.context.active_ids; + // we don't push active_ids if it's a single element array containing the active_id + // to make the url shorter in most cases + if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) { + state.active_ids = action.context.active_ids.join(','); + } + } + state = _.extend({}, controller.widget.getState(), state); + return state; + }, + /** + * Returns the current horizontal and vertical scroll positions. + * + * @private + * @returns {Object} + */ + _getScrollPosition: function () { + var scrollPosition; + this.trigger_up('getScrollPosition', { + callback: function (_scrollPosition) { + scrollPosition = _scrollPosition; + } + }); + return scrollPosition; + }, + /** + * Dispatches the given action to the corresponding handler to execute it, + * according to its type. This function can be overridden to extend the + * range of supported action types. + * + * @private + * @param {Object} action + * @param {string} action.type + * @param {Object} options + * @returns {Promise} resolved when the action has been executed ; rejected + * if the type of action isn't supported, or if the action can't be + * executed + */ + _handleAction: function (action, options) { + if (!action.type) { + console.error("No type for action", action); + return Promise.reject(); + } + switch (action.type) { + case 'ir.actions.act_url': + return this._executeURLAction(action, options); + case 'ir.actions.act_window_close': + return this._executeCloseAction(action, options); + case 'ir.actions.client': + return this._executeClientAction(action, options); + case 'ir.actions.server': + return this._executeServerAction(action, options); + default: + console.error("The ActionManager can't handle actions of type " + + action.type, action); + return Promise.reject(); + } + }, + /** + * Updates the internal state and the DOM with the given controller as + * current controller. + * + * @private + * @param {Object} controller + * @param {string} controller.jsID + * @param {Widget} controller.widget + * @param {integer} controller.index the controller is pushed at that + * position in the controller stack and controllers with an higher index + * are destroyed + */ + _pushController: function (controller) { + var self = this; + + // detach the current controller + this._detachCurrentController(); + + // push the new controller to the stack at the given position, and + // destroy controllers with an higher index + var toDestroy = this.controllerStack.slice(controller.index); + // reject from the list of controllers to destroy the one that we are + // currently pushing, or those linked to the same action as the one + // linked to the controller that we are pushing + toDestroy = _.reject(toDestroy, function (controllerID) { + return controllerID === controller.jsID || + self.controllers[controllerID].actionID === controller.actionID; + }); + this._removeControllers(toDestroy); + this.controllerStack = this.controllerStack.slice(0, controller.index); + this.controllerStack.push(controller.jsID); + + // append the new controller to the DOM + this._appendController(controller); + + // notify the environment of the new action + this.trigger_up('current_action_updated', { + action: this.getCurrentAction(), + controller: controller, + }); + + // close all dialogs when the current controller changes + core.bus.trigger('close_dialogs'); + + // toggle the fullscreen mode for actions in target='fullscreen' + this._toggleFullscreen(); + }, + /** + * Pushes the given state, with additional information about the given + * controller, like the action's id and the controller's title. + * + * @private + * @param {string} controllerID + * @param {Object} [state={}] + */ + _pushState: function (controllerID, state) { + var controller = this.controllers[controllerID]; + if (controller) { + var action = this.actions[controller.actionID]; + if (action.target === 'new' || action.pushState === false) { + // do not push state for actions in target="new" or for actions + // that have been explicitly marked as not pushable + return; + } + state = _.extend({}, state, this._getControllerState(controller.jsID)); + this.trigger_up('push_state', {state: state}); + } + }, + /** + * Loads an action from the database given its ID. + * + * @todo: turn this in a service (DataManager) + * @private + * @param {integer|string} action's ID or xml ID + * @param {Object} context + * @returns {Promise} resolved with the description of the action + */ + _loadAction: function (actionID, context) { + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('load_action', { + actionID: actionID, + context: context, + on_success: resolve, + }); + }); + }, + /** + * Preprocesses the action before it is handled by the ActionManager + * (assigns a JS id, evaluates its context and domains, etc.). + * + * @param {Object} action + * @param {Object} options see @doAction options + */ + _preprocessAction: function (action, options) { + // ensure that the context and domain are evaluated + var context = new Context(this.userContext, options.additional_context, action.context); + action.context = pyUtils.eval('context', context); + if (action.domain) { + action.domain = pyUtils.eval('domain', action.domain, action.context); + } + + action._originalAction = JSON.stringify(action); + + action.jsID = _.uniqueId('action_'); + action.pushState = options.pushState; + }, + /** + * Unlinks the given action and its controller from the internal structures + * and destroys its controllers. + * + * @private + * @param {string} actionID the id of the action to remove + */ + _removeAction: function (actionID) { + var action = this.actions[actionID]; + var controller = this.controllers[action.controllerID]; + delete this.actions[action.jsID]; + delete this.controllers[action.controllerID]; + controller.widget.destroy(); + }, + /** + * Removes the given controllers and their corresponding actions. + * + * @see _removeAction + * @private + * @param {string[]} controllerIDs + */ + _removeControllers: function (controllerIDs) { + var self = this; + var actionsToRemove = _.map(controllerIDs, function (controllerID) { + return self.controllers[controllerID].actionID; + }); + _.each(_.uniq(actionsToRemove), this._removeAction.bind(this)); + }, + /** + * Restores a controller from the controllerStack and destroys all + * controllers stacked over the given controller (called when coming back + * using the breadcrumbs). + * + * @private + * @param {string} controllerID + * @returns {Promise} resolved when the controller has been restored + */ + _restoreController: function (controllerID) { + var self = this; + var controller = this.controllers[controllerID]; + // AAB: AbstractAction should define a proper hook to execute code when + // it is restored (other than do_show), and it should return a promise + var action = this.actions[controller.actionID]; + var def; + if (action.on_reverse_breadcrumb) { + def = action.on_reverse_breadcrumb(); + } + return Promise.resolve(def).then(function () { + return Promise.resolve(controller.widget.do_show()).then(function () { + var index = _.indexOf(self.controllerStack, controllerID); + self._pushController(controller, index); + }); + }); + }, + /** + * Starts the controller by appending it in a document fragment, so that it + * is ready when it will be appended to the DOM. This allows to prevent + * flickering for widgets doing async stuff in willStart() or start(). + * + * Also updates the control panel on any change of the title on controller's + * widget. + * + * @private + * @param {Object} controller + * @returns {Promise} resolved with the controller when it is ready + */ + _startController: function (controller) { + var fragment = document.createDocumentFragment(); + return controller.widget.appendTo(fragment).then(function () { + return controller; + }); + }, + /** + * Toggles the fullscreen mode if there is an action in target='fullscreen' + * in the current stack. + * + * @private + */ + _toggleFullscreen: function () { + var self = this; + var fullscreen = _.some(this.controllerStack, function (controllerID) { + var controller = self.controllers[controllerID]; + return self.actions[controller.actionID].target === 'fullscreen'; + }); + this.trigger_up('toggle_fullscreen', {fullscreen: fullscreen}); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + * @param {string} ev.data.controllerID + */ + _onBreadcrumbClicked: function (ev) { + ev.stopPropagation(); + this._restoreController(ev.data.controllerID); + }, + /** + * Goes back in the history: if a controller is opened in a dialog, closes + * the dialog, otherwise, restores the second to last controller from the + * stack. + * + * @private + */ + _onHistoryBack: function () { + if (this.currentDialogController) { + this._closeDialog(); + } else { + var length = this.controllerStack.length; + if (length > 1) { + this._restoreController(this.controllerStack[length - 2]); + } + } + }, + /** + * Intercepts and triggers a new push_state event, with additional + * information about the given controller. + * + * @private + * @param {OdooEvent} ev + * @param {string} ev.controllerID + * @param {Object} [ev.state={}] + */ + _onPushState: function (ev) { + if (ev.target !== this) { + ev.stopPropagation(); + this._pushState(ev.data.controllerID, ev.data.state); + } + }, + /** + * Intercepts and triggers a redirection on a link. + * + * @private + * @param {OdooEvent} ev + * @param {integer} ev.data.res_id + * @param {string} ev.data.res_model + */ + _onRedirect: function (ev) { + this.do_action({ + type:'ir.actions.act_window', + view_mode: 'form', + res_model: ev.data.res_model, + views: [[false, 'form']], + res_id: ev.data.res_id, + }); + }, +}); + +return ActionManager; + +}); -- cgit v1.2.3