flectra.define('web.ActionManager', function (require) { "use strict"; /** * ActionManager * * The action manager is quite important: it makes sure that actions (the Flectra * objects, such as a client action, or a act_window) are properly started, * and coordinated. */ var Bus = require('web.Bus'); var ControlPanel = require('web.ControlPanel'); var Context = require('web.Context'); var core = require('web.core'); var crash_manager = require('web.crash_manager'); var data = require('web.data'); var data_manager = require('web.data_manager'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); var framework = require('web.framework'); var pyeval = require('web.pyeval'); var session = require('web.session'); var ViewManager = require('web.ViewManager'); var Widget = require('web.Widget'); /** * Class representing the actions of the ActionManager * Basic implementation for client actions that are functions */ var Action = core.Class.extend({ init: function(action) { this.action_descr = action; this.title = action.display_name || action.name; }, /** * This method should restore this previously loaded action * Calls on_reverse_breadcrumb_callback if defined * @return {Deferred} resolved when widget is enabled */ restore: function() { if (this.on_reverse_breadcrumb_callback) { return this.on_reverse_breadcrumb_callback(); } }, /** * There is nothing to detach in the case of a client function */ detach: function() { }, /** * Destroyer: there is nothing to destroy in the case of a client function */ destroy: function() { }, /** * Sets the on_reverse_breadcrumb_callback to be called when coming back to that action * @param {Function} [callback] the callback */ set_on_reverse_breadcrumb: function(callback) { this.on_reverse_breadcrumb_callback = callback; }, /** * Not implemented for client actions */ setScrollTop: function() { }, /** * Stores the DOM fragment of the action * @param {jQuery} [$fragment] the DOM fragment */ set_fragment: function($fragment) { this.$fragment = $fragment; }, /** * Not implemented for client actions * @return {int} the number of pixels the webclient is scrolled when leaving the action */ getScrollTop: function() { return 0; }, /** * @return {Object} the description of the action */ get_action_descr: function() { return this.action_descr; }, /** * @return {Object} dictionnary that will be interpreted to display the breadcrumbs */ get_breadcrumbs: function() { return { title: this.title, action: this }; }, /** * @return {int} the number of views stacked, i.e. 0 for client functions */ get_nb_views: function() { return 0; }, /** * @return {jQuery} the DOM fragment of the action */ get_fragment: function() { return this.$fragment; }, /** * @return {string} the active view, i.e. empty for client actions */ get_active_view: function() { return ''; }, }); /** * Specialization of Action for client actions that are Widgets */ var WidgetAction = Action.extend({ /** * Initializes the WidgetAction * Sets the title of the widget */ init: function(action, widget) { this._super(action); this.widget = widget; if (!this.widget.get('title')) { this.widget.set('title', this.title); } this.widget.on('change:title', this, function(widget) { this.title = widget.get('title'); }); }, /** * Restores WidgetAction by calling do_show on its widget */ restore: function() { var self = this; return $.when(this._super()).then(function() { return self.widget.do_show(); }); }, /** * Detaches the action's widget from the DOM * @return the widget's $el */ detach: function() { // Hack to remove badly inserted nvd3 tooltips ; should be removed when upgrading nvd3 lib $('body > .nvtooltip').remove(); return dom.detach([{widget: this.widget}]); }, /** * Destroys the widget */ destroy: function() { this.widget.destroy(); }, }); /** * Specialization of WidgetAction for window actions (i.e. ViewManagers) */ var ViewManagerAction = WidgetAction.extend({ /** * Restores a ViewManagerAction * Switches to the requested view by calling select_view on the ViewManager * @param {int} [view_index] the index of the view to select */ restore: function(view_index) { var _super = this._super.bind(this); return this.widget.select_view(view_index).then(function() { return _super(); }); }, /** * Sets the on_reverse_breadcrumb_callback and the scrollTop to apply when * coming back to that action * @param {Function} [callback] the callback * @param {int} [scrollTop] the number of pixels to scroll */ set_on_reverse_breadcrumb: function(callback, scrollTop) { this._super(callback); this.setScrollTop(scrollTop); }, /** * Sets the scroll position of the widgets's active_view * @todo: replace this with a generic get/set local state mechanism. * @see getScrollTop * * @override * @param {integer} [scrollTop] the number of pixels to scroll */ setScrollTop: function (scrollTop) { var activeView = this.widget.active_view; var viewController = activeView && activeView.controller; if (viewController) { viewController.setScrollTop(scrollTop); } }, /** * Returns the current scrolling offset for the current action. We have to * ask nicely the question to the active view, because the answer depends * on the view. * * @todo: replace this mechanism with a generic getLocalState and * getLocalState. Scrolling behaviour is only a part of what we might want * to restore. * * @override * @returns {integer} the number of pixels the webclient is currently * scrolled */ getScrollTop: function () { var activeView = this.widget.active_view; var viewController = activeView && activeView.controller; return viewController ? viewController.getScrollTop() : 0; }, /** * @return {Array} array of Objects that will be interpreted to display the breadcrumbs */ get_breadcrumbs: function() { var self = this; return this.widget.view_stack.map(function (view, index) { return { title: view.controller && view.controller.get('title') || self.title, index: index, action: self, }; }); }, /** * @return {int} the number of views stacked in the ViewManager */ get_nb_views: function() { return this.widget.view_stack.length; }, /** * @return {string} the active view of the ViewManager */ get_active_view: function() { return this.widget.active_view.type; } }); var ActionManager = Widget.extend({ template: 'ActionManager', /** * Called each time the action manager is attached into the DOM */ on_attach_callback: function() { this.is_in_DOM = true; if (this.inner_widget && this.inner_widget.on_attach_callback) { this.inner_widget.on_attach_callback(); } }, /** * Called each time the action manager is detached from the DOM */ on_detach_callback: function() { this.is_in_DOM = false; if (this.inner_widget && this.inner_widget.on_detach_callback) { this.inner_widget.on_detach_callback(); } }, init: function(parent, options) { this._super(parent); this.action_stack = []; this.inner_action = null; this.inner_widget = null; this.webclient = options && options.webclient; this.dialog = null; this.dialog_widget = null; this.on('history_back', this, this.proxy('history_back')); }, start: function() { this._super(); // Instantiate a unique main ControlPanel used by widgets of actions in this.action_stack this.main_control_panel = new ControlPanel(this); // Listen to event "on_breadcrumb_click" trigerred on the control panel when // clicking on a part of the breadcrumbs. Call select_action for this breadcrumb. this.main_control_panel.on("on_breadcrumb_click", this, _.debounce(function(action, index) { this.select_action(action, index); }, 200, true)); // Listen to event "DOM_updated" to restore the scroll position core.bus.on('DOM_updated', this, function() { // DO NOT FORWARDPORT THIS if (this.inner_action && !$('.dialog').length) { this.trigger_up('scrollTo', {offset: this.inner_action.getScrollTop() || 0}); } }); // Insert the main control panel into the DOM return this.main_control_panel.insertBefore(this.$el); }, dialog_stop: function (reason) { if (this.dialog) { this.dialog.destroy(reason); } this.dialog = null; }, /** * Add a new action to the action manager * * @param {Widget} widget typically, widgets added are openerp.web.ViewManager. The action manager uses the stack of actions to handle the breadcrumbs. * @param {Object} action_descr new action description * @param {Object} options * @param options.on_reverse_breadcrumb will be called when breadcrumb is clicked on * @param options.clear_breadcrumbs: boolean, if true, action stack is destroyed */ push_action: function(widget, action_descr, options) { var self = this; var old_action_stack = this.action_stack; var old_action = this.inner_action; var old_widget = this.inner_widget; var actions_to_destroy; options = options || {}; // Empty action_stack or replace last action if requested if (options.clear_breadcrumbs) { actions_to_destroy = this.action_stack; this.action_stack = []; } else if (options.replace_last_action && this.action_stack.length > 0) { actions_to_destroy = [this.action_stack.pop()]; } // Instantiate the new action var new_action; if (widget instanceof ViewManager) { new_action = new ViewManagerAction(action_descr, widget); } else if (widget instanceof Widget) { new_action = new WidgetAction(action_descr, widget); } else { new_action = new Action(action_descr); } // Set on_reverse_breadcrumb callback on previous inner_action if (this.webclient && old_action) { old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb, this.webclient.getScrollTop()); } // Update action_stack (must be done before appendTo to properly // compute the breadcrumbs and to perform do_push_state) this.action_stack.push(new_action); this.inner_action = new_action; this.inner_widget = widget; if (widget.need_control_panel) { // Set the ControlPanel bus on the widget to allow it to communicate its status widget.set_cp_bus(this.main_control_panel.get_bus()); } // render the inner_widget in a fragment, and append it to the // document only when it's ready var new_widget_fragment = document.createDocumentFragment(); return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() { // Detach the fragment of the previous action and store it within the action if (old_action) { old_action.set_fragment(old_action.detach()); } if (!widget.need_control_panel) { // Hide the main ControlPanel for widgets that do not use it self.main_control_panel.do_hide(); } // most of the time, the self.$el element should already be empty, // because we detached the old action just a few line up. However, // it may happen that it is not empty, for example when a view // manager was unable to load a view because of a crash. In any // case, this is done as a safety measure to avoid the 'double view' // situation that we had when the web client was unable to recover // from a crash. self.$el.empty(); dom.append(self.$el, new_widget_fragment, { in_DOM: self.is_in_DOM, callbacks: [{widget: self.inner_widget}], }); if (actions_to_destroy) { self.clear_action_stack(actions_to_destroy); } self.toggle_fullscreen(); self.trigger_up('current_action_updated', {action: new_action}); }).fail(function () { // Destroy failed action and restore internal state new_action.destroy(); self.action_stack = old_action_stack; self.inner_action = old_action; self.inner_widget = old_widget; }); }, setScrollTop: function(scrollTop) { if (this.inner_action) { this.inner_action.setScrollTop(scrollTop); } }, get_breadcrumbs: function () { return _.flatten(_.map(this.action_stack, function (action) { return action.get_breadcrumbs(); }), true); }, get_title: function () { if (this.action_stack.length === 1) { // horrible hack to display the action title instead of "New" for the actions // that use a form view to edit something that do not correspond to a real model // for example, point of sale "Your Session" or most settings form, var action = this.action_stack[0]; if (action.get_breadcrumbs().length === 1) { return action.title; } } var last_breadcrumb = _.last(this.get_breadcrumbs()); return last_breadcrumb ? last_breadcrumb.title : ""; }, get_action_stack: function () { return this.action_stack; }, get_inner_action: function() { return this.inner_action; }, get_inner_widget: function() { return this.inner_widget; }, history_back: function() { if (this.dialog) { this.dialog.close(); return; } var nbViews = this.inner_action.get_nb_views(); if (nbViews > 1) { // Stay on this action, but select the previous view return this.select_action(this.inner_action, nbViews - 2); } var nbActions = this.action_stack.length; if (nbActions > 1) { // Select the previous action var action = this.action_stack[nbActions - 2]; nbViews = action.get_nb_views(); return this.select_action(action, nbViews - 1); } else if (nbActions === 1 && nbViews === 1) { return this.select_action(this.action_stack[0], 0); } return $.Deferred().reject(); }, select_action: function(action, index) { var self = this; var def = this.webclient && this.webclient.clear_uncommitted_changes() || $.when(); return def.then(function() { // Set the new inner_action/widget and update the action stack var old_action = self.inner_action; var action_index = self.action_stack.indexOf(action); var to_destroy = self.action_stack.splice(action_index + 1); self.inner_action = action; self.inner_widget = action.widget; return $.when(action.restore(index)).done(function() { // Hide the ControlPanel if the widget doesn't use it if (!self.inner_widget.need_control_panel) { self.main_control_panel.do_hide(); } // Attach the DOM of the action and restore the scroll position only if necessary if (action !== old_action) { // Clear the action stack (this also removes the current action from the DOM) self.clear_action_stack(to_destroy); // Append the fragment of the action to restore to self.$el dom.append(self.$el, action.get_fragment(), { in_DOM: self.is_in_DOM, callbacks: [{widget: action.widget}], }); } self.trigger_up('current_action_updated', {action: action}); }); }).fail(function() { return $.Deferred().reject(); }); }, clear_action_stack: function(action_stack) { _.map(action_stack || this.action_stack, function(action) { action.destroy(); }); if (!action_stack) { this.action_stack = []; this.inner_action = null; this.inner_widget = null; } this.toggle_fullscreen(); }, toggle_fullscreen: function() { var is_fullscreen = _.some(this.action_stack, function(action) { return action.action_descr.target === "fullscreen"; }); this.trigger_up("toggle_fullscreen", {fullscreen: is_fullscreen}); }, do_push_state: function(state) { if (!this.webclient || this.dialog) { return; } state = state || {}; if (this.inner_action) { var inner_action_descr = this.inner_action.get_action_descr(); if (inner_action_descr._push_me === false) { // this action has been explicitly marked as not pushable return; } state.title = this.get_title(); if(inner_action_descr.type == 'ir.actions.act_window') { state.model = inner_action_descr.res_model; } if (inner_action_descr.menu_id) { state.menu_id = inner_action_descr.menu_id; } if (inner_action_descr.id) { state.action = inner_action_descr.id; } else if (inner_action_descr.type == 'ir.actions.client') { state.action = inner_action_descr.tag; var params = {}; _.each(inner_action_descr.params, function(v, k) { if(_.isString(v) || _.isNumber(v)) { params[k] = v; } }); state = _.extend(params || {}, state); } if (inner_action_descr.context) { var active_id = inner_action_descr.context.active_id; if (active_id) { state.active_id = active_id; } var active_ids = inner_action_descr.context.active_ids; if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) { // We don't push active_ids if it's a single element array containing the active_id // This makes the url shorter in most cases. state.active_ids = inner_action_descr.context.active_ids.join(','); } } } this.webclient.do_push_state(state); }, do_load_state: function(state, warm) { var self = this; var action_loaded; if (state.action) { if (_.isString(state.action) && core.action_registry.contains(state.action)) { var action_client = { type: "ir.actions.client", tag: state.action, params: state, _push_me: state._push_me, }; if (warm) { this.null_action(); } action_loaded = this.do_action(action_client); } else { var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action; if (run_action) { var add_context = {}; if (state.active_id) { add_context.active_id = state.active_id; } if (state.active_ids) { // The jQuery BBQ plugin does some parsing on values that are valid integers. // It means that if there's only one item, it will do parseInt() on it, // otherwise it will keep the comma seperated list as string. add_context.active_ids = state.active_ids.toString().split(',').map(function(id) { return parseInt(id, 10) || id; }); } else if (state.active_id) { add_context.active_ids = [state.active_id]; } add_context.params = state; if (warm) { this.null_action(); } action_loaded = this.do_action(state.action, { additional_context: add_context, res_id: state.id, view_type: state.view_type, }); } } } else if (state.model && state.id) { // TODO handle context & domain ? if (warm) { this.null_action(); } var action = { res_model: state.model, res_id: state.id, type: 'ir.actions.act_window', views: [[_.isNumber(state.view_id) ? state.view_id : false, 'form']] }; action_loaded = this.do_action(action); } else if (state.sa) { // load session action if (warm) { this.null_action(); } action_loaded = this._rpc({ route: '/web/session/get_session_action', params: {key: state.sa}, }) .then(function(action) { if (action) { return self.do_action(action); } }); } return $.when(action_loaded || null).then(function() { if (self.inner_widget && self.inner_widget.do_load_state) { return self.inner_widget.do_load_state(state, warm); } }); }, /** * Execute an OpenERP action * * @param {Number|String|String|Object} action Can be either an action id, an action XML id, a client action tag or an action descriptor. * @param {Object} [options] * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on. * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions) * @param {Function} [options.action_menu_id] Manually set the menu id on the fly. * @param {Object} [options.additional_context] Additional context to be merged with the action's context. * @return {jQuery.Deferred} Action loaded */ do_action: function(action, options) { options = _.defaults(options || {}, { clear_breadcrumbs: false, replace_last_action: false, on_reverse_breadcrumb: function() {}, hide_breadcrumb: false, on_close: function() {}, on_load: function() {}, action_menu_id: null, additional_context: {}, }); if (action === false) { action = { type: 'ir.actions.act_window_close' }; } else if (_.isString(action) && core.action_registry.contains(action)) { var action_client = { type: "ir.actions.client", tag: action, params: {} }; return this.do_action(action_client, options); } else if (_.isNumber(action) || _.isString(action)) { var self = this; var additional_context = { active_id : options.additional_context.active_id, active_ids : options.additional_context.active_ids, active_model : options.additional_context.active_model }; return data_manager.load_action(action, additional_context).then(function(result) { return self.do_action(result, options); }); } core.bus.trigger('action', action); // Force clear breadcrumbs if action's target is main options.clear_breadcrumbs = (action.target === 'main') || options.clear_breadcrumbs; // Ensure context & domain are evaluated and can be manipulated/used var user_context = this.getSession().user_context; var ncontext = new Context(user_context, options.additional_context, action.context || {}); action.context = pyeval.eval('context', ncontext); if (action.context.active_id || action.context.active_ids) { // Here we assume that when an `active_id` or `active_ids` is used // in the context, we are in a `related` action, so we disable the // searchview's default custom filters. action.context.search_disable_custom_filters = true; } if (action.domain) { action.domain = pyeval.eval( 'domain', action.domain, action.context || {}); } if (!action.type) { console.error("No type for action", action); return $.Deferred().reject(); } var type = action.type.replace(/\./g,'_'); action.menu_id = options.action_menu_id; action.res_id = options.res_id || action.res_id; action.context.params = _.extend({ 'action' : action.id }, action.context.params); if (!(type in this)) { console.error("Action manager can't handle action of type " + action.type, action); return $.Deferred().reject(); } // Special case for Dashboards, this should definitively be done upstream if (action.res_model === 'board.board' && action.view_mode === 'form') { action.target = 'inline'; _.extend(action.flags, { headless: true, views_switcher: false, display_title: false, search_view: false, pager: false, sidebar: false, action_buttons: false }); } else { var popup = action.target === 'new'; var inline = action.target === 'inline' || action.target === 'inlineview'; var form = _.str.startsWith(action.view_mode, 'form'); action.flags = _.defaults(action.flags || {}, { views_switcher : !popup && !inline, search_view : !(popup && form) && !inline, action_buttons : !popup && !inline, sidebar : !popup && !inline, pager : (!popup || !form) && !inline, display_title : !popup, headless: (popup || inline) && form, search_disable_custom_filters: action.context && action.context.search_disable_custom_filters, }); } return $.when(this[type](action, options)).then(function (executor_action) { options.on_load(executor_action); return action; }); }, null_action: function() { this.dialog_stop(); this.clear_action_stack(); }, /** * * @param {Object} executor * @param {Object} executor.action original action * @param {Function} executor.widget function used to fetch the widget instance * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new * @param {Function} executor.post_process cleanup called after a widget has been added as inner_widget * @param {Object} options * @return {Deferred<*>} */ ir_actions_common: function(executor, options) { var self = this; if (executor.action.target === 'new') { var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null; if (pre_dialog){ // prevent previous dialog to consider itself closed, // right now, as we're opening a new one (prevents // reload of original form view) pre_dialog.off('closed', null, pre_dialog.on_close); } if (this.dialog_widget && !this.dialog_widget.isDestroyed()) { this.dialog_widget.destroy(); } // explicitly passing a closing action to dialog_stop() prevents // it from reloading the original form view this.dialog_stop(executor.action); this.dialog = new Dialog(this, _.defaults(options || {}, { title: executor.action.name, dialogClass: executor.klass, buttons: [], size: executor.action.context.dialog_size, })); // chain on_close triggers with previous dialog, if any this.dialog.on_close = function(){ options.on_close.apply(null, arguments); if (pre_dialog && pre_dialog.on_close){ // no parameter passed to on_close as this will // only be called when the last dialog is truly // closing, and *should* trigger a reload of the // underlying form view (see comments above) pre_dialog.on_close(); } if (!pre_dialog) { self.dialog = null; } }; this.dialog.on("closed", null, this.dialog.on_close); this.dialog_widget = executor.widget(); var $dialogFooter; if (this.dialog_widget instanceof ViewManager) { executor.action.viewManager = this.dialog_widget; $dialogFooter = $('
'); // fake dialog footer in which view // manager buttons will be put _.defaults(this.dialog_widget.flags, { $buttons: $dialogFooter, footer_to_buttons: true, }); if (this.dialog_widget.action.view_mode === 'form') { this.dialog_widget.flags.headless = true; } } if (this.dialog_widget.need_control_panel) { // Set a fake bus to Dialogs needing a ControlPanel as they should not // communicate with the main ControlPanel this.dialog_widget.set_cp_bus(new Bus()); } this.dialog_widget.setParent(this.dialog); var fragment = document.createDocumentFragment(); return this.dialog_widget.appendTo(fragment).then(function () { var def = $.Deferred(); self.dialog.opened().then(function () { dom.append(self.dialog.$el, fragment, { in_DOM: true, callbacks: [{widget: self.dialog_widget}], }); if ($dialogFooter) { self.dialog.$footer.empty().append($dialogFooter.contents()); } if (options.state && self.dialog_widget.do_load_state) { return self.dialog_widget.do_load_state(options.state); } }) .done(def.resolve.bind(def)) .fail(def.reject.bind(def)); self.dialog.open(); return def; }).then(function () { return executor.action; }); } var def = this.inner_action && this.webclient && this.webclient.clear_uncommitted_changes() || $.when(); return def.then(function() { self.dialog_stop(executor.action); return self.push_action(executor.action.viewManager = executor.widget(), executor.action, options).then(function () { return executor.action; }); }).fail(function() { return $.Deferred().reject(); }); }, ir_actions_act_window: function (action, options) { var self = this; var flags = action.flags || {}; if (!('auto_search' in flags)) { flags.auto_search = action.auto_search !== false; } options.action = action; options.action_manager = this; var 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; } var views = action.views; return this.ir_actions_common({ widget: function () { return new ViewManager(self, dataset, views, flags, options); }, action: action, klass: 'o_act_window', }, options); }, ir_actions_client: function (action, options) { var self = this; var ClientWidget = core.action_registry.get(action.tag); if (!ClientWidget) { return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'."); } if (!(ClientWidget.prototype instanceof Widget)) { var next; if ((next = ClientWidget(this, action))) { return this.do_action(next, options); } return $.when(); } return this.ir_actions_common({ widget: function () { return new ClientWidget(self, action, options); }, action: action, klass: 'oe_act_client', }, options).then(function () { if (action.tag !== 'reload') {self.do_push_state({});} }); }, ir_actions_act_window_close: function (action, options) { if (!this.dialog) { options.on_close(); } this.dialog_stop(); // Display rainbowman on appropriate actions if (action.effect) { this.trigger_up('show_effect', action.effect); } return $.when(); }, ir_actions_server: function (action, options) { var self = this; return this._rpc({ route: '/web/action/run', params: { action_id: action.id, context: action.context || {} }, }) .then(function (action) { return self.do_action(action, options); }); }, ir_actions_report: function(action, options) { var self = this; framework.blockUI(); action = _.clone(action); var eval_contexts = ([session.user_context] || []).concat([action.context]); action.context = pyeval.eval('contexts',eval_contexts); // iOS devices doesn't allow iframe use the way we do it, // opening a new window seems the best way to workaround if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) { var params = { action: JSON.stringify(action), token: new Date().getTime() }; var url = session.url('/web/report', params); framework.unblockUI(); $('')[0].click(); return; } var c = crash_manager; return $.Deferred(function (d) { session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, complete: framework.unblockUI, success: function(){ if (!self.dialog) { options.on_close(); } self.dialog_stop(); d.resolve(); }, error: function () { c.rpc_error.apply(c, arguments); d.reject(); } }); }); }, ir_actions_act_url: function (action, options) { var url = action.url; if (session.debug && url && url.length && url[0] === '/') { url = $.param.querystring(url, {debug: session.debug}); } if (action.target === 'self') { framework.redirect(url); return $.Deferred(); // The action is finished only when the redirection is done } else { window.open(url, '_blank'); options.on_close(); } return $.when(); }, }); return ActionManager; });