[REF] web: move ControlPanel inside controllers/actions
This branch introduces a large-scale reorganization of the
component tree generated by the web client. The short version is
that now, the control panel is a child of the view controller and
no longer a sibling. Graphically (and simplified), we go from
this:
webClient
/ | \
... ... actionManager
/ \
controlPanel viewController
/ \
to this:
webClient
/ | \
... ... actionManager
|
viewController
/ | \
ControlPanelController
/ \
CPRenderer CPModel
The motivation is that this work moves the code where it should be.
Before this commit, it was kind of weird to have code in the
controllers to render buttons outside of their root node (in
renderButtons). Also, the action manager had to take care of
coordinating search view states between view transitions.
So, this work simplifies the code. It also makes it easier to
extend. We see day after day that Odoo needs to take care of more
complex UI needs, and in many cases, these needs were quite
difficult to implement (we prefer spaghettis in our plates, not in
our code). The changes in this branch should open the way to
implement these features.
For example,
- it will now be easy to add the possibility of views (for example,
the search view or a new ControlPanel view) to add custom buttons
(of type action or object) in the control panel.
- Another need will be to serialize/ restore the state of the
search view across action boundaries (needed by the dashboard).
- Another example is the possibility for views to customize easily
the presence/absence of sub menus (filters/groupbys/favorites/
time range/...)
- Another need is an easier way for views/client action to customize
their control panel.
This commit also contains a large rewrite of the search view (so it
is more inline with our architecture and easier to maintain).
Part of task 1893568
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Martin Geubelle <mge@odoo.com>
This commit is contained in:
co-authored by
Aaron Bohy
Mathieu Duckerts-Antoine
Martin Geubelle
parent
48a50b4bc0
commit
40dd121938
@@ -2,82 +2,126 @@ odoo.define('web.AbstractAction', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* We define here the generic notion of Action (from the point of view of the
|
||||
* web client). In short, an action is a widget which controls the main part
|
||||
* of the screen (everything below the control panel).
|
||||
*
|
||||
* More precisely, the action manager is the component that coordinates a stack
|
||||
* of actions. Whenever the user navigates in the interface, switches views,
|
||||
* open different menus, the action manager creates/updates/destroys special
|
||||
* widgets which are actions. These actions need to answer to a standardised
|
||||
* API, which is the reason for this AbstractAction class.
|
||||
*
|
||||
* In practice, most actions are view controllers (coming from a
|
||||
* ir.action.act_window). However, some actions are 'client actions'. They
|
||||
* also need to be an AbstractAction for a better cooperation with the action
|
||||
* manager.
|
||||
* We define here the AbstractAction widget, which implements the ActionMixin.
|
||||
* All client actions must extend this widget.
|
||||
*
|
||||
* @module web.AbstractAction
|
||||
*/
|
||||
|
||||
var ActionMixin = require('web.ActionMixin');
|
||||
var ControlPanelView = require('web.ControlPanelView');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var AbstractAction = Widget.extend({
|
||||
var AbstractAction = Widget.extend(ActionMixin, {
|
||||
config: {
|
||||
ControlPanelView: ControlPanelView,
|
||||
},
|
||||
|
||||
/**
|
||||
* Called each time the action is attached into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {},
|
||||
/**
|
||||
* Called each time the action is detached from the DOM.
|
||||
*/
|
||||
on_detach_callback: function () {},
|
||||
/**
|
||||
* Called by the action manager when action is restored (typically, when the
|
||||
* user clicks on the action in the breadcrumb)
|
||||
* If this flag is set to true, the client action will create a control
|
||||
* panel whenever it is created.
|
||||
*
|
||||
* @returns {Deferred|undefined}
|
||||
* @type boolean
|
||||
*/
|
||||
willRestore: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
hasControlPanel: false,
|
||||
|
||||
/**
|
||||
* In some situations, we need confirmation from the controller that the
|
||||
* current state can be destroyed without prejudice to the user. For
|
||||
* example, if the user has edited a form, maybe we should ask him if we
|
||||
* can discard all his changes when we switch to another action. In that
|
||||
* case, the action manager will call this method. If the returned
|
||||
* deferred is succesfully resolved, then we can destroy the current action,
|
||||
* otherwise, we need to stop.
|
||||
* If true, this flag indicates that the client action should automatically
|
||||
* fetch the <arch> of a search view (or control panel view). Note that
|
||||
* to do that, it also needs a specific modelName.
|
||||
*
|
||||
* @returns {Deferred} resolved if the action can be removed, rejected otherwise
|
||||
* For example, the Discuss application adds the following line in its
|
||||
* constructor:
|
||||
*
|
||||
* this.controlPanelParams.modelName = 'mail.message';
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
canBeRemoved: function () {
|
||||
loadControlPanel: false,
|
||||
|
||||
/**
|
||||
* A client action might want to use a search bar in its control panel, or
|
||||
* it could choose not to use it.
|
||||
*
|
||||
* Note that it only makes sense if hasControlPanel is set to true.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
withSearchBar: false,
|
||||
|
||||
/**
|
||||
* This parameter can be set to customize the available sub menus in the
|
||||
* controlpanel (Filters/Group By/Favorites). This is basically a list of
|
||||
* the sub menus that we want to use.
|
||||
*
|
||||
* Note that it only makes sense if hasControlPanel is set to true.
|
||||
*
|
||||
* For example, set ['filter', 'favorite'] to enable the Filters and
|
||||
* Favorites menus.
|
||||
*
|
||||
* @type string[]
|
||||
*/
|
||||
searchMenuTypes: [],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* @param {Widget} parent
|
||||
* @param {Object} action
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
init: function (parent, action, options) {
|
||||
this._super(parent);
|
||||
this._title = action.display_name || action.name;
|
||||
this.controlPanelParams = {
|
||||
actionId: action.id,
|
||||
context: action.context,
|
||||
breadcrumbs: options && options.breadcrumbs || [],
|
||||
title: this.getTitle(),
|
||||
viewId: action.search_view_id && action.search_view_id[0],
|
||||
withSearchBar: this.withSearchBar,
|
||||
searchMenuTypes: this.searchMenuTypes,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* The willStart method is actually quite complicated if the client action
|
||||
* has a controlPanel, because it needs to prepare it.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
if (this.hasControlPanel) {
|
||||
var params = this.controlPanelParams;
|
||||
var def;
|
||||
if (this.loadControlPanel) {
|
||||
def = this
|
||||
.loadFieldView(params.modelName, params.context || {}, params.viewId, 'search')
|
||||
.then(function (fieldsView) {
|
||||
params.viewInfo = {
|
||||
arch: fieldsView.arch,
|
||||
fields: fieldsView.fields,
|
||||
};
|
||||
});
|
||||
}
|
||||
return $.when(def).then(function () {
|
||||
var controlPanelView = new self.config.ControlPanelView(params);
|
||||
return controlPanelView.getController(self).then(function (controlPanel) {
|
||||
self._controlPanel = controlPanel;
|
||||
return self._controlPanel.appendTo(document.createDocumentFragment());
|
||||
});
|
||||
});
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* This function is called when the current context (~state) of the action
|
||||
* should be known. For instance, if the action is a view controller,
|
||||
* this may be useful to reinstantiate the view in the same state.
|
||||
* @override
|
||||
*/
|
||||
getContext: function () {
|
||||
},
|
||||
/**
|
||||
* Gives the focus to the action
|
||||
*/
|
||||
giveFocus: function () {
|
||||
},
|
||||
/**
|
||||
* Renders the buttons to append, in most cases, to the control panel (in
|
||||
* the bottom left corner). When the action is rendered in a dialog, those
|
||||
* buttons might be moved to the dialog's footer.
|
||||
*
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
start: function () {
|
||||
if (this._controlPanel) {
|
||||
this._controlPanel.$el.prependTo(this.$el);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -60,10 +60,11 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
},
|
||||
load_filters: function (event) {
|
||||
return data_manager
|
||||
.load_filters(event.data.dataset, event.data.action_id)
|
||||
.load_filters(event.data)
|
||||
.then(event.data.on_success);
|
||||
},
|
||||
create_filter: '_onCreateFilter',
|
||||
delete_filter: '_onDeleteFilter',
|
||||
push_state: '_onPushState',
|
||||
show_effect: '_onShowEffect',
|
||||
// session
|
||||
@@ -218,7 +219,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
this.action_manager = new ActionManager(this, session.user_context);
|
||||
var fragment = document.createDocumentFragment();
|
||||
return this.action_manager.appendTo(fragment).then(function () {
|
||||
dom.append(self.$('.o_main_content'), fragment, {
|
||||
dom.append(self.$el, fragment, {
|
||||
in_DOM: true,
|
||||
callbacks: [{widget: self.action_manager}],
|
||||
});
|
||||
@@ -367,6 +368,18 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
.create_filter(e.data.filter)
|
||||
.then(e.data.on_success);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} e
|
||||
* @param {Object} e.data.filter the filter description
|
||||
* @param {function} e.data.on_success called when the RPC succeeds with its
|
||||
* returned value as argument
|
||||
*/
|
||||
_onDeleteFilter: function (e) {
|
||||
data_manager
|
||||
.delete_filter(e.data.filterId)
|
||||
.then(e.data.on_success);
|
||||
},
|
||||
/**
|
||||
* Displays a warning in a dialog or with the notification service
|
||||
*
|
||||
|
||||
@@ -10,10 +10,8 @@ odoo.define('web.ActionManager', function (require) {
|
||||
*/
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var Bus = require('web.Bus');
|
||||
var concurrency = require('web.concurrency');
|
||||
var Context = require('web.Context');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
@@ -24,7 +22,7 @@ var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var ActionManager = Widget.extend({
|
||||
className: 'o_content',
|
||||
className: 'o_action_manager',
|
||||
custom_events: {
|
||||
breadcrumb_clicked: '_onBreadcrumbClicked',
|
||||
history_back: '_onHistoryBack',
|
||||
@@ -63,17 +61,6 @@ var ActionManager = Widget.extend({
|
||||
// dialog (i.e. coming from an action with target='new')
|
||||
this.currentDialogController = null;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
// AAB: temporarily instantiate a unique main ControlPanel used by
|
||||
// controllers in the controllerStack
|
||||
this.controlPanel = new ControlPanel(this);
|
||||
var def = this.controlPanel.insertBefore(this.$el);
|
||||
|
||||
return $.when(def, this._super.apply(this, arguments));
|
||||
},
|
||||
/**
|
||||
* Called each time the action manager is attached into the DOM.
|
||||
*/
|
||||
@@ -266,14 +253,6 @@ var ActionManager = Widget.extend({
|
||||
if (controller.scrollPosition) {
|
||||
this.trigger_up('scrollTo', controller.scrollPosition);
|
||||
}
|
||||
|
||||
if (!controller.widget.need_control_panel) {
|
||||
this.controlPanel.do_hide();
|
||||
} else {
|
||||
this.controlPanel.update({
|
||||
breadcrumbs: this._getBreadcrumbs(),
|
||||
}, {clear: false});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Closes the current dialog, if any. Because we listen to the 'closed'
|
||||
@@ -330,19 +309,12 @@ var ActionManager = Widget.extend({
|
||||
return this._executeActionInDialog(action, options);
|
||||
}
|
||||
|
||||
var controller = self.controllers[action.controllerID];
|
||||
return this.clearUncommittedChanges()
|
||||
.then(function () {
|
||||
var controller = self.controllers[action.controllerID];
|
||||
var widget = controller.widget;
|
||||
// AAB: this will be moved to the Controller
|
||||
if (widget.need_control_panel) {
|
||||
// set the ControlPanel bus on the controller to allow it to
|
||||
// communicate its status
|
||||
widget.set_cp_bus(self.controlPanel.get_bus());
|
||||
}
|
||||
return self.dp.add(self._startController(controller));
|
||||
})
|
||||
.then(function (controller) {
|
||||
.then(function () {
|
||||
if (self.currentDialogController) {
|
||||
self._closeDialog({ silent: true });
|
||||
}
|
||||
@@ -358,7 +330,7 @@ var ActionManager = Widget.extend({
|
||||
}
|
||||
|
||||
// update the internal state and the DOM
|
||||
self._pushController(controller, options);
|
||||
self._pushController(controller);
|
||||
|
||||
// toggle the fullscreen mode for actions in target='fullscreen'
|
||||
self._toggleFullscreen();
|
||||
@@ -387,12 +359,6 @@ var ActionManager = Widget.extend({
|
||||
var self = this;
|
||||
var controller = this.controllers[action.controllerID];
|
||||
var widget = controller.widget;
|
||||
// AAB: this will be moved to the Controller
|
||||
if (widget.need_control_panel) {
|
||||
// set the ControlPanel bus on the controller to allow it to
|
||||
// communicate its status
|
||||
widget.set_cp_bus(new Bus());
|
||||
}
|
||||
|
||||
return this._startController(controller).then(function (controller) {
|
||||
var prevDialogOnClose;
|
||||
@@ -472,20 +438,18 @@ var ActionManager = Widget.extend({
|
||||
}
|
||||
|
||||
var controllerID = _.uniqueId('controller_');
|
||||
|
||||
var index = this._getControllerStackIndex(options);
|
||||
options.breadcrumbs = this._getBreadcrumbs(this.controllerStack.slice(0, index));
|
||||
options.controllerID = controllerID;
|
||||
var widget = new ClientAction(this, action, options);
|
||||
var controller = {
|
||||
actionID: action.jsID,
|
||||
index: index,
|
||||
jsID: controllerID,
|
||||
widget: new ClientAction(this, action, options),
|
||||
title: widget.getTitle(),
|
||||
widget: widget,
|
||||
};
|
||||
// AAB: TODO: simplify this with AbstractAction (implement a getTitle
|
||||
// function that returns action.name by default, and that can be
|
||||
// overriden in client actions and view controllers)
|
||||
Object.defineProperty(controller, 'title', {
|
||||
get: function () {
|
||||
return controller.widget.get('title') || action.display_name || action.name;
|
||||
},
|
||||
});
|
||||
this.controllers[controllerID] = controller;
|
||||
action.controllerID = controllerID;
|
||||
return this._executeAction(action, options).done(function () {
|
||||
@@ -586,33 +550,51 @@ var ActionManager = Widget.extend({
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Returns a description of the current stack of controllers, used to render
|
||||
* the breadcrumbs. It is an array of Objects with keys 'title' (what to
|
||||
* display in the breadcrumbs) and 'controllerID' (the ID of the
|
||||
* corresponding controller, used to restore it when this part of the
|
||||
* Returns a description of the controllers in the given controller stack.
|
||||
* It is used to render the breadcrumbs. It is an array of Objects with keys
|
||||
* 'title' (what to display in the breadcrumbs) and 'controllerID' (the ID
|
||||
* of the corresponding controller, used to restore it when this part of the
|
||||
* breadcrumbs is clicked).
|
||||
* Ignores the content of the stack of controllers if the action of the
|
||||
* last controller of the stack is flagged with 'no_breadcrumbs', indicating
|
||||
* that the breadcrumbs should not be displayed for that action.
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} controllerStack
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_getBreadcrumbs: function () {
|
||||
_getBreadcrumbs: function (controllerStack) {
|
||||
var self = this;
|
||||
var currentController = this.getCurrentController();
|
||||
var noBreadcrumbs = !currentController ||
|
||||
this.actions[currentController.actionID].context.no_breadcrumbs;
|
||||
if (noBreadcrumbs) {
|
||||
return [];
|
||||
}
|
||||
return _.map(this.controllerStack, function (controllerID) {
|
||||
return _.map(controllerStack, function (controllerID) {
|
||||
return {
|
||||
title: self.controllers[controllerID].title,
|
||||
controllerID: controllerID,
|
||||
title: self.controllers[controllerID].title,
|
||||
};
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns the index where a controller should be inserted in the controller
|
||||
* stack according to the given options. By default, a controller is pushed
|
||||
* on the top of the stack.
|
||||
*
|
||||
* @private
|
||||
* @param {options} [options.clear_breadcrumbs=false] if true, insert at
|
||||
* index 0 and remove all other controllers
|
||||
* @param {options} [options.index=null] if given, that index is returned
|
||||
* @param {options} [options.replace_last_action=false] if true, replace the
|
||||
* last controller of the stack
|
||||
* @returns {integer} index
|
||||
*/
|
||||
_getControllerStackIndex: function (options) {
|
||||
var index;
|
||||
if ('index' in options) {
|
||||
index = options.index;
|
||||
} else if (options.clear_breadcrumbs) {
|
||||
index = 0;
|
||||
} else if (options.replace_last_action) {
|
||||
index = this.controllerStack.length - 1;
|
||||
} else {
|
||||
index = this.controllerStack.length;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
/**
|
||||
* Returns an object containing information about the given controller, like
|
||||
* its title, its action's id, the active_id and active_ids of the action...
|
||||
@@ -625,7 +607,7 @@ var ActionManager = Widget.extend({
|
||||
var controller = this.controllers[controllerID];
|
||||
var action = this.actions[controller.actionID];
|
||||
var state = {
|
||||
title: controller.title,
|
||||
title: controller.widget.getTitle(),
|
||||
};
|
||||
if (action.id) {
|
||||
state.action = action.id;
|
||||
@@ -706,41 +688,28 @@ var ActionManager = Widget.extend({
|
||||
* @param {Object} controller
|
||||
* @param {string} controller.jsID
|
||||
* @param {Widget} controller.widget
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.clear_breadcrumbs=false] if true, destroys all
|
||||
* controllers from the controller stack before adding the given one
|
||||
* @param {Object} [options.replace_last_action=false] if true, replaces the
|
||||
* last controller of the controller stack by the given one
|
||||
* @param {integer} [options.index] if given, pushes the controller at that
|
||||
* position in the controller stack, and destroys the controllers with an
|
||||
* higher index
|
||||
* @param {integer} controller.index the controller is pushed at that
|
||||
* position in the controller stack and controllers with an higher index
|
||||
* are destroyed
|
||||
*/
|
||||
_pushController: function (controller, options) {
|
||||
options = options || {};
|
||||
_pushController: function (controller) {
|
||||
var self = this;
|
||||
|
||||
// detach the current controller
|
||||
this._detachCurrentController();
|
||||
|
||||
// empty the controller stack or replace the last controller as requested,
|
||||
// destroy the removed controllers and push the new controller to the stack
|
||||
var toDestroy;
|
||||
if (options.clear_breadcrumbs) {
|
||||
toDestroy = this.controllerStack;
|
||||
this.controllerStack = [];
|
||||
} else if (options.replace_last_action && this.controllerStack.length > 0) {
|
||||
toDestroy = [this.controllerStack.pop()];
|
||||
} else if (options.index !== undefined) {
|
||||
toDestroy = this.controllerStack.splice(options.index);
|
||||
// reject from the list of controllers to destroy the one that we are
|
||||
// currently pushing, or those linked to the same action as the one
|
||||
// linked to the controller that we are pushing
|
||||
toDestroy = _.reject(toDestroy, function (controllerID) {
|
||||
return controllerID === controller.jsID ||
|
||||
self.controllers[controllerID].actionID === controller.actionID;
|
||||
});
|
||||
}
|
||||
// push the new controller to the stack at the given position, and
|
||||
// destroy controllers with an higher index
|
||||
var toDestroy = this.controllerStack.slice(controller.index);
|
||||
// reject from the list of controllers to destroy the one that we are
|
||||
// currently pushing, or those linked to the same action as the one
|
||||
// linked to the controller that we are pushing
|
||||
toDestroy = _.reject(toDestroy, function (controllerID) {
|
||||
return controllerID === controller.jsID ||
|
||||
self.controllers[controllerID].actionID === controller.actionID;
|
||||
});
|
||||
this._removeControllers(toDestroy);
|
||||
this.controllerStack = this.controllerStack.slice(0, controller.index);
|
||||
this.controllerStack.push(controller.jsID);
|
||||
|
||||
// append the new controller to the DOM
|
||||
@@ -861,7 +830,7 @@ var ActionManager = Widget.extend({
|
||||
return $.when(def).then(function () {
|
||||
return $.when(controller.widget.do_show()).then(function () {
|
||||
var index = _.indexOf(self.controllerStack, controllerID);
|
||||
self._pushController(controller, {index: index});
|
||||
self._pushController(controller, index);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -878,19 +847,7 @@ var ActionManager = Widget.extend({
|
||||
* @returns {Deferred<Object>} resolved with the controller when it is ready
|
||||
*/
|
||||
_startController: function (controller) {
|
||||
var self = this;
|
||||
var fragment = document.createDocumentFragment();
|
||||
// AAB: change this logic to stop using the properties mixin
|
||||
controller.widget.on("change:title", this, function () {
|
||||
if (self.getCurrentController() !== controller) {
|
||||
return;
|
||||
}
|
||||
var action = self.actions[controller.actionID];
|
||||
if (!action.flags || !action.flags.headless) {
|
||||
var breadcrumbs = self._getBreadcrumbs();
|
||||
self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false});
|
||||
}
|
||||
});
|
||||
return controller.widget.appendTo(fragment).then(function () {
|
||||
return controller;
|
||||
});
|
||||
|
||||
@@ -10,21 +10,12 @@ 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 pyUtils = require('web.py_utils');
|
||||
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',
|
||||
get_controller_context: '_onGetControllerContext',
|
||||
navigation_move: '_onNavigationMove',
|
||||
update_filters: '_onUpdateFilters',
|
||||
search: '_onSearch',
|
||||
switch_view: '_onSwitchView',
|
||||
}),
|
||||
|
||||
@@ -32,6 +23,26 @@ ActionManager.include({
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Override to handle the case of lazy-loaded controllers, which may be the
|
||||
* last controller in the stack, but which should not be considered as
|
||||
* current controller as they don't have an alive widget.
|
||||
*
|
||||
* Note: this function assumes that there can be at most one lazy loaded
|
||||
* controller in the stack
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getCurrentController: function () {
|
||||
var currentController = this._super.apply(this, arguments);
|
||||
var action = currentController && this.actions[currentController.actionID];
|
||||
if (action && action.type === 'ir.actions.act_window' && !currentController.widget) {
|
||||
var lastControllerID = this.controllerStack.pop();
|
||||
currentController = this._super.apply(this, arguments);
|
||||
this.controllerStack.push(lastControllerID);
|
||||
}
|
||||
return currentController;
|
||||
},
|
||||
/**
|
||||
* Overrides to handle the case where an 'ir.actions.act_window' has to be
|
||||
* loaded.
|
||||
@@ -59,9 +70,9 @@ ActionManager.include({
|
||||
currentAction.type === 'ir.actions.act_window') {
|
||||
// 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 viewOptions = {currentId: state.id};
|
||||
var viewType = state.view_type || currentController.viewType;
|
||||
return this._switchController(currentAction, viewType);
|
||||
return this._switchController(currentAction, viewType, viewOptions);
|
||||
} else if (!core.action_registry.contains(state.action)) {
|
||||
// the action to load isn't the current one, so execute it
|
||||
var context = {};
|
||||
@@ -113,59 +124,6 @@ ActionManager.include({
|
||||
// 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) {
|
||||
// if requested, keep the searchview of the current action instead of
|
||||
// creating a new one
|
||||
if (action._keepSearchView) {
|
||||
var currentAction = this.getCurrentAction();
|
||||
if (currentAction) {
|
||||
action.searchView = currentAction.searchView;
|
||||
action.env = currentAction.env; // make those actions share the same env
|
||||
return $.when(currentAction.searchView);
|
||||
} else {
|
||||
// there is not searchview to keep, so reset the flag to false
|
||||
// to ensure that the one that will be created will be correctly
|
||||
// destroyed
|
||||
action._keepSearchView = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -181,6 +139,9 @@ ActionManager.include({
|
||||
* @param {Object} [viewOptions] dict of options passed to the initialization
|
||||
* of the controller's widget
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.controllerID=false] when the controller has
|
||||
* previously been lazy-loaded, we want to keep its jsID when loading it
|
||||
* @param {integer} [options.index=0] the controller's index in the stack
|
||||
* @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
|
||||
@@ -195,31 +156,36 @@ ActionManager.include({
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
|
||||
var controllerID = _.uniqueId('controller_');
|
||||
options = options || {};
|
||||
var index = options.index || 0;
|
||||
var controllerID = options.controllerID || _.uniqueId('controller_');
|
||||
var controller = {
|
||||
actionID: action.jsID,
|
||||
className: 'o_act_window', // used to remove the padding in dialogs
|
||||
index: index,
|
||||
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);
|
||||
return controller.widget ?
|
||||
controller.widget.getTitle() :
|
||||
(action.display_name || action.name);
|
||||
},
|
||||
});
|
||||
this.controllers[controllerID] = controller;
|
||||
|
||||
if (!options || !options.lazy) {
|
||||
if (!options.lazy) {
|
||||
// build the view options from different sources
|
||||
viewOptions = _.extend({
|
||||
var flags = action.flags || {};
|
||||
viewOptions = _.extend({}, flags, flags[viewType], viewOptions, {
|
||||
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 });
|
||||
|
||||
breadcrumbs: this._getBreadcrumbs(this.controllerStack.slice(0, index)),
|
||||
// pass the controllerID to the views as an hook for further
|
||||
// communication with trigger_up
|
||||
controllerID: controllerID,
|
||||
});
|
||||
var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
|
||||
var def = $.Deferred();
|
||||
action.controllers[viewType] = def;
|
||||
@@ -263,9 +229,6 @@ ActionManager.include({
|
||||
// its reference is removed
|
||||
controllerDef.reject();
|
||||
});
|
||||
if (action.searchView && !action._keepSearchView) {
|
||||
action.searchView.destroy();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Executes actions of type 'ir.actions.act_window'.
|
||||
@@ -280,75 +243,64 @@ ActionManager.include({
|
||||
*/
|
||||
_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.dp.add(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.controlPanelFieldsView = fieldsViews.search;
|
||||
action.controllers = {};
|
||||
|
||||
// select the first view to display, and optionally the main view
|
||||
// which will be lazyloaded
|
||||
var firstView = options.viewType && _.findWhere(views, {type: options.viewType});
|
||||
var mainView;
|
||||
if (firstView) {
|
||||
if (!firstView.multiRecord && views[0].multiRecord) {
|
||||
mainView = views[0];
|
||||
// select the current view to display, and optionally the main view
|
||||
// of the action which will be lazyloaded
|
||||
var curView = options.viewType && _.findWhere(views, {type: options.viewType});
|
||||
var lazyView;
|
||||
if (curView) {
|
||||
if (!curView.multiRecord && views[0].multiRecord) {
|
||||
lazyView = views[0];
|
||||
}
|
||||
} else {
|
||||
firstView = views[0];
|
||||
curView = views[0];
|
||||
}
|
||||
|
||||
// use mobile-friendly view by default in mobile, if possible
|
||||
if (config.device.isMobile) {
|
||||
if (!firstView.isMobileFriendly) {
|
||||
firstView = self._findMobileView(views, firstView.multiRecord) || firstView;
|
||||
if (!curView.isMobileFriendly) {
|
||||
curView = self._findMobileView(views, curView.multiRecord) || curView;
|
||||
}
|
||||
if (mainView && !mainView.isMobileFriendly) {
|
||||
mainView = self._findMobileView(views, mainView.multiRecord) || mainView;
|
||||
if (lazyView && !lazyView.isMobileFriendly) {
|
||||
lazyView = self._findMobileView(views, lazyView.multiRecord) || lazyView;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
var lazyViewDef;
|
||||
if (lazyView) {
|
||||
// if the main view is lazy-loaded, its (lazy-loaded) controller is inserted
|
||||
// into the controller stack (so that breadcrumbs can be correctly computed),
|
||||
// so we force clear_breadcrumbs to false so that it won't be removed when the
|
||||
// current controller will be inserted afterwards
|
||||
options.clear_breadcrumbs = false;
|
||||
// this controller being lazy-loaded, this call is actually sync
|
||||
lazyViewDef = self._createViewController(action, lazyView.type, {}, {lazy: true})
|
||||
.then(function (lazyLoadedController) {
|
||||
self.controllerStack.push(lazyLoadedController.jsID);
|
||||
});
|
||||
}
|
||||
return $.when(def).then(function () {
|
||||
var defs = [];
|
||||
defs.push(self._createViewController(action, firstView.type));
|
||||
if (mainView) {
|
||||
defs.push(self._createViewController(action, mainView.type, {}, {lazy: true}));
|
||||
}
|
||||
return self.dp.add($.when.apply($, defs));
|
||||
}).then(function (controller, lazyLoadedController) {
|
||||
action.controllerID = controller.jsID;
|
||||
return self._executeAction(action, options).done(function () {
|
||||
if (lazyLoadedController) {
|
||||
// controller should be placed just before the current one
|
||||
var index = self.controllerStack.length - 1;
|
||||
self.controllerStack.splice(index, 0, lazyLoadedController.jsID);
|
||||
self.controlPanel.update({
|
||||
breadcrumbs: self._getBreadcrumbs(),
|
||||
}, {clear: false});
|
||||
}
|
||||
});
|
||||
}).fail(self._destroyWindowAction.bind(self, action));
|
||||
return self.dp.add($.when(lazyViewDef))
|
||||
.then(function () {
|
||||
var viewOptions = {
|
||||
controllerState: options.controllerState,
|
||||
currentId: options.resID,
|
||||
};
|
||||
var curViewDef = self._createViewController(action, curView.type, viewOptions, {
|
||||
index: self._getControllerStackIndex(options),
|
||||
});
|
||||
return self.dp.add(curViewDef);
|
||||
})
|
||||
.then(function (controller) {
|
||||
action.controllerID = controller.jsID;
|
||||
return self._executeAction(action, options);
|
||||
})
|
||||
.fail(self._destroyWindowAction.bind(self, action));
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -367,49 +319,6 @@ ActionManager.include({
|
||||
multiRecord: multiRecord,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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 resID = options.resID || action.res_id;
|
||||
return {
|
||||
modelName: action.res_model,
|
||||
ids: resID ? [resID] : undefined,
|
||||
currentId: resID || undefined,
|
||||
domain: [],
|
||||
context: action.context || {},
|
||||
groupBy: [],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 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 = action.views[0][1] === '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,
|
||||
@@ -483,72 +392,20 @@ ActionManager.include({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_loadViews: function (action) {
|
||||
var inDialog = action.target === 'new';
|
||||
var inline = action.target === 'inline';
|
||||
var options = {
|
||||
action_id: action.id,
|
||||
toolbar: action.flags.hasSidebar,
|
||||
toolbar: inDialog && inline,
|
||||
};
|
||||
var views = action.views.slice();
|
||||
if (action.flags.hasSearchView) {
|
||||
if (!inline && !(inDialog && action.views[0][1] === 'form')) {
|
||||
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);
|
||||
},
|
||||
/**
|
||||
* Overrides to handle the 'keepSearchView' option. If set to true, the
|
||||
* search view of the current action will be re-used in the new action, i.e.
|
||||
* the environment (domain, context, groupby) will be shared between both
|
||||
* actions.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_preprocessAction: function (action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (action.type === 'ir.actions.act_window' && options.keepSearchView) {
|
||||
action._keepSearchView = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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 = pyUtils.eval_domains_and_contexts({
|
||||
domains: [action.domain || []].concat(domains || []),
|
||||
contexts: [action_context].concat(contexts || []),
|
||||
group_by_seq: groupbys || [],
|
||||
eval_context: this.userContext,
|
||||
});
|
||||
var groupBy = results.group_by.length ?
|
||||
results.group_by :
|
||||
(action.context.group_by || []);
|
||||
groupBy = (typeof groupBy === 'string') ? [groupBy] : groupBy;
|
||||
|
||||
if (results.error) {
|
||||
throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
|
||||
JSON.stringify(results.error)));
|
||||
}
|
||||
|
||||
var context = _.omit(results.context, 'time_ranges');
|
||||
|
||||
return {
|
||||
context: context,
|
||||
domain: results.domain,
|
||||
groupBy: groupBy,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Overrides to handle the case of 'ir.actions.act_window' actions, i.e.
|
||||
* destroys all controllers associated to the given action, and its search
|
||||
@@ -598,8 +455,8 @@ ActionManager.include({
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Handles the switch from a controller to another inside the same window
|
||||
* action.
|
||||
* Handles the switch from a controller to another (either inside the same
|
||||
* window action, or from a window action to another using the breadcrumbs).
|
||||
*
|
||||
* @private
|
||||
* @param {Object} controller the controller to switch to
|
||||
@@ -608,18 +465,55 @@ ActionManager.include({
|
||||
*/
|
||||
_switchController: function (action, viewType, viewOptions) {
|
||||
var self = this;
|
||||
var view = _.findWhere(action.views, {type: viewType});
|
||||
if (!view) {
|
||||
// can't switch to an unknown view
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
var currentController = this.getCurrentController();
|
||||
var index;
|
||||
if (currentController.actionID !== action.jsID) {
|
||||
// the requested controller is from another action, so we went back
|
||||
// to a previous action using the breadcrumbs
|
||||
var controller = _.findWhere(this.controllers, {
|
||||
actionID: action.jsID,
|
||||
viewType: viewType,
|
||||
});
|
||||
index = _.indexOf(this.controllerStack, controller.jsID);
|
||||
} else {
|
||||
// the requested controller is from the same action as the current
|
||||
// one, so we either
|
||||
// 1) go one step back from a mono record view to a multi record
|
||||
// one using the breadcrumbs
|
||||
// 2) or we switched from a view to another using the view
|
||||
// switcher
|
||||
// 3) or we opened a record from a multi record view
|
||||
if (view.multiRecord) {
|
||||
// cases 1) and 2) (with multi record views): replace the first
|
||||
// controller linked to the same action in the stack
|
||||
index = _.findIndex(this.controllerStack, function (controllerID) {
|
||||
return self.controllers[controllerID].actionID === action.jsID;
|
||||
});
|
||||
} else if (!_.findWhere(action.views, {type: currentController.viewType}).multiRecord) {
|
||||
// case 2) (with mono record views): replace the last
|
||||
// controller by the new one if they are from the same action
|
||||
// and if they both are mono record
|
||||
index = this.controllerStack.length - 1;
|
||||
} else {
|
||||
// case 3): insert the controller on the top of the controller
|
||||
// stack
|
||||
index = this.controllerStack.length;
|
||||
}
|
||||
}
|
||||
|
||||
var newController = function () {
|
||||
var newController = function (controllerID) {
|
||||
var options = {
|
||||
controllerID: controllerID,
|
||||
index: index,
|
||||
};
|
||||
return self
|
||||
._createViewController(action, viewType, viewOptions)
|
||||
._createViewController(action, viewType, viewOptions, options)
|
||||
.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);
|
||||
});
|
||||
};
|
||||
@@ -637,15 +531,9 @@ ActionManager.include({
|
||||
} 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];
|
||||
});
|
||||
// lazy loaded -> load it now (with same jsID)
|
||||
return newController(controller.jsID);
|
||||
} else {
|
||||
viewOptions = _.extend(viewOptions || {}, action.env);
|
||||
return $.when(controller.widget.willRestore()).then(function () {
|
||||
return controller.widget.reload(viewOptions).then(function () {
|
||||
return controller;
|
||||
@@ -656,22 +544,7 @@ ActionManager.include({
|
||||
}
|
||||
|
||||
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});
|
||||
return self._pushController(controller);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -679,18 +552,6 @@ ActionManager.include({
|
||||
// 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
|
||||
@@ -802,67 +663,6 @@ ActionManager.include({
|
||||
return self.doAction(action, options).then(ev.data.on_success, ev.data.on_fail);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Handles a context request: provides to the caller the context of the
|
||||
* current controller.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {function} ev.data.callback used to send the requested context
|
||||
*/
|
||||
_onGetControllerContext: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var currentController = this.getCurrentController();
|
||||
var context = currentController && currentController.widget.getContext();
|
||||
ev.data.callback(context || {});
|
||||
},
|
||||
/**
|
||||
* Called mainly from the control panel when the focus should be given to a
|
||||
* controller
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNavigationMove : function (ev) {
|
||||
switch(ev.data.direction) {
|
||||
case 'down' :
|
||||
var currentController = this.getCurrentController().widget;
|
||||
currentController.giveFocus();
|
||||
ev.stopPropagation();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handles a request to add/remove search view filters.
|
||||
*
|
||||
* @param {OdooEvent} ev
|
||||
* @param {string} ev.data.controllerID
|
||||
* @param {Array[Object]} [ev.data.newFilters]
|
||||
* @param {Array[Object]} [ev.data.filtersToRemove]
|
||||
* @param {function} ev.data.callback called with the added filters as arg
|
||||
*/
|
||||
_onUpdateFilters: function (ev) {
|
||||
var controller = this.controllers[ev.data.controllerID];
|
||||
var action = this.actions[controller.actionID];
|
||||
var data = ev.data;
|
||||
var addedFilters = action.searchView.updateFilters(data.newFilters, data.filtersToRemove);
|
||||
data.callback(addedFilters);
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
@@ -882,13 +682,11 @@ ActionManager.include({
|
||||
// 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) {
|
||||
var options = {
|
||||
controllerState: currentController.widget.exportState(),
|
||||
currentId: ev.data.res_id,
|
||||
};
|
||||
if (ev.data.mode) {
|
||||
options.mode = ev.data.mode;
|
||||
}
|
||||
this._switchController(action, viewType, options);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
odoo.define('web.ActionMixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* We define here the ActionMixin, the generic notion of action (from the point
|
||||
* of view of the web client). In short, an action is a widget which controls
|
||||
* the main part of the screen (everything below the navbar).
|
||||
*
|
||||
* More precisely, the action manager is the component that coordinates a stack
|
||||
* of actions. Whenever the user navigates in the interface, switches views,
|
||||
* open different menus, the action manager creates/updates/destroys special
|
||||
* widgets which implements the ActionMixin. These actions need to answer to a
|
||||
* standardised API, which is the reason for this mixin.
|
||||
*
|
||||
* In practice, most actions are view controllers (coming from an
|
||||
* ir.action.act_window). However, some actions are 'client actions'. They
|
||||
* also need to implement the ActionMixin for a better cooperation with the
|
||||
* action manager.
|
||||
*
|
||||
* @module web.ActionMixin
|
||||
*/
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
var ActionMixin = {
|
||||
template: 'Action',
|
||||
|
||||
/**
|
||||
* The action mixin assumes that it is rendered with the 'Action' template.
|
||||
* This template has a special zone ('.o_content') where the content should
|
||||
* be added. Actions that want to automatically render a template there
|
||||
* should define the contentTemplate key. In short, client actions should
|
||||
* probably define a contentTemplate key, and not a template key.
|
||||
*/
|
||||
contentTemplate: null,
|
||||
|
||||
/**
|
||||
* If an action wants to use a control panel, it will be created and
|
||||
* registered in this _controlPanel key (the widget). The way this control
|
||||
* panel is created is up to the implementation (so, view controllers or
|
||||
* client actions may have different needs).
|
||||
*
|
||||
* Note that most of the time, this key should be set by the framework, not
|
||||
* by the code of the client action.
|
||||
*/
|
||||
_controlPanel: null,
|
||||
|
||||
/**
|
||||
* String containing the title of the client action (which may be needed to
|
||||
* display in the breadcrumbs zone of the control panel).
|
||||
*
|
||||
* @see _setTitle
|
||||
*/
|
||||
_title: '',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
renderElement: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.contentTemplate) {
|
||||
var content = core.qweb.render(this.contentTemplate, {widget: this});
|
||||
this.$('.o_content').append(content);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called each time the action is attached into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {},
|
||||
/**
|
||||
* Called each time the action is detached from the DOM.
|
||||
*/
|
||||
on_detach_callback: function () {},
|
||||
/**
|
||||
* Called by the action manager when action is restored (typically, when the
|
||||
* user clicks on the action in the breadcrumb)
|
||||
*
|
||||
* @returns {Deferred|undefined}
|
||||
*/
|
||||
willRestore: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* In some situations, we need confirmation from the controller that the
|
||||
* current state can be destroyed without prejudice to the user. For
|
||||
* example, if the user has edited a form, maybe we should ask him if we
|
||||
* can discard all his changes when we switch to another action. In that
|
||||
* case, the action manager will call this method. If the returned
|
||||
* deferred is succesfully resolved, then we can destroy the current action,
|
||||
* otherwise, we need to stop.
|
||||
*
|
||||
* @returns {Deferred} resolved if the action can be removed, rejected
|
||||
* otherwise
|
||||
*/
|
||||
canBeRemoved: function () {
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* This function is called when the current state of the action
|
||||
* should be known. For instance, if the action is a view controller,
|
||||
* this may be useful to reinstantiate a view in the same state.
|
||||
* Typically the state can (and should) be encoded in a query object of
|
||||
* the form
|
||||
* {
|
||||
* context: {...},
|
||||
* groupBy: [...],
|
||||
* domain = [...],
|
||||
* orderedBy = [...],
|
||||
* }
|
||||
* where the context key can contain many information.
|
||||
* This method is mainly called during the creation of a custom filter.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getOwnedQueryParams: function () {
|
||||
return {};
|
||||
},
|
||||
/**
|
||||
* Returns a title that may be displayed in the breadcrumb area. For
|
||||
* example, the name of the record (for a form view). This is actually
|
||||
* important for the action manager: this is the way it is able to give
|
||||
* the proper titles for other actions.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getTitle: function () {
|
||||
return this._title;
|
||||
},
|
||||
/**
|
||||
* Gives the focus to the action
|
||||
*/
|
||||
giveFocus: function () {
|
||||
},
|
||||
/**
|
||||
* Renders the buttons to append, in most cases, to the control panel (in
|
||||
* the bottom left corner). When the action is rendered in a dialog, those
|
||||
* buttons might be moved to the dialog's footer.
|
||||
*
|
||||
* @param {jQuery Node} $node
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
},
|
||||
/**
|
||||
* This is the main method to customize the controlpanel content.
|
||||
*
|
||||
* @see updateContents method in ControlPanelRenderer for more info
|
||||
*
|
||||
* @param {Object} [status]
|
||||
* @param {string} [status.title]
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.clear]
|
||||
*/
|
||||
updateControlPanel: function (status, options) {
|
||||
if (this._controlPanel) {
|
||||
status = status || {};
|
||||
status.title = status.title || this.getTitle();
|
||||
this._controlPanel.updateContents(status, options || {});
|
||||
}
|
||||
},
|
||||
// TODO: add hooks methods:
|
||||
// - onRestoreHook (on_reverse_breadcrumbs)
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} title
|
||||
*/
|
||||
_setTitle: function (title) {
|
||||
this._title = title;
|
||||
this.updateControlPanel({title: this._title}, {clear: false});
|
||||
},
|
||||
};
|
||||
|
||||
return ActionMixin;
|
||||
|
||||
});
|
||||
@@ -1,286 +0,0 @@
|
||||
odoo.define('web.ControlPanelMixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Mixin allowing widgets to communicate with the ControlPanel. Widgets needing a
|
||||
* ControlPanel should use this mixin and call update_control_panel(cp_status) where
|
||||
* cp_status contains information for the ControlPanel to update itself.
|
||||
*
|
||||
* Note that the API is slightly awkward. Hopefully we will improve this when
|
||||
* we get the time to refactor the control panel.
|
||||
*
|
||||
* For example, here is what a typical client action would need to do to add
|
||||
* support for a control panel with some buttons::
|
||||
*
|
||||
* var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||
*
|
||||
* var SomeClientAction = Widget.extend(ControlPanelMixin, {
|
||||
* ...
|
||||
* start: function () {
|
||||
* this._renderButtons();
|
||||
* this._updateControlPanel();
|
||||
* ...
|
||||
* },
|
||||
* do_show: function () {
|
||||
* ...
|
||||
* this._updateControlPanel();
|
||||
* },
|
||||
* _renderButtons: function () {
|
||||
* this.$buttons = $(QWeb.render('SomeTemplate.Buttons'));
|
||||
* this.$buttons.on('click', ...);
|
||||
* },
|
||||
* _updateControlPanel: function () {
|
||||
* this.update_control_panel({
|
||||
* cp_content: {
|
||||
* $buttons: this.$buttons,
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
var ControlPanelMixin = {
|
||||
need_control_panel: true,
|
||||
/**
|
||||
* @param {web.Bus} [cp_bus] Bus to communicate with the ControlPanel
|
||||
*/
|
||||
set_cp_bus: function(cp_bus) {
|
||||
this.cp_bus = cp_bus;
|
||||
},
|
||||
/**
|
||||
* Triggers 'update' on the cp_bus to update the ControlPanel according to cp_status
|
||||
* @param {Object} [cp_status] see web.ControlPanel.update() for a description
|
||||
* @param {Object} [options] see web.ControlPanel.update() for a description
|
||||
*/
|
||||
update_control_panel: function(cp_status, options) {
|
||||
if (this.cp_bus) {
|
||||
this.cp_bus.trigger("update", cp_status || {}, options || {});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return ControlPanelMixin;
|
||||
|
||||
});
|
||||
|
||||
odoo.define('web.ControlPanel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Bus = require('web.Bus');
|
||||
var data = require('web.data');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var ControlPanel = Widget.extend({
|
||||
template: 'ControlPanel',
|
||||
/**
|
||||
* @param {String} [template] the QWeb template to render the ControlPanel.
|
||||
* By default, the template 'ControlPanel' will be used
|
||||
*/
|
||||
init: function(parent, template) {
|
||||
this._super(parent);
|
||||
if (template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
this.bus = new Bus(this);
|
||||
// the updateIndex is used to prevent concurrent updates of the control
|
||||
// panel depending on asynchronous code to be executed in the wrong order
|
||||
this.bus.updateIndex = 0;
|
||||
this.bus.on("update", this, this.update);
|
||||
},
|
||||
/**
|
||||
* Renders the control panel and creates a dictionnary of its exposed elements
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
start: function() {
|
||||
// Exposed jQuery nodesets
|
||||
this.nodes = {
|
||||
$buttons: this.$('.o_cp_buttons'),
|
||||
$pager: this.$('.o_cp_pager'),
|
||||
$searchview: this.$('.o_cp_searchview'),
|
||||
$searchview_buttons: this.$('.o_search_options'),
|
||||
$sidebar: this.$('.o_cp_sidebar'),
|
||||
$switch_buttons: this.$('.o_cp_switch_buttons'),
|
||||
};
|
||||
|
||||
// Prevent the search dropdowns to close when clicking inside them
|
||||
this.$el.on('click.bs.dropdown', '.o_search_options .dropdown-menu', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// By default, hide the ControlPanel and remove its contents from the DOM
|
||||
this._toggle_visibility(false);
|
||||
|
||||
return this._super();
|
||||
},
|
||||
/**
|
||||
* @return {Object} the Bus the ControlPanel is listening on
|
||||
*/
|
||||
get_bus: function() {
|
||||
return this.bus;
|
||||
},
|
||||
/**
|
||||
* Updates the content and displays the ControlPanel
|
||||
* @param {Object} [status.active_view] the current active view
|
||||
* @param {Array} [status.breadcrumbs] the breadcrumbs to display (see _render_breadcrumbs() for
|
||||
* precise description)
|
||||
* @param {Object} [status.cp_content] dictionnary containing the new ControlPanel jQuery elements
|
||||
* @param {Boolean} [status.hidden] true if the ControlPanel should be hidden
|
||||
* @param {openerp.web.SearchView} [status.searchview] the searchview widget
|
||||
* @param {Boolean} [status.search_view_hidden] true if the searchview is hidden, false otherwise
|
||||
* @param {Boolean} [options.clear] set to true to clear from control panel
|
||||
* elements that are not in status.cp_content
|
||||
*/
|
||||
update: function(status, options) {
|
||||
this.bus.updateIndex++;
|
||||
|
||||
this._toggle_visibility(!status.hidden);
|
||||
|
||||
// Don't update the ControlPanel in headless mode as the views have
|
||||
// inserted themselves the buttons where they want, so inserting them
|
||||
// again in the ControlPanel will remove them from where they should be
|
||||
if (!status.hidden) {
|
||||
options = _.defaults({}, options, {
|
||||
clear: true, // clear control panel by default
|
||||
});
|
||||
var new_cp_content = status.cp_content || {};
|
||||
|
||||
// Render the breadcrumbs
|
||||
if (status.breadcrumbs) {
|
||||
this.$('.breadcrumb').html(this._render_breadcrumbs(status.breadcrumbs));
|
||||
}
|
||||
|
||||
// Detach control_panel old content and attach new elements
|
||||
var toDetach = this.nodes;
|
||||
if (status.searchview && this.searchview === status.searchview) {
|
||||
// If the searchview is the same as before, don't detach it s.t.
|
||||
// we don't loose any floating content, nor the focus
|
||||
toDetach = _.omit(toDetach, '$searchview');
|
||||
new_cp_content = _.omit(new_cp_content, '$searchview');
|
||||
}
|
||||
if (options.clear) {
|
||||
this._detach_content(toDetach);
|
||||
// Show the searchview buttons area, which might have been hidden by
|
||||
// the searchview, as client actions may insert elements into it
|
||||
this.nodes.$searchview_buttons.show();
|
||||
} else {
|
||||
this._detach_content(_.pick(toDetach, _.keys(new_cp_content)));
|
||||
}
|
||||
this._attach_content(new_cp_content);
|
||||
if (options.clear || status.searchview) {
|
||||
this.searchview = status.searchview;
|
||||
}
|
||||
|
||||
// Update the searchview and switch buttons
|
||||
if (status.searchview || options.clear) {
|
||||
this._update_search_view(status.searchview, status.search_view_hidden, status.groupable, status.enableTimeRangeMenu);
|
||||
}
|
||||
if (status.active_view_selector) {
|
||||
this._update_switch_buttons(status.active_view_selector);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Private function that hides (or shows) the ControlPanel in headless (resp. non-headless) mode
|
||||
* Also detaches or attaches its contents to clean the DOM
|
||||
* @param {Boolean} [visible] true to show the control panel, false to hide it
|
||||
*/
|
||||
_toggle_visibility: function(visible) {
|
||||
this.do_toggle(visible);
|
||||
if (!visible && !this.$content) {
|
||||
this.$content = this.$el.contents().detach();
|
||||
} else if (this.$content) {
|
||||
this.$content.appendTo(this.$el);
|
||||
this.$content = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Private function that detaches the content of the ControlPanel
|
||||
* @param {Object} [elements_to_detach] subset of this.nodes to detach
|
||||
*/
|
||||
_detach_content: function(elements_to_detach) {
|
||||
_.each(elements_to_detach, function($nodeset) {
|
||||
$nodeset.contents().detach();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Private function that attaches content to the ControlPanel
|
||||
* @param {Object} [content] dictionnary of jQuery elements to attach, whose keys
|
||||
* are jQuery nodes identifiers in this.nodes
|
||||
*/
|
||||
_attach_content: function(content) {
|
||||
var self = this;
|
||||
_.each(content, function($nodeset, $element) {
|
||||
if ($nodeset && self.nodes[$element]) {
|
||||
self.nodes[$element].append($nodeset);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Private function that removes active class on all switch-buttons and adds
|
||||
* it to the one of the active view
|
||||
* @param {Object} [active_view_selector] the selector of the div to activate
|
||||
*/
|
||||
_update_switch_buttons: function(active_view_selector) {
|
||||
_.each(this.nodes.$switch_buttons.find('button'), function(button) {
|
||||
$(button).removeClass('active');
|
||||
});
|
||||
this.$(active_view_selector).addClass('active');
|
||||
},
|
||||
/**
|
||||
* Private function that renders the breadcrumbs
|
||||
* @param {Array} [breadcrumbs] list of objects containing the following keys:
|
||||
* - action: the action to execute when clicking on this part of the breadcrumbs
|
||||
* - index: the index in the breadcrumbs (starting at 0)
|
||||
* - title: what to display in the breadcrumbs
|
||||
* @return {Array} list of breadcrumbs' li jQuery elements
|
||||
*/
|
||||
_render_breadcrumbs: function (breadcrumbs) {
|
||||
var self = this;
|
||||
return breadcrumbs.map(function (bc, index) {
|
||||
return self._render_breadcrumbs_li(bc, index, breadcrumbs.length);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Private function that renders a breadcrumbs' li Jquery element
|
||||
*/
|
||||
_render_breadcrumbs_li: function (bc, index, length) {
|
||||
var self = this;
|
||||
var is_last = (index === length-1);
|
||||
var li_content = bc.title && _.escape(bc.title.trim()) || data.noDisplayContent;
|
||||
var $bc = $('<li>', {class: 'breadcrumb-item'})
|
||||
.append(is_last ? li_content : $('<a>', {href: '#'}).html(li_content))
|
||||
.toggleClass('active', is_last);
|
||||
if (!is_last) {
|
||||
$bc.click(function (ev) {
|
||||
ev.preventDefault();
|
||||
self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
|
||||
});
|
||||
}
|
||||
return $bc;
|
||||
},
|
||||
/**
|
||||
* Private function that updates the SearchView's visibility and extend the
|
||||
* breadcrumbs area if the SearchView is not visible
|
||||
*
|
||||
* @private
|
||||
* @param {SearchView} [searchview] the searchview Widget
|
||||
* @param {boolean} [isHidden] visibility of the searchview
|
||||
* @param {boolean} [groupable] visibility of the groupable menu (only
|
||||
* relevant if searchview is visible)
|
||||
*/
|
||||
_update_search_view: function (searchview, isHidden, groupable, enableTimeRangeMenu) {
|
||||
if (searchview) {
|
||||
searchview.toggle_visibility(!isHidden);
|
||||
if (groupable !== undefined){
|
||||
searchview.groupby_menu.do_toggle(groupable);
|
||||
}
|
||||
if (enableTimeRangeMenu !== undefined){
|
||||
searchview.displayTimeRangeMenu(enableTimeRangeMenu);
|
||||
}
|
||||
}
|
||||
this.nodes.$searchview.toggle(!isHidden);
|
||||
this.$el.toggleClass('o_breadcrumb_full', !!isHidden);
|
||||
},
|
||||
});
|
||||
|
||||
return ControlPanel;
|
||||
|
||||
});
|
||||
@@ -25,6 +25,8 @@ var Registry = Class.extend({
|
||||
*/
|
||||
init: function (mapping) {
|
||||
this.map = Object.create(mapping || null);
|
||||
this._scoreMapping = Object.create(null);
|
||||
this._sortedKeys = null;
|
||||
this.listeners = []; // listening callbacks on newly added items.
|
||||
},
|
||||
|
||||
@@ -39,9 +41,12 @@ var Registry = Class.extend({
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @param {[number]} score if given, this value will be used to order keys
|
||||
* @returns {Registry} can be used to chain add calls.
|
||||
*/
|
||||
add: function (key, value) {
|
||||
add: function (key, value, score) {
|
||||
this._scoreMapping[key] = score === undefined ? key : score;
|
||||
this._sortedKeys = null;
|
||||
this.map[key] = value;
|
||||
_.each(this.listeners, function (callback) {
|
||||
callback(key, value);
|
||||
@@ -49,7 +54,7 @@ var Registry = Class.extend({
|
||||
return this;
|
||||
},
|
||||
/**
|
||||
* Check if the registry contains the value
|
||||
* Check if the registry contains the key
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
@@ -58,18 +63,12 @@ var Registry = Class.extend({
|
||||
return (key in this.map);
|
||||
},
|
||||
/**
|
||||
* Creates and returns a copy of the current mapping, with the provided
|
||||
* mapping argument added in (replacing existing keys if needed)
|
||||
* Returns the content of the registry (an object mapping keys to values)
|
||||
*
|
||||
* Parent and child remain linked, a new key in the parent (which is not
|
||||
* overwritten by the child) will appear in the child.
|
||||
*
|
||||
* @param {Object} [mapping={}] a mapping of keys to object-paths
|
||||
* @returns {Object}
|
||||
*/
|
||||
extend: function (mapping) {
|
||||
var child = new Registry(this.map);
|
||||
_.extend(child.map, mapping);
|
||||
return child;
|
||||
entries: function () {
|
||||
return Object.create(this.map);
|
||||
},
|
||||
/**
|
||||
* Returns the value associated to the given key.
|
||||
@@ -95,6 +94,23 @@ var Registry = Class.extend({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* Return the list of keys in map object.
|
||||
*
|
||||
* The registry guarantees that the keys have a consistent order, defined by
|
||||
* the 'score' value when the item has been added.
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
keys: function () {
|
||||
var self = this;
|
||||
if (!this._sortedKeys) {
|
||||
this._sortedKeys = _.sortBy(Object.keys(this.map), function (key) {
|
||||
return self._scoreMapping[key] || 0;
|
||||
});
|
||||
}
|
||||
return this._sortedKeys;
|
||||
},
|
||||
/**
|
||||
* Register a callback to execute when items are added to the registry.
|
||||
*
|
||||
@@ -103,6 +119,17 @@ var Registry = Class.extend({
|
||||
onAdd: function (callback) {
|
||||
this.listeners.push(callback);
|
||||
},
|
||||
/**
|
||||
* Return the list of values in map object
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
values: function () {
|
||||
var self = this;
|
||||
return this.keys().map(function (key) {
|
||||
return self.map[key];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Registry;
|
||||
|
||||
@@ -139,8 +139,8 @@ var ServicesMixin = {
|
||||
promise.abort = abort.bind(def);
|
||||
return promise;
|
||||
},
|
||||
loadFieldView: function (dataset, view_id, view_type, options) {
|
||||
return this.loadViews(dataset.model, dataset.get_context().eval(), [[view_id, view_type]], options).then(function (result) {
|
||||
loadFieldView: function (modelName, context, view_id, view_type, options) {
|
||||
return this.loadViews(modelName, context, [[view_id, view_type]], options).then(function (result) {
|
||||
return result[view_type];
|
||||
});
|
||||
},
|
||||
@@ -155,11 +155,28 @@ var ServicesMixin = {
|
||||
});
|
||||
return def;
|
||||
},
|
||||
loadFilters: function (dataset, action_id) {
|
||||
loadFilters: function (modelName, actionId, context) {
|
||||
var def = $.Deferred();
|
||||
this.trigger_up('load_filters', {
|
||||
dataset: dataset,
|
||||
action_id: action_id,
|
||||
modelName: modelName,
|
||||
actionId: actionId,
|
||||
context: context,
|
||||
on_success: def.resolve.bind(def),
|
||||
});
|
||||
return def;
|
||||
},
|
||||
createFilter: function (filter) {
|
||||
var def = $.Deferred();
|
||||
this.trigger_up('create_filter', {
|
||||
filter: filter,
|
||||
on_success: def.resolve.bind(def),
|
||||
});
|
||||
return def;
|
||||
},
|
||||
deleteFilter: function (filterId) {
|
||||
var def = $.Deferred();
|
||||
this.trigger_up('delete_filter', {
|
||||
filterId: filterId,
|
||||
on_success: def.resolve.bind(def),
|
||||
});
|
||||
return def;
|
||||
|
||||
@@ -16,7 +16,7 @@ odoo.define('web.relational_fields', function (require) {
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var basicFields = require('web.basic_fields');
|
||||
var concurrency = require('web.concurrency');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
var ControlPanelView = require('web.ControlPanelView');
|
||||
var dialogs = require('web.view_dialogs');
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
@@ -1030,7 +1030,14 @@ var FieldX2Many = AbstractField.extend({
|
||||
}
|
||||
var self = this;
|
||||
var defs = [];
|
||||
this.control_panel = new ControlPanel(this, "X2ManyControlPanel");
|
||||
var controlPanelView = new ControlPanelView({
|
||||
template: 'X2ManyControlPanel',
|
||||
withSearchBar: false,
|
||||
});
|
||||
var cpDef = controlPanelView.getController(this).then(function (controlPanel) {
|
||||
self._controlPanel = controlPanel;
|
||||
return self._controlPanel.prependTo(self.$el);
|
||||
});
|
||||
this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, {
|
||||
single_page_hidden: true,
|
||||
withAccessKey: false,
|
||||
@@ -1053,9 +1060,9 @@ var FieldX2Many = AbstractField.extend({
|
||||
});
|
||||
this._renderButtons();
|
||||
defs.push(this.pager.appendTo($('<div>'))); // start the pager
|
||||
defs.push(this.control_panel.prependTo(this.$el));
|
||||
defs.push(cpDef);
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self.control_panel.update({
|
||||
self._controlPanel.updateContents({
|
||||
cp_content: {
|
||||
$buttons: self.$buttons,
|
||||
$pager: self.pager.$el,
|
||||
|
||||
@@ -4,7 +4,6 @@ odoo.define('report.client_action', function (require) {
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||
var session = require('web.session');
|
||||
var utils = require('report.utils');
|
||||
|
||||
@@ -17,9 +16,9 @@ var AUTHORIZED_MESSAGES = [
|
||||
'report:do_action',
|
||||
];
|
||||
|
||||
var ReportAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
template: 'report.client_action',
|
||||
var ReportAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
contentTemplate: 'report.client_action',
|
||||
|
||||
init: function (parent, action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -27,7 +26,7 @@ var ReportAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
options = options || {};
|
||||
|
||||
this.action_manager = parent;
|
||||
this.title = options.display_name || options.name;
|
||||
this._title = options.display_name || options.name;
|
||||
|
||||
this.edit_mode_available = false;
|
||||
this.in_edit_mode = false;
|
||||
@@ -42,7 +41,6 @@ var ReportAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.set('title', this.title);
|
||||
this.iframe = this.$('iframe')[0];
|
||||
return $.when(this._super.apply(this, arguments), session.is_bound).then(function () {
|
||||
var web_base_url = session['web.base.url'];
|
||||
@@ -93,7 +91,7 @@ var ReportAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
},
|
||||
|
||||
_update_control_panel: function () {
|
||||
this.update_control_panel({
|
||||
this.updateControlPanel({
|
||||
cp_content: {
|
||||
$buttons: this.$buttons,
|
||||
},
|
||||
|
||||
@@ -39,8 +39,6 @@ return {
|
||||
// registries
|
||||
action_registry : new Registry(),
|
||||
crash_registry: new Registry(),
|
||||
search_filters_registry: new Registry(),
|
||||
search_widgets_registry: new Registry(),
|
||||
serviceRegistry: new Registry(),
|
||||
/**
|
||||
* @type {String}
|
||||
|
||||
@@ -128,15 +128,20 @@ return core.Class.extend({
|
||||
*
|
||||
* @param {Object} [dataset] the dataset for which the filters are loaded
|
||||
* @param {int} [action_id] the id of the action (optional)
|
||||
* @param {Object} params
|
||||
* @param {string} params.modelName
|
||||
* @param {Object} params.context
|
||||
* @param {integer} params.actionId
|
||||
* @return {Deferred} resolved with the requested filters
|
||||
*/
|
||||
load_filters: function (dataset, action_id) {
|
||||
var key = this._gen_key(dataset.model, action_id);
|
||||
load_filters: function (params) {
|
||||
var key = this._gen_key(params.modelName, params.actionId);
|
||||
if (!this._cache.filters[key]) {
|
||||
this._cache.filters[key] = rpc.query({
|
||||
args: [dataset.model, action_id],
|
||||
args: [params.modelName, params.actionId],
|
||||
kwargs: {
|
||||
context: dataset.get_context(),
|
||||
context: params.context || {},
|
||||
// get_context() de dataset
|
||||
},
|
||||
model: 'ir.filters',
|
||||
method: 'get_filters',
|
||||
@@ -158,26 +163,26 @@ return core.Class.extend({
|
||||
model: 'ir.filters',
|
||||
method: 'create_or_replace',
|
||||
})
|
||||
.then(function (filter_id) {
|
||||
.then(function (filterId) {
|
||||
var key = [
|
||||
filter.model_id,
|
||||
filter.action_id || false,
|
||||
].join(',');
|
||||
self._invalidate(self._cache.filters, key);
|
||||
return filter_id;
|
||||
return filterId;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls 'unlink' on 'ir_filters'.
|
||||
*
|
||||
* @param {Object} [filter] the description of the filter to remove
|
||||
* @param {integer} filterId, Id of the filter to remove
|
||||
* @return {Deferred}
|
||||
*/
|
||||
delete_filter: function (filter) {
|
||||
delete_filter: function (filterId) {
|
||||
var self = this;
|
||||
return rpc.query({
|
||||
args: [filter.id],
|
||||
args: [filterId],
|
||||
model: 'ir.filters',
|
||||
method: 'unlink',
|
||||
})
|
||||
|
||||
@@ -345,8 +345,8 @@ DebugManager.include({
|
||||
action: this._action,
|
||||
can_edit: this._can_edit_views,
|
||||
controller: this._controller,
|
||||
controlPanelView: this._controller && this._controller._controlPanel,
|
||||
manager: this,
|
||||
searchview: this._controller && this._controller.searchView,
|
||||
view: this._controller && _.findWhere(this._action.views, {
|
||||
type: this._controller.viewType,
|
||||
}),
|
||||
|
||||
@@ -12,18 +12,21 @@ odoo.define('web.AbstractController', function (require) {
|
||||
* reading localstorage, ...) has to go through the controller.
|
||||
*/
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var ActionMixin = require('web.ActionMixin');
|
||||
var ajax = require('web.ajax');
|
||||
var concurrency = require('web.concurrency');
|
||||
var config = require('web.config');
|
||||
var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||
var core = require('web.core');
|
||||
var mvc = require('web.mvc');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
var AbstractController = mvc.Controller.extend(ActionMixin, {
|
||||
custom_events: {
|
||||
get_controller_query_params: '_onGetControllerQueryParams',
|
||||
navigation_move: '_onNavigationMove',
|
||||
open_record: '_onOpenRecord',
|
||||
search: '_onSearch',
|
||||
switch_view: '_onSwitchView',
|
||||
},
|
||||
events: {
|
||||
@@ -31,50 +34,30 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {AbstractModel} model
|
||||
* @param {AbstractRenderer} renderer
|
||||
* @param {object} params
|
||||
* @override
|
||||
* @param {string} params.modelName
|
||||
* @param {string} [params.controllerID] an id to ease the communication
|
||||
* with upstream components
|
||||
* @param {ControlPanelController} [params.controlPanel]
|
||||
* @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);
|
||||
this.model = model;
|
||||
this.renderer = renderer;
|
||||
this._controlPanel = params.controlPanel;
|
||||
this._title = params.displayName;
|
||||
this.modelName = params.modelName;
|
||||
this.handle = params.handle;
|
||||
this.activeActions = params.activeActions;
|
||||
this.controllerID = params.controllerID;
|
||||
this.initialState = params.initialState;
|
||||
this.bannerRoute = params.bannerRoute;
|
||||
// use a DropPrevious to correctly handle concurrent updates
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
// 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.groupable = params.groupable;
|
||||
this.enableTimeRangeMenu = params.enableTimeRangeMenu;
|
||||
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;
|
||||
// use a DropPrevious to correctly handle concurrent updates
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
},
|
||||
/**
|
||||
* Simply renders and updates the url.
|
||||
@@ -86,13 +69,13 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
this.$el.addClass('o_view_controller');
|
||||
|
||||
// render the ControlPanel elements (buttons, pager, sidebar...)
|
||||
this.controlPanelElements = this._renderControlPanelElements();
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (self._controlPanel) {
|
||||
// render the ControlPanel elements (buttons, pager, sidebar...)
|
||||
self.controlPanelElements = self._renderControlPanelElements();
|
||||
self._controlPanel.$el.prependTo(self.$el);
|
||||
}
|
||||
|
||||
return $.when(
|
||||
this._super.apply(this, arguments),
|
||||
this.renderer.appendTo(this.$el)
|
||||
).then(function () {
|
||||
return self._update(self.initialState);
|
||||
});
|
||||
},
|
||||
@@ -106,14 +89,14 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
|
||||
this.controlPanelElements.$switch_buttons.off();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Called each time the controller is attached into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
if (this.searchView) {
|
||||
this.searchView.on_attach_callback();
|
||||
if (this._controlPanel) {
|
||||
this._controlPanel.on_attach_callback();
|
||||
}
|
||||
this.renderer.on_attach_callback();
|
||||
},
|
||||
@@ -153,26 +136,18 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Returns any special keys that may be useful when reloading the view to
|
||||
* get the same effect. This is necessary for saving the current view in
|
||||
* the favorites. For example, a graph view might want to add a key to
|
||||
* save the current graph type.
|
||||
* Export the state of the controller containing information that is shared
|
||||
* between different controllers of a same action (like the current
|
||||
* searchQuery of the controlPanel).
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
return {};
|
||||
},
|
||||
/**
|
||||
* Returns a title that may be displayed in the breadcrumb area. For
|
||||
* example, the name of the record.
|
||||
*
|
||||
* note: this will be moved to AbstractAction
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getTitle: function () {
|
||||
return this.displayName;
|
||||
exportState: function () {
|
||||
var state = {};
|
||||
if (this._controlPanel) {
|
||||
state.cpState = this._controlPanel.exportState();
|
||||
}
|
||||
return state;
|
||||
},
|
||||
/**
|
||||
* Gives the focus to the renderer
|
||||
@@ -199,7 +174,16 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
reload: function (params) {
|
||||
return this.update(params || {});
|
||||
params = params || {};
|
||||
var def;
|
||||
var controllerState = params.controllerState || {};
|
||||
var cpState = controllerState.cpState;
|
||||
if (this._controlPanel && cpState) {
|
||||
def = this._controlPanel.importState(cpState).then(function (searchQuery) {
|
||||
params = _.extend({}, params, searchQuery);
|
||||
});
|
||||
}
|
||||
return $.when(def).then(this.update.bind(this, params, {}));
|
||||
},
|
||||
/**
|
||||
* For views that require a pager, this method will be called to allow the
|
||||
@@ -246,16 +230,16 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
// and this controller should no longer update the control panel.
|
||||
// note that this won't be necessary as soon as each controller will have
|
||||
// its own control panel
|
||||
var cpUpdateIndex = this.cp_bus && this.cp_bus.updateIndex;
|
||||
var cpUpdateIndex = this._controlPanel && this._controlPanel.updateIndex;
|
||||
return this.dp.add(def).then(function (handle) {
|
||||
if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
|
||||
if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
|
||||
return;
|
||||
}
|
||||
self.handle = handle || self.handle; // update handle if we reloaded
|
||||
var state = self.model.get(self.handle);
|
||||
var localState = self.renderer.getLocalState();
|
||||
return self.dp.add(self.renderer.updateState(state, params)).then(function () {
|
||||
if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
|
||||
if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
|
||||
return;
|
||||
}
|
||||
self.renderer.setLocalState(localState);
|
||||
@@ -327,7 +311,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
js.remove();
|
||||
});
|
||||
return $.when.apply($, defs).then(function () {
|
||||
$banner.prependTo(self.$el);
|
||||
$banner.prependTo(self.$('> .o_content'));
|
||||
self._$banner = $banner;
|
||||
});
|
||||
});
|
||||
@@ -342,24 +326,20 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @returns {Object} an object containing the control panel jQuery elements
|
||||
*/
|
||||
_renderControlPanelElements: function () {
|
||||
var elements = {};
|
||||
var elements = {
|
||||
$buttons: $('<div>'),
|
||||
$sidebar: $('<div>'),
|
||||
$pager: $('<div>'),
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
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;
|
||||
},
|
||||
@@ -379,6 +359,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
var template = config.device.isMobile ? 'ControlPanel.SwitchButtons.Mobile' : 'ControlPanel.SwitchButtons';
|
||||
var $switchButtons = $(QWeb.render(template, {
|
||||
viewType: this.viewType,
|
||||
views: views,
|
||||
}));
|
||||
// create bootstrap tooltips
|
||||
@@ -392,14 +373,21 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
self.trigger_up('switch_view', {view_type: viewType});
|
||||
}, 200, true));
|
||||
|
||||
// set active view's icon as view switcher button's icon in mobile
|
||||
if (config.device.isMobile) {
|
||||
// set active view's icon as view switcher button's icon
|
||||
var activeView = _.findWhere(views, {type: this.viewType});
|
||||
$switchButtons.find('.o_switch_view_button_icon').addClass('fa fa-lg ' + activeView.icon);
|
||||
}
|
||||
|
||||
return $switchButtons;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_startRenderer: function () {
|
||||
return this.renderer.appendTo(this.$('.o_content'));
|
||||
},
|
||||
/**
|
||||
* This method is called after each update or when the start method is
|
||||
* completed.
|
||||
@@ -413,23 +401,12 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @param {Object} state the state given by the model
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_update: function (state) {
|
||||
_update: function () {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
this.update_control_panel({
|
||||
active_view_selector: '.o_cp_switch_' + this.viewType,
|
||||
this.updateControlPanel({
|
||||
cp_content: cpContent,
|
||||
hidden: !this.withControlPanel,
|
||||
searchview: this.searchView,
|
||||
search_view_hidden: !this.searchable || this.searchviewHidden,
|
||||
groupable: this.groupable,
|
||||
enableTimeRangeMenu: this.enableTimeRangeMenu,
|
||||
title: this.getTitle(),
|
||||
});
|
||||
|
||||
this._pushState();
|
||||
@@ -478,6 +455,36 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
this.do_action($target.attr('name'));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* FIXME: this logic should be rethought
|
||||
*
|
||||
* Handles a context request: provides to the caller the state of the
|
||||
* current controller.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {function} ev.data.callback used to send the requested state
|
||||
*/
|
||||
_onGetControllerQueryParams: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var state = this.getOwnedQueryParams();
|
||||
ev.data.callback(state || {});
|
||||
},
|
||||
/**
|
||||
* Called mainly from the control panel when the focus should be given to
|
||||
* the controller
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNavigationMove : function (ev) {
|
||||
switch(ev.data.direction) {
|
||||
case 'down' :
|
||||
ev.stopPropagation();
|
||||
this.giveFocus();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* When an Odoo event arrives requesting a record to be opened, this method
|
||||
* gets the res_id, and request a switch view in the appropriate mode
|
||||
@@ -502,6 +509,20 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
|
||||
model: this.modelName,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
* @param {Array[]} ev.data.domain
|
||||
* @param {Object} ev.data.context
|
||||
* @param {string[]} ev.data.groupby
|
||||
*/
|
||||
_onSearch: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.reload(_.extend({offset: 0}, ev.data));
|
||||
},
|
||||
/**
|
||||
* Intercepts the 'switch_view' event to add the controllerID into the data,
|
||||
* and lets the event bubble up.
|
||||
|
||||
@@ -14,52 +14,21 @@ odoo.define('web.AbstractModel', function (require) {
|
||||
* in order to be able to notify its parent by bubbling events up.
|
||||
*/
|
||||
|
||||
var Class = require('web.Class');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
var mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
|
||||
var AbstractModel = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
/**
|
||||
* @param {Widget} parent
|
||||
*/
|
||||
init: function (parent) {
|
||||
mixins.EventDispatcherMixin.init.call(this);
|
||||
this.setParent(parent);
|
||||
},
|
||||
var mvc = require('web.mvc');
|
||||
|
||||
var AbstractModel = mvc.Model.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method should return the complete state necessary for the renderer
|
||||
* to display the currently viewed data.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
get: function () {
|
||||
},
|
||||
/**
|
||||
* The load method is called once in a model, when we load the data for the
|
||||
* first time. The method returns (a deferred that resolves to) some kind
|
||||
* of token/handle. The handle can then be used with the get method to
|
||||
* access a representation of the data.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.modelName the name of the model
|
||||
* @returns {Deferred} The deferred resolves to some kind of handle
|
||||
*/
|
||||
load: function (params) {
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* When something changes, the data may need to be refetched. This is the
|
||||
* job for this method: reloading (only if necessary) all the data and
|
||||
* making sure that they are ready to be redisplayed.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @returns {Deferred}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
reload: function (params) {
|
||||
return $.when();
|
||||
|
||||
@@ -7,22 +7,18 @@ odoo.define('web.AbstractRenderer', function (require) {
|
||||
*
|
||||
*/
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
var mvc = require('web.mvc');
|
||||
|
||||
/**
|
||||
* @class AbstractRenderer
|
||||
*/
|
||||
return Widget.extend({
|
||||
return mvc.Renderer.extend({
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {any} state
|
||||
* @param {Object} params
|
||||
* @override
|
||||
* @param {string} [params.noContentHelp]
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
this._super(parent);
|
||||
this.state = state;
|
||||
this._super.apply(this, arguments);
|
||||
this.arch = params.arch;
|
||||
this.noContentHelp = params.noContentHelp;
|
||||
},
|
||||
|
||||
@@ -23,14 +23,16 @@ odoo.define('web.AbstractView', function (require) {
|
||||
* in most case discarded.
|
||||
*/
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var Class = require('web.Class');
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var utils = require('web.utils');
|
||||
var ControlPanelView = require('web.ControlPanelView');
|
||||
var mvc = require('web.mvc');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var AbstractView = Class.extend({
|
||||
var Factory = mvc.Factory;
|
||||
|
||||
var AbstractView = Factory.extend({
|
||||
// name displayed in view switchers
|
||||
display_name: '',
|
||||
// indicates whether or not the view is mobile-friendly
|
||||
@@ -40,22 +42,20 @@ var AbstractView = Class.extend({
|
||||
// multi_record is used to distinguish views displaying a single record
|
||||
// (e.g. FormView) from those that display several records (e.g. ListView)
|
||||
multi_record: true,
|
||||
// determine if a search view should be displayed in the control panel and
|
||||
// allowed to interact with the view. Currently, the only not searchable
|
||||
// views are the form view and the diagram view.
|
||||
searchable: true,
|
||||
// viewType is the type of the view, like 'form', 'kanban', 'list'...
|
||||
viewType: undefined,
|
||||
// if searchable, this flag determines if the search view will display a
|
||||
// groupby menu or not. This is useful for the views which do not support
|
||||
// grouping data.
|
||||
groupable: true,
|
||||
enableTimeRangeMenu: false,
|
||||
config: {
|
||||
// determines if a search bar is available
|
||||
withSearchBar: true,
|
||||
// determines the search menus available and their orders
|
||||
searchMenuTypes: ['filter', 'groupBy', 'favorite'],
|
||||
// determines if a control panel should be instantiated
|
||||
withControlPanel: true,
|
||||
// determines the MVC components to use
|
||||
config: _.extend({}, Factory.prototype.config, {
|
||||
Model: AbstractModel,
|
||||
Renderer: AbstractRenderer,
|
||||
Controller: AbstractController,
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* The constructor function is supposed to set 3 variables: rendererParams,
|
||||
@@ -65,33 +65,32 @@ var AbstractView = Class.extend({
|
||||
* @constructs AbstractView
|
||||
*
|
||||
* @param {Object} viewInfo
|
||||
* @param {Object} viewInfo.arch
|
||||
* @param {Object|string} viewInfo.arch
|
||||
* @param {Object} viewInfo.fields
|
||||
* @param {Object} viewInfo.fieldsInfo
|
||||
* @param {Object} params
|
||||
* @param {string} params.modelName The actual model name
|
||||
* @param {Object} params.context
|
||||
* @param {number} [params.count]
|
||||
* @param {string} [params.modelName]
|
||||
* @param {Object} [params.action={}]
|
||||
* @param {Object} [params.context={}]
|
||||
* @param {string} [params.controllerID]
|
||||
* @param {string[]} params.domain
|
||||
* @param {string[][]} params.timeRange
|
||||
* @param {string[][]} params.comparisonTimeRange
|
||||
* @param {string} params.timeRangeDescription
|
||||
* @param {string} params.comparisonTimeRangeDescription
|
||||
* @param {boolean} params.compare
|
||||
* @param {string[]} params.groupBy
|
||||
* @param {number} [params.count]
|
||||
* @param {number} [params.currentId]
|
||||
* @param {boolean} params.isEmbedded
|
||||
* @param {string} [params.controllerState]
|
||||
* @param {string} [params.displayName]
|
||||
* @param {Array[]} [params.domain=[]]
|
||||
* @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]
|
||||
* @param {boolean} [params.isEmbedded=false]
|
||||
* @param {Object} [params.searchQuery={}]
|
||||
* @param {Object} [params.searchQuery.context={}]
|
||||
* @param {Array[]} [params.searchQuery.domain=[]]
|
||||
* @param {string[]} [params.searchQuery.groupBy=[]]
|
||||
* @param {Object} [params.userContext={}]
|
||||
* @param {boolean} [params.withControlPanel=true]
|
||||
*/
|
||||
init: function (viewInfo, params) {
|
||||
var action = params.action || {};
|
||||
params = _.defaults(params, this._extractParamsFromAction(action));
|
||||
|
||||
// in general, the fieldsView has to be processed by the View (e.g. the
|
||||
// arch is a string that needs to be parsed) ; the only exception is for
|
||||
// inline form views inside form views, as they are processed alongside
|
||||
@@ -103,89 +102,51 @@ var AbstractView = Class.extend({
|
||||
} else {
|
||||
this.fieldsView = viewInfo;
|
||||
}
|
||||
this.fields = this.fieldsView.viewFields;
|
||||
this.arch = this.fieldsView.arch;
|
||||
// the boolean parameter 'isEmbedded' determines if the view should be considered
|
||||
// as a subview. For now this is only used by the graph controller that appends a
|
||||
// 'Group By' button beside the 'Measures' button when the graph view is embedded.
|
||||
this.fields = this.fieldsView.viewFields;
|
||||
this.userContext = params.userContext || {};
|
||||
this.withControlPanel = params.withControlPanel;
|
||||
|
||||
// the boolean parameter 'isEmbedded' determines if the view should be
|
||||
// considered as a subview. For now this is only used by the graph
|
||||
// controller that appends a 'Group By' button beside the 'Measures'
|
||||
// button when the graph view is embedded.
|
||||
var isEmbedded = params.isEmbedded || false;
|
||||
|
||||
this.rendererParams = {
|
||||
arch: this.arch,
|
||||
isEmbedded: isEmbedded,
|
||||
noContentHelp: params.action && params.action.help,
|
||||
noContentHelp: params.noContentHelp,
|
||||
};
|
||||
|
||||
var timeRangeMenuData = params.context.timeRangeMenuData;
|
||||
var timeRange = [];
|
||||
var comparisonTimeRange = [];
|
||||
var compare = false;
|
||||
var timeRangeDescription = "";
|
||||
var comparisonTimeRangeDescription = "";
|
||||
if (this.enableTimeRangeMenu && timeRangeMenuData) {
|
||||
timeRange = timeRangeMenuData.timeRange;
|
||||
comparisonTimeRange = timeRangeMenuData.comparisonTimeRange;
|
||||
compare = comparisonTimeRange.length > 0;
|
||||
timeRangeDescription = timeRangeMenuData.timeRangeDescription;
|
||||
comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription;
|
||||
this.rendererParams.timeRangeDescription = timeRangeDescription;
|
||||
this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
|
||||
}
|
||||
|
||||
this.controllerParams = {
|
||||
modelName: params.modelName,
|
||||
actionViews: params.actionViews,
|
||||
activeActions: {
|
||||
edit: this.arch.attrs.edit ? JSON.parse(this.arch.attrs.edit) : true,
|
||||
create: this.arch.attrs.create ? JSON.parse(this.arch.attrs.create) : true,
|
||||
delete: this.arch.attrs.delete ? JSON.parse(this.arch.attrs.delete) : true,
|
||||
duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true,
|
||||
},
|
||||
groupable: this.groupable,
|
||||
enableTimeRangeMenu: this.enableTimeRangeMenu,
|
||||
isEmbedded: isEmbedded,
|
||||
controllerID: params.controllerID,
|
||||
bannerRoute: this.arch.attrs.banner_route,
|
||||
controllerID: params.controllerID,
|
||||
displayName: params.displayName,
|
||||
isEmbedded: isEmbedded,
|
||||
isMultiRecord: this.multi_record,
|
||||
modelName: params.modelName,
|
||||
viewType: this.viewType,
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
|
||||
var groupBy = params.groupBy;
|
||||
if (typeof groupBy === 'string') {
|
||||
groupBy = [groupBy];
|
||||
}
|
||||
|
||||
var controllerState = params.controllerState || {};
|
||||
var currentId = controllerState.currentId || params.currentId;
|
||||
this.loadParams = {
|
||||
context: params.context,
|
||||
count: params.count || ((this.controllerParams.ids !== undefined) &&
|
||||
this.controllerParams.ids.length) || 0,
|
||||
domain: params.domain,
|
||||
timeRange: timeRange,
|
||||
timeRangeDescription: timeRangeDescription,
|
||||
comparisonTimeRange: comparisonTimeRange,
|
||||
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
|
||||
compare: compare,
|
||||
groupedBy: groupBy,
|
||||
modelName: params.modelName,
|
||||
res_id: params.currentId,
|
||||
res_ids: params.ids,
|
||||
orderedBy: params.context ? params.context.orderedBy : [],
|
||||
res_id: currentId,
|
||||
res_ids: controllerState.resIds || params.ids || (currentId ? [currentId] : undefined),
|
||||
};
|
||||
if (params.modelName) {
|
||||
this.loadParams.modelName = params.modelName;
|
||||
}
|
||||
// default_order is like:
|
||||
// 'name,id desc'
|
||||
// but we need it like:
|
||||
@@ -197,8 +158,23 @@ var AbstractView = Class.extend({
|
||||
return {name: order[0], asc: order[1] !== 'desc'};
|
||||
});
|
||||
}
|
||||
if (params.searchQuery) {
|
||||
this._updateMVCParams(params.searchQuery);
|
||||
}
|
||||
|
||||
this.userContext = params.userContext;
|
||||
this.controlPanelParams = {
|
||||
action: action,
|
||||
activateDefaultFavorite: params.activateDefaultFavorite,
|
||||
breadcrumbs: params.breadcrumbs,
|
||||
context: this.loadParams.context,
|
||||
domain: this.loadParams.domain,
|
||||
modelName: params.modelName,
|
||||
searchMenuTypes: params.searchMenuTypes,
|
||||
state: controllerState.cpState,
|
||||
viewInfo: params.controlPanelFieldsView,
|
||||
withBreadcrumbs: params.withBreadcrumbs,
|
||||
withSearchBar: params.withSearchBar,
|
||||
};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -206,69 +182,48 @@ var AbstractView = Class.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Main method of the view class. Create a controller, and make sure that
|
||||
* data and libraries are loaded.
|
||||
*
|
||||
* There is a unusual thing going in this method with parents: we create
|
||||
* renderer/model with parent as parent, then we have to reassign them at
|
||||
* the end to make sure that we have the proper relationships. This is
|
||||
* necessary to solve the problem that the controller need the model and the
|
||||
* renderer to be instantiated, but the model need a parent to be able to
|
||||
* load itself, and the renderer needs the data in its constructor.
|
||||
*
|
||||
* @param {Widget} parent The parent of the resulting Controller (most
|
||||
* likely an action manager)
|
||||
* @returns {Deferred} The deferred resolves to a controller
|
||||
* @override
|
||||
*/
|
||||
getController: function (parent) {
|
||||
var self = this;
|
||||
// check if a model already exists, as if not, one will be created and
|
||||
// we'll have to set the controller as its parent
|
||||
var alreadyHasModel = !!this.model;
|
||||
return $.when(this._loadData(parent), ajax.loadLibs(this)).then(function () {
|
||||
var state = self.model.get(arguments[0]);
|
||||
var renderer = self.getRenderer(parent, state);
|
||||
var Controller = self.Controller || self.config.Controller;
|
||||
var controllerParams = _.extend({
|
||||
initialState: state,
|
||||
}, self.controllerParams);
|
||||
var controller = new Controller(parent, self.model, renderer, controllerParams);
|
||||
renderer.setParent(controller);
|
||||
|
||||
if (!alreadyHasModel) {
|
||||
// if we have a model, it already has a parent. Otherwise, we
|
||||
// set the controller, so the rpcs from the model actually work
|
||||
self.model.setParent(controller);
|
||||
var def;
|
||||
if (this.withControlPanel) {
|
||||
def = this._createControlPanel(parent);
|
||||
}
|
||||
var _super = this._super.bind(this);
|
||||
return $.when(def).then(function (controlPanel) {
|
||||
if (controlPanel) {
|
||||
var searchQuery = controlPanel.getSearchQuery();
|
||||
self._updateMVCParams(searchQuery);
|
||||
}
|
||||
return controller;
|
||||
// get the parent of the model if it already exists, as _super will
|
||||
// set the new controller as parent, which we don't want
|
||||
var modelParent = self.model && self.model.getParent();
|
||||
return _super(parent).done(function (controller) {
|
||||
if (controlPanel) {
|
||||
controlPanel.setParent(controller);
|
||||
}
|
||||
if (modelParent) {
|
||||
// if we already add a model, restore its parent
|
||||
self.model.setParent(modelParent);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns the view model or create an instance of it if none
|
||||
* Ensures that only one instance of AbstractModel is created
|
||||
*
|
||||
* @param {Widget} parent the parent of the model, if it has to be created
|
||||
* @return {Object} instance of the view model
|
||||
* @override
|
||||
*/
|
||||
getModel: function (parent) {
|
||||
getModel: function () {
|
||||
if (!this.model) {
|
||||
var Model = this.config.Model;
|
||||
this.model = new Model(parent);
|
||||
this.model = this._super.apply(this, arguments);
|
||||
}
|
||||
return this.model;
|
||||
},
|
||||
/**
|
||||
* Returns the a new view renderer instance
|
||||
*
|
||||
* @param {Widget} parent the parent of the model, if it has to be created
|
||||
* @param {Object} state the information related to the rendered view
|
||||
* @return {Object} instance of the view renderer
|
||||
*/
|
||||
getRenderer: function (parent, state) {
|
||||
var Renderer = this.config.Renderer;
|
||||
return new Renderer(parent, state, this.rendererParams);
|
||||
},
|
||||
/**
|
||||
* this is useful to customize the actual class to use before calling
|
||||
* This is useful to customize the actual class to use before calling
|
||||
* createView.
|
||||
*
|
||||
* @param {Controller} Controller
|
||||
@@ -282,16 +237,61 @@ var AbstractView = Class.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load initial data from the model
|
||||
* Instantiates and starts a ControlPanelController.
|
||||
*
|
||||
* @private
|
||||
* @param {Widget} parent the parent of the model
|
||||
* @returns {Deferred<*>} a deferred that resolves to whatever the model
|
||||
* decide to return
|
||||
* @param {Widget} parent
|
||||
* @returns {ControlPanelController}
|
||||
*/
|
||||
_loadData: function (parent) {
|
||||
var model = this.getModel(parent);
|
||||
return model.load(this.loadParams);
|
||||
_createControlPanel: function (parent) {
|
||||
var self = this;
|
||||
var controlPanelView = new ControlPanelView(this.controlPanelParams);
|
||||
return controlPanelView.getController(parent).then(function (controlPanel) {
|
||||
self.controllerParams.controlPanel = controlPanel;
|
||||
return controlPanel.appendTo(document.createDocumentFragment()).then(function () {
|
||||
return controlPanel;
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} [action]
|
||||
* @param {Object} [action.context || {}]
|
||||
* @param {boolean} [action.context.no_breadcrumbs=false]
|
||||
* @param {integer} [action.context.active_id]
|
||||
* @param {integer[]} [action.context.active_ids]
|
||||
* @param {Object} [action.controlPanelFieldsView]
|
||||
* @param {string} [action.display_name]
|
||||
* @param {Array[]} [action.domain=[]]
|
||||
* @param {string} [action.help]
|
||||
* @param {integer} [action.id]
|
||||
* @param {integer} [action.limit]
|
||||
* @param {string} [action.name]
|
||||
* @param {string} [action.res_model]
|
||||
* @param {string} [action.target]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_extractParamsFromAction: function (action) {
|
||||
action = action || {};
|
||||
var context = action.context || {};
|
||||
var inline = action.target === 'inline';
|
||||
return {
|
||||
actionId: action.id || false,
|
||||
actionViews: action.views || [],
|
||||
activateDefaultFavorite: !context.active_id && !context.active_ids,
|
||||
context: action.context || {},
|
||||
controlPanelFieldsView: action.controlPanelFieldsView,
|
||||
currentId: action.res_id ? action.res_id : undefined, // load returns 0
|
||||
displayName: action.display_name || action.name,
|
||||
domain: action.domain || [],
|
||||
limit: action.limit,
|
||||
modelName: action.res_model,
|
||||
noContentHelp: action.help,
|
||||
searchMenuTypes: inline ? [] : this.searchMenuTypes,
|
||||
withBreadcrumbs: 'no_breadcrumbs' in context ? !context.no_breadcrumbs : true,
|
||||
withControlPanel: this.withControlPanel,
|
||||
withSearchBar: inline ? false : this.withSearchBar,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Processes a fieldsView. In particular, parses its arch.
|
||||
@@ -303,12 +303,44 @@ var AbstractView = Class.extend({
|
||||
*/
|
||||
_processFieldsView: function (fieldsView) {
|
||||
var fv = _.extend({}, fieldsView);
|
||||
var doc = $.parseXML(fv.arch).documentElement;
|
||||
var stripWhitespaces = doc.nodeName.toLowerCase() !== 'kanban';
|
||||
fv.arch = utils.xml_to_json(doc, stripWhitespaces);
|
||||
fv.arch = viewUtils.parseArch(fv.arch);
|
||||
fv.viewFields = _.defaults({}, fv.viewFields, fv.fields);
|
||||
return fv;
|
||||
},
|
||||
/**
|
||||
* Hook to update the renderer, controller and load params with the result
|
||||
* of a search (i.e. a context, a domain and a groupBy).
|
||||
*
|
||||
* @private
|
||||
* @param {Object} searchQuery
|
||||
* @param {Object} searchQuery.context
|
||||
* @param {Object} [searchQuery.context.timeRangeMenuData={}]
|
||||
* @param {Array[]} [searchQuery.context.timeRangeMenuData.comparisonTimeRange=[]]
|
||||
* @param {string} [searchQuery.context.timeRangeMenuData.comparisonTimeRangeDescription='']
|
||||
* @param {string} [searchQuery.context.timeRangeMenuData.timeRangeDescription='']
|
||||
* @param {Array[]} [searchQuery.context.timeRangeMenuData.timeRange=[]]
|
||||
* @param {Array[]} searchQuery.domain
|
||||
* @param {string[]} searchQuery.groupBy
|
||||
*/
|
||||
_updateMVCParams: function (searchQuery) {
|
||||
var timeRangeMenuData = searchQuery.context.timeRangeMenuData || {};
|
||||
var comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
|
||||
var comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription || '';
|
||||
var timeRangeDescription = timeRangeMenuData.timeRangeDescription || '';
|
||||
this.loadParams = _.extend(this.loadParams, {
|
||||
compare: comparisonTimeRange.length > 0,
|
||||
comparisonTimeRange: comparisonTimeRange,
|
||||
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
|
||||
context: searchQuery.context,
|
||||
domain: searchQuery.domain,
|
||||
groupedBy: searchQuery.groupBy,
|
||||
timeRange: timeRangeMenuData.timeRange || [],
|
||||
timeRangeDescription: timeRangeMenuData.timeRangeDescription || '',
|
||||
});
|
||||
this.loadParams.orderedBy = searchQuery.orderedBy ? searchQuery.orderedBy : this.loadParams.orderedBy;
|
||||
this.rendererParams.timeRangeDescription = timeRangeDescription;
|
||||
this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
|
||||
},
|
||||
});
|
||||
|
||||
return AbstractView;
|
||||
|
||||
@@ -36,8 +36,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
this.confirmOnDelete = params.confirmOnDelete;
|
||||
this.hasButtons = params.hasButtons;
|
||||
FieldManagerMixin.init.call(this, this.model);
|
||||
this.handle = params.initialState.id;
|
||||
this.mode = params.mode || 'readonly';
|
||||
this.handle = this.initialState.id;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -201,6 +201,18 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
self._updatePager();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
reload: function (params) {
|
||||
if (params && params.controllerState) {
|
||||
if (params.controllerState.currentId) {
|
||||
params.currentId = params.controllerState.currentId;
|
||||
}
|
||||
params.ids = params.controllerState.resIds;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -368,6 +380,20 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
this.$buttons.find('button').removeAttr('disabled');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Override to add the current record ID (currentId) and the list of ids
|
||||
* (resIds) in the current dataPoint to the exported state.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
exportState: function () {
|
||||
var state = this._super.apply(this, arguments);
|
||||
var env = this.model.get(this.handle, {env: true});
|
||||
return _.extend(state, {
|
||||
currentId: env.currentId,
|
||||
resIds: env.ids,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns the new sidebar env
|
||||
*
|
||||
@@ -487,7 +513,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
var sidebarEnv = this._getSidebarEnv();
|
||||
this.sidebar.updateEnv(sidebarEnv);
|
||||
}
|
||||
this.trigger_up('env_updated', {controllerID: this.controllerID, env: env});
|
||||
},
|
||||
/**
|
||||
* Helper method, to make sure the information displayed by the pager is up
|
||||
|
||||
@@ -2053,14 +2053,12 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
// We don't do it now because it's not an important case.
|
||||
// However, we can for sure increment by 1 if we are on the last page.
|
||||
|
||||
var handleFieldValue = computedList.data[index].data[handleField];
|
||||
if (position === 'top') {
|
||||
handleFieldValue--;
|
||||
} else if (list.count <= list.offset + list.limit - (list.tempLimitIncrement || 0)) {
|
||||
handleFieldValue++;
|
||||
}
|
||||
|
||||
return {
|
||||
field: handleField,
|
||||
value: handleFieldValue,
|
||||
@@ -4383,7 +4381,9 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
if (options.context !== undefined) {
|
||||
element.context = options.context;
|
||||
element.orderedBy = options.context.orderedBy || element.orderedBy;
|
||||
}
|
||||
if (options.orderedBy !== undefined) {
|
||||
element.orderedBy = options.orderedBy || element.orderedBy;
|
||||
}
|
||||
if (options.domain !== undefined) {
|
||||
element.domain = options.domain;
|
||||
|
||||
@@ -517,35 +517,6 @@ var BasicRenderer = AbstractRenderer.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Renders a button according to a given node element.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} node
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.extraClass]
|
||||
* @param {boolean} [options.textAsTitle=false]
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
_renderButtonFromNode: function (node, options) {
|
||||
var btnOptions = {
|
||||
attrs: _.omit(node.attrs, 'icon', 'string', 'type', 'attrs', 'modifiers', 'options'),
|
||||
icon: node.attrs.icon,
|
||||
};
|
||||
if (options && options.extraClass) {
|
||||
var classes = btnOptions.attrs.class ? btnOptions.attrs.class.split(' ') : [];
|
||||
btnOptions.attrs.class = _.uniq(classes.concat(options.extraClass.split(' '))).join(' ');
|
||||
}
|
||||
var str = (node.attrs.string || '').replace(/_/g, '');
|
||||
if (str) {
|
||||
if (options && options.textAsTitle) {
|
||||
btnOptions.attrs.title = str;
|
||||
} else {
|
||||
btnOptions.text = str;
|
||||
}
|
||||
}
|
||||
return dom.renderButton(btnOptions);
|
||||
},
|
||||
/**
|
||||
* Instantiates the appropriate AbstractField specialization for the given
|
||||
* node and prepares its rendering and addition to the DOM. Indeed, the
|
||||
|
||||
@@ -51,7 +51,6 @@ var BasicView = AbstractView.extend({
|
||||
|
||||
this.loadParams.fieldsInfo = this.fieldsInfo;
|
||||
this.loadParams.fields = this.fields;
|
||||
this.loadParams.context = params.context || {};
|
||||
this.loadParams.limit = parseInt(this.arch.attrs.limit, 10) || params.limit;
|
||||
this.loadParams.viewType = this.viewType;
|
||||
this.loadParams.parentID = params.parentID;
|
||||
@@ -96,19 +95,17 @@ var BasicView = AbstractView.extend({
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_loadData: function () {
|
||||
_loadData: function (model) {
|
||||
if (this.recordID) {
|
||||
var self = this;
|
||||
|
||||
// Add the fieldsInfo of the current view to the given recordID,
|
||||
// as it will be shared between two views, and it must be able to
|
||||
// handle changes on fields that are only on this view.
|
||||
this.model.addFieldsInfo(this.recordID, {
|
||||
model.addFieldsInfo(this.recordID, {
|
||||
fields: this.fields,
|
||||
fieldsInfo: this.fieldsInfo,
|
||||
});
|
||||
|
||||
var record = this.model.get(this.recordID);
|
||||
var record = model.get(this.recordID);
|
||||
var viewType = this.viewType;
|
||||
var viewFields = Object.keys(record.fieldsInfo[viewType]);
|
||||
var fieldNames = _.difference(viewFields, Object.keys(record.data));
|
||||
@@ -168,14 +165,14 @@ var BasicView = AbstractView.extend({
|
||||
// onchange RPCs), that we haven't been able to process earlier
|
||||
// (because those fields were unknow at that time). So we ask
|
||||
// the model to process them.
|
||||
def = this.model.applyRawChanges(record.id, viewType).then(function () {
|
||||
if (self.model.isNew(record.id)) {
|
||||
return self.model.applyDefaultValues(record.id, {}, {
|
||||
def = model.applyRawChanges(record.id, viewType).then(function () {
|
||||
if (model.isNew(record.id)) {
|
||||
return model.applyDefaultValues(record.id, {}, {
|
||||
fieldNames: fieldNames,
|
||||
viewType: viewType,
|
||||
});
|
||||
} else {
|
||||
return self.model.reload(record.id, {
|
||||
return model.reload(record.id, {
|
||||
fieldNames: fieldNames,
|
||||
keepChanges: true,
|
||||
viewType: viewType,
|
||||
@@ -184,7 +181,7 @@ var BasicView = AbstractView.extend({
|
||||
});
|
||||
}
|
||||
return $.when(def).then(function () {
|
||||
return record.id;
|
||||
return model.get(record.id);
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
|
||||
@@ -75,7 +75,7 @@ var CalendarController = AbstractController.extend({
|
||||
* @returns {string}
|
||||
*/
|
||||
getTitle: function () {
|
||||
return this.get('title');
|
||||
return this._title;
|
||||
},
|
||||
/**
|
||||
* Render the buttons according to the CalendarView.buttons template and
|
||||
@@ -156,7 +156,7 @@ var CalendarController = AbstractController.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_updateRecord: function (record) {
|
||||
return this.model.updateRecord(record).then(this.reload.bind(this));
|
||||
return this.model.updateRecord(record).then(this.reload.bind(this, {}));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -430,7 +430,7 @@ var CalendarController = AbstractController.extend({
|
||||
this.$buttons.find('.active').removeClass('active');
|
||||
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
|
||||
}
|
||||
this.set({title: this.displayName + ' (' + event.data.title + ')'});
|
||||
this._setTitle(this.displayName + ' (' + event.data.title + ')');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ var CalendarView = AbstractView.extend({
|
||||
Renderer: CalendarRenderer,
|
||||
},
|
||||
viewType: 'calendar',
|
||||
groupable: false,
|
||||
searchMenuTypes: ['filter', 'favorite'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
odoo.define('web.ControlPanelController', function (require) {
|
||||
"use strict";
|
||||
|
||||
var mvc = require('web.mvc');
|
||||
|
||||
var ControlPanelController = mvc.Controller.extend({
|
||||
className: 'o_cp_controller',
|
||||
custom_events: {
|
||||
facet_removed: '_onFacetRemoved',
|
||||
get_search_query: '_onGetSearchQuery',
|
||||
item_option_clicked: '_onItemOptionClicked',
|
||||
item_trashed: '_onItemTrashed',
|
||||
menu_item_clicked: '_onMenuItemClicked',
|
||||
new_favorite: '_onNewFavorite',
|
||||
new_filters: '_onNewFilters',
|
||||
new_groupBy: '_onNewGroupBy',
|
||||
activate_time_range: '_onActivateTimeRange',
|
||||
autocompletion_filter: '_onAutoCompletionFilter',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {string} params.modelName
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.modelName = params.modelName;
|
||||
},
|
||||
/**
|
||||
* Called when the control panel is inserted into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this.renderer.on_attach_callback();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see ControlPanelModel (exportState)
|
||||
* @returns {Object}
|
||||
*/
|
||||
exportState: function () {
|
||||
return this.model.exportState();
|
||||
},
|
||||
/**
|
||||
* Compute the search related values that will be used to fetch data.
|
||||
*
|
||||
* @returns {Object} object with keys 'context', 'domain', 'groupBy'
|
||||
*/
|
||||
getSearchQuery: function () {
|
||||
return this.model.getQuery();
|
||||
},
|
||||
/**
|
||||
* @param {Object} state a ControlPanelModel state
|
||||
* @returns {Deferred<Object>} the result of `getSearchState`
|
||||
*/
|
||||
importState: function (state) {
|
||||
var defs = [];
|
||||
this.model.importState(state);
|
||||
defs.push(this.getSearchQuery());
|
||||
defs.push(this.renderer.updateState(this.model.get()));
|
||||
return $.when(defs).then(function (defsResults) {
|
||||
return defsResults[0];
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Called at each switch view. This is required until the control panel is
|
||||
* shared between controllers of an action.
|
||||
*
|
||||
* @param {string} controllerID
|
||||
*/
|
||||
setControllerID: function (controllerID) {
|
||||
this.controllerID = controllerID;
|
||||
},
|
||||
/**
|
||||
* Update the content and displays the ControlPanel.
|
||||
*
|
||||
* @see ControlPanelRenderer (updateContents)
|
||||
* @param {Object} status
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
updateContents: function (status, options) {
|
||||
this.renderer.updateContents(status, options);
|
||||
},
|
||||
/**
|
||||
* Update the domain of the search view by adding and/or removing filters.
|
||||
*
|
||||
* @todo: the way it is done could be improved, but the actual state of the
|
||||
* searchview doesn't allow to do much better.
|
||||
|
||||
* @param {Object[]} newFilters list of filters to add, described by
|
||||
* objects with keys domain (the domain as an Array), description (the text
|
||||
* to display in the facet) and type with value 'filter'.
|
||||
* @param {string[]} filtersToRemove list of filter ids to remove
|
||||
* (previously added ones)
|
||||
* @returns {string[]} list of added filters (to pass as filtersToRemove
|
||||
* for a further call to this function)
|
||||
*/
|
||||
updateFilters: function (newFilters, filtersToRemove) {
|
||||
var newFilterIDS = this.model.createNewFilters(newFilters);
|
||||
this.model.deactivateFilters(filtersToRemove);
|
||||
this._reportNewQueryAndRender();
|
||||
return newFilterIDS;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
_getSubMenus: function () {
|
||||
return this.renderer.$subMenus;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_reportNewQueryAndRender: function () {
|
||||
this.trigger_up('search', this.model.getQuery());
|
||||
var state = this.model.get();
|
||||
return this.renderer.updateState(state);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onActivateTimeRange: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.activateTimeRange(
|
||||
ev.data.id,
|
||||
ev.data.timeRangeId,
|
||||
ev.data.comparisonTimeRangeId
|
||||
);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onAutoCompletionFilter: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.toggleAutoCompletionFilter(ev.data);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onFacetRemoved: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var group = ev.data.group;
|
||||
if (!group) {
|
||||
group = this.renderer.getLastFacet();
|
||||
}
|
||||
this.model.deactivateGroup(group.id);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onGetSearchQuery: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var query = this.getSearchQuery();
|
||||
ev.data.callback(query);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onItemOptionClicked: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.toggleFilterWithOptions(ev.data.id, ev.data.optionId);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onItemTrashed: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var def = this.model.deleteFilterEverywhere(ev.data.id);
|
||||
def.then(this._reportNewQueryAndRender.bind(this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onMenuItemClicked: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.toggleFilter(ev.data.id);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNewFavorite: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var def = this.model.createNewFavorite(ev.data);
|
||||
def.then(this._reportNewQueryAndRender.bind(this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNewFilters: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.createNewFilters(ev.data.filters);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNewGroupBy: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.model.createNewGroupBy(ev.data);
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
});
|
||||
|
||||
return ControlPanelController;
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
odoo.define('web.ControlPanelRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var data = require('web.data');
|
||||
var FavoriteMenu = require('web.FavoriteMenu');
|
||||
var FilterMenu = require('web.FilterMenu');
|
||||
var GroupByMenu = require('web.GroupByMenu');
|
||||
var mvc = require('web.mvc');
|
||||
var SearchBar = require('web.SearchBar');
|
||||
var TimeRangeMenu = require('web.TimeRangeMenu');
|
||||
|
||||
var Renderer = mvc.Renderer;
|
||||
|
||||
var ControlPanelRenderer = Renderer.extend({
|
||||
template: 'ControlPanel',
|
||||
custom_events: {
|
||||
get_action_info: '_onGetActionInfo',
|
||||
},
|
||||
events: _.extend({}, Renderer.prototype.events, {
|
||||
'click.bs.dropdown .o_search_options .dropdown-menu': '_onDropdownClicked',
|
||||
'click .o_searchview_more': '_onMore',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} [params.action] current action if any
|
||||
* @param {Object} [params.context]
|
||||
* @param {Object[]} [params.breadcrumbs=[]] list of breadcrumbs elements
|
||||
* @param {boolean} [params.withBreadcrumbs=false] if false, breadcrumbs
|
||||
* won't be rendered
|
||||
* @param {boolean} [params.withSearchBar=false] if false, no search bar
|
||||
* is rendered
|
||||
* @param {string[]} [params.searchMenuTypes=[]] determines the search menus
|
||||
* that are displayed.
|
||||
* @param {String} [params.template] the QWeb template to render the
|
||||
* ControlPanel. By default, the template 'ControlPanel' will be used.
|
||||
* @param {string} [params.title=''] the title visible in control panel
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this._breadcrumbs = params.breadcrumbs || [];
|
||||
this._title = params.title || '';
|
||||
this.withBreadcrumbs = params.withBreadcrumbs;
|
||||
this.withSearchBar = params.withSearchBar;
|
||||
if (params.template) {
|
||||
this.template = params.template;
|
||||
}
|
||||
this.context = params.context;
|
||||
|
||||
this.$subMenus = null;
|
||||
this.action = params.action;
|
||||
this.displaySearchMenu = true;
|
||||
this.menusSetup = false;
|
||||
this.searchMenuTypes = params.searchMenuTypes || [];
|
||||
this.subMenus = {};
|
||||
},
|
||||
/**
|
||||
* Render the control panel and create a dictionnary of its exposed elements.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
// exposed jQuery nodesets
|
||||
this.nodes = {
|
||||
$buttons: this.$('.o_cp_buttons'),
|
||||
$pager: this.$('.o_cp_pager'),
|
||||
$sidebar: this.$('.o_cp_sidebar'),
|
||||
$switch_buttons: this.$('.o_cp_switch_buttons'),
|
||||
};
|
||||
|
||||
// if we don't use the default search bar and buttons, we expose the
|
||||
// corresponding areas for custom content
|
||||
if (!this.withSearchBar) {
|
||||
this.nodes.$searchview = this.$('.o_cp_searchview');
|
||||
}
|
||||
if (this.searchMenuTypes.length === 0) {
|
||||
this.nodes.$searchview_buttons = this.$('.o_search_options');
|
||||
}
|
||||
|
||||
if (this.withBreadcrumbs) {
|
||||
this._renderBreadcrumbs();
|
||||
}
|
||||
|
||||
var superDef = this._super.apply(this, arguments);
|
||||
var searchDef = this._renderSearch();
|
||||
return $.when(superDef, searchDef).then(function () {
|
||||
self._setSearchMenusVisibility();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._focusSearchInput();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
getLastFacet: function () {
|
||||
return this.state.facets.slice(-1)[0];
|
||||
},
|
||||
/**
|
||||
* This function is called when actions call 'updateControlPanel' with
|
||||
* custom contents to insert in the exposed areas.
|
||||
*
|
||||
* @param {Object} status
|
||||
* @param {Object} [status.cp_content] dictionnary containing the jQuery
|
||||
* elements to insert in the exposed areas
|
||||
* @param {string} [status.breadcrumbs] the breadcrumbs to display before
|
||||
* the current controller
|
||||
* @param {string} [status.title] the title of the current controller, to
|
||||
* display at the end of the breadcrumbs
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.clear=true] set to false to keep control panel
|
||||
* elements that are not in status.cp_content (useful for partial updates)
|
||||
*/
|
||||
updateContents: function (status, options) {
|
||||
var new_cp_content = status.cp_content || {};
|
||||
var clear = 'clear' in (options || {}) ? options.clear : true;
|
||||
|
||||
if (this.withBreadcrumbs) {
|
||||
this._breadcrumbs = status.breadcrumbs || this._breadcrumbs;
|
||||
this._title = status.title || this._title;
|
||||
this._renderBreadcrumbs();
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
this._detachContent(this.nodes);
|
||||
} else {
|
||||
this._detachContent(_.pick(this.nodes, _.keys(new_cp_content)));
|
||||
}
|
||||
this._attachContent(new_cp_content);
|
||||
},
|
||||
/**
|
||||
* Update the state of the renderer state. It retriggers a full rerendering.
|
||||
*
|
||||
* @param {Object} state
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
updateState: function (state) {
|
||||
this.state = state;
|
||||
return this._renderSearch();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} content dictionnary of jQuery elements to attach, whose
|
||||
* keys are jQuery nodes identifiers in this.nodes
|
||||
*/
|
||||
_attachContent: function (content) {
|
||||
for (var $element in content) {
|
||||
var $nodeset = content[$element];
|
||||
if ($nodeset && this.nodes[$element]) {
|
||||
this.nodes[$element].append($nodeset);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} content subset of this.nodes to detach
|
||||
*/
|
||||
_detachContent: function (content) {
|
||||
for (var $element in content) {
|
||||
content[$element].contents().detach();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_focusSearchInput: function () {
|
||||
if (this.withSearchBar && !config.device.isMobile) {
|
||||
// in mobile mode, we would rather not focus manually the
|
||||
// input, because it opens up the integrated keyboard, which is
|
||||
// not what you expect when you just selected a filter.
|
||||
this.searchBar.focus();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} menuType
|
||||
* @returns {Objects[]} menuItems
|
||||
*/
|
||||
_getMenuItems: function (menuType) {
|
||||
var menuItems;
|
||||
if (menuType === 'filter') {
|
||||
menuItems = this.state.filters;
|
||||
}
|
||||
if (menuType === 'groupBy') {
|
||||
menuItems = this.state.groupBys;
|
||||
}
|
||||
if (menuType === 'timeRange') {
|
||||
menuItems = this.state.timeRanges;
|
||||
}
|
||||
if (menuType === 'favorite') {
|
||||
menuItems = this.state.favorites;
|
||||
}
|
||||
return menuItems;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_getSubMenusPlace: function () {
|
||||
return $('<div>').appendTo(this.$('.o_search_options'));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderBreadcrumbs: function () {
|
||||
var self = this;
|
||||
var breadcrumbsDescriptors = this._breadcrumbs.concat({title: this._title});
|
||||
var breadcrumbs = breadcrumbsDescriptors.map(function (bc, index) {
|
||||
return self._renderBreadcrumbsItem(bc, index, breadcrumbsDescriptors.length);
|
||||
});
|
||||
this.$('.breadcrumb').html(breadcrumbs);
|
||||
},
|
||||
/**
|
||||
* Render a breadcrumbs' li jQuery element.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} bc
|
||||
* @param {string} bc.title
|
||||
* @param {string} bc.controllerID
|
||||
* @param {integer} index
|
||||
* @param {integer} length
|
||||
* @returns {jQueryElement} $bc
|
||||
*/
|
||||
_renderBreadcrumbsItem: function (bc, index, length) {
|
||||
var self = this;
|
||||
var is_last = (index === length-1);
|
||||
var li_content = bc.title && _.escape(bc.title.trim()) || data.noDisplayContent;
|
||||
var $bc = $('<li>', {class: 'breadcrumb-item'})
|
||||
.append(is_last ? li_content : $('<a>', {href: '#'}).html(li_content))
|
||||
.toggleClass('active', is_last);
|
||||
if (!is_last) {
|
||||
$bc.click(function (ev) {
|
||||
ev.preventDefault();
|
||||
self.trigger_up('breadcrumb_clicked', {controllerID: bc.controllerID});
|
||||
});
|
||||
}
|
||||
return $bc;
|
||||
},
|
||||
/**
|
||||
* Renderer the search bar and the search menus
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_renderSearch: function () {
|
||||
var defs = [];
|
||||
if (this.menusSetup) {
|
||||
this._updateMenus();
|
||||
} else {
|
||||
this.menusSetup = true;
|
||||
defs = defs.concat(this._setupMenus());
|
||||
}
|
||||
if (this.withSearchBar) {
|
||||
defs.push(this._renderSearchBar());
|
||||
}
|
||||
return $.when(this, defs).then(this._focusSearchInput.bind(this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_renderSearchBar: function () {
|
||||
// TODO: might need a reload instead of a destroy/instantiate
|
||||
var oldSearchBar = this.searchBar;
|
||||
this.searchBar = new SearchBar(this, {
|
||||
context: this.context,
|
||||
facets: this.state.facets,
|
||||
fields: this.state.fields,
|
||||
filters: this.state.filters,
|
||||
filterFields: this.state.filterFields,
|
||||
groupBys: this.state.groupBys,
|
||||
});
|
||||
return this.searchBar.appendTo(this.$('.o_searchview')).then(function () {
|
||||
if (oldSearchBar) {
|
||||
oldSearchBar.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Hide or show the search menus according to this.displaySearchMenu.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setSearchMenusVisibility: function () {
|
||||
this.$('.o_searchview_more')
|
||||
.toggleClass('fa-search-plus', !this.displaySearchMenu)
|
||||
.toggleClass('fa-search-minus', this.displaySearchMenu);
|
||||
this.$('.o_search_options')
|
||||
.toggleClass('o_hidden', !this.displaySearchMenu);
|
||||
},
|
||||
/**
|
||||
* Create a new menu of the given type and append it to this.$subMenus.
|
||||
* This menu is also added to this.subMenus.
|
||||
*
|
||||
* @private
|
||||
* @param {string} menuType
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_setupMenu: function (menuType) {
|
||||
var Menu;
|
||||
var menu;
|
||||
if (menuType === 'filter') {
|
||||
Menu = FilterMenu;
|
||||
}
|
||||
if (menuType === 'groupBy') {
|
||||
Menu = GroupByMenu;
|
||||
}
|
||||
if (menuType === 'timeRange') {
|
||||
Menu = TimeRangeMenu;
|
||||
}
|
||||
if (menuType === 'favorite') {
|
||||
Menu = FavoriteMenu;
|
||||
}
|
||||
if (_.contains(['filter', 'groupBy', 'timeRange'], menuType)) {
|
||||
menu = new Menu(this, this._getMenuItems(menuType), this.state.fields);
|
||||
}
|
||||
if (menuType === 'favorite') {
|
||||
menu = new Menu(this, this._getMenuItems(menuType), this.action);
|
||||
}
|
||||
this.subMenus[menuType] = menu;
|
||||
return menu.appendTo(this.$subMenus);
|
||||
},
|
||||
/**
|
||||
* Instantiate the search menu determined by this.searchMenuTypes.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred[]}
|
||||
*/
|
||||
_setupMenus: function () {
|
||||
this.$subMenus = this._getSubMenusPlace();
|
||||
return this.searchMenuTypes.map(this._setupMenu.bind(this));
|
||||
},
|
||||
/**
|
||||
* Update the search menus.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateMenus: function () {
|
||||
var self = this;
|
||||
this.searchMenuTypes.forEach(function (menuType) {
|
||||
self.subMenus[menuType].update(self._getMenuItems(menuType));
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Prevent the search dropdowns from closing when clicking inside them.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onDropdownClicked: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
/**
|
||||
* Toggle the search menus visibility.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onMore: function () {
|
||||
this.displaySearchMenu = !this.displaySearchMenu;
|
||||
this._setSearchMenusVisibility();
|
||||
},
|
||||
});
|
||||
|
||||
return ControlPanelRenderer;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
odoo.define('web.ControlPanelView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ControlPanelController = require('web.ControlPanelController');
|
||||
var ControlPanelModel = require('web.ControlPanelModel');
|
||||
var ControlPanelRenderer = require('web.ControlPanelRenderer');
|
||||
var controlPanelViewParameters = require('web.controlPanelViewParameters');
|
||||
var mvc = require('web.mvc');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var DEFAULT_INTERVAL = controlPanelViewParameters.DEFAULT_INTERVAL;
|
||||
var DEFAULT_PERIOD = controlPanelViewParameters.DEFAULT_PERIOD;
|
||||
var INTERVAL_OPTIONS = controlPanelViewParameters.INTERVAL_OPTIONS;
|
||||
var PERIOD_OPTIONS = controlPanelViewParameters.PERIOD_OPTIONS;
|
||||
|
||||
var Factory = mvc.Factory;
|
||||
|
||||
var ControlPanelView = Factory.extend({
|
||||
config: _.extend({}, Factory.prototype.config, {
|
||||
Controller: ControlPanelController,
|
||||
Model: ControlPanelModel,
|
||||
Renderer: ControlPanelRenderer,
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} [params={}]
|
||||
* @param {Object} [params.action={}]
|
||||
* @param {Object} [params.context={}]
|
||||
* @param {string} [params.domain=[]]
|
||||
* @param {string} [params.modelName]
|
||||
* @param {string[]} [params.searchMenuTypes=[]]
|
||||
* determines search menus displayed.
|
||||
* @param {Object} [params.state] used to determine the control panel model
|
||||
* essential content at load. For instance, state can be the state of an
|
||||
* other control panel model that we want to use.
|
||||
* @param {string} [params.template] the QWeb template to render
|
||||
* @param {Object} [params.viewInfo={arch: '<search/>', fields: {}}] a
|
||||
* search fieldsview
|
||||
* @param {string} [params.viewInfo.arch]
|
||||
* @param {boolean} [params.withBreadcrumbs=true] if set to false,
|
||||
* breadcrumbs won't be rendered
|
||||
* @param {boolean} [params.withSearchBar=true] if set to false, no default
|
||||
* search bar will be rendered
|
||||
* @param {boolean} [activateDefaultFavorite=false] determine if the default
|
||||
* custom filters can be activated
|
||||
*/
|
||||
init: function (params) {
|
||||
var self = this;
|
||||
this._super();
|
||||
params = params || {};
|
||||
var viewInfo = params.viewInfo || {arch: '<search/>', fields: {}};
|
||||
var context = _.extend({}, params.context);
|
||||
var domain = params.domain || [];
|
||||
var action = params.action || {};
|
||||
|
||||
this.searchDefaults = {};
|
||||
Object.keys(context).forEach(function (key) {
|
||||
var match = /^search_default_(.*)$/.exec(key);
|
||||
if (match) {
|
||||
self.searchDefaults[match[1]] = context[key];
|
||||
delete context[key];
|
||||
}
|
||||
});
|
||||
|
||||
this.arch = viewUtils.parseArch(viewInfo.arch);
|
||||
this.fields = viewInfo.fields;
|
||||
|
||||
this.controllerParams.modelName = params.modelName;
|
||||
|
||||
this.modelParams.context = context;
|
||||
this.modelParams.domain = domain;
|
||||
this.modelParams.modelName = params.modelName;
|
||||
this.modelParams.actionId = action.id;
|
||||
this.modelParams.fields = this.fields;
|
||||
|
||||
this.rendererParams.action = action;
|
||||
this.rendererParams.breadcrumbs = params.breadcrumbs;
|
||||
this.rendererParams.context = context;
|
||||
this.rendererParams.searchMenuTypes = params.searchMenuTypes || [];
|
||||
this.rendererParams.template = params.template;
|
||||
this.rendererParams.withBreadcrumbs = params.withBreadcrumbs !== false;
|
||||
this.rendererParams.withSearchBar = 'withSearchBar' in params ? params.withSearchBar : true;
|
||||
|
||||
this.loadParams.withSearchBar = 'withSearchBar' in params ? params.withSearchBar : true;
|
||||
this.loadParams.searchMenuTypes = params.searchMenuTypes || [];
|
||||
this.loadParams.activateDefaultFavorite = params.activateDefaultFavorite;
|
||||
if (this.loadParams.withSearchBar) {
|
||||
if (params.state) {
|
||||
this.loadParams.initialState = params.state;
|
||||
} else {
|
||||
// groups are determined in _parseSearchArch
|
||||
this.loadParams.groups = [];
|
||||
this.loadParams.timeRanges = context.time_ranges;
|
||||
this._parseSearchArch(this.arch);
|
||||
}
|
||||
}
|
||||
|
||||
PERIOD_OPTIONS = PERIOD_OPTIONS.map(function (option) {
|
||||
return _.extend(option, {description: option.description.toString()});
|
||||
});
|
||||
INTERVAL_OPTIONS = INTERVAL_OPTIONS.map(function (option) {
|
||||
return _.extend(option, {description: option.description.toString()});
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} child parsed arch node
|
||||
* @returns {Object}
|
||||
*/
|
||||
_evalArchChild: function (child) {
|
||||
if (child.attrs.context) {
|
||||
try {
|
||||
var context = pyUtils.eval('context', child.attrs.context);
|
||||
if (context.group_by) {
|
||||
// let us extract basic data since we just evaluated context
|
||||
// and use a correct tag!
|
||||
child.tag = 'groupBy';
|
||||
child.attrs.fieldName = context.group_by.split(':')[0];
|
||||
child.attrs.defaultInterval = context.group_by.split(':')[1];
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return child;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} filter
|
||||
* @param {Object} attrs
|
||||
*/
|
||||
_extractAttributes: function (filter, attrs) {
|
||||
filter.isDefault = this.searchDefaults[attrs.name] ? true : false;
|
||||
filter.description = attrs.string ||
|
||||
attrs.help ||
|
||||
attrs.name ||
|
||||
attrs.domain ||
|
||||
'Ω';
|
||||
if (filter.type === 'filter') {
|
||||
filter.domain = attrs.domain;
|
||||
if (attrs.date) {
|
||||
filter.fieldName = attrs.date;
|
||||
filter.fieldType = this.fields[attrs.date].type;
|
||||
// we should be able to declare list of options per date filter
|
||||
// (request of POs) (same remark for groupbys)
|
||||
filter.hasOptions = true;
|
||||
filter.options = PERIOD_OPTIONS;
|
||||
filter.defaultOptionId = attrs.default_period ||
|
||||
DEFAULT_PERIOD;
|
||||
filter.currentOptionId = false;
|
||||
}
|
||||
} else if (filter.type === 'groupBy') {
|
||||
filter.fieldName = attrs.fieldName;
|
||||
filter.fieldType = this.fields[attrs.fieldName].type;
|
||||
if (_.contains(['date', 'datetime'], filter.fieldType)) {
|
||||
filter.hasOptions = true;
|
||||
filter.options = INTERVAL_OPTIONS;
|
||||
filter.defaultOptionId = attrs.defaultInterval ||
|
||||
DEFAULT_INTERVAL;
|
||||
filter.currentOptionId = false;
|
||||
}
|
||||
} else if (filter.type === 'field') {
|
||||
var field = this.fields[attrs.name];
|
||||
filter.attrs = attrs;
|
||||
filter.autoCompleteValues = [];
|
||||
if (filter.isDefault) {
|
||||
// on field, default can be used with a value
|
||||
filter.defaultValue = this.searchDefaults[filter.attrs.name];
|
||||
}
|
||||
if (!attrs.string) {
|
||||
attrs.string = field.string;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Parse the arch of a 'search' view.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} arch arch with root node <search>
|
||||
*/
|
||||
_parseSearchArch: function (arch) {
|
||||
var self = this;
|
||||
var preFilters = _.flatten(arch.children.map(function (child) {
|
||||
return child.tag !== 'group' ?
|
||||
self._evalArchChild(child) :
|
||||
child.children.map(self._evalArchChild);
|
||||
}));
|
||||
preFilters.push({tag: 'separator'});
|
||||
|
||||
var filter;
|
||||
var currentTag;
|
||||
var currentGroup = [];
|
||||
var groupOfGroupBys = [];
|
||||
var groupNumber = 1;
|
||||
|
||||
_.each(preFilters, function (preFilter) {
|
||||
if (preFilter.tag !== currentTag || _.contains(['separator', 'field'], preFilter.tag)) {
|
||||
if (currentGroup.length) {
|
||||
if (currentTag === 'groupBy') {
|
||||
groupOfGroupBys = groupOfGroupBys.concat(currentGroup);
|
||||
} else {
|
||||
self.loadParams.groups.push(currentGroup);
|
||||
}
|
||||
}
|
||||
currentTag = preFilter.tag;
|
||||
currentGroup = [];
|
||||
groupNumber++;
|
||||
}
|
||||
if (preFilter.tag !== 'separator') {
|
||||
filter = {
|
||||
type: preFilter.tag,
|
||||
// we need to codify here what we want to keep from attrs
|
||||
// and how, for now I put everything.
|
||||
// In some sence, some filter are active (totally determined, given)
|
||||
// and others are passive (require input(s) to become determined)
|
||||
// What is the right place to process the attrs?
|
||||
};
|
||||
if (filter.type === 'filter' || filter.type === 'groupBy') {
|
||||
filter.groupNumber = groupNumber;
|
||||
}
|
||||
self._extractAttributes(filter, preFilter.attrs);
|
||||
currentGroup.push(filter);
|
||||
}
|
||||
});
|
||||
if (groupOfGroupBys.length) {
|
||||
this.loadParams.groups.push(groupOfGroupBys);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return ControlPanelView;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
odoo.define('web.MobileControlPanelController', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var ControlPanelController = require('web.ControlPanelController');
|
||||
|
||||
if (!config.device.isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
ControlPanelController.include({
|
||||
custom_events:_.extend({}, ControlPanelController.prototype.custom_events, {
|
||||
'search_bar_cleared': '_onSearchBarCleared',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* There is a 'Clear' button in the mobile control panel view.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSearchBarCleared: function () {
|
||||
var self = this;
|
||||
this.model.get().query.forEach(function (groupId) {
|
||||
self.model.deactivateGroup(groupId);
|
||||
});
|
||||
this._reportNewQueryAndRender();
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
+37
-34
@@ -1,16 +1,16 @@
|
||||
odoo.define('web.SearchViewMobile', function (require) {
|
||||
odoo.define('web.MobileControlPanelRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var SearchView = require('web.SearchView');
|
||||
var ControlPanelRenderer = require('web.ControlPanelRenderer');
|
||||
|
||||
if (!config.device.isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
SearchView.include({
|
||||
template:'SearchViewMobile',
|
||||
events:_.extend({}, SearchView.prototype.events, {
|
||||
ControlPanelRenderer.include({
|
||||
template:'MobileControlPanel',
|
||||
events:_.extend({}, ControlPanelRenderer.prototype.events, {
|
||||
'click .o_mobile_search_close, .o_mobile_search_show_result, .o_enable_searchview': '_toggleMobileSearchView',
|
||||
'click': '_onOpenMobileSearchView',
|
||||
'click .o_mobile_search_clear_facets': '_onEmptyAll',
|
||||
@@ -18,43 +18,47 @@ SearchView.include({
|
||||
'hide.bs.dropdown .o_mobile_search_filter .o_dropdown': '_onDropdownToggle',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
renderFacets: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$('.o_mobile_search_clear_facets')
|
||||
.toggleClass('o_hidden', !this.query.length);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
toggle_visibility: function (is_visible) {
|
||||
// Do not do anything, toggling visibility of searchview is handled
|
||||
// explicitly for mobile
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Overridden to correctly place submenu inside the mobile section.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_getButtonsElement: function () {
|
||||
_getSubMenusPlace: function () {
|
||||
return this.$('.o_mobile_search_filter');
|
||||
},
|
||||
/**
|
||||
* Toggle mobile search view screen
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_render: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.$('.o_mobile_search_clear_facets')
|
||||
.toggleClass('o_hidden', !self.state.query.length);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_renderSearchBar: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.searchBar.$el.detach().insertAfter(self.$('.o_mobile_search_header'));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Toggles mobile search view screen.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleMobileSearchView: function () {
|
||||
this.$('.o_enable_searchview').toggleClass('btn-secondary', !!this.query.length);
|
||||
this.$('.o_enable_searchview').toggleClass('btn-secondary', !!this.state.query.length);
|
||||
this.$('.o_mobile_search').toggleClass('o_hidden');
|
||||
},
|
||||
|
||||
@@ -63,7 +67,7 @@ SearchView.include({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle the little arrow in main buttons
|
||||
* Toggles the little arrow in main buttons.
|
||||
*
|
||||
* @private
|
||||
* @param {BootstrapEvent} ev
|
||||
@@ -72,16 +76,15 @@ SearchView.include({
|
||||
$(ev.currentTarget).find('.fa-chevron-right').toggleClass('fa-chevron-down');
|
||||
},
|
||||
/**
|
||||
* Clears all filters from the search view
|
||||
* Clears all filters from the search view.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onEmptyAll: function () {
|
||||
this.query.reset();
|
||||
this.trigger_up('search_bar_cleared');
|
||||
},
|
||||
/**
|
||||
* Open the mobile search view screen
|
||||
* Opens the mobile search view screen.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
@@ -93,4 +96,4 @@ SearchView.include({
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
odoo.define('web.AddNewFavoriteMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var favoritesSubmenusRegistry = require('web.favorites_submenus_registry');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var AddNewFavoriteMenu = Widget.extend({
|
||||
template: 'AddNewFavoriteMenu',
|
||||
events: _.extend({}, Widget.prototype.events, {
|
||||
'click .o_save_favorite': '_onSaveFavoriteClick',
|
||||
'click .o_add_favorite.o_menu_header': '_onMenuHeaderClick',
|
||||
'keyup .o_save_name input': '_onKeyUp',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.action
|
||||
* @param {Object} params.favorites
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this._super(parent);
|
||||
this.favorites = params.favorites;
|
||||
this.action = params.action;
|
||||
this.isOpen = false;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._render();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the menu and re-render the widget.
|
||||
*/
|
||||
closeMenu: function () {
|
||||
this.isOpen = false;
|
||||
this._render();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
this.renderElement();
|
||||
if (this.isOpen) {
|
||||
var $input = this.$('.o_favorite_name input.o_input');
|
||||
$input.val(this.action.name);
|
||||
$input.focus();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_saveFavorite: function () {
|
||||
var self = this;
|
||||
var $inputs = this.$('input');
|
||||
var description = $inputs[0].value;
|
||||
var isDefault = $inputs[1].checked;
|
||||
var isShared = $inputs[2].checked;
|
||||
|
||||
if (!description.length){
|
||||
this.do_warn(_t("Error"), _t("A name for your favorite is required."));
|
||||
$inputs[0].focus();
|
||||
return;
|
||||
}
|
||||
var descriptionAlreadyExists = !!this.favorites.find(function (favorite) {
|
||||
return favorite.description === description;
|
||||
});
|
||||
if (descriptionAlreadyExists) {
|
||||
this.do_warn(_t("Error"), _t("Filter with same name already exists."));
|
||||
$inputs[0].focus();
|
||||
return;
|
||||
}
|
||||
this.trigger_up('new_favorite', {
|
||||
type: 'favorite',
|
||||
description: description,
|
||||
isDefault: isDefault,
|
||||
isShared: isShared,
|
||||
on_success: function () {self.generatorMenuIsOpen = false;},
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
/**
|
||||
* Hide and display the submenu which allows adding custom filters.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleMenu: function () {
|
||||
this.isOpen = !this.isOpen;
|
||||
this._render();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onKeyUp: function (ev) {
|
||||
if (ev.which === $.ui.keyCode.ENTER) {
|
||||
this._saveFavorite();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onMenuHeaderClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._toggleMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onSaveFavoriteClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._saveFavorite();
|
||||
},
|
||||
});
|
||||
|
||||
favoritesSubmenusRegistry.add('add_new_favorite_menu', AddNewFavoriteMenu, 0);
|
||||
|
||||
return AddNewFavoriteMenu;
|
||||
|
||||
});
|
||||
+29
-6
@@ -1,10 +1,13 @@
|
||||
odoo.define('web.TimeRangeMenuOptions', function (require) {
|
||||
odoo.define('web.controlPanelViewParameters', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
var _lt = core._lt;
|
||||
|
||||
var PeriodOptions = [
|
||||
// for FilterMenu
|
||||
var DEFAULT_PERIOD = 'this_month';
|
||||
var PERIOD_OPTIONS = [
|
||||
{description: _lt('Last 7 Days'), optionId: 'last_7_days', groupId: 1},
|
||||
{description: _lt('Last 30 Days'), optionId: 'last_30_days', groupId: 1},
|
||||
{description: _lt('Last 365 Days'), optionId: 'last_365_days', groupId: 1},
|
||||
@@ -20,14 +23,34 @@ var PeriodOptions = [
|
||||
{description: _lt('Last Year'), optionId: 'last_year', groupId: 3},
|
||||
];
|
||||
|
||||
var ComparisonOptions = [
|
||||
// for GroupBy menu
|
||||
var GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
|
||||
var DEFAULT_INTERVAL = 'month';
|
||||
var INTERVAL_OPTIONS = [
|
||||
{description: _lt('Day'), optionId: 'day', groupId: 1},
|
||||
{description: _lt('Week'), optionId: 'week', groupId: 1},
|
||||
{description: _lt('Month'), optionId: 'month', groupId: 1},
|
||||
{description: _lt('Quarter'), optionId: 'quarter', groupId: 1},
|
||||
{description: _lt('Year'), optionId: 'year', groupId: 1},
|
||||
];
|
||||
|
||||
// for TimeRangeMenu
|
||||
var DEFAULT_TIMERANGE = DEFAULT_PERIOD;
|
||||
var TIME_RANGE_OPTIONS = PERIOD_OPTIONS;
|
||||
var COMPARISON_TIME_RANGE_OPTIONS = [
|
||||
{description: _lt('Previous Period'), optionId: 'previous_period'},
|
||||
{description: _lt('Previous Year'), optionId: 'previous_year'}
|
||||
];
|
||||
|
||||
return {
|
||||
PeriodOptions: PeriodOptions,
|
||||
ComparisonOptions: ComparisonOptions,
|
||||
COMPARISON_TIME_RANGE_OPTIONS: COMPARISON_TIME_RANGE_OPTIONS,
|
||||
DEFAULT_INTERVAL: DEFAULT_INTERVAL,
|
||||
DEFAULT_PERIOD: DEFAULT_PERIOD,
|
||||
DEFAULT_TIMERANGE: DEFAULT_TIMERANGE,
|
||||
GROUPABLE_TYPES: GROUPABLE_TYPES,
|
||||
INTERVAL_OPTIONS: INTERVAL_OPTIONS,
|
||||
PERIOD_OPTIONS: PERIOD_OPTIONS,
|
||||
TIME_RANGE_OPTIONS: TIME_RANGE_OPTIONS,
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
odoo.define('web.FavoriteMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
var favorites_submenus_registry = require('web.favorites_submenus_registry');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var FavoriteMenu = DropdownMenu.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} action
|
||||
*/
|
||||
init: function (parent, favorites, action) {
|
||||
this._super(parent, favorites);
|
||||
this.action = action;
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.dropdownCategory = 'favorite';
|
||||
this.dropdownTitle = _t('Favorites');
|
||||
this.dropdownIcon = 'fa fa-star';
|
||||
this.dropdownSymbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : false;
|
||||
this.dropdownStyle.mainButton.class = 'o_favorites_menu_button ' +
|
||||
this.dropdownStyle.mainButton.class;
|
||||
|
||||
},
|
||||
/**
|
||||
* Render the template used to register a new favorite and append it
|
||||
* to the basic dropdown menu.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
var params = {
|
||||
favorites: this.items,
|
||||
action: this.action,
|
||||
};
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.$menu.addClass('o_favorites_menu');
|
||||
this.subMenus = [];
|
||||
favorites_submenus_registry.values().forEach(function (SubMenu) {
|
||||
var subMenu = new SubMenu(self, params);
|
||||
subMenu.appendTo(self.$menu);
|
||||
self.subMenus.push(subMenu);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_closeSubMenus: function () {
|
||||
_.invoke(this.subMenus, 'closeMenu');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._closeSubMenus();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onTrashButtonClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var self = this;
|
||||
var id = $(event.currentTarget).data('id');
|
||||
var favorite = this.items.find(function (favorite) {
|
||||
return favorite.id === id;
|
||||
});
|
||||
var globalWarning = _t("This filter is global and will be removed for everybody if you continue.");
|
||||
var warning = _t("Are you sure that you want to remove this filter?");
|
||||
var message = favorite.userId ? warning : globalWarning;
|
||||
|
||||
Dialog.confirm(self, message, {
|
||||
title: _t("Warning"),
|
||||
confirm_callback: function () {
|
||||
self.trigger_up('item_trashed', {id: id});
|
||||
},
|
||||
});
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
return FavoriteMenu;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
odoo.define('web.favorites_submenus_registry', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
return new Registry();
|
||||
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
odoo.define('web.FilterMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
var search_filters = require('web.search_filters');
|
||||
var time = require('web.time');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var FilterMenu = DropdownMenu.extend({
|
||||
custom_events: {
|
||||
confirm_proposition: '_onConfirmProposition',
|
||||
remove_proposition: '_onRemoveProposition',
|
||||
},
|
||||
events: _.extend({}, DropdownMenu.prototype.events, {
|
||||
'click .o_add_custom_filter': '_onAddCustomFilterClick',
|
||||
'click .o_add_condition': '_onAddCondition',
|
||||
'click .o_apply_filter': '_onApplyClick',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} fields
|
||||
*/
|
||||
init: function (parent, filters, fields) {
|
||||
this._super(parent, filters);
|
||||
|
||||
// determines where the filter menu is displayed and its style
|
||||
this.isMobile = config.device.isMobile;
|
||||
// determines when the 'Add custom filter' submenu is open
|
||||
this.generatorMenuIsOpen = false;
|
||||
this.propositions = [];
|
||||
this.fields = _.pick(fields, function (field, name) {
|
||||
return field.selectable !== false && name !== 'id';
|
||||
});
|
||||
this.fields.id = {string: 'ID', type: 'id', searchable: true};
|
||||
this.dropdownCategory = 'filter';
|
||||
this.dropdownTitle = _t('Filters');
|
||||
this.dropdownIcon = 'fa fa-filter';
|
||||
this.dropdownSymbol = this.isMobile ?
|
||||
'fa fa-chevron-right float-right mt4' :
|
||||
false;
|
||||
this.dropdownStyle.mainButton.class = 'o_filters_menu_button ' +
|
||||
this.dropdownStyle.mainButton.class;
|
||||
},
|
||||
/**
|
||||
* Render the template used to add a new custom filter and append it
|
||||
* to the basic dropdown menu.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.$menu.addClass('o_filters_menu');
|
||||
this._renderGeneratorMenu();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a proposition inside the custom filter edition menu.
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Deferred}
|
||||
*/
|
||||
_appendProposition: function () {
|
||||
// make modern sear_filters code!!! It works but...
|
||||
var prop = new search_filters.ExtendedSearchProposition(this, this.fields);
|
||||
this.propositions.push(prop);
|
||||
this.$('.o_apply_filter').prop('disabled', false);
|
||||
prop.insertBefore(this.$addFilterMenu);
|
||||
},
|
||||
/**
|
||||
* Confirm a filter proposition, creates it and add it to the menu.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_commitSearch: function () {
|
||||
var filters = _.invoke(this.propositions, 'get_filter').map(function (preFilter) {
|
||||
return {
|
||||
type: 'filter',
|
||||
description: preFilter.attrs.string,
|
||||
domain: Domain.prototype.arrayToString(preFilter.attrs.domain),
|
||||
};
|
||||
});
|
||||
// TO DO intercepts 'new_filters' and decide what to do whith filters
|
||||
// rewrite web.search_filters?
|
||||
this.trigger_up('new_filters', {filters: filters});
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
this._toggleCustomFilterMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderGeneratorMenu: function () {
|
||||
this.$el.find('.o_generator_menu').remove();
|
||||
if (!this.generatorMenuIsOpen) {
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
}
|
||||
var $generatorMenu = QWeb.render('FilterMenuGenerator', {widget: this});
|
||||
this.$menu.append($generatorMenu);
|
||||
this.$addFilterMenu = this.$menu.find('.o_add_filter_menu');
|
||||
if (this.generatorMenuIsOpen && !this.propositions.length) {
|
||||
this._appendProposition();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderMenuItems: function () {
|
||||
var self= this;
|
||||
this._super.apply(this, arguments);
|
||||
// the following code adds tooltip on date options in order
|
||||
// to alert the user of the meaning of intervals
|
||||
var $options = this.$('.o_item_option');
|
||||
$options.each(function () {
|
||||
var $option = $(this);
|
||||
$option.tooltip({
|
||||
delay: { show: 500, hide: 0 },
|
||||
title: function () {
|
||||
var itemId = $option.attr('data-item_id');
|
||||
var optionId = $option.attr('data-option_id');
|
||||
var fieldName = _.findWhere(self.items, {id: itemId}).fieldName;
|
||||
var domain = Domain.prototype.constructDomain(fieldName, optionId, 'date', true);
|
||||
var evaluatedDomain = Domain.prototype.stringToArray(domain);
|
||||
var dateFormat = time.getLangDateFormat();
|
||||
var dateStart = moment(evaluatedDomain[1][2], "YYYY-MM-DD", 'en').format(dateFormat);
|
||||
var dateEnd = moment(evaluatedDomain[2][2], "YYYY-MM-DD", 'en').format(dateFormat);
|
||||
if (optionId === 'today' || optionId === 'yesterday') {
|
||||
return dateStart;
|
||||
}
|
||||
return _.str.sprintf(_t('From %s To %s'), dateStart, dateEnd);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Hide and display the submenu which allows adding custom filters.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleCustomFilterMenu: function () {
|
||||
this.generatorMenuIsOpen = !this.generatorMenuIsOpen;
|
||||
this._renderGeneratorMenu();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddCondition: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._appendProposition();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddCustomFilterClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._toggleCustomFilterMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onApplyClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._commitSearch();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.generatorMenuIsOpen = false;
|
||||
this._renderGeneratorMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onConfirmProposition: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._commitSearch();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onRemoveProposition: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.propositions = _.without(this.propositions, ev.target);
|
||||
if (!this.propositions.length) {
|
||||
this.$('.o_apply_filter').prop('disabled', true);
|
||||
}
|
||||
ev.target.destroy();
|
||||
},
|
||||
});
|
||||
|
||||
return FilterMenu;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
odoo.define('web.GroupByMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var controlPanelViewParameters = require('web.controlPanelViewParameters');
|
||||
var core = require('web.core');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var DEFAULT_INTERVAL = controlPanelViewParameters.DEFAULT_INTERVAL;
|
||||
var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
|
||||
var INTERVAL_OPTIONS = controlPanelViewParameters.INTERVAL_OPTIONS;
|
||||
|
||||
var GroupByMenu = DropdownMenu.extend({
|
||||
events: _.extend({}, DropdownMenu.prototype.events, {
|
||||
'click .o_add_custom_group': '_onAddCustomGroupClick',
|
||||
'click button.o_apply_group': '_onButtonApplyClick',
|
||||
'click .o_group_selector': '_onGroupSelectorClick',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {Object[]} groupBys list of groupBys (type IGroupBy below)
|
||||
* interface IGroupBy {
|
||||
* itemId: string; unique id associated with the groupby
|
||||
* fieldName: string; field name without interval!
|
||||
* description: string; label printed on screen
|
||||
* groupId: string;
|
||||
* isActive: boolean; determines if the groupby is considered active
|
||||
* hasOptions: boolean; true when the groupBy has fieldType 'date' or 'datetime' (optional)
|
||||
* options: array of objects with 'optionId' and 'description' keys; (optional)
|
||||
* defaultOptionId: string refers to an optionId that should be activated by default
|
||||
* currentOptionId: string refers to an optionId that is activated if item is active (optional)
|
||||
* }
|
||||
* @param {Object} fields 'field_get' of a model: mapping from field name
|
||||
* to an object with its properties
|
||||
* @param {Object} options
|
||||
* @param {string} options.headerStyle conditions the style of the main button
|
||||
*/
|
||||
init: function (parent, groupBys, fields, options) {
|
||||
var self = this;
|
||||
this._super(parent, groupBys);
|
||||
this.fields = fields;
|
||||
// determines when the 'Add custom groupby' submenu is open
|
||||
this.generatorMenuIsOpen = false;
|
||||
// determines list of options used by groupBys of type 'date'
|
||||
this.groupableFields = [];
|
||||
_.each(fields, function (field, name) {
|
||||
if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) {
|
||||
self.groupableFields.push(_.extend({}, field, {
|
||||
name: name,
|
||||
}));
|
||||
}
|
||||
});
|
||||
this.groupableFields = _.sortBy(this.groupableFields, 'string');
|
||||
// determines the list of field names that can be added to the menu
|
||||
// via the 'Add Custom Groupby' menu
|
||||
this.presentedFields = this._setPresentedFields(groupBys);
|
||||
|
||||
// determines where the filter menu is displayed and partly its style
|
||||
this.isMobile = config.device.isMobile;
|
||||
|
||||
this.dropdownCategory = 'groupby';
|
||||
this.dropdownTitle = _t('Group By');
|
||||
this.dropdownIcon = 'fa fa-bars';
|
||||
this.dropdownSymbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : false;
|
||||
// the default style of the groupby menu can be changed here using the options key 'headerStyle'
|
||||
if (options && options.headerStyle === 'primary') {
|
||||
this.dropdownStyle = {
|
||||
el: {class: 'btn-group o_group_by_menu o_dropdown', attrs: {'role': 'group'}},
|
||||
mainButton: {class: 'btn btn-primary dropdown-toggle'},
|
||||
};
|
||||
}
|
||||
INTERVAL_OPTIONS = INTERVAL_OPTIONS.map(function (option) {
|
||||
return _.extend(option, {description: option.description.toString()});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the template used to add a new custom groupby and append it
|
||||
* to the basic dropdown menu.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.$menu.addClass('o_group_by_menu');
|
||||
this._renderGeneratorMenu();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object[]} groupBys
|
||||
*/
|
||||
update: function (groupBys) {
|
||||
this._super.apply(this, arguments);
|
||||
this.presentedFields = this._setPresentedFields(groupBys);
|
||||
this._renderGeneratorMenu();
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method called via the 'Add Custom Groupby' menu to create a new groupby
|
||||
* and add it (activated) to the menu. In case the field is of type date, it
|
||||
* is activated using the option determined by the parameter
|
||||
* 'DEFAULT_INTERVAL'.
|
||||
*
|
||||
* @private
|
||||
* @param {string} fieldName
|
||||
*/
|
||||
_addGroupby: function (fieldName) {
|
||||
var field = this.presentedFields.find(function (field) {
|
||||
return field.name === fieldName;
|
||||
});
|
||||
var groupBy = {
|
||||
type: 'groupBy',
|
||||
description: field.string,
|
||||
fieldName: fieldName,
|
||||
fieldType: field.type,
|
||||
};
|
||||
if (_.contains(['date', 'datetime'], field.type)) {
|
||||
groupBy.hasOptions = true;
|
||||
groupBy.options = INTERVAL_OPTIONS;
|
||||
groupBy.defaultOptionId = DEFAULT_INTERVAL;
|
||||
groupBy.currentOptionId = false;
|
||||
}
|
||||
this.trigger_up('new_groupBy', groupBy);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderGeneratorMenu: function () {
|
||||
this.$el.find('.o_generator_menu').remove();
|
||||
var $generatorMenu = QWeb.render('GroupByMenuGenerator', {widget: this});
|
||||
this.$menu.append($generatorMenu);
|
||||
this.$addCustomGroup = this.$menu.find('.o_add_custom_group');
|
||||
this.$groupSelector = this.$menu.find('.o_group_selector');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setPresentedFields: function (groupBys) {
|
||||
return this.groupableFields.filter(function (field) {
|
||||
var groupByFields = _.pluck(groupBys, 'fieldName');
|
||||
return !_.contains(groupByFields, field.name);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_toggleCustomGroupMenu: function () {
|
||||
this.generatorMenuIsOpen = !this.generatorMenuIsOpen;
|
||||
this._renderGeneratorMenu();
|
||||
if (this.generatorMenuIsOpen) {
|
||||
this.$groupSelector.focus();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle the 'Add Custom Group'.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddCustomGroupClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._toggleCustomGroupMenu();
|
||||
},
|
||||
/*
|
||||
* @override
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.generatorMenuIsOpen = false;
|
||||
this._renderGeneratorMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onButtonApplyClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var fieldName = this.$groupSelector.val();
|
||||
this.generatorMenuIsOpen = false;
|
||||
this._addGroupby(fieldName);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onGroupSelectorClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
return GroupByMenu;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
odoo.define('web.GroupByMenuMixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
var GroupByMenu = require('web.GroupByMenu');
|
||||
var controlPanelViewParameters = require('web.controlPanelViewParameters');
|
||||
|
||||
/**
|
||||
* The aim of this mixin is to facilitate the interaction between
|
||||
* a view controller and a dropdown menu with its control panel
|
||||
* TO DO: the pivot subview has two types of groupbys so that it will not
|
||||
* understand the current implementation of this mixin
|
||||
*
|
||||
* @mixin
|
||||
* @name GroupByMenuMixin
|
||||
*/
|
||||
var GroupByMenuMixin = {
|
||||
init: function () {
|
||||
this.custom_events = _.extend({}, this.custom_events, {
|
||||
menu_item_clicked: '_onMenuItemClicked',
|
||||
item_option_clicked: '_onItemOptionClicked',
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Instantiate a widget GroupByMenu and incorporate it to the control panel
|
||||
* of an embedded view. This is used for instance in the dashboard view in
|
||||
* enterprise where there is no GroupBy menu in the search view because it
|
||||
* would not make sense to have one at the global level.
|
||||
* This function is called by renderButtons when the parameter
|
||||
* 'this.isEmbedded' is set to true.
|
||||
*
|
||||
* private
|
||||
* @param {jQuery} $node
|
||||
* @param {Object} groupableFields
|
||||
*/
|
||||
_addGroupByMenu: function ($node, groupableFields) {
|
||||
this.sortedFieldNames = _.sortBy(Object.keys(groupableFields), function (fieldName) {
|
||||
return groupableFields[fieldName].string;
|
||||
});
|
||||
this.groupableFields = groupableFields;
|
||||
var groupBys = this._getGroupBys(this.model.get().groupedBy);
|
||||
this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields);
|
||||
this.groupByMenu.insertAfter($node.find('div:first'));
|
||||
},
|
||||
/**
|
||||
* This method puts the active groupBys in a convenient form.
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} activeGroupBys
|
||||
* returns {string[]} groupBysNormalized
|
||||
*/
|
||||
_normalizeActiveGroupBys: function (activeGroupBys) {
|
||||
var groupBysNormalized = activeGroupBys.map(function (groupBy) {
|
||||
return {fieldName: groupBy.split(':')[0], interval: groupBy.split(':')[1] || false};
|
||||
});
|
||||
return groupBysNormalized;
|
||||
},
|
||||
/**
|
||||
* This method has to be implemented by the view controller that needs to
|
||||
* interpret the click in an appropriate manner.
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} groupBys
|
||||
*/
|
||||
_setGroupby: function (groupBys) {},
|
||||
/**
|
||||
* Return the list of groupBys in a form suitable for the groupByMenu. We do
|
||||
* this each time because we want to be synchronized with the view model.
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} activeGroupBys
|
||||
* returns {Object[]} groupBys
|
||||
*/
|
||||
_getGroupBys: function (activeGroupBys) {
|
||||
var self = this;
|
||||
var groupBys = [];
|
||||
var groupBysNormalized = this._normalizeActiveGroupBys(activeGroupBys);
|
||||
this.sortedFieldNames.forEach(function (fieldName) {
|
||||
var field = self.groupableFields[fieldName];
|
||||
var groupByActivity = _.findWhere(groupBysNormalized, {fieldName: fieldName});
|
||||
var groupBy = {
|
||||
id: fieldName,
|
||||
isActive: groupByActivity ? true : false,
|
||||
description: field.string,
|
||||
};
|
||||
if (_.contains(['date', 'datetime'], field.type)) {
|
||||
groupBy.hasOptions = true;
|
||||
groupBy.options = controlPanelViewParameters.INTERVAL_OPTIONS;
|
||||
groupBy.currentOptionId = groupByActivity && groupByActivity.interval ?
|
||||
groupByActivity.interval :
|
||||
false;
|
||||
}
|
||||
groupBys.push(groupBy);
|
||||
});
|
||||
return groupBys;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onItemOptionClicked: function (ev) {
|
||||
var fieldName = ev.data.id;
|
||||
var optionId = ev.data.optionId;
|
||||
var activeGroupBys = this.model.get().groupedBy;
|
||||
var currentOptionId = activeGroupBys.reduce(
|
||||
function (optionId, groupby) {
|
||||
if (groupby.split(':')[0] === fieldName){
|
||||
optionId = groupby.split(':')[1] || controlPanelViewParameters.DEFAULT_INTERVAL;
|
||||
}
|
||||
return optionId;
|
||||
},
|
||||
false
|
||||
);
|
||||
var groupByFieldNames = _.map(activeGroupBys, function (groupby) {
|
||||
return groupby.split(':')[0];
|
||||
});
|
||||
var indexOfGroupby = groupByFieldNames.indexOf(fieldName);
|
||||
if (indexOfGroupby === -1) {
|
||||
activeGroupBys.push(fieldName + ':' + optionId);
|
||||
} else if (currentOptionId === optionId) {
|
||||
activeGroupBys.splice(indexOfGroupby, 1);
|
||||
} else {
|
||||
activeGroupBys.splice(indexOfGroupby, 1, fieldName + ':' + optionId);
|
||||
}
|
||||
this._setGroupby(activeGroupBys);
|
||||
this.groupByMenu.update(this._getGroupBys(activeGroupBys));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onMenuItemClicked: function (ev) {
|
||||
var fieldName = ev.data.id;
|
||||
var activeGroupBys = this.model.get().groupedBy;
|
||||
var groupByFieldNames = _.map(activeGroupBys, function (groupby) {
|
||||
return groupby.split(':')[0];
|
||||
});
|
||||
var indexOfGroupby = groupByFieldNames.indexOf(fieldName);
|
||||
if (indexOfGroupby === -1) {
|
||||
activeGroupBys.push(fieldName);
|
||||
} else {
|
||||
activeGroupBys.splice(indexOfGroupby, 1);
|
||||
}
|
||||
this._setGroupby(activeGroupBys);
|
||||
this.groupByMenu.update(this._getGroupBys(activeGroupBys));
|
||||
},
|
||||
};
|
||||
|
||||
return GroupByMenuMixin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
odoo.define('web.SearchBar', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AutoComplete = require('web.AutoComplete');
|
||||
var searchBarAutocompleteRegistry = require('web.search_bar_autocomplete_sources_registry');
|
||||
var SearchFacet = require('web.SearchFacet');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var SearchBar = Widget.extend({
|
||||
template: 'SearchView.SearchBar',
|
||||
events: _.extend({}, Widget.prototype.events, {
|
||||
'keydown': '_onKeydown',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} [params]
|
||||
* @param {Object} [params.context]
|
||||
* @param {Object[]} [params.facets]
|
||||
* @param {Object} [params.fields]
|
||||
* @param {Object[]} [params.filterFields]
|
||||
* @param {Object[]} [params.filters]
|
||||
* @param {Object[]} [params.groupBys]
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.context = params.context;
|
||||
|
||||
this.facets = params.facets;
|
||||
this.fields = params.fields;
|
||||
this.filters = params.filters;
|
||||
this.filterFields = params.filterFields;
|
||||
this.groupBys = params.groupBys;
|
||||
|
||||
this.autoCompleteSources = [];
|
||||
this.searchFacets = [];
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$input = this.$('input');
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
_.each(this.facets, function (facet) {
|
||||
defs.push(self._renderFacet(facet));
|
||||
});
|
||||
defs.push(this._setupAutoCompletion());
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focus the searchbar.
|
||||
*/
|
||||
focus: function () {
|
||||
this.$input.focus();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_focusFollowing: function () {
|
||||
var focusedIndex = this._getFocusedFacetIndex();
|
||||
var $toFocus;
|
||||
if (focusedIndex === this.searchFacets.length - 1) {
|
||||
$toFocus = this.$input;
|
||||
} else {
|
||||
$toFocus = this.searchFacets.length && this.searchFacets[focusedIndex + 1].$el;
|
||||
}
|
||||
|
||||
if ($toFocus.length) {
|
||||
$toFocus.focus();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_focusPreceding: function () {
|
||||
var focusedIndex = this._getFocusedFacetIndex();
|
||||
var $toFocus;
|
||||
if (focusedIndex === -1) {
|
||||
$toFocus = this.searchFacets.length && _.last(this.searchFacets).$el;
|
||||
} else if (focusedIndex === 0) {
|
||||
$toFocus = this.$input;
|
||||
} else {
|
||||
$toFocus = this.searchFacets.length && this.searchFacets[focusedIndex - 1].$el;
|
||||
}
|
||||
|
||||
if ($toFocus.length) {
|
||||
$toFocus.focus();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getFocusedFacetIndex: function () {
|
||||
return _.findIndex(this.searchFacets, function (searchFacet) {
|
||||
return searchFacet.$el[0] === document.activeElement;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Provide auto-completion result for req.term.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} req request to complete
|
||||
* @param {String} req.term searched term to complete
|
||||
* @param {Function} callback
|
||||
*/
|
||||
_getAutoCompleteSources: function (req, callback) {
|
||||
var defs = _.invoke(this.autoCompleteSources, 'getAutocompletionValues', req.term);
|
||||
$.when.apply($, defs).then(function () {
|
||||
callback(_(arguments).chain()
|
||||
.compact()
|
||||
.flatten(true)
|
||||
.value());
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} facet
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_renderFacet: function (facet) {
|
||||
var searchFacet = new SearchFacet(this, facet);
|
||||
this.searchFacets.push(searchFacet);
|
||||
return searchFacet.insertBefore(this.$('input'));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_setupAutoCompletion: function () {
|
||||
var self = this;
|
||||
this._setupAutoCompletionWidgets();
|
||||
this.autoComplete = new AutoComplete(this, {
|
||||
source: this._getAutoCompleteSources.bind(this),
|
||||
select: this._onAutoCompleteSelected.bind(this),
|
||||
get_search_string: function () {
|
||||
return self.$input.val().trim();
|
||||
},
|
||||
});
|
||||
return this.autoComplete.appendTo(this.$el);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setupAutoCompletionWidgets: function () {
|
||||
var self = this;
|
||||
var registry = searchBarAutocompleteRegistry;
|
||||
_.each(this.filterFields, function (filter) {
|
||||
var field = self.fields[filter.attrs.name];
|
||||
var Obj = registry.getAny([filter.attrs.widget, field.type]);
|
||||
if (Obj) {
|
||||
self.autoCompleteSources.push(new (Obj) (self, filter, field, self.context));
|
||||
}
|
||||
});
|
||||
_.each(this.filters, function (filter) {
|
||||
self.autoCompleteSources.push(new (registry.get('filter'))(self, filter));
|
||||
});
|
||||
_.each(this.groupBys, function (filter) {
|
||||
self.autoCompleteSources.push(new (registry.get('groupby'))(self, filter));
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} e
|
||||
* @param {Object} ui
|
||||
* @param {Object} ui.item selected completion item
|
||||
*/
|
||||
_onAutoCompleteSelected: function (e, ui) {
|
||||
e.preventDefault();
|
||||
var filter = ui.item.facet.filter;
|
||||
if (filter.type === 'field') {
|
||||
var values = filter.autoCompleteValues;
|
||||
values.push(ui.item.facet.values[0]);
|
||||
this.trigger_up('autocompletion_filter', {
|
||||
filterId: filter.id,
|
||||
autoCompleteValues: values,
|
||||
});
|
||||
} else {
|
||||
this.trigger_up('autocompletion_filter', {
|
||||
filterId: filter.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyEvent} e
|
||||
*/
|
||||
_onKeydown: function (e) {
|
||||
switch(e.which) {
|
||||
case $.ui.keyCode.LEFT:
|
||||
this._focusPreceding();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case $.ui.keyCode.RIGHT:
|
||||
this._focusFollowing();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
if (this.$input.val() === '') {
|
||||
this.trigger_up('facet_removed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return SearchBar;
|
||||
|
||||
});
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
odoo.define('web.SearchBarAutoCompleteSources', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Class = require('web.Class');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var field_utils = require('web.field_utils');
|
||||
var mixins = require('web.mixins');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var time = require('web.time');
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var FilterInterface = Class.extend(mixins.EventDispatcherMixin, {
|
||||
completion_label: _lt("%s"),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} filter
|
||||
*/
|
||||
init: function (parent, filter) {
|
||||
mixins.EventDispatcherMixin.init.call(this);
|
||||
this.setParent(parent);
|
||||
|
||||
this.filter = filter;
|
||||
},
|
||||
/**
|
||||
* Fetch auto-completion values for the widget.
|
||||
*
|
||||
* The completion values should be an array of objects with keys facet and
|
||||
* label. They will be used by search_bar in @_onAutoCompleteSelected.
|
||||
*
|
||||
* @param {string} value value to getAutocompletionValues
|
||||
* @returns {Deferred<null|Array>}
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
var result;
|
||||
value = value.toLowerCase();
|
||||
if (fuzzy.test(value, this.filter.description)) {
|
||||
result = [{
|
||||
label: _.str.sprintf(this.completion_label.toString(),
|
||||
_.escape(this.filter.description)),
|
||||
facet: {
|
||||
filter: this.filter,
|
||||
},
|
||||
}];
|
||||
}
|
||||
return $.when(result);
|
||||
},
|
||||
});
|
||||
|
||||
var Filter = FilterInterface.extend({
|
||||
completion_label: _lt("Filter on: %s"),
|
||||
});
|
||||
|
||||
var GroupBy = FilterInterface.extend({
|
||||
completion_label: _lt("Group by: %s"),
|
||||
});
|
||||
|
||||
var Field = FilterInterface.extend(ServicesMixin, {
|
||||
default_operator: '=',
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} field
|
||||
* @param {Object} filter
|
||||
* @param {Object} context needed for extra rpc (i.e. m2o)
|
||||
*/
|
||||
init: function (parent, filter, field, context) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.field = field;
|
||||
this.filter = filter;
|
||||
this.attrs = _.extend({}, field, filter.attrs);
|
||||
this.context = context;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
return $.when([{
|
||||
label: this._getAutocompletionLabel(value),
|
||||
facet: this._getFacetValue(value),
|
||||
}]);
|
||||
},
|
||||
/**
|
||||
* TODO: the domain logic (getDomain, operator, etc.) should not be put here
|
||||
* (this has nothing to do with the autocomplete sources).
|
||||
*
|
||||
* This method is used by the ControlPanelModel when setting the
|
||||
* `filter_domain`.
|
||||
*
|
||||
* @param {Object[]} values
|
||||
* @returns {string}
|
||||
*/
|
||||
getDomain: function (values) {
|
||||
if (!values.length) { return; }
|
||||
|
||||
var valueToDomain;
|
||||
var self = this;
|
||||
var domain = this.attrs.filter_domain;
|
||||
if (domain) {
|
||||
valueToDomain = function (facetValue) {
|
||||
return Domain.prototype.stringToArray(
|
||||
domain,
|
||||
{
|
||||
// these are the values that can be used in search view
|
||||
// fields `filter_domain` attribute
|
||||
self: self._valueFrom(facetValue),
|
||||
raw_value: facetValue.value,
|
||||
}
|
||||
);
|
||||
};
|
||||
} else {
|
||||
valueToDomain = function (facetValue) {
|
||||
return self._makeDomain(
|
||||
self.attrs.name,
|
||||
self.attrs.operator || self.default_operator,
|
||||
facetValue
|
||||
);
|
||||
};
|
||||
}
|
||||
var domains = values.map(valueToDomain);
|
||||
|
||||
domains = domains.map(Domain.prototype.arrayToString);
|
||||
return pyUtils.assembleDomains(domains, 'OR');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {any} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_getAutocompletionLabel: function (value) {
|
||||
return _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s for: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + _.escape(value) + '</strong>'});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {any} value
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFacetValue: function (value) {
|
||||
return {
|
||||
filter: this.filter,
|
||||
values: [{label: value, value: value}],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Function creating the returned domain for the field, override this
|
||||
* methods in children if you only need to customize the field's domain
|
||||
* without more complex alterations or tests (and without the need to
|
||||
* change override the handling of filter_domain)
|
||||
*
|
||||
* @private
|
||||
* @param {String} name the field's name
|
||||
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
|
||||
* @param {Number|String} facet parsed value for the field
|
||||
* @returns {Array<Array>} domain to include in the resulting search
|
||||
*/
|
||||
_makeDomain: function (name, operator, facet) {
|
||||
return [[name, operator, this._valueFrom(facet)]];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} facetValue
|
||||
* @param {any} facetValue.value
|
||||
* @returns {any}
|
||||
*/
|
||||
_valueFrom: function (facetValue) {
|
||||
return facetValue.value;
|
||||
},
|
||||
});
|
||||
|
||||
var CharField = Field.extend({
|
||||
default_operator: 'ilike',
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
if (_.isEmpty(value)) { return $.when(null); }
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var NumberField = Field.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
var val = this.parse(value);
|
||||
if (isNaN(val)) { return $.when(); }
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var IntegerField = NumberField.extend({
|
||||
error_message: _t("not a valid integer"),
|
||||
parse: function (value) {
|
||||
try {
|
||||
return field_utils.parse.integer(value);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var FloatField = NumberField.extend({
|
||||
error_message: _t("not a valid number"),
|
||||
parse: function (value) {
|
||||
try {
|
||||
return field_utils.parse.float(value);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var SelectionField = Field.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
var self = this;
|
||||
var results = _(this.attrs.selection).chain()
|
||||
.filter(function (sel) {
|
||||
var selValue = sel[0], label = sel[1];
|
||||
if (selValue === undefined || !label) { return false; }
|
||||
return label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||||
})
|
||||
.map(function (sel) {
|
||||
return {
|
||||
label: _.escape(sel[1]),
|
||||
indent: true,
|
||||
facet: self._getFacetValue(sel)
|
||||
};
|
||||
}).value();
|
||||
if (_.isEmpty(results)) { return $.when(null); }
|
||||
return $.when.call(null, [{
|
||||
label: _.escape(this.attrs.string)
|
||||
}].concat(results));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {any} value
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFacetValue: function (value) {
|
||||
return {
|
||||
filter: this.filter,
|
||||
values: [{label: value[1], value: value[0]}],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
var BooleanField = SelectionField.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.attrs.selection = [
|
||||
[true, _t("Yes")],
|
||||
[false, _t("No")]
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
var DateField = Field.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
// Make sure the value has a correct format before the creation of the moment object. See
|
||||
// issue https://github.com/moment/moment/issues/1407
|
||||
var t, v;
|
||||
try {
|
||||
t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
|
||||
v = field_utils.parse[t](value, {type: t}, {timezone: true});
|
||||
} catch (e) {
|
||||
return $.when(null);
|
||||
}
|
||||
|
||||
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
|
||||
if (!m.isValid()) { return $.when(null); }
|
||||
var dateString = field_utils.format[t](m, {type: t});
|
||||
var label = this._getAutocompletionLabel(dateString);
|
||||
var facet = this._getFacetValue(dateString, m.toDate());
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: facet,
|
||||
}]);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getAutocompletionLabel: function (value) {
|
||||
return _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s at: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + value + '</strong>'});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {any} rawValue
|
||||
*/
|
||||
_getFacetValue: function (value, rawValue) {
|
||||
var facet = this._super.apply(this, arguments);
|
||||
facet.values[0].value = rawValue;
|
||||
return facet;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_valueFrom: function (facetValue) {
|
||||
return time.date_to_str(facetValue.value);
|
||||
},
|
||||
});
|
||||
|
||||
var DateTimeField = DateField.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_valueFrom: function (facetValue) {
|
||||
return time.datetime_to_str(facetValue.value);
|
||||
},
|
||||
});
|
||||
|
||||
var ManyToOneField = CharField.extend({
|
||||
default_operator: {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getAutocompletionValues: function (value) {
|
||||
if (_.isEmpty(value)) { return $.when(null); }
|
||||
var label = this._getAutocompletionLabel(value);
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: this._getFacetValue(value),
|
||||
expand: this._expand.bind(this),
|
||||
}]);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getExpandedFacetValue: function (value) {
|
||||
return {
|
||||
filter: this.filter,
|
||||
values: [{label: value[1], value: value[0]}],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getFacetValue: function (value) {
|
||||
return {
|
||||
filter: this.filter,
|
||||
values: [{label: value, value: value, operator: 'ilike'}],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_expand: function (value) {
|
||||
var self = this;
|
||||
var args = this.attrs.domain;
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = Domain.prototype.stringToArray(args);
|
||||
} catch(e) {
|
||||
args = [];
|
||||
}
|
||||
}
|
||||
return this._rpc({
|
||||
model: this.attrs.relation,
|
||||
method: 'name_search',
|
||||
kwargs: {
|
||||
name: value,
|
||||
args: args,
|
||||
limit: 8,
|
||||
context: this.context,
|
||||
},
|
||||
})
|
||||
.then(function (results) {
|
||||
if (_.isEmpty(results)) { return null; }
|
||||
return _(results).map(function (result) {
|
||||
return {
|
||||
label: _.escape(result[1]),
|
||||
facet: self._getExpandedFacetValue(result)
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_makeDomain: function (name, operator, facetValue) {
|
||||
operator = facetValue.operator || operator;
|
||||
|
||||
switch(operator){
|
||||
case this.default_operator:
|
||||
return [[name, '=', facetValue.value]];
|
||||
case 'ilike':
|
||||
return [[name, 'ilike', facetValue.value]];
|
||||
case 'child_of':
|
||||
return [[name, 'child_of', facetValue.value]];
|
||||
}
|
||||
return this._super(name, operator, facetValue);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_valueFrom: function (facetValue) {
|
||||
return facetValue.label;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
BooleanField: BooleanField,
|
||||
CharField: CharField,
|
||||
DateField: DateField,
|
||||
DateTimeField: DateTimeField,
|
||||
Field: Field,
|
||||
Filter: Filter,
|
||||
FloatField: FloatField,
|
||||
GroupBy: GroupBy,
|
||||
IntegerField: IntegerField,
|
||||
ManyToOneField: ManyToOneField,
|
||||
NumberField: NumberField,
|
||||
SelectionField: SelectionField,
|
||||
};
|
||||
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
odoo.define('web.search_bar_autocomplete_sources_registry', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
return new Registry();
|
||||
|
||||
});
|
||||
|
||||
odoo.define('web._search_bar_autocomplete_sources_registry', function(require) {
|
||||
"use strict";
|
||||
|
||||
var registry = require('web.search_bar_autocomplete_sources_registry');
|
||||
var SearchBarAutoCompleteSources = require('web.SearchBarAutoCompleteSources');
|
||||
|
||||
// fields
|
||||
registry
|
||||
.add('char', SearchBarAutoCompleteSources.CharField)
|
||||
.add('text', SearchBarAutoCompleteSources.CharField)
|
||||
.add('html', SearchBarAutoCompleteSources.CharField)
|
||||
.add('boolean', SearchBarAutoCompleteSources.BooleanField)
|
||||
.add('integer', SearchBarAutoCompleteSources.IntegerField)
|
||||
.add('id', SearchBarAutoCompleteSources.IntegerField)
|
||||
.add('float', SearchBarAutoCompleteSources.FloatField)
|
||||
.add('monetary', SearchBarAutoCompleteSources.FloatField)
|
||||
.add('selection', SearchBarAutoCompleteSources.SelectionField)
|
||||
.add('datetime', SearchBarAutoCompleteSources.DateTimeField)
|
||||
.add('date', SearchBarAutoCompleteSources.DateField)
|
||||
.add('many2one', SearchBarAutoCompleteSources.ManyToOneField)
|
||||
.add('many2many', SearchBarAutoCompleteSources.CharField)
|
||||
.add('one2many', SearchBarAutoCompleteSources.CharField);
|
||||
|
||||
// others
|
||||
registry.add('filter', SearchBarAutoCompleteSources.Filter);
|
||||
registry.add('groupby', SearchBarAutoCompleteSources.GroupBy);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
odoo.define('web.SearchFacet', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var SearchFacet = Widget.extend({
|
||||
template: 'SearchView.SearchFacet',
|
||||
events: _.extend({}, Widget.prototype.events, {
|
||||
'click .o_facet_remove': '_onFacetRemove',
|
||||
'keydown': '_onKeydown',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} facet
|
||||
*/
|
||||
init: function (parent, facet) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var self = this;
|
||||
this.facet = facet;
|
||||
this.facetValues = _.map(this.facet.filters, function (filter) {
|
||||
return self._getFilterDescription(filter);
|
||||
});
|
||||
this.separator = this._getSeparator();
|
||||
this.icon = this._getIcon();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the correct description according to filter.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getFilterDescription: function (filter) {
|
||||
if (filter.type === 'field') {
|
||||
var values = _.pluck(filter.autoCompleteValues, 'label');
|
||||
return values.join(_t(' or '));
|
||||
}
|
||||
var description = filter.description;
|
||||
if (filter.hasOptions) {
|
||||
var optionValue =_.findWhere(filter.options, {
|
||||
optionId: filter.currentOptionId,
|
||||
});
|
||||
description += ': ' + optionValue.description;
|
||||
}
|
||||
if (filter.type === 'timeRange') {
|
||||
var timeRangeValue =_.findWhere(filter.timeRangeOptions, {
|
||||
optionId: filter.timeRangeId,
|
||||
});
|
||||
description += ': ' + timeRangeValue.description;
|
||||
if (filter.comparisonTimeRangeId) {
|
||||
var comparisonTimeRangeValue =_.findWhere(filter.comparisonTimeRangeOptions, {
|
||||
optionId: filter.comparisonTimeRangeId,
|
||||
});
|
||||
description += ' / ' + comparisonTimeRangeValue.description;
|
||||
}
|
||||
}
|
||||
return description;
|
||||
},
|
||||
/**
|
||||
* Get the correct icon according to facet type.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getIcon: function () {
|
||||
var icon;
|
||||
if (this.facet.type === 'filter') {
|
||||
icon = 'fa-filter';
|
||||
} else if (this.facet.type === 'groupBy') {
|
||||
icon = 'fa-bars';
|
||||
} else if (this.facet.type === 'favorite') {
|
||||
icon = 'fa-star';
|
||||
} else if (this.facet.type === 'timeRange') {
|
||||
icon = 'fa-calendar';
|
||||
}
|
||||
return icon;
|
||||
},
|
||||
/**
|
||||
* Get the correct separator according to facet type.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getSeparator: function () {
|
||||
var separator;
|
||||
if (this.facet.type === 'filter') {
|
||||
separator = _t('or');
|
||||
} else if (this.facet.type === 'field') {
|
||||
separator = _t('or');
|
||||
} else if (this.facet.type === 'groupBy') {
|
||||
separator = '>';
|
||||
}
|
||||
return separator;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onFacetRemove: function () {
|
||||
this.trigger_up('facet_removed', {group: this.facet});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydown: function (ev) {
|
||||
switch (ev.which) {
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
this.trigger_up('facet_removed', {group: this.facet});
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return SearchFacet;
|
||||
|
||||
});
|
||||
+27
-25
@@ -4,12 +4,13 @@ odoo.define('web.search_filters', function (require) {
|
||||
var core = require('web.core');
|
||||
var datepicker = require('web.datepicker');
|
||||
var field_utils = require('web.field_utils');
|
||||
var search_filters_registry = require('web.search_filters_registry');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{
|
||||
var ExtendedSearchProposition = Widget.extend({
|
||||
template: 'SearchView.extended_search.proposition',
|
||||
events: {
|
||||
'change .o_searchview_extended_prop_field': 'changed',
|
||||
@@ -25,10 +26,7 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @constructs instance.web.search.ExtendedSearchProposition
|
||||
* @extends instance.web.Widget
|
||||
*
|
||||
* @param parent
|
||||
* @override
|
||||
* @param fields
|
||||
*/
|
||||
init: function (parent, fields) {
|
||||
@@ -46,8 +44,8 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext
|
||||
},
|
||||
changed: function () {
|
||||
var nval = this.$(".o_searchview_extended_prop_field").val();
|
||||
if(this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) {
|
||||
this.select_field(_.detect(this.fields, function (x) {return x.name == nval;}));
|
||||
if(this.attrs.selected === null || this.attrs.selected === undefined || nval !== this.attrs.selected.name) {
|
||||
this.select_field(_.detect(this.fields, function (x) {return x.name === nval;}));
|
||||
}
|
||||
},
|
||||
operator_changed: function (e) {
|
||||
@@ -71,7 +69,7 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext
|
||||
}
|
||||
|
||||
var type = field.type;
|
||||
var Field = core.search_filters_registry.getAny([type, "char"]);
|
||||
var Field = search_filters_registry.getAny([type, "char"]);
|
||||
|
||||
this.value = new Field(this, field);
|
||||
_.each(this.value.operators, function (operator) {
|
||||
@@ -84,8 +82,9 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext
|
||||
|
||||
},
|
||||
get_filter: function () {
|
||||
if (this.attrs.selected === null || this.attrs.selected === undefined)
|
||||
if (this.attrs.selected === null || this.attrs.selected === undefined) {
|
||||
return null;
|
||||
}
|
||||
var field = this.attrs.selected,
|
||||
op_select = this.$('.o_searchview_extended_prop_op')[0],
|
||||
operator = op_select.options[op_select.selectedIndex];
|
||||
@@ -172,6 +171,15 @@ var Char = Field.extend({
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
get_domain: function (field, operator) {
|
||||
switch (operator.value) {
|
||||
case '∃': return [[field.name, '!=', false]];
|
||||
case '∄': return [[field.name, '=', false]];
|
||||
// does not work in all cases
|
||||
// default: return "[('" + field.name + "', '" + operator.value + "', '" + this.get_value() + "')]";
|
||||
default: return [[field.name, operator.value, this.get_value()]];
|
||||
}
|
||||
},
|
||||
get_value: function () {
|
||||
return this.$el.val();
|
||||
}
|
||||
@@ -358,23 +366,17 @@ var Boolean = Field.extend({
|
||||
}
|
||||
});
|
||||
|
||||
core.search_filters_registry
|
||||
.add('char', Char)
|
||||
.add('text', Char)
|
||||
.add('one2many', Char)
|
||||
.add('many2one', Char)
|
||||
.add('many2many', Char)
|
||||
.add('datetime', DateTime)
|
||||
.add('date', Date)
|
||||
.add('integer', Integer)
|
||||
.add('float', Float)
|
||||
.add('monetary', Float)
|
||||
.add('boolean', Boolean)
|
||||
.add('selection', Selection)
|
||||
.add('id', Id);
|
||||
|
||||
return {
|
||||
ExtendedSearchProposition: ExtendedSearchProposition
|
||||
Boolean: Boolean,
|
||||
Char: Char,
|
||||
Date: Date,
|
||||
DateTime: DateTime,
|
||||
ExtendedSearchProposition: ExtendedSearchProposition,
|
||||
Field: Field,
|
||||
Float: Float,
|
||||
Id: Id,
|
||||
Integer: Integer,
|
||||
Selection: Selection,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
odoo.define('web.search_filters_registry', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
return new Registry();
|
||||
|
||||
});
|
||||
|
||||
odoo.define('web._search_filters_registry', function(require) {
|
||||
"use strict";
|
||||
|
||||
var registry = require('web.search_filters_registry');
|
||||
var search_filters = require('web.search_filters');
|
||||
|
||||
registry
|
||||
.add('boolean', search_filters.Boolean)
|
||||
.add('char', search_filters.Char)
|
||||
.add('date', search_filters.Date)
|
||||
.add('datetime', search_filters.DateTime)
|
||||
.add('float', search_filters.Float)
|
||||
.add('id', search_filters.Id)
|
||||
.add('integer', search_filters.Integer)
|
||||
.add('many2many', search_filters.Char)
|
||||
.add('many2one', search_filters.Char)
|
||||
.add('monetary', search_filters.Float)
|
||||
.add('one2many', search_filters.Char)
|
||||
.add('text', search_filters.Char)
|
||||
.add('selection', search_filters.Selection);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
odoo.define('web.TimeRangeMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var controlPanelViewParameters = require('web.controlPanelViewParameters');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var COMPARISON_TIME_RANGE_OPTIONS = controlPanelViewParameters.COMPARISON_TIME_RANGE_OPTIONS;
|
||||
var PERIOD_OPTIONS = controlPanelViewParameters.PERIOD_OPTIONS;
|
||||
|
||||
var TimeRangeMenu = Widget.extend({
|
||||
template: 'web.TimeRangeMenu',
|
||||
events: {
|
||||
'click .o_apply_range': '_onApplyButtonClick',
|
||||
'click .o_comparison_checkbox': '_onCheckBoxClick',
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {Object[]} timeRanges
|
||||
*
|
||||
*/
|
||||
init: function (parent, timeRanges) {
|
||||
this._super.apply(this, arguments);
|
||||
// determine header style
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.symbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : 'caret';
|
||||
// fixed parameters
|
||||
this.periodOptions = PERIOD_OPTIONS;
|
||||
this.comparisonTimeRangeOptions = COMPARISON_TIME_RANGE_OPTIONS;
|
||||
this.periodGroups = _.uniq(PERIOD_OPTIONS.map(function (option) {
|
||||
return option.groupId;
|
||||
}));
|
||||
// variable parameters
|
||||
this.timeRanges = timeRanges;
|
||||
this.configuration = {
|
||||
comparisonIsSelected: false,
|
||||
comparisonTimeRangeId: false,
|
||||
id: false,
|
||||
timeRangeId: false,
|
||||
};
|
||||
this._configure();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {Object[]} timeRanges
|
||||
*/
|
||||
update: function (timeRanges) {
|
||||
this.timeRanges = timeRanges;
|
||||
this._configure();
|
||||
this.renderElement();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_configure: function () {
|
||||
this.configuration = this.timeRanges.find(function (timeRange) {
|
||||
return timeRange.isActive;
|
||||
}) || this.configuration;
|
||||
this.configuration.comparisonIsSelected = !!this.configuration.comparisonTimeRangeId;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onApplyButtonClick: function () {
|
||||
var id = this.$('.o_date_field_selector').val();
|
||||
var timeRangeId = this.$('.o_time_range_selector').val();
|
||||
var comparisonTimeRangeId = false;
|
||||
if (this.configuration.comparisonIsSelected) {
|
||||
comparisonTimeRangeId = this.$('.o_comparison_time_range_selector').val();
|
||||
}
|
||||
this.trigger_up('activate_time_range', {
|
||||
id: id,
|
||||
timeRangeId: timeRangeId,
|
||||
comparisonTimeRangeId: comparisonTimeRangeId
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onCheckBoxClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.configuration.comparisonIsSelected = this.$('.o_comparison_checkbox').prop('checked');
|
||||
this.$('.o_comparison_time_range_selector').toggleClass('o_hidden');
|
||||
this.$el.addClass('open');
|
||||
}
|
||||
});
|
||||
|
||||
return TimeRangeMenu;
|
||||
|
||||
});
|
||||
@@ -132,7 +132,7 @@ var FormController = BasicController.extend({
|
||||
* @param {jQueryElement} $node
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
var $footer = this.footerToButtons ? this.$('footer') : null;
|
||||
var $footer = this.footerToButtons ? this.renderer.$('footer') : null;
|
||||
var mustRenderFooterButtons = $footer && $footer.length;
|
||||
if (!this.defaultButtons && !mustRenderFooterButtons) {
|
||||
return;
|
||||
@@ -219,7 +219,7 @@ var FormController = BasicController.extend({
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function (changedFields) {
|
||||
// the title could have been changed
|
||||
self.set('title', self.getTitle());
|
||||
self._setTitle(self.getTitle());
|
||||
self._updateEnv();
|
||||
|
||||
if (_t.database.multi_lang && changedFields.length) {
|
||||
@@ -250,6 +250,9 @@ var FormController = BasicController.extend({
|
||||
* @override
|
||||
*/
|
||||
update: function (params, options) {
|
||||
if ('currentId' in params && !params.currentId) {
|
||||
this.mode = 'edit'; // if there is no record, we are in 'edit' mode
|
||||
}
|
||||
params = _.extend({viewType: 'form', mode: this.mode}, params);
|
||||
return this._super(params, options);
|
||||
},
|
||||
@@ -395,7 +398,7 @@ var FormController = BasicController.extend({
|
||||
*/
|
||||
_update: function () {
|
||||
var title = this.getTitle();
|
||||
this.set('title', title);
|
||||
this._setTitle(title);
|
||||
this._updateButtons();
|
||||
this._updateSidebar();
|
||||
return this._super.apply(this, arguments).then(this.autofocus.bind(this));
|
||||
@@ -406,7 +409,7 @@ var FormController = BasicController.extend({
|
||||
_updateButtons: function () {
|
||||
if (this.$buttons) {
|
||||
if (this.footerToButtons) {
|
||||
var $footer = this.$('footer');
|
||||
var $footer = this.renderer.$('footer');
|
||||
if ($footer.length) {
|
||||
this.$buttons.empty().append($footer);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ var BasicRenderer = require('web.BasicRenderer');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
@@ -439,7 +440,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderHeaderButton: function (node) {
|
||||
var $button = this._renderButtonFromNode(node);
|
||||
var $button = viewUtils.renderButtonFromNode(node);
|
||||
|
||||
// Current API of odoo for rendering buttons is "if classes are given
|
||||
// use those on top of the 'btn' and 'btn-{size}' classes, otherwise act
|
||||
@@ -639,7 +640,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderStatButton: function (node) {
|
||||
var $button = this._renderButtonFromNode(node, {
|
||||
var $button = viewUtils.renderButtonFromNode(node, {
|
||||
extraClass: 'oe_stat_button',
|
||||
});
|
||||
$button.append(_.map(node.children, this._renderNode.bind(this)));
|
||||
@@ -685,7 +686,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderTagButton: function (node) {
|
||||
var $button = this._renderButtonFromNode(node);
|
||||
var $button = viewUtils.renderButtonFromNode(node);
|
||||
$button.append(_.map(node.children, this._renderNode.bind(this)));
|
||||
this._addOnClickAction($button, node);
|
||||
this._handleAttributes($button, node);
|
||||
|
||||
@@ -18,7 +18,8 @@ var FormView = BasicView.extend({
|
||||
display_name: _lt('Form'),
|
||||
icon: 'fa-edit',
|
||||
multi_record: false,
|
||||
searchable: false,
|
||||
withSearchBar: false,
|
||||
searchMenuTypes: [],
|
||||
jsLibs: [],
|
||||
viewType: 'form',
|
||||
/**
|
||||
@@ -27,17 +28,14 @@ var FormView = BasicView.extend({
|
||||
init: function (viewInfo, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var mode = params.mode || params.context.form_view_initial_mode ||
|
||||
(params.currentId ? 'readonly' : 'edit');
|
||||
var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
|
||||
this.loadParams.type = 'record';
|
||||
|
||||
this.controllerParams.disableAutofocus = params.disable_autofocus;
|
||||
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
||||
this.controllerParams.footerToButtons = params.footerToButtons;
|
||||
if ('action' in params && 'flags' in params.action) {
|
||||
this.controllerParams.footerToButtons = params.action.flags.footerToButtons;
|
||||
}
|
||||
|
||||
var defaultButtons = 'default_buttons' in params ? params.default_buttons : true;
|
||||
this.controllerParams.defaultButtons = defaultButtons;
|
||||
this.controllerParams.mode = mode;
|
||||
@@ -63,6 +61,26 @@ var FormView = BasicView.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {string} [action.target]
|
||||
*/
|
||||
_extractParamsFromAction: function (action) {
|
||||
var params = this._super.apply(this, arguments);
|
||||
var inDialog = action.target === 'new';
|
||||
var inline = action.target === 'inline';
|
||||
var fullscreen = action.target === 'fullscreen';
|
||||
params.withControlPanel = !(inDialog || inline);
|
||||
params.footerToButtons = inDialog;
|
||||
params.hasSearchView = inDialog ? false : params.hasSearchView;
|
||||
params.searchMenuTypes = inDialog ? [] : params.searchMenuTypes;
|
||||
if (inDialog || inline || fullscreen) {
|
||||
params.mode = 'edit';
|
||||
} else if (action.context && action.context.form_view_initial_mode) {
|
||||
params.mode = action.context.form_view_initial_mode;
|
||||
}
|
||||
return params;
|
||||
},
|
||||
/**
|
||||
* Loads the subviews for x2many fields when they are not inline
|
||||
*
|
||||
|
||||
@@ -6,13 +6,11 @@ odoo.define('web.GraphController', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var GroupByMenuInterfaceMixin = require('web.GroupByMenuInterfaceMixin');
|
||||
var GroupByMenuMixin = require('web.GroupByMenuMixin');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{
|
||||
className: 'o_graph',
|
||||
|
||||
var GraphController = AbstractController.extend(GroupByMenuMixin,{
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
@@ -24,19 +22,24 @@ var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{
|
||||
* @param {string[]} params.groupableFields,
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
GroupByMenuInterfaceMixin.init.call(this);
|
||||
GroupByMenuMixin.init.call(this);
|
||||
this._super.apply(this, arguments);
|
||||
this.measures = params.measures;
|
||||
// this parameter condition the appearance of a 'Group By'
|
||||
// button in the control panel owned by the graph view.
|
||||
this.isEmbedded = params.isEmbedded;
|
||||
|
||||
// this parameter determines what is the list of fields
|
||||
// that may be used within the groupby menu available when
|
||||
// the view is embedded
|
||||
this.groupableFields = params.groupableFields;
|
||||
// extends custom_events key. This allow the controller
|
||||
// to listen to information comming from the groupby menu.
|
||||
// this is used when the view is embedded.
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.addClass('o_graph_controller');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @todo check if this can be removed (mostly duplicate with
|
||||
@@ -62,18 +65,20 @@ var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{
|
||||
* @override
|
||||
* @returns {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
getOwnedQueryParams: function () {
|
||||
var state = this.model.get();
|
||||
return {
|
||||
graph_measure: state.measure,
|
||||
graph_mode: state.mode,
|
||||
graph_groupbys: state.groupedBy,
|
||||
// this parameter is not used anywher for now
|
||||
// the idea would be to seperate intervals from
|
||||
// fieldnames in groupbys. This could be done
|
||||
// in graph view only or everywhere but this is
|
||||
// a big refactoring.
|
||||
graph_intervalMapping: state.intervalMapping,
|
||||
context: {
|
||||
graph_measure: state.measure,
|
||||
graph_mode: state.mode,
|
||||
graph_groupbys: state.groupedBy,
|
||||
// this parameter is not used anywher for now
|
||||
// the idea would be to seperate intervals from
|
||||
// fieldnames in groupbys. This could be done
|
||||
// in graph view only or everywhere but this is
|
||||
// a big refactoring.
|
||||
graph_intervalMapping: state.intervalMapping,
|
||||
}
|
||||
};
|
||||
},
|
||||
/**
|
||||
@@ -110,10 +115,10 @@ var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} groupbys
|
||||
* @param {string[]} groupBys
|
||||
*/
|
||||
_setGroupby: function (groupbys) {
|
||||
this.update({groupBy: groupbys});
|
||||
_setGroupby: function (groupBys) {
|
||||
this.update({groupBy: groupBys});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@ var CHART_TYPES = ['pie', 'bar', 'line'];
|
||||
var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class));
|
||||
|
||||
return AbstractRenderer.extend({
|
||||
className: "o_graph_container",
|
||||
className: "o_graph_renderer",
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
|
||||
@@ -36,7 +36,8 @@ var GraphView = AbstractView.extend({
|
||||
Renderer: GraphRenderer,
|
||||
},
|
||||
viewType: 'graph',
|
||||
enableTimeRangeMenu: 'true',
|
||||
searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
|
||||
@@ -238,7 +238,7 @@ var KanbanController = BasicController.extend({
|
||||
self.renderer.updateColumn(db_id, data);
|
||||
});
|
||||
});
|
||||
}).fail(this.reload.bind(this));
|
||||
}).fail(this.reload.bind(this, {}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -279,7 +279,7 @@ var KanbanController = BasicController.extend({
|
||||
} else if (this.on_create && this.on_create !== 'quick_create') {
|
||||
// Execute the given action
|
||||
this.do_action(this.on_create, {
|
||||
on_close: this.reload.bind(this),
|
||||
on_close: this.reload.bind(this, {}),
|
||||
additional_context: state.context,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -30,13 +30,11 @@ var KanbanView = BasicView.extend({
|
||||
*/
|
||||
init: function (viewInfo, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.loadParams.limit = this.loadParams.limit || 40;
|
||||
// in mobile, columns are lazy-loaded, so set 'openGroupByDefault' to
|
||||
// false so that they will won't be loaded by the initial load
|
||||
this.loadParams.openGroupByDefault = config.device.isMobile ? false : true;
|
||||
this.loadParams.type = 'list';
|
||||
this.loadParams.groupBy = this.arch.attrs.default_group_by ? [this.arch.attrs.default_group_by] : (params.groupBy || []);
|
||||
var progressBar;
|
||||
utils.traverse(this.arch, function (n) {
|
||||
var isProgressBar = (n.tag === 'progressbar');
|
||||
@@ -106,6 +104,11 @@ var KanbanView = BasicView.extend({
|
||||
}
|
||||
return true;
|
||||
},
|
||||
_updateMVCParams: function () {
|
||||
this._super.apply(this, arguments);
|
||||
var defaultGroupBy = this.arch.attrs.default_group_by;
|
||||
this.loadParams.groupBy = defaultGroupBy ? [defaultGroupBy] : (this.loadParams.groupedBy || []);
|
||||
},
|
||||
});
|
||||
|
||||
return KanbanView;
|
||||
|
||||
@@ -56,10 +56,6 @@ var QuickCreateFormModel = BasicModel.extend({
|
||||
});
|
||||
|
||||
var QuickCreateFormController = FormController.extend({
|
||||
custom_events: _.extend({}, FormController.prototype.custom_events, {
|
||||
env_updated: '_onEnvUpdated',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -83,23 +79,10 @@ var QuickCreateFormController = FormController.extend({
|
||||
getChanges: function () {
|
||||
return this.model.getChanges(this.handle);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stops the propagation of the 'env_updated' event to prevent interferences
|
||||
* with the kanban controller.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onEnvUpdated: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
var QuickCreateFormView = FormView.extend({
|
||||
withControlPanel: false,
|
||||
config: _.extend({}, FormView.prototype.config, {
|
||||
Model: QuickCreateFormModel,
|
||||
Renderer: QuickCreateFormRenderer,
|
||||
|
||||
@@ -87,10 +87,10 @@ var ListController = BasicController.extend({
|
||||
/*
|
||||
* @override
|
||||
*/
|
||||
getContext: function () {
|
||||
var context = this._super.apply(this, arguments);
|
||||
context.orderedBy = this.model.get(this.handle, {raw: true}).orderedBy || [];
|
||||
return context;
|
||||
getOwnedQueryParams: function () {
|
||||
var state = this._super.apply(this, arguments);
|
||||
var orderedBy = this.model.get(this.handle, {raw: true}).orderedBy || [];
|
||||
return _.extend({}, state, {orderedBy: orderedBy});
|
||||
},
|
||||
/**
|
||||
* Returns the list of currently selected res_ids (with the check boxes on
|
||||
|
||||
@@ -8,6 +8,7 @@ var dom = require('web.dom');
|
||||
var field_utils = require('web.field_utils');
|
||||
var Pager = require('web.Pager');
|
||||
var utils = require('web.utils');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -329,7 +330,7 @@ var ListRenderer = BasicRenderer.extend({
|
||||
*/
|
||||
_renderButton: function (record, node) {
|
||||
var self = this;
|
||||
var $button = this._renderButtonFromNode(node, {
|
||||
var $button = viewUtils.renderButtonFromNode(node, {
|
||||
extraClass: node.attrs.icon ? 'o_icon_button' : undefined,
|
||||
textAsTitle: !!node.attrs.icon,
|
||||
});
|
||||
|
||||
@@ -42,7 +42,6 @@ var ListView = BasicView.extend({
|
||||
this.controllerParams.editable = this.arch.attrs.editable;
|
||||
this.controllerParams.hasSidebar = params.hasSidebar;
|
||||
this.controllerParams.toolbarActions = viewInfo.toolbar;
|
||||
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
|
||||
this.controllerParams.mode = mode;
|
||||
this.controllerParams.selectedRecords = selectedRecords;
|
||||
|
||||
@@ -55,6 +54,15 @@ var ListView = BasicView.extend({
|
||||
this.loadParams.limit = this.loadParams.limit || 80;
|
||||
this.loadParams.type = 'list';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//---
|
||||
|
||||
_updateMVCParams: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
|
||||
},
|
||||
});
|
||||
|
||||
return ListView;
|
||||
|
||||
@@ -22,7 +22,7 @@ var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var PivotController = AbstractController.extend({
|
||||
template: 'PivotView',
|
||||
contentTemplate: 'PivotView',
|
||||
events: {
|
||||
'click .o_pivot_header_cell_opened': '_onOpenHeaderClick',
|
||||
'click .o_pivot_header_cell_closed': '_onClosedHeaderClick',
|
||||
@@ -83,12 +83,14 @@ var PivotController = AbstractController.extend({
|
||||
* @override method from AbstractController
|
||||
* @returns {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
getOwnedQueryParams: function () {
|
||||
var state = this.model.get();
|
||||
return {
|
||||
pivot_measures: state.measures,
|
||||
pivot_column_groupby: state.colGroupBys,
|
||||
pivot_row_groupby: state.rowGroupBys,
|
||||
context: {
|
||||
pivot_measures: state.measures,
|
||||
pivot_column_groupby: state.colGroupBys,
|
||||
pivot_row_groupby: state.rowGroupBys,
|
||||
}
|
||||
};
|
||||
},
|
||||
/**
|
||||
@@ -171,6 +173,13 @@ var PivotController = AbstractController.extend({
|
||||
.css(cssProps)
|
||||
.addClass('show');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_startRenderer: function () {
|
||||
return this.renderer.appendTo(this.$('.o_pivot'));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,8 @@ var PivotView = AbstractView.extend({
|
||||
Renderer: PivotRenderer,
|
||||
},
|
||||
viewType: 'pivot',
|
||||
enableTimeRangeMenu: 'true',
|
||||
searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
@@ -117,10 +118,9 @@ var PivotView = AbstractView.extend({
|
||||
this.controllerParams.groupableFields = groupableFields;
|
||||
// retrieve form and list view ids from the action to open those views
|
||||
// when a data cell of the pivot view is clicked
|
||||
|
||||
this.controllerParams.views = [
|
||||
_findView(params.action && params.action.views, 'list'),
|
||||
_findView(params.action && params.action.views, 'form'),
|
||||
_findView(params.actionViews, 'list'),
|
||||
_findView(params.actionViews, 'form'),
|
||||
];
|
||||
function _findView(views, viewType) {
|
||||
var view = _.find(views, function (view) {
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
odoo.define('web.FavoriteMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var data_manager = require('web.data_manager');
|
||||
var Domain = require('web.Domain');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
return Widget.extend({
|
||||
template: 'SearchView.FavoriteMenu',
|
||||
events: {
|
||||
'click .dropdown-item': function (ev) {
|
||||
ev.preventDefault();
|
||||
},
|
||||
'click .o_save_search': function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.toggle_save_menu();
|
||||
},
|
||||
'click .o_save_name button': 'save_favorite',
|
||||
'hidden.bs.dropdown': '_closeMenus',
|
||||
'keyup .o_save_name input': function (ev) {
|
||||
if (ev.which === $.ui.keyCode.ENTER) {
|
||||
this.save_favorite();
|
||||
}
|
||||
},
|
||||
},
|
||||
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 = action;
|
||||
this.action_id = action.id;
|
||||
this.filters = {};
|
||||
this.isMobile = config.device.isMobile;
|
||||
_.each(filters, this.add_filter.bind(this));
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$filters = {};
|
||||
this.$save_search = this.$('.o_save_search');
|
||||
this.$save_name = this.$('.o_save_name');
|
||||
this.$inputs = this.$save_name.find('input');
|
||||
this.$user_divider = this.$('.dropdown-divider.user_filter');
|
||||
this.$shared_divider = this.$('.dropdown-divider.shared_filter');
|
||||
this.$inputs.eq(0).val(this.searchview.get_title());
|
||||
var $shared_filter = this.$inputs.eq(1),
|
||||
$default_filter = this.$inputs.eq(2);
|
||||
$shared_filter.click(function () {$default_filter.prop('checked', false);});
|
||||
$default_filter.click(function () {$shared_filter.prop('checked', false);});
|
||||
|
||||
this.query
|
||||
.on('remove', function (facet) {
|
||||
if (facet.get('is_custom_filter')) {
|
||||
self.clear_selection();
|
||||
}
|
||||
})
|
||||
.on('reset', this.proxy('clear_selection'));
|
||||
|
||||
_.each(this.filters, this.append_filter.bind(this));
|
||||
|
||||
return this._super();
|
||||
},
|
||||
toggle_save_menu: function (is_open) {
|
||||
this.$save_search
|
||||
.toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
|
||||
.toggleClass('o_open_menu', is_open);
|
||||
this.$save_name.toggle(is_open);
|
||||
if (this.$save_search.hasClass('o_open_menu')) {
|
||||
this.$save_name.find('input').first().focus();
|
||||
}
|
||||
},
|
||||
_closeMenus: function () {
|
||||
this.toggle_save_menu(false);
|
||||
},
|
||||
save_favorite: function () {
|
||||
var self = this,
|
||||
filter_name = this.$inputs[0].value,
|
||||
default_filter = this.$inputs[1].checked,
|
||||
shared_filter = this.$inputs[2].checked;
|
||||
if (!filter_name.length){
|
||||
this.do_warn(_t("Error"), _t("Filter name is required."));
|
||||
this.$inputs.first().focus();
|
||||
return;
|
||||
}
|
||||
if (_.chain(this.filters)
|
||||
.pluck('name')
|
||||
.contains(filter_name).value()) {
|
||||
this.do_warn(_t("Error"), _t("Filter with same name already exists."));
|
||||
this.$inputs.first().focus();
|
||||
return;
|
||||
}
|
||||
var user_context = this.getSession().user_context;
|
||||
// get search params with domains not evaluated
|
||||
var search = this.searchview.build_search_data(true);
|
||||
var domain = pyUtils.assembleDomains(search.domains, 'AND');
|
||||
var controllerContext;
|
||||
this.trigger_up('get_controller_context', {
|
||||
callback: function (ctx) {
|
||||
if (ctx && ctx.orderedBy) {
|
||||
var sort = _.map(ctx.orderedBy, function (obj) {
|
||||
var order = obj.asc ? 'ASC' : 'DESC';
|
||||
return obj.name + ' ' + order;
|
||||
});
|
||||
self.searchview.dataset.set_sort(sort);
|
||||
}
|
||||
controllerContext = _.omit(ctx, ['orderedBy']);
|
||||
},
|
||||
});
|
||||
|
||||
var results = pyUtils.eval_domains_and_contexts({
|
||||
domains: [],
|
||||
contexts: [user_context].concat(search.contexts.concat(controllerContext || [])),
|
||||
group_by_seq: search.groupbys || [],
|
||||
});
|
||||
if (!_.isEmpty(results.group_by)) {
|
||||
results.context.group_by = results.group_by;
|
||||
}
|
||||
// Don't save user_context keys in the custom filter, otherwise end
|
||||
// up with e.g. wrong uid or lang stored *and used in subsequent
|
||||
// reqs*
|
||||
var ctx = results.context;
|
||||
_(_.keys(session.user_context)).each(function (key) {
|
||||
delete ctx[key];
|
||||
});
|
||||
var filter = {
|
||||
name: filter_name,
|
||||
user_id: shared_filter ? false : session.uid,
|
||||
model_id: this.target_model,
|
||||
context: results.context,
|
||||
domain: domain,
|
||||
sort: JSON.stringify(this.searchview.dataset._sort),
|
||||
is_default: default_filter,
|
||||
action_id: this.action_id,
|
||||
};
|
||||
return this._createFilter(filter).then(function (id) {
|
||||
filter.id = id;
|
||||
self.toggle_save_menu(false);
|
||||
self.$save_name.find('input').val('').prop('checked', false);
|
||||
self.add_filter(filter);
|
||||
self.append_filter(filter);
|
||||
self.toggle_filter(filter, true);
|
||||
});
|
||||
},
|
||||
get_default_filter: function () {
|
||||
var personal_filter = _.find(this.filters, function (filter) {
|
||||
return filter.user_id && filter.is_default;
|
||||
});
|
||||
if (personal_filter) {
|
||||
return personal_filter;
|
||||
}
|
||||
return _.find(this.filters, function (filter) {
|
||||
return !filter.user_id && filter.is_default;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Generates a mapping key (in the filters and $filter mappings) for the
|
||||
* filter descriptor object provided (as returned by ``get_filters``).
|
||||
*
|
||||
* The mapping key is guaranteed to be unique for a given (user_id, name)
|
||||
* pair.
|
||||
*
|
||||
* @param {Object} filter
|
||||
* @param {String} filter.name
|
||||
* @param {Number|Pair<Number, String>} [filter.user_id]
|
||||
* @return {String} mapping key corresponding to the filter
|
||||
*/
|
||||
key_for: function (filter) {
|
||||
var user_id = filter.user_id,
|
||||
action_id = filter.action_id,
|
||||
uid = (user_id instanceof Array) ? user_id[0] : user_id,
|
||||
act_id = (action_id instanceof Array) ? action_id[0] : action_id;
|
||||
return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name);
|
||||
},
|
||||
/**
|
||||
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
|
||||
* filter descriptor
|
||||
*
|
||||
* @param {Object} filter
|
||||
* @param {String} filter.name
|
||||
* @param {Object} [filter.context]
|
||||
* @param {Array} [filter.domain]
|
||||
* @return {Object}
|
||||
*/
|
||||
facet_for: function (filter) {
|
||||
var self = this;
|
||||
return {
|
||||
category: _t("Custom Filter"),
|
||||
icon: 'fa-star',
|
||||
field: {
|
||||
get_context: function () {
|
||||
var sortParsed = JSON.parse(filter.sort || "[]");
|
||||
var orderedBy = [];
|
||||
_.each(sortParsed, function (sort) {
|
||||
orderedBy.push({
|
||||
name: sort[0] === '-' ? sort.slice(1) : sort,
|
||||
asc: sort[0] === '-' ? false : true,
|
||||
});
|
||||
});
|
||||
|
||||
return _.defaults({}, filter.context, {orderedBy : orderedBy});
|
||||
},
|
||||
get_groupby: function () { return [filter.context]; },
|
||||
// facet is not used
|
||||
get_domain: function (facet, noEvaluation) {
|
||||
noEvaluation = noEvaluation || false;
|
||||
var userContext = self.getSession().user_context;
|
||||
var domain = pyUtils.assembleDomains([filter.domain]);
|
||||
if (!noEvaluation) {
|
||||
return Domain.prototype.stringToArray(domain, userContext);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
},
|
||||
_id: filter.id,
|
||||
is_custom_filter: true,
|
||||
values: [{label: filter.name, value: null}]
|
||||
};
|
||||
},
|
||||
clear_selection: function () {
|
||||
this.$('.selected').removeClass('selected');
|
||||
},
|
||||
/**
|
||||
* Adds a filter description to the filters dict
|
||||
* @param {Object} [filter] the filter description
|
||||
*/
|
||||
add_filter: function (filter) {
|
||||
this.filters[this.key_for(filter)] = filter;
|
||||
},
|
||||
/**
|
||||
* Creates a $filter JQuery node, adds it to the $filters dict and appends it to the filter menu
|
||||
* @param {Object} [filter] the filter description
|
||||
*/
|
||||
append_filter: function (filter) {
|
||||
var self = this;
|
||||
var key = this.key_for(filter);
|
||||
|
||||
if (filter.user_id) {
|
||||
this.$user_divider.show();
|
||||
} else {
|
||||
this.$shared_divider.show();
|
||||
}
|
||||
if (!(key in this.$filters)) {
|
||||
var $filter = $('<div>', {class: 'position-relative'})
|
||||
.addClass(filter.user_id ? 'o-searchview-custom-private'
|
||||
: 'o-searchview-custom-public')
|
||||
.append($('<a>', {href: '#', class: 'dropdown-item'}).text(filter.name))
|
||||
.append($('<span>', {
|
||||
class: 'fa fa-trash-o o-remove-filter',
|
||||
on: {
|
||||
click: function (event) {
|
||||
event.stopImmediatePropagation();
|
||||
self.remove_filter(filter, $filter, key);
|
||||
},
|
||||
},
|
||||
}))
|
||||
.insertBefore(filter.user_id ? this.$user_divider : this.$shared_divider);
|
||||
this.$filters[key] = $filter;
|
||||
}
|
||||
this.$filters[key].unbind('click').click(function () {
|
||||
self.toggle_filter(filter);
|
||||
});
|
||||
},
|
||||
toggle_filter: function (filter, preventSearch) {
|
||||
var current = this.query.find(function (facet) {
|
||||
return facet.get('_id') === filter.id;
|
||||
});
|
||||
if (current) {
|
||||
this.query.remove(current);
|
||||
this.$filters[this.key_for(filter)].find('.dropdown-item').removeClass('selected');
|
||||
return;
|
||||
}
|
||||
this.query.reset([this.facet_for(filter)], {
|
||||
preventSearch: preventSearch || false});
|
||||
|
||||
// Load sort settings on view
|
||||
if (!_.isUndefined(filter.sort)){
|
||||
var sort_items = JSON.parse(filter.sort);
|
||||
this.searchview.dataset.set_sort(sort_items);
|
||||
}
|
||||
|
||||
this.$filters[this.key_for(filter)].find('.dropdown-item').addClass('selected');
|
||||
},
|
||||
remove_filter: function (filter, $filter, key) {
|
||||
var self = this;
|
||||
var global_warning = _t("This filter is global and will be removed for everybody if you continue."),
|
||||
warning = _t("Are you sure that you want to remove this filter?");
|
||||
if (!confirm(filter.user_id ? warning : global_warning)) {
|
||||
return;
|
||||
}
|
||||
return data_manager
|
||||
.delete_filter(filter)
|
||||
.done(function () {
|
||||
$filter.remove();
|
||||
delete self.$filters[key];
|
||||
delete self.filters[key];
|
||||
var has_user_filter = _.find(self.filters, function(filter) { return filter.user_id; });
|
||||
var has_shared_filer = _.find(self.filters, function(filter) { return !filter.user_id; });
|
||||
if (!has_user_filter) {
|
||||
self.$user_divider.hide();
|
||||
}
|
||||
if (!has_shared_filer) {
|
||||
self.$shared_divider.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new search view filter.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} filter the filter description
|
||||
* @returns {$.Deferred} resolved with the RPC's result when it succeeds
|
||||
*/
|
||||
_createFilter: function (filter) {
|
||||
var def = $.Deferred();
|
||||
this.trigger_up('create_filter', {
|
||||
filter: filter,
|
||||
on_success: def.resolve.bind(def),
|
||||
});
|
||||
return def;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,253 +0,0 @@
|
||||
odoo.define('web.FiltersMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
var search_filters = require('web.search_filters');
|
||||
var time = require('web.time');
|
||||
var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var PERIOD_OPTIONS = TimeRangeMenuOptions.PeriodOptions;
|
||||
|
||||
var DEFAULT_PERIOD = 'this_month';
|
||||
|
||||
var FiltersMenu = DropdownMenu.extend({
|
||||
custom_events: {
|
||||
remove_proposition: '_onRemoveProposition',
|
||||
confirm_proposition: '_onConfirmProposition',
|
||||
},
|
||||
events: _.extend({}, DropdownMenu.prototype.events, {
|
||||
'click .o_add_custom_filter': '_onAddCustomFilterClick',
|
||||
'click .o_add_condition': '_appendProposition',
|
||||
'click .o_apply_filter': '_onApplyClick',
|
||||
':hover .o_item_option': '_onOptionHover',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {Object[]} filters list of filters (type IFilter below)
|
||||
* interface IFilter {
|
||||
* itemId: string; unique id associated with the filter
|
||||
* domain: string;
|
||||
* isPeriod: boolean
|
||||
* fieldName: string; fieldName used to generate a domain in case isDate is true (and domain empty)
|
||||
* description: string; label printed on screen
|
||||
* groupId: string;
|
||||
* isActive: boolean; (optional) determines if the filter is considered active
|
||||
* isOpen: boolean; (optional) in case there are options the submenu presenting the options
|
||||
* is opened or closed according to isOpen
|
||||
* isRemovable: boolean; (optional) can be removed from menu
|
||||
* options: array of objects with 'optionId' and 'description' keys; (optional)
|
||||
* defaultOptionId: string refers to an optionId (optional)
|
||||
* currentOptionId: string refers to an optionId that is activated if item is active (optional)
|
||||
* }
|
||||
* @param {Object} fields
|
||||
*/
|
||||
init: function (parent, filters, fields) {
|
||||
// determines where the filter menu is displayed and its style
|
||||
this.isMobile = config.device.isMobile;
|
||||
// determines list of options used by filter of type 'date'
|
||||
this.periodOptions = PERIOD_OPTIONS;
|
||||
this.defaultOptionId = DEFAULT_PERIOD;
|
||||
// determines when the 'Add custom filter' submenu is open
|
||||
this.generatorMenuIsOpen = false;
|
||||
this.propositions = [];
|
||||
this.fields = _.pick(fields, function (field, name) {
|
||||
return field.selectable !== false && name !== 'id';
|
||||
});
|
||||
this.fields.id = {string: 'ID', type: 'id', searchable: true};
|
||||
var dropdownHeader = {
|
||||
category: 'filterCategory',
|
||||
title: _t('Filters'),
|
||||
icon: 'fa fa-filter',
|
||||
symbol: this.isMobile ? 'fa fa-chevron-right float-right mt4' : false,
|
||||
};
|
||||
this._super(parent, dropdownHeader, filters, this.fields);
|
||||
},
|
||||
|
||||
/**
|
||||
* render the template used to add a new custom filter and append it
|
||||
* to the basic dropdown menu
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.$menu.addClass('o_filters_menu');
|
||||
var generatorMenu = QWeb.render('FiltersMenuGenerator', {widget: this});
|
||||
this.$menu.append(generatorMenu);
|
||||
this.$addCustomFilter = this.$menu.find('.o_add_custom_filter');
|
||||
this.$addFilterMenu = this.$menu.find('.o_add_filter_menu');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a proposition inside the custom filter edition menu
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Deferred}
|
||||
*/
|
||||
_appendProposition: function () {
|
||||
var prop = new search_filters.ExtendedSearchProposition(this, this.fields);
|
||||
this.propositions.push(prop);
|
||||
this.$('.o_apply_filter').prop('disabled', false);
|
||||
return prop.insertBefore(this.$addFilterMenu);
|
||||
},
|
||||
/**
|
||||
* Confirm a filter selection, creates it and add it to the menu
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_commitSearch: function () {
|
||||
var self = this;
|
||||
var filters = _.invoke(this.propositions, 'get_filter');
|
||||
_.each(filters, function (filter) {
|
||||
filter.attrs.domain = Domain.prototype.arrayToString(filter.attrs.domain);
|
||||
});
|
||||
var groupId = _.uniqueId('__group__');
|
||||
var data = [];
|
||||
filters.forEach(function (filter) {
|
||||
var filterName = _.uniqueId('__filter__');
|
||||
var filterItem = {
|
||||
itemId: filterName,
|
||||
description: filter.attrs.string,
|
||||
groupId: groupId,
|
||||
isActive: true,
|
||||
};
|
||||
self._prepareItem(filterItem);
|
||||
data.push({
|
||||
itemId: filterName,
|
||||
groupId: groupId,
|
||||
filter: filter,
|
||||
});
|
||||
self.items.push(filterItem);
|
||||
});
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('new_filters', data);
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
this._toggleCustomFilterMenu();
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item
|
||||
*/
|
||||
_prepareItem: function (item) {
|
||||
if (item.isPeriod) {
|
||||
item.options = this.periodOptions;
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_renderMenuItems: function () {
|
||||
var self= this;
|
||||
this._super.apply(this, arguments);
|
||||
// the following code adds tooltip on date options in order
|
||||
// to alert the user of the meaning of intervals
|
||||
var $options = this.$('.o_filters_menu .o_item_option');
|
||||
$options.each(function () {
|
||||
var $option = $(this);
|
||||
$option.tooltip({
|
||||
delay: { show: 500, hide: 0 },
|
||||
title: function () {
|
||||
var itemId = $option.attr('data-item_id');
|
||||
var optionId = $option.attr('data-option_id');
|
||||
var fieldName = _.findWhere(self.items, {itemId: itemId}).fieldName;
|
||||
var domain = Domain.prototype.constructDomain(fieldName, optionId, 'date', true);
|
||||
var evaluatedDomain = Domain.prototype.stringToArray(domain);
|
||||
var dateFormat = time.getLangDateFormat();
|
||||
var dateStart = moment(evaluatedDomain[1][2], "YYYY-MM-DD", 'en').format(dateFormat);
|
||||
var dateEnd = moment(evaluatedDomain[2][2], "YYYY-MM-DD", 'en').format(dateFormat);
|
||||
if (optionId === 'today' || optionId === 'yesterday') {
|
||||
return dateStart;
|
||||
}
|
||||
return _.str.sprintf(_t('From %s To %s'), dateStart, dateEnd);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Hide and display the sub menu which allows adding custom filters
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleCustomFilterMenu: function () {
|
||||
var self = this;
|
||||
this.generatorMenuIsOpen = !this.generatorMenuIsOpen;
|
||||
var def;
|
||||
if (this.generatorMenuIsOpen && !this.propositions.length) {
|
||||
def = this._appendProposition();
|
||||
}
|
||||
if (!this.generatorMenuIsOpen) {
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
}
|
||||
$.when(def).then(function () {
|
||||
self.$addCustomFilter
|
||||
.toggleClass('o_closed_menu', !self.generatorMenuIsOpen)
|
||||
.toggleClass('o_open_menu', self.generatorMenuIsOpen);
|
||||
self.$('.o_add_filter_menu').toggle();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onAddCustomFilterClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._toggleCustomFilterMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onApplyClick: function (event) {
|
||||
event.stopPropagation();
|
||||
this._commitSearch();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onConfirmProposition: function (event) {
|
||||
event.stopPropagation();
|
||||
this._commitSearch();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onRemoveProposition: function (event) {
|
||||
event.stopPropagation();
|
||||
this.propositions = _.without(this.propositions, event.target);
|
||||
if (!this.propositions.length) {
|
||||
this.$('.o_apply_filter').prop('disabled', true);
|
||||
}
|
||||
event.target.destroy();
|
||||
},
|
||||
});
|
||||
|
||||
return FiltersMenu;
|
||||
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
odoo.define('web.GroupByMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
|
||||
|
||||
var DEFAULT_INTERVAL = 'month';
|
||||
|
||||
var GroupByMenu = DropdownMenu.extend({
|
||||
events: _.extend({}, DropdownMenu.prototype.events,
|
||||
{
|
||||
'click .o_add_custom_group': '_onAddCustomGroupClick',
|
||||
'click button.o_apply_group': '_onButtonApplyClick',
|
||||
'click .o_group_selector': '_onGroupSelectorClick',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {Object[]} groupbys list of groupbys (type IGroupBy below)
|
||||
* interface IGroupBy {
|
||||
* itemId: string; unique id associated with the groupby
|
||||
* fieldName: string; field name without interval!
|
||||
* description: string; label printed on screen
|
||||
* groupId: string;
|
||||
* isDate: boolean;
|
||||
* isActive: boolean; (optional) determines if the groupby is considered active
|
||||
* isOpen: boolean; (optional) in case there are options the submenu presenting the options
|
||||
* is opened or closed according to isOpen
|
||||
* isRemovable: boolean; (optional) can be removed from menu
|
||||
* options: array of objects with 'optionId' and 'description' keys; (optional)
|
||||
* currentOptionId: string refers to an optionId that is activated if item is active (optional)
|
||||
* }
|
||||
* @param {Object} fields 'field_get' of a model: mapping from field name
|
||||
* to an object with its properties
|
||||
* @param {Object} options
|
||||
* @param {string} options.headerStyle conditions the style of the main button
|
||||
*/
|
||||
init: function (parent, groupbys, fields, options) {
|
||||
var self = this;
|
||||
this.fields = fields;
|
||||
// determines when the 'Add custom groupby' submenu is open
|
||||
this.generatorMenuIsOpen = false;
|
||||
// determines list of options used by groupbys of type 'date'
|
||||
this.intervalOptions = [
|
||||
{description: 'Day', optionId: 'day', groupId: 1},
|
||||
{description: 'Week', optionId: 'week', groupId: 1},
|
||||
{description: 'Month', optionId: 'month', groupId: 1},
|
||||
{description: 'Quarter', optionId: 'quarter', groupId: 1},
|
||||
{description: 'Year', optionId: 'year', groupId: 1},
|
||||
];
|
||||
this.defaultOptionId = DEFAULT_INTERVAL;
|
||||
this.groupableFields = [];
|
||||
_.each(fields, function (field, name) {
|
||||
if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) {
|
||||
self.groupableFields.push(_.extend({}, field, {
|
||||
name: name,
|
||||
isDate: _.contains(['date', 'datetime'], field.type),
|
||||
}));
|
||||
}
|
||||
});
|
||||
this.groupableFields = _.sortBy(this.groupableFields, 'string');
|
||||
_.each(groupbys, this._prepareItem.bind(this));
|
||||
// determines the list of field names that can be added to the menu
|
||||
// via the 'Add Custom Groupby' menu
|
||||
this.presentedFields = _.filter(this.groupableFields, function (field) {
|
||||
var groupByFields = _.pluck(groupbys, 'fieldName');
|
||||
return !_.contains(groupByFields, field.name);
|
||||
});
|
||||
// determines where the filter menu is displayed and partly its style
|
||||
this.isMobile = config.device.isMobile;
|
||||
// the default style of the groupby menu can be changed here using the options key 'headerStyle'
|
||||
var style;
|
||||
if (options && options.headerStyle === 'primary') {
|
||||
style = {
|
||||
el: {class: 'btn-group o_graph_groupbys_menu o_dropdown', attrs: {'role': 'group'}},
|
||||
mainButton: {class: 'btn btn-primary dropdown-toggle'},
|
||||
};
|
||||
}
|
||||
var dropdownHeader = {
|
||||
category: 'groupByCategory',
|
||||
title: _t('Group By'),
|
||||
icon: 'fa fa-bars',
|
||||
symbol: this.isMobile ? 'fa fa-chevron-right float-right mt4' : false,
|
||||
style: style,
|
||||
};
|
||||
this._super(parent, dropdownHeader, groupbys, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* render the template used to add a new custom groupby and append it
|
||||
* to the basic dropdown menu
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.$menu.addClass('o_group_by_menu');
|
||||
var $generatorMenu = QWeb.render('GroupbyMenuGenerator', {widget: this});
|
||||
this.$menu.append($generatorMenu);
|
||||
this.$addCustomGroup = this.$menu.find('.o_add_custom_group');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* method called via the 'Add Custom Groupby' menu to create
|
||||
* a new groupby and add it (activated) to the menu
|
||||
* In case the field is of type date, it is activated using
|
||||
* the option determined by the parameter 'DEFAULT_INTERVAL'
|
||||
*
|
||||
* @private
|
||||
* @param {string} fieldName
|
||||
*/
|
||||
_addGroupby: function (fieldName) {
|
||||
var field = _.findWhere(this.groupableFields, {name: fieldName});
|
||||
var groupbyName = _.uniqueId('__groupby__');
|
||||
var groupby = {
|
||||
itemId: groupbyName,
|
||||
description: field.string,
|
||||
fieldName: fieldName,
|
||||
groupId: _.uniqueId('__group__'),
|
||||
isActive: true,
|
||||
};
|
||||
var eventData = _.clone(groupby);
|
||||
this._prepareItem(groupby);
|
||||
if (groupby.hasOptions) {
|
||||
groupby.defaultOptionId = DEFAULT_INTERVAL;
|
||||
groupby.currentOptionId = DEFAULT_INTERVAL;
|
||||
groupby.isDate = true;
|
||||
eventData.optionId = groupby.currentOptionId;
|
||||
eventData.isDate = true;
|
||||
}
|
||||
this.items.push(groupby);
|
||||
var fieldIndex = this.presentedFields.indexOf(field);
|
||||
this.presentedFields.splice(fieldIndex, 1);
|
||||
this._renderGeneratorMenu();
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('new_groupby', eventData);
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item
|
||||
*/
|
||||
_prepareItem: function (item) {
|
||||
if (_.contains(['date', 'datetime'], this.fields[item.fieldName].type)) {
|
||||
item.options = this.intervalOptions;
|
||||
}
|
||||
// super has to be called here because we need to add options to groupby
|
||||
// before to call it since some keys in a groupby are initialized using
|
||||
// the keys 'options' and 'defaultOptionId'
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderGeneratorMenu: function () {
|
||||
this.$el.find('.o_generator_menu').remove();
|
||||
var $generatorMenu = QWeb.render('GroupbyMenuGenerator', {widget: this});
|
||||
this.$menu.append($generatorMenu);
|
||||
this.$addCustomGroup = this.$menu.find('.o_add_custom_group');
|
||||
this.$groupSelector = this.$menu.find('.o_group_selector');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_toggleCustomGroupMenu: function () {
|
||||
this.generatorMenuIsOpen = !this.generatorMenuIsOpen;
|
||||
this._renderGeneratorMenu();
|
||||
if (this.generatorMenuIsOpen) {
|
||||
this.$groupSelector.focus();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* toggle the 'Add Custom Group'
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onAddCustomGroupClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._toggleCustomGroupMenu();
|
||||
},
|
||||
/*
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
* @param {jQueryEvent} event
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.generatorMenuIsOpen = false;
|
||||
this._renderGeneratorMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onButtonApplyClick: function (event) {
|
||||
event.stopPropagation();
|
||||
var fieldName = this.$groupSelector.val();
|
||||
this._addGroupby(fieldName);
|
||||
this._toggleCustomGroupMenu();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onGroupSelectorClick: function (event) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
return GroupByMenu;
|
||||
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
odoo.define('web.GroupByMenuInterfaceMixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
var GroupByMenu = require('web.GroupByMenu');
|
||||
|
||||
/**
|
||||
* The aim of this mixin is to facilitate the interaction between
|
||||
* a view controller and a dropdown menu with its control panel
|
||||
* TO DO: the pivot subview has two types of groupbys so that it will not
|
||||
* understand the current implementation of this mixin
|
||||
*
|
||||
* @mixin
|
||||
* @name GroupByMenuInterfaceMixin
|
||||
*/
|
||||
var GroupByMenuInterfaceMixin = {
|
||||
|
||||
init: function () {
|
||||
this.custom_events = _.extend({}, this.custom_events, {
|
||||
menu_item_toggled: '_onMenuItemToggled',
|
||||
item_option_changed : '_onItemOptionChanged',});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* this function instantiate a widget GroupByMenu and
|
||||
* incorporate it to the control panel of the graph view.
|
||||
* This is used for instance in the dashboard view in enterprise
|
||||
* where there is no Group by menu in the search view because it
|
||||
* would not make sense to have one at the global level.
|
||||
* this function is called by renderButtons when the parameter
|
||||
* 'this.isEmbedded' is set to true.
|
||||
*
|
||||
* private
|
||||
* @param {jQuery} $node
|
||||
*/
|
||||
_addGroupByMenu: function ($node, groupableFields) {
|
||||
var groupbys = [];
|
||||
var activeGroupbys = [];
|
||||
_.each(this.model.get().groupedBy, function (groupby) {
|
||||
activeGroupbys.push({fieldName: groupby.split(':')[0], interval: groupby.split(':')[1] || false});
|
||||
});
|
||||
var groupId = '__group__1';
|
||||
_.each(groupableFields, function (field, fieldName) {
|
||||
var groupby = _.findWhere(activeGroupbys, {fieldName: fieldName});
|
||||
groupbys.push({
|
||||
isActive: groupby ? true : false,
|
||||
description: field.string,
|
||||
itemId: fieldName,
|
||||
fieldName: fieldName,
|
||||
groupId: groupId,
|
||||
defaultOptionId: groupby ? groupby.interval : false,
|
||||
});
|
||||
});
|
||||
groupbys = _.sortBy(groupbys, 'description');
|
||||
var groupByMenu = new GroupByMenu(this, groupbys, groupableFields, {headerStyle: 'primary'});
|
||||
groupByMenu.insertAfter($node.find('div:first'));
|
||||
},
|
||||
|
||||
/**
|
||||
* This method has to be implemented by the view controller
|
||||
* that needs to interpret the click in an appropriate
|
||||
* manner
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} groupbys
|
||||
*/
|
||||
_setGroupby: function (groupbys) {
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onItemOptionChanged: function (ev) {
|
||||
var fieldName = ev.data.itemId;
|
||||
var interval = ev.data.optionId;
|
||||
var groupbys = this.model.get().groupedBy;
|
||||
var groupbysNormalized = _.map(groupbys, function (groupby) {
|
||||
return groupby.split(':')[0];
|
||||
});
|
||||
var indexOfGroupby = groupbysNormalized.indexOf(fieldName);
|
||||
groupbys.splice(indexOfGroupby, 1, fieldName + ':' + interval);
|
||||
this._setGroupby(groupbys);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onMenuItemToggled: function (ev) {
|
||||
var fieldName = ev.data.itemId;
|
||||
var interval = ev.data.optionId;
|
||||
var groupbys = this.model.get().groupedBy;
|
||||
var groupbysNormalized = _.map(groupbys, function (groupby) {
|
||||
return groupby.split(':')[0];
|
||||
});
|
||||
var indexOfGroupby = groupbysNormalized.indexOf(fieldName);
|
||||
if (indexOfGroupby === -1) {
|
||||
groupbys.push(fieldName + (interval ? (':' + interval) : ''));
|
||||
} else {
|
||||
groupbys.splice(indexOfGroupby, 1);
|
||||
}
|
||||
this._setGroupby(groupbys);
|
||||
},
|
||||
};
|
||||
|
||||
return GroupByMenuInterfaceMixin;
|
||||
|
||||
});
|
||||
@@ -1,840 +0,0 @@
|
||||
odoo.define('web.search_inputs', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var field_utils = require('web.field_utils');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var time = require('web.time');
|
||||
var utils = require('web.utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var Input = Widget.extend( /** @lends instance.web.search.Input# */{
|
||||
/**
|
||||
* @constructs instance.web.search.Input
|
||||
* @extends instance.web.Widget
|
||||
*
|
||||
* @param parent
|
||||
*/
|
||||
init: function (parent) {
|
||||
this._super(parent);
|
||||
this.searchview = parent;
|
||||
this.load_attrs({});
|
||||
},
|
||||
/**
|
||||
* Fetch auto-completion values for the widget.
|
||||
*
|
||||
* The completion values should be an array of objects with keys category,
|
||||
* label, value prefixed with an object with keys type=section and label
|
||||
*
|
||||
* @param {String} value value to complete
|
||||
* @returns {jQuery.Deferred<null|Array>}
|
||||
*/
|
||||
complete: function (value) {
|
||||
return $.when(null);
|
||||
},
|
||||
/**
|
||||
* Returns a Facet instance for the provided defaults if they apply to
|
||||
* this widget, or null if they don't.
|
||||
*
|
||||
* This default implementation will try calling
|
||||
* :js:func:`instance.web.search.Input#facet_for` if the widget's name
|
||||
* matches the input key
|
||||
*
|
||||
* @param {Object} defaults
|
||||
* @returns {jQuery.Deferred<null|Object>}
|
||||
*/
|
||||
facet_for_defaults: function (defaults) {
|
||||
if (!this.attrs ||
|
||||
!(this.attrs.name in defaults && defaults[this.attrs.name])) {
|
||||
return $.when(null);
|
||||
}
|
||||
return this.facet_for(defaults[this.attrs.name]);
|
||||
},
|
||||
get_context: function () {
|
||||
throw new Error(
|
||||
"get_context not implemented for widget " + this.attrs.type);
|
||||
},
|
||||
get_groupby: function () {
|
||||
throw new Error(
|
||||
"get_groupby not implemented for widget " + this.attrs.type);
|
||||
},
|
||||
get_domain: function () {
|
||||
throw new Error(
|
||||
"get_domain not implemented for widget " + this.attrs.type);
|
||||
},
|
||||
load_attrs: function (attrs) {
|
||||
if (!_.isObject(attrs.modifiers)) {
|
||||
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
|
||||
}
|
||||
this.attrs = attrs;
|
||||
},
|
||||
/**
|
||||
* Returns whether the input is "visible". The default behavior is to
|
||||
* query the ``modifiers.invisible`` flag on the input's description or
|
||||
* view node.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
visible: function () {
|
||||
return !this.attrs.modifiers.invisible;
|
||||
},
|
||||
});
|
||||
|
||||
var Field = Input.extend( /** @lends instance.web.search.Field# */ {
|
||||
template: 'SearchView.field',
|
||||
default_operator: '=',
|
||||
/**
|
||||
* @constructs instance.web.search.Field
|
||||
* @extends instance.web.search.Input
|
||||
*
|
||||
* @param view_section
|
||||
* @param field
|
||||
* @param parent
|
||||
*/
|
||||
init: function (view_section, field, parent) {
|
||||
this._super(parent);
|
||||
this.load_attrs(_.extend({}, field, view_section.attrs));
|
||||
},
|
||||
facet_for: function (value) {
|
||||
return $.when({
|
||||
field: this,
|
||||
category: this.attrs.string || this.attrs.name,
|
||||
values: [{label: String(value), value: value}]
|
||||
});
|
||||
},
|
||||
value_from: function (facetValue) {
|
||||
return facetValue.get('value');
|
||||
},
|
||||
get_context: function (facet) {
|
||||
var self = this;
|
||||
// A field needs a context to send when active
|
||||
var context = this.attrs.context;
|
||||
if (_.isEmpty(context) || !facet.values.length) {
|
||||
return;
|
||||
}
|
||||
var contexts = facet.values.map(function (facetValue) {
|
||||
return new Context(context)
|
||||
.set_eval_context({self: self.value_from(facetValue)});
|
||||
});
|
||||
|
||||
if (contexts.length === 1) { return contexts[0]; }
|
||||
|
||||
return _.extend(new Context(), {
|
||||
__contexts: contexts
|
||||
});
|
||||
},
|
||||
get_groupby: function () {},
|
||||
/**
|
||||
* Function creating the returned domain for the field, override this
|
||||
* methods in children if you only need to customize the field's domain
|
||||
* without more complex alterations or tests (and without the need to
|
||||
* change override the handling of filter_domain)
|
||||
*
|
||||
* @param {String} name the field's name
|
||||
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
|
||||
* @param {Number|String} facet parsed value for the field
|
||||
* @returns {Array<Array>} domain to include in the resulting search
|
||||
*/
|
||||
make_domain: function (name, operator, facet) {
|
||||
return [[name, operator, this.value_from(facet)]];
|
||||
},
|
||||
get_domain: function (facet, noDomainEvaluation) {
|
||||
if (!facet.values.length) { return; }
|
||||
|
||||
var value_to_domain;
|
||||
var self = this;
|
||||
var domain = this.attrs.filter_domain;
|
||||
if (domain) {
|
||||
value_to_domain = function (facetValue) {
|
||||
return Domain.prototype.stringToArray(
|
||||
domain,
|
||||
{self: self.value_from(facetValue), raw_value: facetValue.attributes.value}
|
||||
);
|
||||
};
|
||||
} else {
|
||||
value_to_domain = function (facetValue) {
|
||||
return self.make_domain(
|
||||
self.attrs.name,
|
||||
self.attrs.operator || self.default_operator,
|
||||
facetValue
|
||||
);
|
||||
};
|
||||
}
|
||||
var domains = facet.values.map(value_to_domain);
|
||||
|
||||
// here 'noDomainEvaluation' means 'string representation'
|
||||
if (noDomainEvaluation) {
|
||||
domains = domains.map(Domain.prototype.arrayToString);
|
||||
return pyUtils.assembleDomains(domains, 'OR');
|
||||
}
|
||||
if (domains.length === 1) {
|
||||
return domains[0];
|
||||
}
|
||||
_.each(domains, Domain.prototype.normalizeArray);
|
||||
var ors = _.times(domains.length - 1, _.constant("|"));
|
||||
return ors.concat.apply(ors, domains);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Implementation of the ``char`` OpenERP field type:
|
||||
*
|
||||
* * Default operator is ``ilike`` rather than ``=``
|
||||
*
|
||||
* * The Javascript and the HTML values are identical (strings)
|
||||
*
|
||||
* @class
|
||||
* @extends instance.web.search.Field
|
||||
*/
|
||||
var CharField = Field.extend( /** @lends instance.web.search.CharField# */ {
|
||||
default_operator: 'ilike',
|
||||
complete: function (value) {
|
||||
if (_.isEmpty(value)) { return $.when(null); }
|
||||
var label = _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s for: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + _.escape(value) + '</strong>'});
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: value, value: value}]
|
||||
}
|
||||
}]);
|
||||
}
|
||||
});
|
||||
|
||||
var NumberField = Field.extend(/** @lends instance.web.search.NumberField# */{
|
||||
complete: function (value) {
|
||||
var val = this.parse(value);
|
||||
if (isNaN(val)) { return $.when(); }
|
||||
var label = _.str.sprintf(
|
||||
_t("Search %(field)s for: %(value)s"), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + _.escape(value) + '</strong>'});
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: value, value: val}]
|
||||
}
|
||||
}]);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends instance.web.search.NumberField
|
||||
*/
|
||||
var IntegerField = NumberField.extend(/** @lends instance.web.search.IntegerField# */{
|
||||
error_message: _t("not a valid integer"),
|
||||
parse: function (value) {
|
||||
try {
|
||||
return field_utils.parse.integer(value);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends instance.web.search.NumberField
|
||||
*/
|
||||
var FloatField = NumberField.extend(/** @lends instance.web.search.FloatField# */{
|
||||
error_message: _t("not a valid number"),
|
||||
parse: function (value) {
|
||||
try {
|
||||
return field_utils.parse.float(value);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility function for m2o & selection fields taking a selection/name_get pair
|
||||
* (value, name) and converting it to a Facet descriptor
|
||||
*
|
||||
* @param {instance.web.search.Field} field holder field
|
||||
* @param {Array} pair pair value to convert
|
||||
*/
|
||||
function facet_from(field, pair) {
|
||||
return {
|
||||
field: field,
|
||||
category: field.attrs.string,
|
||||
values: [{label: pair[1], value: pair[0]}]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends instance.web.search.Field
|
||||
*/
|
||||
var SelectionField = Field.extend(/** @lends instance.web.search.SelectionField# */{
|
||||
// This implementation is a basic <select> field, but it may have to be
|
||||
// altered to be more in line with the GTK client, which uses a combo box
|
||||
// (~ jquery.autocomplete):
|
||||
// * If an option was selected in the list, behave as currently
|
||||
// * If something which is not in the list was entered (via the text input),
|
||||
// the default domain should become (`ilike` string_value) but **any
|
||||
// ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
|
||||
// is specified. So at least get_domain needs to be quite a bit
|
||||
// overridden (if there's no @value and there is no filter_domain and
|
||||
// there is no @operator, return [[name, 'ilike', str_val]]
|
||||
template: 'SearchView.field.selection',
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// prepend empty option if there is no empty option in the selection list
|
||||
this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
|
||||
return !item[1];
|
||||
});
|
||||
},
|
||||
complete: function (needle) {
|
||||
var self = this;
|
||||
var results = _(this.attrs.selection).chain()
|
||||
.filter(function (sel) {
|
||||
var value = sel[0], label = sel[1];
|
||||
if (value === undefined || !label) { return false; }
|
||||
return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
|
||||
})
|
||||
.map(function (sel) {
|
||||
return {
|
||||
label: _.escape(sel[1]),
|
||||
indent: true,
|
||||
facet: facet_from(self, sel)
|
||||
};
|
||||
}).value();
|
||||
if (_.isEmpty(results)) { return $.when(null); }
|
||||
return $.when.call(null, [{
|
||||
label: _.escape(this.attrs.string)
|
||||
}].concat(results));
|
||||
},
|
||||
facet_for: function (value) {
|
||||
var match = _(this.attrs.selection).detect(function (sel) {
|
||||
return sel[0] === value;
|
||||
});
|
||||
if (!match) { return $.when(null); }
|
||||
return $.when(facet_from(this, match));
|
||||
}
|
||||
});
|
||||
|
||||
var BooleanField = SelectionField.extend(/** @lends instance.web.search.BooleanField# */{
|
||||
/**
|
||||
* @constructs instance.web.search.BooleanField
|
||||
* @extends instance.web.search.BooleanField
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.attrs.selection = [
|
||||
[true, _t("Yes")],
|
||||
[false, _t("No")]
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends instance.web.search.DateField
|
||||
*/
|
||||
var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
value_from: function (facetValue) {
|
||||
var serverFormat = (this.attrs && this.attrs.type === 'datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
return facetValue.get('valueAsMoment').clone().format(serverFormat);
|
||||
},
|
||||
complete: function (needle) {
|
||||
// Make sure the needle has a correct format before the creation of the moment object. See
|
||||
// issue https://github.com/moment/moment/issues/1407
|
||||
var t, v;
|
||||
try {
|
||||
t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
|
||||
v = field_utils.parse[t](needle, {type: t}, {timezone: true});
|
||||
} catch (e) {
|
||||
return $.when(null);
|
||||
}
|
||||
|
||||
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
|
||||
if (!m.isValid()) { return $.when(null); }
|
||||
var date_string = field_utils.format[t](m, {type: t});
|
||||
var label = _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s at: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + date_string + '</strong>'});
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: date_string, value: m.toDate(), valueAsMoment: m.clone()}]
|
||||
}
|
||||
}]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Implementation of the ``datetime`` openerp field type:
|
||||
*
|
||||
* * Uses the same widget as the ``date`` field type (a simple date)
|
||||
*
|
||||
* * Builds a slighly more complex, it's a datetime range (includes time)
|
||||
* spanning the whole day selected by the date widget
|
||||
*
|
||||
* @class
|
||||
* @extends instance.web.DateField
|
||||
*/
|
||||
var DateTimeField = DateField;
|
||||
|
||||
var ManyToOneField = CharField.extend({
|
||||
default_operator: {},
|
||||
init: function (view_section, field, parent) {
|
||||
this._super(view_section, field, parent);
|
||||
this.searchview = parent;
|
||||
},
|
||||
|
||||
complete: function (value) {
|
||||
if (_.isEmpty(value)) { return $.when(null); }
|
||||
var label = _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s for: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
value: '<strong>' + _.escape(value) + '</strong>'});
|
||||
return $.when([{
|
||||
label: label,
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: value, value: value, operator: 'ilike'}]
|
||||
},
|
||||
expand: this.expand.bind(this),
|
||||
}]);
|
||||
},
|
||||
|
||||
expand: function (needle) {
|
||||
var self = this;
|
||||
// FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
|
||||
var context = pyUtils.eval(
|
||||
'contexts', [this.searchview.dataset.get_context()]);
|
||||
var args = this.attrs.domain;
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = Domain.prototype.stringToArray(args);
|
||||
} catch(e) {
|
||||
args = [];
|
||||
}
|
||||
}
|
||||
return this._rpc({
|
||||
model: this.attrs.relation,
|
||||
method: 'name_search',
|
||||
kwargs: {
|
||||
name: needle,
|
||||
args: args,
|
||||
limit: 8,
|
||||
context: context
|
||||
},
|
||||
})
|
||||
.then(function (results) {
|
||||
if (_.isEmpty(results)) { return null; }
|
||||
return _(results).map(function (result) {
|
||||
return {
|
||||
label: _.escape(result[1]),
|
||||
facet: facet_from(self, result)
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
facet_for: function (value) {
|
||||
var self = this;
|
||||
if (value instanceof Array) {
|
||||
if (value.length === 2 && _.isString(value[1])) {
|
||||
return $.when(facet_from(this, value));
|
||||
}
|
||||
utils.assert(value.length <= 1,
|
||||
_t("M2O search fields do not currently handle multiple default values"));
|
||||
// there are many cases of {search_default_$m2ofield: [id]}, need
|
||||
// to handle this as if it were a single value.
|
||||
value = value[0];
|
||||
}
|
||||
var context = pyUtils.eval('contexts', [this.searchview.dataset.get_context()]);
|
||||
|
||||
return this._rpc({
|
||||
model: this.attrs.relation,
|
||||
method: 'name_get',
|
||||
args: [value],
|
||||
context: context,
|
||||
})
|
||||
.then(function (names) {
|
||||
if (_(names).isEmpty()) { return null; }
|
||||
return facet_from(self, names[0]);
|
||||
});
|
||||
},
|
||||
value_from: function (facetValue) {
|
||||
return facetValue.get('label');
|
||||
},
|
||||
make_domain: function (name, operator, facetValue) {
|
||||
operator = facetValue.get('operator') || operator;
|
||||
|
||||
switch(operator){
|
||||
case this.default_operator:
|
||||
return [[name, '=', facetValue.get('value')]];
|
||||
case 'ilike':
|
||||
return [[name, 'ilike', facetValue.get('value')]];
|
||||
case 'child_of':
|
||||
return [[name, 'child_of', facetValue.get('value')]];
|
||||
}
|
||||
return this._super(name, operator, facetValue);
|
||||
},
|
||||
get_context: function (facet) {
|
||||
var values = facet.values;
|
||||
if (_.isEmpty(this.attrs.context) && values.length === 1) {
|
||||
var c = {};
|
||||
var v = values.at(0);
|
||||
if (v.get('operator') !== 'ilike') {
|
||||
c['default_' + this.attrs.name] = v.get('value');
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return this._super(facet);
|
||||
}
|
||||
});
|
||||
|
||||
var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
template: 'SearchView.filters',
|
||||
events: {
|
||||
click: 'toggle_filter',
|
||||
},
|
||||
|
||||
icon: "fa-filter",
|
||||
completion_label: _lt("Filter on: %s"),
|
||||
|
||||
/**
|
||||
* Inclusive group of filters, creates a continuous "button" with clickable
|
||||
* sections (the normal display for filters is to be a self-contained button)
|
||||
*
|
||||
* @constructs instance.web.search.FilterGroup
|
||||
* @extends instance.web.search.Input
|
||||
*
|
||||
* @param {Array<instance.web.search.Filter>} filters elements of the group
|
||||
* @param {instance.web.SearchView} parent parent in which the filters are contained
|
||||
* @param {Object} intervalMapping, a key is a field name and the corresponding value
|
||||
* is the current interval used
|
||||
* (necessarily the field is of type 'date' or 'datetime')
|
||||
*/
|
||||
init: function (filters, parent, intervalMapping, periodMapping) {
|
||||
// If all filters are group_by and we're not initializing a GroupbyGroup,
|
||||
// create a GroupbyGroup instead of the current FilterGroup
|
||||
if (!(this instanceof GroupbyGroup) &&
|
||||
_(filters).all(function (f) {
|
||||
if (!f.attrs.context) { return false; }
|
||||
var c = pyUtils.eval('context', f.attrs.context);
|
||||
return !_.isEmpty(c.group_by);})) {
|
||||
return new GroupbyGroup(filters, parent, intervalMapping);
|
||||
}
|
||||
this._super(parent);
|
||||
this.filters = filters;
|
||||
this.searchview = parent;
|
||||
this.intervalMapping = intervalMapping;
|
||||
this.periodMapping = periodMapping || [];
|
||||
this.searchview.query.on('add remove change reset', this.proxy('search_change'));
|
||||
},
|
||||
start: function () {
|
||||
return $.when(null);
|
||||
},
|
||||
/**
|
||||
* Handles change of the search query: any of the group's filter which is
|
||||
* in the search query should be visually checked in the drawer
|
||||
*/
|
||||
search_change: function () {
|
||||
var self = this;
|
||||
var $filters = this.$el ? this.$el.removeClass('selected') : $();
|
||||
var facet = this.searchview.query.find(_.bind(this.match_facet, this));
|
||||
if (!facet) { return; }
|
||||
facet.values.each(function (v) {
|
||||
var i = _(self.filters).indexOf(v.get('value'));
|
||||
if (i === -1) { return; }
|
||||
$filters.filter(function () {
|
||||
return Number($(this).data('index')) === i;
|
||||
}).addClass('selected');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Matches the group to a facet, in order to find if the group is
|
||||
* represented in the current search query
|
||||
*/
|
||||
match_facet: function (facet) {
|
||||
return facet.get('field') === this;
|
||||
},
|
||||
make_facet: function (values) {
|
||||
return {
|
||||
cat: 'filterCategory',
|
||||
category: _t("Filter"),
|
||||
icon: this.icon,
|
||||
separator: _t(" or "),
|
||||
values: values,
|
||||
field: this
|
||||
};
|
||||
},
|
||||
make_value: function (filter) {
|
||||
return {
|
||||
label: filter.attrs.string || filter.attrs.help || filter.attrs.name,
|
||||
value: filter
|
||||
};
|
||||
},
|
||||
facet_for_defaults: function (defaults) {
|
||||
var self = this;
|
||||
var fs = _(this.filters).chain()
|
||||
.filter(function (f) {
|
||||
return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
|
||||
}).map(function (f) {
|
||||
return self.make_value(f);
|
||||
}).value();
|
||||
if (_.isEmpty(fs)) { return $.when(null); }
|
||||
return $.when(this.make_facet(fs));
|
||||
},
|
||||
/**
|
||||
* Fetches contexts for all enabled filters in the group
|
||||
*
|
||||
* @param {openerp.web.search.Facet} facet
|
||||
* @return {*} combined contexts of the enabled filters in this group
|
||||
*/
|
||||
get_context: function (facet) {
|
||||
var contexts = facet.values.chain()
|
||||
.map(function (f) { return f.get('value').attrs.context; })
|
||||
.without('{}')
|
||||
.reject(_.isEmpty)
|
||||
.value();
|
||||
|
||||
if (!contexts.length) { return; }
|
||||
if (contexts.length === 1) { return contexts[0]; }
|
||||
return _.extend(new Context(), {
|
||||
__contexts: contexts
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Fetches group_by sequence for all enabled filters in the group
|
||||
*
|
||||
* @param {VS.model.SearchFacet} facet
|
||||
* @return {Array} enabled filters in this group
|
||||
*/
|
||||
get_groupby: function (facet) {
|
||||
var self = this;
|
||||
return facet.values.chain()
|
||||
.map(function (f) {
|
||||
var fieldName = f.get('value').attrs.fieldName;
|
||||
var defaultInterval = f.get('value').attrs.defaultInterval;
|
||||
var couple = _.findWhere(self.intervalMapping, {groupby: f.get('value')});
|
||||
var interval = couple ? couple.interval: undefined;
|
||||
var context = _.clone(f.get('value').attrs.context);
|
||||
if (context && interval) {
|
||||
if (defaultInterval) {
|
||||
context = context.replace(defaultInterval, interval);
|
||||
} else if (fieldName){
|
||||
context = context.replace(fieldName, fieldName + ':' + interval);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
})
|
||||
.without('{}')
|
||||
.reject(_.isEmpty)
|
||||
.value();
|
||||
},
|
||||
/**
|
||||
* Handles domains-fetching for all the filters within it: groups them.
|
||||
*
|
||||
* @param {VS.model.SearchFacet} facet
|
||||
* @param {boolean} noEvaluation determines if the domains are evaluated or not.
|
||||
By default, domains are evaluated.
|
||||
* @return {*} combined domains of the enabled filters in this group
|
||||
*/
|
||||
get_domain: function (facet, noEvaluation) {
|
||||
noEvaluation = noEvaluation || false;
|
||||
var self = this;
|
||||
var userContext = this.getSession().user_context;
|
||||
var domains = facet.values.chain()
|
||||
.map(function (f) {
|
||||
var filter = f.get('value');
|
||||
var attributes = filter.attrs;
|
||||
var domain = attributes.domain;
|
||||
var fieldName = attributes.date;
|
||||
if (!domain && fieldName) {
|
||||
var couple = _.findWhere(self.periodMapping, {filter: filter});
|
||||
var period = couple ? couple.period: 'this_month';
|
||||
var type = couple ? couple.type: 'date';
|
||||
domain = Domain.prototype.constructDomain(fieldName, period, type);
|
||||
}
|
||||
return domain;
|
||||
})
|
||||
.without('[]')
|
||||
.reject(_.isEmpty)
|
||||
.map(function (domain) {
|
||||
domain = domain.replace(/%%/g, '%');
|
||||
return domain;
|
||||
})
|
||||
.value();
|
||||
|
||||
if (!domains.length) {
|
||||
return;
|
||||
}
|
||||
// domains belong the same groups. We need to make a union.
|
||||
var domain = pyUtils.assembleDomains(domains, 'OR');
|
||||
if (!noEvaluation) {
|
||||
return Domain.prototype.stringToArray(domain, userContext);
|
||||
}
|
||||
return domain;
|
||||
},
|
||||
toggle_filter: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggle(this.filters[Number($(e.currentTarget).data('index'))]);
|
||||
},
|
||||
toggle: function (filter, options) {
|
||||
this.searchview.query.toggle(this.make_facet([this.make_value(filter)]), options);
|
||||
},
|
||||
is_visible: function () {
|
||||
return _.some(this.filters, function (filter) {
|
||||
return !filter.attrs.invisible;
|
||||
});
|
||||
},
|
||||
complete: function (item) {
|
||||
var self = this;
|
||||
item = item.toLowerCase();
|
||||
var facet_values = _(this.filters).chain()
|
||||
.filter(function (filter) { return filter.visible(); })
|
||||
.filter(function (filter) {
|
||||
var at = {
|
||||
string: filter.attrs.string || '',
|
||||
help: filter.attrs.help || '',
|
||||
name: filter.attrs.name || ''
|
||||
};
|
||||
var include = _.str.include;
|
||||
return include(at.string.toLowerCase(), item)
|
||||
|| include(at.help.toLowerCase(), item)
|
||||
|| include(at.name.toLowerCase(), item);
|
||||
})
|
||||
.map(this.make_value)
|
||||
.value();
|
||||
if (_(facet_values).isEmpty()) { return $.when(null); }
|
||||
return $.when(_.map(facet_values, function (facet_value) {
|
||||
return {
|
||||
label: _.str.sprintf(self.completion_label.toString(),
|
||||
_.escape(facet_value.label)),
|
||||
facet: self.make_facet([facet_value])
|
||||
};
|
||||
}));
|
||||
},
|
||||
/*
|
||||
* @param {Object} intervalMapping
|
||||
*/
|
||||
updateIntervalMapping: function (intervalMapping) {
|
||||
this.intervalMapping = intervalMapping;
|
||||
},
|
||||
/*
|
||||
* @param {Object} periodMapping
|
||||
*/
|
||||
updatePeriodMapping: function (periodMapping) {
|
||||
this.intervalMapping = periodMapping;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
});
|
||||
|
||||
var GroupbyGroup = FilterGroup.extend({
|
||||
icon: 'fa-bars',
|
||||
completion_label: _lt("Group by: %s"),
|
||||
init: function (filters, parent) {
|
||||
this._super.apply(this, arguments);
|
||||
this.searchview = parent;
|
||||
// Not flanders: facet unicity is handled through the
|
||||
// (category, field) pair of facet attributes. This is all well and
|
||||
// good for regular filter groups where a group matches a facet, but for
|
||||
// groupby we want a single facet. So cheat: add an attribute on the
|
||||
// view which proxies to the first GroupbyGroup, so it can be used
|
||||
// for every GroupbyGroup and still provides the various methods needed
|
||||
// by the search view. Use weirdo name to avoid risks of conflicts
|
||||
if (!this.searchview._s_groupby) {
|
||||
this.searchview._s_groupby = {
|
||||
help: "See GroupbyGroup#init",
|
||||
get_context: this.proxy('get_context'),
|
||||
get_domain: this.proxy('get_domain'),
|
||||
get_groupby: this.proxy('get_groupby')
|
||||
};
|
||||
}
|
||||
},
|
||||
match_facet: function (facet) {
|
||||
return facet.get('field') === this.searchview._s_groupby;
|
||||
},
|
||||
make_facet: function (values) {
|
||||
return {
|
||||
cat: 'groupByCategory',
|
||||
category: _t("Group By"),
|
||||
icon: this.icon,
|
||||
separator: " > ",
|
||||
values: values,
|
||||
field: this.searchview._s_groupby
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var Filter = Input.extend(/** @lends instance.web.search.Filter# */{
|
||||
template: 'SearchView.filter',
|
||||
/**
|
||||
* Implementation of the OpenERP filters (button with a context and/or
|
||||
* a domain sent as-is to the search view)
|
||||
*
|
||||
* Filters are only attributes holder, the actual work (compositing
|
||||
* domains and contexts, converting between facets and filters) is
|
||||
* performed by the filter group.
|
||||
*
|
||||
* @constructs instance.web.search.Filter
|
||||
* @extends instance.web.search.Input
|
||||
*
|
||||
* @param node
|
||||
* @param parent
|
||||
*/
|
||||
init: function (node, parent) {
|
||||
this._super(parent);
|
||||
this.load_attrs(node.attrs);
|
||||
},
|
||||
facet_for: function () { return $.when(null); },
|
||||
get_context: function () { },
|
||||
get_domain: function () { },
|
||||
});
|
||||
|
||||
/**
|
||||
* Registry of search fields, called by :js:class:`instance.web.SearchView` to
|
||||
* find and instantiate its field widgets.
|
||||
*/
|
||||
|
||||
core.search_widgets_registry
|
||||
.add('char', CharField)
|
||||
.add('text', CharField)
|
||||
.add('html', CharField)
|
||||
.add('boolean', BooleanField)
|
||||
.add('integer', IntegerField)
|
||||
.add('id', IntegerField)
|
||||
.add('float', FloatField)
|
||||
.add('monetary', FloatField)
|
||||
.add('selection', SelectionField)
|
||||
.add('datetime', DateTimeField)
|
||||
.add('date', DateField)
|
||||
.add('many2one', ManyToOneField)
|
||||
.add('many2many', CharField)
|
||||
.add('one2many', CharField);
|
||||
|
||||
|
||||
return {
|
||||
FilterGroup: FilterGroup,
|
||||
Filter: Filter,
|
||||
Field: Field,
|
||||
GroupbyGroup: GroupbyGroup,
|
||||
};
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
||||
odoo.define('web.TimeRangeMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var ComparisonOptions = TimeRangeMenuOptions.ComparisonOptions;
|
||||
var PeriodOptions = TimeRangeMenuOptions.PeriodOptions;
|
||||
|
||||
var TimeRangeMenu = Widget.extend({
|
||||
template: 'web.TimeRangeMenu',
|
||||
events: {
|
||||
'click .o_apply_range': '_onApplyButtonClick',
|
||||
'click .o_comparison_checkbox': '_onCheckBoxClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* override
|
||||
* @param {Widget} parent
|
||||
* @param {Object} fields
|
||||
* @param {Object} configuration
|
||||
*
|
||||
*/
|
||||
init: function(parent, fields, configuration) {
|
||||
var self = this;
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.symbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : 'caret';
|
||||
this._super(parent);
|
||||
this.dateFields = [];
|
||||
_.each(fields, function (field, name) {
|
||||
if (field.sortable && _.contains(['date', 'datetime'], field.type)) {
|
||||
self.dateFields.push(_.extend({}, field, {
|
||||
name: name,
|
||||
}));
|
||||
}
|
||||
});
|
||||
this.periodOptions = PeriodOptions;
|
||||
this.periodGroups = PeriodOptions.reduce(
|
||||
function (acc, option) {
|
||||
if (!_.contains(acc, option.groupId)) {
|
||||
acc.push(option.groupId);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
this.comparisonOptions = ComparisonOptions;
|
||||
|
||||
// Following steps determine initial configuration
|
||||
this.isActive = false;
|
||||
this.timeRangeId = undefined;
|
||||
this.comparisonIsSelected = false;
|
||||
this.comparisonTimeRangeId = undefined;
|
||||
this.dateField = {};
|
||||
if (configuration && configuration.field && configuration.range) {
|
||||
this.isActive = true;
|
||||
var dateField = _.findWhere(this.dateFields, {name: configuration.field});
|
||||
this.dateField = {
|
||||
name: dateField.name,
|
||||
description: dateField.string,
|
||||
type: dateField.type,
|
||||
};
|
||||
this.timeRangeId = configuration.range;
|
||||
if (configuration.comparison_range) {
|
||||
this.comparisonIsSelected = true;
|
||||
this.comparisonTimeRangeId = configuration.comparison_range;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
deactivate: function () {
|
||||
this.isActive = false;
|
||||
this.comparisonIsSelected = false;
|
||||
this.renderElement();
|
||||
},
|
||||
/**
|
||||
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
|
||||
* filter descriptor
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
facetFor: function () {
|
||||
var fieldDescription;
|
||||
var timeRange = "[]";
|
||||
var timeRangeDescription;
|
||||
var comparisonTimeRange = "[]";
|
||||
var comparisonTimeRangeDescription;
|
||||
|
||||
if (this.isActive) {
|
||||
fieldDescription = this.dateField.description;
|
||||
if (this.timeRangeId !== 'custom') {
|
||||
timeRange = Domain.prototype.constructDomain(
|
||||
this.dateField.name,
|
||||
this.timeRangeId,
|
||||
this.dateField.type
|
||||
);
|
||||
timeRangeDescription = _.findWhere(
|
||||
this.periodOptions,
|
||||
{optionId: this.timeRangeId}
|
||||
).description;
|
||||
}
|
||||
if (this.comparisonIsSelected) {
|
||||
comparisonTimeRange = Domain.prototype.constructDomain(
|
||||
this.dateField.name,
|
||||
this.timeRangeId,
|
||||
this.dateField.type,
|
||||
null,
|
||||
this.comparisonTimeRangeId
|
||||
);
|
||||
comparisonTimeRangeDescription = _.findWhere(
|
||||
this.comparisonOptions,
|
||||
{optionId: this.comparisonTimeRangeId}
|
||||
).description;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cat: 'timeRangeCategory',
|
||||
category: _t("Time Range"),
|
||||
icon: 'fa fa-calendar',
|
||||
field: {
|
||||
get_context: function (facet, noDomainEvaluation) {
|
||||
if (!noDomainEvaluation) {
|
||||
timeRange = Domain.prototype.stringToArray(timeRange);
|
||||
comparisonTimeRange = Domain.prototype.stringToArray(comparisonTimeRange);
|
||||
}
|
||||
return {
|
||||
timeRangeMenuData: {
|
||||
timeRange: timeRange,
|
||||
timeRangeDescription: timeRangeDescription,
|
||||
comparisonTimeRange: comparisonTimeRange,
|
||||
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
|
||||
}
|
||||
};
|
||||
},
|
||||
get_groupby: function () {},
|
||||
get_domain: function () {}
|
||||
},
|
||||
isRange: true,
|
||||
values: [{
|
||||
label: fieldDescription + ': ' + timeRangeDescription +
|
||||
(
|
||||
comparisonTimeRangeDescription ?
|
||||
(' / ' + comparisonTimeRangeDescription) :
|
||||
''
|
||||
),
|
||||
value: null,
|
||||
}],
|
||||
};
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onApplyButtonClick: function () {
|
||||
this.isActive = true;
|
||||
var dateFieldName = this.$('.o_date_field_selector').val();
|
||||
this.timeRangeId = this.$('.o_time_range_selector').val();
|
||||
if (this.comparisonIsSelected) {
|
||||
this.comparisonTimeRangeId = this.$('.o_comparison_time_range_selector').val();
|
||||
}
|
||||
this.dateField = {
|
||||
name: dateFieldName,
|
||||
type: _.findWhere(this.dateFields, {name: dateFieldName}).type,
|
||||
description: _.findWhere(this.dateFields, {name: dateFieldName}).string,
|
||||
};
|
||||
|
||||
this.renderElement();
|
||||
this.trigger_up('time_range_modified');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {JQueryEvent} ev
|
||||
*/
|
||||
_onCheckBoxClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.comparisonIsSelected = this.$('.o_comparison_checkbox').prop('checked');
|
||||
this.$('.o_comparison_time_range_selector').toggleClass('o_hidden');
|
||||
this.$el.addClass('open');
|
||||
}
|
||||
});
|
||||
|
||||
return TimeRangeMenu;
|
||||
|
||||
});
|
||||
@@ -3,13 +3,10 @@ odoo.define('web.view_dialogs', function (require) {
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Dialog = require('web.Dialog');
|
||||
var dom = require('web.dom');
|
||||
var ListController = require('web.ListController');
|
||||
var ListView = require('web.ListView');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var SearchView = require('web.SearchView');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -21,9 +18,6 @@ var _t = core._t;
|
||||
var ViewDialog = Dialog.extend({
|
||||
custom_events: _.extend({}, Dialog.prototype.custom_events, {
|
||||
push_state: '_onPushState',
|
||||
env_updated: function (event) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* @constructor
|
||||
@@ -45,10 +39,6 @@ var ViewDialog = Dialog.extend({
|
||||
this.domain = options.domain || [];
|
||||
this.context = options.context || {};
|
||||
this.options = _.extend(this.options || {}, options || {});
|
||||
|
||||
// FIXME: remove this once a dataset won't be necessary anymore to interact
|
||||
// with data_manager and instantiate views
|
||||
this.dataset = new data.DataSet(this, this.res_model, this.context);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -182,7 +172,7 @@ var FormViewDialog = ViewDialog.extend({
|
||||
if (this.options.fields_view) {
|
||||
fields_view_def = $.when(this.options.fields_view);
|
||||
} else {
|
||||
fields_view_def = this.loadFieldView(this.dataset, this.options.view_id, 'form');
|
||||
fields_view_def = this.loadFieldView(this.res_model, this.context, this.options.view_id, 'form');
|
||||
}
|
||||
|
||||
fields_view_def.then(function (viewInfo) {
|
||||
@@ -271,17 +261,23 @@ var FormViewDialog = ViewDialog.extend({
|
||||
});
|
||||
|
||||
var SelectCreateListController = ListController.extend({
|
||||
// Override the ListView to handle the custom events 'open_record' (triggered when clicking on a
|
||||
// row of the list) such that it triggers up 'select_record' with its res_id.
|
||||
custom_events: _.extend({}, ListController.prototype.custom_events, {
|
||||
open_record: function (event) {
|
||||
var selectedRecord = this.model.get(event.data.id);
|
||||
this.trigger_up('select_record', {
|
||||
id: selectedRecord.res_id,
|
||||
display_name: selectedRecord.data.display_name,
|
||||
});
|
||||
},
|
||||
}),
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Override to select the clicked record instead of opening it
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_onOpenRecord: function (ev) {
|
||||
var selectedRecord = this.model.get(ev.data.id);
|
||||
this.trigger_up('select_record', {
|
||||
id: selectedRecord.res_id,
|
||||
display_name: selectedRecord.data.display_name,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -299,13 +295,6 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
event.stopPropagation();
|
||||
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 action manager
|
||||
var d = event.data;
|
||||
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
|
||||
this.list_controller.reload(searchData);
|
||||
},
|
||||
get_controller_context: '_onGetControllerContext',
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -320,7 +309,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
this._super.apply(this, arguments);
|
||||
_.defaults(this.options, { initial_view: 'search' });
|
||||
this.on_selected = this.options.on_selected || (function () {});
|
||||
this.initial_ids = this.options.initial_ids;
|
||||
this.initialIDs = this.options.initial_ids;
|
||||
},
|
||||
|
||||
open: function () {
|
||||
@@ -328,26 +317,13 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
return this.create_edit_record();
|
||||
}
|
||||
var self = this;
|
||||
var user_context = this.getSession().user_context;
|
||||
|
||||
var _super = this._super.bind(this);
|
||||
var context = pyUtils.eval_domains_and_contexts({
|
||||
domains: [],
|
||||
contexts: [user_context, this.context]
|
||||
}).context;
|
||||
var search_defaults = {};
|
||||
_.each(context, function (value_, key) {
|
||||
var match = /^search_default_(.*)$/.exec(key);
|
||||
if (match) {
|
||||
search_defaults[match[1]] = value_;
|
||||
}
|
||||
});
|
||||
this.loadViews(this.dataset.model, this.dataset.get_context().eval(), [[false, 'list'], [false, 'search']], {})
|
||||
.then(this.setup.bind(this, search_defaults))
|
||||
this.loadViews(this.res_model, this.context, [[false, 'list'], [false, 'search']], {})
|
||||
.then(this.setup.bind(this))
|
||||
.then(function (fragment) {
|
||||
self.opened().then(function () {
|
||||
dom.append(self.$el, fragment, {
|
||||
callbacks: [{widget: self.list_controller}],
|
||||
callbacks: [{widget: self.listController}],
|
||||
in_DOM: true,
|
||||
});
|
||||
self.set_buttons(self.__buttons);
|
||||
@@ -357,65 +333,50 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
return this;
|
||||
},
|
||||
|
||||
setup: function (search_defaults, fields_views) {
|
||||
setup: function (fieldsViews) {
|
||||
var self = this;
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
var searchDef = $.Deferred();
|
||||
|
||||
// Set the dialog's header and its search view
|
||||
var $header = $('<div/>').addClass('o_modal_header').appendTo(fragment);
|
||||
var $pager = $('<div/>').addClass('o_pager').appendTo($header);
|
||||
var options = {
|
||||
$buttons: $('<div/>').addClass('o_search_options').appendTo($header),
|
||||
search_defaults: search_defaults,
|
||||
};
|
||||
var searchview = new SearchView(this, this.dataset, fields_views.search, options);
|
||||
searchview.prependTo($header).done(function () {
|
||||
var d = searchview.build_search_data();
|
||||
if (self.initial_ids) {
|
||||
d.domains.push([["id", "in", self.initial_ids]]);
|
||||
self.initial_ids = undefined;
|
||||
}
|
||||
var searchData = self._process_search_data(d.domains, d.contexts, d.groupbys);
|
||||
searchDef.resolve(searchData);
|
||||
});
|
||||
|
||||
return $.when(searchDef).then(function (searchResult) {
|
||||
// Set the list view
|
||||
var listView = new ListView(fields_views.list, _.extend({
|
||||
context: searchResult.context,
|
||||
domain: searchResult.domain,
|
||||
groupBy: searchResult.groupBy,
|
||||
modelName: self.dataset.model,
|
||||
hasSelectors: !self.options.disable_multiple_selection,
|
||||
readonly: true,
|
||||
}, self.options.list_view_options));
|
||||
listView.setController(SelectCreateListController);
|
||||
return listView.getController(self);
|
||||
}).then(function (controller) {
|
||||
self.list_controller = controller;
|
||||
// Set the dialog's buttons
|
||||
var domain = this.domain;
|
||||
if (this.initialIDs) {
|
||||
domain = domain.concat([['id', 'in', this.initialIDs]]);
|
||||
}
|
||||
var listView = new ListView(fieldsViews.list, _.extend({
|
||||
action: {
|
||||
controlPanelFieldsView: fieldsViews.search,
|
||||
},
|
||||
action_buttons: false,
|
||||
context: this.context,
|
||||
domain: domain,
|
||||
hasSelectors: !this.options.disable_multiple_selection,
|
||||
modelName: this.res_model,
|
||||
readonly: true,
|
||||
withBreadcrumbs: false,
|
||||
}, this.options.list_view_options));
|
||||
listView.setController(SelectCreateListController);
|
||||
return listView.getController(this).then(function (controller) {
|
||||
self.listController = controller;
|
||||
// render the footer buttons
|
||||
self.__buttons = [{
|
||||
text: _t("Cancel"),
|
||||
classes: "btn-secondary o_form_button_cancel",
|
||||
classes: 'btn-secondary o_form_button_cancel',
|
||||
close: true,
|
||||
}];
|
||||
if (!self.options.no_create) {
|
||||
self.__buttons.unshift({
|
||||
text: _t("Create"),
|
||||
classes: "btn-primary",
|
||||
classes: 'btn-primary',
|
||||
click: self.create_edit_record.bind(self)
|
||||
});
|
||||
}
|
||||
if (!self.options.disable_multiple_selection) {
|
||||
self.__buttons.unshift({
|
||||
text: _t("Select"),
|
||||
classes: "btn-primary o_select_button",
|
||||
classes: 'btn-primary o_select_button',
|
||||
disabled: true,
|
||||
close: true,
|
||||
click: function () {
|
||||
var records = self.list_controller.getSelectedRecords();
|
||||
var records = self.listController.getSelectedRecords();
|
||||
var values = _.map(records, function (record) {
|
||||
return {
|
||||
id: record.res_id,
|
||||
@@ -426,28 +387,11 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
},
|
||||
});
|
||||
}
|
||||
return self.list_controller.appendTo(fragment);
|
||||
return self.listController.appendTo(fragment);
|
||||
}).then(function () {
|
||||
searchview.toggle_visibility(true);
|
||||
self.list_controller.do_show();
|
||||
self.list_controller.renderPager($pager);
|
||||
return fragment;
|
||||
});
|
||||
},
|
||||
_process_search_data: function (domains, contexts, groupbys) {
|
||||
var results = pyUtils.eval_domains_and_contexts({
|
||||
domains: [this.domain].concat(domains),
|
||||
contexts: [this.context].concat(contexts),
|
||||
group_by_seq: groupbys || [],
|
||||
eval_context: this.getSession().user_context,
|
||||
});
|
||||
var context = _.omit(results.context, function (value, key) { return key.indexOf('search_default_') === 0; });
|
||||
return {
|
||||
context: context,
|
||||
domain: results.domain,
|
||||
groupBy: results.group_by,
|
||||
};
|
||||
},
|
||||
create_edit_record: function () {
|
||||
var self = this;
|
||||
var dialog = new FormViewDialog(this, _.extend({}, this.options, {
|
||||
@@ -469,23 +413,6 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
this.trigger_up('form_dialog_discarded');
|
||||
return true;
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles a context request: provides to the caller the context of the
|
||||
* list controller.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {function} ev.data.callback used to send the requested context
|
||||
*/
|
||||
_onGetControllerContext: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var context = this.list_controller && this.list_controller.getContext();
|
||||
ev.data.callback(context || {});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
odoo.define('web.viewUtils', function () {
|
||||
odoo.define('web.viewUtils', function (require) {
|
||||
"use strict";
|
||||
|
||||
var utils = {
|
||||
var dom = require('web.dom');
|
||||
var utils = require('web.utils');
|
||||
|
||||
var viewUtils = {
|
||||
/**
|
||||
* Returns the value of a group dataPoint, i.e. the value of the groupBy
|
||||
* field for the records in that group.
|
||||
@@ -40,8 +43,45 @@ var utils = {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @param {string} arch view arch
|
||||
* @returns {Object} parsed arch
|
||||
*/
|
||||
parseArch: function (arch) {
|
||||
var doc = $.parseXML(arch).documentElement;
|
||||
var stripWhitespaces = doc.nodeName.toLowerCase() !== 'kanban';
|
||||
return utils.xml_to_json(doc, stripWhitespaces);
|
||||
},
|
||||
/**
|
||||
* Renders a button according to a given arch node element.
|
||||
*
|
||||
* @param {Object} node
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.extraClass]
|
||||
* @param {boolean} [options.textAsTitle=false]
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
renderButtonFromNode: function (node, options) {
|
||||
var btnOptions = {
|
||||
attrs: _.omit(node.attrs, 'icon', 'string', 'type', 'attrs', 'modifiers', 'options'),
|
||||
icon: node.attrs.icon,
|
||||
};
|
||||
if (options && options.extraClass) {
|
||||
var classes = btnOptions.attrs.class ? btnOptions.attrs.class.split(' ') : [];
|
||||
btnOptions.attrs.class = _.uniq(classes.concat(options.extraClass.split(' '))).join(' ');
|
||||
}
|
||||
var str = (node.attrs.string || '').replace(/_/g, '');
|
||||
if (str) {
|
||||
if (options && options.textAsTitle) {
|
||||
btnOptions.attrs.title = str;
|
||||
} else {
|
||||
btnOptions.text = str;
|
||||
}
|
||||
}
|
||||
return dom.renderButton(btnOptions);
|
||||
},
|
||||
};
|
||||
|
||||
return utils;
|
||||
return viewUtils;
|
||||
|
||||
});
|
||||
|
||||
@@ -6,81 +6,48 @@ var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
|
||||
var DropdownMenu = Widget.extend({
|
||||
template: 'web.DropdownMenu',
|
||||
|
||||
events: {
|
||||
'click .o_menu_item': '_onItemClick',
|
||||
'click .o_item_option': '_onOptionClick',
|
||||
'click span.o_trash_button': '_onTrashClick',
|
||||
'click span.o_trash_button': '_onTrashButtonClick',
|
||||
'hidden.bs.dropdown': '_onBootstrapClose',
|
||||
'click .dropdown-item-text': '_onDropDownItemTextClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @param {Widget} parent
|
||||
* @param {Object} dropdownHeader object used to customize the dropdown menu.
|
||||
* @param {String} dropdownHeader.title
|
||||
* @param {String} dropdownHeader.icon
|
||||
* @param {String} dropdownHeader.symbol
|
||||
* @param {String} dropdownHeader.category descripbes the type of items
|
||||
* @param {String} dropdownHeader.style the button style
|
||||
* @param {Object[]} items list of menu items
|
||||
*
|
||||
* Menu items:
|
||||
*
|
||||
* * itemId: string; (optional) unique id associated with the item
|
||||
* * description: string; label printed on screen
|
||||
* * groupId: string;
|
||||
* * isActive: boolean; (optional) determines if the item is considered active
|
||||
* * isOpen: boolean; (optional) in case there are options the submenu presenting the options is opened or closed according to isOpen
|
||||
* * isRemovable: boolean; (optional) can be removed from menu options: array of objects with 'optionId' and 'description' keys; (optional)
|
||||
* * currentOptionId: string refers to an optionId that is activated if item is active (optional)
|
||||
*/
|
||||
init: function (parent, dropdownHeader, items) {
|
||||
init: function (parent, items) {
|
||||
this._super(parent);
|
||||
this.dropdownCategory = dropdownHeader.category;
|
||||
this.dropdownTitle = dropdownHeader.title;
|
||||
this.dropdownIcon = dropdownHeader.icon;
|
||||
this.dropdownSymbol = dropdownHeader.symbol || false;
|
||||
// should be specified
|
||||
this.dropdownCategory = null;
|
||||
this.dropdownTitle = null;
|
||||
this.dropdownIcon = null;
|
||||
this.dropdownSymbol = false;
|
||||
// this parameter fixes the menu style. By default,
|
||||
// the style used is the one used in the search view
|
||||
this.dropdownStyle = dropdownHeader.style || {
|
||||
this.dropdownStyle = {
|
||||
el: {class: 'btn-group o_dropdown', attrs: {}},
|
||||
mainButton: {class: 'o_dropdown_toggler_btn btn btn-secondary dropdown-toggle' + (this.dropdownSymbol ? ' o-no-caret' : '')},
|
||||
};
|
||||
mainButton: {
|
||||
class: 'o_dropdown_toggler_btn btn btn-secondary ' +
|
||||
'dropdown-toggle ' +
|
||||
(this.dropdownSymbol ? 'o-no-caret' : '')
|
||||
},
|
||||
};
|
||||
this.items = items;
|
||||
_.each(this.items, this._prepareItem.bind(this));
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
this.openItems = {};
|
||||
},
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {string[]} activeItemIds
|
||||
* @param {Object} items
|
||||
*/
|
||||
updateItemsStatus: function (activeItemIds) {
|
||||
_.each(this.items, function (item) {
|
||||
if (!_.contains(activeItemIds, item.itemId)) {
|
||||
item.isActive = false;
|
||||
item.currentOptionId = false;
|
||||
} else {
|
||||
item.isActive = true;
|
||||
item.currentOptionId = item.hasOptions ?
|
||||
(item.currentOptionId || item.defaultOptionId) :
|
||||
false;
|
||||
}
|
||||
});
|
||||
update: function (items) {
|
||||
this.items = items;
|
||||
this._renderMenuItems();
|
||||
},
|
||||
|
||||
@@ -88,99 +55,13 @@ var DropdownMenu = Widget.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_removeItem: function (itemId) {
|
||||
var item, itemIndex;
|
||||
item = _.findWhere(this.items, {itemId: itemId});
|
||||
itemIndex = this.items.indexOf(item);
|
||||
this.items.splice(itemIndex, 1);
|
||||
this._renderMenuItems();
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
groupId: item.groupId,
|
||||
};
|
||||
this.trigger_up('menu_item_deleted', eventData);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderMenuItems: function () {
|
||||
var newMenuItems = QWeb.render('DropdownMenu.MenuItems', {widget: this});
|
||||
this.$el.find('.o_menu_item, .dropdown-divider[data-removable="1"]').remove();
|
||||
this.$('.o_dropdown_menu').prepend($(newMenuItems));
|
||||
},
|
||||
/**
|
||||
* this function computes some default values for some of the item keys
|
||||
* according to values passed at initialization
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item
|
||||
*/
|
||||
_prepareItem: function (item) {
|
||||
item.itemId = item.itemId || _.uniqueId('__item__');
|
||||
item.isOpen = item.isOpen || false;
|
||||
item.isRemovable = item.isRemovable || false;
|
||||
if (item.options && item.options.length !== 0) {
|
||||
item.options = _.sortBy(item.options, 'groupId');
|
||||
item.hasOptions = true;
|
||||
item.defaultOptionId = item.defaultOptionId || this.defaultOptionId;
|
||||
item.currentOptionId = item.isActive && item.defaultOptionId ? item.defaultOptionId : false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_toggleMenuItem: function (itemId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
if (!item.hasOptions) {
|
||||
item.isActive = !item.isActive;
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
isActive: item.isActive,
|
||||
groupId: item.groupId,
|
||||
};
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('menu_item_toggled', eventData);
|
||||
}
|
||||
if (item.hasOptions) {
|
||||
item.isOpen = !item.isOpen;
|
||||
this._renderMenuItems();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
* @param {string} optionId
|
||||
*/
|
||||
_toggleItemOption: function (itemId, optionId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
var initialState = item.isActive;
|
||||
if (item.currentOptionId === optionId) {
|
||||
item.isActive = false;
|
||||
item.currentOptionId = false;
|
||||
} else {
|
||||
item.isActive = true;
|
||||
item.currentOptionId = optionId;
|
||||
}
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
groupId: item.groupId,
|
||||
isActive: item.isActive,
|
||||
optionId: item.currentOptionId,
|
||||
};
|
||||
this._renderMenuItems();
|
||||
if (item.isActive !== initialState) {
|
||||
this.trigger_up('menu_item_toggled', eventData);
|
||||
} else {
|
||||
this.trigger_up('item_option_changed', eventData);
|
||||
}
|
||||
this.$('.o_dropdown_menu').prepend(newMenuItems);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -195,9 +76,7 @@ var DropdownMenu = Widget.extend({
|
||||
* @param {jQueryEvent} event
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
_.each(this.items, function (item) {
|
||||
item.isOpen = false;
|
||||
});
|
||||
this.openItems = {};
|
||||
this._renderMenuItems();
|
||||
},
|
||||
/**
|
||||
@@ -216,8 +95,16 @@ var DropdownMenu = Widget.extend({
|
||||
_onItemClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var itemId = $(event.currentTarget).data('id');
|
||||
this._toggleMenuItem(itemId);
|
||||
var id = $(event.currentTarget).data('id');
|
||||
var item = this.items.find(function (item) {
|
||||
return item.id === id;
|
||||
});
|
||||
if (item.hasOptions) {
|
||||
this.openItems[id] = !this.openItems[id];
|
||||
this._renderMenuItems();
|
||||
} else {
|
||||
this.trigger_up('menu_item_clicked', {id: id});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -227,20 +114,19 @@ var DropdownMenu = Widget.extend({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var optionId = $(event.currentTarget).data('option_id');
|
||||
var itemId = $(event.currentTarget).data('item_id');
|
||||
this._toggleItemOption(itemId, optionId);
|
||||
var id = $(event.currentTarget).data('item_id');
|
||||
this.trigger_up('item_option_clicked', {id: id, optionId: optionId});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*
|
||||
* To implement in child
|
||||
*/
|
||||
_onTrashClick: function (event) {
|
||||
event.stopPropagation();
|
||||
var itemId = $(event.currentTarget).data('id');
|
||||
this._removeItem(itemId);
|
||||
},
|
||||
_onTrashButtonClick: function (event) {
|
||||
},
|
||||
});
|
||||
|
||||
return DropdownMenu;
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
odoo.define('web.OldDropdownMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
|
||||
var OldDropdownMenu = Widget.extend({
|
||||
template: 'web.OldDropdownMenu',
|
||||
events: {
|
||||
'click .o_menu_item': '_onItemClick',
|
||||
'click .o_item_option': '_onOptionClick',
|
||||
'click span.o_trash_button': '_onTrashClick',
|
||||
'hidden.bs.dropdown': '_onBootstrapClose',
|
||||
},
|
||||
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @param {Widget} parent
|
||||
* @param {Object} dropdownHeader object used to customize the dropdown menu.
|
||||
* @param {String} dropdownHeader.title
|
||||
* @param {String} dropdownHeader.icon
|
||||
* @param {String} dropdownHeader.symbol
|
||||
* @param {String} dropdownHeader.category descripbes the type of items
|
||||
* @param {String} dropdownHeader.style the button style
|
||||
* @param {Object[]} items list of menu items
|
||||
*
|
||||
* Menu items:
|
||||
*
|
||||
* * itemId: string; (optional) unique id associated with the item
|
||||
* * description: string; label printed on screen
|
||||
* * groupId: string;
|
||||
* * isActive: boolean; (optional) determines if the item is considered active
|
||||
* * isOpen: boolean; (optional) in case there are options the submenu presenting the options is opened or closed according to isOpen
|
||||
* * isRemovable: boolean; (optional) can be removed from menu options: array of objects with 'optionId' and 'description' keys; (optional)
|
||||
* * currentOptionId: string refers to an optionId that is activated if item is active (optional)
|
||||
*/
|
||||
init: function (parent, dropdownHeader, items) {
|
||||
this._super(parent);
|
||||
this.dropdownCategory = dropdownHeader.category;
|
||||
this.dropdownTitle = dropdownHeader.title;
|
||||
this.dropdownIcon = dropdownHeader.icon;
|
||||
this.dropdownSymbol = dropdownHeader.symbol || false;
|
||||
// this parameter fixes the menu style. By default,
|
||||
// the style used is the one used in the search view
|
||||
this.dropdownStyle = dropdownHeader.style || {
|
||||
el: {class: 'btn-group o_dropdown', attrs: {}},
|
||||
mainButton: {class: 'o_dropdown_toggler_btn btn btn-secondary dropdown-toggle' + (this.dropdownSymbol ? ' o-no-caret' : '')},
|
||||
};
|
||||
this.items = items;
|
||||
_.each(this.items, this._prepareItem.bind(this));
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {string[]} activeItemIds
|
||||
*/
|
||||
updateItemsStatus: function (activeItemIds) {
|
||||
_.each(this.items, function (item) {
|
||||
if (!_.contains(activeItemIds, item.itemId)) {
|
||||
item.isActive = false;
|
||||
item.currentOptionId = false;
|
||||
} else {
|
||||
item.isActive = true;
|
||||
item.currentOptionId = item.hasOptions ?
|
||||
(item.currentOptionId || item.defaultOptionId) :
|
||||
false;
|
||||
}
|
||||
});
|
||||
this._renderMenuItems();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_removeItem: function (itemId) {
|
||||
var item, itemIndex;
|
||||
item = _.findWhere(this.items, {itemId: itemId});
|
||||
itemIndex = this.items.indexOf(item);
|
||||
this.items.splice(itemIndex, 1);
|
||||
this._renderMenuItems();
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
groupId: item.groupId,
|
||||
};
|
||||
this.trigger_up('menu_item_deleted', eventData);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderMenuItems: function () {
|
||||
var newMenuItems = QWeb.render('OldDropdownMenu.MenuItems', {widget: this});
|
||||
this.$el.find('.o_menu_item, .dropdown-divider[data-removable="1"]').remove();
|
||||
this.$('.o_dropdown_menu').prepend($(newMenuItems));
|
||||
},
|
||||
/**
|
||||
* this function computes some default values for some of the item keys
|
||||
* according to values passed at initialization
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item
|
||||
*/
|
||||
_prepareItem: function (item) {
|
||||
item.itemId = item.itemId || _.uniqueId('__item__');
|
||||
item.isOpen = item.isOpen || false;
|
||||
item.isRemovable = item.isRemovable || false;
|
||||
if (item.options && item.options.length !== 0) {
|
||||
item.options = _.sortBy(item.options, 'groupId');
|
||||
item.hasOptions = true;
|
||||
item.defaultOptionId = item.defaultOptionId || this.defaultOptionId;
|
||||
item.currentOptionId = item.isActive && item.defaultOptionId ? item.defaultOptionId : false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_toggleMenuItem: function (itemId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
if (!item.hasOptions) {
|
||||
item.isActive = !item.isActive;
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
isActive: item.isActive,
|
||||
groupId: item.groupId,
|
||||
};
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('menu_item_toggled', eventData);
|
||||
}
|
||||
if (item.hasOptions) {
|
||||
item.isOpen = !item.isOpen;
|
||||
this._renderMenuItems();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
* @param {string} optionId
|
||||
*/
|
||||
_toggleItemOption: function (itemId, optionId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
var initialState = item.isActive;
|
||||
if (item.currentOptionId === optionId) {
|
||||
item.isActive = false;
|
||||
item.currentOptionId = false;
|
||||
} else {
|
||||
item.isActive = true;
|
||||
item.currentOptionId = optionId;
|
||||
}
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
groupId: item.groupId,
|
||||
isActive: item.isActive,
|
||||
optionId: item.currentOptionId,
|
||||
};
|
||||
this._renderMenuItems();
|
||||
if (item.isActive !== initialState) {
|
||||
this.trigger_up('menu_item_toggled', eventData);
|
||||
} else {
|
||||
this.trigger_up('item_option_changed', eventData);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is called when Bootstrap trigger a 'close' event (for example
|
||||
* when the user clicks outside the dropdown menu).
|
||||
*
|
||||
* @private
|
||||
* @param {jQueryEvent} event
|
||||
*/
|
||||
_onBootstrapClose: function () {
|
||||
_.each(this.items, function (item) {
|
||||
item.isOpen = false;
|
||||
});
|
||||
this._renderMenuItems();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onItemClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var itemId = $(event.currentTarget).data('id');
|
||||
this._toggleMenuItem(itemId);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onOptionClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var optionId = $(event.currentTarget).data('option_id');
|
||||
var itemId = $(event.currentTarget).data('item_id');
|
||||
this._toggleItemOption(itemId, optionId);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onTrashClick: function (event) {
|
||||
event.stopPropagation();
|
||||
var itemId = $(event.currentTarget).data('id');
|
||||
this._removeItem(itemId);
|
||||
},
|
||||
});
|
||||
|
||||
return OldDropdownMenu;
|
||||
|
||||
});
|
||||
@@ -34,14 +34,16 @@ var PieChart = Widget.extend({
|
||||
});
|
||||
|
||||
this.subViewParams = {
|
||||
context: record.context,
|
||||
domain: domain,
|
||||
groupBy: [],
|
||||
modelName: record.model,
|
||||
withControlPanel: false,
|
||||
isEmbedded: true,
|
||||
mode: 'pie',
|
||||
};
|
||||
this.subViewParams.searchQuery = {
|
||||
context: record.context,
|
||||
domain: domain,
|
||||
groupBy: [],
|
||||
};
|
||||
|
||||
this.viewInfo = {
|
||||
arch: arch,
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.o_breadcrumb_full > .breadcrumb {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .o_cp_searchview {
|
||||
width: 50%;
|
||||
min-height: $o-cp-breadcrumb-height;
|
||||
|
||||
@@ -42,4 +42,8 @@
|
||||
content: "\f00c";
|
||||
}
|
||||
}
|
||||
.o_trash_button {
|
||||
padding-left: 8px;
|
||||
@include o-position-absolute($right: $dropdown-item-padding-x * 0.5);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.o_graph {
|
||||
.o_graph_container {
|
||||
.o_graph_controller {
|
||||
.o_graph_renderer {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
.o_graph_svg_container {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
}
|
||||
|
||||
.o_searchview_input_container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
@@ -182,7 +183,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.o_add_filter_menu { // apply and add buttons
|
||||
display: none; // Needed by the webclient
|
||||
@include o-search-options-dropdown-custom-item;
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,6 @@
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.dropdown-divider {display: none;} // hide the divider by default (no favorite)
|
||||
.o-searchview-custom-private, .o-searchview-custom-public {
|
||||
.o-remove-filter {
|
||||
@include o-position-absolute(50%, 12px);
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
.o_dropdown_toggler_btn {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// We disable the backdrop in this case because it prevents any
|
||||
|
||||
@@ -9,30 +9,24 @@ html {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
> .o_main {
|
||||
> .o_action_manager {
|
||||
direction: ltr; //Define direction attribute here so when rtlcss preprocessor run, it converts it to rtl
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
min-height: 0; // Allow flex to shrink this element as much as it wants
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.o_main_content {
|
||||
flex: 1 1 auto;
|
||||
> .o_action {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.o_control_panel {
|
||||
> .o_cp_controller {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.o_content {
|
||||
> .o_content {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
position: relative; // Allow to redistribute the 100% height to its child
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
> .o_view_controller {
|
||||
@include o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
|
||||
height: 100%;
|
||||
}
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +180,17 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div t-name="Action" class="o_action">
|
||||
<!-- CP -->
|
||||
<div class="o_content"/>
|
||||
</div>
|
||||
|
||||
<t t-name="ControlPanel">
|
||||
<div class="o_control_panel">
|
||||
<ol class="breadcrumb" role="navigation"/>
|
||||
<div class="o_cp_searchview" role="search"/>
|
||||
<div class="o_cp_searchview" role="search">
|
||||
<t t-call="SearchView"/>
|
||||
</div>
|
||||
<div class="o_cp_left">
|
||||
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel toolbar"/>
|
||||
<aside class="o_cp_sidebar"/>
|
||||
@@ -204,7 +211,7 @@
|
||||
|
||||
<t t-name="ControlPanel.SwitchButton">
|
||||
<button type="button" t-att-accesskey="view.accessKey"
|
||||
t-attf-class="btn btn-secondary fa fa-lg #{view.icon} o_cp_switch_#{view.type}"
|
||||
t-attf-class="btn btn-secondary fa fa-lg #{view.icon} o_cp_switch_#{view.type} #{viewType === view.type ? ' active' : ''}"
|
||||
t-attf-aria-label="View #{view.type}"
|
||||
t-att-data-view-type="view.type" t-attf-title="View #{view.type}" tabindex="-1"/>
|
||||
</t>
|
||||
@@ -276,7 +283,7 @@
|
||||
<a role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fieldsView.view_id" class="dropdown-item">
|
||||
Edit View: <t t-esc="view.displayName"/>
|
||||
</a>
|
||||
<a t-if="searchview and searchview.$el.is(':visible')" role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="action.searchFieldsView.view_id" class="dropdown-item">Edit SearchView</a>
|
||||
<a t-if="controlPanelView and controlPanelView.$el.is(':visible')" role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="action.controlPanelFieldsView.view_id" class="dropdown-item">Edit ControlPanelView</a>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
@@ -1073,29 +1080,29 @@
|
||||
<t t-if="widget.items.length !== 0">
|
||||
<t t-set="group" t-value="0"/>
|
||||
<t t-foreach="widget.items" t-as="item">
|
||||
<div t-if="group !== item.groupId" class="dropdown-divider" data-removable="1"/>
|
||||
<div t-if="group !== item.groupNumber" class="dropdown-divider" data-removable="1"/>
|
||||
<div class="o_menu_item"
|
||||
t-att-data-id="item.itemId">
|
||||
t-att-data-id="item.id">
|
||||
<a href="#" t-attf-class="dropdown-item#{item.isActive ? ' selected ' : ''}">
|
||||
<t t-esc="item.description"/>
|
||||
<span t-if="item.isRemovable" t-att-data-id="item.itemId" class="o_trash_button" > <span class="fa fa-trash-o"></span> </span>
|
||||
<span t-if="item.hasOptions" t-att-data-id="item.itemId" class="o_submenu_switcher">
|
||||
<span t-att-class="item.isOpen ? 'fa fa-caret-down' : 'fa fa-caret-right'"></span>
|
||||
<span t-if="item.isRemovable" t-att-data-id="item.id" class="o_trash_button" > <span class="fa fa-trash-o"></span> </span>
|
||||
<span t-if="item.hasOptions" t-att-data-id="item.id" class="o_submenu_switcher">
|
||||
<span t-att-class="widget.openItems[item.id] ? 'fa fa-caret-down' : 'fa fa-caret-right'"></span>
|
||||
</span>
|
||||
</a>
|
||||
<ul t-if="item.hasOptions && item.isOpen" class="list-unstyled">
|
||||
<ul t-if="item.hasOptions && widget.openItems[item.id]" class="list-unstyled">
|
||||
<t t-set="subgroup" t-value="0"/>
|
||||
<t t-foreach="_.values(item.options)" t-as="option">
|
||||
<li t-if="subgroup !== option.groupId" class="dropdown-divider" data-removable="1"/>
|
||||
<t t-foreach="item.options" t-as="option">
|
||||
<li t-if="subgroup !== option.groupNumber" class="dropdown-divider" data-removable="1"/>
|
||||
<li class="o_item_option"
|
||||
t-att-data-item_id="item.itemId" t-att-data-option_id="option.optionId">
|
||||
t-att-data-item_id="item.id" t-att-data-option_id="option.optionId">
|
||||
<a href="#" t-attf-class="dropdown-item#{item.currentOptionId === option.optionId ? ' selected': ''} pl-5"><t t-esc="option.description"/></a>
|
||||
</li>
|
||||
<t t-set="subgroup" t-value="option.groupId"/>
|
||||
<t t-set="subgroup" t-value="option.groupNumber"/>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-set="group" t-value="item.groupId"/>
|
||||
<t t-set="group" t-value="item.groupNumber"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
@@ -1109,9 +1116,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-name="GroupbyMenuGenerator">
|
||||
<div t-if="Object.keys(widget.presentedFields).length !== 0 && widget.items.length !== 0" class="dropdown-divider o_generator_menu"/>
|
||||
<button t-if="Object.keys(widget.presentedFields).length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_group">Add Custom Group</button>
|
||||
<t t-name="GroupByMenuGenerator">
|
||||
<div t-if="widget.presentedFields.length !== 0 && widget.items.length !== 0" class="dropdown-divider o_generator_menu"/>
|
||||
<button t-if="widget.presentedFields.length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_group">Add Custom Group</button>
|
||||
<div t-if="widget.generatorMenuIsOpen" class="dropdown-item-text o_generator_menu">
|
||||
<select class="w-auto o_input o_group_selector o_add_group">
|
||||
<t t-foreach="widget.presentedFields" t-as="field">
|
||||
@@ -1124,10 +1131,10 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="FiltersMenuGenerator">
|
||||
<div class="dropdown-divider"/>
|
||||
<button t-if="Object.keys(widget.fields).length !== 0" type="button" class="dropdown-item o_add_custom_filter o_closed_menu">Add Custom Filter</button>
|
||||
<div class="dropdown-item-text o_add_filter_menu">
|
||||
<t t-name="FilterMenuGenerator">
|
||||
<div t-if="Object.keys(widget.fields).length !== 0 && widget.items.length !== 0" class="dropdown-divider o_generator_menu"/>
|
||||
<button t-if="Object.keys(widget.fields).length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_filter">Add Custom Filter</button>
|
||||
<div t-if="widget.generatorMenuIsOpen" class="dropdown-item-text o_generator_menu o_add_filter_menu">
|
||||
<button class="btn btn-primary o_apply_filter" type="button">Apply</button>
|
||||
<button class="btn btn-secondary o_add_condition" type="button">
|
||||
<span class="fa fa-plus-circle"/>
|
||||
@@ -1145,15 +1152,15 @@
|
||||
<div class="dropdown-item-text">
|
||||
<label for="date_field_selector">Based On</label>
|
||||
<select id="date_field_selector" class="o_input o_date_field_selector">
|
||||
<t t-foreach="widget.dateFields" t-as="field">
|
||||
<t t-if="widget.dateField.name === field.name">
|
||||
<option t-att-value="field.name" selected="selected">
|
||||
<t t-esc="field.string"/>
|
||||
<t t-foreach="widget.timeRanges" t-as="timeRange">
|
||||
<t t-if="widget.configuration.id === timeRange.id">
|
||||
<option t-att-value="timeRange.id" selected="selected">
|
||||
<t t-esc="timeRange.description"/>
|
||||
</option>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<option t-att-value="field.name">
|
||||
<t t-esc="field.string"/>
|
||||
<option t-att-value="timeRange.id">
|
||||
<t t-esc="timeRange.description"/>
|
||||
</option>
|
||||
</t>
|
||||
</t>
|
||||
@@ -1166,7 +1173,7 @@
|
||||
<optgroup t-if="group !== 1"/>
|
||||
<t t-foreach="widget.periodOptions" t-as="option">
|
||||
<t t-if="option.groupId === group">
|
||||
<t t-if="widget.timeRangeId === option.optionId">
|
||||
<t t-if="widget.configuration.timeRangeId === option.optionId">
|
||||
<option t-att-value="option.optionId" selected="selected">
|
||||
<t t-esc="option.description"/>
|
||||
</option>
|
||||
@@ -1182,14 +1189,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<t t-set="isSelected" t-value="widget.comparisonIsSelected"/>
|
||||
<t t-set="isSelected" t-value="widget.configuration.comparisonIsSelected"/>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" id="comparison_checkbox" class="custom-control-input o_comparison_checkbox" t-att-checked="isSelected ? 'checked' : undefined"/>
|
||||
<label for="comparison_checkbox" class="custom-control-label">Compare To</label>
|
||||
</div>
|
||||
<select t-att-class="'o_input o_comparison_time_range_selector' + (widget.comparisonIsSelected ? '' : ' o_hidden')">
|
||||
<t t-foreach="widget.comparisonOptions" t-as="option">
|
||||
<t t-if="widget.comparisonTimeRangeId === option.optionId">
|
||||
<select t-att-class="'o_input o_comparison_time_range_selector' + (widget.configuration.comparisonIsSelected ? '' : ' o_hidden')">
|
||||
<t t-foreach="widget.comparisonTimeRangeOptions" t-as="option">
|
||||
<t t-if="widget.configuration.comparisonTimeRangeId === option.optionId">
|
||||
<option t-att-value="option.optionId" selected="selected">
|
||||
<t t-esc="option.description"/>
|
||||
</option>
|
||||
@@ -1208,41 +1215,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div t-name="SearchView" class="o_searchview" role="search" aria-autocomplete="list">
|
||||
<span class="o_searchview_more fa" title="Advanced Search..." role="img" aria-label="Advanced Search..."/>
|
||||
<div class="o_searchview_input_container"/>
|
||||
</div>
|
||||
<t t-name="SearchViewMobile">
|
||||
<div class="o_searchview" role="search" aria-autocomplete="list">
|
||||
<button class="o_enable_searchview btn fa fa-search"/>
|
||||
<div class="o_mobile_search o_hidden">
|
||||
<div class="o_mobile_search_header">
|
||||
<span class="o_mobile_search_close float-left mt16 mb16 mr8 ml16">
|
||||
<i class="fa fa-arrow-left"/>
|
||||
<strong class="float-right ml8">FILTER</strong>
|
||||
</span>
|
||||
<span class="float-right o_mobile_search_clear_facets mt16 mr16">CLEAR</span>
|
||||
</div>
|
||||
<div class="o_searchview_input_container"/>
|
||||
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16"/>
|
||||
<div class="btn btn-primary o_mobile_search_show_result">SEE RESULT</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<input t-name="SearchView.InputView" type="text" class="o_searchview_input" accesskey="Q" placeholder="Search..." role="searchbox" aria-haspopup="true"/>
|
||||
<div t-name="SearchView.FacetView" tabindex="0" class="o_searchview_facet" role="img" aria-label="search">
|
||||
<span t-if="widget.model.has('icon')" t-att-class="'fa ' + widget.model.get('icon') + ' o_searchview_facet_label'"/>
|
||||
<span t-if="!widget.model.has('icon')" class="o_searchview_facet_label">
|
||||
<t t-esc="widget.model.get('category')"/>
|
||||
<div t-name="SearchView.SearchBar" class="o_searchview_input_container">
|
||||
<input type="text" class="o_searchview_input" accesskey="Q" placeholder="Search..." role="searchbox" aria-haspopup="true"/>
|
||||
</div>
|
||||
|
||||
<div t-name="SearchView.SearchFacet" tabindex="0" class="o_searchview_facet" role="img" aria-label="search">
|
||||
<span t-if="widget.facet.type === 'field'" class="o_searchview_facet_label">
|
||||
<t t-esc="widget.facet.filters[0].attrs.string"/>
|
||||
</span>
|
||||
<div class="o_facet_values"/>
|
||||
<span t-else="1" t-att-class="'fa ' + widget.icon + ' o_searchview_facet_label'"/>
|
||||
<div class="o_facet_values">
|
||||
<t t-foreach="widget.facetValues" t-as="facetValue">
|
||||
<span t-if="facetValue_index > 0" class="o_facet_values_sep" t-esc="widget.separator"/> <span t-esc="facetValue"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="fa fa-sm fa-remove o_facet_remove" role="img" aria-label="Remove" title="Remove"/>
|
||||
</div>
|
||||
<span t-name="SearchView.FacetView.Value">
|
||||
<t t-esc="widget.model.get('label') + (widget.optionLabel ? (': ' + widget.optionLabel) : '')"/>
|
||||
</span>
|
||||
|
||||
<t t-name="SearchView.autocomplete">
|
||||
<div class="dropdown-menu o_searchview_autocomplete" role="menu"/>
|
||||
</t>
|
||||
@@ -1259,65 +1252,6 @@
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<t t-name="SearchView.field">
|
||||
<label t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
|
||||
t-att-title="attrs.help"
|
||||
t-att-for="element_id"
|
||||
t-att-style="style">
|
||||
<t t-esc="attrs.string || attrs.name"/>
|
||||
<span t-if="attrs.help">?</span>
|
||||
</label>
|
||||
<div t-att-style="style">
|
||||
<input type="text" size="15" class="o_input"
|
||||
t-att-name="attrs.name"
|
||||
t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined"
|
||||
t-att-id="element_id"
|
||||
t-att-value="defaults[attrs.name] || ''"/>
|
||||
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.date">
|
||||
<label t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
|
||||
t-att-title="attrs.help"
|
||||
t-att-for="element_id"
|
||||
t-att-style="style">
|
||||
<t t-esc="attrs.string || attrs.name"/>
|
||||
<span t-if="attrs.help">?</span>
|
||||
</label>
|
||||
<div t-att-style="style">
|
||||
<span t-att-id="element_id"/>
|
||||
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.field.selection">
|
||||
<label t-att-title="attrs.help"
|
||||
t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
|
||||
t-att-for="element_id"
|
||||
t-att-style="style">
|
||||
<t t-esc="attrs.string || attrs.name"/>
|
||||
<span t-if="attrs.help">?</span>
|
||||
</label>
|
||||
<div t-att-style="style">
|
||||
<select class="o_input"
|
||||
t-att-name="attrs.name"
|
||||
t-att-id="element_id"
|
||||
t-att-autofocus="attrs.default_focus === '1' || undefined">
|
||||
<option t-if="prepend_empty"/>
|
||||
<t t-foreach="attrs.selection" t-as="option">
|
||||
<t t-set="selected" t-value="defaults[attrs.name] === option[0]"/>
|
||||
<option t-if="selected"
|
||||
t-attf-selected="selected"
|
||||
t-att-value="option_index">
|
||||
<t t-esc="option[1]"/>
|
||||
</option>
|
||||
<option t-if="!selected" t-att-value="option_index">
|
||||
<t t-esc="option[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.extended_search.proposition">
|
||||
<div role="menuitem" class="dropdown-item-text o_filter_condition">
|
||||
<span class="o_or_filter">or</span>
|
||||
@@ -1351,30 +1285,56 @@
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown">
|
||||
<button t-attf-class="o_dropdown_toggler_btn btn btn-secondary dropdown-toggle#{widget.isMobile ? ' o-no-caret' : ''}" data-toggle="dropdown" aria-expanded="false" tabindex="-1" aria-label="Favorites" title="Favorites">
|
||||
<span class="fa fa-star"/> Favorites <span t-if="widget.isMobile" class="fa fa-chevron-right float-right mt4"/>
|
||||
</button>
|
||||
<div class="dropdown-menu o_favorites_menu" role="menu">
|
||||
<div class="dropdown-divider user_filter"/>
|
||||
<div class="dropdown-divider shared_filter"/>
|
||||
<a role="menuitem" href="#" class="dropdown-item o_save_search o_closed_menu">Save current search</a>
|
||||
<div role="menuitem" class="dropdown-item-text o_save_name">
|
||||
<t t-name="MobileControlPanel">
|
||||
<div class="o_control_panel">
|
||||
<ol class="breadcrumb" role="navigation"/>
|
||||
<div class="o_cp_searchview" role="search">
|
||||
<t t-call="MobileSearchView"/>
|
||||
</div>
|
||||
<div class="o_cp_left">
|
||||
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel toolbar"/>
|
||||
<aside class="o_cp_sidebar"/>
|
||||
</div>
|
||||
<div class="o_cp_right">
|
||||
<div class="btn-group o_search_options" role="search"/>
|
||||
<nav class="o_cp_pager" role="search" aria-label="Pager"/>
|
||||
<nav class="btn-group o_cp_switch_buttons" role="toolbar" aria-label="View switcher"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="MobileSearchView">
|
||||
<div class="o_searchview" role="search" aria-autocomplete="list">
|
||||
<button class="o_enable_searchview btn fa fa-search"/>
|
||||
<div class="o_mobile_search o_hidden">
|
||||
<div class="o_mobile_search_header">
|
||||
<span class="o_mobile_search_close float-left mt16 mb16 mr8 ml16">
|
||||
<i class="fa fa-arrow-left"/>
|
||||
<strong class="float-right ml8">FILTER</strong>
|
||||
</span>
|
||||
<span class="float-right o_mobile_search_clear_facets mt16 mr16">CLEAR</span>
|
||||
</div>
|
||||
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16"/>
|
||||
<div class="btn btn-primary o_mobile_search_show_result">SEE RESULT</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div t-name="AddNewFavoriteMenu">
|
||||
<button type="button" class="dropdown-item o_generator_menu o_add_favorite o_menu_header">Save Current Search</button>
|
||||
<div t-if="widget.isOpen" class="dropdown-item-text o_generator_menu">
|
||||
<div role="menuitem" class="dropdown-item-text o_favorite_name">
|
||||
<input type="text" class="o_input"></input>
|
||||
</div>
|
||||
<div role="menuitem" class="dropdown-item-text o_save_name">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" id="o_fav_menu_use_by_default" class="custom-control-input"/>
|
||||
<label for="o_fav_menu_use_by_default" class="custom-control-label">Use by default</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" id="o_favorite_use_by_default" class="custom-control-input"/>
|
||||
<label for="o_favorite_use_by_default" class="custom-control-label">Use by default</label>
|
||||
</div>
|
||||
<div role="menuitem" class="dropdown-item-text o_save_name">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" id="o_fav_menu_share_all_users" class="custom-control-input"/>
|
||||
<label for="o_fav_menu_share_all_users" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" id="o_favorite_share_all_users" class="custom-control-input"/>
|
||||
<label for="o_favorite_share_all_users" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
|
||||
</div>
|
||||
<div class="dropdown-item-text o_save_name">
|
||||
<div class="dropdown-item-text o_save_favorite">
|
||||
<button role="menuitem" type="button" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ QUnit.module('ActionManager', {
|
||||
assert.containsOnce(actionManager, '.o_form_view');
|
||||
|
||||
// go back to lazy loaded view
|
||||
testUtils.dom.click($('.o_control_panel .breadcrumb .breadcrumb-item:first'));
|
||||
testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb .breadcrumb-item:first'));
|
||||
assert.containsNone(actionManager, '.o_form_view');
|
||||
assert.containsNone(actionManager, '.o_list_view');
|
||||
assert.containsOnce(actionManager, '.o_kanban_view');
|
||||
@@ -125,9 +125,9 @@ QUnit.module('ActionManager', {
|
||||
|
||||
actionManager.doAction(1);
|
||||
|
||||
assert.containsOnce($('.o_control_panel'), '.o_cp_switch_buttons button[data-toggle="dropdown"]');
|
||||
assert.hasClass($('.o_cp_switch_buttons .o_cp_switch_kanban'), 'active');
|
||||
assert.hasClass($('.o_cp_switch_buttons .o_switch_view_button_icon'), 'fa-th-large');
|
||||
assert.containsOnce(actionManager.$('.o_control_panel'), '.o_cp_switch_buttons button[data-toggle="dropdown"]');
|
||||
assert.hasClass(actionManager.$('.o_cp_switch_buttons .o_cp_switch_kanban'), 'active');
|
||||
assert.hasClass(actionManager.$('.o_cp_switch_buttons .o_switch_view_button_icon'), 'fa-th-large');
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ var NotificationService = require('web.NotificationService');
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var AbstractStorageService = require('web.AbstractStorageService');
|
||||
var BasicFields = require('web.basic_fields');
|
||||
var ControlPanelMixin = require('web.ControlPanelMixin');
|
||||
var core = require('web.core');
|
||||
var ListController = require('web.ListController');
|
||||
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
|
||||
@@ -647,9 +646,9 @@ QUnit.module('ActionManager', {
|
||||
assert.expect(2);
|
||||
|
||||
var ClientAction = AbstractAction.extend({
|
||||
className: 'o_client_action_test',
|
||||
start: function () {
|
||||
this.$el.text('Hello World');
|
||||
this.$el.addClass('o_client_action_test');
|
||||
},
|
||||
});
|
||||
core.action_registry.add('HelloWorldTest', ClientAction);
|
||||
@@ -983,7 +982,7 @@ QUnit.module('ActionManager', {
|
||||
QUnit.test('should not push a loaded state of a client action', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var ClientAction = Widget.extend({
|
||||
var ClientAction = AbstractAction.extend({
|
||||
init: function (parent, action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.controllerID = options.controllerID;
|
||||
@@ -1033,8 +1032,8 @@ QUnit.module('ActionManager', {
|
||||
QUnit.test('change a param of an ir.actions.client in the url', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
className: 'o_client_action',
|
||||
var ClientAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
init: function (parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
var context = action.context;
|
||||
@@ -1042,11 +1041,13 @@ QUnit.module('ActionManager', {
|
||||
},
|
||||
start: function () {
|
||||
assert.step('start');
|
||||
this.$el.text(this.a);
|
||||
this.$('.o_content').text(this.a);
|
||||
this.$el.addClass('o_client_action');
|
||||
this.trigger_up('push_state', {
|
||||
controllerID: this.controllerID,
|
||||
state: {a: this.a},
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
core.action_registry.add('ClientAction', ClientAction);
|
||||
@@ -1060,7 +1061,7 @@ QUnit.module('ActionManager', {
|
||||
// execute the client action
|
||||
actionManager.doAction(9);
|
||||
|
||||
assert.strictEqual(actionManager.$('.o_client_action').text(), 'default value',
|
||||
assert.strictEqual(actionManager.$('.o_client_action .o_content').text(), 'default value',
|
||||
"should have rendered the client action");
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
|
||||
"there should be one controller in the breadcrumbs");
|
||||
@@ -1071,7 +1072,7 @@ QUnit.module('ActionManager', {
|
||||
a: 'new value',
|
||||
});
|
||||
|
||||
assert.strictEqual(actionManager.$('.o_client_action').text(), 'new value',
|
||||
assert.strictEqual(actionManager.$('.o_client_action .o_content').text(), 'new value',
|
||||
"should have rerendered the client action with the correct param");
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
|
||||
"there should still be one controller in the breadcrumbs");
|
||||
@@ -1672,9 +1673,9 @@ QUnit.module('ActionManager', {
|
||||
assert.expect(3);
|
||||
|
||||
var ClientAction = AbstractAction.extend({
|
||||
className: 'o_client_action_test',
|
||||
start: function () {
|
||||
this.$el.text('Hello World');
|
||||
this.$el.addClass('o_client_action_test');
|
||||
},
|
||||
});
|
||||
core.action_registry.add('HelloWorldTest', ClientAction);
|
||||
@@ -1700,11 +1701,13 @@ QUnit.module('ActionManager', {
|
||||
QUnit.test('client action with control panel', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
className: 'o_client_action_test',
|
||||
var ClientAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
start: function () {
|
||||
this.$el.text('Hello World');
|
||||
this.set('title', 'Hello'); // AAB: drop this and replace by getTitle()
|
||||
this.$('.o_content').text('Hello World');
|
||||
this.$el.addClass('o_client_action_test');
|
||||
this._setTitle('Hello');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
core.action_registry.add('HelloWorldTest', ClientAction);
|
||||
@@ -1718,7 +1721,7 @@ QUnit.module('ActionManager', {
|
||||
"there should be one controller in the breadcrumbs");
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Hello',
|
||||
"breadcrumbs should still display the title of the controller");
|
||||
assert.strictEqual(actionManager.$('.o_client_action_test').text(),
|
||||
assert.strictEqual(actionManager.$('.o_client_action_test .o_content').text(),
|
||||
'Hello World', "should have correctly rendered the client action");
|
||||
|
||||
actionManager.destroy();
|
||||
@@ -1728,12 +1731,7 @@ QUnit.module('ActionManager', {
|
||||
QUnit.test('state is pushed for client actions', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
className: 'o_client_action_test',
|
||||
start: function () {
|
||||
this.$el.text('Hello World');
|
||||
},
|
||||
});
|
||||
var ClientAction = AbstractAction.extend({});
|
||||
var actionManager = createActionManager({
|
||||
intercepts: {
|
||||
push_state: function () {
|
||||
@@ -1754,16 +1752,18 @@ QUnit.module('ActionManager', {
|
||||
QUnit.test('breadcrumb is updated on title change', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var ClientAction = Widget.extend(ControlPanelMixin, {
|
||||
className: 'o_client_action_test',
|
||||
var ClientAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
events: {
|
||||
click: function () {
|
||||
this.set("title", 'new title');
|
||||
this._setTitle('new title');
|
||||
},
|
||||
},
|
||||
start: function () {
|
||||
this.set("title", 'initial title');
|
||||
this.$el.text('Hello World');
|
||||
this._setTitle('initial title');
|
||||
this.$('.o_content').text('Hello World');
|
||||
this.$el.addClass('o_client_action_test');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
var actionManager = createActionManager();
|
||||
@@ -2985,9 +2985,11 @@ QUnit.module('ActionManager', {
|
||||
assert.expect(4);
|
||||
|
||||
testUtils.mock.patch(ListController, {
|
||||
getContext: function () {
|
||||
getOwnedQueryParams: function () {
|
||||
return {
|
||||
shouldBeInFilterContext: true,
|
||||
context: {
|
||||
shouldBeInFilterContext: true,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -3031,9 +3033,9 @@ QUnit.module('ActionManager', {
|
||||
|
||||
// save filter
|
||||
testUtils.dom.click($('.o_control_panel .o_search_options .o_dropdown_toggler_btn:contains(Favorites)'));
|
||||
testUtils.dom.click($('.o_control_panel .o_save_search'));
|
||||
$('.o_control_panel .o_save_name input[type=text]').val('some name'); // name the filter
|
||||
testUtils.dom.click($('.o_control_panel .o_save_name button'));
|
||||
testUtils.dom.click($('.o_control_panel .o_add_favorite'));
|
||||
$('.o_control_panel .o_favorite_name input[type=text]').val('some name'); // name the filter
|
||||
testUtils.dom.click($('.o_control_panel .o_save_favorite button'));
|
||||
|
||||
testUtils.mock.unpatch(ListController);
|
||||
actionManager.destroy();
|
||||
@@ -3064,47 +3066,6 @@ QUnit.module('ActionManager', {
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("doAction with option 'keepSearchView'", function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.actions.push({
|
||||
id: 33,
|
||||
name: 'Partners',
|
||||
res_model: 'partner',
|
||||
search_view_id: [1, 'a specific search view'],
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
});
|
||||
|
||||
var checkRPC = false;
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (checkRPC && route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args.domain, [['bar', '=', 1]],
|
||||
"should search with the correct domain");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
actionManager.doAction(33);
|
||||
|
||||
checkRPC = true;
|
||||
testUtils.dom.click($('.o_control_panel .o_search_options .o_dropdown_toggler_btn:contains(Filters)'));
|
||||
testUtils.dom.click($('.o_control_panel .o_filters_menu a:contains(Bar)'));
|
||||
assert.strictEqual($('.o_control_panel .o_facet_values').text().trim(), 'Bar',
|
||||
"the filter on Bar should appear in the search view");
|
||||
|
||||
actionManager.doAction(3, {keepSearchView: true});
|
||||
assert.strictEqual($('.o_control_panel .o_facet_values').text().trim(), 'Bar',
|
||||
"the filter on Bar should still be in the search view");
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("current act_window action is stored in session_storage", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -3259,12 +3220,14 @@ QUnit.module('ActionManager', {
|
||||
assert.expect(4);
|
||||
|
||||
var ClientAction = AbstractAction.extend({
|
||||
className: 'o_test',
|
||||
on_attach_callback: function () {
|
||||
assert.step('on_attach_callback');
|
||||
assert.ok(actionManager.currentDialogController,
|
||||
"the currentDialogController should have been set already");
|
||||
},
|
||||
start: function () {
|
||||
this.$el.addClass('o_test');
|
||||
},
|
||||
});
|
||||
core.action_registry.add('test', ClientAction);
|
||||
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
odoo.define('web.search_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var SearchView = require('web.SearchView');
|
||||
|
||||
QUnit.module('chrome', {}, function () {
|
||||
|
||||
QUnit.module('Search View');
|
||||
|
||||
QUnit.test('Adding a facet to the query creates a facet and a value', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
var field = {};
|
||||
query.add({
|
||||
category: 'Foo',
|
||||
field: field,
|
||||
values: [{label: 'Value', value: 3}]
|
||||
});
|
||||
|
||||
var facet = query.at(0);
|
||||
assert.strictEqual(facet.get('category'), 'Foo');
|
||||
assert.strictEqual(facet.get('field'), field);
|
||||
assert.deepEqual(facet.get('values'), [{label: 'Value', value: 3}]);
|
||||
});
|
||||
|
||||
QUnit.test('Adding two facets', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
query.add([
|
||||
{ category: 'Foo', field: {}, values: [{label: 'Value', value: 3}] },
|
||||
{ category: 'Bar', field: {}, values: [{label: 'Value 2', value: 4}] }
|
||||
]);
|
||||
|
||||
assert.strictEqual(query.length, 2);
|
||||
assert.strictEqual(query.at(0).values.length, 1);
|
||||
assert.strictEqual(query.at(1).values.length, 1);
|
||||
});
|
||||
|
||||
QUnit.test('If a facet already exists, add values to it', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
var field = {};
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
|
||||
|
||||
assert.strictEqual(query.length, 1, "adding an existing facet should merge new values into old facet");
|
||||
var facet = query.at(0);
|
||||
assert.deepEqual(facet.get('values'), [
|
||||
{label: 'V1', value: 0},
|
||||
{label: 'V2', value: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Facet being implicitly changed should trigger change, not add', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
var field = {}, added = false, changed = false;
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
query.on('add', function () { added = true; })
|
||||
.on('change', function () { changed = true; });
|
||||
query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
|
||||
|
||||
assert.ok(!added, "query.add adding values to a facet should not trigger an add");
|
||||
assert.ok(changed, "query.add adding values to a facet should not trigger a change");
|
||||
});
|
||||
|
||||
QUnit.test('Toggling a facet, value which does not exist should add it', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
var field = {};
|
||||
query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
|
||||
assert.strictEqual(query.length, 1, "Should have created a single facet");
|
||||
var facet = query.at(0);
|
||||
assert.strictEqual(facet.values.length, 1, "Facet should have a single value");
|
||||
assert.deepEqual(facet.get('values'), [{label: 'V1', value: 0}],
|
||||
"Facet's value should match input");
|
||||
});
|
||||
|
||||
QUnit.test('Toggling a facet which exists with a value which does not should add the value to the facet', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var field = {};
|
||||
var query = new SearchView.SearchQuery();
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
|
||||
|
||||
assert.strictEqual(query.length, 1, "Should have edited the existing facet");
|
||||
var facet = query.at(0);
|
||||
assert.strictEqual(facet.values.length, 2, "Should have added the value to the existing facet");
|
||||
assert.deepEqual(facet.get('values'), [
|
||||
{label: 'V1', value: 0},
|
||||
{label: 'V2', value: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var field = {};
|
||||
var query = new SearchView.SearchQuery();
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
|
||||
|
||||
query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
|
||||
|
||||
assert.strictEqual(query.length, 1, 'Should have the same single facet');
|
||||
var facet = query.at(0);
|
||||
assert.strictEqual(facet.values.length, 1, "Should only have one value left in the facet");
|
||||
assert.deepEqual(facet.get('values'), [
|
||||
{label: 'V1', value: 0}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Toggling off the last value of a facet should remove the facet', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var field = {};
|
||||
var query = new SearchView.SearchQuery();
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
|
||||
query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
|
||||
assert.strictEqual(query.length, 0, 'Should have removed the facet');
|
||||
});
|
||||
|
||||
QUnit.test('Intermediate emptiness should not remove the facet', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var field = {};
|
||||
var query = new SearchView.SearchQuery();
|
||||
query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
|
||||
|
||||
query.toggle({category: 'A', field: field, values: [
|
||||
{label: 'V1', value: 0},
|
||||
{label: 'V2', value: 1}
|
||||
]});
|
||||
|
||||
assert.strictEqual(query.length, 1, 'Should not have removed the facet');
|
||||
var facet = query.at(0);
|
||||
assert.strictEqual(facet.values.length, 1, "Should have one value");
|
||||
assert.deepEqual(facet.get('values'), [
|
||||
{label: 'V2', value: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('Reseting with multiple facets should still work to load defaults', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var query = new SearchView.SearchQuery();
|
||||
var field = {};
|
||||
query.reset([
|
||||
{category: 'A', field: field, values: [{label: 'V1', value: 0}]},
|
||||
{category: 'A', field: field, values: [{label: 'V2', value: 1}]}]);
|
||||
|
||||
assert.strictEqual(query.length, 1, 'Should have created a single facet');
|
||||
assert.strictEqual(query.at(0).values.length, 2, 'the facet should have merged two values');
|
||||
assert.deepEqual(query.at(0).get('values'), [
|
||||
{label: 'V1', value: 0},
|
||||
{label: 'V2', value: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
// TODO add a test to check that combining two filters with a or normalizes the two filters before
|
||||
// Note: maybe domain.js will be improved to make this easier
|
||||
});
|
||||
});
|
||||
@@ -19,35 +19,6 @@ QUnit.module('core', {}, function () {
|
||||
assert.strictEqual(registry.get('foo'), foo);
|
||||
});
|
||||
|
||||
QUnit.test('extension', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var foo = {};
|
||||
var foo2 = {};
|
||||
var registry = new Registry({
|
||||
foo: foo,
|
||||
});
|
||||
var registry2 = registry.extend({foo: foo2});
|
||||
assert.strictEqual(registry.get('foo'), foo);
|
||||
assert.strictEqual(registry2.get('foo'), foo2);
|
||||
});
|
||||
|
||||
QUnit.test('remain-linked', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var foo = {};
|
||||
var foo2 = {};
|
||||
var registry = new Registry({
|
||||
foo: foo,
|
||||
});
|
||||
|
||||
var registry2 = registry.extend();
|
||||
|
||||
registry.add('foo2', foo2);
|
||||
|
||||
assert.strictEqual(registry.get('foo2'), foo2);
|
||||
assert.strictEqual(registry2.get('foo2'), foo2);
|
||||
});
|
||||
|
||||
QUnit.test('multiget', function (assert) {
|
||||
assert.expect(1);
|
||||
@@ -64,17 +35,18 @@ QUnit.module('core', {}, function () {
|
||||
"Registry getAny should find first defined key");
|
||||
});
|
||||
|
||||
QUnit.test('extended-multiget', function (assert) {
|
||||
assert.expect(1);
|
||||
QUnit.test('keys and values are properly ordered', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var foo = {};
|
||||
var bar = {};
|
||||
var registry = new Registry({
|
||||
foo: foo,
|
||||
bar: bar,
|
||||
});
|
||||
var registry2 = registry.extend();
|
||||
assert.strictEqual(registry2.getAny(['qux', 'grault', 'bar', 'foo']), bar);
|
||||
var registry = new Registry();
|
||||
|
||||
registry
|
||||
.add('fred', 'foo', 3)
|
||||
.add('george', 'bar', 2)
|
||||
.add('ronald', 'qux', 4);
|
||||
|
||||
assert.deepEqual(registry.keys(), ['george', 'fred', 'ronald']);
|
||||
assert.deepEqual(registry.values(), ['bar', 'foo', 'qux']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
var val = '<script>throw Error();</script><script>throw Error();</script>';
|
||||
testUtils.fields.editInput(form.$('input'), val);
|
||||
testUtils.fields.editInput(form.$('input.o_field_widget[name="foo"]'), val);
|
||||
|
||||
// save
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
@@ -18,9 +18,6 @@ var DebouncedField = basicFields.DebouncedField;
|
||||
var JournalDashboardGraph = basicFields.JournalDashboardGraph;
|
||||
var _t = core._t;
|
||||
|
||||
var DOM = testUtils.DOM;
|
||||
var mock = testUtils.mock;
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('basic_fields', {
|
||||
@@ -166,7 +163,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
// change the value
|
||||
testUtils.fields.editInput(form.$('input'), 'new value');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
|
||||
assert.verifySteps([], "_setValue shouldn't have been called yet");
|
||||
|
||||
// save
|
||||
@@ -236,12 +233,12 @@ QUnit.module('basic_fields', {
|
||||
"checkbox should now be unchecked");
|
||||
|
||||
// check the checkbox by clicking on label
|
||||
testUtils.dom.click(form.$('label:first'));
|
||||
testUtils.dom.click(form.$('.o_form_view label:first'));
|
||||
assert.containsOnce(form, '.o_field_boolean input:checked',
|
||||
"checkbox should now be checked");
|
||||
|
||||
// uncheck it back
|
||||
testUtils.dom.click(form.$('label:first'));
|
||||
testUtils.dom.click(form.$('.o_form_view label:first'));
|
||||
assert.containsNone(form, '.o_field_boolean input:checked',
|
||||
"checkbox should now be unchecked");
|
||||
|
||||
@@ -394,17 +391,17 @@ QUnit.module('basic_fields', {
|
||||
assert.containsOnce(list, 'button i.fa.fa-circle.text-muted',
|
||||
"should have 1 muted button");
|
||||
|
||||
assert.hasAttrValue(list.$('button').first(), 'title', "Reported in last payslips",
|
||||
"active buttons should have proper tooltip");
|
||||
assert.hasAttrValue(list.$('button').last(), 'title', "To Report in Payslip",
|
||||
"inactive buttons should have proper tooltip");
|
||||
assert.hasAttrValue(list.$('.o_list_view button').first(), 'title',
|
||||
"Reported in last payslips", "active buttons should have proper tooltip");
|
||||
assert.hasAttrValue(list.$('.o_list_view button').last(), 'title',
|
||||
"To Report in Payslip", "inactive buttons should have proper tooltip");
|
||||
|
||||
// clicking on first button to check the state is properly changed
|
||||
testUtils.dom.click(list.$('button').first());
|
||||
testUtils.dom.click(list.$('.o_list_view button').first());
|
||||
assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 3,
|
||||
"should have 3 green buttons");
|
||||
|
||||
testUtils.dom.click(list.$('button').first());
|
||||
testUtils.dom.click(list.$('.o_list_view button').first());
|
||||
assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 4,
|
||||
"should have 4 green buttons");
|
||||
list.destroy();
|
||||
@@ -553,14 +550,14 @@ QUnit.module('basic_fields', {
|
||||
'The value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '0.000',
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '0.000',
|
||||
'The value should be rendered with correct precision.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '108.2458938598598');
|
||||
assert.strictEqual(form.$('input').val(), '108.2458938598598',
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598');
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '18.8958938598598');
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598');
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896',
|
||||
'The new value should be rounded properly.');
|
||||
@@ -691,13 +688,13 @@ QUnit.module('basic_fields', {
|
||||
'The value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '-8.9',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '-8.9',
|
||||
'The input should be rendered without the currency symbol.');
|
||||
assert.strictEqual(form.$('input').parent().children().first().text(), '$',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$',
|
||||
'The input should be preceded by a span containing the currency symbol.');
|
||||
|
||||
testUtils.fields.editInput(form.$('.o_field_monetary input'), '109.2458938598598');
|
||||
assert.strictEqual(form.$('input').val(), '109.2458938598598',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '109.2458938598598',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -730,7 +727,7 @@ QUnit.module('basic_fields', {
|
||||
'Float field with option type must have a type attribute.');
|
||||
assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number',
|
||||
'Float field with option type must have a type attribute equals to "number".');
|
||||
testUtils.fields.editInput(form.$('input'), '123456.7890');
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890');
|
||||
testUtils.form.clickSave(form);
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').val(), '123456.789',
|
||||
@@ -763,7 +760,7 @@ QUnit.module('basic_fields', {
|
||||
assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text',
|
||||
'Float field with option type must have a text type (default type).');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '123456.7890');
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890');
|
||||
testUtils.form.clickSave(form);
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').val(), '123,456.8',
|
||||
@@ -1086,7 +1083,7 @@ QUnit.module('basic_fields', {
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '<script>throw Error();</script>');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '<script>throw Error();</script>');
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), '<script>throw Error();</script>',
|
||||
'the value should have been properly escaped');
|
||||
@@ -3329,13 +3326,13 @@ QUnit.module('basic_fields', {
|
||||
'The value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '9.10',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '9.10',
|
||||
'The input should be rendered without the currency symbol.');
|
||||
assert.strictEqual(form.$('input').parent().children().first().text(), '$',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$',
|
||||
'The input should be preceded by a span containing the currency symbol.');
|
||||
|
||||
testUtils.fields.editInput(form.$('.o_field_monetary input'), '108.2458938598598');
|
||||
assert.strictEqual(form.$('input').val(), '108.2458938598598',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3370,13 +3367,13 @@ QUnit.module('basic_fields', {
|
||||
'The value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').first().val(), '0.00',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '0.00',
|
||||
'The input should be rendered without the currency symbol.');
|
||||
assert.strictEqual(form.$('input').parent().children().eq(1).text(), '€',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), '€',
|
||||
'The input should be followed by a span containing the currency symbol.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input').first(), '108.2458938598598');
|
||||
assert.strictEqual(form.$('input').first().val(), '108.2458938598598',
|
||||
testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '108.2458938598598');
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3427,13 +3424,13 @@ QUnit.module('basic_fields', {
|
||||
'The value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').first().val(), '99.1234',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.1234',
|
||||
'The input should be rendered without the currency symbol.');
|
||||
assert.strictEqual(form.$('input').parent().children().eq(1).text(), 'Bs.F',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), 'Bs.F',
|
||||
'The input should be followed by a span containing the currency symbol.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input').first(), '99.111111111');
|
||||
assert.strictEqual(form.$('input').first().val(), '99.111111111',
|
||||
testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '99.111111111');
|
||||
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.111111111',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3743,11 +3740,11 @@ QUnit.module('basic_fields', {
|
||||
'Integer field should be considered set for value 0.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '0',
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '0',
|
||||
'The value should be rendered correctly in edit mode.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '-18');
|
||||
assert.strictEqual(form.$('input').val(), '-18',
|
||||
testUtils.fields.editInput(form.$('input[name=int_field]'), '-18');
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '-18',
|
||||
'The value should be correctly displayed in the input.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3831,7 +3828,7 @@ QUnit.module('basic_fields', {
|
||||
assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number',
|
||||
'Integer field with option type must have a type attribute equals to "number".');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '1234567890');
|
||||
testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890');
|
||||
testUtils.form.clickSave(form);
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').val(), '1234567890',
|
||||
@@ -3864,7 +3861,7 @@ QUnit.module('basic_fields', {
|
||||
assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text',
|
||||
'Integer field without option type must have a text type (default type).');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '1234567890');
|
||||
testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890');
|
||||
testUtils.form.clickSave(form);
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').val(), '1,234,567,890',
|
||||
@@ -3903,11 +3900,11 @@ QUnit.module('basic_fields', {
|
||||
'The formatted time value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '09:06',
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '09:06',
|
||||
'The value should be rendered correctly in the input.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '-11:48');
|
||||
assert.strictEqual(form.$('input').val(), '-11:48',
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '-11:48');
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '-11:48',
|
||||
'The new value should be displayed properly in the input.');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3945,10 +3942,10 @@ QUnit.module('basic_fields', {
|
||||
'The formatted value should be displayed properly.');
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), '4.55',
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '4.55',
|
||||
'The value should be rendered correctly in the input.');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), '2.3');
|
||||
testUtils.fields.editInput(form.$('input[name=qux]'), '2.3');
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30',
|
||||
@@ -4057,7 +4054,7 @@ QUnit.module('basic_fields', {
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
|
||||
config: {
|
||||
device: {
|
||||
size_class: config.device.SIZES.LG,
|
||||
|
||||
@@ -262,7 +262,7 @@ QUnit.module('fields', {}, function () {
|
||||
'the removed record should not be in kanban anymore');
|
||||
|
||||
// save the record
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -431,7 +431,7 @@ QUnit.module('fields', {}, function () {
|
||||
'the updated row still has the correct values');
|
||||
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.containsN(form, '.o_list_view td.o_list_number', 2,
|
||||
'should contain 2 subrecords');
|
||||
assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
|
||||
@@ -545,7 +545,7 @@ QUnit.module('fields', {}, function () {
|
||||
'new name', 'the updated row still has the correct values');
|
||||
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.containsN(form, '.o_list_view td.o_list_number', 2,
|
||||
'should contain 2 subrecords');
|
||||
assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
|
||||
@@ -687,73 +687,26 @@ QUnit.module('fields', {}, function () {
|
||||
}
|
||||
});
|
||||
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(0).prop('checked'),
|
||||
assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(0).prop('checked'),
|
||||
"first checkbox should not be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
|
||||
assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(1).prop('checked'),
|
||||
"second checkbox should not be checked");
|
||||
assert.ok(form.$('.custom-checkbox input').eq(2).prop('checked'),
|
||||
assert.ok(form.$('.o_form_view .custom-checkbox input').eq(2).prop('checked'),
|
||||
"third checkbox should be checked");
|
||||
|
||||
testUtils.dom.click(form.$('.custom-checkbox input:checked'));
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.o_form_view .custom-checkbox input:checked'));
|
||||
testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
|
||||
|
||||
assert.ok(form.$('.custom-checkbox input').eq(0).prop('checked'),
|
||||
assert.ok(form.$('.o_form_view .custom-checkbox input').eq(0).prop('checked'),
|
||||
"first checkbox should be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
|
||||
assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(1).prop('checked'),
|
||||
"second checkbox should not be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(2).prop('checked'),
|
||||
assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(2).prop('checked'),
|
||||
"third checkbox should not be checked");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2many checkboxes with default values', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
this.data.partner.fields.turtles.default = [3];
|
||||
this.data.partner.fields.turtles.type = "many2many";
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles" widget="many2many_checkboxes">' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0].turtles, [[6, false, [1]]],
|
||||
"correct values should have been sent to create");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(0).prop('checked'),
|
||||
"first checkbox should not be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
|
||||
"second checkbox should not be checked");
|
||||
assert.ok(form.$('.custom-checkbox input').eq(2).prop('checked'),
|
||||
"third checkbox should be checked");
|
||||
|
||||
testUtils.dom.click(form.$('.custom-checkbox input:checked'));
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
testUtils.dom.click(form.$('.custom-checkbox input').first());
|
||||
|
||||
assert.ok(form.$('.custom-checkbox input').eq(0).prop('checked'),
|
||||
"first checkbox should be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
|
||||
"second checkbox should not be checked");
|
||||
assert.notOk(form.$('.custom-checkbox input').eq(2).prop('checked'),
|
||||
"third checkbox should not be checked");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -904,7 +857,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary').first());
|
||||
|
||||
// there is nothing left to save -> should not do a 'write' RPC
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.verifySteps([
|
||||
'read', // read initial record (on partner)
|
||||
@@ -980,27 +933,27 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
|
||||
'there should be 40 records displayed on page 1');
|
||||
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
|
||||
assert.verifySteps(['read', 'onchange', 'read', 'read']);
|
||||
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
|
||||
'41-45 / 45', "pager should be correct");
|
||||
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
|
||||
'there should be 5 records displayed on page 2');
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
|
||||
'1-40 / 45', "pager should be correct");
|
||||
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
|
||||
'there should be 40 records displayed on page 1');
|
||||
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
|
||||
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
|
||||
'41-45 / 45', "pager should be correct");
|
||||
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
|
||||
'there should be 5 records displayed on page 2');
|
||||
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
|
||||
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
|
||||
'1-40 / 45', "pager should be correct");
|
||||
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
|
||||
|
||||
@@ -244,7 +244,7 @@ QUnit.module('fields', {}, function () {
|
||||
// save and close modal
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
|
||||
// save form
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
// click next on pager
|
||||
testUtils.dom.click(form.pager.$('.o_pager_next'));
|
||||
|
||||
@@ -316,7 +316,7 @@ QUnit.module('fields', {}, function () {
|
||||
// save and close modal
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
|
||||
// save form
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
// click next on pager
|
||||
testUtils.dom.click(form.pager.$('.o_pager_next'));
|
||||
|
||||
@@ -562,7 +562,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// save the main record, and check that no extra rpcs are done (record
|
||||
// is not dirty, only a related record was modified)
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.verifySteps(['read', 'get_formview_id', 'load_views', 'read', 'write', 'onchange', 'read']);
|
||||
form.destroy();
|
||||
});
|
||||
@@ -681,7 +681,7 @@ QUnit.module('fields', {}, function () {
|
||||
'value of the m2o should have been correctly updated');
|
||||
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('a.o_form_uri').text(), 'Partner 20',
|
||||
"should display correct value after save");
|
||||
|
||||
@@ -709,7 +709,7 @@ QUnit.module('fields', {}, function () {
|
||||
// Remove value from many2one and then save, there should not have href with id and model on m2o anchor
|
||||
testUtils.form.clickEdit(form);
|
||||
form.$('.o_field_many2one input').val('').trigger('keyup').trigger('focusout');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.hasAttrValue(form.$('a.o_form_uri'), 'href', "#",
|
||||
"href should have #");
|
||||
@@ -795,10 +795,10 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
assert.strictEqual(form.$('input').val(), "first record",
|
||||
assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), "first record",
|
||||
"actual field value should be displayed to be edited");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.ok(form.$('a:contains(and some address)').length,
|
||||
"should still display additional result");
|
||||
@@ -959,7 +959,7 @@ QUnit.module('fields', {}, function () {
|
||||
},
|
||||
});
|
||||
await testUtils.fields.many2one.searchAndClickItem('trululu', 'b');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.verifySteps(['name_create'],
|
||||
"should wait for the name_create before creating the record");
|
||||
@@ -1059,7 +1059,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.many2one.searchAndClickItem('trululu', {search: 'b'});
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.verifySteps(['name_create'],
|
||||
"should wait for the name_create before creating the record");
|
||||
@@ -1138,7 +1138,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.hasClass(form.$('.o_data_row:nth(1)'),'o_selected_row',
|
||||
"the second row should be in edition");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.containsOnce(form, '.o_data_row',
|
||||
"there should be 1 row saved (the second one was empty and invalid)");
|
||||
@@ -1674,7 +1674,7 @@ QUnit.module('fields', {}, function () {
|
||||
displayName = 'new value';
|
||||
testUtils.dom.click(form.$('.o_data_cell'));
|
||||
testUtils.fields.editInput(form.$('.o_data_cell input'), displayName);
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -1727,7 +1727,7 @@ QUnit.module('fields', {}, function () {
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -1866,7 +1866,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// Saving the modal and then the original model
|
||||
testUtils.dom.click($modal.find('.modal-footer .btn-primary'));
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.verifySteps(['onchange sequence', 'partner_type write']);
|
||||
|
||||
@@ -1897,7 +1897,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
testUtils.dom.click(form.$('input'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=product_id] input'));
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -1923,7 +1923,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
testUtils.dom.click(form.$('input:eq(2)'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -1956,7 +1956,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
|
||||
assert.strictEqual(count, 3, "should have done 3 rpcs (default_get, onchange, name_get)");
|
||||
assert.strictEqual(form.$('input').val(), 'hello world',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'hello world',
|
||||
"should have taken the correct display name");
|
||||
form.destroy();
|
||||
});
|
||||
@@ -1973,15 +1973,15 @@ QUnit.module('fields', {}, function () {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty');
|
||||
assert.strictEqual(form.$('a[name=product_id]').text(), '', 'the tag a should be empty');
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
testUtils.fields.many2one.clickOpenDropdown('product_id');
|
||||
testUtils.fields.many2one.clickItem('product_id','xphone');
|
||||
assert.strictEqual(form.$('input').val(), "xphone", "should have selected xphone");
|
||||
assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), "xphone", "should have selected xphone");
|
||||
|
||||
testUtils.form.clickDiscard(form);;
|
||||
assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty');
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.strictEqual(form.$('a[name=product_id]').text(), '', 'the tag a should be empty');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2103,7 +2103,7 @@ QUnit.module('fields', {}, function () {
|
||||
// select a new value then cancel the creation of the new one --> restore the previous
|
||||
testUtils.fields.many2one.clickOpenDropdown('product_id');
|
||||
testUtils.fields.many2one.clickItem('product_id','o');
|
||||
assert.strictEqual(form.$('input').val(), "xphone", "should have selected xphone");
|
||||
assert.strictEqual(form.$('.o_field_many2one input').val(), "xphone", "should have selected xphone");
|
||||
form.$('.o_field_many2one input').focus().val('new product').trigger('keyup').trigger('blur');
|
||||
assert.strictEqual($('.modal').length, 1, "there should be one opened modal");
|
||||
testUtils.dom.click($('.modal .modal-footer .btn:contains(Cancel)'));
|
||||
@@ -2739,7 +2739,7 @@ QUnit.module('fields', {}, function () {
|
||||
'The display name field should have the right value');
|
||||
|
||||
// Save the whole thing
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
x2mList = form.$('.o_field_x2many_list[name=p]');
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.fields.editInput($secondTarget, 9);
|
||||
testUtils.fields.editInput($secondTarget, $secondTarget.val() + 9);
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -397,6 +397,11 @@ QUnit.module('fields', {}, function () {
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
intercepts: {
|
||||
load_filters: function (event) {
|
||||
throw new Error('Should not load filters');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -700,7 +705,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
|
||||
"should have the 3 rows in the new order");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.deepEqual(_.map(this.data.turtle.records, function (turtle) {
|
||||
return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int');
|
||||
@@ -777,7 +782,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_data_cell').eq(1));
|
||||
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
|
||||
testUtils.fields.editSelect($cell, "hop");
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -839,7 +844,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_data_cell').eq(1));
|
||||
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
|
||||
testUtils.fields.editSelect($cell, "hop");
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/),
|
||||
["second", "record", "aaa"],
|
||||
@@ -960,7 +965,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should still have the 3 rows in the correct order");
|
||||
assert.strictEqual(turtleOnchange, 3, "should update all lines");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -1004,7 +1009,7 @@ QUnit.module('fields', {}, function () {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
|
||||
"should have the 3 rows in the correct order");
|
||||
|
||||
@@ -1021,8 +1026,8 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.form.clickSave(form);
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip",
|
||||
"should display to record in 'turtle_int' order");
|
||||
@@ -1098,7 +1103,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp');
|
||||
|
||||
// the domain fail if the widget does not use the allready loaded data.
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
|
||||
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "blurp#21#22#23#24#25#26#27#28#29",
|
||||
"should display the records in order with the changes");
|
||||
@@ -1130,7 +1135,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
|
||||
"should display the records in order after resequence (display record with turtle_int=0)");
|
||||
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
|
||||
assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
|
||||
"should display the records in order after resequence");
|
||||
@@ -1184,7 +1189,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first'));
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1');
|
||||
@@ -1259,7 +1264,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first'));
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1');
|
||||
@@ -1693,7 +1698,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip",
|
||||
"should edit the correct row");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
|
||||
"should still have the 3 rows in the new order");
|
||||
|
||||
@@ -1904,7 +1909,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length,
|
||||
0, 'new record should not be on page 1');
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
// delete a record on page one
|
||||
checkRead = true;
|
||||
@@ -1920,7 +1925,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
|
||||
'1-40 / 45', "pager range should be correct");
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
// add and delete records in both pages
|
||||
testUtils.form.clickEdit(form);
|
||||
@@ -1956,7 +1961,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length,
|
||||
1, 'new records should be on page 2');
|
||||
// save
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2088,7 +2093,7 @@ QUnit.module('fields', {}, function () {
|
||||
"first row of o2m should be readonly again");
|
||||
|
||||
// discard changes
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
|
||||
"changes shouldn't have been discarded yet, waiting for user confirmation");
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary'));
|
||||
@@ -2100,7 +2105,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_one2many tbody td').first());
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value");
|
||||
testUtils.dom.click(form.$el);
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
// FIXME: this next test doesn't pass as the save of updates of
|
||||
// relational data is temporarily disabled
|
||||
// assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
|
||||
@@ -2191,7 +2196,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should have 1 delete button (another record is supposed to have been unlinked)");
|
||||
|
||||
// save and check that the correct command has been generated
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2251,7 +2256,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should have 1 remove button (another record is supposed to have been deleted)");
|
||||
|
||||
// save and check that the correct command has been generated
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
// FIXME: it would be nice to test that the view is re-rendered correctly,
|
||||
// but as the relational data isn't re-fetched, the rendering is ok even
|
||||
@@ -2451,7 +2456,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.dom.click(form.$el);
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 5',
|
||||
"pager should display the correct total");
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2522,7 +2527,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
|
||||
'the remaining subrecord should be "new name"');
|
||||
|
||||
testUtils.form.clickSave(form);; // save the record
|
||||
testUtils.form.clickSave(form); // save the record
|
||||
assert.strictEqual(nbWrite, 1, "should have write the changes in DB");
|
||||
|
||||
form.destroy();
|
||||
@@ -2622,7 +2627,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// enter another value and save
|
||||
testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'gemuse');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows");
|
||||
assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
|
||||
"should have one td with the new value");
|
||||
@@ -2661,7 +2666,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should have 3 data rows");
|
||||
|
||||
// cancel the edition
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
testUtils.dom.click($('.modal-footer button.btn-primary').first());
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
"should have 1 data rows");
|
||||
@@ -2754,7 +2759,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// click on Add an item again, then click on save
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.containsNone(form, 'tr.o_data_row',
|
||||
"should still have 0 data rows");
|
||||
|
||||
@@ -2785,13 +2790,13 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
// the record currently being added should not count in the pager
|
||||
assert.ok(!form.$('.o_cp_pager').is(':visible'),
|
||||
"pager should not be visible");
|
||||
assert.isNotVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable one2many list, adding line, then discarding', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expect(2);
|
||||
|
||||
this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' });
|
||||
this.data.partner.records[0].turtles = [1, 2, 3, 4];
|
||||
@@ -2813,13 +2818,15 @@ QUnit.module('fields', {}, function () {
|
||||
// add a record, then discard
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
|
||||
// confirm the discard operation
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary'));
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should still be visible");
|
||||
assert.isVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager'));
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should display correct values");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2849,9 +2856,8 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
// go on next page. The new record is not valid and should be discarded
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
"should have 1 data rows");
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
assert.containsOnce(form, 'tr.o_data_row');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2885,15 +2891,15 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// go to next page. The new record is not valid, but dirty. we should
|
||||
// see a confirm dialog
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should still display the correct total");
|
||||
|
||||
// click on cancel
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-secondary'));
|
||||
|
||||
assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4',
|
||||
"pager should again display the correct total");
|
||||
assert.containsOnce(form, '.o_field_one2many input.o_field_invalid',
|
||||
"there should be an invalid input in the one2many");
|
||||
@@ -2927,16 +2933,14 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
// go on next page
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
// discard
|
||||
testUtils.form.clickDiscard(form);;
|
||||
testUtils.form.clickDiscard(form);
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary'));
|
||||
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
"should have 1 data row");
|
||||
assert.ok(!form.$('.o_cp_pager').is(':visible'),
|
||||
"pager should not be visible");
|
||||
assert.containsOnce(form, 'tr.o_data_row');
|
||||
assert.isNotVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3079,7 +3083,7 @@ QUnit.module('fields', {}, function () {
|
||||
"onchange should have been triggered");
|
||||
|
||||
// save and check everything is fine
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1,
|
||||
"should have one row with turtle_foo value");
|
||||
assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1,
|
||||
@@ -3284,7 +3288,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_field_one2many tbody td').first());
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value");
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange',
|
||||
"display name of first record in o2m list should be 'new value'");
|
||||
@@ -3430,7 +3434,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1));
|
||||
testUtils.dom.click($('.day:contains(22)'));
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']);
|
||||
form.destroy();
|
||||
@@ -3501,7 +3505,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'z');
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -3566,7 +3570,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first'));
|
||||
testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp');
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -3664,7 +3668,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.containsN(form, '.o_field_widget', delta,
|
||||
"all (non visible) field widgets should have been destroyed");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').text(), "first20second-20",
|
||||
"should correctly refresh the records after save");
|
||||
@@ -3958,7 +3962,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should display 'xpad' in a td");
|
||||
|
||||
// save the record
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -4186,7 +4190,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should not have unselected the row after edition");
|
||||
|
||||
testUtils.fields.editInput(form.$('td input.o_field_widget'), 'abc');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('td:contains(abc)').length, 1,
|
||||
"should have a row with the correct value");
|
||||
@@ -4531,7 +4535,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
testUtils.dom.click(form.$('td.o_field_x2many_list_row_add a'));
|
||||
testUtils.fields.editInput(form.$('td input[name="turtle_foo"]'), 'cat');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -4768,7 +4772,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(0) td:first'));
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(nbFetchSpecialDomain, 1,
|
||||
"should only fetch special domain once");
|
||||
form.destroy();
|
||||
@@ -5068,7 +5072,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
|
||||
'should display the name, one2many and many2many value');
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
|
||||
'should display the name, one2many and many2many value after save');
|
||||
@@ -5263,7 +5267,7 @@ QUnit.module('fields', {}, function () {
|
||||
// confirm the changes in the modal
|
||||
testUtils.dom.click($('.modal .modal-footer .btn-primary'));
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -5401,7 +5405,7 @@ QUnit.module('fields', {}, function () {
|
||||
'My little Foo Value', "should have a value for the foo field");
|
||||
|
||||
// save the view to force a create of the new record in the one2many
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.containsOnce(form, '.o_field_widget .o_kanban_view',
|
||||
"should have one inner kanban view for the one2many field");
|
||||
assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 1,
|
||||
@@ -5489,7 +5493,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.hasClass(form.$('tr.o_data_row:first'),'o_selected_row',
|
||||
"first row should be selected");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -5536,7 +5540,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.hasClass(form.$('tr.o_data_row:eq(1)'),'o_selected_row',
|
||||
"second row should be selected");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -6049,7 +6053,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
testUtils.dom.click(form.$('.o_data_cell:first'));
|
||||
testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'abc');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -6254,16 +6258,13 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 41,
|
||||
"should have 41 data rows on the current page");
|
||||
assert.hasClass(form.$('tr.o_data_row').last(),'o_selected_row',
|
||||
"last row should be selected");
|
||||
assert.containsN(form, 'tr.o_data_row', 41);
|
||||
assert.hasClass(form.$('tr.o_data_row').last(), 'o_selected_row');
|
||||
|
||||
testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 40,
|
||||
"should have 40 data rows on the current page");
|
||||
assert.containsN(form, 'tr.o_data_row', 40);
|
||||
|
||||
assert.verifySteps(['read', 'read', 'default_get', 'write', 'read', 'read']);
|
||||
form.destroy();
|
||||
@@ -6342,27 +6343,23 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 2,
|
||||
"should have 2 data rows on the current page");
|
||||
assert.containsN(form, 'tr.o_data_row', 2);
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 3,
|
||||
"should have 3 data rows on the current page (2 records and the created line)");
|
||||
assert.containsN(form, 'tr.o_data_row', 3);
|
||||
|
||||
assert.hasClass(form.$('tr.o_data_row').first(),'o_selected_row',
|
||||
"first row should be selected");
|
||||
assert.hasClass(form.$('tr.o_data_row').first(), 'o_selected_row');
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row input').val(), 'default foo',
|
||||
"selected input should have correct string");
|
||||
|
||||
testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 40,
|
||||
"should have 40 data rows on the current page");
|
||||
assert.containsN(form, 'tr.o_data_row', 40);
|
||||
|
||||
assert.verifySteps(['read', 'read', 'read', 'default_get', 'write', 'read', 'read']);
|
||||
form.destroy();
|
||||
@@ -6571,7 +6568,7 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual(form.$('.o_data_cell').text(), '1 record',
|
||||
"should correctly display the value of the inner o2m");
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -6634,7 +6631,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// we make sure here that when we save, the values are the current
|
||||
// values displayed in the field.
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -6664,7 +6661,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'pinky pie');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -6730,7 +6727,7 @@ QUnit.module('fields', {}, function () {
|
||||
"should have added the new row to the nested one2many");
|
||||
testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary'));
|
||||
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -6857,7 +6854,7 @@ QUnit.module('fields', {}, function () {
|
||||
var inputText = 'ninja';
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText);
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
|
||||
form.destroy();
|
||||
@@ -6898,7 +6895,7 @@ QUnit.module('fields', {}, function () {
|
||||
var inputText = 'ninja';
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText);
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), inputText + "blipyopkawa");
|
||||
form.destroy();
|
||||
@@ -7187,7 +7184,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
|
||||
'data has been properly loaded');
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
|
||||
'data has been properly saved');
|
||||
@@ -7414,7 +7411,7 @@ QUnit.module('fields', {}, function () {
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(1)'));
|
||||
// click add pasta
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(2)'));
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual($('.o_data_cell').text(), "pizzapasta");
|
||||
|
||||
form.destroy();
|
||||
@@ -7638,7 +7635,7 @@ QUnit.module('fields', {}, function () {
|
||||
// open first partner so changes from previous action are applied
|
||||
testUtils.dom.click(form.$('.o_data_row:eq(0)'));
|
||||
testUtils.dom.click($('.modal .btn-primary'));
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(numUserOnchange, 2,
|
||||
'there should 2 and only 2 onchange from closing the partner modal');
|
||||
@@ -7796,7 +7793,7 @@ QUnit.module('fields', {}, function () {
|
||||
form.$('.o_input[name="turtle_foo"]'),
|
||||
'ninja',
|
||||
['input', $.Event('keydown', {which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB,})]
|
||||
);;
|
||||
);
|
||||
|
||||
assert.strictEqual(form.$('.o_field_text').text(), 'blipninja',
|
||||
'current line should be saved');
|
||||
@@ -7900,11 +7897,11 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), 'some foo value');
|
||||
testUtils.fields.editInput(form.$('.o_field_widget[name=turtles] input'), 'some foo value');
|
||||
|
||||
// click to add a second row to unselect the current one, then save
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
assert.containsOnce(form, '.o_form_readonly',
|
||||
"form view should be in readonly");
|
||||
@@ -7977,7 +7974,7 @@ QUnit.module('fields', {}, function () {
|
||||
'The display name field should have the right value');
|
||||
|
||||
// Save the whole thing
|
||||
testUtils.form.clickSave(form);;
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
x2mList = form.$('.o_field_x2many_list[name=p]');
|
||||
|
||||
|
||||
@@ -615,7 +615,7 @@ QUnit.module('relational_fields', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('widget selection, edition and on many2one field', function (assert) {
|
||||
QUnit.test('widget selection, edition and on many2one field', function (assert) {
|
||||
assert.expect(18);
|
||||
|
||||
this.data.partner.onchanges = {product_id: function () {}};
|
||||
@@ -640,7 +640,7 @@ QUnit.module('relational_fields', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(!form.$('select').length, "should not have a select tag in dom");
|
||||
assert.containsNone(form.$('.o_form_view'), 'select');
|
||||
assert.strictEqual(form.$('.o_field_widget[name=product_id]').text(), 'xphone',
|
||||
"should have rendered the many2one field correctly");
|
||||
assert.strictEqual(form.$('.o_field_widget[name=trululu]').text(), '',
|
||||
@@ -650,11 +650,10 @@ QUnit.module('relational_fields', {
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
assert.containsN(form, 'select', 3,
|
||||
"should have 3 select tag in dom");
|
||||
assert.strictEqual(form.$('select[name="product_id"] option:contains(xphone)').length, 1,
|
||||
assert.containsN(form.$('.o_form_view'), 'select', 3);
|
||||
assert.containsOnce(form, 'select[name="product_id"] option:contains(xphone)',
|
||||
"should have fetched xphone option");
|
||||
assert.strictEqual(form.$('select[name="product_id"] option:contains(xpad)').length, 1,
|
||||
assert.containsOnce(form, 'select[name="product_id"] option:contains(xpad)',
|
||||
"should have fetched xpad option");
|
||||
assert.strictEqual(form.$('select[name="product_id"]').val(), "37",
|
||||
"should have correct product_id value");
|
||||
@@ -757,7 +756,7 @@ QUnit.module('relational_fields', {
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.fields.editSelect(form.$('select'), "false");
|
||||
testUtils.fields.editSelect(form.$('.o_form_view select'), 'false');
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
form.destroy();
|
||||
@@ -1297,11 +1296,11 @@ QUnit.module('relational_fields', {
|
||||
|
||||
// update foo, which will trigger an onchange and update timmy
|
||||
// -> m2mtags input should not have taken the focus
|
||||
form.$('input:first').focus();
|
||||
testUtils.fields.editInput(form.$('input:first'), 'trigger onchange');
|
||||
form.$('input[name=foo]').focus();
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger onchange');
|
||||
assert.containsNone(form, '.o_field_many2manytags .badge',
|
||||
"should contain no tags");
|
||||
assert.strictEqual(form.$('input:first').get(0), document.activeElement,
|
||||
assert.strictEqual(form.$('input[name=foo]').get(0), document.activeElement,
|
||||
"foo input should have kept the focus");
|
||||
|
||||
// add a tag -> m2mtags input should still have the focus
|
||||
@@ -1864,7 +1863,6 @@ QUnit.module('relational_fields', {
|
||||
|
||||
QUnit.test('name_create in form dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
@@ -1891,7 +1889,7 @@ QUnit.module('relational_fields', {
|
||||
|
||||
testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.many2one.searchAndClickItem('product_id',
|
||||
{selector:'.modal', search:'new record'});
|
||||
{selector: '.modal', search: 'new record'});
|
||||
|
||||
assert.verifySteps(['name_create']);
|
||||
|
||||
@@ -1922,7 +1920,7 @@ QUnit.module('relational_fields', {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// Cuurent Form
|
||||
// Current Form
|
||||
assert.equal(form.$('.o_form_uri.o_field_widget[name=reference]').text(), 'xphone',
|
||||
'the field reference of the form should have the right value');
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ QUnit.module('special_fields', {
|
||||
'</form>',
|
||||
});
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('select').length, 1, "should have the select field");
|
||||
assert.containsOnce(form, 'select[name=tz]');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ var testUtilsMock = require('web.test_utils_mock');
|
||||
var testUtilsModal = require('web.test_utils_modal');
|
||||
var testUtilsPivot = require('web.test_utils_pivot');
|
||||
var tools = require('web.tools');
|
||||
|
||||
function deprecated(fn, type) {
|
||||
var msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
|
||||
`Please use 'testUtils.${type}.${fn.name}' instead.`;
|
||||
return tools.deprecated(fn, msg);
|
||||
};
|
||||
}
|
||||
|
||||
// Loading static files cannot be properly simulated when their real content is
|
||||
// really needed. This is the case for static XML files so we load them here,
|
||||
@@ -108,6 +109,7 @@ return $.when(
|
||||
createActionManager: testUtilsCreate.createActionManager,
|
||||
createDebugManager: testUtilsCreate.createDebugManager,
|
||||
createAsyncView: testUtilsCreate.createAsyncView,
|
||||
createControlPanel: testUtilsCreate.createControlPanel,
|
||||
createView: testUtilsCreate.createView,
|
||||
createModel: testUtilsCreate.createModel,
|
||||
createParent: testUtilsCreate.createParent,
|
||||
|
||||
@@ -12,9 +12,9 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
|
||||
var ActionManager = require('web.ActionManager');
|
||||
var config = require('web.config');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
var dom = require('web.dom');
|
||||
var ControlPanelView = require('web.ControlPanelView');
|
||||
var DebugManager = require('web.DebugManager');
|
||||
var dom = require('web.dom');
|
||||
var testUtilsMock = require('web.test_utils_mock');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -70,8 +70,13 @@ function createActionManager (params) {
|
||||
actionManager.destroy = originalDestroy;
|
||||
widget.destroy();
|
||||
};
|
||||
actionManager.appendTo(widget.$el);
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
actionManager.appendTo(fragment).then(function () {
|
||||
dom.append(widget.$el, fragment, {
|
||||
callbacks: [{ widget: actionManager }],
|
||||
in_DOM: true,
|
||||
});
|
||||
});
|
||||
return actionManager;
|
||||
}
|
||||
|
||||
@@ -113,32 +118,46 @@ function createAsyncView(params) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
// reproduce the DOM environment of views
|
||||
var $webClient = $('<div>').addClass('o_web_client').prependTo($target);
|
||||
var $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
|
||||
|
||||
|
||||
// add mock environment: mock server, session, fieldviewget, ...
|
||||
var mockServer = testUtilsMock.addMockEnvironment(widget, params);
|
||||
var viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
|
||||
|
||||
// create the view
|
||||
var viewOptions = {
|
||||
var View = params.View;
|
||||
var viewOptions = _.defaults({}, params.viewOptions, {
|
||||
modelName: params.model || 'foo',
|
||||
ids: 'res_id' in params ? [params.res_id] : undefined,
|
||||
currentId: 'res_id' in params ? params.res_id : undefined,
|
||||
domain: params.domain || [],
|
||||
context: params.context || {},
|
||||
groupBy: params.groupBy || [],
|
||||
};
|
||||
});
|
||||
// patch the View to handle the groupBy given in params, as we can't give it
|
||||
// in init (unlike the domain and context which can be set in the action)
|
||||
testUtilsMock.patch(View, {
|
||||
_updateMVCParams: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
|
||||
},
|
||||
});
|
||||
if (params.hasSelectors) {
|
||||
viewOptions.hasSelectors = params.hasSelectors;
|
||||
}
|
||||
|
||||
_.extend(viewOptions, params.viewOptions);
|
||||
|
||||
var view = new params.View(viewInfo, viewOptions);
|
||||
|
||||
// reproduce the DOM environment of views
|
||||
var $web_client = $('<div>').addClass('o_web_client').prependTo($target);
|
||||
var controlPanel = new ControlPanel(widget);
|
||||
controlPanel.appendTo($web_client);
|
||||
var $content = $('<div>').addClass('o_content').appendTo($web_client);
|
||||
var view;
|
||||
if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
|
||||
// TODO: probably needs to create an helper just for that
|
||||
view = new params.View({
|
||||
viewInfo: viewInfo,
|
||||
modelName: params.model || 'foo',
|
||||
});
|
||||
} else {
|
||||
view = new params.View(viewInfo, viewOptions);
|
||||
}
|
||||
|
||||
if (params.interceptsPropagate) {
|
||||
_.each(params.interceptsPropagate, function (cb, name) {
|
||||
@@ -155,16 +174,15 @@ function createAsyncView(params) {
|
||||
// when it will be called the second time (by its parent)
|
||||
delete view.destroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
// link the view to the control panel
|
||||
view.set_cp_bus(controlPanel.get_bus());
|
||||
testUtilsMock.unpatch(View);
|
||||
};
|
||||
|
||||
// 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($content, fragment, {
|
||||
dom.prepend($actionManager, fragment, {
|
||||
callbacks: [{ widget: view }],
|
||||
in_DOM: true,
|
||||
});
|
||||
@@ -176,7 +194,95 @@ function createAsyncView(params) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* create a controlPanel from various parameters.
|
||||
*
|
||||
* It returns an instance of ControlPanelController
|
||||
*
|
||||
* @param {Object} [params={}]
|
||||
* @param {Object} [params.action={}]
|
||||
* @param {Object} [params.context={}]
|
||||
* @param {Object} [params.debug=false] if true, the widget will be appended in
|
||||
* the DOM. Also, RPCs and uncaught OdooEvent will be logged
|
||||
* @param {string} [params.domain=[]]
|
||||
* @param {integer} [params.fieldDebounce=0] the debounce value to use for the
|
||||
* duration of the test.
|
||||
* @param {Object} params.intercepts an object with event names as key, and
|
||||
* callback as value. Each key,value will be used to intercept the event.
|
||||
* Note that this is particularly useful if you want to intercept events going
|
||||
* up in the init process of the view, because there are no other way to do it
|
||||
* after this method returns
|
||||
* @param {string} [params.modelName]
|
||||
* @param {string[]} [params.searchMenuTypes = ['filter', 'groupBy', 'favorite']]
|
||||
* determines search menus displayed.
|
||||
* @param {string} [params.template] the QWeb template to render
|
||||
* @param {Object} [params.viewInfo={arch: '<controlpanel/>', fields: {}}]
|
||||
a controlpanel (or search) fieldsview
|
||||
* @param {string} [params.viewInfo.arch]
|
||||
* @param {boolean} [params.context.no_breadcrumbs=false] if set to true,
|
||||
* breadcrumbs won't be rendered
|
||||
* @param {boolean} [params.withBreadcrumbs=true] if set to false,
|
||||
* breadcrumbs won't be rendered
|
||||
* @param {boolean} [params.withSearchBar=true] if set to false, no default
|
||||
* search bar will be rendered
|
||||
*
|
||||
* @returns {Deferred<ControlPanel>} resolves with an instance of the ControlPanelController
|
||||
*/
|
||||
function createControlPanel(params) {
|
||||
params = params || {};
|
||||
var $target = $('#qunit-fixture');
|
||||
if (params.debug) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
// reproduce the DOM environment of a view control panel
|
||||
var $webClient = $('<div>').addClass('o_web_client').prependTo($target);
|
||||
var $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
|
||||
var $action = $('<div>').addClass('o_action').appendTo($actionManager);
|
||||
|
||||
var widget = new Widget();
|
||||
// add mock environment: mock server, session, fieldviewget, ...
|
||||
var mockServer = testUtilsMock.addMockEnvironment(widget, params);
|
||||
if (!params.viewInfo) {
|
||||
try {
|
||||
params.viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
|
||||
} catch (e) {
|
||||
// if an error occurs we keep params.viewInfo undefined.
|
||||
// It will be set to {arch: '<controlpanel/>', fields: {}} in
|
||||
// ControlPanelView init function.
|
||||
}
|
||||
}
|
||||
|
||||
var viewOptions = _.defaults({}, params.viewOptions, {
|
||||
modelName: params.model,
|
||||
searchMenuTypes: params.searchMenuTypes,
|
||||
viewInfo: params.viewInfo,
|
||||
});
|
||||
var controlPanelView = new ControlPanelView(viewOptions);
|
||||
return controlPanelView.getController(widget).then(function (controlPanel) {
|
||||
// override the controlPanel's 'destroy' so that it calls 'destroy' on
|
||||
// the widget instead, as the widget is the parent of the controlPanel
|
||||
// and the mockServer.
|
||||
controlPanel.__destroy = controlPanel.destroy;
|
||||
controlPanel.destroy = function () {
|
||||
// remove the override to properly destroy the controlPanel and its
|
||||
// children when it will be called the second time (by its parent)
|
||||
delete controlPanel.destroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
// render the controlPanel in a fragment as it must be able to render
|
||||
// correctly without being in the DOM
|
||||
var fragment = document.createDocumentFragment();
|
||||
return controlPanel.appendTo(fragment).then(function () {
|
||||
dom.prepend($action, fragment, {
|
||||
callbacks: [{ widget: controlPanel }],
|
||||
in_DOM: true,
|
||||
});
|
||||
return controlPanel;
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create and return an instance of DebugManager with all rpcs going through a
|
||||
* mock method, assuming that the user has access rights, and is an admin.
|
||||
@@ -279,6 +385,7 @@ function createView(params) {
|
||||
return {
|
||||
createActionManager: createActionManager,
|
||||
createAsyncView: createAsyncView,
|
||||
createControlPanel: createControlPanel,
|
||||
createDebugManager: createDebugManager,
|
||||
createModel: createModel,
|
||||
createParent: createParent,
|
||||
|
||||
@@ -109,7 +109,7 @@ QUnit.module('DebugManager', {}, function () {
|
||||
'ir.default': { // We just need this to be defined
|
||||
fields: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var form = testUtils.createView({
|
||||
View: FormView,
|
||||
@@ -138,6 +138,7 @@ QUnit.module('DebugManager', {}, function () {
|
||||
|
||||
// Simulate update debug manager from web client
|
||||
var action = {
|
||||
controlPanelFieldsView: {},
|
||||
views: [{
|
||||
fieldsView: {
|
||||
view_id: 1,
|
||||
|
||||
@@ -3,9 +3,9 @@ odoo.define('web.abstract_view_tests', function (require) {
|
||||
|
||||
var AbstractView = require('web.AbstractView');
|
||||
var ajax = require('web.ajax');
|
||||
var ListView = require('web.ListView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
var createAsyncView = testUtils.createAsyncView;
|
||||
|
||||
QUnit.module('Views', {
|
||||
@@ -112,22 +112,36 @@ QUnit.module('Views', {
|
||||
defs.c.resolve();
|
||||
});
|
||||
|
||||
QUnit.test('groupBy attribute can be a string, instead of a list of strings', function (assert) {
|
||||
assert.expect(2);
|
||||
QUnit.test('group_by from context can be a string, instead of a list of strings', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var list = testUtils.createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
var actionManager = createActionManager({
|
||||
actions: [{
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
res_model: 'foo',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
context: {
|
||||
group_by: 'bar',
|
||||
},
|
||||
}],
|
||||
archs: {
|
||||
'foo,false,list': '<tree><field name="foo"/><field name="bar"/></tree>',
|
||||
'foo,false,search': '<search></search>',
|
||||
},
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
|
||||
groupBy: 'bar',
|
||||
mockRPC: function (route, args) {
|
||||
assert.strictEqual(args.method, 'read_group');
|
||||
assert.deepEqual(args.kwargs.groupby, ['bar']);
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'read_group') {
|
||||
assert.deepEqual(args.kwargs.groupby, ['bar']);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
list.destroy();
|
||||
|
||||
actionManager.doAction(1);
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -253,17 +253,17 @@ QUnit.module('Views', {
|
||||
actionManager.doAction(1);
|
||||
|
||||
// displays month mode by default
|
||||
assert.strictEqual(actionManager.controlPanel.$('.breadcrumb-item').text(),
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(),
|
||||
'Meetings Test (Dec 11 – 17, 2016)', "should display the current week");
|
||||
|
||||
// switch to day mode
|
||||
testUtils.dom.click(actionManager.controlPanel.$('.o_calendar_button_day'));
|
||||
assert.strictEqual(actionManager.controlPanel.$('.breadcrumb-item').text(),
|
||||
testUtils.dom.click($('.o_control_panel .o_calendar_button_day'));
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(),
|
||||
'Meetings Test (December 12, 2016)', "should display the current day");
|
||||
|
||||
// switch to month mode
|
||||
testUtils.dom.click(actionManager.controlPanel.$('.o_calendar_button_month'));
|
||||
assert.strictEqual(actionManager.controlPanel.$('.breadcrumb-item').text(),
|
||||
testUtils.dom.click($('.o_control_panel .o_calendar_button_month'));
|
||||
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(),
|
||||
'Meetings Test (December 2016)', "should display the current month");
|
||||
|
||||
actionManager.destroy();
|
||||
@@ -2058,7 +2058,9 @@ QUnit.module('Views', {
|
||||
// call destroy function of controller to ensure that it correctly destroys everything
|
||||
calendar.__destroy();
|
||||
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent");
|
||||
// + 1 (parent)
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 1,
|
||||
"every widget must be destroyed exept the parent");
|
||||
|
||||
calendar.destroy();
|
||||
|
||||
@@ -2116,8 +2118,8 @@ QUnit.module('Views', {
|
||||
testUtils.mock.unpatch(Dialog);
|
||||
});
|
||||
|
||||
QUnit.test('calendar is configured to hide the groupby menu', function (assert) {
|
||||
assert.expect(2);
|
||||
QUnit.test('calendar is configured to have no groupBy menu', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var archs = {
|
||||
'event,1,calendar': '<calendar class="o_calendar_test" '+
|
||||
@@ -2144,9 +2146,8 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
var $groupBy = actionManager.controlPanel.$('span.fa.fa-bars');
|
||||
assert.strictEqual($groupBy.length, 1, 'just making sure we have the groupby menu');
|
||||
assert.ok(!$groupBy.is(':visible'), 'groupby menu should not be visible');
|
||||
assert.containsNone(actionManager.$('.o_control_panel .o_search_options span.fa.fa-bars'),
|
||||
"the control panel has no groupBy menu");
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
odoo.define('web.control_panel_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ControlPanelView = require('web.ControlPanelView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createControlPanel = testUtils.createControlPanel;
|
||||
|
||||
function createControlPanelFactory(arch, fields, params) {
|
||||
params = params || {};
|
||||
arch = arch || "<search></search>";
|
||||
fields = fields || {};
|
||||
var viewInfo = {arch: arch, fields: fields};
|
||||
var controlPanelFactory = new ControlPanelView({viewInfo: viewInfo, context: {}});
|
||||
return controlPanelFactory;
|
||||
}
|
||||
|
||||
QUnit.module('Views', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: 'char' },
|
||||
foo: {string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true},
|
||||
date_field: {string: "Date", type: "date", store: true, sortable: true},
|
||||
float_field: {string: "Float", type: "float"},
|
||||
bar: {string: "Bar", type: "many2one", relation: 'partner'},
|
||||
},
|
||||
records: [],
|
||||
onchanges: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.module('ControlPanelView');
|
||||
|
||||
QUnit.module('Control Panel Arch Parsing');
|
||||
|
||||
QUnit.test('empty arch', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var controlPanelFactory = createControlPanelFactory();
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[],
|
||||
"there should be no group at all"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse one field tag', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<field name=\"bar\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[[{
|
||||
attrs: {
|
||||
name: "bar",
|
||||
string: "Bar"
|
||||
},
|
||||
autoCompleteValues: [],
|
||||
description: "bar",
|
||||
isDefault: false,
|
||||
type: "field"
|
||||
}]],
|
||||
"there should be one group with one field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse one separator tag', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<separator/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[],
|
||||
"there should be no group at all");
|
||||
});
|
||||
|
||||
QUnit.test('parse one separator tag and one field tag', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<separator/>" +
|
||||
"<field name=\"bar\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[[
|
||||
{
|
||||
attrs: {
|
||||
name: "bar",
|
||||
string: "Bar"
|
||||
},
|
||||
autoCompleteValues: [],
|
||||
description: "bar",
|
||||
isDefault: false,
|
||||
type: "field"
|
||||
}
|
||||
]],
|
||||
"there should be one group with one field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse one filter tag', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<filter name=\"filter\" string=\"Hello\" " +
|
||||
"domain=\"[]\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[[{
|
||||
description: "Hello",
|
||||
groupNumber: 2,
|
||||
domain: "[]",
|
||||
isDefault: false,
|
||||
type: "filter"
|
||||
}]],
|
||||
"there should be one group with one filter"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse one groupBy tag', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<groupBy name=\"groupby\" string=\"Hi\" " +
|
||||
"context=\"{\'group_by\': \'date_field:day\'}\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[[
|
||||
{
|
||||
currentOptionId: false,
|
||||
defaultOptionId: "day",
|
||||
description: "Hi",
|
||||
fieldName: "date_field",
|
||||
fieldType: "date",
|
||||
groupNumber: 2,
|
||||
hasOptions: true,
|
||||
isDefault: false,
|
||||
options: [
|
||||
{
|
||||
description: "Day",
|
||||
groupId: 1,
|
||||
optionId: "day"
|
||||
},
|
||||
{
|
||||
description: "Week",
|
||||
groupId: 1,
|
||||
optionId: "week"
|
||||
},
|
||||
{
|
||||
description: "Month",
|
||||
groupId: 1,
|
||||
optionId: "month"
|
||||
},
|
||||
{
|
||||
description: "Quarter",
|
||||
groupId: 1,
|
||||
optionId: "quarter"
|
||||
},
|
||||
{
|
||||
description: "Year",
|
||||
groupId: 1,
|
||||
optionId: "year"
|
||||
}
|
||||
],
|
||||
type: "groupBy"
|
||||
}
|
||||
]],
|
||||
"there should be one group with one groupBy with options"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse two filter tags', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<filter name=\"filter_1\" string=\"Hello One\" " +
|
||||
"domain=\"[]\"/>" +
|
||||
"<filter name=\"filter_2\" string=\"Hello Two\" " +
|
||||
"domain=\"[(\'bar\', \'=\', 3)]\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[[
|
||||
{
|
||||
"description": "Hello One",
|
||||
"domain": "[]",
|
||||
"groupNumber": 2,
|
||||
"isDefault": false,
|
||||
"type": "filter"
|
||||
},
|
||||
{
|
||||
"description": "Hello Two",
|
||||
"domain": "[('bar', '=', 3)]",
|
||||
"groupNumber": 2,
|
||||
"isDefault": false,
|
||||
"type": "filter"
|
||||
}
|
||||
]],
|
||||
'there should be one group of two filters'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse two filter tags separated by a separator', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<filter name=\"filter_1\" string=\"Hello One\" " +
|
||||
"domain=\"[]\"/>" +
|
||||
"<separator/>" +
|
||||
"<filter name=\"filter_2\" string=\"Hello Two\" " +
|
||||
"domain=\"[(\'bar\', \'=\', 3)]\"/>" +
|
||||
"</search>";
|
||||
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[
|
||||
[
|
||||
{
|
||||
description: "Hello One",
|
||||
domain: "[]",
|
||||
groupNumber: 2,
|
||||
isDefault: false,
|
||||
type: "filter"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
description: "Hello Two",
|
||||
domain: "[('bar', '=', 3)]",
|
||||
groupNumber: 4,
|
||||
isDefault: false,
|
||||
type: "filter"
|
||||
}
|
||||
]
|
||||
],
|
||||
"there should be two groups of one filter"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse one filter tag and one field', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<filter name=\"filter\" string=\"Hello\" domain=\"[]\"/>" +
|
||||
"<field name=\"bar\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[
|
||||
[
|
||||
{
|
||||
description: "Hello",
|
||||
domain: "[]",
|
||||
groupNumber: 2,
|
||||
isDefault: false,
|
||||
type: "filter"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
attrs: {
|
||||
name: "bar",
|
||||
string: "Bar"
|
||||
},
|
||||
autoCompleteValues: [],
|
||||
description: "bar",
|
||||
isDefault: false,
|
||||
type: "field"
|
||||
}
|
||||
]
|
||||
],
|
||||
"there should be one group with a filter and one group with a field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('parse two field tags', function (assert) {
|
||||
assert.expect(1);
|
||||
var arch = "<search>" +
|
||||
"<field name=\"foo\"/>" +
|
||||
"<field name=\"bar\"/>" +
|
||||
"</search>";
|
||||
var fields = this.data.partner.fields;
|
||||
var controlPanelFactory = createControlPanelFactory(arch, fields);
|
||||
assert.deepEqual(
|
||||
controlPanelFactory.loadParams.groups,
|
||||
[
|
||||
[
|
||||
{
|
||||
attrs: {
|
||||
name: "foo",
|
||||
string: "Foo"
|
||||
},
|
||||
autoCompleteValues: [],
|
||||
description: "foo",
|
||||
isDefault: false,
|
||||
type: "field"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
attrs: {
|
||||
name: "bar",
|
||||
string: "Bar"
|
||||
},
|
||||
autoCompleteValues: [],
|
||||
description: "bar",
|
||||
isDefault: false,
|
||||
type: "field"
|
||||
}
|
||||
]
|
||||
],
|
||||
"there should be two groups of a single field"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -159,8 +159,7 @@ QUnit.module('Views', {
|
||||
assert.containsOnce(form, 'label:contains(f3_description)');
|
||||
assert.containsOnce(form, 'div.o_field_one2many table');
|
||||
assert.containsOnce(form, 'tbody td:not(.o_list_record_selector) .custom-checkbox input:checked');
|
||||
assert.strictEqual(form.get('title'), 'second record',
|
||||
"title should be display_name of record");
|
||||
assert.containsOnce(form, '.o_control_panel .breadcrumb:contains(second record)');
|
||||
assert.containsNone(form, 'label.o_form_label_empty:contains(timmy)');
|
||||
|
||||
form.destroy();
|
||||
@@ -1510,8 +1509,8 @@ QUnit.module('Views', {
|
||||
testUtils.form.clickCreate(form);
|
||||
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
|
||||
|
||||
assert.strictEqual(form.$('input:first').val(), "My little Foo Value",
|
||||
"should have correct default_get value");
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), "My little Foo Value",
|
||||
"should have correct default_get value");
|
||||
testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
|
||||
assert.strictEqual(this.data.partner.records.length, n + 1, "should have created a record");
|
||||
@@ -1668,9 +1667,9 @@ QUnit.module('Views', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
}).then(function (form) {
|
||||
assert.ok(form.$('input').val(), 'aaa',
|
||||
'default value should be correctly displayed');
|
||||
assert.strictEqual(nameGetCount, 1, 'should have done one name_get');
|
||||
assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa',
|
||||
"default value should be correctly displayed");
|
||||
assert.strictEqual(nameGetCount, 1, "should have done one name_get");
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
@@ -1689,11 +1688,13 @@ QUnit.module('Views', {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.get('title'), 'first record',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
|
||||
"should have the display name of the record as title");
|
||||
|
||||
testUtils.form.clickCreate(form);
|
||||
assert.strictEqual(form.get('title'), _t("New"),
|
||||
"should have the display name of the record as title");
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), _t("New"),
|
||||
"should have the display name of the record as title");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -1717,13 +1718,13 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.get('title'), 'first record',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
|
||||
"should have the display name of the record as title");
|
||||
|
||||
testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
testUtils.dom.click(form.sidebar.$('a:contains(Duplicate)'));
|
||||
|
||||
assert.strictEqual(form.get('title'), 'first record (copy)',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record (copy)',
|
||||
"should have duplicated the record");
|
||||
|
||||
assert.strictEqual(form.mode, "edit", 'should be in edit mode');
|
||||
@@ -1782,36 +1783,13 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.get('title'), 'first record',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
|
||||
"should have the display name of the record as title");
|
||||
assert.containsNone(form.sidebar, 'a:contains(Duplicate)',
|
||||
"should not contains a 'Duplicate' action");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('buttons in footer are moved to $buttons if necessary', function (assert) {
|
||||
// not sure about this test...
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo"/>' +
|
||||
'<footer>' +
|
||||
'<button string="Create" type="object" class="infooter"/>' +
|
||||
'</footer>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {footerToButtons: true},
|
||||
});
|
||||
|
||||
assert.containsOnce(form.$buttons, 'button.infooter', "footer button should be in footer");
|
||||
assert.containsNone(form, 'button.infooter', "footer button should not be in form");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('clicking on stat buttons in edit mode', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
@@ -1891,13 +1869,13 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.getTitle(), 'second record',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'second record',
|
||||
"should have correct display_name");
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input[name=name]'), 'some other name');
|
||||
|
||||
testUtils.dom.click('.oe_stat_button');
|
||||
assert.strictEqual(form.getTitle(), 'GOLDORAK',
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'GOLDORAK',
|
||||
"should have correct display_name");
|
||||
|
||||
form.destroy();
|
||||
@@ -1916,7 +1894,7 @@ QUnit.module('Views', {
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo"/>' +
|
||||
'<button string="Do something" class="btn-primary" name="abc" type="object"/>' +
|
||||
'<button string="Discard" class="btn-secondary" special="cancel"/>' +
|
||||
'<button string="Or discard" class="btn-secondary" special="cancel"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
@@ -1935,13 +1913,13 @@ QUnit.module('Views', {
|
||||
// make the record dirty
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
|
||||
|
||||
testUtils.dom.clickFirst(form.$('button'));
|
||||
testUtils.dom.click(form.$('button:contains(Do something)'));
|
||||
assert.strictEqual(writeCount, 1, "should have triggered a write");
|
||||
assert.strictEqual(executeActionCount, 1, "should have triggered a execute action");
|
||||
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'abcdef');
|
||||
|
||||
testUtils.dom.click(form.$('button').eq(1));
|
||||
testUtils.dom.click(form.$('button:contains(Or discard)'));
|
||||
assert.strictEqual(writeCount, 1, "should not have triggered a write");
|
||||
assert.strictEqual(executeActionCount, 2, "should have triggered a execute action");
|
||||
|
||||
@@ -1975,7 +1953,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
|
||||
testUtils.dom.click(form.$('button'));
|
||||
testUtils.dom.click(form.$('.o_form_view button'));
|
||||
assert.verifySteps(['read', 'write', 'read', 'execute_action']);
|
||||
|
||||
form.destroy();
|
||||
@@ -2124,14 +2102,16 @@ QUnit.module('Views', {
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'yop',
|
||||
"input should contain yop");
|
||||
|
||||
// click on discard
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.containsNone(document.body, '.modal', 'no confirm modal should be displayed');
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), 'yop', 'field in readonly should display yop');
|
||||
assert.containsNone(document.body, '.modal', "no confirm modal should be displayed");
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), 'yop',
|
||||
"field in readonly should display yop");
|
||||
|
||||
assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
|
||||
assert.strictEqual(nbWrite, 0, "no write RPC should have been done");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2155,23 +2135,26 @@ QUnit.module('Views', {
|
||||
|
||||
// switch to edit mode and edit the foo field
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should contain new value");
|
||||
|
||||
// click on discard and cancel the confirm request
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed");
|
||||
testUtils.dom.click('.modal-footer .btn-secondary');
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should still contain new value');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should still contain new value");
|
||||
|
||||
// click on discard and confirm
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed");
|
||||
testUtils.dom.click('.modal-footer .btn-primary');
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), 'yop', 'field in readonly should display yop');
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), 'yop',
|
||||
"field in readonly should display yop");
|
||||
|
||||
assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
|
||||
assert.strictEqual(nbWrite, 0, "no write RPC should have been done");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2240,7 +2223,8 @@ QUnit.module('Views', {
|
||||
testUtils.fields.many2one.clickOpenDropdown('product_id');
|
||||
testUtils.fields.many2one.clickHighlightedItem('product_id');
|
||||
|
||||
assert.strictEqual(form.$('input').val(), 'xphone', 'input should contain xphone');
|
||||
assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone',
|
||||
"input should contain xphone");
|
||||
|
||||
// click on discard and confirm
|
||||
testUtils.form.clickDiscard(form);
|
||||
@@ -2269,11 +2253,13 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
// edit the foo field
|
||||
assert.strictEqual(form.$('input').val(), 'ABC', 'input should contain ABC');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'ABC',
|
||||
"input should contain ABC");
|
||||
|
||||
testUtils.form.clickDiscard(form);
|
||||
|
||||
assert.containsNone(document.body, '.modal', 'there should not be a confirm modal');
|
||||
assert.containsNone(document.body, '.modal',
|
||||
"there should not be a confirm modal");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2301,22 +2287,27 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
// edit the foo field
|
||||
assert.strictEqual(form.$('input').val(), 'ABC', 'input should contain ABC');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'ABC',
|
||||
"input should contain ABC");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'DEF');
|
||||
|
||||
// discard the changes and check it has properly been discarded
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.containsOnce(document.body, '.modal', 'there should be a confirm modal');
|
||||
assert.strictEqual(form.$('input').val(), 'DEF', 'input should be DEF');
|
||||
assert.containsOnce(document.body, '.modal', "there should be a confirm modal");
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'DEF',
|
||||
"input should be DEF");
|
||||
testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm
|
||||
assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'ABC',
|
||||
"input should now be ABC");
|
||||
|
||||
// redirty and discard the field foo (to make sure initial changes haven't been lost)
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'GHI');
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.strictEqual(form.$('input').val(), 'GHI', 'input should be GHI');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'GHI',
|
||||
"input should be GHI");
|
||||
testUtils.dom.click('.modal-footer .btn-primary') // click on confirm
|
||||
assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'ABC',
|
||||
"input should now be ABC");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2345,11 +2336,12 @@ QUnit.module('Views', {
|
||||
testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)'));
|
||||
testUtils.dom.click(form.sidebar.$('a:contains(Duplicate)'));
|
||||
|
||||
assert.strictEqual(form.$('input').val(), 'tralala', 'input should contain ABC');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'tralala',
|
||||
"input should contain ABC");
|
||||
|
||||
testUtils.form.clickDiscard(form);
|
||||
|
||||
assert.containsNone(document.body, '.modal', 'there should not be a confirm modal');
|
||||
assert.containsNone(document.body, '.modal', "there should not be a confirm modal");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -2376,30 +2368,32 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
|
||||
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2", 'pager limit should be 2');
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '1', "pager value should be 1");
|
||||
assert.strictEqual(form.$('.o_pager_limit').text(), '2', "pager limit should be 2");
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
|
||||
|
||||
// edit the foo field
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should contain new value");
|
||||
|
||||
// click on the pager to switch to the next record and cancel the confirm request
|
||||
testUtils.dom.click('.o_pager_next'); // click on next
|
||||
assert.containsOnce(document.body, '.modal', 'a confirm modal should be displayed');
|
||||
assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed");
|
||||
testUtils.dom.click('.modal-footer .btn-secondary'); // click on cancel
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should still contain new value');
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should still be 1');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should still contain new value");
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '1', "pager value should still be 1");
|
||||
|
||||
// click on the pager to switch to the next record and confirm
|
||||
testUtils.dom.click('.o_pager_next'); // click on next
|
||||
assert.containsOnce(document.body, '.modal', 'a confirm modal should be displayed');
|
||||
assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed");
|
||||
testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm
|
||||
assert.strictEqual(form.$('input').val(), 'blip', 'input should contain blip');
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "input should contain blip");
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '2', "pager value should be 2");
|
||||
|
||||
assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
|
||||
form.destroy();
|
||||
@@ -2422,22 +2416,24 @@ QUnit.module('Views', {
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '1', "pager value should be 1");
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
|
||||
|
||||
// edit the foo field
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should contain new value");
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
|
||||
// click on the pager to switch to the next record and cancel the confirm request
|
||||
testUtils.dom.click('.o_pager_next'); // click on next
|
||||
assert.containsNone(document.body, '.modal:visible', 'no confirm modal should be displayed');
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
|
||||
assert.containsNone(document.body, '.modal:visible',
|
||||
"no confirm modal should be displayed");
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '2', "pager value should be 2");
|
||||
|
||||
assert.containsN(form, '.o_priority .fa-star-o', 2,
|
||||
'priority widget should have been rendered with correct value');
|
||||
@@ -2448,21 +2444,23 @@ QUnit.module('Views', {
|
||||
'priority widget should have been updated');
|
||||
|
||||
testUtils.dom.click('.o_pager_next'); // click on next
|
||||
assert.containsNone(document.body, '.modal:visible', 'no confirm modal should be displayed');
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
|
||||
assert.containsNone(document.body, '.modal:visible',
|
||||
"no confirm modal should be displayed");
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '1', "pager value should be 1");
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain yop');
|
||||
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
|
||||
"input should contain yop");
|
||||
|
||||
// edit the foo field
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'wrong value');
|
||||
|
||||
testUtils.form.clickDiscard(form);
|
||||
assert.containsOnce(document.body, '.modal', 'a confirm modal should be displayed');
|
||||
assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed");
|
||||
testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm
|
||||
testUtils.dom.click('.o_pager_next'); // click on next
|
||||
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
|
||||
assert.strictEqual(form.$('.o_pager_value').text(), '2', "pager value should be 2");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2763,15 +2761,13 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
testUtils.form.clickSave(form);
|
||||
assert.hasClass(form.$('label'),'o_field_invalid',
|
||||
"label should be tagged as invalid");
|
||||
assert.hasClass(form.$('input'),'o_field_invalid',
|
||||
"input should be tagged as invalid");
|
||||
assert.hasClass(form.$('label.o_form_label'),'o_field_invalid');
|
||||
assert.hasClass(form.$('input[name=foo]'),'o_field_invalid');
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), "tralala");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
|
||||
|
||||
assert.containsNone(form, '.o_field_invalid');
|
||||
|
||||
assert.containsNone(form, '.o_field_invalid',
|
||||
"nothing should be marked as invalid");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -2867,13 +2863,11 @@ QUnit.module('Views', {
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '9',
|
||||
"'int_field' value should be 9 before the change");
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '9');
|
||||
|
||||
testUtils.fields.editInput(form.$('input').first(), "tralala");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
|
||||
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '10',
|
||||
"the onchange should have been correctly applied");
|
||||
assert.strictEqual(form.$('input[name=int_field]').val(), '10');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3103,7 +3097,7 @@ QUnit.module('Views', {
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input'), 'let us trigger an onchange');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange');
|
||||
var $o2m = form.$('.o_field_one2many');
|
||||
assert.strictEqual($o2m.find('.o_data_row').length, 2,
|
||||
"there should be two linked record");
|
||||
@@ -3149,7 +3143,7 @@ QUnit.module('Views', {
|
||||
"should have proper initial content");
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), "tralala");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
|
||||
|
||||
assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "goldNo records",
|
||||
"should have proper initial content");
|
||||
@@ -3177,7 +3171,7 @@ QUnit.module('Views', {
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_data_cell').first());
|
||||
testUtils.fields.editInput(form.$('input'), '');
|
||||
testUtils.fields.editInput(form.$('input[name=display_name]'), '');
|
||||
testUtils.dom.click(form.$('.fa-trash-o').eq(1));
|
||||
|
||||
assert.strictEqual($('.modal').find('.modal-title').first().text(), "Warning",
|
||||
@@ -3237,7 +3231,7 @@ QUnit.module('Views', {
|
||||
|
||||
// switch to edit mode
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input'), 'let us trigger an onchange');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange');
|
||||
var $m2m = form.$('.o_field_many2many');
|
||||
assert.strictEqual($m2m.find('.o_data_row').length, 2,
|
||||
"there should be two linked records");
|
||||
@@ -3426,24 +3420,22 @@ QUnit.module('Views', {
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
testUtils.fields.editInput(form.$('input').first(), "1");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '1');
|
||||
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
|
||||
testUtils.fields.editInput(form.$('input').first(), "12");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '12');
|
||||
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
|
||||
|
||||
return waitForFinishedOnChange().then(function () {
|
||||
assert.strictEqual(onchangeNbr, 1, "one onchange has been called");
|
||||
|
||||
// add something in the input, then focus another input
|
||||
testUtils.fields.editInput(form.$('input').first(), "123");
|
||||
form.$('input').first().change();
|
||||
assert.strictEqual(onchangeNbr, 2,
|
||||
"one onchange has been called immediately");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '123');
|
||||
form.$('input[name=foo]').change();
|
||||
assert.strictEqual(onchangeNbr, 2, "one onchange has been called immediately");
|
||||
|
||||
return waitForFinishedOnChange();
|
||||
}).then(function () {
|
||||
assert.strictEqual(onchangeNbr, 2,
|
||||
"no extra onchange should have been called");
|
||||
assert.strictEqual(onchangeNbr, 2, "no extra onchange should have been called");
|
||||
|
||||
form.destroy();
|
||||
done();
|
||||
@@ -3558,7 +3550,7 @@ QUnit.module('Views', {
|
||||
testUtils.form.clickEdit(form);
|
||||
|
||||
// edit a value
|
||||
testUtils.fields.editInput(form.$('input').first(), "1234");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '1234');
|
||||
|
||||
// discard changes
|
||||
testUtils.form.clickDiscard(form);
|
||||
@@ -3599,7 +3591,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), "1234");
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), '1234');
|
||||
|
||||
// save the value and discard directly
|
||||
testUtils.form.clickSave(form);
|
||||
@@ -3657,7 +3649,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input:first'), 'trigger an onchange');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3718,7 +3710,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input:first'), 'trigger an onchange');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
|
||||
"onchange should have been correctly applied on field in o2m list");
|
||||
@@ -3778,7 +3770,7 @@ QUnit.module('Views', {
|
||||
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
|
||||
"the initial value should be the default one");
|
||||
|
||||
testUtils.fields.editInput(form.$('input:first'), 'trigger an onchange');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
|
||||
"onchange should have been correctly applied on field in o2m list");
|
||||
@@ -3927,7 +3919,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.fields.editInput(form.$('input:first'), 'coucou');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'coucou');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -4494,31 +4486,32 @@ QUnit.module('Views', {
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('footers are not duplicated on rerender', function (assert) {
|
||||
assert.expect(2);
|
||||
QUnit.test('buttons in footer are moved to $buttons if necessary', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="foo"/>' +
|
||||
'<footer>' +
|
||||
'<button>Hello</button>' +
|
||||
'</footer>' +
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo"/>' +
|
||||
'<footer>' +
|
||||
'<button string="Create" type="object" class="infooter"/>' +
|
||||
'</footer>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
footerToButtons: true,
|
||||
},
|
||||
viewOptions: {footerToButtons: true},
|
||||
});
|
||||
|
||||
assert.containsNone(form, 'footer',
|
||||
"footer should have been moved outside of the form view");
|
||||
form.reload();
|
||||
assert.containsNone(form, 'footer',
|
||||
"footer should still have been moved outside of the form view");
|
||||
assert.containsOnce(form.$('.o_control_panel'), 'button.infooter');
|
||||
assert.containsNone(form.$('.o_form_view'), 'button.infooter');
|
||||
|
||||
// check that this still works after a reload
|
||||
testUtils.form.reload(form);
|
||||
|
||||
assert.containsOnce(form.$('.o_control_panel'), 'button.infooter');
|
||||
assert.containsNone(form.$('.o_form_view'), 'button.infooter');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -4850,7 +4843,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('display toolbar', function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(8);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -4886,22 +4879,16 @@ QUnit.module('Views', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// The toolbar displayed changes if the module 'document' is installed.
|
||||
// So if the module is installed, the assertion checks for 3 dropdowns
|
||||
// if not only 2 dropdowns are displayed.
|
||||
var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn');
|
||||
var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[1].children;
|
||||
if ('attachment_indexation.document' in odoo.__DEBUG__.services) {
|
||||
assert.strictEqual($dropdowns.length, 3,
|
||||
"there should be 3 dropdowns (print, attachment, action) in the toolbar.");
|
||||
$actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[2].children;
|
||||
} else {
|
||||
assert.strictEqual($dropdowns.length, 2,
|
||||
"there should be 2 dropdowns (print, action) in the toolbar.");
|
||||
}
|
||||
assert.strictEqual($actions.length, 3,
|
||||
"there should be 3 actions");
|
||||
var $customAction = $('.o_web_client .o_control_panel .btn-group .dropdown-menu:last .dropdown-item:nth(2)');
|
||||
|
||||
var $printMenu = form.$('.o_cp_sidebar .o_dropdown:contains(Print)');
|
||||
assert.isNotVisible($printMenu);
|
||||
var $actionMenu = form.$('.o_cp_sidebar .o_dropdown:contains(Action)');
|
||||
assert.isVisible($actionMenu);
|
||||
|
||||
testUtils.dom.click($actionMenu.find('button')); // open Action menu
|
||||
assert.strictEqual($actionMenu.find('.dropdown-item').length, 3,
|
||||
'there should be 3 actions');
|
||||
var $customAction = $actionMenu.find('.dropdown-item:last');
|
||||
assert.strictEqual($customAction.text().trim(), 'Action partner',
|
||||
"the custom action should have 'Action partner' as name");
|
||||
testUtils.mock.intercept(form, 'do_action', function (event) {
|
||||
@@ -4911,7 +4898,6 @@ QUnit.module('Views', {
|
||||
assert.deepEqual(context.active_ids, [1],
|
||||
"the active_ids should be an array with 1 inside.");
|
||||
});
|
||||
testUtils.dom.click($dropdowns.last());
|
||||
testUtils.dom.click($customAction);
|
||||
|
||||
form.destroy();
|
||||
@@ -5020,7 +5006,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.dom.click(form.$('input:first'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -5217,7 +5203,7 @@ QUnit.module('Views', {
|
||||
// client-side sort on edit
|
||||
testUtils.form.clickEdit(form);
|
||||
testUtils.dom.click(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)'));
|
||||
testUtils.fields.editInput($('.modal input[name="foo"]'), 'zzz')
|
||||
testUtils.fields.editInput($('.modal input[name="foo"]'), 'zzz');
|
||||
testUtils.dom.click($('.modal-footer button:first'));
|
||||
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length,
|
||||
"record zzz should be first");
|
||||
@@ -6690,7 +6676,9 @@ QUnit.module('Views', {
|
||||
// call destroy function of controller to ensure that it correctly destroys everything
|
||||
form.__destroy();
|
||||
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent");
|
||||
// + 4 (parent)
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 1,
|
||||
"every widget must be destroyed exept the parent");
|
||||
|
||||
form.destroy();
|
||||
|
||||
@@ -6971,7 +6959,7 @@ QUnit.module('Views', {
|
||||
|
||||
// add a row and partially fill it
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('input:nth(0)').val('abc').trigger('input');
|
||||
form.$('input[name=display_name]').val('abc').trigger('input');
|
||||
|
||||
// click button which will trigger_up 'execute_action' (this will save)
|
||||
values = {
|
||||
@@ -7054,14 +7042,13 @@ QUnit.module('Views', {
|
||||
fieldDebounce: 5000,
|
||||
});
|
||||
|
||||
testUtils.fields.editInput(form.$('input'), 'some foo value');
|
||||
testUtils.fields.editInput(form.$('input[name=foo]'), 'some foo value');
|
||||
// manually save the record, to prevent the field widget to notify the model of its new
|
||||
// value before being requested to
|
||||
form.saveRecord();
|
||||
|
||||
assert.containsOnce(form, '.o_form_readonly',
|
||||
"form view should be in readonly");
|
||||
assert.strictEqual(form.$el.text().trim(), 'some foo value',
|
||||
assert.containsOnce(form, '.o_form_readonly', "form view should be in readonly");
|
||||
assert.strictEqual(form.$('.o_form_view').text().trim(), 'some foo value',
|
||||
"foo field should have correct value");
|
||||
assert.verifySteps(['default_get', 'create', 'read']);
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ QUnit.module('Views', {
|
||||
'<field name="bar"/>' +
|
||||
'</graph>',
|
||||
});
|
||||
assert.strictEqual(graph.$('label').text(), "Partners", "should have 'Partners as title'");
|
||||
assert.strictEqual(graph.$('.o_graph_renderer label').text(), "Partners",
|
||||
"should have 'Partners as title'");
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
@@ -353,7 +354,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('getContext correctly returns mode, measure, groupbys and interval mapping', function (assert) {
|
||||
QUnit.test('getOwnedQueryParams correctly returns mode, measure, groupbys and interval mapping', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
@@ -366,11 +367,13 @@ QUnit.module('Views', {
|
||||
'</graph>',
|
||||
});
|
||||
return concurrency.delay(0).then(function () {
|
||||
assert.deepEqual(graph.getContext(), {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: '__count__',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
assert.deepEqual(graph.getOwnedQueryParams(), {
|
||||
context: {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: '__count__',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
}
|
||||
}, "context should be correct");
|
||||
|
||||
testUtils.dom.click(graph.$buttons.find('.dropdown-toggle:contains(Measures)'));
|
||||
@@ -378,33 +381,39 @@ QUnit.module('Views', {
|
||||
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.deepEqual(graph.getContext(), {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
assert.deepEqual(graph.getOwnedQueryParams(), {
|
||||
context: {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
testUtils.dom.click(graph.$buttons.find('button[data-mode="line"]'));
|
||||
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.deepEqual(graph.getContext(), {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
assert.deepEqual(graph.getOwnedQueryParams(), {
|
||||
context: {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
testUtils.graph.reload(graph, {groupBy: ['product_id', 'color_id']}); // change groupbys
|
||||
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.deepEqual(graph.getContext(), {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id', 'color_id'],
|
||||
graph_intervalMapping: {},
|
||||
assert.deepEqual(graph.getOwnedQueryParams(), {
|
||||
context: {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id', 'color_id'],
|
||||
graph_intervalMapping: {},
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
graph.destroy();
|
||||
|
||||
@@ -155,21 +155,15 @@ QUnit.module('Views', {
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// check archive/restore all actions in kanban header's config dropdown
|
||||
assert.containsOnce(kanban, '.o_kanban_header:first .o_kanban_config .o_column_archive_records');
|
||||
assert.containsOnce(kanban, '.o_kanban_header:first .o_kanban_config .o_column_unarchive_records');
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// archive the records of the first column
|
||||
assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3);
|
||||
envIDs = [4];
|
||||
|
||||
testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last'));
|
||||
testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_archive_records'));
|
||||
@@ -181,6 +175,8 @@ QUnit.module('Views', {
|
||||
assert.ok($('.modal').length, 'a confirm modal should be displayed');
|
||||
testUtils.modal.clickButton('Ok');
|
||||
assert.containsNone(kanban, '.o_kanban_group:last .o_kanban_record', "last column should not contain any records");
|
||||
envIDs = [4];
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -819,14 +815,10 @@ QUnit.module('Views', {
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.strictEqual(event.data.env.ids.length, nbRecords,
|
||||
"should update the env with the records ids");
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.exportState().resIds.length, nbRecords);
|
||||
|
||||
// click to add an element and cancel the quick creation by pressing ESC
|
||||
testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first());
|
||||
|
||||
@@ -867,6 +859,7 @@ QUnit.module('Views', {
|
||||
"should have created a partner");
|
||||
assert.strictEqual(_.last(this.data.partner.records).name, "new partner",
|
||||
"should have correct name");
|
||||
assert.strictEqual(kanban.exportState().resIds.length, nbRecords);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -2408,16 +2401,11 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2);
|
||||
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2);
|
||||
assert.containsN(kanban, '.thisiseditable', 4);
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
|
||||
var $group = kanban.$('.o_kanban_group:nth-child(2)');
|
||||
@@ -2427,6 +2415,8 @@ QUnit.module('Views', {
|
||||
assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record');
|
||||
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 3);
|
||||
assert.containsN(kanban, '.thisiseditable', 4);
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -2637,24 +2627,22 @@ QUnit.module('Views', {
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// fold the second group and check that the res_ids it contains are no
|
||||
// longer in the environment
|
||||
envIDs = [1, 3];
|
||||
testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last'));
|
||||
testUtils.dom.click(kanban.$('.o_kanban_group:last .o_kanban_toggle_fold'));
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// re-open the second group and check that the res_ids it contains are
|
||||
// back in the environment
|
||||
envIDs = [1, 3, 2, 4];
|
||||
testUtils.dom.click(kanban.$('.o_kanban_group:last'));
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -3182,27 +3170,6 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('if view was grouped at start, it stays grouped', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<field name="product_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
});
|
||||
kanban.update({groupBy: []});
|
||||
|
||||
assert.hasClass(kanban.$('.o_kanban_view'), 'o_kanban_grouped');
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('if view was not grouped at start, it can be grouped and ungrouped', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
@@ -3217,11 +3184,13 @@ QUnit.module('Views', {
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
});
|
||||
assert.doesNotHaveClass(kanban.$('.o_kanban_view'), 'o_kanban_grouped', "should not be grouped");
|
||||
|
||||
assert.doesNotHaveClass(kanban.$('.o_kanban_view'), 'o_kanban_grouped');
|
||||
kanban.update({groupBy: ['product_id']});
|
||||
assert.hasClass(kanban.$('.o_kanban_view'),'o_kanban_grouped', "should be grouped");
|
||||
assert.hasClass(kanban.$('.o_kanban_view'),'o_kanban_grouped');
|
||||
kanban.update({groupBy: []});
|
||||
assert.doesNotHaveClass(kanban.$('.o_kanban_view'), 'o_kanban_grouped', "should not be grouped");
|
||||
assert.doesNotHaveClass(kanban.$('.o_kanban_view'), 'o_kanban_grouped');
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -3792,7 +3761,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('resequence columns in grouped by m2o', function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(6);
|
||||
this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
|
||||
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
|
||||
@@ -3807,12 +3776,6 @@ QUnit.module('Views', {
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(kanban.$('.o_kanban_view'),'ui-sortable',
|
||||
@@ -3821,6 +3784,7 @@ QUnit.module('Views', {
|
||||
"should have two columns");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 3,
|
||||
"first column should be id 3 before resequencing");
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// there is a 100ms delay on the d&d feature (jquery sortable) for
|
||||
// kanban columns, making it hard to test. So we rather bypass the d&d
|
||||
@@ -3831,6 +3795,7 @@ QUnit.module('Views', {
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5,
|
||||
"first column should be id 5 after resequencing");
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -3944,16 +3909,11 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts:{
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2,
|
||||
"there should be 2 records in the column");
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// load more
|
||||
envIDs = [1, 2, 3, 4]; // id 3 will be loaded
|
||||
@@ -3961,14 +3921,15 @@ QUnit.module('Views', {
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3,
|
||||
"there should now be 3 records in the column");
|
||||
|
||||
assert.verifySteps([[2, undefined], [2, undefined], [2, 2]],
|
||||
"the records should be correctly fetched");
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
// reload
|
||||
kanban.reload();
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3,
|
||||
"there should still be 3 records in the column after reload");
|
||||
assert.deepEqual(kanban.exportState().resIds, envIDs);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -4471,7 +4432,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('kanban with progressbars: correctly update env when archiving records', function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(2);
|
||||
|
||||
// add active field on partner model and make all records active
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
@@ -4493,22 +4454,16 @@ QUnit.module('Views', {
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
env_updated: function (ev) {
|
||||
assert.step(ev.data.env.ids);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(kanban.exportState().resIds, [1, 2, 3, 4]);
|
||||
|
||||
// archive all records of the first column
|
||||
testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:first'));
|
||||
testUtils.dom.click(kanban.$('.o_column_archive_records:visible'));
|
||||
testUtils.dom.click($('.modal-footer button:first'));
|
||||
|
||||
assert.verifySteps([
|
||||
[1, 2, 3, 4],
|
||||
[1, 2, 3],
|
||||
]);
|
||||
assert.deepEqual(kanban.exportState().resIds, [1, 2, 3]);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -4784,7 +4739,9 @@ QUnit.module('Views', {
|
||||
// call destroy function of controller to ensure that it correctly destroys everything
|
||||
kanban.__destroy();
|
||||
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent");
|
||||
// + 1 (parent)
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 1,
|
||||
"every widget must be destroyed exept the parent");
|
||||
|
||||
kanban.destroy();
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ QUnit.module('Views', {
|
||||
// Ascending order on Date
|
||||
list.$('th.o_column_sortable:contains("Date")').click();
|
||||
|
||||
var listContext = list.getContext();
|
||||
var listContext = list.getOwnedQueryParams();
|
||||
assert.deepEqual(listContext,
|
||||
{
|
||||
orderedBy: [{
|
||||
@@ -416,7 +416,7 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Loading a filter with a sort attribute', function (assert) {
|
||||
QUnit.test('loading a filter with a sort attribute', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.foo.fields.foo.sortable = true;
|
||||
@@ -431,15 +431,6 @@ QUnit.module('Views', {
|
||||
'<field name="foo"/>' +
|
||||
'<field name="date"/>' +
|
||||
'</tree>',
|
||||
context: {
|
||||
orderedBy: [{
|
||||
name: 'date',
|
||||
asc: true,
|
||||
}, {
|
||||
name: 'foo',
|
||||
asc: false,
|
||||
}]
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
if (searchReads === 0) {
|
||||
@@ -453,22 +444,34 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super.apply(this,arguments);
|
||||
},
|
||||
intercepts: {
|
||||
load_filters: function (event) {
|
||||
return $.when([
|
||||
{
|
||||
context: "{}",
|
||||
domain: "[]",
|
||||
id: 7,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[\"date asc\", \"foo desc\"]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}, {
|
||||
context: "{}",
|
||||
domain: "[]",
|
||||
id: 8,
|
||||
is_default: false,
|
||||
name: "My second favorite",
|
||||
sort: "[\"date desc\", \"foo asc\"]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}
|
||||
]).then(event.data.on_success);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Simulate loading a filter
|
||||
list.update({
|
||||
context: {
|
||||
orderedBy: [{
|
||||
name: 'date',
|
||||
asc: false,
|
||||
}, {
|
||||
name: 'foo',
|
||||
asc: true,
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
list.destroy()
|
||||
testUtils.dom.click(list.$('.o_control_panel .o_search_options button.o_favorites_menu_button'));
|
||||
testUtils.dom.click(list.$('.o_control_panel .o_search_options .o_favorites_menu .o_menu_item').eq(1));
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2one field rendering', function (assert) {
|
||||
@@ -1443,7 +1446,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
// Change page
|
||||
testUtils.dom.click(form.$('.o_pager_next'));
|
||||
testUtils.dom.click(form.$('.o_field_widget[name=o2m] .o_pager_next'));
|
||||
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 44',
|
||||
"record 44 should be first");
|
||||
assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 48',
|
||||
@@ -1712,10 +1715,9 @@ QUnit.module('Views', {
|
||||
assert.containsNone(list, '.o_view_nocontent',
|
||||
"should not have a no content helper displayed");
|
||||
assert.containsOnce(list, 'table', "should have rendered a table");
|
||||
assert.strictEqual(list.$el.css('height'), list.$('div.table-responsive').css('height'),
|
||||
assert.strictEqual(list.$('.o_content').css('height'), list.$('div.table-responsive').css('height'),
|
||||
"the div for the table should take the full height");
|
||||
|
||||
|
||||
assert.hasClass(list.$('tbody tr:eq(0)'),'o_selected_row',
|
||||
"the date field td should be in edit mode");
|
||||
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
|
||||
@@ -2051,15 +2053,12 @@ QUnit.module('Views', {
|
||||
assert.strictEqual(event.data.res_id, 4,
|
||||
"'switch_view' event has been triggered");
|
||||
},
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
|
||||
assert.deepEqual(list.exportState().resIds, envIDs);
|
||||
|
||||
// basic rendering tests
|
||||
assert.containsOnce(list, 'tbody', "there should be 1 tbody");
|
||||
@@ -2079,6 +2078,7 @@ QUnit.module('Views', {
|
||||
testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
|
||||
assert.deepEqual(list.exportState().resIds, envIDs);
|
||||
|
||||
var $openGroup = list.$('tbody:nth(1)');
|
||||
assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
|
||||
@@ -2101,6 +2101,7 @@ QUnit.module('Views', {
|
||||
testUtils.dom.click($openGroup.find('.o_group_header:nth(2)'));
|
||||
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
|
||||
assert.deepEqual(list.exportState().resIds, envIDs);
|
||||
|
||||
var $openSubGroup = list.$('tbody:nth(2)');
|
||||
assert.containsN(list, 'tbody', 4, "there should be 4 tbodys");
|
||||
@@ -2118,6 +2119,7 @@ QUnit.module('Views', {
|
||||
testUtils.dom.click(list.$('thead th:last'));
|
||||
assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
|
||||
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
|
||||
assert.deepEqual(list.exportState().resIds, envIDs);
|
||||
|
||||
$openSubGroup = list.$('tbody:nth(2)');
|
||||
assert.containsN(list, 'tbody', 4, "there should be 4 tbodys");
|
||||
@@ -2132,6 +2134,7 @@ QUnit.module('Views', {
|
||||
testUtils.dom.click(list.$('.o_group_header:nth(1)'));
|
||||
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
|
||||
assert.deepEqual(list.exportState().resIds, envIDs);
|
||||
|
||||
assert.containsOnce(list, 'tbody', "there should be 1 tbody");
|
||||
assert.containsN(list, '.o_group_header', 2,
|
||||
@@ -2202,14 +2205,14 @@ QUnit.module('Views', {
|
||||
"height of group header shouldn't have changed");
|
||||
assert.hasClass(list.$('.o_group_header td:last'),'o_group_pager',
|
||||
"last cell of open group header should have classname 'o_group_header'");
|
||||
assert.strictEqual(list.$('.o_pager_value').text(), '1-3',
|
||||
assert.strictEqual(list.$('.o_group_header .o_pager_value').text(), '1-3',
|
||||
"pager's value should be correct");
|
||||
assert.containsN(list, '.o_data_row', 3,
|
||||
"open group should display 3 records");
|
||||
|
||||
// go to next page
|
||||
testUtils.dom.click(list.$('.o_pager_next'));
|
||||
assert.strictEqual(list.$('.o_pager_value').text(), '4-4',
|
||||
testUtils.dom.click(list.$('.o_group_header .o_pager_next'));
|
||||
assert.strictEqual(list.$('.o_group_header .o_pager_value').text(), '4-4',
|
||||
"pager's value should be correct");
|
||||
assert.containsOnce(list, '.o_data_row',
|
||||
"open group should display 1 record");
|
||||
@@ -2584,7 +2587,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('display toolbar', function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(6);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
@@ -2605,13 +2608,20 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn');
|
||||
assert.strictEqual($dropdowns.length, 2,
|
||||
"there should be 2 dropdowns in the toolbar.");
|
||||
var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[1].children;
|
||||
assert.strictEqual($actions.length, 3,
|
||||
var $printMenu = list.$('.o_cp_sidebar .o_dropdown:contains(Print)');
|
||||
assert.isNotVisible($printMenu);
|
||||
var $actionMenu = list.$('.o_cp_sidebar .o_dropdown:contains(Action)');
|
||||
assert.isNotVisible($actionMenu);
|
||||
|
||||
testUtils.dom.click(list.$('.o_list_record_selector:first input'));
|
||||
|
||||
assert.isNotVisible($printMenu);
|
||||
assert.isVisible($actionMenu);
|
||||
|
||||
testUtils.dom.click($actionMenu.find('button')); // open Action menu
|
||||
assert.strictEqual($actionMenu.find('.dropdown-item').length, 3,
|
||||
"there should be 3 actions");
|
||||
var $customAction = $('.o_web_client .o_control_panel .btn-group .dropdown-item:nth(2)');
|
||||
var $customAction = $actionMenu.find('.dropdown-item:last');
|
||||
assert.strictEqual($customAction.text().trim(), 'Action event',
|
||||
"the custom action should have 'Action event' as name");
|
||||
|
||||
@@ -3859,7 +3869,9 @@ QUnit.module('Views', {
|
||||
// call destroy function of controller to ensure that it correctly destroys everything
|
||||
list.__destroy();
|
||||
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent");
|
||||
// + 1 (parent)
|
||||
assert.strictEqual(instanceNumber, initialInstanceNumber + 1,
|
||||
"every widget must be destroyed exept the parent");
|
||||
|
||||
list.destroy();
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ QUnit.module('Views', {
|
||||
_.extend(this.data.partner.fields, {
|
||||
foo: {string: "Foo", type: "integer", store: true},
|
||||
foo2: {string: "Foo2", type: "integer", store: true}
|
||||
})
|
||||
});
|
||||
|
||||
var pivot = createView({
|
||||
View: PivotView,
|
||||
@@ -171,7 +171,7 @@ QUnit.module('Views', {
|
||||
// there should be only one displayed measure as the other one is invisible
|
||||
assert.containsOnce(pivot, '.o_pivot_measure_row');
|
||||
// there should be only one measure besides count, as the other one is invisible
|
||||
assert.containsN(document.body, '.dropdown-item', 2);
|
||||
assert.containsN(pivot, '.o_cp_left .dropdown-item', 2);
|
||||
|
||||
pivot.destroy();
|
||||
});
|
||||
@@ -860,7 +860,7 @@ QUnit.module('Views', {
|
||||
pivot.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('getContext correctly returns measures and groupbys', function (assert) {
|
||||
QUnit.test('getOwnedQueryParams correctly returns measures and groupbys', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var pivot = createView({
|
||||
@@ -873,28 +873,34 @@ QUnit.module('Views', {
|
||||
'</pivot>',
|
||||
});
|
||||
|
||||
assert.deepEqual(pivot.getContext(), {
|
||||
pivot_column_groupby: ['date:day'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: [],
|
||||
assert.deepEqual(pivot.getOwnedQueryParams(), {
|
||||
context: {
|
||||
pivot_column_groupby: ['date:day'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: [],
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
// expand header on field customer
|
||||
testUtils.dom.click(pivot.$('thead .o_pivot_header_cell_closed:nth(1)'));
|
||||
testUtils.dom.click(pivot.$('.o_pivot_field_menu .dropdown-item[data-field="customer"]:first'));
|
||||
assert.deepEqual(pivot.getContext(), {
|
||||
pivot_column_groupby: ['date:day', 'customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: [],
|
||||
assert.deepEqual(pivot.getOwnedQueryParams(), {
|
||||
context: {
|
||||
pivot_column_groupby: ['date:day', 'customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: [],
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
// expand row on field product_id
|
||||
testUtils.dom.click(pivot.$('tbody .o_pivot_header_cell_closed'));
|
||||
testUtils.dom.click(pivot.$('.o_pivot_field_menu .dropdown-item[data-field="product_id"]:first'));
|
||||
assert.deepEqual(pivot.getContext(), {
|
||||
pivot_column_groupby: ['date:day', 'customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: ['product_id'],
|
||||
assert.deepEqual(pivot.getOwnedQueryParams(), {
|
||||
context: {
|
||||
pivot_column_groupby: ['date:day', 'customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: ['product_id'],
|
||||
},
|
||||
}, "context should be correct");
|
||||
|
||||
pivot.destroy();
|
||||
@@ -1195,12 +1201,16 @@ QUnit.module('Views', {
|
||||
// Set a domain
|
||||
testUtils.pivot.reload(pivot, {domain: [['product_id', '=', 41]]});
|
||||
|
||||
var expectedContext = {pivot_column_groupby: ['customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: ['product_id']};
|
||||
var expectedContext = {
|
||||
context: {
|
||||
pivot_column_groupby: ['customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: ['product_id'],
|
||||
},
|
||||
};
|
||||
|
||||
// Mock 'save as favorite'
|
||||
assert.deepEqual(pivot.getContext(), expectedContext,
|
||||
assert.deepEqual(pivot.getOwnedQueryParams(), expectedContext,
|
||||
'The pivot view should have the right context');
|
||||
|
||||
var $xpadHeader = pivot.$('tbody .o_pivot_header_cell_closed[data-original-title=Product]');
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
odoo.define('web.search_filters_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FilterMenu = require('web.FilterMenu');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Domain = require('web.Domain');
|
||||
|
||||
function createFilterMenu(filters, fields, params) {
|
||||
params = params || {};
|
||||
var target = params.debug ? document.body : $('#qunit-fixture');
|
||||
var menu = new FilterMenu(null, filters, fields);
|
||||
testUtils.mock.addMockEnvironment(menu, params);
|
||||
menu.appendTo(target);
|
||||
return menu;
|
||||
}
|
||||
|
||||
QUnit.module('FilterMenu', {
|
||||
beforeEach: function () {
|
||||
this.filters = [
|
||||
{
|
||||
isActive: false,
|
||||
description: 'some filter',
|
||||
domain: '',
|
||||
groupNumber: 1,
|
||||
},
|
||||
];
|
||||
this.fields = {
|
||||
boolean_field: {string: "Boolean Field", type: "boolean", default: true, searchable: true},
|
||||
date_field: {string: "A date", type: "date", searchable: true},
|
||||
char_field: {string: "Char Field", type: "char", default: "foo", searchable: true, trim: true},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test('simple rendering with no filter', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var filterMenu = createFilterMenu([], this.fields);
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
assert.containsNone(filterMenu, '.dropdown-divider');
|
||||
assert.containsNone(filterMenu, '.o_add_filter_menu');
|
||||
assert.containsOnce(filterMenu, '.o_add_custom_filter',
|
||||
'should have one element: a add custom filter item');
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with a filter', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var filterMenu = createFilterMenu(this.filters, this.fields);
|
||||
assert.containsN(filterMenu, '.dropdown-divider, .dropdown-item, .o_add_custom_filter', 4,
|
||||
'should have 4 elements: a hidden separator, a filter, a separator, a add custom filter item');
|
||||
assert.containsOnce(filterMenu, '.o_add_custom_filter');
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on add custom filter opens the submenu', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var filterMenu = createFilterMenu([], this.fields);
|
||||
// open menu dropdown
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
// open add custom filter submenu
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
assert.containsNone(filterMenu, '.dropdown-divider');
|
||||
assert.isVisible(filterMenu.$('.o_add_filter_menu'));
|
||||
assert.containsN(filterMenu, '.dropdown-item, .dropdown-item-text', 3,
|
||||
'should have 3 elements: a add custom filter item, a proposition, a apply button + add condition button');
|
||||
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('removing last prop disable the apply button', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var filterMenu = createFilterMenu([], this.fields);
|
||||
// open menu dropdown and custom filter submenu
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
|
||||
// remove the current unique proposition
|
||||
testUtils.dom.click(filterMenu.$('.o_searchview_extended_delete_prop'));
|
||||
|
||||
assert.containsNone(filterMenu, '.dropdown-divider');
|
||||
assert.containsN(filterMenu, '.dropdown-item, .dropdown-item-text', 2,
|
||||
'should have 2 elements: a add custom filter item, a apply button + add condition button');
|
||||
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('readding a proposition reenable apply button', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var filterMenu = createFilterMenu([], this.fields);
|
||||
// open menu dropdown and custom filter submenu, remove existing prop
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
testUtils.dom.click(filterMenu.$('.o_searchview_extended_delete_prop'));
|
||||
// read a proposition
|
||||
testUtils.dom.click(filterMenu.$('.o_add_condition'));
|
||||
assert.ok(!filterMenu.$('.o_apply_filter').attr('disabled'));
|
||||
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('adding a simple filter works', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
delete this.fields.date_field;
|
||||
var filterMenu = createFilterMenu([], this.fields, {
|
||||
intercepts: {
|
||||
new_filters: function (ev) {
|
||||
var filter = ev.data.filters[0];
|
||||
assert.strictEqual(filter.type, 'filter');
|
||||
assert.strictEqual(filter.description, 'Boolean Field is true');
|
||||
assert.strictEqual(filter.domain, '[[\"boolean_field\",\"=\",True]]');
|
||||
filterMenu.update([{
|
||||
isActive: true,
|
||||
description: '?',
|
||||
domain: '?',
|
||||
groupNumber: 1,
|
||||
}]);
|
||||
},
|
||||
},
|
||||
});
|
||||
// open menu dropdown and custom filter submenu, remove existing prop
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
// click on apply to activate filter
|
||||
testUtils.dom.click(filterMenu.$('.o_apply_filter'));
|
||||
assert.containsNone(filterMenu, '.o_filter_condition');
|
||||
assert.containsN(filterMenu, '.dropdown-divider', 2);
|
||||
assert.isNotVisible(filterMenu.$('.dropdown-divider').eq(0));
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.skip('commit search with an extended proposition with field char does not cause a crash', function (assert) {
|
||||
assert.expect(0);
|
||||
|
||||
this.fields = {many2one_field: {string: "Trululu", type: "many2one", relation: 'partner', selectable: true, searchable: true}};
|
||||
|
||||
var filterMenu = createFilterMenu([], this.fields, {
|
||||
intercepts: {
|
||||
new_filters: function (ev) {
|
||||
var filter = ev.data.filters[0];
|
||||
Domain.prototype.stringToArray(filter.domain);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// open menu dropdown and custom filter submenu, select trululu field and enter string "a", then click apply
|
||||
testUtils.dom.click(filterMenu.$('span.fa-filter'));
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field');
|
||||
testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), "a");
|
||||
testUtils.dom.click(filterMenu.$('.o_apply_filter'));
|
||||
|
||||
|
||||
// open custom filter submenu, select trululu field and enter string "'a'", then click apply
|
||||
testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
|
||||
testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field');
|
||||
testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), '"a"');
|
||||
testUtils.dom.click(filterMenu.$('.o_apply_filter'));
|
||||
filterMenu.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user