[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;
|
||||
|
||||
this.set("title", this.title);
|
||||
var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }];
|
||||
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
|
||||
this.update_control_panel({search_view_hidden: true}, {clear: true});
|
||||
|
||||
this.renderer.prependTo(self.$('.o_form_sheet'));
|
||||
this._renderLines();
|
||||
@@ -122,18 +121,11 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
do_show: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.action_manager) {
|
||||
var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }];
|
||||
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.update_control_panel({search_view_hidden: true}, {clear: true});
|
||||
this.action_manager.do_push_state({
|
||||
action: this.params.tag,
|
||||
active_id: this.params.res_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -126,7 +126,6 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
self.renderButtons();
|
||||
self.renderImportLink();
|
||||
var status = {
|
||||
breadcrumbs: self.action_manager.get_breadcrumbs(),
|
||||
cp_content: {$buttons: self.$buttons},
|
||||
};
|
||||
self.update_control_panel(status);
|
||||
@@ -505,10 +504,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
this.exit();
|
||||
},
|
||||
exit: function () {
|
||||
this.do_action({
|
||||
type: 'ir.actions.client',
|
||||
tag: 'history_back'
|
||||
});
|
||||
this.trigger_up('history_back');
|
||||
},
|
||||
onresults: function (event, from, to, 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
|
||||
* there is no view manager doing the job. Also, we don't actually want to
|
||||
* switch view in dashboard, we want to do a do_action (which will open the
|
||||
* record in a different breadcrumb)
|
||||
* We need to intercept switch_view event coming from sub views, because we
|
||||
* don't actually want to switch view in dashboard, we want to do a
|
||||
* do_action (which will open the record in a different breadcrumb).
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
@@ -126,6 +125,9 @@ FormController.include({
|
||||
});
|
||||
|
||||
FormRenderer.include({
|
||||
custom_events: _.extend({}, FormRenderer.prototype.custom_events, {
|
||||
env_updated: '_onEnvUpdated',
|
||||
}),
|
||||
events: _.extend({}, FormRenderer.prototype.events, {
|
||||
'click .oe_dashboard_column .oe_fold': '_onFoldClick',
|
||||
'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
|
||||
* @param {MouseEvent} event
|
||||
|
||||
@@ -7,7 +7,6 @@ var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var FavoriteMenu = require('web.FavoriteMenu');
|
||||
var pyeval = require('web.pyeval');
|
||||
var ViewManager = require('web.ViewManager');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
@@ -23,11 +22,7 @@ FavoriteMenu.include({
|
||||
if(this.action_id === undefined) {
|
||||
return this._super();
|
||||
}
|
||||
var am = this.findAncestor(function (a) {
|
||||
return a instanceof ActionManager;
|
||||
});
|
||||
if (am && am.get_inner_widget() instanceof ViewManager) {
|
||||
this.view_manager = am.get_inner_widget();
|
||||
if (this.action.type === 'ir.actions.act_window') {
|
||||
this.add_to_dashboard_available = true;
|
||||
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtodashboard'));
|
||||
this.$add_to_dashboard = this.$('.o_add_to_dashboard');
|
||||
@@ -71,7 +66,13 @@ FavoriteMenu.include({
|
||||
context.add({
|
||||
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);
|
||||
for (var k in c) {
|
||||
if (c.hasOwnProperty(k) && /^search_default_/.test(k)) {
|
||||
@@ -88,7 +89,7 @@ FavoriteMenu.include({
|
||||
action_id: self.action_id || false,
|
||||
context_to_save: c,
|
||||
domain: domain,
|
||||
view_mode: self.view_manager.active_view.type,
|
||||
view_mode: controller.viewType,
|
||||
name: name,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<template id="assets_backend" name="board assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<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/favorite_menu.js"></script>
|
||||
</xpath>
|
||||
|
||||
@@ -56,7 +56,7 @@ odoo.define('document.tests', function (require) {
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
return $.when(this.data['ir.attachment'].records);
|
||||
|
||||
@@ -7,27 +7,28 @@ var data = require('web.data');
|
||||
var Domain = require('web.Domain');
|
||||
var FavoriteMenu = require('web.FavoriteMenu');
|
||||
var pyeval = require('web.pyeval');
|
||||
var ViewManager = require('web.ViewManager');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
FavoriteMenu.include({
|
||||
start: function () {
|
||||
this._super();
|
||||
var am = this.findAncestor(function (a) {
|
||||
return a instanceof ActionManager;
|
||||
});
|
||||
if (am && am.get_inner_widget() instanceof ViewManager) {
|
||||
this.view_manager = am.get_inner_widget();
|
||||
if (this.action.type === 'ir.actions.act_window') {
|
||||
this.$('.o_favorites_menu').append(QWeb.render('SearchView.addtogooglespreadsheet'));
|
||||
this.$('.add-to-spreadsheet').click(this.add_to_spreadsheet.bind(this));
|
||||
}
|
||||
},
|
||||
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(),
|
||||
model = this.searchview.dataset.model,
|
||||
list_view = this.view_manager.views.list,
|
||||
list_view_id = list_view ? list_view.view_id : false,
|
||||
list_view = _.findWhere(controller.widget.actionViews, {type: 'list'}),
|
||||
list_view_id = list_view ? list_view.viewID : false,
|
||||
domain = [],
|
||||
groupbys = pyeval.eval('groupbys', sv_data.groupbys).join(" "),
|
||||
ds = new data.DataSet(this, 'google.drive.config');
|
||||
|
||||
@@ -46,7 +46,7 @@ var ChatMixin = {
|
||||
options: options,
|
||||
callback: function(messages) { result = messages;}
|
||||
});
|
||||
return result ? result : $.when();
|
||||
return result ? result : $.when([]);
|
||||
},
|
||||
_joinChannel: function(channel_id) {
|
||||
var result;
|
||||
|
||||
@@ -663,7 +663,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
*/
|
||||
_updateControlPanel: function () {
|
||||
this.update_control_panel({
|
||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
||||
cp_content: {
|
||||
$buttons: this.$buttons,
|
||||
$searchview: this.searchview.$el,
|
||||
@@ -905,6 +904,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
* @param {OdooEvent}
|
||||
*/
|
||||
_onSearch: function (event) {
|
||||
event.stopPropagation();
|
||||
var session = this.getSession();
|
||||
var result = pyeval.eval_domains_and_contexts({
|
||||
domains: event.data.domains,
|
||||
|
||||
@@ -86,7 +86,6 @@ var PlanAction = Widget.extend(ControlPanelMixin, {
|
||||
},
|
||||
update_cp: function () {
|
||||
this.update_control_panel({
|
||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
||||
cp_content: {
|
||||
$buttons: this.$buttons,
|
||||
$searchview: this.searchview.$el,
|
||||
|
||||
@@ -68,7 +68,6 @@ var stock_report_generic = Widget.extend(ControlPanelMixin, {
|
||||
this.renderButtons();
|
||||
}
|
||||
var status = {
|
||||
breadcrumbs: this.actionManager.get_breadcrumbs(),
|
||||
cp_content: {$buttons: this.$buttons},
|
||||
};
|
||||
return this.update_control_panel(status);
|
||||
|
||||
@@ -36,8 +36,8 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
toggle_fullscreen: function (event) {
|
||||
this.toggle_fullscreen(event.data.fullscreen);
|
||||
},
|
||||
current_action_updated: function (e) {
|
||||
this.current_action_updated(e.data.action);
|
||||
current_action_updated: function (ev) {
|
||||
this.current_action_updated(ev.data.action, ev.data.controller);
|
||||
},
|
||||
// GENERIC SERVICES
|
||||
// the next events are dedicated to generic services required by
|
||||
@@ -51,6 +51,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
}
|
||||
},
|
||||
warning: '_onDisplayWarning',
|
||||
load_action: '_onLoadAction',
|
||||
load_views: function (event) {
|
||||
var params = {
|
||||
model: event.data.modelName,
|
||||
@@ -66,6 +67,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
.load_filters(event.data.dataset, event.data.action_id)
|
||||
.then(event.data.on_success);
|
||||
},
|
||||
push_state: '_onPushState',
|
||||
show_effect: '_onShowEffect',
|
||||
// session
|
||||
get_session: function (event) {
|
||||
@@ -199,7 +201,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
};
|
||||
},
|
||||
set_action_manager: function () {
|
||||
this.action_manager = new ActionManager(this, {webclient: this});
|
||||
this.action_manager = new ActionManager(this, session.user_context);
|
||||
return this.action_manager.appendTo(this.$('.o_main_content'));
|
||||
},
|
||||
set_notification_manager: function () {
|
||||
@@ -213,11 +215,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
show_application: function () {
|
||||
},
|
||||
clear_uncommitted_changes: function () {
|
||||
var def = $.Deferred().resolve();
|
||||
core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) {
|
||||
def = def.then(callback);
|
||||
});
|
||||
return def;
|
||||
return this.action_manager.clearUncommittedChanges();
|
||||
},
|
||||
destroy_content: function () {
|
||||
_.each(_.clone(this.getChildren()), function (el) {
|
||||
@@ -264,7 +262,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
* This allows to widgets that are not inside the ActionManager to perform do_action
|
||||
*/
|
||||
do_action: function () {
|
||||
return this.action_manager.do_action.apply(this, arguments);
|
||||
return this.action_manager.doAction.apply(this.action_manager, arguments);
|
||||
},
|
||||
do_reload: function () {
|
||||
var self = this;
|
||||
@@ -274,6 +272,9 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
});
|
||||
},
|
||||
do_push_state: function (state) {
|
||||
if (!state.menu_id && this.menu) { // this.menu doesn't exist in the POS
|
||||
state.menu_id = this.menu.getCurrentPrimaryMenu();
|
||||
}
|
||||
if ('title' in state) {
|
||||
this.set_title(state.title);
|
||||
delete state.title;
|
||||
@@ -304,13 +305,14 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
this.connection_notification = false;
|
||||
}
|
||||
},
|
||||
// Handler to be overwritten
|
||||
current_action_updated: function () {
|
||||
},
|
||||
// --------------------------------------------------------------
|
||||
// Scrolltop handling
|
||||
// --------------------------------------------------------------
|
||||
getScrollTop: function () {
|
||||
/**
|
||||
* Handler to be overridden, called each time the UI is updated by the
|
||||
* ActionManager.
|
||||
*
|
||||
* @param {Object} action the action of the currently displayed controller
|
||||
* @param {Object} controller the currently displayed controller
|
||||
*/
|
||||
current_action_updated: function (action, controller) {
|
||||
},
|
||||
//--------------------------------------------------------------
|
||||
// Misc.
|
||||
@@ -346,6 +348,27 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
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
|
||||
*
|
||||
|
||||
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 () {
|
||||
* this.update_control_panel({
|
||||
* breadcrumbs: this.action_manager.get_breadcrumbs(),
|
||||
* cp_content: {
|
||||
* $buttons: this.$buttons,
|
||||
* },
|
||||
@@ -51,7 +50,9 @@ var ControlPanelMixin = {
|
||||
* @param {Object} [options] see web.ControlPanel.update() for a description
|
||||
*/
|
||||
update_control_panel: function(cp_status, options) {
|
||||
this.cp_bus.trigger("update", cp_status || {}, options || {});
|
||||
if (this.cp_bus) {
|
||||
this.cp_bus.trigger("update", cp_status || {}, options || {});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,7 +89,6 @@ var ControlPanel = Widget.extend({
|
||||
start: function() {
|
||||
// Exposed jQuery nodesets
|
||||
this.nodes = {
|
||||
$breadcrumbs: this.$('.breadcrumb'),
|
||||
$buttons: this.$('.o_cp_buttons'),
|
||||
$pager: this.$('.o_cp_pager'),
|
||||
$searchview: this.$('.o_cp_searchview'),
|
||||
@@ -107,10 +107,6 @@ var ControlPanel = Widget.extend({
|
||||
|
||||
return this._super();
|
||||
},
|
||||
destroy: function() {
|
||||
this._clear_breadcrumbs_handlers();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @return {Object} the Bus the ControlPanel is listening on
|
||||
*/
|
||||
@@ -143,9 +139,7 @@ var ControlPanel = Widget.extend({
|
||||
|
||||
// Render the breadcrumbs
|
||||
if (status.breadcrumbs) {
|
||||
this._clear_breadcrumbs_handlers();
|
||||
this.$breadcrumbs = this._render_breadcrumbs(status.breadcrumbs);
|
||||
new_cp_content.$breadcrumbs = this.$breadcrumbs;
|
||||
this.$('.breadcrumb').html(this._render_breadcrumbs(status.breadcrumbs));
|
||||
}
|
||||
|
||||
// Detach control_panel old content and attach new elements
|
||||
@@ -239,22 +233,11 @@ var ControlPanel = Widget.extend({
|
||||
.toggleClass('active', is_last);
|
||||
if (!is_last) {
|
||||
$bc.click(function () {
|
||||
self.trigger("on_breadcrumb_click", bc.action, bc.index);
|
||||
self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
|
||||
});
|
||||
}
|
||||
return $bc;
|
||||
},
|
||||
/**
|
||||
* Private function that removes event handlers attached on the currently
|
||||
* displayed breadcrumbs.
|
||||
*/
|
||||
_clear_breadcrumbs_handlers: function () {
|
||||
if (this.$breadcrumbs) {
|
||||
_.each(this.$breadcrumbs, function ($bc) {
|
||||
$bc.off();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Private function that updates the SearchView's visibility and extend the
|
||||
* breadcrumbs area if the SearchView is not visible
|
||||
|
||||
@@ -118,6 +118,8 @@ var Menu = Widget.extend({
|
||||
$main_menu = $clicked_menu;
|
||||
}
|
||||
|
||||
this.current_primary_menu = $main_menu.data('menu');
|
||||
|
||||
// Activate current main menu
|
||||
this.$el.find('.active').removeClass('active');
|
||||
$main_menu.parent().addClass('active');
|
||||
@@ -206,6 +208,19 @@ var Menu = Widget.extend({
|
||||
ev.preventDefault();
|
||||
this.menu_click($(ev.currentTarget).data('menu'));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the id of the current primary (first level) menu.
|
||||
*
|
||||
* @returns {integer}
|
||||
*/
|
||||
getCurrentPrimaryMenu: function () {
|
||||
return this.current_primary_menu;
|
||||
},
|
||||
});
|
||||
|
||||
return Menu;
|
||||
|
||||
@@ -30,12 +30,13 @@ return Widget.extend({
|
||||
}
|
||||
},
|
||||
},
|
||||
init: function (parent, query, target_model, action_id, filters) {
|
||||
init: function (parent, query, target_model, action, filters) {
|
||||
this._super.apply(this,arguments);
|
||||
this.searchview = parent;
|
||||
this.query = query;
|
||||
this.target_model = target_model;
|
||||
this.action_id = action_id;
|
||||
this.action = action;
|
||||
this.action_id = action.id;
|
||||
this.filters = {};
|
||||
_.each(filters, this.add_filter.bind(this));
|
||||
},
|
||||
@@ -96,11 +97,7 @@ return Widget.extend({
|
||||
}
|
||||
var user_context = this.getSession().user_context;
|
||||
var search = this.searchview.build_search_data();
|
||||
var view_manager = this.findAncestor(function (a) {
|
||||
// HORRIBLE HACK. PLEASE SAVE ME FROM MYSELF (BUT IN A PAINLESS WAY IF POSSIBLE)
|
||||
return 'active_view' in a;
|
||||
});
|
||||
var view_context = view_manager ? view_manager.active_view.controller.getContext() : {};
|
||||
var view_context = this.action.env.context;
|
||||
var results = pyeval.eval_domains_and_contexts({
|
||||
domains: search.domains,
|
||||
contexts: [user_context].concat(search.contexts.concat(view_context || [])),
|
||||
|
||||
@@ -278,7 +278,7 @@ var SearchView = Widget.extend({
|
||||
this.fields = this.fields_view.fields;
|
||||
this.query = undefined;
|
||||
this.title = this.options.action && this.options.action.name;
|
||||
this.action_id = this.options.action && this.options.action.id;
|
||||
this.action = this.options.action || {};
|
||||
this.search_fields = [];
|
||||
this.filters = [];
|
||||
this.groupbys = [];
|
||||
@@ -297,7 +297,7 @@ var SearchView = Widget.extend({
|
||||
var self = this;
|
||||
var def;
|
||||
if (!this.options.disable_favorites) {
|
||||
def = this.loadFilters(this.dataset, this.action_id).then(function (filters) {
|
||||
def = this.loadFilters(this.dataset, this.action.id).then(function (filters) {
|
||||
self.favorite_filters = filters;
|
||||
});
|
||||
}
|
||||
@@ -328,7 +328,7 @@ var SearchView = Widget.extend({
|
||||
menu_defs.push(this.groupby_menu.appendTo(this.$buttons));
|
||||
}
|
||||
if (!this.options.disable_favorites) {
|
||||
this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id, this.favorite_filters);
|
||||
this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action, this.favorite_filters);
|
||||
menu_defs.push(this.favorite_menu.appendTo(this.$buttons));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
headless: true,
|
||||
};
|
||||
self.action_manager.do_action(result).then(function () {
|
||||
self.action_manager.doAction(result).then(function () {
|
||||
var form = self.action_manager.dialog_widget.views.form.controller;
|
||||
form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
|
||||
form.on('record_saved', self, function() {
|
||||
@@ -110,7 +110,7 @@ return AbstractWebClient.extend({
|
||||
.done(function(result) {
|
||||
var data = result[0];
|
||||
if(data.action_id) {
|
||||
self.action_manager.do_action(data.action_id[0]);
|
||||
self.action_manager.doAction(data.action_id[0]);
|
||||
self.menu.open_action(data.action_id[0]);
|
||||
} else {
|
||||
var first_menu_id = self.menu.$el.find("a:first").data("menu");
|
||||
@@ -140,11 +140,10 @@ return AbstractWebClient.extend({
|
||||
self.menu.menu_click(state.menu_id);
|
||||
});
|
||||
} else {
|
||||
state._push_me = false; // no need to push state back...
|
||||
self.action_manager.do_load_state(state, !!self._current_state).then(function () {
|
||||
var action = self.action_manager.get_inner_action();
|
||||
self.action_manager.loadState(state, !!self._current_state).then(function () {
|
||||
var action = self.action_manager.getCurrentAction();
|
||||
if (action) {
|
||||
self.menu.open_action(action.action_descr.id);
|
||||
self.menu.open_action(action.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -163,10 +162,10 @@ return AbstractWebClient.extend({
|
||||
.then(function (result) {
|
||||
return self.action_mutex.exec(function() {
|
||||
var completed = $.Deferred();
|
||||
$.when(self.action_manager.do_action(result, {
|
||||
self.action_manager.doAction(result, {
|
||||
clear_breadcrumbs: true,
|
||||
action_menu_id: self.menu.current_menu,
|
||||
})).fail(function() {
|
||||
}).fail(function() {
|
||||
self.menu.open_menu(options.previous_menu_id);
|
||||
}).always(function() {
|
||||
completed.resolve();
|
||||
|
||||
@@ -165,12 +165,12 @@ var Dialog = Widget.extend({
|
||||
this.destroy();
|
||||
},
|
||||
|
||||
destroy: function (reason) {
|
||||
destroy: function (arg) {
|
||||
// Need to trigger before real destroy but if 'closed' handler destroys
|
||||
// the widget again, we want to avoid infinite recursion
|
||||
if (!this.__closed) {
|
||||
this.__closed = true;
|
||||
this.trigger("closed", reason);
|
||||
this.trigger("closed", arg);
|
||||
}
|
||||
|
||||
if (this.isDestroyed()) {
|
||||
|
||||
@@ -140,17 +140,6 @@ function Home (parent, action) {
|
||||
}
|
||||
core.action_registry.add("home", Home);
|
||||
|
||||
/**
|
||||
* Client action to go back in breadcrumb history.
|
||||
* If can't go back in history stack, will go back to home.
|
||||
*/
|
||||
function HistoryBack (parent) {
|
||||
parent.history_back().fail(function () {
|
||||
Home(parent);
|
||||
});
|
||||
}
|
||||
core.action_registry.add("history_back", HistoryBack);
|
||||
|
||||
function login() {
|
||||
redirect('/web/login');
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ var ReportAction = Widget.extend(ControlPanelMixin, {
|
||||
|
||||
_update_control_panel: function () {
|
||||
this.update_control_panel({
|
||||
breadcrumbs: this.action_manager.get_breadcrumbs(),
|
||||
cp_content: {
|
||||
$buttons: this.$buttons,
|
||||
},
|
||||
|
||||
@@ -12,12 +12,16 @@ odoo.define('web.AbstractController', function (require) {
|
||||
* reading localstorage, ...) has to go through the controller.
|
||||
*/
|
||||
|
||||
var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var AbstractController = Widget.extend({
|
||||
var AbstractController = Widget.extend(ControlPanelMixin, {
|
||||
custom_events: {
|
||||
open_record: '_onOpenRecord',
|
||||
switch_view: '_onSwitchView',
|
||||
},
|
||||
events: {
|
||||
'click a[type="action"]': '_onActionClicked',
|
||||
@@ -30,8 +34,15 @@ var AbstractController = Widget.extend({
|
||||
* @param {AbstractRenderer} renderer
|
||||
* @param {object} params
|
||||
* @param {string} params.modelName
|
||||
* @param {string} [params.controllerID] an id to ease the communication
|
||||
* with upstream components
|
||||
* @param {any} [params.handle] a handle that will be given to the model (some id)
|
||||
* @param {any} params.initialState the initialState
|
||||
* @param {boolean} params.isMultiRecord
|
||||
* @param {Object[]} params.actionViews
|
||||
* @param {string} params.viewType
|
||||
* @param {boolean} params.withControlPanel set to false to hide the
|
||||
* ControlPanel
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -40,7 +51,23 @@ var AbstractController = Widget.extend({
|
||||
this.modelName = params.modelName;
|
||||
this.handle = params.handle;
|
||||
this.activeActions = params.activeActions;
|
||||
this.controllerID = params.controllerID;
|
||||
this.initialState = params.initialState;
|
||||
|
||||
// those arguments are temporary, they won't be necessary as soon as the
|
||||
// ControlPanel will be handled by the View
|
||||
this.displayName = params.displayName;
|
||||
this.isMultiRecord = params.isMultiRecord;
|
||||
this.searchable = params.searchable;
|
||||
this.searchView = params.searchView;
|
||||
this.searchViewHidden = params.searchViewHidden;
|
||||
this.actionViews = params.actionViews;
|
||||
this.viewType = params.viewType;
|
||||
this.withControlPanel = params.withControlPanel !== false;
|
||||
// override this.need_control_panel so that the ActionManager doesn't
|
||||
// update the control panel when it isn't visible (this is a temporary
|
||||
// hack that can be removed as soon as the CP'll be handled by the view)
|
||||
this.need_control_panel = this.withControlPanel;
|
||||
},
|
||||
/**
|
||||
* Simply renders and updates the url.
|
||||
@@ -48,10 +75,19 @@ var AbstractController = Widget.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this.$el.addClass('o_view_controller');
|
||||
|
||||
// render the ControlPanel elements (buttons, pager, sidebar...)
|
||||
this.controlPanelElements = this._renderControlPanelElements();
|
||||
|
||||
return $.when(
|
||||
this._super.apply(this, arguments),
|
||||
this.renderer.appendTo(this.$el)
|
||||
).then(this._update.bind(this, this.initialState));
|
||||
).then(function () {
|
||||
self._update(self.initialState);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -60,6 +96,9 @@ var AbstractController = Widget.extend({
|
||||
if (this.$buttons) {
|
||||
this.$buttons.off();
|
||||
}
|
||||
if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
|
||||
this.controlPanelElements.$switch_buttons.off();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -70,7 +109,7 @@ var AbstractController = Widget.extend({
|
||||
/**
|
||||
* Discards the changes made on the record associated to the given ID, or
|
||||
* all changes made by the current controller if no recordID is given. For
|
||||
* example, when the user open the 'home' screen, the view manager will call
|
||||
* example, when the user opens the 'home' screen, the action manager calls
|
||||
* this method on the active view to make sure it is ok to open the home
|
||||
* screen (and lose all current state).
|
||||
*
|
||||
@@ -95,24 +134,16 @@ var AbstractController = Widget.extend({
|
||||
getContext: function () {
|
||||
return {};
|
||||
},
|
||||
/**
|
||||
* @see setScrollTop
|
||||
* @returns {number}
|
||||
*/
|
||||
getScrollTop: function () {
|
||||
return this.scrollTop;
|
||||
},
|
||||
/**
|
||||
* Returns a title that may be displayed in the breadcrumb area. For
|
||||
* example, the name of the record.
|
||||
*
|
||||
* Note: this seems wrong right now, it should not be implemented, we have
|
||||
* no guarantee that there is a display_name variable in a controller.
|
||||
* note: this will be moved to AbstractAction
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getTitle: function () {
|
||||
return this.display_name;
|
||||
return this.displayName;
|
||||
},
|
||||
/**
|
||||
* The use of this method is discouraged. It is still snakecased, because
|
||||
@@ -136,12 +167,9 @@ var AbstractController = Widget.extend({
|
||||
return this.update(params || {});
|
||||
},
|
||||
/**
|
||||
* Most likely called by the view manager, this method is responsible for
|
||||
* adding buttons in the control panel (buttons such as save/discard/...)
|
||||
*
|
||||
* Note that there is no guarantee that this method will be called. The
|
||||
* controller is supposed to work even without a view manager, for example
|
||||
* in the frontend (odoo frontend = public website)
|
||||
* Renders the buttons to append, in most cases, to the control panel (in
|
||||
* the bottom left corner). When the view is rendered in a dialog, those
|
||||
* buttons might be moved to the dialog's footer.
|
||||
*
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
@@ -153,7 +181,7 @@ var AbstractController = Widget.extend({
|
||||
* controller can actually render whatever he wants in the pager zone. If
|
||||
* your view does not want a pager, just let this method empty.
|
||||
*
|
||||
* @param {Query Node} $node
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
renderPager: function ($node) {
|
||||
},
|
||||
@@ -161,20 +189,10 @@ var AbstractController = Widget.extend({
|
||||
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu
|
||||
* dropdown in the control panel next to the buttons)
|
||||
*
|
||||
* @param {Query Node} $node
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
renderSidebar: function ($node) {
|
||||
},
|
||||
/**
|
||||
* Not sure about this one, it probably needs to be reworked, maybe merged
|
||||
* in get/set local state methods.
|
||||
*
|
||||
* @see getScrollTop
|
||||
* @param {number} scrollTop
|
||||
*/
|
||||
setScrollTop: function (scrollTop) {
|
||||
this.scrollTop = scrollTop;
|
||||
},
|
||||
/**
|
||||
* This is the main entry point for the controller. Changes from the search
|
||||
* view arrive in this method, and internal changes can sometimes also call
|
||||
@@ -219,7 +237,68 @@ var AbstractController = Widget.extend({
|
||||
* world
|
||||
*/
|
||||
_pushState: function (state) {
|
||||
this.trigger_up('push_state', state || {});
|
||||
this.trigger_up('push_state', {
|
||||
controllerID: this.controllerID,
|
||||
state: state || {},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Renders the control elements (buttons, pager and sidebar) of the current
|
||||
* view.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} an object containing the control panel jQuery elements
|
||||
*/
|
||||
_renderControlPanelElements: function () {
|
||||
var elements = {};
|
||||
|
||||
if (this.withControlPanel) {
|
||||
elements = {
|
||||
$buttons: $('<div>'),
|
||||
$sidebar: $('<div>'),
|
||||
$pager: $('<div>'),
|
||||
};
|
||||
|
||||
this.renderButtons(elements.$buttons);
|
||||
this.renderSidebar(elements.$sidebar);
|
||||
this.renderPager(elements.$pager);
|
||||
// remove the unnecessary outer div
|
||||
elements = _.mapObject(elements, function($node) {
|
||||
return $node && $node.contents();
|
||||
});
|
||||
elements.$switch_buttons = this._renderSwitchButtons();
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
/**
|
||||
* Renders the switch buttons and binds listeners on them.
|
||||
*
|
||||
* @private
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
_renderSwitchButtons: function () {
|
||||
var self = this;
|
||||
var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});
|
||||
|
||||
if (views.length <= 1) {
|
||||
return $();
|
||||
}
|
||||
|
||||
var $switchButtons = $(QWeb.render('ControlPanel.SwitchButtons', {
|
||||
views: views,
|
||||
}));
|
||||
// create bootstrap tooltips
|
||||
_.each(views, function (view) {
|
||||
$switchButtons.filter('.o_cp_switch_' + view.type).tooltip();
|
||||
});
|
||||
// add onclick event listener
|
||||
$switchButtons.filter('button').click(_.debounce(function (event) {
|
||||
var viewType = $(event.target).data('view-type');
|
||||
self.trigger_up('switch_view', {view_type: viewType});
|
||||
}, 200, true));
|
||||
|
||||
return $switchButtons;
|
||||
},
|
||||
/**
|
||||
* This method is called after each update or when the start method is
|
||||
@@ -235,6 +314,22 @@ var AbstractController = Widget.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_update: function (state) {
|
||||
// AAB: update the control panel -> this will be moved elsewhere at some point
|
||||
var cpContent = _.extend({}, this.controlPanelElements);
|
||||
if (this.searchView) {
|
||||
_.extend(cpContent, {
|
||||
$searchview: this.searchView.$el,
|
||||
$searchview_buttons: this.searchView.$buttons.contents(),
|
||||
});
|
||||
}
|
||||
this.update_control_panel({
|
||||
active_view_selector: '.o_cp_switch_' + this.viewType,
|
||||
cp_content: cpContent,
|
||||
hidden: !this.withControlPanel,
|
||||
searchview: this.searchView,
|
||||
search_view_hidden: !this.searchable || this.searchviewHidden,
|
||||
});
|
||||
|
||||
this._pushState();
|
||||
return $.when();
|
||||
},
|
||||
@@ -249,7 +344,7 @@ var AbstractController = Widget.extend({
|
||||
*
|
||||
* Note: this method seems wrong, it relies on the model being a basic model,
|
||||
* to get the res_id. It should receive the res_id in the event data
|
||||
* @todo move this to basic controller? or view manager
|
||||
* @todo move this to basic controller?
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
@@ -278,6 +373,15 @@ var AbstractController = Widget.extend({
|
||||
event.preventDefault();
|
||||
this.do_action(event.target.name);
|
||||
},
|
||||
/**
|
||||
* Intercepts the 'switch_view' event to add the controllerID into the data,
|
||||
* and lets the event bubble up.
|
||||
*
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onSwitchView: function (event) {
|
||||
event.data.controllerID = this.controllerID;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -67,11 +67,18 @@ var AbstractView = Class.extend({
|
||||
* @param {string} params.modelName The actual model name
|
||||
* @param {Object} params.context
|
||||
* @param {number} [params.count]
|
||||
* @param {string} [params.controllerID]
|
||||
* @param {string[]} params.domain
|
||||
* @param {string[]} params.groupBy
|
||||
* @param {number} [params.currentId]
|
||||
* @param {number[]} [params.ids]
|
||||
* @param {boolean} [params.withControlPanel]
|
||||
* @param {boolean} [params.action.flags.headless]
|
||||
* @param {string} [params.action.display_name]
|
||||
* @param {string} [params.action.name]
|
||||
* @param {string} [params.action.help]
|
||||
* @param {string} [params.action.jsID]
|
||||
* @param {boolean} [params.action.views]
|
||||
*/
|
||||
init: function (viewInfo, params) {
|
||||
this.rendererParams = {
|
||||
@@ -87,7 +94,23 @@ var AbstractView = Class.extend({
|
||||
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
|
||||
duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true,
|
||||
},
|
||||
controllerID: params.controllerID,
|
||||
};
|
||||
// AAB: these params won't be necessary as soon as the ControlPanel will
|
||||
// be instantiated by the View
|
||||
this.controllerParams.displayName = params.action && (params.action.display_name || params.action.name);
|
||||
this.controllerParams.isMultiRecord = this.multi_record;
|
||||
this.controllerParams.searchable = this.searchable;
|
||||
this.controllerParams.searchView = params.action && params.action.searchView;
|
||||
this.controllerParams.searchViewHidden = this.searchview_hidden; // AAB: use searchable instead where it is used?
|
||||
this.controllerParams.actionViews = params.action ? params.action.views : [];
|
||||
this.controllerParams.viewType = this.viewType;
|
||||
this.controllerParams.withControlPanel = true;
|
||||
if (params.action && params.action.flags) {
|
||||
this.controllerParams.withControlPanel = !params.action.flags.headless;
|
||||
} else if ('withControlPanel' in params) {
|
||||
this.controllerParams.withControlPanel = params.withControlPanel;
|
||||
}
|
||||
|
||||
this.loadParams = {
|
||||
context: params.context,
|
||||
@@ -133,7 +156,7 @@ var AbstractView = Class.extend({
|
||||
* load itself, and the renderer needs the data in its constructor.
|
||||
*
|
||||
* @param {Widget} parent The parent of the resulting Controller (most
|
||||
* likely a view manager)
|
||||
* likely an action manager)
|
||||
* @returns {Deferred} The deferred resolves to a controller
|
||||
*/
|
||||
getController: function (parent) {
|
||||
|
||||
@@ -200,7 +200,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
_abandonRecord: function (recordID) {
|
||||
recordID = recordID || this.handle;
|
||||
if (recordID === this.handle) {
|
||||
this.trigger_up('switch_to_previous_view');
|
||||
this.trigger_up('history_back');
|
||||
} else {
|
||||
this.model.removeLine(recordID);
|
||||
}
|
||||
@@ -244,17 +244,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
model: record.model,
|
||||
resIDs: record.res_ids,
|
||||
},
|
||||
on_closed: function (reason) {
|
||||
if (!_.isObject(reason)) {
|
||||
reload(reason);
|
||||
}
|
||||
},
|
||||
on_fail: function (reason) {
|
||||
reload().always(function() {
|
||||
def.reject(reason);
|
||||
});
|
||||
},
|
||||
on_success: def.resolve.bind(def),
|
||||
on_fail: function () {
|
||||
reload().always(def.reject.bind(def));
|
||||
},
|
||||
on_closed: reload,
|
||||
});
|
||||
return this.alive(def);
|
||||
},
|
||||
@@ -313,13 +307,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.readonlyIfRealDiscard=false]
|
||||
* After discarding record changes, the usual option is to make the
|
||||
* record readonly. However, the view manager calls this function
|
||||
* record readonly. However, the action manager calls this function
|
||||
* at inappropriate times in the current code and in that case, we
|
||||
* don't want to go back to readonly if there is nothing to discard
|
||||
* (e.g. when switching record in edit mode in form view, we expect
|
||||
* the new record to be in edit mode too, but the view manager calls
|
||||
* this function as the URL changes...) @todo get rid of this when
|
||||
* the view manager is improved.
|
||||
* the webclient/action_manager's hashchange mechanism is improved.
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_discardChanges: function (recordID, options) {
|
||||
@@ -467,7 +461,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
var sidebarEnv = this._getSidebarEnv();
|
||||
this.sidebar.updateEnv(sidebarEnv);
|
||||
}
|
||||
this.trigger_up('env_updated', env);
|
||||
this.trigger_up('env_updated', {controllerID: this.controllerID, env: env});
|
||||
},
|
||||
/**
|
||||
* Helper method, to make sure the information displayed by the pager is up
|
||||
|
||||
@@ -149,7 +149,7 @@ var FormController = BasicController.extend({
|
||||
* inserted
|
||||
**/
|
||||
renderSidebar: function ($node) {
|
||||
if (!this.sidebar && this.hasSidebar) {
|
||||
if (this.hasSidebar) {
|
||||
var otherItems = [];
|
||||
if (this.is_action_enabled('delete')) {
|
||||
otherItems.push({
|
||||
@@ -280,7 +280,7 @@ var FormController = BasicController.extend({
|
||||
_onDeletedRecords: function () {
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
if (!state.res_ids.length) {
|
||||
this.do_action('history_back');
|
||||
this.trigger_up('history_back');
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ odoo.define('web.FormView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BasicView = require('web.BasicView');
|
||||
var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var FormController = require('web.FormController');
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
@@ -25,15 +24,16 @@ var FormView = BasicView.extend({
|
||||
init: function (viewInfo, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
|
||||
var mode = params.mode || params.context.form_view_initial_mode ||
|
||||
(params.currentId ? 'readonly' : 'edit');
|
||||
this.loadParams.type = 'record';
|
||||
|
||||
this.controllerParams.disableAutofocus = params.disable_autofocus;
|
||||
this.controllerParams.hasSidebar = params.sidebar;
|
||||
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
||||
this.controllerParams.footerToButtons = params.footer_to_buttons;
|
||||
this.controllerParams.footerToButtons = params.footerToButtons;
|
||||
if ('action' in params && 'flags' in params.action) {
|
||||
this.controllerParams.footerToButtons = params.action.flags.footer_to_buttons;
|
||||
this.controllerParams.footerToButtons = params.action.flags.footerToButtons;
|
||||
}
|
||||
var defaultButtons = 'default_buttons' in params ? params.default_buttons : true;
|
||||
this.controllerParams.defaultButtons = defaultButtons;
|
||||
|
||||
@@ -128,7 +128,7 @@ var ListController = BasicController.extend({
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
renderSidebar: function ($node) {
|
||||
if (this.hasSidebar && !this.sidebar) {
|
||||
if (this.hasSidebar) {
|
||||
var other = [{
|
||||
label: _t("Export"),
|
||||
callback: this._onExportData.bind(this)
|
||||
|
||||
@@ -30,7 +30,7 @@ var ListView = BasicView.extend({
|
||||
*
|
||||
* @param {Object} viewInfo
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.sidebar
|
||||
* @param {boolean} params.hasSidebar
|
||||
* @param {boolean} [params.hasSelectors=true]
|
||||
*/
|
||||
init: function (viewInfo, params) {
|
||||
@@ -40,7 +40,7 @@ var ListView = BasicView.extend({
|
||||
var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly";
|
||||
|
||||
this.controllerParams.editable = arch.attrs.editable;
|
||||
this.controllerParams.hasSidebar = params.sidebar;
|
||||
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
||||
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
|
||||
this.controllerParams.mode = mode;
|
||||
|
||||
@@ -175,8 +175,9 @@ var FormViewDialog = ViewDialog.extend({
|
||||
currentId: self.res_id || undefined,
|
||||
index: 0,
|
||||
mode: self.res_id && self.options.readonly ? 'readonly' : 'edit',
|
||||
footer_to_buttons: true,
|
||||
footerToButtons: true,
|
||||
default_buttons: false,
|
||||
withControlPanel: false,
|
||||
model: self.model,
|
||||
parentID: self.parentID,
|
||||
recordID: self.recordID,
|
||||
@@ -190,9 +191,9 @@ var FormViewDialog = ViewDialog.extend({
|
||||
}
|
||||
self.form_view.appendTo(fragment)
|
||||
.then(function () {
|
||||
var $buttons = $('<div>');
|
||||
self.form_view.renderButtons($buttons);
|
||||
self.opened().always(function () {
|
||||
var $buttons = $('<div>');
|
||||
self.form_view.renderButtons($buttons);
|
||||
if ($buttons.children().length) {
|
||||
self.$footer.empty().append($buttons.contents());
|
||||
}
|
||||
@@ -257,7 +258,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length);
|
||||
},
|
||||
search: function (event) {
|
||||
event.stopPropagation(); // prevent this event from bubbling up to the view manager
|
||||
event.stopPropagation(); // prevent this event from bubbling up to the action manager
|
||||
var d = event.data;
|
||||
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
|
||||
this.list_controller.reload(searchData);
|
||||
|
||||
@@ -10,7 +10,6 @@ var field_utils = require('web.field_utils');
|
||||
var session = require('web.session');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var utils = require('web.utils');
|
||||
var ViewManager = require('web.ViewManager');
|
||||
var WebClient = require('web.WebClient');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -332,42 +331,35 @@ DebugManager.include({
|
||||
);
|
||||
},
|
||||
update: function (tag, descriptor, widget) {
|
||||
switch (tag) {
|
||||
case 'action':
|
||||
if (this._view_manager) {
|
||||
this._view_manager.off('switch_mode', this);
|
||||
}
|
||||
if (!(widget instanceof ViewManager)) {
|
||||
this._active_view = null;
|
||||
this._view_manager = null;
|
||||
break;
|
||||
}
|
||||
this._view_manager = widget;
|
||||
widget.on('switch_mode', this, function () {
|
||||
this.update('view', null, widget);
|
||||
});
|
||||
case 'view':
|
||||
this._active_view = widget.active_view;
|
||||
if (tag === 'action' || tag === 'view') {
|
||||
this._controller = widget;
|
||||
}
|
||||
|
||||
return this._super(tag, descriptor).then(function () {
|
||||
this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', {
|
||||
manager: this,
|
||||
action: this._action,
|
||||
view: this._active_view,
|
||||
can_edit: this._can_edit_views,
|
||||
searchview: this._view_manager && this._view_manager.searchview,
|
||||
controller: this._controller,
|
||||
manager: this,
|
||||
searchview: this._controller && this._controller.searchView,
|
||||
view: this._controller && _.findWhere(this._action.views, {
|
||||
type: this._controller.viewType,
|
||||
}),
|
||||
}));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
get_metadata: function() {
|
||||
var ds = this._view_manager.dataset;
|
||||
if (!this._active_view.controller.getSelectedIds().length) {
|
||||
var self = this;
|
||||
var selectedIDs = this._controller.getSelectedIds();
|
||||
if (!selectedIDs.length) {
|
||||
console.warn(_t("No metadata available"));
|
||||
return;
|
||||
}
|
||||
ds.call('get_metadata', [this._active_view.controller.getSelectedIds()]).done(function(result) {
|
||||
this._rpc({
|
||||
model: this._action.res_model,
|
||||
method: 'get_metadata',
|
||||
args: [selectedIDs],
|
||||
}).done(function(result) {
|
||||
var metadata = result[0];
|
||||
metadata.creator = field_utils.format.many2one(metadata.create_uid);
|
||||
metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid);
|
||||
@@ -376,7 +368,7 @@ DebugManager.include({
|
||||
var modificationDate = field_utils.parse.datetime(metadata.write_date);
|
||||
metadata.write_date = field_utils.format.datetime(modificationDate);
|
||||
new Dialog(this, {
|
||||
title: _.str.sprintf(_t("Metadata (%s)"), ds.model),
|
||||
title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model),
|
||||
size: 'medium',
|
||||
$content: QWeb.render('WebClient.DebugViewLog', {
|
||||
perm : metadata,
|
||||
@@ -400,11 +392,12 @@ DebugManager.include({
|
||||
return [value, displayed];
|
||||
};
|
||||
|
||||
var renderer = this._active_view.controller.renderer;
|
||||
var fields = self._active_view.fields_view.fields;
|
||||
var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
|
||||
var fieldNamesInView = renderer.state.getFieldNames();
|
||||
var fieldsValues = renderer.state.data;
|
||||
var renderer = this._controller.renderer;
|
||||
var state = renderer.state;
|
||||
var fields = state.fields;
|
||||
var fieldsInfo = state.fieldsInfo.form;
|
||||
var fieldNamesInView = state.getFieldNames();
|
||||
var fieldsValues = state.data;
|
||||
var modifierDatas = {};
|
||||
_.each(fieldNamesInView, function (fieldName) {
|
||||
modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
|
||||
@@ -416,14 +409,14 @@ DebugManager.include({
|
||||
var modifierData = modifierDatas[fieldName];
|
||||
var invisibleOrReadOnly;
|
||||
if (modifierData) {
|
||||
var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
|
||||
var evaluatedModifiers = modifierData.evaluatedModifiers[state.id];
|
||||
invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
|
||||
}
|
||||
var fieldInfo = fields[fieldName];
|
||||
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
|
||||
var value = valueDisplayed[0];
|
||||
var displayed = valueDisplayed[1];
|
||||
// ignore fields which are empty, invisible, readonly, o2m
|
||||
// ignore fields which are empty, invisible, readonly, o2m
|
||||
// or m2m
|
||||
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
||||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
||||
@@ -479,7 +472,7 @@ DebugManager.include({
|
||||
model: 'ir.default',
|
||||
method: 'set',
|
||||
args: [
|
||||
self._active_view.fields_view.model,
|
||||
self._controller.fields_view.model,
|
||||
fieldToSet,
|
||||
value,
|
||||
allUsers,
|
||||
@@ -502,7 +495,7 @@ DebugManager.include({
|
||||
var dialog = new Dialog(this, { title: _t("Fields View Get") });
|
||||
dialog.opened().then(function () {
|
||||
$('<pre>').text(utils.json_node_to_xml(
|
||||
self._active_view.controller.renderer.arch, true)
|
||||
self._controller.renderer.arch, true)
|
||||
).appendTo(dialog.$el);
|
||||
});
|
||||
dialog.open();
|
||||
@@ -788,30 +781,80 @@ if (config.debug) {
|
||||
SystrayMenu.Items.push(DebugManager);
|
||||
|
||||
WebClient.include({
|
||||
current_action_updated: function(action) {
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
current_action_updated: function (action, controller) {
|
||||
this._super.apply(this, arguments);
|
||||
var action_descr = action && action.action_descr;
|
||||
var action_widget = action && action.widget;
|
||||
var debug_manager = _.find(this.systray_menu.widgets, function(item) {return item instanceof DebugManager; });
|
||||
debug_manager.update('action', action_descr, action_widget);
|
||||
var debugManager = _.find(this.systray_menu.widgets, function(item) {
|
||||
return item instanceof DebugManager;
|
||||
});
|
||||
debugManager.update('action', action, controller && controller.widget);
|
||||
},
|
||||
});
|
||||
|
||||
ActionManager.include({
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the action of the controller currently opened in a dialog,
|
||||
* i.e. a target='new' action, if any.
|
||||
*
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getCurrentActionInDialog: function () {
|
||||
if (this.currentDialogController) {
|
||||
return this.actions[this.currentDialogController.actionID];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* Returns the controller currently opened in a dialog, if any.
|
||||
*
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getCurrentControllerInDialog: function () {
|
||||
return this.currentDialogController;
|
||||
},
|
||||
});
|
||||
|
||||
Dialog.include({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
open: function() {
|
||||
var self = this;
|
||||
|
||||
var parent = self.getParent();
|
||||
if (parent instanceof ActionManager && parent.dialog_widget) {
|
||||
// Instantiate the DebugManager and insert it into the DOM once dialog is opened
|
||||
this.opened(function() {
|
||||
self.debug_manager = new DebugManager(self);
|
||||
var $header = self.$modal.find('.modal-header:first');
|
||||
return self.debug_manager.prependTo($header).then(function() {
|
||||
self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
|
||||
});
|
||||
});
|
||||
}
|
||||
// if the dialog is opened by the ActionManager, instantiate a
|
||||
// DebugManager and insert it into the DOM once the dialog is opened
|
||||
// (delay this with a setTimeout(0) to ensure that the internal
|
||||
// state, i.e. the current action and controller, of the
|
||||
// ActionManager is set to properly update the DebugManager)
|
||||
this.opened(function() {
|
||||
setTimeout(function () {
|
||||
var parent = self.getParent();
|
||||
if (parent instanceof ActionManager) {
|
||||
var action = parent.getCurrentActionInDialog();
|
||||
if (action) {
|
||||
var controller = parent.getCurrentControllerInDialog();
|
||||
self.debugManager = new DebugManager(self);
|
||||
var $header = self.$modal.find('.modal-header:first');
|
||||
return self.debugManager.prependTo($header).then(function () {
|
||||
self.debugManager.update('action', action, controller.widget);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -431,7 +431,7 @@
|
||||
|
||||
// One2Many, Many2Many outside of group
|
||||
.o_field_widget {
|
||||
&.o_view_manager_content, &.o_field_one2many, &.o_field_many2many {
|
||||
&.o_field_one2many, &.o_field_many2many {
|
||||
width: 100%;
|
||||
> div {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.o_graph {
|
||||
height: 100%;
|
||||
|
||||
.o_graph_svg_container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.o_view_manager_content {
|
||||
position: static; // Reset position for view managers in modals (default value: absolute)
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.o_pivot {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
// 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
|
||||
.o_view_manager_content > div {
|
||||
height: 100%;
|
||||
}
|
||||
.oe_view_nocontent {
|
||||
a {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -155,14 +155,9 @@
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.o_view_manager_content {
|
||||
> .o_view_controller {
|
||||
.o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
|
||||
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>
|
||||
</form>
|
||||
|
||||
<div t-name="ActionManager" class="o_content"/>
|
||||
|
||||
<t t-name="ControlPanel">
|
||||
<div class="o_control_panel">
|
||||
<ol class="breadcrumb"/>
|
||||
@@ -201,9 +199,9 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="ViewManager.switch-buttons">
|
||||
<t t-name="ControlPanel.SwitchButtons">
|
||||
<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-att-aria-label="view.type"
|
||||
t-att-data-view-type="view.type" t-att-title="view.label"/>
|
||||
@@ -259,14 +257,14 @@
|
||||
<li class="divider"/>
|
||||
<t t-if="view.type === 'form'">
|
||||
<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>
|
||||
</t>
|
||||
</t>
|
||||
<li><a href="#" data-action="fvg">Fields View Get</a></li>
|
||||
<t t-if="can_edit">
|
||||
<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-elif="view.type == 'list'">Edit List 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>
|
||||
</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="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>
|
||||
|
||||
@@ -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
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
* @param {Object[]} [options.actions=[]]
|
||||
* @param {Object} [options.archs={}] dict of archs with keys being strings like
|
||||
* 'model,id,viewType'
|
||||
* @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.actions = options.actions || [];
|
||||
this.archs = options.archs || {};
|
||||
},
|
||||
|
||||
@@ -431,6 +433,23 @@ var MockServer = Class.extend({
|
||||
}
|
||||
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
|
||||
*
|
||||
@@ -943,7 +962,7 @@ var MockServer = Class.extend({
|
||||
_performRpc: function (route, args) {
|
||||
switch (route) {
|
||||
case '/web/action/load':
|
||||
return $.when(this._mockLoadAction(args));
|
||||
return $.when(this._mockLoadAction(args.kwargs));
|
||||
|
||||
case '/web/dataset/search_read':
|
||||
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.
|
||||
*/
|
||||
|
||||
var ActionManager = require('web.ActionManager');
|
||||
var ajax = require('web.ajax');
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
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.
|
||||
* 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
|
||||
* 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
|
||||
* appropriate.
|
||||
* started.
|
||||
*
|
||||
* Most views can be tested synchronously (@see createView), but some view have
|
||||
* external dependencies (like lazy loaded libraries). In that case, it is
|
||||
@@ -145,9 +198,9 @@ function createAsyncView(params) {
|
||||
|
||||
// reproduce the DOM environment of views
|
||||
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 $view_manager = $('<div>').addClass('o_view_manager_content').appendTo($content);
|
||||
|
||||
return view.getController(widget).then(function (view) {
|
||||
// override the view's 'destroy' so that it calls 'destroy' on the widget
|
||||
@@ -159,29 +212,21 @@ function createAsyncView(params) {
|
||||
widget.destroy();
|
||||
$('#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
|
||||
// without being in the DOM
|
||||
var fragment = document.createDocumentFragment();
|
||||
return view.appendTo(fragment).then(function () {
|
||||
dom.append($view_manager, fragment, {
|
||||
dom.append($content, fragment, {
|
||||
callbacks: [{widget: view}],
|
||||
in_DOM: true,
|
||||
});
|
||||
view.$el.on('click', 'a', function (ev) {
|
||||
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;
|
||||
});
|
||||
@@ -250,6 +295,7 @@ function addMockEnvironment(widget, params) {
|
||||
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
||||
}
|
||||
var mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
currentDate: params.currentDate,
|
||||
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) {
|
||||
mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, {
|
||||
args: [],
|
||||
@@ -617,6 +671,7 @@ return $.when(
|
||||
}, 0);
|
||||
return {
|
||||
addMockEnvironment: addMockEnvironment,
|
||||
createActionManager: createActionManager,
|
||||
createAsyncView: createAsyncView,
|
||||
createModel: createModel,
|
||||
createParent: createParent,
|
||||
|
||||
@@ -1451,7 +1451,7 @@ QUnit.module('Views', {
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
@@ -1597,7 +1597,7 @@ QUnit.module('Views', {
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
return $.when([]);
|
||||
@@ -1628,7 +1628,7 @@ QUnit.module('Views', {
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
return $.when([]);
|
||||
@@ -1659,7 +1659,7 @@ QUnit.module('Views', {
|
||||
'</footer>' +
|
||||
'</form>',
|
||||
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");
|
||||
@@ -2050,7 +2050,7 @@ QUnit.module('Views', {
|
||||
data: this.data,
|
||||
arch: '<form string="Partners"><field name="date"></field></form>',
|
||||
intercepts: {
|
||||
switch_to_previous_view: function (event) {
|
||||
history_back: function () {
|
||||
form.update({}, {reload: false});
|
||||
}
|
||||
},
|
||||
@@ -2086,9 +2086,9 @@ QUnit.module('Views', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
switch_to_previous_view: function (event) {
|
||||
history_back: function () {
|
||||
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
|
||||
// was another view, we would have switched to that view
|
||||
// instead
|
||||
@@ -2126,7 +2126,7 @@ QUnit.module('Views', {
|
||||
data: this.data,
|
||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||
intercepts: {
|
||||
switch_to_previous_view: function () {
|
||||
history_back: function () {
|
||||
assert.ok(true, "should have sent correct event");
|
||||
}
|
||||
}
|
||||
@@ -2155,9 +2155,9 @@ QUnit.module('Views', {
|
||||
data: this.data,
|
||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||
intercepts: {
|
||||
switch_to_previous_view: function (event) {
|
||||
history_back: function () {
|
||||
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
|
||||
// was another view, we would have switched to that view
|
||||
// instead
|
||||
@@ -2197,7 +2197,7 @@ QUnit.module('Views', {
|
||||
data: this.data,
|
||||
arch: '<form string="Partners"><field name="foo"></field></form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
return $.when([]);
|
||||
@@ -2542,7 +2542,7 @@ QUnit.module('Views', {
|
||||
viewOptions: {
|
||||
ids: [1, 2, 4],
|
||||
index: 0,
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
@@ -2586,7 +2586,7 @@ QUnit.module('Views', {
|
||||
viewOptions: {
|
||||
ids: [1],
|
||||
index: 0,
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
@@ -2602,15 +2602,14 @@ QUnit.module('Views', {
|
||||
form.sidebar.$('button.o_dropdown_toggler_btn').click();
|
||||
form.sidebar.$('a:contains(Delete)').click();
|
||||
|
||||
testUtils.intercept(form, 'do_action', function (event) {
|
||||
assert.strictEqual(event.data.action, 'history_back',
|
||||
"should trigger an history back action");
|
||||
testUtils.intercept(form, 'history_back', function () {
|
||||
assert.step('history_back');
|
||||
});
|
||||
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
|
||||
$('.modal .modal-footer button.btn-primary').click();
|
||||
assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
|
||||
|
||||
assert.verifySteps(['read', 'unlink']);
|
||||
assert.verifySteps(['read', 'unlink', 'history_back']);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -4287,7 +4286,7 @@ QUnit.module('Views', {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
footer_to_buttons: true,
|
||||
footerToButtons: true,
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
@@ -4326,7 +4325,7 @@ QUnit.module('Views', {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
footer_to_buttons: true,
|
||||
footerToButtons: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4684,7 +4683,7 @@ QUnit.module('Views', {
|
||||
print: [],
|
||||
},
|
||||
viewOptions: {
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/action/load') {
|
||||
@@ -5943,7 +5942,7 @@ QUnit.module('Views', {
|
||||
'<field name="display_name"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
// rpcs done by the sidebar
|
||||
|
||||
@@ -164,7 +164,7 @@ QUnit.module('Views', {
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
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");
|
||||
},
|
||||
},
|
||||
@@ -384,7 +384,7 @@ QUnit.module('Views', {
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
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");
|
||||
},
|
||||
}
|
||||
@@ -666,7 +666,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
assert.deepEqual(event.data, {
|
||||
assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), {
|
||||
mode: 'readonly',
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
@@ -733,7 +733,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
intercepts: {
|
||||
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");
|
||||
},
|
||||
},
|
||||
@@ -977,7 +977,7 @@ QUnit.module('Views', {
|
||||
groupBy: ['product_id'],
|
||||
intercepts: {
|
||||
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");
|
||||
},
|
||||
},
|
||||
@@ -1659,7 +1659,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
intercepts: {
|
||||
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");
|
||||
},
|
||||
},
|
||||
@@ -1823,7 +1823,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
intercepts:{
|
||||
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");
|
||||
},
|
||||
},
|
||||
|
||||
@@ -150,7 +150,7 @@ QUnit.module('Views', {
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
arch: '<tree delete="0"><field name="foo"/></tree>',
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ QUnit.module('Views', {
|
||||
'<field name="foo"/>' +
|
||||
'<field name="m2o" invisible="1"/>' +
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
mockRPC: function (route) {
|
||||
assert.step(_.last(route.split('/')));
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -765,7 +765,7 @@ QUnit.module('Views', {
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
});
|
||||
|
||||
@@ -795,7 +795,7 @@ QUnit.module('Views', {
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
viewOptions: {sidebar: true},
|
||||
viewOptions: {hasSidebar: true},
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/dataset/call_kw/ir.attachment/search_read') {
|
||||
@@ -1686,7 +1686,7 @@ QUnit.module('Views', {
|
||||
"'switch_view' event has been triggered");
|
||||
},
|
||||
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");
|
||||
},
|
||||
},
|
||||
@@ -2031,7 +2031,7 @@ QUnit.module('Views', {
|
||||
'<field name="bar"/>' +
|
||||
'</tree>',
|
||||
intercepts: {
|
||||
warning: function (ev) {
|
||||
warning: function () {
|
||||
warnings++;
|
||||
},
|
||||
},
|
||||
@@ -2070,14 +2070,13 @@ QUnit.module('Views', {
|
||||
arch: '<tree><field name="name"/></tree>',
|
||||
});
|
||||
|
||||
testUtils.intercept(list, "switch_view", function (event) {
|
||||
assert.deepEqual(event.data, {
|
||||
'model': 'event',
|
||||
'res_id': '2-20170808020000',
|
||||
'view_type': 'form',
|
||||
'mode': 'readonly'
|
||||
},
|
||||
"should trigger switch to the form view for the record virtual id");
|
||||
testUtils.intercept(list, 'switch_view', function (event) {
|
||||
assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), {
|
||||
mode: 'readonly',
|
||||
model: 'event',
|
||||
res_id: '2-20170808020000',
|
||||
view_type: 'form',
|
||||
}, "should trigger a switch_view event to the form view for the record virtual id");
|
||||
});
|
||||
list.$('td:contains(virtual)').click();
|
||||
|
||||
@@ -2092,7 +2091,7 @@ QUnit.module('Views', {
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
mockRPC: function (route) {
|
||||
assert.step(route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -2220,7 +2219,7 @@ QUnit.module('Views', {
|
||||
print: [],
|
||||
},
|
||||
viewOptions: {
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
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,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/export/formats') {
|
||||
@@ -144,7 +144,7 @@ QUnit.module('widgets', {
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
sidebar: true,
|
||||
hasSidebar: true,
|
||||
},
|
||||
mockRPC: function (route) {
|
||||
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/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_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/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/web_client.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/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/pager_tests.js"></script>
|
||||
|
||||
@@ -534,6 +535,7 @@
|
||||
<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/tests/chrome/action_manager_mobile_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/kanban_mobile_tests.js"></script>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -303,7 +303,6 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
|
||||
$searchview: this.$searchview,
|
||||
$buttons: QWeb.render("website.GoToButtons"),
|
||||
},
|
||||
breadcrumbs: this.getParent().get_breadcrumbs(),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user