[REF] web,*: ActionManager/ViewManager refactoring
This rev. is the first part (out of 3) of the refactoring of the
ActionManager and ViewManager layer of the webclient.
The main changes are:
- there is no ViewManager anymore, its work is now handled by the
ActionManager itself, but isolated in a specific file ; this
eases a lot of things, as the breadcrumbs handling for example.
- the ActionManager code is converted to the new coding principles
and guidelines ; mainly, children widgets communicate with it by
triggering events up, and not by function calls anymore.
- the ActionManager layer is now testable, and a lot of tests have
already been written.
- the code in other addons has been adapted consequently.
What's coming next:
- introduce the 'AbstractAction' Widget, and make client actions
and view controllers inherit from it ; this widget will
implement a common API that could be used uniformly by the
ActionManager (e.g. restore(), canBeLeft(), renderButtons(),
getTitle()...).
- move the ControlPanel handling from the ActionManager to the
AbstractAction.
This commit is contained in:
@@ -106,8 +106,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.set("title", this.title);
|
this.set("title", this.title);
|
||||||
var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }];
|
this.update_control_panel({search_view_hidden: true}, {clear: true});
|
||||||
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
|
|
||||||
|
|
||||||
this.renderer.prependTo(self.$('.o_form_sheet'));
|
this.renderer.prependTo(self.$('.o_form_sheet'));
|
||||||
this._renderLines();
|
this._renderLines();
|
||||||
@@ -122,18 +121,11 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
|||||||
do_show: function () {
|
do_show: function () {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
if (this.action_manager) {
|
if (this.action_manager) {
|
||||||
var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }];
|
this.update_control_panel({search_view_hidden: true}, {clear: true});
|
||||||
while (breadcrumbs.length) {
|
|
||||||
if (breadcrumbs[breadcrumbs.length-1].action.widget === this) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
breadcrumbs.pop();
|
|
||||||
}
|
|
||||||
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
|
|
||||||
this.action_manager.do_push_state({
|
this.action_manager.do_push_state({
|
||||||
action: this.params.tag,
|
action: this.params.tag,
|
||||||
active_id: this.params.res_id,
|
active_id: this.params.res_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
|||||||
self.renderButtons();
|
self.renderButtons();
|
||||||
self.renderImportLink();
|
self.renderImportLink();
|
||||||
var status = {
|
var status = {
|
||||||
breadcrumbs: self.action_manager.get_breadcrumbs(),
|
|
||||||
cp_content: {$buttons: self.$buttons},
|
cp_content: {$buttons: self.$buttons},
|
||||||
};
|
};
|
||||||
self.update_control_panel(status);
|
self.update_control_panel(status);
|
||||||
@@ -505,10 +504,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
|||||||
this.exit();
|
this.exit();
|
||||||
},
|
},
|
||||||
exit: function () {
|
exit: function () {
|
||||||
this.do_action({
|
this.trigger_up('history_back');
|
||||||
type: 'ir.actions.client',
|
|
||||||
tag: 'history_back'
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onresults: function (event, from, to, message) {
|
onresults: function (event, from, to, message) {
|
||||||
var no_messages = _.isEmpty(message);
|
var no_messages = _.isEmpty(message);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
odoo.define('board.ActionManager', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of this file is to patch the ActionManager to properly generate
|
||||||
|
* the flags for the 'ir.actions.act_window' of model 'board.board'.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ActionManager = require('web.ActionManager');
|
||||||
|
|
||||||
|
ActionManager.include({
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_executeWindowAction: function (action) {
|
||||||
|
if (action.res_model === 'board.board' && action.view_mode === 'form') {
|
||||||
|
action.target = 'inline';
|
||||||
|
_.extend(action.flags, {
|
||||||
|
hasSearchView: false,
|
||||||
|
hasSidebar: false,
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -104,10 +104,9 @@ FormController.include({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to intercept switch_view event coming from sub views, because
|
* We need to intercept switch_view event coming from sub views, because we
|
||||||
* there is no view manager doing the job. Also, we don't actually want to
|
* don't actually want to switch view in dashboard, we want to do a
|
||||||
* switch view in dashboard, we want to do a do_action (which will open the
|
* do_action (which will open the record in a different breadcrumb).
|
||||||
* record in a different breadcrumb)
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
@@ -126,6 +125,9 @@ FormController.include({
|
|||||||
});
|
});
|
||||||
|
|
||||||
FormRenderer.include({
|
FormRenderer.include({
|
||||||
|
custom_events: _.extend({}, FormRenderer.prototype.custom_events, {
|
||||||
|
env_updated: '_onEnvUpdated',
|
||||||
|
}),
|
||||||
events: _.extend({}, FormRenderer.prototype.events, {
|
events: _.extend({}, FormRenderer.prototype.events, {
|
||||||
'click .oe_dashboard_column .oe_fold': '_onFoldClick',
|
'click .oe_dashboard_column .oe_fold': '_onFoldClick',
|
||||||
'click .oe_dashboard_link_change_layout': '_onChangeLayout',
|
'click .oe_dashboard_link_change_layout': '_onChangeLayout',
|
||||||
@@ -339,6 +341,16 @@ FormRenderer.include({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Stops the propagation of 'update_env' events triggered by the controllers
|
||||||
|
* instantiated by the dashboard.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onEnvUpdated: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {MouseEvent} event
|
* @param {MouseEvent} event
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ var core = require('web.core');
|
|||||||
var Domain = require('web.Domain');
|
var Domain = require('web.Domain');
|
||||||
var FavoriteMenu = require('web.FavoriteMenu');
|
var FavoriteMenu = require('web.FavoriteMenu');
|
||||||
var pyeval = require('web.pyeval');
|
var pyeval = require('web.pyeval');
|
||||||
var ViewManager = require('web.ViewManager');
|
|
||||||
|
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
var QWeb = core.qweb;
|
var QWeb = core.qweb;
|
||||||
@@ -23,11 +22,7 @@ FavoriteMenu.include({
|
|||||||
if(this.action_id === undefined) {
|
if(this.action_id === undefined) {
|
||||||
return this._super();
|
return this._super();
|
||||||
}
|
}
|
||||||
var am = this.findAncestor(function (a) {
|
if (this.action.type === 'ir.actions.act_window') {
|
||||||
return a instanceof ActionManager;
|
|
||||||
});
|
|
||||||
if (am && am.get_inner_widget() instanceof ViewManager) {
|
|
||||||
this.view_manager = am.get_inner_widget();
|
|
||||||
this.add_to_dashboard_available = true;
|
this.add_to_dashboard_available = true;
|
||||||
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtodashboard'));
|
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtodashboard'));
|
||||||
this.$add_to_dashboard = this.$('.o_add_to_dashboard');
|
this.$add_to_dashboard = this.$('.o_add_to_dashboard');
|
||||||
@@ -71,7 +66,13 @@ FavoriteMenu.include({
|
|||||||
context.add({
|
context.add({
|
||||||
group_by: pyeval.eval('groupbys', search_data.groupbys || [])
|
group_by: pyeval.eval('groupbys', search_data.groupbys || [])
|
||||||
});
|
});
|
||||||
context.add(this.view_manager.active_view.controller.getContext());
|
// AAB: trigger_up an event that will be intercepted by the controller,
|
||||||
|
// as soon as the controller is the parent of the control panel
|
||||||
|
var am = this.findAncestor(function (a) {
|
||||||
|
return a instanceof ActionManager;
|
||||||
|
});
|
||||||
|
var controller = am.getCurrentController();
|
||||||
|
context.add(controller.widget.getContext());
|
||||||
var c = pyeval.eval('context', context);
|
var c = pyeval.eval('context', context);
|
||||||
for (var k in c) {
|
for (var k in c) {
|
||||||
if (c.hasOwnProperty(k) && /^search_default_/.test(k)) {
|
if (c.hasOwnProperty(k) && /^search_default_/.test(k)) {
|
||||||
@@ -88,7 +89,7 @@ FavoriteMenu.include({
|
|||||||
action_id: self.action_id || false,
|
action_id: self.action_id || false,
|
||||||
context_to_save: c,
|
context_to_save: c,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
view_mode: self.view_manager.active_view.type,
|
view_mode: controller.viewType,
|
||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<template id="assets_backend" name="board assets" inherit_id="web.assets_backend">
|
<template id="assets_backend" name="board assets" inherit_id="web.assets_backend">
|
||||||
<xpath expr="." position="inside">
|
<xpath expr="." position="inside">
|
||||||
<link rel="stylesheet" href="/board/static/src/less/dashboard.less"/>
|
<link rel="stylesheet" href="/board/static/src/less/dashboard.less"/>
|
||||||
|
<script type="text/javascript" src="/board/static/src/js/action_manager_board_action.js"></script>
|
||||||
<script type="text/javascript" src="/board/static/src/js/dashboard.js"></script>
|
<script type="text/javascript" src="/board/static/src/js/dashboard.js"></script>
|
||||||
<script type="text/javascript" src="/board/static/src/js/favorite_menu.js"></script>
|
<script type="text/javascript" src="/board/static/src/js/favorite_menu.js"></script>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ odoo.define('document.tests', function (require) {
|
|||||||
'</sheet>' +
|
'</sheet>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
return $.when(this.data['ir.attachment'].records);
|
return $.when(this.data['ir.attachment'].records);
|
||||||
|
|||||||
@@ -7,27 +7,28 @@ var data = require('web.data');
|
|||||||
var Domain = require('web.Domain');
|
var Domain = require('web.Domain');
|
||||||
var FavoriteMenu = require('web.FavoriteMenu');
|
var FavoriteMenu = require('web.FavoriteMenu');
|
||||||
var pyeval = require('web.pyeval');
|
var pyeval = require('web.pyeval');
|
||||||
var ViewManager = require('web.ViewManager');
|
|
||||||
|
|
||||||
var QWeb = core.qweb;
|
var QWeb = core.qweb;
|
||||||
|
|
||||||
FavoriteMenu.include({
|
FavoriteMenu.include({
|
||||||
start: function () {
|
start: function () {
|
||||||
this._super();
|
this._super();
|
||||||
var am = this.findAncestor(function (a) {
|
if (this.action.type === 'ir.actions.act_window') {
|
||||||
return a instanceof ActionManager;
|
|
||||||
});
|
|
||||||
if (am && am.get_inner_widget() instanceof ViewManager) {
|
|
||||||
this.view_manager = am.get_inner_widget();
|
|
||||||
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtogooglespreadsheet'));
|
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtogooglespreadsheet'));
|
||||||
this.$('.add-to-spreadsheet').click(this.add_to_spreadsheet.bind(this));
|
this.$('.add-to-spreadsheet').click(this.add_to_spreadsheet.bind(this));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
add_to_spreadsheet: function () {
|
add_to_spreadsheet: function () {
|
||||||
|
// AAB: trigger_up an event that will be intercepted by the controller,
|
||||||
|
// as soon as the controller is the parent of the control panel
|
||||||
|
var am = this.findAncestor(function (a) {
|
||||||
|
return a instanceof ActionManager;
|
||||||
|
});
|
||||||
|
var controller = am.getCurrentController();
|
||||||
var sv_data = this.searchview.build_search_data(),
|
var sv_data = this.searchview.build_search_data(),
|
||||||
model = this.searchview.dataset.model,
|
model = this.searchview.dataset.model,
|
||||||
list_view = this.view_manager.views.list,
|
list_view = _.findWhere(controller.widget.actionViews, {type: 'list'}),
|
||||||
list_view_id = list_view ? list_view.view_id : false,
|
list_view_id = list_view ? list_view.viewID : false,
|
||||||
domain = [],
|
domain = [],
|
||||||
groupbys = pyeval.eval('groupbys', sv_data.groupbys).join(" "),
|
groupbys = pyeval.eval('groupbys', sv_data.groupbys).join(" "),
|
||||||
ds = new data.DataSet(this, 'google.drive.config');
|
ds = new data.DataSet(this, 'google.drive.config');
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ var ChatMixin = {
|
|||||||
options: options,
|
options: options,
|
||||||
callback: function(messages) { result = messages;}
|
callback: function(messages) { result = messages;}
|
||||||
});
|
});
|
||||||
return result ? result : $.when();
|
return result ? result : $.when([]);
|
||||||
},
|
},
|
||||||
_joinChannel: function(channel_id) {
|
_joinChannel: function(channel_id) {
|
||||||
var result;
|
var result;
|
||||||
|
|||||||
@@ -663,7 +663,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
*/
|
*/
|
||||||
_updateControlPanel: function () {
|
_updateControlPanel: function () {
|
||||||
this.update_control_panel({
|
this.update_control_panel({
|
||||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
|
||||||
cp_content: {
|
cp_content: {
|
||||||
$buttons: this.$buttons,
|
$buttons: this.$buttons,
|
||||||
$searchview: this.searchview.$el,
|
$searchview: this.searchview.$el,
|
||||||
@@ -905,6 +904,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
* @param {OdooEvent}
|
* @param {OdooEvent}
|
||||||
*/
|
*/
|
||||||
_onSearch: function (event) {
|
_onSearch: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
var session = this.getSession();
|
var session = this.getSession();
|
||||||
var result = pyeval.eval_domains_and_contexts({
|
var result = pyeval.eval_domains_and_contexts({
|
||||||
domains: event.data.domains,
|
domains: event.data.domains,
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ var PlanAction = Widget.extend(ControlPanelMixin, {
|
|||||||
},
|
},
|
||||||
update_cp: function () {
|
update_cp: function () {
|
||||||
this.update_control_panel({
|
this.update_control_panel({
|
||||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
|
||||||
cp_content: {
|
cp_content: {
|
||||||
$buttons: this.$buttons,
|
$buttons: this.$buttons,
|
||||||
$searchview: this.searchview.$el,
|
$searchview: this.searchview.$el,
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ var stock_report_generic = Widget.extend(ControlPanelMixin, {
|
|||||||
this.renderButtons();
|
this.renderButtons();
|
||||||
}
|
}
|
||||||
var status = {
|
var status = {
|
||||||
breadcrumbs: this.actionManager.get_breadcrumbs(),
|
|
||||||
cp_content: {$buttons: this.$buttons},
|
cp_content: {$buttons: this.$buttons},
|
||||||
};
|
};
|
||||||
return this.update_control_panel(status);
|
return this.update_control_panel(status);
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
toggle_fullscreen: function (event) {
|
toggle_fullscreen: function (event) {
|
||||||
this.toggle_fullscreen(event.data.fullscreen);
|
this.toggle_fullscreen(event.data.fullscreen);
|
||||||
},
|
},
|
||||||
current_action_updated: function (e) {
|
current_action_updated: function (ev) {
|
||||||
this.current_action_updated(e.data.action);
|
this.current_action_updated(ev.data.action, ev.data.controller);
|
||||||
},
|
},
|
||||||
// GENERIC SERVICES
|
// GENERIC SERVICES
|
||||||
// the next events are dedicated to generic services required by
|
// the next events are dedicated to generic services required by
|
||||||
@@ -51,6 +51,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
warning: '_onDisplayWarning',
|
warning: '_onDisplayWarning',
|
||||||
|
load_action: '_onLoadAction',
|
||||||
load_views: function (event) {
|
load_views: function (event) {
|
||||||
var params = {
|
var params = {
|
||||||
model: event.data.modelName,
|
model: event.data.modelName,
|
||||||
@@ -66,6 +67,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
.load_filters(event.data.dataset, event.data.action_id)
|
.load_filters(event.data.dataset, event.data.action_id)
|
||||||
.then(event.data.on_success);
|
.then(event.data.on_success);
|
||||||
},
|
},
|
||||||
|
push_state: '_onPushState',
|
||||||
show_effect: '_onShowEffect',
|
show_effect: '_onShowEffect',
|
||||||
// session
|
// session
|
||||||
get_session: function (event) {
|
get_session: function (event) {
|
||||||
@@ -199,7 +201,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
set_action_manager: function () {
|
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'));
|
return this.action_manager.appendTo(this.$('.o_main_content'));
|
||||||
},
|
},
|
||||||
set_notification_manager: function () {
|
set_notification_manager: function () {
|
||||||
@@ -213,11 +215,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
show_application: function () {
|
show_application: function () {
|
||||||
},
|
},
|
||||||
clear_uncommitted_changes: function () {
|
clear_uncommitted_changes: function () {
|
||||||
var def = $.Deferred().resolve();
|
return this.action_manager.clearUncommittedChanges();
|
||||||
core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) {
|
|
||||||
def = def.then(callback);
|
|
||||||
});
|
|
||||||
return def;
|
|
||||||
},
|
},
|
||||||
destroy_content: function () {
|
destroy_content: function () {
|
||||||
_.each(_.clone(this.getChildren()), function (el) {
|
_.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
|
* This allows to widgets that are not inside the ActionManager to perform do_action
|
||||||
*/
|
*/
|
||||||
do_action: function () {
|
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 () {
|
do_reload: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -274,6 +272,9 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
do_push_state: function (state) {
|
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) {
|
if ('title' in state) {
|
||||||
this.set_title(state.title);
|
this.set_title(state.title);
|
||||||
delete state.title;
|
delete state.title;
|
||||||
@@ -304,13 +305,14 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
this.connection_notification = false;
|
this.connection_notification = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Handler to be overwritten
|
/**
|
||||||
current_action_updated: function () {
|
* Handler to be overridden, called each time the UI is updated by the
|
||||||
},
|
* ActionManager.
|
||||||
// --------------------------------------------------------------
|
*
|
||||||
// Scrolltop handling
|
* @param {Object} action the action of the currently displayed controller
|
||||||
// --------------------------------------------------------------
|
* @param {Object} controller the currently displayed controller
|
||||||
getScrollTop: function () {
|
*/
|
||||||
|
current_action_updated: function (action, controller) {
|
||||||
},
|
},
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
// Misc.
|
// Misc.
|
||||||
@@ -346,6 +348,27 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
|||||||
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
|
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 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
|
* Displays a visual effect (for example, a rainbowman0
|
||||||
*
|
*
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -31,7 +31,6 @@ odoo.define('web.ControlPanelMixin', function (require) {
|
|||||||
* },
|
* },
|
||||||
* _updateControlPanel: function () {
|
* _updateControlPanel: function () {
|
||||||
* this.update_control_panel({
|
* this.update_control_panel({
|
||||||
* breadcrumbs: this.action_manager.get_breadcrumbs(),
|
|
||||||
* cp_content: {
|
* cp_content: {
|
||||||
* $buttons: this.$buttons,
|
* $buttons: this.$buttons,
|
||||||
* },
|
* },
|
||||||
@@ -51,7 +50,9 @@ var ControlPanelMixin = {
|
|||||||
* @param {Object} [options] see web.ControlPanel.update() for a description
|
* @param {Object} [options] see web.ControlPanel.update() for a description
|
||||||
*/
|
*/
|
||||||
update_control_panel: function(cp_status, options) {
|
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() {
|
start: function() {
|
||||||
// Exposed jQuery nodesets
|
// Exposed jQuery nodesets
|
||||||
this.nodes = {
|
this.nodes = {
|
||||||
$breadcrumbs: this.$('.breadcrumb'),
|
|
||||||
$buttons: this.$('.o_cp_buttons'),
|
$buttons: this.$('.o_cp_buttons'),
|
||||||
$pager: this.$('.o_cp_pager'),
|
$pager: this.$('.o_cp_pager'),
|
||||||
$searchview: this.$('.o_cp_searchview'),
|
$searchview: this.$('.o_cp_searchview'),
|
||||||
@@ -107,10 +107,6 @@ var ControlPanel = Widget.extend({
|
|||||||
|
|
||||||
return this._super();
|
return this._super();
|
||||||
},
|
},
|
||||||
destroy: function() {
|
|
||||||
this._clear_breadcrumbs_handlers();
|
|
||||||
return this._super.apply(this, arguments);
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* @return {Object} the Bus the ControlPanel is listening on
|
* @return {Object} the Bus the ControlPanel is listening on
|
||||||
*/
|
*/
|
||||||
@@ -143,9 +139,7 @@ var ControlPanel = Widget.extend({
|
|||||||
|
|
||||||
// Render the breadcrumbs
|
// Render the breadcrumbs
|
||||||
if (status.breadcrumbs) {
|
if (status.breadcrumbs) {
|
||||||
this._clear_breadcrumbs_handlers();
|
this.$('.breadcrumb').html(this._render_breadcrumbs(status.breadcrumbs));
|
||||||
this.$breadcrumbs = this._render_breadcrumbs(status.breadcrumbs);
|
|
||||||
new_cp_content.$breadcrumbs = this.$breadcrumbs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach control_panel old content and attach new elements
|
// Detach control_panel old content and attach new elements
|
||||||
@@ -239,22 +233,11 @@ var ControlPanel = Widget.extend({
|
|||||||
.toggleClass('active', is_last);
|
.toggleClass('active', is_last);
|
||||||
if (!is_last) {
|
if (!is_last) {
|
||||||
$bc.click(function () {
|
$bc.click(function () {
|
||||||
self.trigger("on_breadcrumb_click", bc.action, bc.index);
|
self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return $bc;
|
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
|
* Private function that updates the SearchView's visibility and extend the
|
||||||
* breadcrumbs area if the SearchView is not visible
|
* breadcrumbs area if the SearchView is not visible
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ var Menu = Widget.extend({
|
|||||||
$main_menu = $clicked_menu;
|
$main_menu = $clicked_menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.current_primary_menu = $main_menu.data('menu');
|
||||||
|
|
||||||
// Activate current main menu
|
// Activate current main menu
|
||||||
this.$el.find('.active').removeClass('active');
|
this.$el.find('.active').removeClass('active');
|
||||||
$main_menu.parent().addClass('active');
|
$main_menu.parent().addClass('active');
|
||||||
@@ -206,6 +208,19 @@ var Menu = Widget.extend({
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.menu_click($(ev.currentTarget).data('menu'));
|
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;
|
return Menu;
|
||||||
|
|||||||
@@ -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._super.apply(this,arguments);
|
||||||
this.searchview = parent;
|
this.searchview = parent;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.target_model = target_model;
|
this.target_model = target_model;
|
||||||
this.action_id = action_id;
|
this.action = action;
|
||||||
|
this.action_id = action.id;
|
||||||
this.filters = {};
|
this.filters = {};
|
||||||
_.each(filters, this.add_filter.bind(this));
|
_.each(filters, this.add_filter.bind(this));
|
||||||
},
|
},
|
||||||
@@ -96,11 +97,7 @@ return Widget.extend({
|
|||||||
}
|
}
|
||||||
var user_context = this.getSession().user_context;
|
var user_context = this.getSession().user_context;
|
||||||
var search = this.searchview.build_search_data();
|
var search = this.searchview.build_search_data();
|
||||||
var view_manager = this.findAncestor(function (a) {
|
var view_context = this.action.env.context;
|
||||||
// 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 results = pyeval.eval_domains_and_contexts({
|
var results = pyeval.eval_domains_and_contexts({
|
||||||
domains: search.domains,
|
domains: search.domains,
|
||||||
contexts: [user_context].concat(search.contexts.concat(view_context || [])),
|
contexts: [user_context].concat(search.contexts.concat(view_context || [])),
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ var SearchView = Widget.extend({
|
|||||||
this.fields = this.fields_view.fields;
|
this.fields = this.fields_view.fields;
|
||||||
this.query = undefined;
|
this.query = undefined;
|
||||||
this.title = this.options.action && this.options.action.name;
|
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.search_fields = [];
|
||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.groupbys = [];
|
this.groupbys = [];
|
||||||
@@ -297,7 +297,7 @@ var SearchView = Widget.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var def;
|
var def;
|
||||||
if (!this.options.disable_favorites) {
|
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;
|
self.favorite_filters = filters;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ var SearchView = Widget.extend({
|
|||||||
menu_defs.push(this.groupby_menu.appendTo(this.$buttons));
|
menu_defs.push(this.groupby_menu.appendTo(this.$buttons));
|
||||||
}
|
}
|
||||||
if (!this.options.disable_favorites) {
|
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));
|
menu_defs.push(this.favorite_menu.appendTo(this.$buttons));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -83,7 +83,7 @@ return AbstractWebClient.extend({
|
|||||||
action_buttons: true,
|
action_buttons: true,
|
||||||
headless: 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;
|
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("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
|
||||||
form.on('record_saved', self, function() {
|
form.on('record_saved', self, function() {
|
||||||
@@ -110,7 +110,7 @@ return AbstractWebClient.extend({
|
|||||||
.done(function(result) {
|
.done(function(result) {
|
||||||
var data = result[0];
|
var data = result[0];
|
||||||
if(data.action_id) {
|
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]);
|
self.menu.open_action(data.action_id[0]);
|
||||||
} else {
|
} else {
|
||||||
var first_menu_id = self.menu.$el.find("a:first").data("menu");
|
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);
|
self.menu.menu_click(state.menu_id);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
state._push_me = false; // no need to push state back...
|
self.action_manager.loadState(state, !!self._current_state).then(function () {
|
||||||
self.action_manager.do_load_state(state, !!self._current_state).then(function () {
|
var action = self.action_manager.getCurrentAction();
|
||||||
var action = self.action_manager.get_inner_action();
|
|
||||||
if (action) {
|
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) {
|
.then(function (result) {
|
||||||
return self.action_mutex.exec(function() {
|
return self.action_mutex.exec(function() {
|
||||||
var completed = $.Deferred();
|
var completed = $.Deferred();
|
||||||
$.when(self.action_manager.do_action(result, {
|
self.action_manager.doAction(result, {
|
||||||
clear_breadcrumbs: true,
|
clear_breadcrumbs: true,
|
||||||
action_menu_id: self.menu.current_menu,
|
action_menu_id: self.menu.current_menu,
|
||||||
})).fail(function() {
|
}).fail(function() {
|
||||||
self.menu.open_menu(options.previous_menu_id);
|
self.menu.open_menu(options.previous_menu_id);
|
||||||
}).always(function() {
|
}).always(function() {
|
||||||
completed.resolve();
|
completed.resolve();
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ var Dialog = Widget.extend({
|
|||||||
this.destroy();
|
this.destroy();
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy: function (reason) {
|
destroy: function (arg) {
|
||||||
// Need to trigger before real destroy but if 'closed' handler destroys
|
// Need to trigger before real destroy but if 'closed' handler destroys
|
||||||
// the widget again, we want to avoid infinite recursion
|
// the widget again, we want to avoid infinite recursion
|
||||||
if (!this.__closed) {
|
if (!this.__closed) {
|
||||||
this.__closed = true;
|
this.__closed = true;
|
||||||
this.trigger("closed", reason);
|
this.trigger("closed", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDestroyed()) {
|
if (this.isDestroyed()) {
|
||||||
|
|||||||
@@ -140,17 +140,6 @@ function Home (parent, action) {
|
|||||||
}
|
}
|
||||||
core.action_registry.add("home", Home);
|
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() {
|
function login() {
|
||||||
redirect('/web/login');
|
redirect('/web/login');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ var ReportAction = Widget.extend(ControlPanelMixin, {
|
|||||||
|
|
||||||
_update_control_panel: function () {
|
_update_control_panel: function () {
|
||||||
this.update_control_panel({
|
this.update_control_panel({
|
||||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
|
||||||
cp_content: {
|
cp_content: {
|
||||||
$buttons: this.$buttons,
|
$buttons: this.$buttons,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ odoo.define('web.AbstractController', function (require) {
|
|||||||
* reading localstorage, ...) has to go through the controller.
|
* reading localstorage, ...) has to go through the controller.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||||
|
var core = require('web.core');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
|
|
||||||
|
var QWeb = core.qweb;
|
||||||
|
|
||||||
var AbstractController = Widget.extend({
|
var AbstractController = Widget.extend(ControlPanelMixin, {
|
||||||
custom_events: {
|
custom_events: {
|
||||||
open_record: '_onOpenRecord',
|
open_record: '_onOpenRecord',
|
||||||
|
switch_view: '_onSwitchView',
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
'click a[type="action"]': '_onActionClicked',
|
'click a[type="action"]': '_onActionClicked',
|
||||||
@@ -30,8 +34,15 @@ var AbstractController = Widget.extend({
|
|||||||
* @param {AbstractRenderer} renderer
|
* @param {AbstractRenderer} renderer
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {string} params.modelName
|
* @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.handle] a handle that will be given to the model (some id)
|
||||||
* @param {any} params.initialState the initialState
|
* @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) {
|
init: function (parent, model, renderer, params) {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
@@ -40,7 +51,23 @@ var AbstractController = Widget.extend({
|
|||||||
this.modelName = params.modelName;
|
this.modelName = params.modelName;
|
||||||
this.handle = params.handle;
|
this.handle = params.handle;
|
||||||
this.activeActions = params.activeActions;
|
this.activeActions = params.activeActions;
|
||||||
|
this.controllerID = params.controllerID;
|
||||||
this.initialState = params.initialState;
|
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.
|
* Simply renders and updates the url.
|
||||||
@@ -48,10 +75,19 @@ var AbstractController = Widget.extend({
|
|||||||
* @returns {Deferred}
|
* @returns {Deferred}
|
||||||
*/
|
*/
|
||||||
start: function () {
|
start: function () {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.$el.addClass('o_view_controller');
|
||||||
|
|
||||||
|
// render the ControlPanel elements (buttons, pager, sidebar...)
|
||||||
|
this.controlPanelElements = this._renderControlPanelElements();
|
||||||
|
|
||||||
return $.when(
|
return $.when(
|
||||||
this._super.apply(this, arguments),
|
this._super.apply(this, arguments),
|
||||||
this.renderer.appendTo(this.$el)
|
this.renderer.appendTo(this.$el)
|
||||||
).then(this._update.bind(this, this.initialState));
|
).then(function () {
|
||||||
|
self._update(self.initialState);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
@@ -60,6 +96,9 @@ var AbstractController = Widget.extend({
|
|||||||
if (this.$buttons) {
|
if (this.$buttons) {
|
||||||
this.$buttons.off();
|
this.$buttons.off();
|
||||||
}
|
}
|
||||||
|
if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
|
||||||
|
this.controlPanelElements.$switch_buttons.off();
|
||||||
|
}
|
||||||
return this._super.apply(this, arguments);
|
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
|
* 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
|
* 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
|
* this method on the active view to make sure it is ok to open the home
|
||||||
* screen (and lose all current state).
|
* screen (and lose all current state).
|
||||||
*
|
*
|
||||||
@@ -95,24 +134,16 @@ var AbstractController = Widget.extend({
|
|||||||
getContext: function () {
|
getContext: function () {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* @see setScrollTop
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
getScrollTop: function () {
|
|
||||||
return this.scrollTop;
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Returns a title that may be displayed in the breadcrumb area. For
|
* Returns a title that may be displayed in the breadcrumb area. For
|
||||||
* example, the name of the record.
|
* example, the name of the record.
|
||||||
*
|
*
|
||||||
* Note: this seems wrong right now, it should not be implemented, we have
|
* note: this will be moved to AbstractAction
|
||||||
* no guarantee that there is a display_name variable in a controller.
|
|
||||||
*
|
*
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getTitle: function () {
|
getTitle: function () {
|
||||||
return this.display_name;
|
return this.displayName;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* The use of this method is discouraged. It is still snakecased, because
|
* The use of this method is discouraged. It is still snakecased, because
|
||||||
@@ -136,12 +167,9 @@ var AbstractController = Widget.extend({
|
|||||||
return this.update(params || {});
|
return this.update(params || {});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Most likely called by the view manager, this method is responsible for
|
* Renders the buttons to append, in most cases, to the control panel (in
|
||||||
* adding buttons in the control panel (buttons such as save/discard/...)
|
* the bottom left corner). When the view is rendered in a dialog, those
|
||||||
*
|
* buttons might be moved to the dialog's footer.
|
||||||
* 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)
|
|
||||||
*
|
*
|
||||||
* @param {jQuery Node} $node
|
* @param {jQuery Node} $node
|
||||||
*/
|
*/
|
||||||
@@ -153,7 +181,7 @@ var AbstractController = Widget.extend({
|
|||||||
* controller can actually render whatever he wants in the pager zone. If
|
* controller can actually render whatever he wants in the pager zone. If
|
||||||
* your view does not want a pager, just let this method empty.
|
* your view does not want a pager, just let this method empty.
|
||||||
*
|
*
|
||||||
* @param {Query Node} $node
|
* @param {jQuery Node} $node
|
||||||
*/
|
*/
|
||||||
renderPager: function ($node) {
|
renderPager: function ($node) {
|
||||||
},
|
},
|
||||||
@@ -161,20 +189,10 @@ var AbstractController = Widget.extend({
|
|||||||
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu
|
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu
|
||||||
* dropdown in the control panel next to the buttons)
|
* dropdown in the control panel next to the buttons)
|
||||||
*
|
*
|
||||||
* @param {Query Node} $node
|
* @param {jQuery Node} $node
|
||||||
*/
|
*/
|
||||||
renderSidebar: function ($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
|
* 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
|
* view arrive in this method, and internal changes can sometimes also call
|
||||||
@@ -219,7 +237,68 @@ var AbstractController = Widget.extend({
|
|||||||
* world
|
* world
|
||||||
*/
|
*/
|
||||||
_pushState: function (state) {
|
_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
|
* This method is called after each update or when the start method is
|
||||||
@@ -235,6 +314,22 @@ var AbstractController = Widget.extend({
|
|||||||
* @returns {Deferred}
|
* @returns {Deferred}
|
||||||
*/
|
*/
|
||||||
_update: function (state) {
|
_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();
|
this._pushState();
|
||||||
return $.when();
|
return $.when();
|
||||||
},
|
},
|
||||||
@@ -249,7 +344,7 @@ var AbstractController = Widget.extend({
|
|||||||
*
|
*
|
||||||
* Note: this method seems wrong, it relies on the model being a basic model,
|
* 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
|
* 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
|
* @private
|
||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
@@ -278,6 +373,15 @@ var AbstractController = Widget.extend({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.do_action(event.target.name);
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -67,11 +67,18 @@ var AbstractView = Class.extend({
|
|||||||
* @param {string} params.modelName The actual model name
|
* @param {string} params.modelName The actual model name
|
||||||
* @param {Object} params.context
|
* @param {Object} params.context
|
||||||
* @param {number} [params.count]
|
* @param {number} [params.count]
|
||||||
|
* @param {string} [params.controllerID]
|
||||||
* @param {string[]} params.domain
|
* @param {string[]} params.domain
|
||||||
* @param {string[]} params.groupBy
|
* @param {string[]} params.groupBy
|
||||||
* @param {number} [params.currentId]
|
* @param {number} [params.currentId]
|
||||||
* @param {number[]} [params.ids]
|
* @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.help]
|
||||||
|
* @param {string} [params.action.jsID]
|
||||||
|
* @param {boolean} [params.action.views]
|
||||||
*/
|
*/
|
||||||
init: function (viewInfo, params) {
|
init: function (viewInfo, params) {
|
||||||
this.rendererParams = {
|
this.rendererParams = {
|
||||||
@@ -87,7 +94,23 @@ var AbstractView = Class.extend({
|
|||||||
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
|
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
|
||||||
duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : 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 = {
|
this.loadParams = {
|
||||||
context: params.context,
|
context: params.context,
|
||||||
@@ -133,7 +156,7 @@ var AbstractView = Class.extend({
|
|||||||
* load itself, and the renderer needs the data in its constructor.
|
* load itself, and the renderer needs the data in its constructor.
|
||||||
*
|
*
|
||||||
* @param {Widget} parent The parent of the resulting Controller (most
|
* @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
|
* @returns {Deferred} The deferred resolves to a controller
|
||||||
*/
|
*/
|
||||||
getController: function (parent) {
|
getController: function (parent) {
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
|||||||
_abandonRecord: function (recordID) {
|
_abandonRecord: function (recordID) {
|
||||||
recordID = recordID || this.handle;
|
recordID = recordID || this.handle;
|
||||||
if (recordID === this.handle) {
|
if (recordID === this.handle) {
|
||||||
this.trigger_up('switch_to_previous_view');
|
this.trigger_up('history_back');
|
||||||
} else {
|
} else {
|
||||||
this.model.removeLine(recordID);
|
this.model.removeLine(recordID);
|
||||||
}
|
}
|
||||||
@@ -244,17 +244,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
|||||||
model: record.model,
|
model: record.model,
|
||||||
resIDs: record.res_ids,
|
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_success: def.resolve.bind(def),
|
||||||
|
on_fail: function () {
|
||||||
|
reload().always(def.reject.bind(def));
|
||||||
|
},
|
||||||
|
on_closed: reload,
|
||||||
});
|
});
|
||||||
return this.alive(def);
|
return this.alive(def);
|
||||||
},
|
},
|
||||||
@@ -313,13 +307,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
|||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
* @param {boolean} [options.readonlyIfRealDiscard=false]
|
* @param {boolean} [options.readonlyIfRealDiscard=false]
|
||||||
* After discarding record changes, the usual option is to make the
|
* 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
|
* 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
|
* 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
|
* (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
|
* 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
|
* 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}
|
* @returns {Deferred}
|
||||||
*/
|
*/
|
||||||
_discardChanges: function (recordID, options) {
|
_discardChanges: function (recordID, options) {
|
||||||
@@ -467,7 +461,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
|||||||
var sidebarEnv = this._getSidebarEnv();
|
var sidebarEnv = this._getSidebarEnv();
|
||||||
this.sidebar.updateEnv(sidebarEnv);
|
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
|
* Helper method, to make sure the information displayed by the pager is up
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ var FormController = BasicController.extend({
|
|||||||
* inserted
|
* inserted
|
||||||
**/
|
**/
|
||||||
renderSidebar: function ($node) {
|
renderSidebar: function ($node) {
|
||||||
if (!this.sidebar && this.hasSidebar) {
|
if (this.hasSidebar) {
|
||||||
var otherItems = [];
|
var otherItems = [];
|
||||||
if (this.is_action_enabled('delete')) {
|
if (this.is_action_enabled('delete')) {
|
||||||
otherItems.push({
|
otherItems.push({
|
||||||
@@ -280,7 +280,7 @@ var FormController = BasicController.extend({
|
|||||||
_onDeletedRecords: function () {
|
_onDeletedRecords: function () {
|
||||||
var state = this.model.get(this.handle, {raw: true});
|
var state = this.model.get(this.handle, {raw: true});
|
||||||
if (!state.res_ids.length) {
|
if (!state.res_ids.length) {
|
||||||
this.do_action('history_back');
|
this.trigger_up('history_back');
|
||||||
} else {
|
} else {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ odoo.define('web.FormView', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var BasicView = require('web.BasicView');
|
var BasicView = require('web.BasicView');
|
||||||
var Context = require('web.Context');
|
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var FormController = require('web.FormController');
|
var FormController = require('web.FormController');
|
||||||
var FormRenderer = require('web.FormRenderer');
|
var FormRenderer = require('web.FormRenderer');
|
||||||
@@ -25,15 +24,16 @@ var FormView = BasicView.extend({
|
|||||||
init: function (viewInfo, params) {
|
init: function (viewInfo, params) {
|
||||||
this._super.apply(this, arguments);
|
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.loadParams.type = 'record';
|
||||||
|
|
||||||
this.controllerParams.disableAutofocus = params.disable_autofocus;
|
this.controllerParams.disableAutofocus = params.disable_autofocus;
|
||||||
this.controllerParams.hasSidebar = params.sidebar;
|
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
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) {
|
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;
|
var defaultButtons = 'default_buttons' in params ? params.default_buttons : true;
|
||||||
this.controllerParams.defaultButtons = defaultButtons;
|
this.controllerParams.defaultButtons = defaultButtons;
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ var ListController = BasicController.extend({
|
|||||||
* @param {jQuery Node} $node
|
* @param {jQuery Node} $node
|
||||||
*/
|
*/
|
||||||
renderSidebar: function ($node) {
|
renderSidebar: function ($node) {
|
||||||
if (this.hasSidebar && !this.sidebar) {
|
if (this.hasSidebar) {
|
||||||
var other = [{
|
var other = [{
|
||||||
label: _t("Export"),
|
label: _t("Export"),
|
||||||
callback: this._onExportData.bind(this)
|
callback: this._onExportData.bind(this)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var ListView = BasicView.extend({
|
|||||||
*
|
*
|
||||||
* @param {Object} viewInfo
|
* @param {Object} viewInfo
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {boolean} params.sidebar
|
* @param {boolean} params.hasSidebar
|
||||||
* @param {boolean} [params.hasSelectors=true]
|
* @param {boolean} [params.hasSelectors=true]
|
||||||
*/
|
*/
|
||||||
init: function (viewInfo, params) {
|
init: function (viewInfo, params) {
|
||||||
@@ -40,7 +40,7 @@ var ListView = BasicView.extend({
|
|||||||
var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly";
|
var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly";
|
||||||
|
|
||||||
this.controllerParams.editable = arch.attrs.editable;
|
this.controllerParams.editable = arch.attrs.editable;
|
||||||
this.controllerParams.hasSidebar = params.sidebar;
|
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
||||||
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
|
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
|
||||||
this.controllerParams.mode = mode;
|
this.controllerParams.mode = mode;
|
||||||
|
|||||||
@@ -175,8 +175,9 @@ var FormViewDialog = ViewDialog.extend({
|
|||||||
currentId: self.res_id || undefined,
|
currentId: self.res_id || undefined,
|
||||||
index: 0,
|
index: 0,
|
||||||
mode: self.res_id && self.options.readonly ? 'readonly' : 'edit',
|
mode: self.res_id && self.options.readonly ? 'readonly' : 'edit',
|
||||||
footer_to_buttons: true,
|
footerToButtons: true,
|
||||||
default_buttons: false,
|
default_buttons: false,
|
||||||
|
withControlPanel: false,
|
||||||
model: self.model,
|
model: self.model,
|
||||||
parentID: self.parentID,
|
parentID: self.parentID,
|
||||||
recordID: self.recordID,
|
recordID: self.recordID,
|
||||||
@@ -190,9 +191,9 @@ var FormViewDialog = ViewDialog.extend({
|
|||||||
}
|
}
|
||||||
self.form_view.appendTo(fragment)
|
self.form_view.appendTo(fragment)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
var $buttons = $('<div>');
|
|
||||||
self.form_view.renderButtons($buttons);
|
|
||||||
self.opened().always(function () {
|
self.opened().always(function () {
|
||||||
|
var $buttons = $('<div>');
|
||||||
|
self.form_view.renderButtons($buttons);
|
||||||
if ($buttons.children().length) {
|
if ($buttons.children().length) {
|
||||||
self.$footer.empty().append($buttons.contents());
|
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);
|
this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length);
|
||||||
},
|
},
|
||||||
search: function (event) {
|
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 d = event.data;
|
||||||
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
|
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
|
||||||
this.list_controller.reload(searchData);
|
this.list_controller.reload(searchData);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ var field_utils = require('web.field_utils');
|
|||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
var SystrayMenu = require('web.SystrayMenu');
|
var SystrayMenu = require('web.SystrayMenu');
|
||||||
var utils = require('web.utils');
|
var utils = require('web.utils');
|
||||||
var ViewManager = require('web.ViewManager');
|
|
||||||
var WebClient = require('web.WebClient');
|
var WebClient = require('web.WebClient');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
|
|
||||||
@@ -332,42 +331,35 @@ DebugManager.include({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
update: function (tag, descriptor, widget) {
|
update: function (tag, descriptor, widget) {
|
||||||
switch (tag) {
|
if (tag === 'action' || tag === 'view') {
|
||||||
case 'action':
|
this._controller = widget;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._super(tag, descriptor).then(function () {
|
return this._super(tag, descriptor).then(function () {
|
||||||
this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', {
|
this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', {
|
||||||
manager: this,
|
|
||||||
action: this._action,
|
action: this._action,
|
||||||
view: this._active_view,
|
|
||||||
can_edit: this._can_edit_views,
|
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));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
get_metadata: function() {
|
get_metadata: function() {
|
||||||
var ds = this._view_manager.dataset;
|
var self = this;
|
||||||
if (!this._active_view.controller.getSelectedIds().length) {
|
var selectedIDs = this._controller.getSelectedIds();
|
||||||
|
if (!selectedIDs.length) {
|
||||||
console.warn(_t("No metadata available"));
|
console.warn(_t("No metadata available"));
|
||||||
return;
|
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];
|
var metadata = result[0];
|
||||||
metadata.creator = field_utils.format.many2one(metadata.create_uid);
|
metadata.creator = field_utils.format.many2one(metadata.create_uid);
|
||||||
metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_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);
|
var modificationDate = field_utils.parse.datetime(metadata.write_date);
|
||||||
metadata.write_date = field_utils.format.datetime(modificationDate);
|
metadata.write_date = field_utils.format.datetime(modificationDate);
|
||||||
new Dialog(this, {
|
new Dialog(this, {
|
||||||
title: _.str.sprintf(_t("Metadata (%s)"), ds.model),
|
title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model),
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
$content: QWeb.render('WebClient.DebugViewLog', {
|
$content: QWeb.render('WebClient.DebugViewLog', {
|
||||||
perm : metadata,
|
perm : metadata,
|
||||||
@@ -400,11 +392,12 @@ DebugManager.include({
|
|||||||
return [value, displayed];
|
return [value, displayed];
|
||||||
};
|
};
|
||||||
|
|
||||||
var renderer = this._active_view.controller.renderer;
|
var renderer = this._controller.renderer;
|
||||||
var fields = self._active_view.fields_view.fields;
|
var state = renderer.state;
|
||||||
var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
|
var fields = state.fields;
|
||||||
var fieldNamesInView = renderer.state.getFieldNames();
|
var fieldsInfo = state.fieldsInfo.form;
|
||||||
var fieldsValues = renderer.state.data;
|
var fieldNamesInView = state.getFieldNames();
|
||||||
|
var fieldsValues = state.data;
|
||||||
var modifierDatas = {};
|
var modifierDatas = {};
|
||||||
_.each(fieldNamesInView, function (fieldName) {
|
_.each(fieldNamesInView, function (fieldName) {
|
||||||
modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
|
modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
|
||||||
@@ -416,14 +409,14 @@ DebugManager.include({
|
|||||||
var modifierData = modifierDatas[fieldName];
|
var modifierData = modifierDatas[fieldName];
|
||||||
var invisibleOrReadOnly;
|
var invisibleOrReadOnly;
|
||||||
if (modifierData) {
|
if (modifierData) {
|
||||||
var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
|
var evaluatedModifiers = modifierData.evaluatedModifiers[state.id];
|
||||||
invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
|
invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
|
||||||
}
|
}
|
||||||
var fieldInfo = fields[fieldName];
|
var fieldInfo = fields[fieldName];
|
||||||
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
|
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
|
||||||
var value = valueDisplayed[0];
|
var value = valueDisplayed[0];
|
||||||
var displayed = valueDisplayed[1];
|
var displayed = valueDisplayed[1];
|
||||||
// ignore fields which are empty, invisible, readonly, o2m
|
// ignore fields which are empty, invisible, readonly, o2m
|
||||||
// or m2m
|
// or m2m
|
||||||
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
||||||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
||||||
@@ -479,7 +472,7 @@ DebugManager.include({
|
|||||||
model: 'ir.default',
|
model: 'ir.default',
|
||||||
method: 'set',
|
method: 'set',
|
||||||
args: [
|
args: [
|
||||||
self._active_view.fields_view.model,
|
self._controller.fields_view.model,
|
||||||
fieldToSet,
|
fieldToSet,
|
||||||
value,
|
value,
|
||||||
allUsers,
|
allUsers,
|
||||||
@@ -502,7 +495,7 @@ DebugManager.include({
|
|||||||
var dialog = new Dialog(this, { title: _t("Fields View Get") });
|
var dialog = new Dialog(this, { title: _t("Fields View Get") });
|
||||||
dialog.opened().then(function () {
|
dialog.opened().then(function () {
|
||||||
$('<pre>').text(utils.json_node_to_xml(
|
$('<pre>').text(utils.json_node_to_xml(
|
||||||
self._active_view.controller.renderer.arch, true)
|
self._controller.renderer.arch, true)
|
||||||
).appendTo(dialog.$el);
|
).appendTo(dialog.$el);
|
||||||
});
|
});
|
||||||
dialog.open();
|
dialog.open();
|
||||||
@@ -788,30 +781,80 @@ if (config.debug) {
|
|||||||
SystrayMenu.Items.push(DebugManager);
|
SystrayMenu.Items.push(DebugManager);
|
||||||
|
|
||||||
WebClient.include({
|
WebClient.include({
|
||||||
current_action_updated: function(action) {
|
//----------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
current_action_updated: function (action, controller) {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
var action_descr = action && action.action_descr;
|
var debugManager = _.find(this.systray_menu.widgets, function(item) {
|
||||||
var action_widget = action && action.widget;
|
return item instanceof DebugManager;
|
||||||
var debug_manager = _.find(this.systray_menu.widgets, function(item) {return item instanceof DebugManager; });
|
});
|
||||||
debug_manager.update('action', action_descr, action_widget);
|
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({
|
Dialog.include({
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
open: function() {
|
open: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
// if the dialog is opened by the ActionManager, instantiate a
|
||||||
var parent = self.getParent();
|
// DebugManager and insert it into the DOM once the dialog is opened
|
||||||
if (parent instanceof ActionManager && parent.dialog_widget) {
|
// (delay this with a setTimeout(0) to ensure that the internal
|
||||||
// Instantiate the DebugManager and insert it into the DOM once dialog is opened
|
// state, i.e. the current action and controller, of the
|
||||||
this.opened(function() {
|
// ActionManager is set to properly update the DebugManager)
|
||||||
self.debug_manager = new DebugManager(self);
|
this.opened(function() {
|
||||||
var $header = self.$modal.find('.modal-header:first');
|
setTimeout(function () {
|
||||||
return self.debug_manager.prependTo($header).then(function() {
|
var parent = self.getParent();
|
||||||
self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
|
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);
|
return this._super.apply(this, arguments);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Block fields
|
// Block fields
|
||||||
&.note-editor, &.oe_form_field_html, &.oe_form_field_html_text, &.oe_form_field_text, &.o_view_manager_content, &.o_field_domain, &.o_graph_linechart, &.o_graph_barchart {
|
&.note-editor, &.oe_form_field_html, &.oe_form_field_html_text, &.oe_form_field_text, &.o_field_domain, &.o_graph_linechart, &.o_graph_barchart {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -431,7 +431,7 @@
|
|||||||
|
|
||||||
// One2Many, Many2Many outside of group
|
// One2Many, Many2Many outside of group
|
||||||
.o_field_widget {
|
.o_field_widget {
|
||||||
&.o_view_manager_content, &.o_field_one2many, &.o_field_many2many {
|
&.o_field_one2many, &.o_field_many2many {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
> div {
|
> div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
.o_graph {
|
.o_graph {
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.o_graph_svg_container {
|
.o_graph_svg_container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -23,10 +23,6 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_view_manager_content {
|
|
||||||
position: static; // Reset position for view managers in modals (default value: absolute)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.o_pivot {
|
.o_pivot {
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
// Inform the user that he may click on a cell to be redirected to a list view of the
|
// Inform the user that he may click on a cell to be redirected to a list view of the
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// No content helper
|
// No content helper
|
||||||
.o_view_manager_content > div {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.oe_view_nocontent {
|
.oe_view_nocontent {
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -155,14 +155,9 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.o_view_manager_content {
|
> .o_view_controller {
|
||||||
.o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
|
.o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.o_view_manager_content {
|
|
||||||
position: static; // Reset position for nested view managers
|
|
||||||
height: auto; // reset height inheritance for nested view managers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,8 +177,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div t-name="ActionManager" class="o_content"/>
|
|
||||||
|
|
||||||
<t t-name="ControlPanel">
|
<t t-name="ControlPanel">
|
||||||
<div class="o_control_panel">
|
<div class="o_control_panel">
|
||||||
<ol class="breadcrumb"/>
|
<ol class="breadcrumb"/>
|
||||||
@@ -201,9 +199,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<t t-name="ViewManager.switch-buttons">
|
<t t-name="ControlPanel.SwitchButtons">
|
||||||
<t t-foreach="views" t-as="view">
|
<t t-foreach="views" t-as="view">
|
||||||
<button type="button" t-att-accesskey="view.accesskey"
|
<button type="button" t-att-accesskey="view.accessKey"
|
||||||
t-attf-class="btn btn-icon fa fa-lg #{view.icon} o_cp_switch_#{view.type}"
|
t-attf-class="btn btn-icon fa fa-lg #{view.icon} o_cp_switch_#{view.type}"
|
||||||
t-att-aria-label="view.type"
|
t-att-aria-label="view.type"
|
||||||
t-att-data-view-type="view.type" t-att-title="view.label"/>
|
t-att-data-view-type="view.type" t-att-title="view.label"/>
|
||||||
@@ -259,14 +257,14 @@
|
|||||||
<li class="divider"/>
|
<li class="divider"/>
|
||||||
<t t-if="view.type === 'form'">
|
<t t-if="view.type === 'form'">
|
||||||
<li><a href="#" data-action="set_defaults">Set Defaults</a></li>
|
<li><a href="#" data-action="set_defaults">Set Defaults</a></li>
|
||||||
<t t-if="view.controller.getSelectedIds().length === 1">
|
<t t-if="controller.getSelectedIds().length === 1">
|
||||||
<li><a href="#" data-action="get_metadata">View Metadata</a></li>
|
<li><a href="#" data-action="get_metadata">View Metadata</a></li>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
<li><a href="#" data-action="fvg">Fields View Get</a></li>
|
<li><a href="#" data-action="fvg">Fields View Get</a></li>
|
||||||
<t t-if="can_edit">
|
<t t-if="can_edit">
|
||||||
<li>
|
<li>
|
||||||
<a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">
|
<a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fieldsView.view_id">
|
||||||
<t t-if="view.type == 'form'">Edit Form view</t>
|
<t t-if="view.type == 'form'">Edit Form view</t>
|
||||||
<t t-elif="view.type == 'list'">Edit List view</t>
|
<t t-elif="view.type == 'list'">Edit List view</t>
|
||||||
<t t-elif="view.type == 'kanban'">Edit Kanban view</t>
|
<t t-elif="view.type == 'kanban'">Edit Kanban view</t>
|
||||||
@@ -277,7 +275,7 @@
|
|||||||
<t t-else="1">Edit view, type: <t t-esc="_.str.capitalize(view.type)"/></t>
|
<t t-else="1">Edit view, type: <t t-esc="_.str.capitalize(view.type)"/></t>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit Search view</a></li>
|
<li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="action.searchFieldsView.view_id">Edit SearchView</a></li>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
odoo.define('web.action_manager_mobile_tests', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var testUtils = require('web.test_utils');
|
||||||
|
|
||||||
|
var createActionManager = testUtils.createActionManager;
|
||||||
|
|
||||||
|
QUnit.module('ActionManager', {
|
||||||
|
beforeEach: function () {
|
||||||
|
this.data = {
|
||||||
|
partner: {
|
||||||
|
fields: {
|
||||||
|
foo: {string: "Foo", type: "char"},
|
||||||
|
},
|
||||||
|
records: [
|
||||||
|
{id: 1, display_name: "First record", foo: "yop"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.actions = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Partners Action 1',
|
||||||
|
res_model: 'partner',
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
views: [[false, 'list'], [false, 'kanban'], [false, 'form']],
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Partners Action 2',
|
||||||
|
res_model: 'partner',
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
views: [[false, 'list'], [false, 'form']],
|
||||||
|
}];
|
||||||
|
|
||||||
|
this.archs = {
|
||||||
|
// kanban views
|
||||||
|
'partner,false,kanban': '<kanban><templates><t t-name="kanban-box">' +
|
||||||
|
'<div class="oe_kanban_global_click"><field name="foo"/></div>' +
|
||||||
|
'</t></templates></kanban>',
|
||||||
|
|
||||||
|
// list views
|
||||||
|
'partner,false,list': '<tree><field name="foo"/></tree>',
|
||||||
|
|
||||||
|
// form views
|
||||||
|
'partner,false,form': '<form>' +
|
||||||
|
'<group>' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'</group>' +
|
||||||
|
'</form>',
|
||||||
|
|
||||||
|
// search views
|
||||||
|
'partner,false,search': '<search><field name="foo" string="Foo"/></search>',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}, function () {
|
||||||
|
QUnit.test('uses a mobile-friendly view by default (if possible)', function (assert) {
|
||||||
|
assert.expect(4);
|
||||||
|
|
||||||
|
var actionManager = createActionManager({
|
||||||
|
actions: this.actions,
|
||||||
|
archs: this.archs,
|
||||||
|
data: this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// should default on a mobile-friendly view (kanban) for action 1
|
||||||
|
actionManager.doAction(1);
|
||||||
|
|
||||||
|
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
|
||||||
|
"should not have rendered the list view");
|
||||||
|
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
|
||||||
|
"should have rendered the kanban view");
|
||||||
|
|
||||||
|
// there is no mobile-friendly view for action 2, should use the first one (list)
|
||||||
|
actionManager.doAction(2);
|
||||||
|
|
||||||
|
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
|
||||||
|
"should have rendered the list view");
|
||||||
|
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
|
||||||
|
"there should be no kanban view in the DOM");
|
||||||
|
|
||||||
|
actionManager.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ var MockServer = Class.extend({
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
|
* @param {Object[]} [options.actions=[]]
|
||||||
* @param {Object} [options.archs={}] dict of archs with keys being strings like
|
* @param {Object} [options.archs={}] dict of archs with keys being strings like
|
||||||
* 'model,id,viewType'
|
* 'model,id,viewType'
|
||||||
* @param {boolean} [options.debug=false] logs RPCs if set to true
|
* @param {boolean} [options.debug=false] logs RPCs if set to true
|
||||||
@@ -47,6 +48,7 @@ var MockServer = Class.extend({
|
|||||||
|
|
||||||
this.currentDate = options.currentDate || moment().format("YYYY-MM-DD");
|
this.currentDate = options.currentDate || moment().format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
this.actions = options.actions || [];
|
||||||
this.archs = options.archs || {};
|
this.archs = options.archs || {};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -431,6 +433,23 @@ var MockServer = Class.extend({
|
|||||||
}
|
}
|
||||||
return modelFields;
|
return modelFields;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Simulate a call to the '/web/action/load' route
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Object} kwargs
|
||||||
|
* @param {integer} kwargs.action_id
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
_mockLoadAction: function (kwargs) {
|
||||||
|
var action = _.findWhere(this.actions, {id: parseInt(kwargs.action_id)});
|
||||||
|
if (!action) {
|
||||||
|
// when the action doesn't exist, the real server doesn't crash, it
|
||||||
|
// simply returns false
|
||||||
|
console.warn("No action found for ID " + event.data.actionID);
|
||||||
|
}
|
||||||
|
return action || false;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Simulate a 'load_views' operation
|
* Simulate a 'load_views' operation
|
||||||
*
|
*
|
||||||
@@ -943,7 +962,7 @@ var MockServer = Class.extend({
|
|||||||
_performRpc: function (route, args) {
|
_performRpc: function (route, args) {
|
||||||
switch (route) {
|
switch (route) {
|
||||||
case '/web/action/load':
|
case '/web/action/load':
|
||||||
return $.when(this._mockLoadAction(args));
|
return $.when(this._mockLoadAction(args.kwargs));
|
||||||
|
|
||||||
case '/web/dataset/search_read':
|
case '/web/dataset/search_read':
|
||||||
return $.when(this._mockSearchReadController(args));
|
return $.when(this._mockSearchReadController(args));
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ odoo.define('web.test_utils', function (require) {
|
|||||||
* instance of a view, appended in the dom, ready to be tested.
|
* instance of a view, appended in the dom, ready to be tested.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var ActionManager = require('web.ActionManager');
|
||||||
var ajax = require('web.ajax');
|
var ajax = require('web.ajax');
|
||||||
var basic_fields = require('web.basic_fields');
|
var basic_fields = require('web.basic_fields');
|
||||||
var config = require('web.config');
|
var config = require('web.config');
|
||||||
|
var ControlPanel = require('web.ControlPanel');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var dom = require('web.dom');
|
var dom = require('web.dom');
|
||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
@@ -58,6 +60,58 @@ function observe(widget) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create and return an instance of ActionManager with all rpcs going through a
|
||||||
|
* mock method using the data, actions and archs objects as sources.
|
||||||
|
*
|
||||||
|
* @param {Object} [params]
|
||||||
|
* @param {Object} [params.actions] the actions given to the mock server
|
||||||
|
* @param {Object} [params.archs] this archs given to the mock server
|
||||||
|
* @param {Object} [params.data] the business data given to the mock server
|
||||||
|
* @param {boolean} [params.debug]
|
||||||
|
* @param {function} [params.mockRPC]
|
||||||
|
* @returns {ActionManager}
|
||||||
|
*/
|
||||||
|
var createActionManager = function (params) {
|
||||||
|
params = params || {};
|
||||||
|
var $target = $('#qunit-fixture');
|
||||||
|
if (params.debug) {
|
||||||
|
$target = $('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
var widget = new Widget();
|
||||||
|
// when 'document' addon is installed, the sidebar does a 'search_read' on
|
||||||
|
// model 'ir_attachment' each time a record is open, so we monkey-patch
|
||||||
|
// 'mockRPC' to mute those RPCs, so that the tests can be written uniformly,
|
||||||
|
// whether or not 'document' is installed
|
||||||
|
var mockRPC = params.mockRPC;
|
||||||
|
_.extend(params, {
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.model === 'ir.attachment') {
|
||||||
|
return $.when([]);
|
||||||
|
}
|
||||||
|
if (mockRPC) {
|
||||||
|
return mockRPC.apply(this, arguments);
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
addMockEnvironment(widget, _.defaults(params, {debounce: false}));
|
||||||
|
widget.appendTo($target);
|
||||||
|
widget.$el.addClass('o_web_client');
|
||||||
|
|
||||||
|
var userContext = params.context && params.context.user_context || {};
|
||||||
|
var actionManager = new ActionManager(widget, userContext);
|
||||||
|
var originalDestroy = ActionManager.prototype.destroy;
|
||||||
|
actionManager.destroy = function () {
|
||||||
|
actionManager.destroy = originalDestroy;
|
||||||
|
widget.destroy();
|
||||||
|
};
|
||||||
|
actionManager.appendTo(widget.$el);
|
||||||
|
|
||||||
|
return actionManager;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a view synchronously. This method uses the createAsyncView method.
|
* create a view synchronously. This method uses the createAsyncView method.
|
||||||
* Most views are synchronous, so the deferred can be resolved immediately and
|
* Most views are synchronous, so the deferred can be resolved immediately and
|
||||||
@@ -87,8 +141,7 @@ function createView(params) {
|
|||||||
*
|
*
|
||||||
* It returns the instance of the view, properly created, with all rpcs going
|
* It returns the instance of the view, properly created, with all rpcs going
|
||||||
* through a mock method using the data object as source, and already loaded/
|
* through a mock method using the data object as source, and already loaded/
|
||||||
* started (with a do_search). The buttons/pager should also be created, if
|
* started.
|
||||||
* appropriate.
|
|
||||||
*
|
*
|
||||||
* Most views can be tested synchronously (@see createView), but some view have
|
* Most views can be tested synchronously (@see createView), but some view have
|
||||||
* external dependencies (like lazy loaded libraries). In that case, it is
|
* external dependencies (like lazy loaded libraries). In that case, it is
|
||||||
@@ -145,9 +198,9 @@ function createAsyncView(params) {
|
|||||||
|
|
||||||
// reproduce the DOM environment of views
|
// reproduce the DOM environment of views
|
||||||
var $web_client = $('<div>').addClass('o_web_client').prependTo($target);
|
var $web_client = $('<div>').addClass('o_web_client').prependTo($target);
|
||||||
var $control_panel = $('<div>').addClass('o_control_panel').appendTo($web_client);
|
var controlPanel = new ControlPanel(widget);
|
||||||
|
controlPanel.appendTo($web_client);
|
||||||
var $content = $('<div>').addClass('o_content').appendTo($web_client);
|
var $content = $('<div>').addClass('o_content').appendTo($web_client);
|
||||||
var $view_manager = $('<div>').addClass('o_view_manager_content').appendTo($content);
|
|
||||||
|
|
||||||
return view.getController(widget).then(function (view) {
|
return view.getController(widget).then(function (view) {
|
||||||
// override the view's 'destroy' so that it calls 'destroy' on the widget
|
// override the view's 'destroy' so that it calls 'destroy' on the widget
|
||||||
@@ -159,29 +212,21 @@ function createAsyncView(params) {
|
|||||||
widget.destroy();
|
widget.destroy();
|
||||||
$('#qunit-fixture').off('DOMNodeInserted.removeSRC');
|
$('#qunit-fixture').off('DOMNodeInserted.removeSRC');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// link the view to the control panel
|
||||||
|
view.set_cp_bus(controlPanel.get_bus());
|
||||||
|
|
||||||
// render the view in a fragment as they must be able to render correctly
|
// render the view in a fragment as they must be able to render correctly
|
||||||
// without being in the DOM
|
// without being in the DOM
|
||||||
var fragment = document.createDocumentFragment();
|
var fragment = document.createDocumentFragment();
|
||||||
return view.appendTo(fragment).then(function () {
|
return view.appendTo(fragment).then(function () {
|
||||||
dom.append($view_manager, fragment, {
|
dom.append($content, fragment, {
|
||||||
callbacks: [{widget: view}],
|
callbacks: [{widget: view}],
|
||||||
in_DOM: true,
|
in_DOM: true,
|
||||||
});
|
});
|
||||||
view.$el.on('click', 'a', function (ev) {
|
view.$el.on('click', 'a', function (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
});
|
});
|
||||||
}).then(function () {
|
|
||||||
var $buttons = $('<div>');
|
|
||||||
view.renderButtons($buttons);
|
|
||||||
$buttons.contents().appendTo($control_panel);
|
|
||||||
|
|
||||||
var $sidebar = $('<div>');
|
|
||||||
view.renderSidebar($sidebar);
|
|
||||||
$sidebar.contents().appendTo($control_panel);
|
|
||||||
|
|
||||||
var $pager = $('<div>');
|
|
||||||
view.renderPager($pager);
|
|
||||||
$pager.contents().appendTo($control_panel);
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
});
|
});
|
||||||
@@ -250,6 +295,7 @@ function addMockEnvironment(widget, params) {
|
|||||||
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
||||||
}
|
}
|
||||||
var mockServer = new Server(params.data, {
|
var mockServer = new Server(params.data, {
|
||||||
|
actions: params.actions,
|
||||||
archs: params.archs,
|
archs: params.archs,
|
||||||
currentDate: params.currentDate,
|
currentDate: params.currentDate,
|
||||||
debug: params.debug,
|
debug: params.debug,
|
||||||
@@ -336,6 +382,14 @@ function addMockEnvironment(widget, params) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
intercept(widget, 'load_action', function (event) {
|
||||||
|
mockServer.performRpc('/web/action/load', {
|
||||||
|
kwargs: {action_id: event.data.actionID},
|
||||||
|
}).then(function (action) {
|
||||||
|
event.data.on_success(action);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
intercept(widget, "load_views", function (event) {
|
intercept(widget, "load_views", function (event) {
|
||||||
mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, {
|
mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, {
|
||||||
args: [],
|
args: [],
|
||||||
@@ -617,6 +671,7 @@ return $.when(
|
|||||||
}, 0);
|
}, 0);
|
||||||
return {
|
return {
|
||||||
addMockEnvironment: addMockEnvironment,
|
addMockEnvironment: addMockEnvironment,
|
||||||
|
createActionManager: createActionManager,
|
||||||
createAsyncView: createAsyncView,
|
createAsyncView: createAsyncView,
|
||||||
createModel: createModel,
|
createModel: createModel,
|
||||||
createParent: createParent,
|
createParent: createParent,
|
||||||
|
|||||||
@@ -1451,7 +1451,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="foo"/>' +
|
'<field name="foo"/>' +
|
||||||
'</sheet>' +
|
'</sheet>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
@@ -1597,7 +1597,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="foo"/>' +
|
'<field name="foo"/>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
return $.when([]);
|
return $.when([]);
|
||||||
@@ -1628,7 +1628,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="foo"/>' +
|
'<field name="foo"/>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
return $.when([]);
|
return $.when([]);
|
||||||
@@ -1659,7 +1659,7 @@ QUnit.module('Views', {
|
|||||||
'</footer>' +
|
'</footer>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {footer_to_buttons: true},
|
viewOptions: {footerToButtons: true},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.ok(form.$buttons.find('button.infooter').length, "footer button should be in footer");
|
assert.ok(form.$buttons.find('button.infooter').length, "footer button should be in footer");
|
||||||
@@ -2050,7 +2050,7 @@ QUnit.module('Views', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<form string="Partners"><field name="date"></field></form>',
|
arch: '<form string="Partners"><field name="date"></field></form>',
|
||||||
intercepts: {
|
intercepts: {
|
||||||
switch_to_previous_view: function (event) {
|
history_back: function () {
|
||||||
form.update({}, {reload: false});
|
form.update({}, {reload: false});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2086,9 +2086,9 @@ QUnit.module('Views', {
|
|||||||
return this._super.apply(this, arguments);
|
return this._super.apply(this, arguments);
|
||||||
},
|
},
|
||||||
intercepts: {
|
intercepts: {
|
||||||
switch_to_previous_view: function (event) {
|
history_back: function () {
|
||||||
assert.ok(true, "should have sent correct event");
|
assert.ok(true, "should have sent correct event");
|
||||||
// simulate the response from the view manager, in the case
|
// simulate the response from the action manager, in the case
|
||||||
// where we have only one active view (the form). If there
|
// where we have only one active view (the form). If there
|
||||||
// was another view, we would have switched to that view
|
// was another view, we would have switched to that view
|
||||||
// instead
|
// instead
|
||||||
@@ -2126,7 +2126,7 @@ QUnit.module('Views', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||||
intercepts: {
|
intercepts: {
|
||||||
switch_to_previous_view: function () {
|
history_back: function () {
|
||||||
assert.ok(true, "should have sent correct event");
|
assert.ok(true, "should have sent correct event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2155,9 +2155,9 @@ QUnit.module('Views', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||||
intercepts: {
|
intercepts: {
|
||||||
switch_to_previous_view: function (event) {
|
history_back: function () {
|
||||||
assert.ok(true, "should have sent correct event");
|
assert.ok(true, "should have sent correct event");
|
||||||
// simulate the response from the view manager, in the case
|
// simulate the response from the action manager, in the case
|
||||||
// where we have only one active view (the form). If there
|
// where we have only one active view (the form). If there
|
||||||
// was another view, we would have switched to that view
|
// was another view, we would have switched to that view
|
||||||
// instead
|
// instead
|
||||||
@@ -2197,7 +2197,7 @@ QUnit.module('Views', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
return $.when([]);
|
return $.when([]);
|
||||||
@@ -2542,7 +2542,7 @@ QUnit.module('Views', {
|
|||||||
viewOptions: {
|
viewOptions: {
|
||||||
ids: [1, 2, 4],
|
ids: [1, 2, 4],
|
||||||
index: 0,
|
index: 0,
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
@@ -2586,7 +2586,7 @@ QUnit.module('Views', {
|
|||||||
viewOptions: {
|
viewOptions: {
|
||||||
ids: [1],
|
ids: [1],
|
||||||
index: 0,
|
index: 0,
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
@@ -2602,15 +2602,14 @@ QUnit.module('Views', {
|
|||||||
form.sidebar.$('button.o_dropdown_toggler_btn').click();
|
form.sidebar.$('button.o_dropdown_toggler_btn').click();
|
||||||
form.sidebar.$('a:contains(Delete)').click();
|
form.sidebar.$('a:contains(Delete)').click();
|
||||||
|
|
||||||
testUtils.intercept(form, 'do_action', function (event) {
|
testUtils.intercept(form, 'history_back', function () {
|
||||||
assert.strictEqual(event.data.action, 'history_back',
|
assert.step('history_back');
|
||||||
"should trigger an history back action");
|
|
||||||
});
|
});
|
||||||
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
|
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
|
||||||
$('.modal .modal-footer button.btn-primary').click();
|
$('.modal .modal-footer button.btn-primary').click();
|
||||||
assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
|
assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
|
||||||
|
|
||||||
assert.verifySteps(['read', 'unlink']);
|
assert.verifySteps(['read', 'unlink', 'history_back']);
|
||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4287,7 +4286,7 @@ QUnit.module('Views', {
|
|||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
footer_to_buttons: true,
|
footerToButtons: true,
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -4326,7 +4325,7 @@ QUnit.module('Views', {
|
|||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
footer_to_buttons: true,
|
footerToButtons: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4684,7 +4683,7 @@ QUnit.module('Views', {
|
|||||||
print: [],
|
print: [],
|
||||||
},
|
},
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (route === '/web/action/load') {
|
if (route === '/web/action/load') {
|
||||||
@@ -5943,7 +5942,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="display_name"/>' +
|
'<field name="display_name"/>' +
|
||||||
'</form>',
|
'</form>',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||||
// rpcs done by the sidebar
|
// rpcs done by the sidebar
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ QUnit.module('Views', {
|
|||||||
groupBy: ['bar'],
|
groupBy: ['bar'],
|
||||||
intercepts: {
|
intercepts: {
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(event.data.ids, envIDs,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -384,7 +384,7 @@ QUnit.module('Views', {
|
|||||||
groupBy: ['bar'],
|
groupBy: ['bar'],
|
||||||
intercepts: {
|
intercepts: {
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.strictEqual(event.data.ids.length, nbRecords,
|
assert.strictEqual(event.data.env.ids.length, nbRecords,
|
||||||
"should update the env with the records ids");
|
"should update the env with the records ids");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -666,7 +666,7 @@ QUnit.module('Views', {
|
|||||||
},
|
},
|
||||||
intercepts: {
|
intercepts: {
|
||||||
switch_view: function (event) {
|
switch_view: function (event) {
|
||||||
assert.deepEqual(event.data, {
|
assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), {
|
||||||
mode: 'readonly',
|
mode: 'readonly',
|
||||||
model: 'partner',
|
model: 'partner',
|
||||||
res_id: 1,
|
res_id: 1,
|
||||||
@@ -733,7 +733,7 @@ QUnit.module('Views', {
|
|||||||
},
|
},
|
||||||
intercepts: {
|
intercepts: {
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(event.data.ids, envIDs,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -977,7 +977,7 @@ QUnit.module('Views', {
|
|||||||
groupBy: ['product_id'],
|
groupBy: ['product_id'],
|
||||||
intercepts: {
|
intercepts: {
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(envIDs, event.data.ids,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1659,7 +1659,7 @@ QUnit.module('Views', {
|
|||||||
},
|
},
|
||||||
intercepts: {
|
intercepts: {
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(event.data.ids, envIDs,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1823,7 +1823,7 @@ QUnit.module('Views', {
|
|||||||
},
|
},
|
||||||
intercepts:{
|
intercepts:{
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(event.data.ids, envIDs,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ QUnit.module('Views', {
|
|||||||
View: ListView,
|
View: ListView,
|
||||||
model: 'foo',
|
model: 'foo',
|
||||||
data: this.data,
|
data: this.data,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
arch: '<tree delete="0"><field name="foo"/></tree>',
|
arch: '<tree delete="0"><field name="foo"/></tree>',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="foo"/>' +
|
'<field name="foo"/>' +
|
||||||
'<field name="m2o" invisible="1"/>' +
|
'<field name="m2o" invisible="1"/>' +
|
||||||
'</tree>',
|
'</tree>',
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route) {
|
||||||
assert.step(_.last(route.split('/')));
|
assert.step(_.last(route.split('/')));
|
||||||
return this._super.apply(this, arguments);
|
return this._super.apply(this, arguments);
|
||||||
},
|
},
|
||||||
@@ -765,7 +765,7 @@ QUnit.module('Views', {
|
|||||||
View: ListView,
|
View: ListView,
|
||||||
model: 'foo',
|
model: 'foo',
|
||||||
data: this.data,
|
data: this.data,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
arch: '<tree><field name="foo"/></tree>',
|
arch: '<tree><field name="foo"/></tree>',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -795,7 +795,7 @@ QUnit.module('Views', {
|
|||||||
View: ListView,
|
View: ListView,
|
||||||
model: 'foo',
|
model: 'foo',
|
||||||
data: this.data,
|
data: this.data,
|
||||||
viewOptions: {sidebar: true},
|
viewOptions: {hasSidebar: true},
|
||||||
arch: '<tree><field name="foo"/></tree>',
|
arch: '<tree><field name="foo"/></tree>',
|
||||||
mockRPC: function (route) {
|
mockRPC: function (route) {
|
||||||
if (route === '/web/dataset/call_kw/ir.attachment/search_read') {
|
if (route === '/web/dataset/call_kw/ir.attachment/search_read') {
|
||||||
@@ -1686,7 +1686,7 @@ QUnit.module('Views', {
|
|||||||
"'switch_view' event has been triggered");
|
"'switch_view' event has been triggered");
|
||||||
},
|
},
|
||||||
env_updated: function (event) {
|
env_updated: function (event) {
|
||||||
assert.deepEqual(event.data.ids, envIDs,
|
assert.deepEqual(event.data.env.ids, envIDs,
|
||||||
"should notify the environment with the correct ids");
|
"should notify the environment with the correct ids");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2031,7 +2031,7 @@ QUnit.module('Views', {
|
|||||||
'<field name="bar"/>' +
|
'<field name="bar"/>' +
|
||||||
'</tree>',
|
'</tree>',
|
||||||
intercepts: {
|
intercepts: {
|
||||||
warning: function (ev) {
|
warning: function () {
|
||||||
warnings++;
|
warnings++;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2070,14 +2070,13 @@ QUnit.module('Views', {
|
|||||||
arch: '<tree><field name="name"/></tree>',
|
arch: '<tree><field name="name"/></tree>',
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.intercept(list, "switch_view", function (event) {
|
testUtils.intercept(list, 'switch_view', function (event) {
|
||||||
assert.deepEqual(event.data, {
|
assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), {
|
||||||
'model': 'event',
|
mode: 'readonly',
|
||||||
'res_id': '2-20170808020000',
|
model: 'event',
|
||||||
'view_type': 'form',
|
res_id: '2-20170808020000',
|
||||||
'mode': 'readonly'
|
view_type: 'form',
|
||||||
},
|
}, "should trigger a switch_view event to the form view for the record virtual id");
|
||||||
"should trigger switch to the form view for the record virtual id");
|
|
||||||
});
|
});
|
||||||
list.$('td:contains(virtual)').click();
|
list.$('td:contains(virtual)').click();
|
||||||
|
|
||||||
@@ -2092,7 +2091,7 @@ QUnit.module('Views', {
|
|||||||
model: 'foo',
|
model: 'foo',
|
||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route) {
|
||||||
assert.step(route);
|
assert.step(route);
|
||||||
return this._super.apply(this, arguments);
|
return this._super.apply(this, arguments);
|
||||||
},
|
},
|
||||||
@@ -2220,7 +2219,7 @@ QUnit.module('Views', {
|
|||||||
print: [],
|
print: [],
|
||||||
},
|
},
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ QUnit.module('Views', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.intercept(parent, 'env_updated', function () {
|
testUtils.intercept(parent, 'env_updated', function () {
|
||||||
throw new Error("The environment should not be propagated to the view manager");
|
throw new Error("The environment should not be propagated to the action manager");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ QUnit.module('widgets', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<tree><field name="foo"/></tree>',
|
arch: '<tree><field name="foo"/></tree>',
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
mockRPC: function (route) {
|
mockRPC: function (route) {
|
||||||
if (route === '/web/export/formats') {
|
if (route === '/web/export/formats') {
|
||||||
@@ -144,7 +144,7 @@ QUnit.module('widgets', {
|
|||||||
data: this.data,
|
data: this.data,
|
||||||
arch: '<tree><field name="foo"/></tree>',
|
arch: '<tree><field name="foo"/></tree>',
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
sidebar: true,
|
hasSidebar: true,
|
||||||
},
|
},
|
||||||
mockRPC: function (route) {
|
mockRPC: function (route) {
|
||||||
if (route === '/web/export/formats') {
|
if (route === '/web/export/formats') {
|
||||||
|
|||||||
@@ -149,8 +149,8 @@
|
|||||||
<script type="text/javascript" src="/web/static/src/js/core/domain.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/core/domain.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/core/pyeval.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/core/pyeval.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/chrome/action_manager.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/chrome/action_manager.js"></script>
|
||||||
|
<script type="text/javascript" src="/web/static/src/js/chrome/action_manager_act_window.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/chrome/control_panel.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/chrome/control_panel.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/chrome/view_manager.js"></script>
|
|
||||||
<script type="text/javascript" src="/web/static/src/js/chrome/abstract_web_client.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/chrome/abstract_web_client.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/chrome/web_client.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/chrome/web_client.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/src/js/_deprecated/data.js"></script>
|
<script type="text/javascript" src="/web/static/src/js/_deprecated/data.js"></script>
|
||||||
@@ -489,6 +489,7 @@
|
|||||||
<script type="text/javascript" src="/web/static/tests/core/util_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/core/util_tests.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/tests/core/widget_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/core/widget_tests.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_tests.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/tests/chrome/search_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/chrome/search_tests.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/tests/chrome/pager_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/chrome/pager_tests.js"></script>
|
||||||
|
|
||||||
@@ -534,6 +535,7 @@
|
|||||||
<t t-call="web.js_tests_assets"/>
|
<t t-call="web.js_tests_assets"/>
|
||||||
<script type="text/javascript" src="/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js"></script>
|
<script type="text/javascript" src="/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_mobile_tests.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/tests/views/kanban_mobile_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/views/kanban_mobile_tests.js"></script>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
|
|||||||
$searchview: this.$searchview,
|
$searchview: this.$searchview,
|
||||||
$buttons: QWeb.render("website.GoToButtons"),
|
$buttons: QWeb.render("website.GoToButtons"),
|
||||||
},
|
},
|
||||||
breadcrumbs: this.getParent().get_breadcrumbs(),
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user