summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js55
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js1561
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js756
-rw-r--r--addons/web/static/src/js/chrome/control_panel.js27
-rw-r--r--addons/web/static/src/js/chrome/menu.js15
-rw-r--r--addons/web/static/src/js/chrome/search_menus.js11
-rw-r--r--addons/web/static/src/js/chrome/search_view.js6
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js671
-rw-r--r--addons/web/static/src/js/chrome/web_client.js15
-rw-r--r--addons/web/static/src/js/core/dialog.js4
-rw-r--r--addons/web/static/src/js/core/misc.js11
-rw-r--r--addons/web/static/src/js/report/client_action.js1
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js170
-rw-r--r--addons/web/static/src/js/views/abstract_view.js25
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js22
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js4
-rw-r--r--addons/web/static/src/js/views/form/form_view.js10
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js2
-rw-r--r--addons/web/static/src/js/views/list/list_view.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js9
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js145
21 files changed, 1854 insertions, 1670 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 45a2b0b1e8c..c491297abfc 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -36,8 +36,8 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
toggle_fullscreen: function (event) {
this.toggle_fullscreen(event.data.fullscreen);
},
- current_action_updated: function (e) {
- this.current_action_updated(e.data.action);
+ current_action_updated: function (ev) {
+ this.current_action_updated(ev.data.action, ev.data.controller);
},
// GENERIC SERVICES
// the next events are dedicated to generic services required by
@@ -51,6 +51,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
}
},
warning: '_onDisplayWarning',
+ load_action: '_onLoadAction',
load_views: function (event) {
var params = {
model: event.data.modelName,
@@ -66,6 +67,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
.load_filters(event.data.dataset, event.data.action_id)
.then(event.data.on_success);
},
+ push_state: '_onPushState',
show_effect: '_onShowEffect',
// session
get_session: function (event) {
@@ -199,7 +201,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
};
},
set_action_manager: function () {
- this.action_manager = new ActionManager(this, {webclient: this});
+ this.action_manager = new ActionManager(this, session.user_context);
return this.action_manager.appendTo(this.$('.o_main_content'));
},
set_notification_manager: function () {
@@ -213,11 +215,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
show_application: function () {
},
clear_uncommitted_changes: function () {
- var def = $.Deferred().resolve();
- core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) {
- def = def.then(callback);
- });
- return def;
+ return this.action_manager.clearUncommittedChanges();
},
destroy_content: function () {
_.each(_.clone(this.getChildren()), function (el) {
@@ -264,7 +262,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
* This allows to widgets that are not inside the ActionManager to perform do_action
*/
do_action: function () {
- return this.action_manager.do_action.apply(this, arguments);
+ return this.action_manager.doAction.apply(this.action_manager, arguments);
},
do_reload: function () {
var self = this;
@@ -274,6 +272,9 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
});
},
do_push_state: function (state) {
+ if (!state.menu_id && this.menu) { // this.menu doesn't exist in the POS
+ state.menu_id = this.menu.getCurrentPrimaryMenu();
+ }
if ('title' in state) {
this.set_title(state.title);
delete state.title;
@@ -304,13 +305,14 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
this.connection_notification = false;
}
},
- // Handler to be overwritten
- current_action_updated: function () {
- },
- // --------------------------------------------------------------
- // Scrolltop handling
- // --------------------------------------------------------------
- getScrollTop: function () {
+ /**
+ * Handler to be overridden, called each time the UI is updated by the
+ * ActionManager.
+ *
+ * @param {Object} action the action of the currently displayed controller
+ * @param {Object} controller the currently displayed controller
+ */
+ current_action_updated: function (action, controller) {
},
//--------------------------------------------------------------
// Misc.
@@ -347,6 +349,27 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
}
},
/**
+ * Loads an action from the database given its ID.
+ *
+ * @private
+ * @param {OdooEvent} event
+ * @param {integer} event.data.actionID
+ * @param {Object} event.data.context
+ * @param {function} event.data.on_success
+ */
+ _onLoadAction: function (event) {
+ data_manager
+ .load_action(event.data.actionID, event.data.context)
+ .then(event.data.on_success);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} e
+ */
+ _onPushState: function (e) {
+ this.do_push_state(e.data.state);
+ },
+ /**
* Displays a visual effect (for example, a rainbowman0
*
* @private
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index e72db2c3a0c..ef2baaaad38 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -4,957 +4,886 @@ odoo.define('web.ActionManager', function (require) {
/**
* ActionManager
*
- * The action manager is quite important: it makes sure that actions (the Odoo
- * objects, such as a client action, or a act_window) are properly started,
- * and coordinated.
+ * The ActionManager is one of the centrepieces in the WebClient architecture.
+ * Its role is to makes sure that Odoo actions are properly started and
+ * coordinated.
*/
var Bus = require('web.Bus');
-var ControlPanel = require('web.ControlPanel');
+var concurrency = require('web.concurrency');
var Context = require('web.Context');
+var ControlPanel = require('web.ControlPanel');
+var config = require('web.config');
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;
+var ActionManager = Widget.extend({
+ className: 'o_content',
+ custom_events: {
+ breadcrumb_clicked: '_onBreadcrumbClicked',
+ history_back: '_onHistoryBack',
+ push_state: '_onPushState',
},
+
/**
- * @return {Object} dictionnary that will be interpreted to display the breadcrumbs
+ * @override
+ * @param {Object} [userContext={}]
*/
- get_breadcrumbs: function() {
- return { title: this.title, action: this };
+ init: function (parent, userContext) {
+ this._super.apply(this, arguments);
+ this.userContext = userContext || {};
+
+ // use a DropPrevious to drop previous actions when multiple actions are
+ // run simultaneously
+ this.dp = new concurrency.DropPrevious();
+
+ // 'actions' is an Object that registers the actions that are currently
+ // handled by the ActionManager (either stacked in the current window,
+ // or opened in dialogs)
+ this.actions = {};
+
+ // 'controllers' is an Object that registers the alive controllers
+ // linked registered actions, a controller being Object with keys
+ // (amongst others) 'jsID' (a local identifier) and 'widget' (the
+ // instance of the controller's widget)
+ this.controllers = {};
+
+ // 'controllerStack' is the stack of ids of the controllers currently
+ // displayed in the current window
+ this.controllerStack = [];
+
+ // 'currentDialogController' is the current controller opened in a
+ // dialog (i.e. coming from an action with target='new')
+ this.currentDialogController = null;
},
+
/**
- * @return {int} the number of views stacked, i.e. 0 for client functions
+ * @override
*/
- get_nb_views: function() {
- return 0;
+ start: function () {
+ // AAB: temporarily instantiate a unique main ControlPanel used by
+ // controllers in the controllerStack
+ this.controlPanel = new ControlPanel(this);
+ var def = this.controlPanel.insertBefore(this.$el);
+
+ return $.when(def, this._super.apply(this, arguments));
},
/**
- * @return {jQuery} the DOM fragment of the action
+ * Called each time the action manager is attached into the DOM.
*/
- get_fragment: function() {
- return this.$fragment;
+ on_attach_callback: function() {
+ this.isInDOM = true;
+ var currentController = this.getCurrentController();
+ if (currentController && currentController.widget.on_attach_callback) {
+ currentController.widget.on_attach_callback();
+ }
},
/**
- * @return {string} the active view, i.e. empty for client actions
+ * Called each time the action manager is detached from the DOM.
*/
- get_active_view: function() {
- return '';
+ on_detach_callback: function() {
+ this.isInDOM = false;
+ var currentController = this.getCurrentController();
+ if (currentController && currentController.widget.on_detach_callback) {
+ currentController.widget.on_detach_callback();
+ }
},
-});
-/**
- * Specialization of Action for client actions that are Widgets
- */
-var WidgetAction = Action.extend({
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
/**
- * Initializes the WidgetAction
- * Sets the title of the widget
+ * This function is called when the current controller is about to be
+ * removed from the DOM, because a new one will be pushed, or an old one
+ * will be restored. It ensures that the current controller can be left (for
+ * instance, that it has no unsaved changes).
+ *
+ * @returns {Deferred} resolved if the current controller can be left,
+ * rejected otherwise.
*/
- init: function(action, widget) {
- this._super(action);
-
- this.widget = widget;
- if (!this.widget.get('title')) {
- this.widget.set('title', this.title);
+ clearUncommittedChanges: function () {
+ var currentController = this.getCurrentController();
+ // AAB: with AbstractAction, the second part of the condition won't be
+ // necessary anymore, as there will be such a function it its API
+ if (currentController && currentController.widget.discardChanges) {
+ return currentController.widget.discardChanges(undefined, {
+ // AAB: get rid of this option when on_hashchange mechanism is improved
+ readonlyIfRealDiscard: true,
+ });
}
- this.widget.on('change:title', this, function(widget) {
- this.title = widget.get('title');
- });
+ return $.when();
},
/**
- * Restores WidgetAction by calling do_show on its widget
- */
- restore: function() {
+ * This is the entry point to execute Odoo actions, given as an ID in
+ * database, an xml ID, a client action tag or an action descriptor.
+ *
+ * @param {number|string|Object} action the action to execute
+ * @param {Object} [options]
+ * @param {Object} [options.additional_context] additional context to be
+ * merged with the action's context.
+ * @param {boolean} [options.clear_breadcrumbs=false] set to true to clear
+ * the breadcrumbs history list
+ * @param {Function} [options.on_close] callback to be executed when the
+ * current action is active again (typically, if the new action is
+ * executed in target="new", on_close will be executed when the dialog is
+ * closed, if the current controller is still active)
+ * @param {Function} [options.on_reverse_breadcrumb] callback to be executed
+ * whenever an anterior breadcrumb item is clicked on
+ * @param {boolean} [options.pushState=true] set to false to prevent the
+ * ActionManager from pushing the state when the action is executed (this
+ * is useful when we come from a loadState())
+ * @param {boolean} [options.replace_last_action=false] set to true to
+ * replace last part of the breadcrumbs with the action
+ * @return {Deferred} resolved when the action is loaded and appended to the
+ * DOM ; rejected if the action can't be executed (e.g. if doAction has
+ * been called to execute another action before this one was complete).
+ */
+ doAction: function (action, options) {
var self = this;
- return $.when(this._super()).then(function() {
- return self.widget.do_show();
+ options = _.defaults({}, options, {
+ additional_context: {},
+ clear_breadcrumbs: false,
+ on_close: function () {},
+ on_reverse_breadcrumb: function () {},
+ pushState: true,
+ replace_last_action: false,
});
- },
- /**
- * 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();
+ // build or load an action descriptor for the given action
+ var def;
+ if (_.isString(action) && core.action_registry.contains(action)) {
+ // action is a tag of a client action
+ action = { type: 'ir.actions.client', tag: action };
+ } else if (_.isNumber(action) || _.isString(action)) {
+ // action is an id or xml id
+ def = this._loadAction(action, {
+ active_id: options.additional_context.active_id,
+ active_ids: options.additional_context.active_ids,
+ active_model: options.additional_context.active_model,
+ }).then(function (result) {
+ action = result;
+ });
+ }
+
+ return this.dp.add($.when(def)).then(function () {
+ action.jsID = _.uniqueId('action_');
+ action.pushState = options.pushState;
+
+ // action.target 'main' is equivalent to 'current' except that it
+ // also clears the breadcrumbs
+ options.clear_breadcrumbs = action.target === 'main' ||
+ options.clear_breadcrumbs;
+
+ // ensure that the context and domain are evaluated
+ var context = new Context(self.userContext, options.additional_context, action.context);
+ action.context = pyeval.eval('context', context);
+ if (action.domain) {
+ action.domain = pyeval.eval('domain', action.domain, action.context);
+ }
+
+ return self._handleAction(action, options);
});
},
/**
- * 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
+ * Compatibility with client actions that are still using do_push_state.
+ *
+ * @todo: convert all of them to trigger_up('push_state') instead.
+ * @param {Object} state
*/
- set_on_reverse_breadcrumb: function(callback, scrollTop) {
- this._super(callback);
- this.setScrollTop(scrollTop);
+ do_push_state: function (state) {
+ this.trigger_up('push_state', {state: state});
},
/**
- * Sets the scroll position of the widgets's active_view
- * @todo: replace this with a generic get/set local state mechanism.
- * @see getScrollTop
+ * Returns the action of the last controller in the controllerStack, i.e.
+ * the action of the currently displayed controller in the main window (not
+ * in a dialog), and null if there is no controller in the stack.
*
- * @override
- * @param {integer} [scrollTop] the number of pixels to scroll
+ * @returns {Object|null}
*/
- setScrollTop: function (scrollTop) {
- var activeView = this.widget.active_view;
- var viewController = activeView && activeView.controller;
- if (viewController) {
- viewController.setScrollTop(scrollTop);
- }
+ getCurrentAction: function () {
+ var controller = this.getCurrentController();
+ return controller ? this.actions[controller.actionID] : null;
},
/**
- * 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.
+ * Returns the last controller in the controllerStack, i.e. the currently
+ * displayed controller in the main window (not in a dialog), and
+ * null if there is no controller in the stack.
*
- * @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
+ * @returns {Object|null}
*/
- getScrollTop: function () {
- var activeView = this.widget.active_view;
- var viewController = activeView && activeView.controller;
- return viewController ? viewController.getScrollTop() : 0;
+ getCurrentController: function () {
+ var currentControllerID = _.last(this.controllerStack);
+ return currentControllerID ? this.controllers[currentControllerID] : null;
},
/**
- * @return {Array} array of Objects that will be interpreted to display the breadcrumbs
+ * Updates the UI according to the given state, for instance, executes a new
+ * action, or updates the state of the current action.
+ *
+ * @param {Object} state
+ * @param {integer|string} [state.action] the action to execute (given its
+ * id or tag for client actions)
+ * @returns {Deferred} resolved when the UI has been updated
*/
- 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,
+ loadState: function (state) {
+ var action;
+ if (!state.action) {
+ return $.when();
+ }
+ if (_.isString(state.action) && core.action_registry.contains(state.action)) {
+ action = {
+ params: state,
+ tag: state.action,
+ type: 'ir.actions.client',
};
+ } else {
+ action = state.action;
+ }
+ return this.doAction(action, {
+ clear_breadcrumbs: true,
+ pushState: false,
});
},
- /**
- * @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',
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * Called each time the action manager is attached into the DOM
+ * Appends the given controller to the DOM and restores its scroll position.
+ * Also updates the control panel.
+ *
+ * @private
+ * @param {Object} controller
*/
- 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();
+ _appendController: function (controller) {
+ dom.append(this.$el, controller.widget.$el, {
+ in_DOM: this.isInDOM,
+ callbacks: [{widget: controller.widget}],
+ });
+
+ this.trigger_up('scrollTo', {offset: controller.scrollTop || 0});
+
+ if (!controller.widget.need_control_panel) {
+ this.controlPanel.do_hide();
+ } else {
+ this.controlPanel.update({
+ breadcrumbs: this._getBreadcrumbs(),
+ }, {clear: false});
}
},
/**
- * Called each time the action manager is detached from the DOM
+ * Closes the current dialog, if any. Because we listen to the 'closed'
+ * event triggered by the dialog when it is closed, this also destroys the
+ * embedded controller and removes the reference to the corresponding action.
+ * This also executes the 'on_close' handler in some cases.
+ *
+ * @private
+ * @param {boolean} [silent=false] if true, the 'on_close' handler won't be
+ * called ; this is in general the case when the current dialog is closed
+ * because another action is opened, so we don't want the former action
+ * to execute its handler as it won't be displayed anyway
*/
- 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();
+ _closeDialog: function (silent) {
+ if (this.currentDialogController) {
+ this.currentDialogController.dialog.destroy(silent);
}
},
- 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() {
- if (this.inner_action) {
- 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);
+ /**
+ * Detaches the current controller from the DOM and stores its scroll
+ * position, in case we'd come back to that controller later.
+ *
+ * @private
+ */
+ _detachCurrentController: function () {
+ var currentController = this.getCurrentController();
+ if (currentController) {
+ currentController.scrollTop = this._getScrollTop();
+ dom.detach([{widget: currentController.widget}]);
}
- this.dialog = null;
},
/**
- * Add a new action to the action manager
+ * Executes actions for which a controller has to be appended to the DOM,
+ * either in the main content (target="current", by default), or in a dialog
+ * (target="new").
*
- * @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
+ * @private
+ * @param {Object} action
+ * @param {widget} action.controller a Widget instance to append to the DOM
+ * @param {string} [action.target="current"] set to "new" to render the
+ * controller in a dialog
+ * @param {Object} options @see doAction for details
+ * @returns {Deferred} resolved when the controller is started and appended
*/
- push_action: function(widget, action_descr, options) {
+ _executeAction: function (action, 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 || {};
+ this.actions[action.jsID] = action;
- // 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()];
+ if (action.target === 'new') {
+ return this._executeActionInDialog(action, options);
}
- // 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);
- }
+ return this.clearUncommittedChanges()
+ .then(function () {
+ var controller = self.controllers[action.controllerID];
+ var widget = controller.widget;
+ // AAB: this will be moved to the Controller
+ if (widget.need_control_panel) {
+ // set the ControlPanel bus on the controller to allow it to
+ // communicate its status
+ widget.set_cp_bus(self.controlPanel.get_bus());
+ }
+ return self._startController(controller);
+ })
+ .then(function (controller) {
+ if (self.currentDialogController) {
+ self._closeDialog(true);
+ }
- // 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());
- }
+ // store the optional 'on_reverse_breadcrumb' handler
+ // AAB: store it on the AbstractAction instance, and call it
+ // automatically when the action is restored
+ if (options.on_reverse_breadcrumb) {
+ var currentAction = self.getCurrentAction();
+ if (currentAction) {
+ currentAction.on_reverse_breadcrumb = options.on_reverse_breadcrumb;
+ }
+ }
+
+ // update the internal state and the DOM
+ self._pushController(controller, options);
- // 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;
+ // toggle the fullscreen mode for actions in target='fullscreen'
+ self._toggleFullscreen();
+ return action;
+ })
+ .fail(function () {
+ self._removeAction(action.jsID);
+ });
+ },
+ /**
+ * Executes actions with attribute target='new'. Such actions are rendered
+ * in a dialog.
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} options @see doAction for details
+ * @returns {Deferred} resolved when the controller is rendered inside a
+ * dialog appended to the DOM
+ */
+ _executeActionInDialog: function (action, options) {
+ var self = this;
+ var controller = this.controllers[action.controllerID];
+ var widget = controller.widget;
+ // AAB: this will be moved to the Controller
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());
+ // set the ControlPanel bus on the controller to allow it to
+ // communicate its status
+ widget.set_cp_bus(new 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();
+ return this._startController(controller).then(function (controller) {
+ if (self.currentDialogController) {
+ self._closeDialog(true);
}
- // 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}],
+ var dialog = new Dialog(self, _.defaults({}, options, {
+ buttons: [],
+ dialogClass: controller.className,
+ title: action.name,
+ size: action.context.dialog_size,
+ }));
+ dialog.on('closed', self, function (silent) {
+ self._removeAction(action.jsID);
+ self.currentDialogController = null;
+ if (silent !== true) {
+ options.on_close();
+ }
+ });
+ controller.dialog = dialog;
+
+ return dialog.open().opened(function () {
+ dom.append(dialog.$el, widget.$el, {
+ in_DOM: true,
+ callbacks: [{widget: dialog}],
+ });
+ // AAB: renderButtons will be a function of AbstractAction, so this
+ // test won't be necessary anymore
+ if (widget.renderButtons) {
+ widget.renderButtons(dialog.$footer);
+ }
+
+ self.currentDialogController = controller;
+
+ return action;
});
- 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;
+ self._removeAction(action.jsID);
});
},
- setScrollTop: function(scrollTop) {
- if (this.inner_action) {
- this.inner_action.setScrollTop(scrollTop);
+ /**
+ * Executes actions of type 'ir.actions.client'.
+ *
+ * @private
+ * @param {Object} action the description of the action to execute
+ * @param {string} action.tag the key of the action in the action_registry
+ * @param {Object} options @see doAction for details
+ * @returns {Deferred} resolved when the client action has been executed
+ */
+ _executeClientAction: function (action, options) {
+ var self = this;
+ var ClientAction = core.action_registry.get(action.tag);
+ if (!ClientAction) {
+ console.error("Could not find client action " + action.tag, action);
+ return $.Deferred().reject();
}
- },
- 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;
+ if (!(ClientAction.prototype instanceof Widget)) {
+ // the client action might be a function, which is executed and
+ // whose returned value might be another action to execute
+ var next = ClientAction(this, action);
+ if (next) {
+ return this.doAction(next, options);
}
+ return $.when();
}
- 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;
+
+ var controllerID = _.uniqueId('controller_');
+ var controller = {
+ actionID: action.jsID,
+ jsID: controllerID,
+ widget: new ClientAction(this, action, options),
+ };
+ // AAB: TODO: simplify this with AbstractAction (implement a getTitle
+ // function that returns action.name by default, and that can be
+ // overriden in client actions and view controllers)
+ Object.defineProperty(controller, 'title', {
+ get: function () {
+ return controller.widget.get('title') || action.display_name || action.name;
+ },
+ });
+ this.controllers[controllerID] = controller;
+ action.controllerID = controllerID;
+ return this._executeAction(action, options).done(function () {
+ // AAB: this should be done automatically in AbstractAction, so that
+ // it can be overriden by actions that have specific stuff to push
+ // (e.g. Discuss, Views)
+ self._pushState(controllerID, {});
+ });
},
- 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);
+ /**
+ * Executes actions of type 'ir.actions.act_window_close', i.e. closes the
+ * last opened dialog.
+ *
+ * @private
+ * @param {Object} action
+ * @returns {Deferred} resolved immediately
+ */
+ _executeCloseAction: function (action, options) {
+ if (!this.currentDialogController) {
+ options.on_close();
}
- else if (nbActions === 1 && nbViews === 1) {
- return this.select_action(this.action_stack[0], 0);
+
+ this._closeDialog();
+
+ // display some effect (like rainbowman) on appropriate actions
+ if (action.effect) {
+ this.trigger_up('show_effect', action.effect);
}
- return $.Deferred().reject();
+
+ return $.when();
},
- select_action: function(action, index) {
+ /**
+ * Executes actions of type 'ir.actions.server'.
+ *
+ * @private
+ * @param {Object} action the description of the action to execute
+ * @param {integer} action.id the db ID of the action to execute
+ * @param {Object} [action.context]
+ * @param {Object} options @see doAction for details
+ * @returns {Deferred} resolved when the action has been executed
+ */
+ _executeServerAction: function (action, options) {
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();
+ var runDef = this._rpc({
+ route: '/web/action/run',
+ params: {
+ action_id: action.id,
+ context: action.context || {},
+ },
});
- },
- clear_action_stack: function(action_stack) {
- _.map(action_stack || this.action_stack, function(action) {
- action.destroy();
+ return this.dp.add(runDef).then(function (action) {
+ return self.doAction(action, options);
});
- if (!action_stack) {
- this.action_stack = [];
- this.inner_action = null;
- this.inner_widget = null;
+ },
+ /**
+ * Executes actions of type 'ir.actions.act_url', i.e. redirects to the
+ * given url.
+ *
+ * @private
+ * @param {Object} action the description of the action to execute
+ * @param {string} action.url
+ * @param {string} [action.target] set to 'self' to redirect in the current page,
+ * redirects to a new page by default
+ * @param {Object} options @see doAction for details
+ * @returns {Deferred} resolved when the redirection is done (immediately
+ * when redirecting to a new page)
+ */
+ _executeURLAction: function (action, options) {
+ var url = action.url;
+ if (config.debug && url && url.length && url[0] === '/') {
+ url = $.param.querystring(url, {debug: config.debug});
+ }
+
+ if (action.target === 'self') {
+ framework.redirect(url);
+ return $.Deferred();
+ } else {
+ window.open(url, '_blank');
}
- this.toggle_fullscreen();
+
+ options.on_close();
+
+ return $.when();
},
- toggle_fullscreen: function() {
- var is_fullscreen = _.some(this.action_stack, function(action) {
- return action.action_descr.target === "fullscreen";
+ /**
+ * Returns a description of the current stack of controllers, used to render
+ * the breadcrumbs. It is an array of Objects with keys 'title' (what to
+ * display in the breadcrumbs) and 'controllerID' (the ID of the
+ * corresponding controller, used to restore it when this part of the
+ * breadcrumbs is clicked).
+ * Ignores the content of the stack of controllers if the action of the
+ * last controller of the stack is flagged with 'no_breadcrumbs', indicating
+ * that the breadcrumbs should not be displayed for that action.
+ *
+ * @private
+ * @returns {Object[]}
+ */
+ _getBreadcrumbs: function () {
+ var self = this;
+ var currentController = this.getCurrentController();
+ var noBreadcrumbs = !currentController ||
+ this.actions[currentController.actionID].context.no_breadcrumbs;
+ if (noBreadcrumbs) {
+ return [];
+ }
+ return _.map(this.controllerStack, function (controllerID) {
+ return {
+ title: self.controllers[controllerID].title,
+ controllerID: controllerID,
+ };
});
- this.trigger_up("toggle_fullscreen", {fullscreen: is_fullscreen});
},
- do_push_state: function(state) {
- if (!this.webclient || this.dialog) {
- return;
+ /**
+ * Returns an object containing information about the given controller, like
+ * its title, its action's id, the active_id and active_ids of the action...
+ *
+ * @private
+ * @param {string} controllerID
+ * @returns {Object}
+ */
+ _getControllerState: function (controllerID) {
+ var controller = this.controllers[controllerID];
+ var action = this.actions[controller.actionID];
+ var state = {
+ title: controller.title,
+ };
+ if (action.id) {
+ state.action = action.id;
+ } else if (action.type === 'ir.actions.client') {
+ state.action = action.tag;
+ var params = _.pick(action.params, function (v) {
+ return _.isString(v) || _.isNumber(v);
+ });
+ state = _.extend(params || {}, state);
}
- 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 (action.context) {
+ var active_id = action.context.active_id;
+ if (active_id) {
+ state.active_id = active_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(',');
- }
+ var active_ids = action.context.active_ids;
+ // we don't push active_ids if it's a single element array containing the active_id
+ // to make the url shorter in most cases
+ if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
+ state.active_ids = action.context.active_ids.join(',');
}
}
- this.webclient.do_push_state(state);
+ return 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);
+ /**
+ * Returns the current vertical scroll position.
+ *
+ * @private
+ * @returns {integer}
+ */
+ _getScrollTop: function () {
+ var scrollTop;
+ this.trigger_up('getScrollTop', {
+ callback: function (value) {
+ scrollTop = value;
}
});
+ return scrollTop;
},
/**
- * Execute an OpenERP action
+ * Dispatches the given action to the corresponding handler to execute it,
+ * according to its type. This function can be overriden to extend the
+ * range of supported action types.
*
- * @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 || {});
- }
-
+ * @private
+ * @param {Object} action
+ * @param {string} action.type
+ * @param {Object} options
+ * @returns {Deferred} resolved when the action has been executed ; rejected
+ * if the type of action isn't supported, or if the action can't be
+ * executed
+ */
+ _handleAction: function (action, options) {
if (!action.type) {
console.error("No type for action", action);
return $.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();
+ switch (action.type) {
+ case 'ir.actions.act_url':
+ return this._executeURLAction(action, options);
+ case 'ir.actions.act_window_close':
+ return this._executeCloseAction(action, options);
+ case 'ir.actions.client':
+ return this._executeClientAction(action, options);
+ case 'ir.actions.server':
+ return this._executeServerAction(action, options);
+ default:
+ console.error("The ActionManager can't handle actions of type " +
+ action.type, action);
+ return $.Deferred().reject();
}
+ },
+ /**
+ * Updates the internal state and the DOM with the given controller as
+ * current controller.
+ *
+ * @private
+ * @param {Object} controller
+ * @param {string} controller.jsID
+ * @param {Widget} controller.widget
+ * @param {Object} [options]
+ * @param {Object} [options.clear_breadcrumbs=false] if true, destroys all
+ * controllers from the controller stack before adding the given one
+ * @param {Object} [options.replace_last_action=false] if true, replaces the
+ * last controller of the controller stack by the given one
+ * @param {integer} [options.index] if given, pushes the controller at that
+ * position in the controller stack, and destroys the controllers with an
+ * higher index
+ */
+ _pushController: function (controller, options) {
+ options = options || {};
+ var self = this;
- // 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,
+ // detach the current controller
+ this._detachCurrentController();
+
+ // empty the controller stack or replace the last controller as requested,
+ // destroy the removed controllers and push the new controller to the stack
+ var toDestroy;
+ if (options.clear_breadcrumbs) {
+ toDestroy = this.controllerStack;
+ this.controllerStack = [];
+ } else if (options.replace_last_action && this.controllerStack.length > 0) {
+ toDestroy = [this.controllerStack.pop()];
+ } else if (options.index !== undefined) {
+ toDestroy = this.controllerStack.splice(options.index);
+ // reject from the list of controllers to destroy the one that we are
+ // currently pushing, or those linked to the same action as the one
+ // linked to the controller that we are pushing
+ toDestroy = _.reject(toDestroy, function (controllerID) {
+ return controllerID === controller.jsID ||
+ self.controllers[controllerID].actionID === controller.actionID;
});
}
+ this._removeControllers(toDestroy);
+ this.controllerStack.push(controller.jsID);
- return $.when(this[type](action, options)).then(function (executor_action) {
- options.on_load(executor_action);
- return action;
+ // append the new controller to the DOM
+ this._appendController(controller);
+
+ // notify the environment of the new action
+ this.trigger_up('current_action_updated', {
+ action: this.getCurrentAction(),
+ controller: controller,
});
},
- null_action: function() {
- this.dialog_stop();
- this.clear_action_stack();
- },
/**
+ * Pushes the given state, with additional information about the given
+ * controller, like the action's id and the controller's title.
*
- * @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<*>}
+ * @private
+ * @param {string} controllerID
+ * @param {Object} [state={}]
*/
- 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());
+ _pushState: function (controllerID, state) {
+ var controller = this.controllers[controllerID];
+ if (controller) {
+ var action = this.actions[controller.actionID];
+ if (action.target === 'new' || action.pushState === false) {
+ // do not push state for actions in target="new" or for actions
+ // that have been explicitly marked as not pushable
+ return;
}
- 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;
- });
+ state = _.extend({}, state, this._getControllerState(controller.jsID));
+ this.trigger_up('push_state', {state: state});
}
- 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();
+ },
+ /**
+ * Loads an action from the database given its ID.
+ *
+ * @todo: turn this in a service (DataManager)
+ * @private
+ * @param {integer|string} action's ID or xml ID
+ * @param {Object} context
+ * @returns {Deferred<Object>} resolved with the description of the action
+ */
+ _loadAction: function (actionID, context) {
+ var def = $.Deferred();
+ this.trigger_up('load_action', {
+ actionID: actionID,
+ context: context,
+ on_success: def.resolve.bind(def),
});
+ return def;
},
- 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);
+ /**
+ * Unlinks the given action and its controller from the internal structures
+ * and destroys its controllers.
+ *
+ * @private
+ * @param {string} actionID the id of the action to remove
+ */
+ _removeAction: function (actionID) {
+ var action = this.actions[actionID];
+ var controller = this.controllers[action.controllerID];
+ delete this.actions[action.jsID];
+ delete this.controllers[action.controllerID];
+ controller.widget.destroy();
},
- ir_actions_client: function (action, options) {
+ /**
+ * Removes the given controllers and their corresponding actions.
+ *
+ * @see _removeAction
+ * @private
+ * @param {string[]} controllerIDs
+ */
+ _removeControllers: function (controllerIDs) {
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({});}
+ var actionsToRemove = _.map(controllerIDs, function (controllerID) {
+ return self.controllers[controllerID].actionID;
});
+ _.each(_.uniq(actionsToRemove), this._removeAction.bind(this));
},
- 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) {
+ /**
+ * Restores a controller from the controllerStack and destroys all
+ * controllers stacked over the given controller (called when coming back
+ * using the breadcrumbs).
+ *
+ * @private
+ * @param {string} controllerID
+ * @returns {Deferred} resolved when the controller has been restored
+ */
+ _restoreController: function (controllerID) {
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);
+ var controller = this.controllers[controllerID];
+ // AAB: AbstractAction should define a proper hook to execute code when
+ // it is restored (other than do_show), and it should return a deferred
+ var action = this.actions[controller.actionID];
+ var def;
+ if (action.on_reverse_breadcrumb) {
+ def = action.on_reverse_breadcrumb();
+ }
+ return $.when(def).then(function () {
+ return $.when(controller.widget.do_show()).then(function () {
+ var index = _.indexOf(self.controllerStack, controllerID);
+ self._pushController(controller, {index: index});
});
+ });
+ },
+ /**
+ * Starts the controller by appending it in a document fragment, so that it
+ * is ready when it will be appended to the DOM. This allows to prevent
+ * flickering for widgets doing async stuff in willStart() or start().
+ *
+ * @private
+ * @param {Object} controller
+ * @returns {Deferred<Object>} resolved with the controller when it is ready
+ */
+ _startController: function (controller) {
+ var fragment = document.createDocumentFragment();
+ return controller.widget.appendTo(fragment).then(function () {
+ return controller;
+ });
},
- ir_actions_report: function(action, options) {
+ /**
+ * Toggles the fullscreen mode if there is an action in target='fullscreen'
+ * in the current stack.
+ *
+ * @private
+ */
+ _toggleFullscreen: function () {
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();
- }
- });
+ var fullscreen = _.some(this.controllerStack, function (controllerID) {
+ var controller = self.controllers[controllerID];
+ return self.actions[controller.actionID].target === 'fullscreen';
});
+ this.trigger_up('toggle_fullscreen', {fullscreen: fullscreen});
},
- 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
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {OdooEvent} ev.data.controllerID
+ */
+ _onBreadcrumbClicked: function (ev) {
+ ev.stopPropagation();
+ this._restoreController(ev.data.controllerID);
+ },
+ /**
+ * Goes back in the history: if a controller is opened in a dialog, closes
+ * the dialog, otherwise, restores the second to last controller from the
+ * stack.
+ *
+ * @private
+ */
+ _onHistoryBack: function () {
+ if (this.currentDialogController) {
+ this._closeDialog();
} else {
- window.open(url, '_blank');
- options.on_close();
+ var length = this.controllerStack.length;
+ if (length > 1) {
+ this._restoreController(this.controllerStack[length - 2]);
+ }
+ }
+ },
+ /**
+ * Intercepts and triggers a new push_state event, with additional
+ * information about the given controller.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {string} ev.controllerID
+ * @param {Object} [ev.state={}]
+ */
+ _onPushState: function (ev) {
+ if (ev.target !== this) {
+ ev.stopPropagation();
+ this._pushState(ev.data.controllerID, ev.data.state);
}
- return $.when();
},
});
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
new file mode 100644
index 00000000000..a37b37fca73
--- /dev/null
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -0,0 +1,756 @@
+odoo.define('web.ActWindowActionManager', function (require) {
+"use strict";
+
+/**
+ * The purpose of this file is to add the support of Odoo actions of type
+ * 'ir.actions.act_window' to the ActionManager.
+ */
+
+var ActionManager = require('web.ActionManager');
+var config = require('web.config');
+var Context = require('web.Context');
+var core = require('web.core');
+var data = require('web.data'); // this will be removed at some point
+var pyeval = require('web.pyeval');
+var SearchView = require('web.SearchView');
+var view_registry = require('web.view_registry');
+
+var _t = core._t;
+
+ActionManager.include({
+ custom_events: _.extend({}, ActionManager.prototype.custom_events, {
+ env_updated: '_onEnvUpdated',
+ execute_action: '_onExecuteAction',
+ search: '_onSearch',
+ switch_view: '_onSwitchView',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Overrides to handle the case where an 'ir.actions.act_window' has to be
+ * loaded.
+ *
+ * @override
+ * @param {Object} state
+ * @param {integer|string} [state.action] the ID or xml ID of the action to
+ * execute
+ * @param {integer} [state.active_id]
+ * @param {string} [state.active_ids]
+ * @param {integer} [state.id]
+ * @param {integer} [state.view_id=false]
+ * @param {string} [state.view_type]
+ */
+ loadState: function (state) {
+ var action;
+ var options = {pushState: false};
+ if (state.action) {
+ var currentController = this.getCurrentController();
+ var currentAction = currentController && this.actions[currentController.actionID];
+ if (currentAction && currentAction.id === state.action) {
+ // the action to load is already the current one, so update it
+ this._closeDialog(true); // there may be a currently opened dialog, close it
+ currentAction.env.currentId = state.id;
+ var viewType = state.view_type || currentController.viewType;
+ return this._switchController(currentAction, viewType);
+ } else if (!core.action_registry.contains(state.action)) {
+ // the action to load isn't the current one, so execute it
+ var context = {};
+ if (state.active_id) {
+ context.active_id = state.active_id;
+ }
+ if (state.active_ids) {
+ // jQuery's BBQ plugin does some parsing on values that are valid integers
+ // which means that if there's only one item, it will do parseInt() on it,
+ // otherwise it will keep the comma seperated list as string
+ context.active_ids = state.active_ids.split(',').map(function (id) {
+ return parseInt(id, 10) || id;
+ });
+ } else if (state.active_id) {
+ context.active_ids = [state.active_id];
+ }
+ context.params = state;
+ action = state.action;
+ options = _.extend(options, {
+ additional_context: context,
+ resID: state.id,
+ viewType: state.view_type,
+ });
+ }
+ } else if (state.model && state.id) {
+ action = {
+ res_model: state.model,
+ res_id: state.id,
+ type: 'ir.actions.act_window',
+ views: [[state.view_id || false, 'form']],
+ };
+ }
+ if (action) {
+ return this.doAction(action, options);
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Instantiates a search view for a given action, and starts it so that it
+ * is ready to be appended to the DOM.
+ *
+ * @private
+ * @param {Object} action
+ * @returns {Deferred} resolved with the search view when it is ready
+ */
+ _createSearchView: function (action) {
+ // AAB: temporarily create a dataset, until the SearchView is refactored
+ // and stops using it
+ 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;
+ }
+
+ // find 'search_default_*' keys in actions's context
+ var searchDefaults = {};
+ _.each(action.context, function (value, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ searchDefaults[match[1]] = value;
+ }
+ });
+ var searchView = new SearchView(this, dataset, action.searchFieldsView, {
+ $buttons: $('<div>'),
+ action: action,
+ disable_custom_filters: action.flags.disableCustomFilters,
+ search_defaults: searchDefaults,
+ });
+
+ return searchView.appendTo(document.createDocumentFragment()).then(function () {
+ action.searchView = searchView;
+ return searchView;
+ });
+ },
+ /**
+ * Instantiates the controller for a given action and view type, and adds it
+ * to the list of controllers in the action.
+ *
+ * @private
+ * @param {Object} action
+ * @param {AbstractController[]} action.controllers the already created
+ * controllers for this action
+ * @param {Object[]} action.views the views available for the action, each
+ * one containing its fieldsView
+ * @param {Object} action.env
+ * @param {string} viewType
+ * @param {Object} [viewOptions] dict of options passed to the initialization
+ * of the controller's widget
+ * @param {Object} [options]
+ * @param {boolean} [options.lazy=false] set to true to differ the
+ * initialization of the controller's widget
+ * @returns {Deferred<Object>} resolved with the created controller
+ */
+ _createViewController: function (action, viewType, viewOptions, options) {
+ var self = this;
+ var controllerID = _.uniqueId('controller_');
+ var controller = {
+ actionID: action.jsID,
+ className: 'o_act_window', // used to remove the padding in dialogs
+ jsID: controllerID,
+ viewType: viewType,
+ };
+ Object.defineProperty(controller, 'title', {
+ get: function () {
+ // handle the case where the widget is lazy loaded
+ return controller.widget ? controller.widget.getTitle() : (action.display_name || action.name);
+ },
+ });
+ this.controllers[controllerID] = controller;
+
+ if (!options || !options.lazy) {
+ // build the view options from different sources
+ viewOptions = _.extend({
+ action: action,
+ limit: action.limit,
+ }, action.flags, action.flags[viewType], viewOptions, action.env);
+ // pass the controllerID to the views as an hook for further
+ // communication with trigger_up (e.g. for 'env_updated' event)
+ viewOptions = _.extend(viewOptions, { controllerID: controllerID });
+
+ var viewDescr = _.findWhere(action.views, {type: viewType});
+ var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
+ var def = $.Deferred();
+ action.controllers[viewType] = def;
+ view.getController(this).then(function (widget) {
+ if (def.state() === 'rejected') {
+ // the deferred has been rejected meanwhile, meaning that
+ // the action has been removed, so simply destroy the widget
+ widget.destroy();
+ } else {
+ // AAB: change this logic to stop using the properties mixin
+ widget.on("change:title", this, function () {
+ if (!action.flags.headless) {
+ var breadcrumbs = self._getBreadcrumbs();
+ self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false});
+ }
+ });
+ controller.widget = widget;
+
+ def.resolve(controller);
+ }
+ }).fail(def.reject.bind(def));
+ def.fail(function () {
+ delete self.controllers[controllerID];
+ });
+ } else {
+ action.controllers[viewType] = $.Deferred().resolve(controller);
+ }
+
+ return action.controllers[viewType];
+ },
+ /**
+ * Executes actions of type 'ir.actions.act_window'.
+ *
+ * @private
+ * @param {Object} action the description of the action to execute
+ * @param {Array} action.views list of tuples [viewID, viewType]
+ * @param {Object} options @see doAction for details
+ * @param {integer} [options.resID] the current res ID
+ * @param {string} [options.viewType] the view to open
+ * @returns {Deferred} resolved when the action is appended to the DOM
+ */
+ _executeWindowAction: function (action, options) {
+ var self = this;
+
+ if (action.context.active_id || action.context.active_ids) {
+ // we assume that when '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;
+ }
+ action.flags = this._generateActionFlags(action);
+
+ return this._loadViews(action).then(function (fieldsViews) {
+ var views = self._generateActionViews(action, fieldsViews);
+ action._views = action.views; // save the initial attribute
+ action.views = views;
+ if (fieldsViews.search) {
+ action.searchFieldsView = fieldsViews.search;
+ }
+ action.env = self._generateActionEnv(action, options);
+ action.controllers = {};
+
+ // select the first view to display
+ var lazyLoadFirstView = false;
+ var firstView = options.viewType && _.findWhere(views, {type: options.viewType});
+ if (firstView) {
+ if (!firstView.multiRecord && views[0].multiRecord) {
+ lazyLoadFirstView = true;
+ }
+ } else {
+ firstView = views[0];
+ }
+ if (config.device.isMobile && !firstView.isMobileFriendly) {
+ firstView = _.findWhere(action.views, {isMobileFriendly: true}) || firstView;
+ }
+
+ var def;
+ if (action.flags.hasSearchView) {
+ def = self._createSearchView(action).then(function (searchView) {
+ // udpate domain, context and groupby in the env
+ var searchData = searchView.build_search_data();
+ _.extend(action.env, self._processSearchData(action, searchData));
+ });
+ }
+ return $.when(def).then(function () {
+ var defs = [];
+ defs.push(self._createViewController(action, firstView.type));
+ if (lazyLoadFirstView) {
+ defs.push(self._createViewController(action, views[0].type, {}, {lazy: true}));
+ }
+ return $.when.apply($, defs);
+ }).then(function (controller, lazyLoadedController) {
+ action.controllerID = controller.jsID;
+ return self._executeAction(action, options).done(function () {
+ if (lazyLoadedController) {
+ self.controllerStack.unshift(lazyLoadedController.jsID);
+ self.controlPanel.update({
+ breadcrumbs: self._getBreadcrumbs(),
+ }, {clear: false});
+ }
+ });
+ });
+ });
+ },
+ /**
+ * Generates the initial environment of a given action, which is a dict
+ * containing information like the model name, the domain, the context...
+ * That information is shared between the controllers of the corresponding
+ * action.
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} options
+ * @param {integer} [options.resID]
+ * @returns {Object}
+ */
+ _generateActionEnv: function (action, options) {
+ var actionGroupBy = action.context.group_by || [];
+ if (typeof actionGroupBy === 'string') {
+ actionGroupBy = [actionGroupBy];
+ }
+ var resID = options.resID || action.res_id;
+ return {
+ modelName: action.res_model,
+ ids: resID ? [resID] : undefined,
+ currentId: resID || undefined,
+ domain: [],
+ context: action.context || {},
+ groupBy: actionGroupBy,
+ };
+ },
+ /**
+ * Generates the flags of a given action.
+ *
+ * @private
+ * @param {Object} action
+ * @returns {Object}
+ */
+ _generateActionFlags: function (action) {
+ var popup = action.target === 'new';
+ var inline = action.target === 'inline';
+ var form = _.str.startsWith(action.view_mode, 'form');
+ return _.defaults({}, action.flags, {
+ disableCustomFilters: action.context && action.context.search_disable_custom_filters,
+ footerToButtons: popup,
+ hasSearchView: !(popup && form) && !inline,
+ hasSidebar: !popup && !inline,
+ headless: (popup || inline) && form,
+ mode: (popup || inline || action.target === 'fullscreen') && 'edit',
+ });
+ },
+ /**
+ * Generate the description of the views of a given action. For each view,
+ * it generates a dict with information like the fieldsView, the view type,
+ * the Widget to use...
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} fieldsViews
+ * @returns {Object}
+ */
+ _generateActionViews: function (action, fieldsViews) {
+ var views = [];
+ _.each(action.views, function (view) {
+ var viewType = view[1];
+ var fieldsView = fieldsViews[viewType];
+ var View = view_registry.get(fieldsView.arch.attrs.js_class || viewType);
+ if (View) {
+ views.push({
+ accessKey: View.prototype.accessKey,
+ fieldsView: fieldsViews[viewType],
+ icon: View.prototype.icon,
+ isMobileFriendly: View.prototype.mobile_friendly,
+ multiRecord: View.prototype.multi_record,
+ type: viewType,
+ viewID: view[0],
+ Widget: View,
+ });
+ } else {
+ console.error("View type '" + viewType + "' is not present in the view registry.");
+ }
+ });
+ return views;
+ },
+ /**
+ * Overrides to add specific information for controllers from actions of
+ * type 'ir.actions.act_window', like the res_model and the view_type.
+ *
+ * @override
+ * @private
+ */
+ _getControllerState: function (controllerID) {
+ var state = this._super.apply(this, arguments);
+ var controller = this.controllers[controllerID];
+ var action = this.actions[controller.actionID];
+ if (action.type === 'ir.actions.act_window') {
+ state.model = action.res_model;
+ state.view_type = controller.viewType;
+ }
+ return state;
+ },
+ /**
+ * Overrides to handle the 'ir.actions.act_window' actions.
+ *
+ * @override
+ * @private
+ */
+ _handleAction: function (action, options) {
+ if (action.type === 'ir.actions.act_window') {
+ return this._executeWindowAction(action, options);
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Loads the fields_views and fields for the given action.
+ *
+ * @private
+ * @param {Object} action
+ * @returns {Deferred}
+ */
+ _loadViews: function (action) {
+ var options = {
+ action_id: action.id,
+ toolbar: action.flags.hasSidebar,
+ };
+ var views = action.views.slice();
+ if (action.flags.hasSearchView) {
+ options.load_filters = true;
+ var searchviewID = action.search_view_id && action.search_view_id[0];
+ views.push([searchviewID || false, 'search']);
+ }
+ return this.loadViews(action.res_model, action.context, views, options);
+ },
+ /**
+ * Processes the search data sent by the search view.
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} searchData
+ * @param {Object} [searchData.contexts=[]]
+ * @param {Object} [searchData.domains=[]]
+ * @param {Object} [searchData.groupbys=[]]
+ * @returns {Object} an object with keys 'context', 'domain', 'groupBy'
+ */
+ _processSearchData: function (action, searchData) {
+ var contexts = searchData.contexts;
+ var domains = searchData.domains;
+ var groupbys = searchData.groupbys;
+ var action_context = action.context || {};
+ var results = pyeval.eval_domains_and_contexts({
+ domains: [action.domain || []].concat(domains || []),
+ contexts: [action_context].concat(contexts || []),
+ group_by_seq: groupbys || [],
+ eval_context: this.userContext,
+ });
+ if (results.error) {
+ throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
+ JSON.stringify(results.error)));
+ }
+ return {
+ context: results.context,
+ domain: results.domain,
+ groupBy: results.group_by,
+ };
+ },
+ /**
+ * Overrides to handle the case of 'ir.actions.act_window' actions, i.e.
+ * destroys all controllers associated to the given action, and its search
+ * view.
+ *
+ * @override
+ * @private
+ */
+ _removeAction: function (actionID) {
+ var self = this;
+ var action = this.actions[actionID];
+ if (action.type === 'ir.actions.act_window') {
+ delete this.actions[action.jsID];
+ _.each(action.controllers, function (controllerDef) {
+ controllerDef.then(function (controller) {
+ delete self.controllers[controller.jsID];
+ if (controller.widget) {
+ controller.widget.destroy();
+ }
+ });
+ // reject the deferred if it is not yet resolved, so that the
+ // controller is correctly destroyed as soon as it is ready, and
+ // its reference is removed
+ controllerDef.reject();
+ });
+ if (action.searchView) {
+ action.searchView.destroy();
+ }
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
+ * Overrides to handle the case where the controller to restore is from an
+ * 'ir.actions.act_window' action. In this case, only the controllers
+ * stacked over the one to restore *that are not from the same action* are
+ * destroyed.
+ * For instance, when going back to the list controller from a form
+ * controller of the same action using the breadcrumbs, the form controller
+ * isn't destroyed, as it might be reused in the future.
+ *
+ * @override
+ * @private
+ */
+ _restoreController: function (controllerID) {
+ var self = this;
+ var controller = this.controllers[controllerID];
+ var action = this.actions[controller.actionID];
+ if (action.type === 'ir.actions.act_window') {
+ return this.clearUncommittedChanges().then(function () {
+ // AAB: this will be done directly in AbstractAction's restore
+ // function
+ var def;
+ if (action.on_reverse_breadcrumb) {
+ def = action.on_reverse_breadcrumb();
+ }
+ return $.when(def).then(function () {
+ return self._switchController(action, controller.viewType);
+ });
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Handles the switch from a controller to another inside the same window
+ * action.
+ *
+ * @private
+ * @param {Object} controller the controller to switch to
+ * @param {Object} [viewOptions]
+ * @return {Deferred} resolved when the new controller is in the DOM
+ */
+ _switchController: function (action, viewType, viewOptions) {
+ var self = this;
+
+ var newController = function () {
+ return self
+ ._createViewController(action, viewType, viewOptions)
+ .then(function (controller) {
+ // AAB: this will be moved to the Controller
+ var widget = controller.widget;
+ if (widget.need_control_panel) {
+ // set the ControlPanel bus on the controller to allow it to
+ // communicate its status
+ widget.set_cp_bus(self.controlPanel.get_bus());
+ }
+ return self._startController(controller);
+ });
+ };
+
+ var controllerDef = action.controllers[viewType];
+ if (!controllerDef) {
+ controllerDef = newController();
+ } else {
+ controllerDef = controllerDef.then(function (controller) {
+ if (!controller.widget) {
+ // lazy loaded -> load it now
+ return newController().done(function (newController) {
+ // replace the old controller (without widget) by the new one
+ var index = self.controllerStack.indexOf(controller.jsID);
+ self.controllerStack[index] = newController.jsID;
+ delete self.controllers[controller.jsID];
+ });
+ } else {
+ viewOptions = _.extend(viewOptions || {}, action.env);
+ return controller.widget.reload(viewOptions).then(function () {
+ return controller;
+ });
+ }
+ });
+ }
+
+ return this.dp.add(controllerDef).then(function (controller) {
+ var view = _.findWhere(action.views, {type: viewType});
+ var currentController = self.getCurrentController();
+ var index;
+ if (currentController.actionID !== action.jsID) {
+ index = _.indexOf(self.controllerStack, controller.jsID);
+ } else if (view.multiRecord) {
+ // remove other controllers linked to the same action from the stack
+ index = _.findIndex(self.controllerStack, function (controllerID) {
+ return self.controllers[controllerID].actionID === action.jsID;
+ });
+ } else if (!_.findWhere(action.views, {type: currentController.viewType}).multiRecord) {
+ // replace the last controller by the new one if they are from the
+ // same action and if they both are mono record
+ index = self.controllerStack.length - 1;
+ }
+ return self._pushController(controller, {index: index});
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {string} ev.data.controllerID
+ * @param {Object} ev.data.env
+ */
+ _onEnvUpdated: function (ev) {
+ ev.stopPropagation();
+ var controller = this.controllers[ev.data.controllerID];
+ var action = this.actions[controller.actionID];
+ _.extend(action.env, ev.data.env);
+ },
+ /**
+ * Handler for event 'execute_action', which is typically called when a
+ * button is clicked. The button may be of type 'object' (call a given
+ * method of a given model) or 'action' (execute a given action).
+ * Alternatively, the button may have the attribute 'special', and in this
+ * case an 'ir.actions.act_window_close' is executed.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {Object} ev.data.action_data typically, the html attributes of the
+ * button extended with additional information like the context
+ * @param {Object} [ev.data.action_data.special=false]
+ * @param {Object} [ev.data.action_data.type] 'object' or 'action', if set
+ * @param {Object} ev.data.env
+ * @param {function} [ev.data.on_closed]
+ * @param {function} [ev.data.on_fail]
+ * @param {function} [ev.data.on_success]
+ */
+ _onExecuteAction: function (ev) {
+ ev.stopPropagation();
+ var self = this;
+ var actionData = ev.data.action_data;
+ var env = ev.data.env;
+ var context = new Context(env.context, actionData.context || {});
+ var recordID = env.currentID || null; // pyeval handles null value, not undefined
+ var def;
+
+ // determine the action to execute according to the actionData
+ if (actionData.special) {
+ def = $.when({type: 'ir.actions.act_window_close'});
+ } else if (actionData.type === 'object') {
+ // call a Python Object method, which may return an action to execute
+ var args = recordID ? [[recordID]] : [env.resIDs];
+ if (actionData.args) {
+ try {
+ // warning: quotes and double quotes problem due to json and xml clash
+ // maybe we should force escaping in xml or do a better parse of the args array
+ var additionalArgs = JSON.parse(actionData.args.replace(/'/g, '"'));
+ args = args.concat(additionalArgs);
+ } catch (e) {
+ console.error("Could not JSON.parse arguments", actionData.args);
+ }
+ }
+ args.push(context.eval());
+ def = this._rpc({
+ route: '/web/dataset/call_button',
+ params: {
+ args: args,
+ method: actionData.name,
+ model: env.model,
+ },
+ });
+ } else if (actionData.type === 'action') {
+ // execute a given action, so load it first
+ def = this._loadAction(actionData.name, _.extend(pyeval.eval('context', context), {
+ active_model: env.model,
+ active_ids: env.resIDs,
+ active_id: recordID,
+ }));
+ }
+
+ // use the DropPrevious to prevent from executing the handler if another
+ // request (doAction, switchView...) has been done meanwhile ; execute
+ // the fail handler if the 'call_button' or 'loadAction' failed but not
+ // if the request failed due to the DropPrevious,
+ def.fail(ev.data.on_fail);
+ this.dp.add(def).then(function (action) {
+ // show effect if button have effect attribute
+ // rainbowman can be displayed from two places: from attribute on a button or from python
+ // code below handles the first case i.e 'effect' attribute on button.
+ var effect = false;
+ if (actionData.effect) {
+ effect = pyeval.py_eval(actionData.effect);
+ }
+
+ if (action && action.constructor === Object) {
+ // filter out context keys that are specific to the current action, because:
+ // - wrong default_* and search_default_* values won't give the expected result
+ // - wrong group_by values will fail and forbid rendering of the destination view
+ var ctx = new Context(
+ _.object(_.reject(_.pairs(env.context), function (pair) {
+ return pair[0].match('^(?:(?:default_|search_default_|show_).+|' +
+ '.+_view_ref|group_by|group_by_no_leaf|active_id|' +
+ 'active_ids)$') !== null;
+ }))
+ );
+ ctx.add(actionData.context || {});
+ ctx.add({active_model: env.model});
+ if (recordID) {
+ ctx.add({
+ active_id: recordID,
+ active_ids: [recordID],
+ });
+ }
+ ctx.add(action.context || {});
+ action.context = ctx;
+ // in case an effect is returned from python and there is already an effect
+ // attribute on the button, the priority is given to the button attribute
+ action.effect = effect || action.effect;
+ } else {
+ // if action doesn't return anything, but there is an effect
+ // attribute on the button, display rainbowman
+ action = {
+ effect: effect,
+ type: 'ir.actions.act_window_close',
+ };
+ }
+ var options = {on_close: ev.data.on_closed};
+ return self.doAction(action, options).then(ev.data.on_success, ev.data.on_fail);
+ });
+ },
+ /**
+ * Called when there is a change in the search view, so the current action's
+ * environment needs to be updated with the new domain, context and groupby.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSearch: function (ev) {
+ ev.stopPropagation();
+ // AAB: the id of the correct controller should be given in data
+ var currentController = this.getCurrentController();
+ var action = this.actions[currentController.actionID];
+ _.extend(action.env, this._processSearchData(action, ev.data));
+ currentController.widget.reload(_.extend({offset: 0}, action.env));
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {string} ev.data.controllerID the id of the controller that
+ * triggered the event
+ * @param {string} ev.data.viewType the type of view to switch to
+ * @param {integer} [ev.data.res_id] the id of the record to open (for
+ * mono-record views)
+ * @param {mode} [ev.data.mode] the mode to open, i.e. 'edit' or 'readonly'
+ * (only relevant for form views)
+ */
+ _onSwitchView: function (ev) {
+ ev.stopPropagation();
+ var viewType = ev.data.view_type;
+ var currentController = this.getCurrentController();
+ if (currentController.jsID === ev.data.controllerID) {
+ // only switch to the requested view if the controller that
+ // triggered the request is the current controller
+ var action = this.actions[currentController.actionID];
+ if ('res_id' in ev.data) {
+ action.env.currentId = ev.data.res_id;
+ }
+ var options = {};
+ if (viewType === 'form' && !action.env.currentId) {
+ options.mode = 'edit';
+ } else if (ev.data.mode) {
+ options.mode = ev.data.mode;
+ }
+ this._switchController(action, viewType, options);
+ }
+ },
+});
+
+});
diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js
index 98ce7814b7a..3f85d32d2b6 100644
--- a/addons/web/static/src/js/chrome/control_panel.js
+++ b/addons/web/static/src/js/chrome/control_panel.js
@@ -31,7 +31,6 @@ odoo.define('web.ControlPanelMixin', function (require) {
* },
* _updateControlPanel: function () {
* this.update_control_panel({
- * breadcrumbs: this.action_manager.get_breadcrumbs(),
* cp_content: {
* $buttons: this.$buttons,
* },
@@ -51,7 +50,9 @@ var ControlPanelMixin = {
* @param {Object} [options] see web.ControlPanel.update() for a description
*/
update_control_panel: function(cp_status, options) {
- this.cp_bus.trigger("update", cp_status || {}, options || {});
+ if (this.cp_bus) {
+ this.cp_bus.trigger("update", cp_status || {}, options || {});
+ }
},
};
@@ -88,7 +89,6 @@ var ControlPanel = Widget.extend({
start: function() {
// Exposed jQuery nodesets
this.nodes = {
- $breadcrumbs: this.$('.breadcrumb'),
$buttons: this.$('.o_cp_buttons'),
$pager: this.$('.o_cp_pager'),
$searchview: this.$('.o_cp_searchview'),
@@ -107,10 +107,6 @@ var ControlPanel = Widget.extend({
return this._super();
},
- destroy: function() {
- this._clear_breadcrumbs_handlers();
- return this._super.apply(this, arguments);
- },
/**
* @return {Object} the Bus the ControlPanel is listening on
*/
@@ -143,9 +139,7 @@ var ControlPanel = Widget.extend({
// Render the breadcrumbs
if (status.breadcrumbs) {
- this._clear_breadcrumbs_handlers();
- this.$breadcrumbs = this._render_breadcrumbs(status.breadcrumbs);
- new_cp_content.$breadcrumbs = this.$breadcrumbs;
+ this.$('.breadcrumb').html(this._render_breadcrumbs(status.breadcrumbs));
}
// Detach control_panel old content and attach new elements
@@ -239,23 +233,12 @@ var ControlPanel = Widget.extend({
.toggleClass('active', is_last);
if (!is_last) {
$bc.click(function () {
- self.trigger("on_breadcrumb_click", bc.action, bc.index);
+ self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
});
}
return $bc;
},
/**
- * Private function that removes event handlers attached on the currently
- * displayed breadcrumbs.
- */
- _clear_breadcrumbs_handlers: function () {
- if (this.$breadcrumbs) {
- _.each(this.$breadcrumbs, function ($bc) {
- $bc.off();
- });
- }
- },
- /**
* Private function that updates the SearchView's visibility and extend the
* breadcrumbs area if the SearchView is not visible
* @param {openerp.web.SearchView} [searchview] the searchview Widget
diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js
index c9d58d89c81..1e5e2922186 100644
--- a/addons/web/static/src/js/chrome/menu.js
+++ b/addons/web/static/src/js/chrome/menu.js
@@ -118,6 +118,8 @@ var Menu = Widget.extend({
$main_menu = $clicked_menu;
}
+ this.current_primary_menu = $main_menu.data('menu');
+
// Activate current main menu
this.$el.find('.active').removeClass('active');
$main_menu.parent().addClass('active');
@@ -206,6 +208,19 @@ var Menu = Widget.extend({
ev.preventDefault();
this.menu_click($(ev.currentTarget).data('menu'));
},
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the id of the current primary (first level) menu.
+ *
+ * @returns {integer}
+ */
+ getCurrentPrimaryMenu: function () {
+ return this.current_primary_menu;
+ },
});
return Menu;
diff --git a/addons/web/static/src/js/chrome/search_menus.js b/addons/web/static/src/js/chrome/search_menus.js
index fe2ce5f3c32..38f2bb7374c 100644
--- a/addons/web/static/src/js/chrome/search_menus.js
+++ b/addons/web/static/src/js/chrome/search_menus.js
@@ -30,12 +30,13 @@ return Widget.extend({
}
},
},
- init: function (parent, query, target_model, action_id, filters) {
+ init: function (parent, query, target_model, action, filters) {
this._super.apply(this,arguments);
this.searchview = parent;
this.query = query;
this.target_model = target_model;
- this.action_id = action_id;
+ this.action = action;
+ this.action_id = action.id;
this.filters = {};
_.each(filters, this.add_filter.bind(this));
},
@@ -96,11 +97,7 @@ return Widget.extend({
}
var user_context = this.getSession().user_context;
var search = this.searchview.build_search_data();
- var view_manager = this.findAncestor(function (a) {
- // HORRIBLE HACK. PLEASE SAVE ME FROM MYSELF (BUT IN A PAINLESS WAY IF POSSIBLE)
- return 'active_view' in a;
- });
- var view_context = view_manager ? view_manager.active_view.controller.getContext() : {};
+ var view_context = this.action.env.context;
var results = pyeval.eval_domains_and_contexts({
domains: search.domains,
contexts: [user_context].concat(search.contexts.concat(view_context || [])),
diff --git a/addons/web/static/src/js/chrome/search_view.js b/addons/web/static/src/js/chrome/search_view.js
index 1e199e76dff..6fbda215814 100644
--- a/addons/web/static/src/js/chrome/search_view.js
+++ b/addons/web/static/src/js/chrome/search_view.js
@@ -278,7 +278,7 @@ var SearchView = Widget.extend({
this.fields = this.fields_view.fields;
this.query = undefined;
this.title = this.options.action && this.options.action.name;
- this.action_id = this.options.action && this.options.action.id;
+ this.action = this.options.action || {};
this.search_fields = [];
this.filters = [];
this.groupbys = [];
@@ -297,7 +297,7 @@ var SearchView = Widget.extend({
var self = this;
var def;
if (!this.options.disable_favorites) {
- def = this.loadFilters(this.dataset, this.action_id).then(function (filters) {
+ def = this.loadFilters(this.dataset, this.action.id).then(function (filters) {
self.favorite_filters = filters;
});
}
@@ -328,7 +328,7 @@ var SearchView = Widget.extend({
menu_defs.push(this.groupby_menu.appendTo(this.$buttons));
}
if (!this.options.disable_favorites) {
- this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id, this.favorite_filters);
+ this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action, this.favorite_filters);
menu_defs.push(this.favorite_menu.appendTo(this.$buttons));
}
}
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
deleted file mode 100644
index 75029d4cc27..00000000000
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ /dev/null
@@ -1,671 +0,0 @@
-odoo.define('web.ViewManager', function (require) {
-"use strict";
-
-var Context = require('web.Context');
-var ControlPanelMixin = require('web.ControlPanelMixin');
-var core = require('web.core');
-var data = require('web.data');
-var data_manager = require('web.data_manager');
-var dom = require('web.dom');
-var pyeval = require('web.pyeval');
-var SearchView = require('web.SearchView');
-var view_registry = require('web.view_registry');
-var Widget = require('web.Widget');
-
-var QWeb = core.qweb;
-var _t = core._t;
-
-var ViewManager = Widget.extend(ControlPanelMixin, {
- className: "o_view_manager_content",
- custom_events: {
- execute_action: function(event) {
- var data = event.data;
- this.do_execute_action(data.action_data, data.env, data.on_closed)
- .then(data.on_success, data.on_fail);
- },
- search: function(event) {
- var d = event.data;
- _.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
- this.active_view.controller.reload(_.extend({offset: 0}, this.env));
- },
- switch_view: function(event) {
- if ('res_id' in event.data) {
- this.env.currentId = event.data.res_id;
- }
- var options = {};
- if (event.data.view_type === 'form' && !this.env.currentId) {
- options.mode = 'edit';
- } else if (event.data.mode) {
- options.mode = event.data.mode;
- }
- this.switch_mode(event.data.view_type, options);
- },
- env_updated: function(event) {
- _.extend(this.env, event.data);
- },
- push_state: function(event) {
- this.do_push_state(event.data);
- },
- switch_to_previous_view: '_onSwitchToPreviousView',
- },
- /**
- * Called each time the view manager is attached into the DOM
- */
- on_attach_callback: function() {
- this.is_in_DOM = true;
- var controller = this.active_view && this.active_view.controller;
- if (controller && this.active_view.controller.on_attach_callback) {
- this.active_view.controller.on_attach_callback();
- }
- },
- /**
- * Called each time the view manager is detached from the DOM
- */
- on_detach_callback: function() {
- this.is_in_DOM = false;
- var controller = this.active_view && this.active_view.controller;
- if (controller && controller.on_detach_callback) {
- this.active_view.controller.on_detach_callback();
- }
- },
- /**
- * @param {Object} [dataset]
- * @param {Array} [views] List of [view_id, view_type[, fields_view]]
- * @param {Object} [flags] various boolean describing UI state
- */
- init: function(parent, dataset, views, flags, options) {
- var self = this;
- this._super(parent);
-
- this.action = options && options.action || {};
- this.action_manager = options && options.action_manager;
- this.flags = flags || {};
- this.dataset = dataset;
- this.view_order = [];
- this.views = {};
- this.view_stack = []; // used for breadcrumbs
- this.active_view = null;
- this.registry = view_registry;
- this.title = this.action.name;
- var actionGroupBy = self.action.context.group_by;
- if (!actionGroupBy) {
- actionGroupBy = [];
- } else if (typeof actionGroupBy === 'string') {
- actionGroupBy = [actionGroupBy];
- }
- _.each(views, function (view) {
- var view_type = view[1] || view.view_type;
- var View = self.registry.get(view_type); //.prototype.config.Controller;
- if (!View) {
- console.error("View type", "'"+view_type+"'", "is not present in the view registry.");
- return;
- }
- var view_label = View.prototype.display_name;
- var view_descr = {
- accesskey: View.prototype.accesskey,
- button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
- controller: null,
- fields_view: view[2] || view.fields_view,
- icon: View.prototype.icon,
- label: view_label,
- mobile_friendly: View.prototype.mobile_friendly,
- multi_record: View.prototype.multi_record,
- options: _.extend({
- action: self.action,
- limit: self.action.limit,
- views: self.action.views,
- groupBy: actionGroupBy,
- }, self.flags, self.flags[view_type], view.options),
- searchable: View.prototype.searchable,
- title: self.title,
- type: view_type,
- view_id: view[0] || view.view_id,
- };
- self.view_order.push(view_descr);
- self.views[view_type] = view_descr;
- });
- this.first_view = this.views[options && options.view_type]; // view to open first
- this.default_view = this.get_default_view();
-
- // env registers properties shared between views
- this.env = {
- modelName: this.dataset.model,
- ids: this.dataset.ids.length ? this.dataset.ids : undefined,
- currentId: this.dataset.ids.length ? this.dataset.ids[this.dataset.index] : undefined,
- domain: undefined,
- context: this.dataset.context,
- groupBy: undefined,
- };
- },
- willStart: function () {
- var views_def;
- var first_view_to_display = this.first_view || this.default_view;
- if (!first_view_to_display.fields_view || (this.flags.search_view && !this.search_fields_view)) {
- views_def = this.load_views();
- }
- return $.when(this._super(), views_def);
- },
- /**
- * @return {Deferred} initial view and search view (if any) loading promise
- */
- start: function() {
- var self = this;
- var _super = this._super.bind(this, arguments);
- var def;
- if (this.flags.search_view) {
- def = this.setup_search_view().then(function() {
- // udpate domain, context and groupby in the env
- var d = self.searchview.build_search_data();
- _.extend(self.env, self._process_search_data(d.domains, d.contexts, d.groupbys));
- });
- }
- return $.when(def).then(function() {
- var defs = [];
- defs.push(_super());
-
- if (self.flags.views_switcher) {
- self.render_switch_buttons();
- }
-
- // If a non multi-record first_view is given, switch to it but first push the default_view
- // to the view_stack to complete the breadcrumbs
- if (self.first_view && !self.first_view.multi_record && self.default_view.multi_record) {
- self.view_stack.push(self.default_view);
- }
- var view_to_load = self.first_view || self.default_view;
- var options = _.extend({}, view_to_load.options);
- defs.push(self.switch_mode(view_to_load.type, options));
-
- return $.when.apply($, defs);
- }).then(function() {
- if (self.flags.on_load) {
- self.flags.on_load(self);
- }
- core.bus.on('clear_uncommitted_changes', self, function(chain_callbacks) {
- chain_callbacks(function() {
- if (self.active_view.controller) {
- return self.active_view.controller.discardChanges(undefined, {
- readonlyIfRealDiscard: true,
- });
- }
- });
- });
- });
- },
- /**
- * Loads all missing field_views of views in this.views and the search view.
- *
- * @return {Deferred}
- */
- load_views: function () {
- var self = this;
- var views = [];
- _.each(this.views, function (view) {
- if (!view.fields_view) {
- views.push([view.view_id, view.type]);
- }
- });
- var options = {
- action_id: this.action.id,
- toolbar: this.flags.sidebar,
- };
- if (this.flags.search_view && !this.search_fields_view) {
- options.load_filters = true;
- var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
- views.push([searchview_id || false, 'search']);
- }
- var params = {
- model: this.dataset.model,
- context: this.dataset.get_context(),
- views_descr: views,
- };
- return data_manager.load_views(params, options).then(function (fields_views) {
- _.each(fields_views, function (fields_view, view_type) {
- if (view_type === 'search') {
- self.search_fields_view = fields_view;
- } else {
- self.views[view_type].fields_view = fields_view;
- }
- });
- });
- },
- /**
- * Returns the default view with the following fallbacks:
- *
- * - use the default_view defined in the flags, if any
- * - use the first view in the view_order
- *
- * @returns {Object} the default view
- */
- get_default_view: function() {
- return this.views[this.flags.default_view || this.view_order[0].type];
- },
- switch_mode: function(view_type, view_options) {
- var self = this;
- var view = this.views[view_type];
-
- if (!view || this.currently_switching) {
- return $.Deferred().reject();
- } else {
- this.currently_switching = true; // prevent overlapping switches
- }
-
- var old_view = this.active_view;
-
- // Ensure that the fields_view has been loaded
- var views_def;
- if (!view.fields_view) {
- views_def = this.load_views();
- }
-
- return $.when(views_def).then(function () {
- if (view.multi_record) {
- self.view_stack = [];
- } else if (self.view_stack.length > 0 && !(_.last(self.view_stack).multi_record)) {
- // Replace the last view by the new one if both are mono_record
- self.view_stack.pop();
- }
- self.view_stack.push(view);
-
- self.active_view = view;
-
- if (!view.loaded) {
- view_options = _.extend({}, view.options, view_options, self.env);
- if (view_options.groupBy && !view_options.groupBy.length) {
- var actionContext = view_options ? view_options.action.context : {};
- var actionGroupBy = actionContext.group_by;
- if (!actionGroupBy) {
- actionGroupBy = [];
- } else if (typeof actionGroupBy === 'string') {
- actionGroupBy = [actionGroupBy];
- }
- view_options.groupBy = actionGroupBy;
- }
- view.loaded = $.Deferred();
- self.create_view(view, view_options).then(function(controller) {
- view.controller = controller;
- view.$fragment = $('<div>');
- controller.appendTo(view.$fragment).done(function() {
- // Remove the unnecessary outer div
- view.$fragment = view.$fragment.contents();
- view.loaded.resolve();
- });
- }).fail(view.loaded.reject.bind(view.loaded));
- } else {
- view.loaded = view.loaded.then(function() {
- view_options = _.extend({}, view_options, self.env);
- return view.controller.reload(view_options);
- });
- }
-
- return $.when(view.loaded)
- .then(function() {
- self._display_view(old_view);
- self.trigger('switch_mode', view_type, view_options);
- }).fail(function(e) {
- if (!(e && e.code === 200 && e.data.exception_type)) {
- self.do_warn(_t("Error"), view.label + _t(" view couldn't be loaded"));
- }
- // Restore internal state
- self.active_view = old_view;
- self.view_stack.pop();
- });
- }).always(function () {
- self.currently_switching = false;
- });
- },
- _display_view: function (old_view) {
- var view_controller = this.active_view.controller;
- var view_fragment = this.active_view.$fragment;
-
- // Prepare the ControlPanel content and update it
- var view_control_elements = this.render_view_control_elements();
- var cp_status = {
- active_view_selector: '.o_cp_switch_' + this.active_view.type,
- breadcrumbs: this.action_manager && this.action_manager.get_breadcrumbs(),
- cp_content: _.extend({}, this.searchview_elements, view_control_elements),
- hidden: this.flags.headless,
- searchview: this.searchview,
- search_view_hidden: !this.active_view.searchable || this.active_view.searchview_hidden,
- };
- this.update_control_panel(cp_status);
-
- // Detach the old view and store it
- if (old_view && old_view !== this.active_view) {
- // Store the scroll position
- if (this.action_manager && this.action_manager.webclient) {
- old_view.controller.setScrollTop(this.action_manager.webclient.getScrollTop());
- }
- // Do not detach ui-autocomplete elements to let jquery-ui garbage-collect them
- var $to_detach = this.$el.contents().not('.ui-autocomplete');
- old_view.$fragment = dom.detach([{widget: old_view.controller}], {$to_detach: $to_detach});
- }
-
- // If the user switches from a multi-record to a mono-record view,
- // the action manager should be scrolled to the top.
- if (old_view && old_view.controller.multi_record === true && view_controller.multi_record === false) {
- view_controller.setScrollTop(0);
- }
-
- // Append the view fragment to this.$el
- dom.append(this.$el, view_fragment, {
- in_DOM: this.is_in_DOM,
- callbacks: [{widget: view_controller}],
- });
- },
- create_view: function(view_descr, view_options) {
- var self = this;
- var arch = view_descr.fields_view.arch;
- var View = this.registry.get(arch.attrs.js_class || view_descr.type);
- var params = _.extend({}, view_options, {userContext: this.getSession().user_context});
- if (view_descr.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline' || this.action.target === 'fullscreen') ||
- (view_options && (view_options.mode === 'edit' || view_options.context.form_view_initial_mode)))) {
- params.mode = params.initial_mode || 'edit';
- }
- view_descr.searchview_hidden = View.prototype.searchview_hidden;
- var view = new View(view_descr.fields_view, params);
- return view.getController(this).then(function(controller) {
- controller.on('history_back', this, function() {
- if (self.action_manager) self.action_manager.trigger('history_back');
- });
- controller.on("change:title", this, function() {
- if (self.action_manager && !self.flags.headless) {
- var breadcrumbs = self.action_manager.get_breadcrumbs();
- self.update_control_panel({breadcrumbs: breadcrumbs}, {clear: false});
- }
- });
- return controller;
- });
- },
- select_view: function (index) {
- var viewType = this.view_stack[index].type;
- var viewOptions = {};
- if (viewType === 'form') {
- // reload form views in readonly, except for inline actions (i.e.
- // settings views) that stay in edit
- viewOptions.mode = this.action.target === 'inline' ? 'edit' : 'readonly';
- }
- return this.switch_mode(viewType, viewOptions);
- },
- /**
- * Renders the switch buttons for multi- and mono-record views and adds
- * listeners on them, but does not append them to the DOM
- * Sets switch_buttons.$mono and switch_buttons.$multi to send to the ControlPanel
- */
- render_switch_buttons: function() {
- var self = this;
-
- // Partition the views according to their multi-/mono-record status
- var views = _.partition(this.view_order, function(view) {
- return view.multi_record === true;
- });
- var multi_record_views = views[0];
- var mono_record_views = views[1];
-
- // Inner function to render and prepare switch_buttons
- var _render_switch_buttons = function(views) {
- if (views.length > 1) {
- var $switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: views}));
- // Create bootstrap tooltips
- _.each(views, function(view) {
- $switch_buttons.filter('.o_cp_switch_' + view.type).tooltip();
- });
- // Add onclick event listener
- $switch_buttons.filter('button').click(_.debounce(function(event) {
- var view_type = $(event.target).data('view-type');
- self.switch_mode(view_type);
- }, 200, true));
- return $switch_buttons;
- }
- };
-
- // Render switch buttons but do not append them to the DOM as this will
- // be done later, simultaneously to all other ControlPanel elements
- this.switch_buttons = {};
- this.switch_buttons.$multi = _render_switch_buttons(multi_record_views);
- this.switch_buttons.$mono = _render_switch_buttons(mono_record_views);
- },
- /**
- * Renders the control elements (buttons, sidebar, pager) of the current view.
- * Fills this.active_view.control_elements dictionnary with the rendered
- * elements and the adequate view switcher, to send to the ControlPanel.
- * Warning: it should be called before calling do_show on the view as the
- * sidebar is extended to listen on the load_record event triggered as soon
- * as do_show is done (the sidebar should thus be instantiated before).
- */
- render_view_control_elements: function() {
- if (!this.active_view.control_elements) {
- var view_controller = this.active_view.controller;
- var $buttons = this.flags.$buttons;
- var elements = {};
- if (!this.flags.headless) {
- elements = {
- $buttons: $("<div>"),
- $sidebar: $("<div>"),
- $pager: $("<div>"),
- };
- }
- view_controller.renderButtons($buttons ? $buttons.empty() : elements.$buttons);
- view_controller.renderSidebar(elements.$sidebar);
- view_controller.renderPager(elements.$pager);
- // Remove the unnecessary outer div
- elements = _.mapObject(elements, function($node) {
- return $node && $node.contents();
- });
- // Use the adequate view switcher (mono- or multi-record)
- if (this.switch_buttons) {
- if (this.active_view.multi_record) {
- elements.$switch_buttons = this.switch_buttons.$multi;
- } else {
- elements.$switch_buttons = this.switch_buttons.$mono;
- }
- }
-
- // Store the rendered elements in the active_view to allow restoring them later
- this.active_view.control_elements = elements;
- }
- return this.active_view.control_elements;
- },
- /**
- * Sets up the current viewmanager's search view.
- * Sets $searchview and $searchview_buttons in searchview_elements to send to the ControlPanel
- */
- setup_search_view: function() {
- var self = this;
- var search_defaults = {};
- var context = this.action.context || [];
- _.each(context, function (value, key) {
- var match = /^search_default_(.*)$/.exec(key);
- if (match) {
- search_defaults[match[1]] = value;
- }
- });
-
- var options = {
- hidden: this.flags.search_view === false,
- disable_custom_filters: this.flags.search_disable_custom_filters,
- $buttons: $("<div>"),
- action: this.action,
- search_defaults: search_defaults,
- };
- // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will
- // be done later, simultaneously to all other ControlPanel elements
- this.searchview = new SearchView(this, this.dataset, this.search_fields_view, options);
-
- return $.when(this.searchview.appendTo($("<div>"))).done(function() {
- self.searchview_elements = {};
- self.searchview_elements.$searchview = self.searchview.$el;
- self.searchview_elements.$searchview_buttons = self.searchview.$buttons.contents();
- });
- },
- _process_search_data: function(domains, contexts, groupbys) {
- // var controller = this.active_view.controller; // the correct view must be loaded here
- var action_context = this.action.context || {};
- var view_context = {}; //controller.get_context();
- var results = pyeval.eval_domains_and_contexts({
- domains: [this.action.domain || []].concat(domains || []),
- contexts: [action_context, view_context].concat(contexts || []),
- group_by_seq: groupbys || [],
- eval_context: this.getSession().user_context,
- });
- if (results.error) {
- throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
- JSON.stringify(results.error)));
- }
- // this.dataset._model = new Model(this.dataset.model, results.context, results.domain);
- // var groupby = results.group_by.length ? results.group_by : action_context.group_by;
- // if (_.isString(groupby)) {
- // groupby = [groupby];
- // }
- // if (!controller.grouped && !_.isEmpty(groupby)){
- // this.dataset.set_sort([]);
- // }
- return {
- context: results.context,
- domain: results.domain,
- groupBy: results.group_by,
- };
- },
- do_push_state: function(state) {
- if (this.action_manager) {
- state.view_type = this.active_view.type;
- this.action_manager.do_push_state(state);
- }
- },
- do_load_state: function(state) {
- var stateChanged = false;
- if ('id' in state && state.id !== '' && state.id !== this.env.currentId) {
- this.env.currentId = state.id;
- stateChanged = true;
- }
- if (state.view_type && state.view_type !== this.active_view.type) {
- stateChanged = true;
- }
- if (stateChanged) {
- return this.switch_mode(state.view_type);
- }
- },
- /**
- * Fetches and executes the action identified by ``action_data``.
- *
- * @param {Object} action_data the action descriptor data
- * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
- * @param {String} [action_data.special=null] special action handlers, closes the dialog if set
- * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
- * @param {Object} [action_data.context=null] additional action context, to add to the current context
- * @param {string} [action_data.effect] if given, a visual effect (a
- * rainbowman by default) will be displayed when the action is complete,
- * with the string (evaluated) given as options.
- * @param {Object} env
- * @param {string} env.model the model of the record(s) triggering the action
- * @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
- * @param {integer} [env.currentID] the id of the record triggering the action
- * @param {Object} [env.context] a context to pass to the action
- * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
- */
- do_execute_action: function (action_data, env, on_closed) {
- var self = this;
- var result_handler = on_closed || function () {};
- var context = new Context(env.context, action_data.context || {});
- // OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
- var recordID = env.currentID || null;
-
- // response handler
- var handler = function (action) {
- // show effect if button have effect attribute
- // Rainbowman can be displayed from two places : from attribute on a button, or from python.
- // Code below handles the first case i.e 'effect' attribute on button.
- var effect = false;
- if (action_data.effect) {
- effect = pyeval.py_eval(action_data.effect);
- };
-
- if (action && action.constructor === Object) {
- // filter out context keys that are specific to the current action.
- // Wrong default_* and search_default_* values will no give the expected result
- // Wrong group_by values will simply fail and forbid rendering of the destination view
- var ncontext = new Context(
- _.object(_.reject(_.pairs(self.env.context), function(pair) {
- return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
- }))
- );
- ncontext.add(action_data.context || {});
- ncontext.add({active_model: env.model});
- if (recordID) {
- ncontext.add({
- active_id: recordID,
- active_ids: [recordID],
- });
- }
- ncontext.add(action.context || {});
- action.context = ncontext;
- // In case effect data is returned from python and also there is rainbow
- // attribute on button, priority is given to button attribute
- action.effect = effect || action.effect;
- return self.do_action(action, {
- on_close: result_handler,
- });
- } else {
- // If action doesn't return anything, but have effect
- // attribute on button, display rainbowman
- self.do_action({"type":"ir.actions.act_window_close", 'effect': effect});
- return result_handler();
- }
- };
-
- if (action_data.special) {
- return handler({"type":"ir.actions.act_window_close"});
- } else if (action_data.type === "object") {
- var args = recordID ? [[recordID]] : [env.resIDs];
- if (action_data.args) {
- try {
- // Warning: quotes and double quotes problem due to json and xml clash
- // Maybe we should force escaping in xml or do a better parse of the args array
- var additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
- args = args.concat(additional_args);
- } catch(e) {
- console.error("Could not JSON.parse arguments", action_data.args);
- }
- }
- args.push(context);
- var dataset = new data.DataSet(this, env.model, env.context);
- return dataset.call_button(action_data.name, args).then(handler);
- } else if (action_data.type === "action") {
- return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
- active_model: env.model,
- active_ids: env.resIDs,
- active_id: recordID,
- })).then(handler);
- }
- },
- destroy: function () {
- if (this.control_elements) {
- if (this.control_elements.$switch_buttons) {
- this.control_elements.$switch_buttons.off();
- }
- }
- return this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * This handler is probably called by a sub form view when the user discards
- * its value. The usual result of this is that we switch back to the
- * previous view (the first in our action stack). We do this in a really
- * stupid way: we trigger a 'history_back' event, and the action manager
- * will call this very view manager to activate the previous view.
- * @todo: directly switch to previous view
- *
- * @private
- */
- _onSwitchToPreviousView: function () {
- this.trigger_up('history_back');
- }
-});
-
-return ViewManager;
-
-});
diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js
index e725aaee8f6..c39917d5118 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -83,7 +83,7 @@ return AbstractWebClient.extend({
action_buttons: true,
headless: true,
};
- self.action_manager.do_action(result).then(function () {
+ self.action_manager.doAction(result).then(function () {
var form = self.action_manager.dialog_widget.views.form.controller;
form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
form.on('record_saved', self, function() {
@@ -110,7 +110,7 @@ return AbstractWebClient.extend({
.done(function(result) {
var data = result[0];
if(data.action_id) {
- self.action_manager.do_action(data.action_id[0]);
+ self.action_manager.doAction(data.action_id[0]);
self.menu.open_action(data.action_id[0]);
} else {
var first_menu_id = self.menu.$el.find("a:first").data("menu");
@@ -140,11 +140,10 @@ return AbstractWebClient.extend({
self.menu.menu_click(state.menu_id);
});
} else {
- state._push_me = false; // no need to push state back...
- self.action_manager.do_load_state(state, !!self._current_state).then(function () {
- var action = self.action_manager.get_inner_action();
+ self.action_manager.loadState(state, !!self._current_state).then(function () {
+ var action = self.action_manager.getCurrentAction();
if (action) {
- self.menu.open_action(action.action_descr.id);
+ self.menu.open_action(action.id);
}
});
}
@@ -163,10 +162,10 @@ return AbstractWebClient.extend({
.then(function (result) {
return self.action_mutex.exec(function() {
var completed = $.Deferred();
- $.when(self.action_manager.do_action(result, {
+ self.action_manager.doAction(result, {
clear_breadcrumbs: true,
action_menu_id: self.menu.current_menu,
- })).fail(function() {
+ }).fail(function() {
self.menu.open_menu(options.previous_menu_id);
}).always(function() {
completed.resolve();
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index 0bc370f782a..d5be248b16e 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -165,12 +165,12 @@ var Dialog = Widget.extend({
this.destroy();
},
- destroy: function (reason) {
+ destroy: function (arg) {
// Need to trigger before real destroy but if 'closed' handler destroys
// the widget again, we want to avoid infinite recursion
if (!this.__closed) {
this.__closed = true;
- this.trigger("closed", reason);
+ this.trigger("closed", arg);
}
if (this.isDestroyed()) {
diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js
index 99c21c14038..be0f012752c 100644
--- a/addons/web/static/src/js/core/misc.js
+++ b/addons/web/static/src/js/core/misc.js
@@ -140,17 +140,6 @@ function Home (parent, action) {
}
core.action_registry.add("home", Home);
-/**
- * Client action to go back in breadcrumb history.
- * If can't go back in history stack, will go back to home.
- */
-function HistoryBack (parent) {
- parent.history_back().fail(function () {
- Home(parent);
- });
-}
-core.action_registry.add("history_back", HistoryBack);
-
function login() {
redirect('/web/login');
}
diff --git a/addons/web/static/src/js/report/client_action.js b/addons/web/static/src/js/report/client_action.js
index cf6f4c94dfe..2fd2ccd44b3 100644
--- a/addons/web/static/src/js/report/client_action.js
+++ b/addons/web/static/src/js/report/client_action.js
@@ -94,7 +94,6 @@ var ReportAction = Widget.extend(ControlPanelMixin, {
_update_control_panel: function () {
this.update_control_panel({
- breadcrumbs: this.action_manager.get_breadcrumbs(),
cp_content: {
$buttons: this.$buttons,
},
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index 3bdf8ab08ca..150b4803a7a 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -12,12 +12,16 @@ odoo.define('web.AbstractController', function (require) {
* reading localstorage, ...) has to go through the controller.
*/
+var ControlPanelMixin = require('web.ControlPanelMixin');
+var core = require('web.core');
var Widget = require('web.Widget');
+var QWeb = core.qweb;
-var AbstractController = Widget.extend({
+var AbstractController = Widget.extend(ControlPanelMixin, {
custom_events: {
open_record: '_onOpenRecord',
+ switch_view: '_onSwitchView',
},
events: {
'click a[type="action"]': '_onActionClicked',
@@ -30,8 +34,15 @@ var AbstractController = Widget.extend({
* @param {AbstractRenderer} renderer
* @param {object} params
* @param {string} params.modelName
+ * @param {string} [params.controllerID] an id to ease the communication
+ * with upstream components
* @param {any} [params.handle] a handle that will be given to the model (some id)
* @param {any} params.initialState the initialState
+ * @param {boolean} params.isMultiRecord
+ * @param {Object[]} params.actionViews
+ * @param {string} params.viewType
+ * @param {boolean} params.withControlPanel set to false to hide the
+ * ControlPanel
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
@@ -40,7 +51,23 @@ var AbstractController = Widget.extend({
this.modelName = params.modelName;
this.handle = params.handle;
this.activeActions = params.activeActions;
+ this.controllerID = params.controllerID;
this.initialState = params.initialState;
+
+ // those arguments are temporary, they won't be necessary as soon as the
+ // ControlPanel will be handled by the View
+ this.displayName = params.displayName;
+ this.isMultiRecord = params.isMultiRecord;
+ this.searchable = params.searchable;
+ this.searchView = params.searchView;
+ this.searchViewHidden = params.searchViewHidden;
+ this.actionViews = params.actionViews;
+ this.viewType = params.viewType;
+ this.withControlPanel = params.withControlPanel !== false;
+ // override this.need_control_panel so that the ActionManager doesn't
+ // update the control panel when it isn't visible (this is a temporary
+ // hack that can be removed as soon as the CP'll be handled by the view)
+ this.need_control_panel = this.withControlPanel;
},
/**
* Simply renders and updates the url.
@@ -48,10 +75,19 @@ var AbstractController = Widget.extend({
* @returns {Deferred}
*/
start: function () {
+ var self = this;
+
+ this.$el.addClass('o_view_controller');
+
+ // render the ControlPanel elements (buttons, pager, sidebar...)
+ this.controlPanelElements = this._renderControlPanelElements();
+
return $.when(
this._super.apply(this, arguments),
this.renderer.appendTo(this.$el)
- ).then(this._update.bind(this, this.initialState));
+ ).then(function () {
+ self._update(self.initialState);
+ });
},
/**
* @override
@@ -60,6 +96,9 @@ var AbstractController = Widget.extend({
if (this.$buttons) {
this.$buttons.off();
}
+ if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
+ this.controlPanelElements.$switch_buttons.off();
+ }
return this._super.apply(this, arguments);
},
@@ -70,7 +109,7 @@ var AbstractController = Widget.extend({
/**
* Discards the changes made on the record associated to the given ID, or
* all changes made by the current controller if no recordID is given. For
- * example, when the user open the 'home' screen, the view manager will call
+ * example, when the user opens the 'home' screen, the action manager calls
* this method on the active view to make sure it is ok to open the home
* screen (and lose all current state).
*
@@ -96,23 +135,15 @@ var AbstractController = Widget.extend({
return {};
},
/**
- * @see setScrollTop
- * @returns {number}
- */
- getScrollTop: function () {
- return this.scrollTop;
- },
- /**
* Returns a title that may be displayed in the breadcrumb area. For
* example, the name of the record.
*
- * Note: this seems wrong right now, it should not be implemented, we have
- * no guarantee that there is a display_name variable in a controller.
+ * note: this will be moved to AbstractAction
*
* @returns {string}
*/
getTitle: function () {
- return this.display_name;
+ return this.displayName;
},
/**
* The use of this method is discouraged. It is still snakecased, because
@@ -136,12 +167,9 @@ var AbstractController = Widget.extend({
return this.update(params || {});
},
/**
- * Most likely called by the view manager, this method is responsible for
- * adding buttons in the control panel (buttons such as save/discard/...)
- *
- * Note that there is no guarantee that this method will be called. The
- * controller is supposed to work even without a view manager, for example
- * in the frontend (odoo frontend = public website)
+ * Renders the buttons to append, in most cases, to the control panel (in
+ * the bottom left corner). When the view is rendered in a dialog, those
+ * buttons might be moved to the dialog's footer.
*
* @param {jQuery Node} $node
*/
@@ -153,7 +181,7 @@ var AbstractController = Widget.extend({
* controller can actually render whatever he wants in the pager zone. If
* your view does not want a pager, just let this method empty.
*
- * @param {Query Node} $node
+ * @param {jQuery Node} $node
*/
renderPager: function ($node) {
},
@@ -161,21 +189,11 @@ var AbstractController = Widget.extend({
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu
* dropdown in the control panel next to the buttons)
*
- * @param {Query Node} $node
+ * @param {jQuery Node} $node
*/
renderSidebar: function ($node) {
},
/**
- * Not sure about this one, it probably needs to be reworked, maybe merged
- * in get/set local state methods.
- *
- * @see getScrollTop
- * @param {number} scrollTop
- */
- setScrollTop: function (scrollTop) {
- this.scrollTop = scrollTop;
- },
- /**
* This is the main entry point for the controller. Changes from the search
* view arrive in this method, and internal changes can sometimes also call
* this method. It is basically the way everything notifies the controller
@@ -219,7 +237,68 @@ var AbstractController = Widget.extend({
* world
*/
_pushState: function (state) {
- this.trigger_up('push_state', state || {});
+ this.trigger_up('push_state', {
+ controllerID: this.controllerID,
+ state: state || {},
+ });
+ },
+ /**
+ * Renders the control elements (buttons, pager and sidebar) of the current
+ * view.
+ *
+ * @private
+ * @returns {Object} an object containing the control panel jQuery elements
+ */
+ _renderControlPanelElements: function () {
+ var elements = {};
+
+ if (this.withControlPanel) {
+ elements = {
+ $buttons: $('<div>'),
+ $sidebar: $('<div>'),
+ $pager: $('<div>'),
+ };
+
+ this.renderButtons(elements.$buttons);
+ this.renderSidebar(elements.$sidebar);
+ this.renderPager(elements.$pager);
+ // remove the unnecessary outer div
+ elements = _.mapObject(elements, function($node) {
+ return $node && $node.contents();
+ });
+ elements.$switch_buttons = this._renderSwitchButtons();
+ }
+
+ return elements;
+ },
+ /**
+ * Renders the switch buttons and binds listeners on them.
+ *
+ * @private
+ * @returns {jQuery}
+ */
+ _renderSwitchButtons: function () {
+ var self = this;
+ var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});
+
+ if (views.length <= 1) {
+ return $();
+ }
+
+ var $switchButtons = $(QWeb.render('ControlPanel.SwitchButtons', {
+ views: views,
+ }));
+ // create bootstrap tooltips
+ _.each(views, function (view) {
+ $switchButtons.filter('.o_cp_switch_' + view.type).tooltip();
+ });
+ // add onclick event listener
+ $switchButtons.filter('button').click(_.debounce(function (event) {
+ var viewType = $(event.target).data('view-type');
+ self.trigger_up('switch_view', {view_type: viewType});
+ }, 200, true));
+
+ return $switchButtons;
},
/**
* This method is called after each update or when the start method is
@@ -235,6 +314,22 @@ var AbstractController = Widget.extend({
* @returns {Deferred}
*/
_update: function (state) {
+ // AAB: update the control panel -> this will be moved elsewhere at some point
+ var cpContent = _.extend({}, this.controlPanelElements);
+ if (this.searchView) {
+ _.extend(cpContent, {
+ $searchview: this.searchView.$el,
+ $searchview_buttons: this.searchView.$buttons.contents(),
+ });
+ }
+ this.update_control_panel({
+ active_view_selector: '.o_cp_switch_' + this.viewType,
+ cp_content: cpContent,
+ hidden: !this.withControlPanel,
+ searchview: this.searchView,
+ search_view_hidden: !this.searchable || this.searchviewHidden,
+ });
+
this._pushState();
return $.when();
},
@@ -249,7 +344,7 @@ var AbstractController = Widget.extend({
*
* Note: this method seems wrong, it relies on the model being a basic model,
* to get the res_id. It should receive the res_id in the event data
- * @todo move this to basic controller? or view manager
+ * @todo move this to basic controller?
*
* @private
* @param {OdooEvent} event
@@ -278,6 +373,15 @@ var AbstractController = Widget.extend({
event.preventDefault();
this.do_action(event.target.name);
},
+ /**
+ * Intercepts the 'switch_view' event to add the controllerID into the data,
+ * and lets the event bubble up.
+ *
+ * @param {OdooEvent} event
+ */
+ _onSwitchView: function (event) {
+ event.data.controllerID = this.controllerID;
+ },
});
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 2a1ba16a33e..58b2c85a670 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -67,11 +67,18 @@ var AbstractView = Class.extend({
* @param {string} params.modelName The actual model name
* @param {Object} params.context
* @param {number} [params.count]
+ * @param {string} [params.controllerID]
* @param {string[]} params.domain
* @param {string[]} params.groupBy
* @param {number} [params.currentId]
* @param {number[]} [params.ids]
+ * @param {boolean} [params.withControlPanel]
+ * @param {boolean} [params.action.flags.headless]
+ * @param {string} [params.action.display_name]
+ * @param {string} [params.action.name]
* @param {string} [params.action.help]
+ * @param {string} [params.action.jsID]
+ * @param {boolean} [params.action.views]
*/
init: function (viewInfo, params) {
this.rendererParams = {
@@ -87,7 +94,23 @@ var AbstractView = Class.extend({
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true,
},
+ controllerID: params.controllerID,
};
+ // AAB: these params won't be necessary as soon as the ControlPanel will
+ // be instantiated by the View
+ this.controllerParams.displayName = params.action && (params.action.display_name || params.action.name);
+ this.controllerParams.isMultiRecord = this.multi_record;
+ this.controllerParams.searchable = this.searchable;
+ this.controllerParams.searchView = params.action && params.action.searchView;
+ this.controllerParams.searchViewHidden = this.searchview_hidden; // AAB: use searchable instead where it is used?
+ this.controllerParams.actionViews = params.action ? params.action.views : [];
+ this.controllerParams.viewType = this.viewType;
+ this.controllerParams.withControlPanel = true;
+ if (params.action && params.action.flags) {
+ this.controllerParams.withControlPanel = !params.action.flags.headless;
+ } else if ('withControlPanel' in params) {
+ this.controllerParams.withControlPanel = params.withControlPanel;
+ }
this.loadParams = {
context: params.context,
@@ -133,7 +156,7 @@ var AbstractView = Class.extend({
* load itself, and the renderer needs the data in its constructor.
*
* @param {Widget} parent The parent of the resulting Controller (most
- * likely a view manager)
+ * likely an action manager)
* @returns {Deferred} The deferred resolves to a controller
*/
getController: function (parent) {
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 1c8060a6fe1..2c7c1c40210 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -200,7 +200,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
_abandonRecord: function (recordID) {
recordID = recordID || this.handle;
if (recordID === this.handle) {
- this.trigger_up('switch_to_previous_view');
+ this.trigger_up('history_back');
} else {
this.model.removeLine(recordID);
}
@@ -244,17 +244,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
model: record.model,
resIDs: record.res_ids,
},
- on_closed: function (reason) {
- if (!_.isObject(reason)) {
- reload(reason);
- }
- },
- on_fail: function (reason) {
- reload().always(function() {
- def.reject(reason);
- });
- },
on_success: def.resolve.bind(def),
+ on_fail: function () {
+ reload().always(def.reject.bind(def));
+ },
+ on_closed: reload,
});
return this.alive(def);
},
@@ -313,13 +307,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {Object} [options]
* @param {boolean} [options.readonlyIfRealDiscard=false]
* After discarding record changes, the usual option is to make the
- * record readonly. However, the view manager calls this function
+ * record readonly. However, the action manager calls this function
* at inappropriate times in the current code and in that case, we
* don't want to go back to readonly if there is nothing to discard
* (e.g. when switching record in edit mode in form view, we expect
* the new record to be in edit mode too, but the view manager calls
* this function as the URL changes...) @todo get rid of this when
- * the view manager is improved.
+ * the webclient/action_manager's hashchange mechanism is improved.
* @returns {Deferred}
*/
_discardChanges: function (recordID, options) {
@@ -467,7 +461,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var sidebarEnv = this._getSidebarEnv();
this.sidebar.updateEnv(sidebarEnv);
}
- this.trigger_up('env_updated', env);
+ this.trigger_up('env_updated', {controllerID: this.controllerID, env: env});
},
/**
* Helper method, to make sure the information displayed by the pager is up
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 2820dad448f..7b279d44bcd 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -149,7 +149,7 @@ var FormController = BasicController.extend({
* inserted
**/
renderSidebar: function ($node) {
- if (!this.sidebar && this.hasSidebar) {
+ if (this.hasSidebar) {
var otherItems = [];
if (this.is_action_enabled('delete')) {
otherItems.push({
@@ -280,7 +280,7 @@ var FormController = BasicController.extend({
_onDeletedRecords: function () {
var state = this.model.get(this.handle, {raw: true});
if (!state.res_ids.length) {
- this.do_action('history_back');
+ this.trigger_up('history_back');
} else {
this._super.apply(this, arguments);
}
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 42cf5de12e4..2851d3ac72a 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -2,7 +2,6 @@ odoo.define('web.FormView', function (require) {
"use strict";
var BasicView = require('web.BasicView');
-var Context = require('web.Context');
var core = require('web.core');
var FormController = require('web.FormController');
var FormRenderer = require('web.FormRenderer');
@@ -25,15 +24,16 @@ var FormView = BasicView.extend({
init: function (viewInfo, params) {
this._super.apply(this, arguments);
- var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
+ var mode = params.mode || params.context.form_view_initial_mode ||
+ (params.currentId ? 'readonly' : 'edit');
this.loadParams.type = 'record';
this.controllerParams.disableAutofocus = params.disable_autofocus;
- this.controllerParams.hasSidebar = params.sidebar;
+ this.controllerParams.hasSidebar = params.hasSidebar;
this.controllerParams.toolbarActions = viewInfo.toolbar;
- this.controllerParams.footerToButtons = params.footer_to_buttons;
+ this.controllerParams.footerToButtons = params.footerToButtons;
if ('action' in params && 'flags' in params.action) {
- this.controllerParams.footerToButtons = params.action.flags.footer_to_buttons;
+ this.controllerParams.footerToButtons = params.action.flags.footerToButtons;
}
var defaultButtons = 'default_buttons' in params ? params.default_buttons : true;
this.controllerParams.defaultButtons = defaultButtons;
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 982d5456bca..33c1d234c7a 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -128,7 +128,7 @@ var ListController = BasicController.extend({
* @param {jQuery Node} $node
*/
renderSidebar: function ($node) {
- if (this.hasSidebar && !this.sidebar) {
+ if (this.hasSidebar) {
var other = [{
label: _t("Export"),
callback: this._onExportData.bind(this)
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index 8a989dad35e..e970f825846 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -30,7 +30,7 @@ var ListView = BasicView.extend({
*
* @param {Object} viewInfo
* @param {Object} params
- * @param {boolean} params.sidebar
+ * @param {boolean} params.hasSidebar
* @param {boolean} [params.hasSelectors=true]
*/
init: function (viewInfo, params) {
@@ -40,7 +40,7 @@ var ListView = BasicView.extend({
var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly";
this.controllerParams.editable = arch.attrs.editable;
- this.controllerParams.hasSidebar = params.sidebar;
+ this.controllerParams.hasSidebar = params.hasSidebar;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
this.controllerParams.mode = mode;
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index aea29d96ee2..72e215f9c24 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -175,8 +175,9 @@ var FormViewDialog = ViewDialog.extend({
currentId: self.res_id || undefined,
index: 0,
mode: self.res_id && self.options.readonly ? 'readonly' : 'edit',
- footer_to_buttons: true,
+ footerToButtons: true,
default_buttons: false,
+ withControlPanel: false,
model: self.model,
parentID: self.parentID,
recordID: self.recordID,
@@ -190,9 +191,9 @@ var FormViewDialog = ViewDialog.extend({
}
self.form_view.appendTo(fragment)
.then(function () {
- var $buttons = $('<div>');
- self.form_view.renderButtons($buttons);
self.opened().always(function () {
+ var $buttons = $('<div>');
+ self.form_view.renderButtons($buttons);
if ($buttons.children().length) {
self.$footer.empty().append($buttons.contents());
}
@@ -257,7 +258,7 @@ var SelectCreateDialog = ViewDialog.extend({
this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length);
},
search: function (event) {
- event.stopPropagation(); // prevent this event from bubbling up to the view manager
+ event.stopPropagation(); // prevent this event from bubbling up to the action manager
var d = event.data;
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
this.list_controller.reload(searchData);
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index 2c9cf3d4457..50e0497b91b 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -10,7 +10,6 @@ var field_utils = require('web.field_utils');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var utils = require('web.utils');
-var ViewManager = require('web.ViewManager');
var WebClient = require('web.WebClient');
var Widget = require('web.Widget');
@@ -332,42 +331,35 @@ DebugManager.include({
);
},
update: function (tag, descriptor, widget) {
- switch (tag) {
- case 'action':
- if (this._view_manager) {
- this._view_manager.off('switch_mode', this);
- }
- if (!(widget instanceof ViewManager)) {
- this._active_view = null;
- this._view_manager = null;
- break;
- }
- this._view_manager = widget;
- widget.on('switch_mode', this, function () {
- this.update('view', null, widget);
- });
- case 'view':
- this._active_view = widget.active_view;
+ if (tag === 'action' || tag === 'view') {
+ this._controller = widget;
}
-
return this._super(tag, descriptor).then(function () {
this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', {
- manager: this,
action: this._action,
- view: this._active_view,
can_edit: this._can_edit_views,
- searchview: this._view_manager && this._view_manager.searchview,
+ controller: this._controller,
+ manager: this,
+ searchview: this._controller && this._controller.searchView,
+ view: this._controller && _.findWhere(this._action.views, {
+ type: this._controller.viewType,
+ }),
}));
}.bind(this));
},
get_metadata: function() {
- var ds = this._view_manager.dataset;
- if (!this._active_view.controller.getSelectedIds().length) {
+ var self = this;
+ var selectedIDs = this._controller.getSelectedIds();
+ if (!selectedIDs.length) {
console.warn(_t("No metadata available"));
return;
}
- ds.call('get_metadata', [this._active_view.controller.getSelectedIds()]).done(function(result) {
+ this._rpc({
+ model: this._action.res_model,
+ method: 'get_metadata',
+ args: [selectedIDs],
+ }).done(function(result) {
var metadata = result[0];
metadata.creator = field_utils.format.many2one(metadata.create_uid);
metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid);
@@ -376,7 +368,7 @@ DebugManager.include({
var modificationDate = field_utils.parse.datetime(metadata.write_date);
metadata.write_date = field_utils.format.datetime(modificationDate);
new Dialog(this, {
- title: _.str.sprintf(_t("Metadata (%s)"), ds.model),
+ title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model),
size: 'medium',
$content: QWeb.render('WebClient.DebugViewLog', {
perm : metadata,
@@ -400,11 +392,12 @@ DebugManager.include({
return [value, displayed];
};
- var renderer = this._active_view.controller.renderer;
- var fields = self._active_view.fields_view.fields;
- var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
- var fieldNamesInView = renderer.state.getFieldNames();
- var fieldsValues = renderer.state.data;
+ var renderer = this._controller.renderer;
+ var state = renderer.state;
+ var fields = state.fields;
+ var fieldsInfo = state.fieldsInfo.form;
+ var fieldNamesInView = state.getFieldNames();
+ var fieldsValues = state.data;
var modifierDatas = {};
_.each(fieldNamesInView, function (fieldName) {
modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
@@ -416,14 +409,14 @@ DebugManager.include({
var modifierData = modifierDatas[fieldName];
var invisibleOrReadOnly;
if (modifierData) {
- var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
+ var evaluatedModifiers = modifierData.evaluatedModifiers[state.id];
invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
}
var fieldInfo = fields[fieldName];
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
var value = valueDisplayed[0];
var displayed = valueDisplayed[1];
- // ignore fields which are empty, invisible, readonly, o2m
+ // ignore fields which are empty, invisible, readonly, o2m
// or m2m
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
@@ -479,7 +472,7 @@ DebugManager.include({
model: 'ir.default',
method: 'set',
args: [
- self._active_view.fields_view.model,
+ self._controller.fields_view.model,
fieldToSet,
value,
allUsers,
@@ -502,7 +495,7 @@ DebugManager.include({
var dialog = new Dialog(this, { title: _t("Fields View Get") });
dialog.opened().then(function () {
$('<pre>').text(utils.json_node_to_xml(
- self._active_view.controller.renderer.arch, true)
+ self._controller.renderer.arch, true)
).appendTo(dialog.$el);
});
dialog.open();
@@ -788,30 +781,80 @@ if (config.debug) {
SystrayMenu.Items.push(DebugManager);
WebClient.include({
- current_action_updated: function(action) {
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ current_action_updated: function (action, controller) {
this._super.apply(this, arguments);
- var action_descr = action && action.action_descr;
- var action_widget = action && action.widget;
- var debug_manager = _.find(this.systray_menu.widgets, function(item) {return item instanceof DebugManager; });
- debug_manager.update('action', action_descr, action_widget);
+ var debugManager = _.find(this.systray_menu.widgets, function(item) {
+ return item instanceof DebugManager;
+ });
+ debugManager.update('action', action, controller && controller.widget);
+ },
+ });
+
+ ActionManager.include({
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * Returns the action of the controller currently opened in a dialog,
+ * i.e. a target='new' action, if any.
+ *
+ * @returns {Object|null}
+ */
+ getCurrentActionInDialog: function () {
+ if (this.currentDialogController) {
+ return this.actions[this.currentDialogController.actionID];
+ }
+ return null;
+ },
+ /**
+ * Returns the controller currently opened in a dialog, if any.
+ *
+ * @returns {Object|null}
+ */
+ getCurrentControllerInDialog: function () {
+ return this.currentDialogController;
},
});
Dialog.include({
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
open: function() {
var self = this;
-
- var parent = self.getParent();
- if (parent instanceof ActionManager && parent.dialog_widget) {
- // Instantiate the DebugManager and insert it into the DOM once dialog is opened
- this.opened(function() {
- self.debug_manager = new DebugManager(self);
- var $header = self.$modal.find('.modal-header:first');
- return self.debug_manager.prependTo($header).then(function() {
- self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
- });
- });
- }
+ // if the dialog is opened by the ActionManager, instantiate a
+ // DebugManager and insert it into the DOM once the dialog is opened
+ // (delay this with a setTimeout(0) to ensure that the internal
+ // state, i.e. the current action and controller, of the
+ // ActionManager is set to properly update the DebugManager)
+ this.opened(function() {
+ setTimeout(function () {
+ var parent = self.getParent();
+ if (parent instanceof ActionManager) {
+ var action = parent.getCurrentActionInDialog();
+ if (action) {
+ var controller = parent.getCurrentControllerInDialog();
+ self.debugManager = new DebugManager(self);
+ var $header = self.$modal.find('.modal-header:first');
+ return self.debugManager.prependTo($header).then(function () {
+ self.debugManager.update('action', action, controller.widget);
+ });
+ }
+ }
+ }, 0);
+ });
return this._super.apply(this, arguments);
},