2018-01-16 11:34:37 +01:00
|
|
|
flectra.define('web.ActionManager', function (require) {
|
2018-01-16 06:58:15 +01:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ActionManager
|
|
|
|
*
|
2018-01-16 11:34:37 +01:00
|
|
|
* The action manager is quite important: it makes sure that actions (the Flectra
|
2018-01-16 06:58:15 +01:00
|
|
|
* 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() {
|
2018-04-05 10:25:40 +02:00
|
|
|
// DO NOT FORWARDPORT THIS
|
|
|
|
if (this.inner_action && !$('.dialog').length) {
|
2018-01-16 06:58:15 +01:00
|
|
|
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<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 {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 = $('<div/>'); // 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();
|
|
|
|
$('<a href="'+url+'" target="_blank"></a>')[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;
|
|
|
|
|
|
|
|
});
|