[REF] Web: Actions, ActionManager, ViewManager, Controlpanel refactoring

Big refactoring of Action, WidgetAction and ViewManagerAction to clean
their API. Mainly, renamming from State to Action and removal of unused
functions (or functions that are only used by the DebugManager, for
which we tolerate introspection).

Some logic has also been moved from the ActionManager to the Actions
themselves. The specific behavior of each type of Action is now handled
in the implementation of those Actions.

In ActionManager, this.states is renammed to this.action_stack to be
consistent with view_stack in ViewManager.

The setup_search_view() and render_switch_buttons() functions have been
moved from ControlPanel to ViewManager as it was before, since actual
design allows it and it makes more sense.

The ControlPanel API has also been simplified as some functions were not
used anymore. The ControlPanel is now updated directly by the widgets
using it, bypassing the ActionManager. The control panel elements are
stored by the widgets themselves to allow restoring them later.
This commit is contained in:
Aaron Bohy
2015-03-20 16:34:41 +01:00
parent 913942a327
commit 20b4aaab83
6 changed files with 348 additions and 396 deletions
+5 -5
View File
@@ -539,12 +539,12 @@ var bankStatementReconciliation = Widget.extend({
.call("get_object_reference", ['account', 'action_bank_statement_tree'])
.then(function (result) {
var action_id = result[1];
var breadcrumbs = self.action_manager.get_states();
var state = _.find(breadcrumbs, function(state){
return state.widget.action && state.widget.action.id === action_id;
var action_stack = self.action_manager.get_action_stack();
var action = _.find(action_stack, function(action){
return action.get_action_descr() && action.get_action_descr().id === action_id;
});
if (state) {
self.action_manager.select_state(state, 0);
if (action) {
self.action_manager.select_action(action, 0);
} else {
self.action_manager.do_action(action_id, {
clear_breadcrumbs: true
+194 -174
View File
@@ -12,94 +12,144 @@ var session = require('web.session');
var ViewManager = require('web.ViewManager');
var Widget = require('web.Widget');
var State = core.Class.extend({
init: function(title, flags) {
this.title = title;
this.flags = flags;
/**
* 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) {
return this.__on_reverse_breadcrumb();
}
},
/**
* 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} [on_reverse_breadcrumb] the callback
*/
set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) {
this.on_reverse_breadcrumb = on_reverse_breadcrumb;
},
/**
* @return {Object} the description of the action
*/
get_action_descr: function() {
return this.action_descr;
},
/**
* @return {Object} dictionnary that will be interpreted to display the breadcrumbs
*/
get_breadcrumbs: function() {
return { title: this.title, action: this };
},
/**
* @return {int} the number of views stacked, i.e. 0 for client functions
*/
get_nb_views: function() {
return 0;
},
enable: function() { return $.when(); },
disable: function() {},
destroy: function() {},
get_widget: function() {},
get_title: function() { return this.title; },
is_headless: function() { return this.flags.headless; },
get_action: function() {},
get_active_view: function() {},
get_dataset: function() {},
get_searchview: function() {},
});
var WidgetState = State.extend({
init: function(title, flags, widget) {
this._super(title, flags);
/**
* Specialization of Action for client actions that are Widgets
*/
var WidgetAction = Action.extend({
/**
* Initializes the WidgetAction
* Sets the title of the widget
*/
init: function(action, widget) {
this._super(action);
this.widget = widget;
if (this.widget.get('title')) {
this.title = title;
} else {
this.widget.set('title', title);
if (!this.widget.get('title')) {
this.widget.set('title', this.title);
}
this.widget.on("do_search", this, function() {
// active_search = this.control_panel.activate_search(view.created);
// call activate search on search view
// domain context and groupby computed are important
});
},
set_cp_content: function(content) { this.cp_content = content; },
get_cp_content: function() { return this.cp_content; },
get_action: function() { return this.widget.action; },
get_active_view: function() { return this.widget.active_view; },
get_dataset: function() { return this.widget.dataset; },
get_searchview: function() { return this.widget.get_search_view(); },
/**
* Restores WidgetAction by calling do_show on its widget
*/
restore: function() {
this.widget.do_show();
return this._super();
},
/**
* Destroys the widget
*/
destroy: function() {
if (this.cp_content && this.cp_content.searchview) {
this.cp_content.searchview.destroy();
}
this.widget.destroy();
},
get_widget: function() {
return this.widget;
},
});
var FunctionState = State.extend({
init: function(title, flags) {
this._super(title, flags);
this.widget = {
view_stack: [{
controller: { get: function () { return this.title; }}
}]
};
}
/**
* 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 self = this;
var _super = this._super;
return this.widget.select_view(view_index).then(function() {
_super.call(self);
});
},
/**
* @return {Array} array of Objects that will be interpreted to display the breadcrumbs
*/
get_breadcrumbs: function() {
var self = this;
return this.widget.view_stack.map(function (view, index) {
return {
title: view.controller.get('title') || self.title,
index: index,
action: self,
};
});
},
/**
* @return {int} the number of views stacked in the ViewManager
*/
get_nb_views: function() {
return this.widget.view_stack.length;
},
});
var ActionManager = Widget.extend({
template: "ActionManager",
init: function(parent) {
this._super(parent);
this.action_stack = [];
this.inner_action = null;
this.inner_widget = null;
this.webclient = parent;
this.dialog = null;
this.dialog_widget = null;
this.states = [];
this.on('history_back', this, this.proxy('history_back'));
},
start: function() {
this._super();
// Instantiate the unique main control panel used by every widget in this.states
// Instantiate a unique main ControlPanel used by widgets of actions in this.action_stack
this.main_control_panel = new ControlPanel(this);
// Listen to event "switch_view" trigerred on the control panel when clicking
// on switch buttons. Forward this event to the current inner_widget
this.main_control_panel.on("switch_view", this, function(view_type) {
this.inner_widget.trigger("switch_view", view_type);
});
// Listen to event "on_breadcrumb_click" trigerred on the control panel when
// clicking on a part of the breadcrumbs. Call select_state for this breadcrumb.
this.main_control_panel.on("on_breadcrumb_click", this, function(state, index) {
this.select_state(state, index);
// clicking on a part of the breadcrumbs. Call select_action for this breadcrumb.
this.main_control_panel.on("on_breadcrumb_click", this, function(action, index) {
this.select_action(action, index);
});
// Append the main control panel to the DOM (inside the ActionManager jQuery element)
@@ -112,90 +162,76 @@ var ActionManager = Widget.extend({
this.dialog = null;
},
/**
* Add a new state to the action manager
* Add a new action to the action manager
*
* widget: typically, widgets added are web.ViewManager. The action manager
* uses this list of widget to handle the breadcrumbs.
* action: new action
* options.on_reverse_breadcrumb: will be called when breadcrumb is selected
* options.clear_breadcrumbs: boolean, if true, current widgets are destroyed
* widget: typically, widgets added are openerp.web.ViewManager. The action manager
* uses the stack of actions to handle the breadcrumbs.
* action_descr: new action description
* options.on_reverse_breadcrumb: will be called when breadcrumb is clicked on
* options.clear_breadcrumbs: boolean, if true, action stack is destroyed
*/
push_state: function(widget, action, options) {
var self = this,
to_destroy,
old_widget = this.inner_widget,
old_state = this.get_current_state();
push_action: function(widget, action_descr, options) {
var self = this;
var to_destroy;
var old_widget = this.inner_widget;
var old_action = this.inner_action;
options = options || {};
// Empty action_stack if requested
if (options.clear_breadcrumbs) {
to_destroy = this.states;
this.states = [];
to_destroy = this.action_stack;
this.action_stack = [];
}
var new_state,
title = action.display_name || action.name;
if (widget instanceof Widget) {
new_state = new WidgetState(title, action.flags, widget);
// 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_state = new FunctionState(title, action.flags);
new_action = new Action(action_descr);
}
this.states.push(new_state);
this.get_current_state().__on_reverse_breadcrumb = options.on_reverse_breadcrumb;
this.inner_action = action;
// Set on_reverse_breadcrumb callback on previous inner_action
if (old_action) {
old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb);
}
// Update action_stack
this.action_stack.push(new_action);
this.inner_action = new_action;
this.inner_widget = widget;
// Sets the main ControlPanel state
// AAB: temporary restrict the use of main control panel to act_window actions
if (action.type === 'ir.actions.act_window') {
this.main_control_panel.set_state(new_state, old_state);
if (old_state) {
// Save the previous state control_panel content to restore it later
old_state.cp_content = this.main_control_panel.get_content();
}
}
// Append the inner_widget and hide the old one
return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
if (old_widget) {
old_widget.$el.hide();
}
if (options.clear_breadcrumbs) {
self.clear_states(to_destroy);
self.clear_action_stack(to_destroy);
}
});
},
get_breadcrumbs: function () {
return _.flatten(_.map(this.states, function (state) {
if (state.widget instanceof ViewManager) {
return state.widget.view_stack.map(function (view, index) {
return {
title: view.controller.get('title') || state.get_title(),
index: index,
widget: state,
};
});
} else {
return { title: state.get_title(), widget: state };
}
return _.flatten(_.map(this.action_stack, function (action) {
return action.get_breadcrumbs();
}), true);
},
get_title: function () {
if (this.states.length === 1) {
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 state = this.states[0];
if (state.widget instanceof ViewManager && state.widget.view_stack.length === 1) {
return state.get_title();
var action = this.action_stack[0];
if (action.get_breadcrumbs().length === 1) {
return action.title;
}
}
return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
},
get_states: function () {
return this.states;
},
get_current_state: function() {
return _.last(this.states);
get_action_stack: function () {
return this.action_stack;
},
get_inner_action: function() {
return this.inner_action;
@@ -204,54 +240,38 @@ var ActionManager = Widget.extend({
return this.inner_widget;
},
history_back: function() {
var state = this.get_current_state();
if (state.widget instanceof ViewManager) {
var nbr_views = state.widget.view_stack.length;
if (nbr_views > 1) {
return this.select_state(state, nbr_views - 2);
}
}
if (this.states.length > 1) {
state = this.states[this.states.length - 2];
var index = state.widget.view_stack && state.widget.view_stack.length - 1;
return this.select_state(state, index);
var nb_views = this.inner_action.get_nb_views();
if (nb_views > 1) {
// Stay on this action, but select the previous view
return this.select_action(this.inner_action, nb_views - 2);
}
if (this.action_stack.length > 1) {
// Select the previous action
var action = this.action_stack[this.action_stack.length - 2];
nb_views = action.get_nb_views();
return this.select_action(action, nb_views - 1);
}
return $.Deferred().reject();
},
select_state: function(state, index) {
var self = this;
select_action: function(action, index) {
if (this.webclient.has_uncommitted_changes()) {
return $.Deferred().reject();
}
// Client widget (-> put in ClientState?)
if (state.__on_reverse_breadcrumb) {
state.__on_reverse_breadcrumb();
}
// Set the control panel new state
// Put in VMState?
// Inform the ControlPanel that the current state changed (mainly restore the searchview
// for the ViewManager to be able to do select_view, i.e. switch_mode)
// Storing in old_state is not necessary here
var old_state = this.get_current_state();
this.main_control_panel.set_state(state, old_state);
var state_index = this.states.indexOf(state),
def = $.when(state.widget.select_view && state.widget.select_view(index));
// Set the new inner_widget and clear the action_stack
this.inner_widget = action.widget;
var action_index = this.action_stack.indexOf(action);
this.clear_action_stack(this.action_stack.splice(action_index + 1));
this.clear_states(this.states.splice(state_index + 1));
this.inner_widget = state.widget;
return def.done(function () {
if (self.inner_widget.do_show) {
self.inner_widget.do_show();
}
});
return action.restore(index);
},
clear_states: function(states) {
_.map(states || this.states, function(state) {
state.destroy();
clear_action_stack: function(action_stack) {
_.map(action_stack || this.action_stack, function(action) {
action.destroy();
});
if (!states) {
this.states = [];
if (!action_stack) {
this.action_stack = [];
this.inner_action = null;
this.inner_widget = null;
}
},
@@ -261,47 +281,48 @@ var ActionManager = Widget.extend({
}
state = state || {};
if (this.inner_action) {
if (this.inner_action._push_me === false) {
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(this.inner_action.type == 'ir.actions.act_window') {
state.model = this.inner_action.res_model;
if(inner_action_descr.type == 'ir.actions.act_window') {
state.model = inner_action_descr.res_model;
}
if (this.inner_action.menu_id) {
state.menu_id = this.inner_action.menu_id;
if (inner_action_descr.menu_id) {
state.menu_id = inner_action_descr.menu_id;
}
if (this.inner_action.id) {
state.action = this.inner_action.id;
} else if (this.inner_action.type == 'ir.actions.client') {
state.action = this.inner_action.tag;
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(this.inner_action.params, function(v, k) {
_.each(inner_action_descr.params, function(v, k) {
if(_.isString(v) || _.isNumber(v)) {
params[k] = v;
}
});
state = _.extend(params || {}, state);
}
if (this.inner_action.context) {
var active_id = this.inner_action.context.active_id;
if (inner_action_descr.context) {
var active_id = inner_action_descr.context.active_id;
if (active_id) {
state.active_id = active_id;
}
var active_ids = this.inner_action.context.active_ids;
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 = this.inner_action.context.active_ids.join(',');
state.active_ids = inner_action_descr.context.active_ids.join(',');
}
}
}
this.webclient.do_push_state(state);
},
do_load_state: function(state, warm) {
var self = this,
action_loaded;
var self = this;
var action_loaded;
if (state.action) {
if (_.isString(state.action) && core.action_registry.contains(state.action)) {
var action_client = {
@@ -338,8 +359,8 @@ var ActionManager = Widget.extend({
action_loaded = this.do_action(state.action, { additional_context: add_context });
$.when(action_loaded || null).done(function() {
self.webclient.menu.is_bound.done(function() {
if (self.inner_action && self.inner_action.id) {
self.webclient.menu.open_action(self.inner_action.id);
if (self.inner_action && self.inner_action.get_action_descr().id) {
self.webclient.menu.open_action(self.inner_action.get_action_descr().id);
}
});
});
@@ -478,15 +499,15 @@ var ActionManager = Widget.extend({
},
null_action: function() {
this.dialog_stop();
this.clear_states();
this.clear_action_stack();
},
/**
*
* @param {Object} executor
* @param {Object} executor.action original action
* @param {Function<web.Widget>} executor.widget function used to fetch the widget instance
* @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<web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
* @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
* @param {Object} options
* @return {*}
*/
@@ -544,7 +565,7 @@ var ActionManager = Widget.extend({
}
widget = executor.widget();
this.dialog_stop(executor.action);
return this.push_state(widget, executor.action, options);
return this.push_action(widget, executor.action, options);
},
ir_actions_act_window: function (action, options) {
var self = this;
@@ -562,7 +583,6 @@ var ActionManager = Widget.extend({
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))) {
+12 -120
View File
@@ -2,16 +2,10 @@ odoo.define('web.ControlPanel', function (require) {
"use strict";
var core = require('web.core');
var SearchView = require('web.SearchView');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var ControlPanel = Widget.extend({
template: 'ControlPanel',
events: {
"click .oe-cp-switch-buttons button": "on_switch_buttons_click",
},
/**
* @param {String} [template] the QWeb template to render the ControlPanel.
* By default, the template 'ControlPanel' will be used
@@ -23,13 +17,8 @@ var ControlPanel = Widget.extend({
}
this.bus = new core.Bus();
this.bus.on("setup_search_view", this, this.setup_search_view);
this.bus.on("render_switch_buttons", this, this.render_switch_buttons);
this.bus.on("update", this, this.update);
this.bus.on("update_breadcrumbs", this, this.update_breadcrumbs);
this.state = null;
this.searchview = null;
},
start: function() {
// Retrieve ControlPanel jQuery nodes
@@ -54,36 +43,6 @@ var ControlPanel = Widget.extend({
get_bus: function() {
return this.bus;
},
/**
* @return {Object} the current State of the ControlPanel
*/
get_state: function() {
return this.state;
},
/**
* Sets the state of the controlpanel (in the case of a viewmanager, set_state must be
* called before switch_mode for the controlpanel and the viewmanager to be synchronized)
* @param {Object} [state] the new state
*/
set_state: function(state) {
this.state = state;
// Set the new state searchview
this.searchview = state.get_searchview();
},
/**
* Returns the content of the ControlPanel without detaching anything from it
* @return {Object} dictionnary containing the detached jQuery elements
*/
get_content: function() {
return {
'$buttons': this.$buttons.contents(),
'$switch_buttons': this.$switch_buttons.contents(),
'$pager': this.$pager.contents(),
'$sidebar': this.$sidebar.contents(),
'$searchview': this.$searchview.contents(),
'$searchview_buttons': this.$searchview_buttons.contents(),
};
},
/**
* Hides (or shows) the ControlPanel in headless (resp. non-headless) mode
* Also detaches or attaches its contents to clean the DOM
@@ -99,15 +58,14 @@ var ControlPanel = Widget.extend({
},
/**
* Detaches the content of the ControlPanel
* @return {Object} dictionnary containing the detached jQuery elements
*/
_detach_content: function() {
var cp_content = this.get_content();
_.each(cp_content, function($content) {
$content.detach();
});
return cp_content;
this.$buttons.contents().detach();
this.$switch_buttons.contents().detach();
this.$pager.contents().detach();
this.$sidebar.contents().detach();
this.$searchview.contents().detach();
this.$searchview_buttons.contents().detach();
},
/**
* Attaches content to the ControlPanel
@@ -139,50 +97,20 @@ var ControlPanel = Widget.extend({
// again in the ControlPanel will removed them from there they should be
this._detach_content();
this._attach_content(status.cp_content);
if (status.active_view) this.update_switch_buttons(status.active_view);
if (status.active_view_selector) this.update_switch_buttons(status.active_view_selector);
if (status.searchview) this.update_search_view(status.searchview, status.search_view_hidden);
if (status.breadcrumbs) this.update_breadcrumbs(status.breadcrumbs);
}
},
/**
* Renders the switch buttons and call set_switch_buttons on the src
* Does not append the switch buttons to the DOM
* @param {Object} [src] the source requesting the switch_buttons
* @param {Array} [views] the array of views
*/
render_switch_buttons: function(src, views) {
if (views.length > 1) {
var self = this;
// Render switch buttons but do not append them to the DOM as this will
// be done later, simultaneously to all other ControlPanel elements
var $switch_buttons = $(QWeb.render('ControlPanel.switch-buttons', {views: views}));
// Create bootstrap tooltips
_.each(views, function(view) {
self.$('.oe-cp-switch-' + view.type).tooltip();
});
}
// Set the buttons jQuery elements in the Widget which made the request
src.set_switch_buttons($switch_buttons);
},
/**
* Triggers an event when switch-buttons are clicked on
*/
on_switch_buttons_click: function(event) {
var view_type = $(event.target).data('view-type');
this.trigger('switch_view', view_type);
},
/**
* Removes active class on all switch-buttons and adds it to the one of the active view
* @param {Object} [active_view] the active_view
* @param {Object} [active_view_selector] the selector of the div to activate
*/
update_switch_buttons: function(active_view) {
_.each(this.$switch_buttons.contents('button'), function(button) {
update_switch_buttons: function(active_view_selector) {
_.each(this.$switch_buttons.find('button'), function(button) {
$(button).removeClass('active');
});
this.$('.oe-cp-switch-' + active_view.type).addClass('active');
this.$(active_view_selector).addClass('active');
},
/**
* Updates the breadcrumbs
@@ -206,48 +134,12 @@ var ControlPanel = Widget.extend({
.toggleClass('active', is_last);
if (!is_last) {
$bc.click(function () {
self.trigger("on_breadcrumb_click", bc.widget, bc.index);
self.trigger("on_breadcrumb_click", bc.action, bc.index);
});
}
return $bc;
}
},
/**
* Sets up the search view and calls set_search_view on the widget requesting it
*
* @param {Object} [src] the widget requesting a search_view
* @param {Object} [action] the action (required to instantiated the SearchView)
* @param {Object} [dataset] the dataset (required to instantiated the SearchView)
* @param {Object} [flags] a dictionnary of Booleans
*/
setup_search_view: function(src, action, dataset, flags) {
var view_id = (action && action.search_view_id && action.search_view_id[0]) || false;
var search_defaults = {};
var context = action ? action.context : [];
_.each(context, function (value, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value;
}
});
var options = {
hidden: flags.search_view === false,
disable_custom_filters: flags.search_disable_custom_filters,
$buttons: $("<div>"),
action: action,
};
// 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
var searchview = new SearchView(this, dataset, view_id, search_defaults, options);
searchview.appendTo($("<div>")).then(function() {
// Set the SearchView in the widget which made the request
src.set_search_view(searchview);
});
},
/**
* Updates the SearchView's visibility and extend the breadcrumbs area if the SearchView is not visible
* @param {openerp.web.SearchView} [searchview] the searchview Widget
+126 -86
View File
@@ -5,8 +5,10 @@ var core = require('web.core');
var data = require('web.data');
var Model = require('web.Model');
var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var ViewManager = Widget.extend({
@@ -46,10 +48,10 @@ var ViewManager = Widget.extend({
this.cp_bus = cp_bus;
_.each(views, function (view) {
var view_type = view[1] || view.view_type,
View = core.view_registry.get(view_type, true),
view_label = View ? View.prototype.display_name: (void 'nope'),
view_descr = {
var view_type = view[1] || view.view_type;
var View = core.view_registry.get(view_type, true);
var view_label = View ? View.prototype.display_name: (void 'nope');
var view_descr = {
controller: null,
options: view.options || {},
view_id: view[0] || view.view_id,
@@ -77,8 +79,8 @@ var ViewManager = Widget.extend({
*/
start: function() {
var self = this;
var default_view = this.get_default_view(),
default_options = this.flags[default_view] && this.flags[default_view].options;
var default_view = this.get_default_view();
var default_options = this.flags[default_view] && this.flags[default_view].options;
this._super();
@@ -97,89 +99,24 @@ var ViewManager = Widget.extend({
if (this.cp_bus) {
this.control_elements = {};
if (this.flags.search_view) {
// Ask the ControlPanel to setup the search view
this.search_view_loaded = $.Deferred();
this.cp_bus.trigger('setup_search_view', this, this.action, this.dataset, this.flags);
$.when(this.search_view_loaded).then(function() {
self.searchview.on('search_data', self, self.search);
});
this.search_view_loaded = this.setup_search_view();
}
if (this.flags.views_switcher) {
// Ask the ControlPanel to render the switch-buttons
self.cp_bus.trigger('render_switch_buttons', self, self.view_order);
this.render_switch_buttons();
}
}
// Switch to the default_view to load it
this.main_view_loaded = this.switch_mode(default_view, null, default_options);
var main_view_loaded = this.switch_mode(default_view, null, default_options);
return $.when(self.main_view_loaded, this.search_view_loaded);
},
/**
* Executed by the ControlPanel when the searchview requested by this ViewManager is loaded
* @param {Widget} [searchview] the SearchView
*/
set_search_view: function(searchview) {
this.searchview = searchview;
_.extend(this.control_elements, {
$searchview: searchview.$el,
$searchview_buttons: searchview.$buttons.contents(),
});
this.search_view_loaded.resolve();
},
/**
* Executed by the ControlPanel when the switch_buttons requested by this ViewManager are rendered
* @param {jQuery} [$switch_buttons] the rendered switch_buttons
*/
set_switch_buttons: function($switch_buttons) {
_.extend(this.control_elements, {
$switch_buttons: $switch_buttons,
});
},
get_search_view: function() {
return this.searchview;
},
/**
* Executed on event "search_data" thrown by the SearchView
*/
search: function(domains, contexts, groupbys) {
var self = this,
controller = this.active_view.controller, // the correct view must be loaded here
action_context = this.action.context || {},
view_context = controller.get_context();
pyeval.eval_domains_and_contexts({
domains: [this.action.domain || []].concat(domains || []),
contexts: [action_context, view_context].concat(contexts || []),
group_by_seq: groupbys || []
}).done(function (results) {
if (results.error) {
self.active_search.resolve();
throw new Error(
_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
self.dataset._model = new Model(
self.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)){
self.dataset.set_sort([]);
}
$.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
self.active_search.resolve();
});
});
return $.when(main_view_loaded, this.search_view_loaded);
},
get_default_view: function() {
return this.flags.default_view || this.view_order[0].type;
},
switch_mode: function(view_type, no_store, view_options) {
var self = this,
view = this.views[view_type];
var self = this;
var view = this.views[view_type];
if (!view) {
return $.Deferred().reject();
@@ -226,7 +163,7 @@ var ViewManager = Widget.extend({
// Render the control elements of the active view (buttons, sidebar, pager)
var view_elements = self.render_view_control_elements();
var status = {
active_view: self.active_view,
active_view_selector: '.oe-cp-switch-' + self.active_view.type,
breadcrumbs: self.action_manager && self.action_manager.get_breadcrumbs(),
cp_content: _.extend({}, self.control_elements, view_elements),
hidden: self.flags.headless,
@@ -239,17 +176,17 @@ var ViewManager = Widget.extend({
});
},
create_view: function(view, view_options) {
var self = this,
View = this.registry.get(view.type),
options = _.clone(view.options),
view_loaded = $.Deferred();
var self = this;
var View = this.registry.get(view.type);
var options = _.clone(view.options);
var view_loaded = $.Deferred();
if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline'))
|| (view_options && view_options.mode === 'edit'))) {
if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline')) ||
(view_options && view_options.mode === 'edit'))) {
options.initial_mode = 'edit';
}
var controller = new View(this, this.dataset, view.view_id, options),
$container = view.$container;
var controller = new View(this, this.dataset, view.view_id, options);
var $container = view.$container;
$container.hide();
view.controller = controller;
@@ -281,6 +218,32 @@ var ViewManager = Widget.extend({
this.view_stack.splice(index);
return this.switch_mode(view_type);
},
/**
* Renders the switch buttons and adds listeners on them but does not append them to the DOM
* Sets $switch_buttons in control_elements to send to the ControlPanel
* @param {Object} [src] the source requesting the switch_buttons
* @param {Array} [views] the array of views
*/
render_switch_buttons: function() {
if (this.flags.views_switcher && this.view_order.length > 1) {
var self = this;
// Render switch buttons but do not append them to the DOM as this will
// be done later, simultaneously to all other ControlPanel elements
this.control_elements.$switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: self.view_order}));
// Create bootstrap tooltips
_.each(this.views, function(view) {
self.control_elements.$switch_buttons.siblings('.oe-cp-switch-' + view.type).tooltip();
});
// Add onclick event listener
this.control_elements.$switch_buttons.siblings('button').click(function() {
var view_type = $(event.target).data('view-type');
self.switch_mode(view_type);
});
}
},
/**
* Renders the control elements (buttons, sidebar, pager) of the current view
* This must be done when active_search is resolved (for KanbanViews)
@@ -315,6 +278,83 @@ var ViewManager = Widget.extend({
get_view_id: function(view_type) {
return this.views[view_type] && this.views[view_type].view_id || false;
},
/**
* Sets up the current viewmanager's search view.
* Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel
*
* @param {Number|false} view_id the view to use or false for a default one
* @returns {jQuery.Deferred} search view startup deferred
*/
setup_search_view: function() {
var self = this;
if (this.searchview) {
this.searchview.destroy();
}
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
var search_defaults = {};
var context = this.action ? 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,
};
// 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, view_id, search_defaults, options);
this.searchview.on('search_data', this, this.search.bind(this));
return $.when(this.searchview.appendTo($("<div>"))).done(function() {
self.control_elements.$searchview = self.searchview.$el;
self.control_elements.$searchview_buttons = self.searchview.$buttons.contents();
});
},
/**
* Executed on event "search_data" thrown by the SearchView
*/
search: function(domains, contexts, groupbys) {
var self = this;
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();
pyeval.eval_domains_and_contexts({
domains: [this.action.domain || []].concat(domains || []),
contexts: [action_context, view_context].concat(contexts || []),
group_by_seq: groupbys || []
}).done(function (results) {
if (results.error) {
self.active_search.resolve();
throw new Error(
_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
self.dataset._model = new Model(
self.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)){
self.dataset.set_sort([]);
}
$.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
self.active_search.resolve();
});
});
},
do_push_state: function(state) {
if (this.action_manager) {
state.view_type = this.active_view.type;
@@ -26,9 +26,9 @@ if (core.debug) {
this.$dropdown = this.$(".js_debug_dropdown");
},
/**
* Updates its attributes according to the current state of the ActionManager
* Updates its attributes according to the inner_widget of the ActionManager
*/
_update_state: function() {
_update: function() {
this.view_manager = odoo.__DEBUG__.services['web.web_client'].action_manager.get_inner_widget();
if (!this.view_manager instanceof ViewManager) { return; }
this.dataset = this.view_manager.dataset;
@@ -38,7 +38,7 @@ if (core.debug) {
return true;
},
/**
* Renders dropdown according to the current state
* Renders the DebugManager dropdown
*/
render_dropdown: function() {
var self = this;
@@ -46,8 +46,8 @@ if (core.debug) {
// Empty the previously rendered dropdown
this.$dropdown.empty();
// Attempt to retrieve the current state of the ActionManager
if (!this._update_state()) {
// Attempt to retrieve the inner_widget of the ActionManager
if (!this._update()) {
// Disable the button when not available
console.warn("DebugManager is not available");
return;
+6 -6
View File
@@ -495,12 +495,6 @@
</div>
</t>
<t t-name="ControlPanel.switch-buttons">
<t t-foreach="views" t-as="view">
<button type="button" t-attf-class="btn btn-default fa oe-cp-switch-#{view.type}" t-att-data-view-type="view.type" t-att-title="view.label"/>
</t>
</t>
<div t-name="ViewManager" class="oe-view-manager">
<div class="oe-view-manager-content">
<t t-foreach="widget.views" t-as="view">
@@ -508,6 +502,12 @@
</t>
</div>
</div>
<t t-name="ViewManager.switch-buttons">
<t t-foreach="views" t-as="view">
<button type="button" t-attf-class="btn btn-default fa oe-cp-switch-#{view.type}" t-att-data-view-type="view.type" t-att-title="view.label"/>
</t>
</t>
<t t-name="WebClient.DebugManager">
<li class="oe_debug_manager">
<a href="#" t-att-title='_t("Open Debug Menu")' class="dropdown-toggle oe_debug_button" data-toggle="dropdown">