summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/action_manager.js
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2015-03-09 16:34:27 +0100
committerGéry Debongnie <ged@odoo.com>2015-03-18 09:01:49 +0100
commite8a00bc50da435d3859118ca127d20a2eb9a0cab (patch)
tree039f6ec40b37392e76c8830a8e518ac40f7e7fec /addons/web/static/src/js/action_manager.js
parent9036084f065c7aca3b2199903f4da67db3501ba7 (diff)
[REF] web: add module system to the web client
This commit introduces a new module system for the javascript code. Instead of using global (namespaced) variables everywhere (like openerp.web.whatever, ...), each file declare one (or more) module with its dependencies. Each module will be loaded after its dependencies, and will be given a reference to the return values. This change is a massive departure from the way the client worked previously. It will sadly break some/most history, but I am sure that the future git ecosystem will allow you to easily browse the history past this revision (one can dream). This commit feature: * main namespace is now odoo instead of openerp * a way to define modules and dependencies (inspired by requirejs) * many files have been splitted in simpler subfiles * basic support for debugging in odoo.__DEBUG__ * no global variable (i.e. nothing in window.openerp) * many bug fixes, many new bugs Future vision: this modularization should simplify future code development, simplify debugging and will allow a serious testing effort
Diffstat (limited to 'addons/web/static/src/js/action_manager.js')
-rw-r--r--addons/web/static/src/js/action_manager.js534
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;
+
+});