diff options
Diffstat (limited to 'addons/web/static/src/js/action_manager.js')
| -rw-r--r-- | addons/web/static/src/js/action_manager.js | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js new file mode 100644 index 00000000000..4c49a7a5772 --- /dev/null +++ b/addons/web/static/src/js/action_manager.js @@ -0,0 +1,534 @@ +odoo.define('web.ActionManager', ['web.core', 'web.crash_manager', 'web.data', 'web.Dialog', 'web.framework', 'web.pyeval', 'web.session', 'web.ViewManager', 'web.Widget'], function (require) { +"use strict"; + +var core = require('web.core'); +var crash_manager = require('web.crash_manager'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +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'); + +var CompoundContext = data.CompoundContext; + +var ActionManager = Widget.extend({ + template: "ActionManager", + init: function(parent) { + this._super(parent); + this.inner_action = null; + this.inner_widget = null; + this.webclient = parent; + this.dialog = null; + this.dialog_widget = null; + this.widgets = []; + this.on('history_back', this, this.proxy('history_back')); + }, + dialog_stop: function (reason) { + if (this.dialog) { + this.dialog.destroy(reason); + } + this.dialog = null; + }, + /** + * Add a new widget to the action manager + * + * widget: typically, widgets added are instance.web.ViewManager. The action manager + * uses this list of widget to handle the breadcrumbs. + * action: new action + * options.on_reverse_breadcrumb: will be called when breadcrumb is selected + * options.clear_breadcrumbs: boolean, if true, current widgets are destroyed + * options.replace_breadcrumb: boolean, if true, replace current breadcrumb + */ + push_widget: function(widget, action, options) { + var self = this, + to_destroy, + old_widget = this.inner_widget; + options = options || {}; + + if (options.clear_breadcrumbs) { + to_destroy = this.widgets; + this.widgets = []; + } else if (options.replace_breadcrumb) { + to_destroy = _.last(this.widgets); + this.widgets = _.initial(this.widgets); + } + if (widget instanceof Widget) { + var title = widget.get('title') || action.display_name || action.name; + widget.set('title', title); + this.widgets.push(widget); + } else { + this.widgets.push({ + view_stack: [{ + controller: {get: function () {return action.display_name || action.name; }}, + }], + destroy: function () {}, + }); + } + _.last(this.widgets).__on_reverse_breadcrumb = options.on_reverse_breadcrumb; + this.inner_action = action; + this.inner_widget = widget; + return $.when(this.inner_widget.appendTo(this.$el)).done(function () { + if ((action.target !== 'inline') && (!action.flags.headless) && widget.$header) { + widget.$header.show(); + } + if (old_widget) { + old_widget.$el.hide(); + } + if (options.clear_breadcrumbs) { + self.clear_widgets(to_destroy); + } + }); + }, + get_breadcrumbs: function () { + return _.flatten(_.map(this.widgets, function (widget) { + if (widget instanceof ViewManager) { + return widget.view_stack.map(function (view, index) { + return { + title: view.controller.get('title') || widget.title, + index: index, + widget: widget, + }; + }); + } else { + return {title: widget.get('title'), widget: widget }; + } + }), true); + }, + get_title: function () { + if (this.widgets.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 widget = this.widgets[0]; + if (widget instanceof ViewManager && widget.view_stack.length === 1) { + return widget.title; + } + } + return _.pluck(this.get_breadcrumbs(), 'title').join(' / '); + }, + get_widgets: function () { + return this.widgets.slice(0); + }, + history_back: function() { + var widget = _.last(this.widgets); + if (widget instanceof ViewManager) { + var nbr_views = widget.view_stack.length; + if (nbr_views > 1) { + return this.select_widget(widget, nbr_views - 2); + } + } + if (this.widgets.length > 1) { + widget = this.widgets[this.widgets.length - 2]; + var index = widget.view_stack && widget.view_stack.length - 1; + return this.select_widget(widget, index); + } + return $.Deferred().reject(); + }, + select_widget: function(widget, index) { + var self = this; + if (this.webclient.has_uncommitted_changes()) { + return $.Deferred().reject(); + } + var widget_index = this.widgets.indexOf(widget), + def = $.when(widget.select_view && widget.select_view(index)); + + return def.done(function () { + if (widget.__on_reverse_breadcrumb) { + widget.__on_reverse_breadcrumb(); + } + _.each(self.widgets.splice(widget_index + 1), function (w) { + w.destroy(); + }); + self.inner_widget = _.last(self.widgets); + if (self.inner_widget.display_breadcrumbs) { + self.inner_widget.display_breadcrumbs(); + } + if (self.inner_widget.do_show) { + self.inner_widget.do_show(); + } + }); + }, + clear_widgets: function(widgets) { + _.invoke(widgets || this.widgets, 'destroy'); + if (!widgets) { + this.widgets = []; + this.inner_widget = null; + } + }, + do_push_state: function(state) { + if (!this.webclient || !this.webclient.do_push_state || this.dialog) { + return; + } + state = state || {}; + if (this.inner_action) { + if (this.inner_action._push_me === false) { + // this action has been explicitly marked as not pushable + return; + } + state.title = this.get_title(); + if(this.inner_action.type == 'ir.actions.act_window') { + state.model = this.inner_action.res_model; + } + if (this.inner_action.menu_id) { + state.menu_id = this.inner_action.menu_id; + } + if (this.inner_action.id) { + state.action = this.inner_action.id; + } else if (this.inner_action.type == 'ir.actions.client') { + state.action = this.inner_action.tag; + var params = {}; + _.each(this.inner_action.params, function(v, k) { + if(_.isString(v) || _.isNumber(v)) { + params[k] = v; + } + }); + state = _.extend(params || {}, state); + } + if (this.inner_action.context) { + var active_id = this.inner_action.context.active_id; + if (active_id) { + state.active_id = active_id; + } + var active_ids = this.inner_action.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 = this.inner_action.context.active_ids.join(','); + } + } + } + this.webclient.do_push_state(state); + }, + do_load_state: function(state, warm) { + var self = this, + 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 }); + $.when(action_loaded || null).done(function() { + self.webclient.menu.is_bound.done(function() { + if (self.inner_action && self.inner_action.id) { + self.webclient.menu.open_action(self.inner_action.id); + } + }); + }); + } + } + } 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: [[false, 'form']] + }; + action_loaded = this.do_action(action); + } else if (state.sa) { + // load session action + if (warm) { + this.null_action(); + } + action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).then(function(action) { + if (action) { + return self.do_action(action); + } + }); + } + + $.when(action_loaded || null).done(function() { + if (self.inner_widget && self.inner_widget.do_load_state) { + self.inner_widget.do_load_state(state, warm); + } + }); + }, + /** + * Execute an OpenERP action + * + * @param {Number|String|Object} Can be either an action id, a client action 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, + on_reverse_breadcrumb: function() {}, + hide_breadcrumb: false, + on_close: 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 self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) { + return self.do_action(result, options); + }); + } + + core.bus.trigger('action', action); + + // Ensure context & domain are evaluated and can be manipulated/used + var ncontext = new CompoundContext(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,'_'); + 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 && !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 + }); + action.menu_id = options.action_menu_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(); + } + return this[type](action, options); + }, + null_action: function() { + this.dialog_stop(); + this.clear_widgets(); + }, + /** + * + * @param {Object} executor + * @param {Object} executor.action original action + * @param {Function<instance.web.Widget>} 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<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget + * @param {Object} options + * @return {*} + */ + ir_actions_common: function(executor, options) { + var widget; + 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('closing', 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, { + title: executor.action.name, + dialogClass: executor.klass, + }); + + // 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(); + } + }; + this.dialog.on("closing", null, this.dialog.on_close); + widget = executor.widget(); + if (widget instanceof ViewManager) { + _.extend(widget.flags, { + $buttons: this.dialog.$buttons, + footer_to_buttons: true, + }); + if (widget.action.view_mode === 'form') { + widget.flags.headless = true; + } + } + this.dialog_widget = widget; + this.dialog_widget.setParent(this.dialog); + var initialized = this.dialog_widget.appendTo(this.dialog.$el); + this.dialog.open(); + return $.when(initialized); + } + if (this.inner_widget && this.webclient.has_uncommitted_changes()) { + return $.Deferred().reject(); + } + widget = executor.widget(); + this.dialog_stop(executor.action); + return this.push_widget(widget, executor.action, options); + }, + ir_actions_act_window: function (action, options) { + var self = this; + + return this.ir_actions_common({ + widget: function () { + return new ViewManager(self, null, null, null, action); + }, + action: action, + klass: 'oe_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); }, + 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(); + return $.when(); + }, + ir_actions_server: function (action, options) { + var self = this; + this.rpc('/web/action/run', { + action_id: action.id, + context: action.context || {} + }).done(function (action) { + self.do_action(action, options); + }); + }, + ir_actions_report_xml: 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 = self.session.url('/web/report', params); + framework.unblockUI(); + $('<a href="'+url+'" target="_blank"></a>')[0].click(); + return; + } + var c = crash_manager; + return $.Deferred(function (d) { + self.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) { + if (action.target === 'self') { + framework.redirect(action.url); + } else { + window.open(action.url, '_blank'); + } + return $.when(); + }, +}); + +return ActionManager; + +}); |
