[IMP] web: add support for interval selection in groupbys
In this commit, we extend the search view to allow the selection of some intervals for date or datetime fields in groupby menu. The valid intervals are day, week, month, quarter, and year. It is also possible to add a button 'Group By' in the control panel owned by a graph view in case it is declared 'embedded' (e.g. in a dashboard view). Note that have three new generic reusable widgets have been introduced in this commit: DropdownMenu, GroupByMenu, and FiltersMenu.
This commit is contained in:
committed by
Géry Debongnie
parent
7bf5dd687b
commit
e1aac71769
@@ -10,7 +10,7 @@ odoo.define('web.AbstractAction', function (require) {
|
||||
* 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 this AbstractAction class.
|
||||
* 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
|
||||
@@ -51,6 +51,13 @@ var AbstractAction = Widget.extend({
|
||||
canBeRemoved: function () {
|
||||
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.
|
||||
*/
|
||||
getContext: function () {
|
||||
},
|
||||
/**
|
||||
* Gives the focus to the action
|
||||
*/
|
||||
|
||||
@@ -76,6 +76,7 @@ var AbstractView = Class.extend({
|
||||
* @param {string[]} params.domain
|
||||
* @param {string[]} params.groupBy
|
||||
* @param {number} [params.currentId]
|
||||
* @param {boolean} params.isEmbedded
|
||||
* @param {number[]} [params.ids]
|
||||
* @param {boolean} [params.withControlPanel]
|
||||
* @param {boolean} [params.action.flags.headless]
|
||||
@@ -99,6 +100,10 @@ var AbstractView = Class.extend({
|
||||
}
|
||||
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.isEmbedded = params.isEmbedded || false;
|
||||
|
||||
this.rendererParams = {
|
||||
arch: this.arch,
|
||||
@@ -114,6 +119,7 @@ var AbstractView = Class.extend({
|
||||
duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true,
|
||||
},
|
||||
groupable: this.groupable,
|
||||
isEmbedded: this.isEmbedded,
|
||||
controllerID: params.controllerID,
|
||||
};
|
||||
// AAB: these params won't be necessary as soon as the ControlPanel will
|
||||
|
||||
@@ -4,13 +4,15 @@ odoo.define('web.GraphController', function (require) {
|
||||
* Odoo Graph view
|
||||
*---------------------------------------------------------*/
|
||||
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var core = require('web.core');
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var GroupByMenuInterfaceMixin = require('web.GroupByMenuInterfaceMixin');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
var GraphController = AbstractController.extend({
|
||||
var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{
|
||||
className: 'o_graph',
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
@@ -18,10 +20,23 @@ var GraphController = AbstractController.extend({
|
||||
* @param {GraphRenderer} renderer
|
||||
* @param {Object} params
|
||||
* @param {string[]} params.measures
|
||||
* @param {boolean} params.isEmbedded
|
||||
* @param {string[]} params.groupableFields,
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
GroupByMenuInterfaceMixin.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.
|
||||
},
|
||||
/**
|
||||
* @todo check if this can be removed (mostly duplicate with
|
||||
@@ -53,6 +68,12 @@ var GraphController = AbstractController.extend({
|
||||
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,
|
||||
};
|
||||
},
|
||||
/**
|
||||
@@ -66,13 +87,18 @@ var GraphController = AbstractController.extend({
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
if ($node) {
|
||||
var context = {measures: _.pairs(_.omit(this.measures, '__count__'))};
|
||||
var context = {
|
||||
measures: _.pairs(_.omit(this.measures, '__count__')),
|
||||
};
|
||||
this.$buttons = $(qweb.render('GraphView.buttons', context));
|
||||
this.$measureList = this.$buttons.find('.o_graph_measures_list');
|
||||
this.$buttons.find('button').tooltip();
|
||||
this.$buttons.click(this._onButtonClick.bind(this));
|
||||
this._updateButtons();
|
||||
this.$buttons.appendTo($node);
|
||||
if (this.isEmbedded) {
|
||||
this._addGroupByMenu($node, this.groupableFields);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -80,10 +106,22 @@ var GraphController = AbstractController.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} groupbys
|
||||
*/
|
||||
_setGroupby: function (groupbys) {
|
||||
this.update({groupBy: groupbys});
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo remove this and directly calls update. Update should be overridden
|
||||
* and modified to call _updateButtons
|
||||
*
|
||||
* private
|
||||
*
|
||||
* @param {string} mode one of 'pie', 'line' or 'bar'
|
||||
*/
|
||||
_setMode: function (mode) {
|
||||
@@ -102,6 +140,8 @@ var GraphController = AbstractController.extend({
|
||||
});
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_update: function () {
|
||||
@@ -133,17 +173,20 @@ var GraphController = AbstractController.extend({
|
||||
/**
|
||||
* Do what need to be done when a button from the control panel is clicked.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onButtonClick: function (event) {
|
||||
var $target = $(event.target);
|
||||
var parent;
|
||||
var field;
|
||||
if ($target.hasClass('o_graph_button')) {
|
||||
this._setMode($target.data('mode'));
|
||||
} else if ($target.parents('.o_graph_measures_list').length) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var parent = $target.parent();
|
||||
var field = parent.data('field');
|
||||
parent = $target.parent();
|
||||
field = parent.data('field');
|
||||
this._setMeasure(field);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,6 +51,8 @@ return AbstractModel.extend({
|
||||
* @param {string[]} params.groupBys a list of valid field names
|
||||
* @param {Object} params.context
|
||||
* @param {string[]} params.domain
|
||||
* @param {Object} params.intervalMapping object linking fieldNames with intervals.
|
||||
* this could be useful to simplify the code. For now this parameter is not used.
|
||||
* @returns {Deferred} The deferred does not return a handle, we don't need
|
||||
* to keep track of various entities.
|
||||
*/
|
||||
@@ -62,6 +64,12 @@ return AbstractModel.extend({
|
||||
this.chart = {
|
||||
data: [],
|
||||
groupedBy: params.groupedBy.length ? params.groupedBy : groupBys,
|
||||
// this parameter is not used anywhere 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.
|
||||
intervalMapping: params.intervalMapping,
|
||||
measure: params.context.graph_measure || params.measure,
|
||||
mode: params.context.graph_mode || params.mode,
|
||||
domain: params.domain,
|
||||
@@ -97,6 +105,9 @@ return AbstractModel.extend({
|
||||
if ('groupBy' in params) {
|
||||
this.chart.groupedBy = params.groupBy.length ? params.groupBy : this.initialGroupBys;
|
||||
}
|
||||
if ('intervalMapping' in params) {
|
||||
this.chart.intervalMapping = params.intervalMapping;
|
||||
}
|
||||
if ('measure' in params) {
|
||||
this.chart.measure = params.measure;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ var GraphRenderer = require('web.GraphRenderer');
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var GROUPABLE_TYPES =
|
||||
['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
|
||||
|
||||
var GraphView = AbstractView.extend({
|
||||
display_name: _lt('Graph'),
|
||||
icon: 'fa-bar-chart',
|
||||
@@ -43,18 +46,22 @@ var GraphView = AbstractView.extend({
|
||||
var measure;
|
||||
var groupBys = [];
|
||||
var measures = {__count__: {string: _t("Count"), type: "integer"}};
|
||||
var groupableFields = {};
|
||||
var intervalMapping = {};
|
||||
this.fields.__count__ = {string: _t("Count"), type: "integer"};
|
||||
|
||||
this.arch.children.forEach(function (field) {
|
||||
var name = field.attrs.name;
|
||||
if (field.attrs.interval) {
|
||||
name += ':' + field.attrs.interval;
|
||||
var fieldName = field.attrs.name;
|
||||
var interval = field.attrs.interval;
|
||||
if (interval) {
|
||||
intervalMapping[fieldName] = interval;
|
||||
fieldName = fieldName + ':' + interval;
|
||||
}
|
||||
if (field.attrs.type === 'measure') {
|
||||
measure = name;
|
||||
measures[name] = self.fields[name];
|
||||
measure = fieldName;
|
||||
measures[fieldName] = self.fields[fieldName];
|
||||
} else {
|
||||
groupBys.push(name);
|
||||
groupBys.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,15 +71,20 @@ var GraphView = AbstractView.extend({
|
||||
_.contains(params.additionalMeasures, name)) {
|
||||
measures[name] = field;
|
||||
}
|
||||
if (_.contains(GROUPABLE_TYPES, field.type)) {
|
||||
groupableFields[name] = field;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.controllerParams.measures = measures;
|
||||
this.controllerParams.groupableFields = groupableFields;
|
||||
this.rendererParams.stacked = this.arch.attrs.stacked !== "False";
|
||||
|
||||
this.loadParams.mode = this.arch.attrs.type || 'bar';
|
||||
this.loadParams.measure = measure || '__count__';
|
||||
this.loadParams.groupBys = groupBys || [];
|
||||
this.loadParams.intervalMapping = intervalMapping;
|
||||
this.loadParams.fields = this.fields;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
odoo.define('web.FilterMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var search_filters = require('web.search_filters');
|
||||
var search_inputs = require('web.search_inputs');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
return Widget.extend({
|
||||
template: 'SearchView.FilterMenu',
|
||||
events: {
|
||||
'click .o_add_filter': function (event) {
|
||||
event.preventDefault();
|
||||
this.toggle_custom_filter_menu();
|
||||
},
|
||||
'click li': function (event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
'hidden.bs.dropdown': function () {
|
||||
this.toggle_custom_filter_menu(false);
|
||||
},
|
||||
'click .o_add_condition': 'append_proposition',
|
||||
'click .o_apply_filter': 'commit_search',
|
||||
'keyup .o_searchview_extended_prop_value': function (ev) {
|
||||
if (ev.which === $.ui.keyCode.ENTER) {
|
||||
this.commit_search();
|
||||
}
|
||||
},
|
||||
},
|
||||
init: function (parent, filters, fields) {
|
||||
this._super(parent);
|
||||
this.filters = filters || [];
|
||||
this.searchview = parent;
|
||||
this.propositions = [];
|
||||
this.custom_filters_open = false;
|
||||
this.fields = _.pick(fields, function (field, name) {
|
||||
return field.selectable !== false && name !== 'id';
|
||||
});
|
||||
this.fields.id = { string: 'ID', type: 'id', searchable: true };
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$menu = this.$('.o_filters_menu');
|
||||
this.$add_filter = this.$('.o_add_filter');
|
||||
this.$apply_filter = this.$('.o_apply_filter');
|
||||
this.$add_filter_menu = this.$('.o_add_filter_menu');
|
||||
_.each(this.filters, function (group) {
|
||||
if (group.is_visible()) {
|
||||
group.insertBefore(self.$add_filter);
|
||||
$('<li class="divider">').insertBefore(self.$add_filter);
|
||||
}
|
||||
});
|
||||
},
|
||||
toggle_custom_filter_menu: function (is_open) {
|
||||
var self = this;
|
||||
this.custom_filters_open = !_.isUndefined(is_open) ? is_open : !this.custom_filters_open;
|
||||
var def;
|
||||
if (this.custom_filters_open && !this.propositions.length) {
|
||||
def = this.append_proposition();
|
||||
}
|
||||
$.when(def).then(function () {
|
||||
self.$add_filter
|
||||
.toggleClass('o_closed_menu', !self.custom_filters_open)
|
||||
.toggleClass('o_open_menu', self.custom_filters_open);
|
||||
self.$add_filter_menu.toggle(self.custom_filters_open);
|
||||
self.$('.o_filter_condition').toggle(self.custom_filters_open);
|
||||
});
|
||||
},
|
||||
append_proposition: function () {
|
||||
var prop = new search_filters.ExtendedSearchProposition(this, this.fields);
|
||||
this.propositions.push(prop);
|
||||
this.$apply_filter.prop('disabled', false);
|
||||
return prop.insertBefore(this.$add_filter_menu);
|
||||
},
|
||||
remove_proposition: function (prop) {
|
||||
this.propositions = _.without(this.propositions, prop);
|
||||
if (!this.propositions.length) {
|
||||
this.$apply_filter.prop('disabled', true);
|
||||
}
|
||||
prop.destroy();
|
||||
},
|
||||
commit_search: function () {
|
||||
var filters = _.invoke(this.propositions, 'get_filter'),
|
||||
filters_widgets = _.map(filters, function (filter) {
|
||||
return new search_inputs.Filter(filter, this);
|
||||
}),
|
||||
filter_group = new search_inputs.FilterGroup(filters_widgets, this.searchview),
|
||||
facets = filters_widgets.map(function (filter) {
|
||||
return filter_group.make_facet([filter_group.make_value(filter)]);
|
||||
});
|
||||
filter_group.insertBefore(this.$add_filter);
|
||||
$('<li class="divider">').insertBefore(this.$add_filter);
|
||||
this.searchview.query.add(facets, {silent: true});
|
||||
this.searchview.query.trigger('reset');
|
||||
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
this.append_proposition();
|
||||
this.toggle_custom_filter_menu(false);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,105 +1,208 @@
|
||||
odoo.define('web.FilterMenu', function (require) {
|
||||
odoo.define('web.FiltersMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
var search_filters = require('web.search_filters');
|
||||
var search_inputs = require('web.search_inputs');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
return Widget.extend({
|
||||
template: 'SearchView.FilterMenu',
|
||||
events: {
|
||||
'click .o_add_filter': function (event) {
|
||||
event.preventDefault();
|
||||
this.toggle_custom_filter_menu();
|
||||
},
|
||||
'click li': function (event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
'hidden.bs.dropdown': function () {
|
||||
this.toggle_custom_filter_menu(false);
|
||||
},
|
||||
'click .o_add_condition': 'append_proposition',
|
||||
'click .o_apply_filter': 'commit_search',
|
||||
'keyup .o_searchview_extended_prop_value': function (ev) {
|
||||
if (ev.which === $.ui.keyCode.ENTER) {
|
||||
this.commit_search();
|
||||
}
|
||||
},
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
|
||||
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',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @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;
|
||||
* 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 'field_get' of a model: mapping from field name
|
||||
* to an object with its properties
|
||||
*/
|
||||
init: function (parent, filters, fields) {
|
||||
this._super(parent);
|
||||
// determines where the filter menu is displayed and its style
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.filters = filters || [];
|
||||
this.searchview = parent;
|
||||
// determines list of options used by filter of type 'date'
|
||||
// TO DO: simplify optionIds below
|
||||
this.intervalOptions = [
|
||||
{description: _t('Day'), optionId: '__interval__day__'},
|
||||
{description: _t('Week'), optionId: '__interval__week__'},
|
||||
{description: _t('Month'), optionId: '__interval__month__'},
|
||||
{description: _t('Year'), optionId: '__interval__year__'},
|
||||
];
|
||||
// determines the default option used in case
|
||||
// it has not been provided for a filter
|
||||
this.defaultOptionId = "__interval__month__";
|
||||
// determines when the 'Add custom filter' submenu is open
|
||||
this.generatorMenuIsOpen = false;
|
||||
this.propositions = [];
|
||||
this.custom_filters_open = false;
|
||||
this.fields = _.pick(fields, function (field, name) {
|
||||
return field.selectable !== false && name !== 'id';
|
||||
});
|
||||
this.fields.id = { string: 'ID', type: 'id', searchable: true };
|
||||
this.fields.id = {string: 'ID', type: 'id', searchable: true};
|
||||
var dropdownHeader = {
|
||||
category: 'filterCategory',
|
||||
title: 'Filters',
|
||||
icon: 'fa fa-filter',
|
||||
symbol: this.isMobile ? 'fa fa-chevron-right pull-right mt4' : 'caret'
|
||||
};
|
||||
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 () {
|
||||
var self = this;
|
||||
this.$menu = this.$('.o_filters_menu');
|
||||
this.$add_filter = this.$('.o_add_filter');
|
||||
this.$apply_filter = this.$('.o_apply_filter');
|
||||
this.$add_filter_menu = this.$('.o_add_filter_menu');
|
||||
_.each(this.filters, function (group) {
|
||||
if (group.is_visible()) {
|
||||
group.insertBefore(self.$add_filter);
|
||||
$('<li class="divider">').insertBefore(self.$add_filter);
|
||||
}
|
||||
});
|
||||
this.$menu = this.$('ul.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');
|
||||
},
|
||||
toggle_custom_filter_menu: function (is_open) {
|
||||
var self = this;
|
||||
this.custom_filters_open = !_.isUndefined(is_open) ? is_open : !this.custom_filters_open;
|
||||
var def;
|
||||
if (this.custom_filters_open && !this.propositions.length) {
|
||||
def = this.append_proposition();
|
||||
}
|
||||
$.when(def).then(function () {
|
||||
self.$add_filter
|
||||
.toggleClass('o_closed_menu', !self.custom_filters_open)
|
||||
.toggleClass('o_open_menu', self.custom_filters_open);
|
||||
self.$add_filter_menu.toggle(self.custom_filters_open);
|
||||
self.$('.o_filter_condition').toggle(self.custom_filters_open);
|
||||
});
|
||||
},
|
||||
append_proposition: function () {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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.$apply_filter.prop('disabled', false);
|
||||
return prop.insertBefore(this.$add_filter_menu);
|
||||
this.$('.o_apply_filter').prop('disabled', false);
|
||||
return prop.insertBefore(this.$addFilterMenu);
|
||||
},
|
||||
remove_proposition: function (prop) {
|
||||
this.propositions = _.without(this.propositions, prop);
|
||||
if (!this.propositions.length) {
|
||||
this.$apply_filter.prop('disabled', true);
|
||||
}
|
||||
prop.destroy();
|
||||
},
|
||||
commit_search: function () {
|
||||
var filters = _.invoke(this.propositions, 'get_filter'),
|
||||
filters_widgets = _.map(filters, function (filter) {
|
||||
return new search_inputs.Filter(filter, this);
|
||||
}),
|
||||
filter_group = new search_inputs.FilterGroup(filters_widgets, this.searchview),
|
||||
facets = filters_widgets.map(function (filter) {
|
||||
return filter_group.make_facet([filter_group.make_value(filter)]);
|
||||
/**
|
||||
* 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');
|
||||
var groupId = _.uniqueId('__group__');
|
||||
var toSendToSearchView = [];
|
||||
filters.forEach(function (filter) {
|
||||
var filterName = _.uniqueId('__filter__');
|
||||
var filterItem = {
|
||||
itemId: filterName,
|
||||
description: filter.attrs.string,
|
||||
groupId: groupId,
|
||||
isActive: true,
|
||||
};
|
||||
self._prepareItem(filterItem);
|
||||
toSendToSearchView.push({
|
||||
itemId: filterName,
|
||||
groupId: groupId,
|
||||
filter: filter,
|
||||
});
|
||||
filter_group.insertBefore(this.$add_filter);
|
||||
$('<li class="divider">').insertBefore(this.$add_filter);
|
||||
this.searchview.query.add(facets, {silent: true});
|
||||
this.searchview.query.trigger('reset');
|
||||
|
||||
self.items.push(filterItem);
|
||||
});
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('new_filter', toSendToSearchView);
|
||||
_.invoke(this.propositions, 'destroy');
|
||||
this.propositions = [];
|
||||
this.append_proposition();
|
||||
this.toggle_custom_filter_menu(false);
|
||||
// this._appendProposition();
|
||||
this._toggleCustomFilterMenu();
|
||||
},
|
||||
/**
|
||||
* 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;
|
||||
|
||||
});
|
||||
|
||||
@@ -3,77 +3,232 @@ odoo.define('web.GroupByMenu', function (require) {
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var search_inputs = require('web.search_inputs');
|
||||
var Widget = require('web.Widget');
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
return Widget.extend({
|
||||
template: 'SearchView.GroupByMenu',
|
||||
events: {
|
||||
'click li': function (event) {
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
'hidden.bs.dropdown': function () {
|
||||
this.toggle_add_menu(false);
|
||||
},
|
||||
'click .o_add_custom_group a': function (event) {
|
||||
event.preventDefault();
|
||||
this.toggle_add_menu();
|
||||
},
|
||||
},
|
||||
init: function (parent, groups, fields) {
|
||||
var GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
|
||||
|
||||
var GroupByMenu = DropdownMenu.extend({
|
||||
events: _.extend({}, DropdownMenu.prototype.events,
|
||||
{
|
||||
'click .o_add_custom_group a': '_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; (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)
|
||||
* defaultOptionId: string refers to an optionId (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._super(parent);
|
||||
this.searchview = parent;
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.groups = groups || [];
|
||||
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'},
|
||||
{description: 'Week', optionId: 'week'},
|
||||
{description: 'Month', optionId: 'month'},
|
||||
{description: 'Quarter', optionId: 'quarter'},
|
||||
{description: 'Year', optionId: 'year'},
|
||||
];
|
||||
// determines the default option used in case
|
||||
// it has not been provided for a groupby
|
||||
this.defaultOptionId = "month";
|
||||
this.groupableFields = [];
|
||||
var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
|
||||
_.each(fields, function (field, name) {
|
||||
if (field.sortable && _.contains(groupable_types, field.type)) {
|
||||
self.groupableFields.push(_.extend({}, field, {name: name}));
|
||||
if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) {
|
||||
self.groupableFields.push(_.extend({}, field, {
|
||||
name: name,
|
||||
isDate: _.contains(['date', 'datetime'], field.type),
|
||||
}));
|
||||
}
|
||||
});
|
||||
self.groupableFields = _.sortBy(this.groupableFields, 'string');
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$menu = this.$('.o_group_by_menu');
|
||||
var divider = this.$menu.find('.divider');
|
||||
_.invoke(this.groups, 'insertBefore', divider);
|
||||
if (this.groups.length) {
|
||||
divider.show();
|
||||
}
|
||||
this.$add_group = this.$menu.find('.o_add_custom_group');
|
||||
this.$menu.append(QWeb.render('GroupByMenuSelector', this));
|
||||
this.$add_group_menu = this.$('.o_add_group');
|
||||
this.$group_selector = this.$('.o_group_selector');
|
||||
this.$('.o_select_group').click(function () {
|
||||
self.toggle_add_menu(false);
|
||||
var field = self.$group_selector.find(':selected').data('name');
|
||||
self.add_groupby_to_menu(field);
|
||||
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 btn-sm dropdown-toggle'},
|
||||
};
|
||||
}
|
||||
var dropdownHeader = {
|
||||
category: 'groupByCategory',
|
||||
title: 'Group By',
|
||||
icon: 'fa fa-bars',
|
||||
symbol: this.isMobile ? 'fa fa-chevron-right pull-right mt4' : 'caret',
|
||||
style: style,
|
||||
};
|
||||
this._super(parent, dropdownHeader, groupbys, options);
|
||||
},
|
||||
toggle_add_menu: function (is_open) {
|
||||
this.$add_group
|
||||
.toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
|
||||
.toggleClass('o_open_menu', is_open);
|
||||
this.$add_group_menu.toggle(is_open);
|
||||
if (this.$add_group.hasClass('o_open_menu')) {
|
||||
this.$group_selector.focus();
|
||||
|
||||
/**
|
||||
* render the template used to add a new custom groupby and append it
|
||||
* to the basic dropdown menu
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
start: function () {
|
||||
this.$menu = this.$('ul.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 'this.defaultOptionId'
|
||||
*
|
||||
* @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: this.groupId,
|
||||
isActive: true,
|
||||
};
|
||||
var eventData = _.clone(groupby);
|
||||
this._prepareItem(groupby);
|
||||
if (groupby.hasOptions) {
|
||||
groupby.currentOptionId = groupby.defaultOptionId;
|
||||
eventData.optionId = groupby.currentOptionId;
|
||||
}
|
||||
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;
|
||||
item.defaultOptionId = item.defaultOptionId || this.defaultOptionId;
|
||||
}
|
||||
// the idea is that we have only one group in the groupby menu
|
||||
// like in the search view.
|
||||
if (!this.groupId) {
|
||||
this.groupId = item.groupId;
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
add_groupby_to_menu: function (field_name) {
|
||||
var filter = new search_inputs.Filter({attrs:{
|
||||
context:"{'group_by':'" + field_name + "''}",
|
||||
name: _.find(this.groupableFields, {name: field_name}).string,
|
||||
}}, this.searchview);
|
||||
var group = new search_inputs.FilterGroup([filter], this.searchview),
|
||||
divider = this.$('.divider').show();
|
||||
group.insertBefore(divider);
|
||||
group.toggle(filter);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
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;
|
||||
|
||||
});
|
||||
@@ -16,7 +16,12 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext
|
||||
'change .o_searchview_extended_prop_op': 'operator_changed',
|
||||
'click .o_searchview_extended_delete_prop': function (e) {
|
||||
e.stopPropagation();
|
||||
this.getParent().remove_proposition(this);
|
||||
this.trigger_up('remove_proposition');
|
||||
},
|
||||
'keyup .o_searchview_extended_prop_value': function (ev) {
|
||||
if (ev.which === $.ui.keyCode.ENTER) {
|
||||
this.trigger_up('confirm_proposition');
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -515,7 +515,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
* @param {Array<instance.web.search.Filter>} filters elements of the group
|
||||
* @param {instance.web.SearchView} parent parent in which the filters are contained
|
||||
*/
|
||||
init: function (filters, parent) {
|
||||
init: function (filters, parent, intervalMapping) {
|
||||
// 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) &&
|
||||
@@ -523,11 +523,12 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
if (!f.attrs.context) { return false; }
|
||||
var c = pyeval.eval('context', f.attrs.context);
|
||||
return !_.isEmpty(c.group_by);})) {
|
||||
return new GroupbyGroup(filters, parent);
|
||||
return new GroupbyGroup(filters, parent, intervalMapping);
|
||||
}
|
||||
this._super(parent);
|
||||
this.filters = filters;
|
||||
this.searchview = parent;
|
||||
this.intervalMapping = intervalMapping;
|
||||
this.searchview.query.on('add remove change reset', this.proxy('search_change'));
|
||||
},
|
||||
start: function () {
|
||||
@@ -560,6 +561,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
},
|
||||
make_facet: function (values) {
|
||||
return {
|
||||
cat: 'filterCategory',
|
||||
category: _t("Filter"),
|
||||
icon: this.icon,
|
||||
separator: _t(" or "),
|
||||
@@ -610,8 +612,23 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
* @return {Array} enabled filters in this group
|
||||
*/
|
||||
get_groupby: function (facet) {
|
||||
var self = this;
|
||||
return facet.values.chain()
|
||||
.map(function (f) { return f.get('value').attrs.context; })
|
||||
.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();
|
||||
@@ -683,14 +700,18 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
facet: self.make_facet([facet_value])
|
||||
};
|
||||
}));
|
||||
},
|
||||
updateIntervalMapping: function (intervalMapping) {
|
||||
this.intervalMapping = intervalMapping;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var GroupbyGroup = FilterGroup.extend({
|
||||
icon: 'fa-bars',
|
||||
completion_label: _lt("Group by: %s"),
|
||||
init: function (filters, parent) {
|
||||
this._super(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
|
||||
@@ -713,6 +734,7 @@ var GroupbyGroup = FilterGroup.extend({
|
||||
},
|
||||
make_facet: function (values) {
|
||||
return {
|
||||
cat: 'groupByCategory',
|
||||
category: _t("Group By"),
|
||||
icon: this.icon,
|
||||
separator: " > ",
|
||||
|
||||
@@ -5,7 +5,7 @@ var AutoComplete = require('web.AutoComplete');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var FavoriteMenu = require('web.FavoriteMenu');
|
||||
var FilterMenu = require('web.FilterMenu');
|
||||
var FiltersMenu = require('web.FiltersMenu');
|
||||
var GroupByMenu = require('web.GroupByMenu');
|
||||
var pyeval = require('web.pyeval');
|
||||
var search_inputs = require('web.search_inputs');
|
||||
@@ -167,7 +167,8 @@ var FacetView = Widget.extend({
|
||||
template: 'SearchView.FacetView',
|
||||
events: {
|
||||
'focus': function () { this.trigger('focused', this); },
|
||||
'blur': function () { this.trigger('blurred', this); },
|
||||
'blur': function () {
|
||||
this.trigger('blurred', this); },
|
||||
'click': function (e) {
|
||||
if ($(e.target).hasClass('o_facet_remove')) {
|
||||
this.model.destroy();
|
||||
@@ -186,9 +187,10 @@ var FacetView = Widget.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
init: function (parent, model) {
|
||||
init: function (parent, model, intervalMapping) {
|
||||
this._super(parent);
|
||||
this.model = model;
|
||||
this.intervalMapping = intervalMapping;
|
||||
this.model.on('change', this.model_changed, this);
|
||||
},
|
||||
destroy: function () {
|
||||
@@ -203,7 +205,9 @@ var FacetView = Widget.extend({
|
||||
if (index > 0) {
|
||||
$('<span/>', {html: self.model.get('separator') || _t(" or ")}).addClass('o_facet_values_sep').appendTo($e);
|
||||
}
|
||||
return new FacetValueView(self, value).appendTo($e);
|
||||
var couple = _.findWhere(self.intervalMapping, {groupby: value.attributes.value});
|
||||
var interval = couple ? couple.interval : undefined;
|
||||
return new FacetValueView(self, value, interval).appendTo($e);
|
||||
}));
|
||||
});
|
||||
},
|
||||
@@ -214,9 +218,22 @@ var FacetView = Widget.extend({
|
||||
|
||||
var FacetValueView = Widget.extend({
|
||||
template: 'SearchView.FacetView.Value',
|
||||
init: function (parent, model) {
|
||||
init: function (parent, model, interval) {
|
||||
this._super(parent);
|
||||
this.model = model;
|
||||
|
||||
var intervalDescription = {
|
||||
day: 'Day',
|
||||
week: 'Week',
|
||||
month: 'Month',
|
||||
quarter: 'Quarter',
|
||||
year: 'Year',
|
||||
};
|
||||
// to do: put a test on interval
|
||||
if (interval) {
|
||||
var intervalLabel = intervalDescription[interval];
|
||||
this.intervalLabel = _t(intervalLabel);
|
||||
}
|
||||
this.model.on('change', this.model_changed, this);
|
||||
},
|
||||
destroy: function () {
|
||||
@@ -257,6 +274,12 @@ var SearchView = Widget.extend({
|
||||
}
|
||||
},
|
||||
},
|
||||
custom_events: {
|
||||
'menu_item_toggled': '_onItemToggled',
|
||||
'item_option_changed': '_onItemOptionChanged',
|
||||
'new_groupby': '_onNewGroupby',
|
||||
'new_filter': '_onNewFilter',
|
||||
},
|
||||
defaults: _.extend({}, Widget.prototype.defaults, {
|
||||
hidden: false,
|
||||
disable_filters: false,
|
||||
@@ -288,6 +311,16 @@ var SearchView = Widget.extend({
|
||||
this.title = this.options.action && this.options.action.name;
|
||||
this.action = this.options.action || {};
|
||||
this.search_fields = [];
|
||||
|
||||
this.selectedGroupIds = {
|
||||
groupByCategory: [],
|
||||
filterCategory: []
|
||||
};
|
||||
this.groupsMapping = [];
|
||||
this.groupbysMapping = [];
|
||||
this.filtersMapping = [];
|
||||
this.intervalMapping = [];
|
||||
|
||||
this.filters = [];
|
||||
this.groupbys = [];
|
||||
var visibleSearchMenu = this.call('local_storage', 'getItem', 'visible_search_menu');
|
||||
@@ -297,7 +330,7 @@ var SearchView = Widget.extend({
|
||||
this.headless = this.options.hidden && _.isEmpty(this.search_defaults);
|
||||
this.$buttons = this.options.$buttons;
|
||||
|
||||
this.filter_menu = undefined;
|
||||
this.filters_menu = undefined;
|
||||
this.groupby_menu = undefined;
|
||||
this.favorite_menu = undefined;
|
||||
},
|
||||
@@ -312,6 +345,7 @@ var SearchView = Widget.extend({
|
||||
return $.when(this._super(), def);
|
||||
},
|
||||
start: function () {
|
||||
var self= this;
|
||||
if (this.headless) {
|
||||
this.do_hide();
|
||||
}
|
||||
@@ -324,24 +358,29 @@ var SearchView = Widget.extend({
|
||||
this.$('.o_searchview_more')
|
||||
.toggleClass('fa-search-minus', this.visible_filters)
|
||||
.toggleClass('fa-search-plus', !this.visible_filters);
|
||||
var menu_defs = [];
|
||||
var def;
|
||||
this.prepare_search_inputs();
|
||||
var $buttons = this._getButtonsElement();
|
||||
if ($buttons) {
|
||||
if (!this.options.disable_filters) {
|
||||
this.filter_menu = new FilterMenu(this, this.filters, this.fields);
|
||||
menu_defs.push(this.filter_menu.appendTo($buttons));
|
||||
}
|
||||
if (!this.options.disable_groupby) {
|
||||
this.groupby_menu = new GroupByMenu(this, this.groupbys, this.fields);
|
||||
menu_defs.push(this.groupby_menu.appendTo($buttons));
|
||||
}
|
||||
if (!this.options.disable_favorites) {
|
||||
this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action, this.favorite_filters);
|
||||
menu_defs.push(this.favorite_menu.appendTo($buttons));
|
||||
def = this.favorite_menu.appendTo($buttons);
|
||||
}
|
||||
}
|
||||
return $.when.apply($, menu_defs).then(this.set_default_filters.bind(this));
|
||||
return $.when(def)
|
||||
.then(this.set_default_filters.bind(this))
|
||||
.then(function () {
|
||||
var menu_defs = [];
|
||||
if (!self.options.disable_groupby) {
|
||||
self.groupby_menu = self._createGroupByMenu();
|
||||
menu_defs.push(self.groupby_menu.prependTo($buttons));
|
||||
}
|
||||
if (!self.options.disable_filters) {
|
||||
self.filters_menu = self._createFiltersMenu();
|
||||
menu_defs.push(self.filters_menu.prependTo($buttons));
|
||||
}
|
||||
return $.when.apply($, menu_defs);
|
||||
});
|
||||
},
|
||||
on_attach_callback: function () {
|
||||
this._focusInput();
|
||||
@@ -416,10 +455,18 @@ var SearchView = Widget.extend({
|
||||
groupbys.push.apply(groupbys, group_by);
|
||||
}
|
||||
});
|
||||
var intervalMappingNormalized = {};
|
||||
|
||||
_.each(this.intervalMapping, function (couple) {
|
||||
var fieldName = couple.groupby.fieldName;
|
||||
var interval = couple.interval;
|
||||
intervalMappingNormalized[fieldName] = interval;
|
||||
});
|
||||
return {
|
||||
domains: domains,
|
||||
contexts: contexts,
|
||||
groupbys: groupbys,
|
||||
intervalMapping: intervalMappingNormalized,
|
||||
};
|
||||
},
|
||||
toggle_visibility: function (is_visible) {
|
||||
@@ -517,21 +564,56 @@ var SearchView = Widget.extend({
|
||||
.$el.focus();
|
||||
},
|
||||
/**
|
||||
* @param {openerp.web.search.SearchQuery | undefined} collection Undefined if event is change
|
||||
* @param {openerp.web.search.Facet} model
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
renderFacets: function (collection, model, options) {
|
||||
renderFacets: function () {
|
||||
var self = this;
|
||||
var started = [];
|
||||
_.invoke(this.input_subviews, 'destroy');
|
||||
this.input_subviews = [];
|
||||
|
||||
var selectedGroupIds = {
|
||||
groupByCategory: [],
|
||||
filterCategory: [],
|
||||
};
|
||||
|
||||
this.query.each(function (facet) {
|
||||
var f = new FacetView(this, facet);
|
||||
|
||||
if (facet.attributes.cat === "groupByCategory") {
|
||||
selectedGroupIds.groupByCategory = selectedGroupIds.groupByCategory.concat(
|
||||
_.compact(
|
||||
_.uniq(
|
||||
_.map(facet.attributes.values, function (value) {
|
||||
var groupby = value.value;
|
||||
var groupbyDescription = _.findWhere(self.groupbysMapping, {groupby: groupby});
|
||||
if (groupbyDescription) {
|
||||
return groupbyDescription.groupId;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (facet.attributes.cat === "filterCategory") {
|
||||
selectedGroupIds.filterCategory = selectedGroupIds.filterCategory.concat(
|
||||
_.uniq(
|
||||
_.compact(
|
||||
_.map(facet.attributes.values, function (value) {
|
||||
var filter = value.value;
|
||||
var filterDescription = _.findWhere(self.filtersMapping, {filter: filter});
|
||||
if (filterDescription) {
|
||||
return filterDescription.groupId;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var f = new FacetView(this, facet, this.intervalMapping);
|
||||
started.push(f.appendTo(self.$('.o_searchview_input_container')));
|
||||
self.input_subviews.push(f);
|
||||
}, this);
|
||||
|
||||
var i = new InputView(this);
|
||||
started.push(i.appendTo(self.$('.o_searchview_input_container')));
|
||||
self.input_subviews.push(i);
|
||||
@@ -547,6 +629,12 @@ var SearchView = Widget.extend({
|
||||
// not what you expect when you just selected a filter.
|
||||
_.last(self.input_subviews).$el.focus();
|
||||
}
|
||||
if (self.groupby_menu) {
|
||||
self._unsetUnusedGroupbys(selectedGroupIds.groupByCategory);
|
||||
}
|
||||
if (self.filters_menu) {
|
||||
self._unsetUnusedFilters(selectedGroupIds.filterCategory);
|
||||
}
|
||||
});
|
||||
},
|
||||
childFocused: function () {
|
||||
@@ -584,6 +672,8 @@ var SearchView = Widget.extend({
|
||||
var context = pyeval.eval('context', item.attrs.context);
|
||||
if (context.group_by) {
|
||||
category = 'group_by';
|
||||
item.attrs.fieldName = context.group_by.split(':')[0];
|
||||
item.attrs.defaultInterval = context.group_by.split(':')[1];
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -601,7 +691,7 @@ var SearchView = Widget.extend({
|
||||
return current_group.push(new search_inputs.Filter(filter.item, self));
|
||||
}
|
||||
if (current_group.length) {
|
||||
var group = new search_inputs.FilterGroup(current_group, self);
|
||||
var group = new search_inputs.FilterGroup(current_group, self, self.intervalMapping);
|
||||
categories[current_category].push(group);
|
||||
current_group = [];
|
||||
}
|
||||
@@ -652,12 +742,12 @@ var SearchView = Widget.extend({
|
||||
attrs: {domain: filter.domain, help: filter.help},
|
||||
};
|
||||
var filterWidget = new search_inputs.Filter(filter);
|
||||
var filterGroup = new search_inputs.FilterGroup([filterWidget], self);
|
||||
var filterGroup = new search_inputs.FilterGroup([filterWidget], self, self.intervalMapping);
|
||||
var facet = filterGroup.make_facet([filterGroup.make_value(filter)]);
|
||||
self.query.add([facet], {silent: true});
|
||||
|
||||
return _.last(self.query.models);
|
||||
})
|
||||
});
|
||||
|
||||
_.each(filtersToRemove, function (filter) {
|
||||
self.query.remove(filter, {silent: true});
|
||||
@@ -684,6 +774,85 @@ var SearchView = Widget.extend({
|
||||
_getButtonsElement: function () {
|
||||
return this.$buttons;
|
||||
},
|
||||
/**
|
||||
* Create a groupby menu. Note that this method has a side effect: it
|
||||
* builds a mapping from a filter name to a 'search filter'.
|
||||
*
|
||||
* @private
|
||||
* @returns {Widget} the processed fieldsView
|
||||
*/
|
||||
_createFiltersMenu: function () {
|
||||
var self = this;
|
||||
var filters = [];
|
||||
|
||||
this.filters.forEach(function (group) {
|
||||
var groupId = _.uniqueId('__group__');
|
||||
group.filters.forEach(function (filter) {
|
||||
if (!filter.attrs.invisible) {
|
||||
var filterId = _.uniqueId('__filter__');
|
||||
filters.push({
|
||||
isActive: !!self.search_defaults[filter.attrs.name],
|
||||
description: filter.attrs.string || filter.attrs.help || filter.attrs.name || filter.attrs.domain || 'Ω',
|
||||
itemId: filterId,
|
||||
domain: filter.attrs.domain,
|
||||
groupId: groupId,
|
||||
});
|
||||
self.filtersMapping.push({filterId: filterId, filter: filter, groupId: groupId});
|
||||
}
|
||||
});
|
||||
self.groupsMapping.push({groupId: groupId, group: group, category: 'Filters'});
|
||||
});
|
||||
|
||||
return new FiltersMenu(self, filters, self.fields);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a groupby menu. Note that this method has a side effect: it
|
||||
* builds a mapping from a filter name to a 'search filter'.
|
||||
*
|
||||
* @private
|
||||
* @returns {Widget} the processed fieldsView
|
||||
*/
|
||||
_createGroupByMenu: function () {
|
||||
var self = this;
|
||||
var groupbys = [];
|
||||
|
||||
this.groupbys.forEach(function (group) {
|
||||
var groupId = _.uniqueId('__group__');
|
||||
group.filters.forEach(function (groupby) {
|
||||
if (!groupby.attrs.invisible) {
|
||||
var fieldName = groupby.attrs.fieldName;
|
||||
var defaultOptionId = groupby.attrs.defaultInterval;
|
||||
var groupbyId = _.uniqueId('__groupby__');
|
||||
groupbys.push({
|
||||
isActive: !!self.search_defaults[groupby.attrs.name],
|
||||
description: groupby.attrs.string,
|
||||
itemId: groupbyId,
|
||||
fieldName: fieldName,
|
||||
groupId: groupId,
|
||||
defaultOptionId: defaultOptionId,
|
||||
});
|
||||
if (self._fieldIsDate(fieldName)) {
|
||||
self.intervalMapping.push({groupby: groupby, interval: defaultOptionId || 'month'});
|
||||
|
||||
}
|
||||
self.groupbysMapping.push({groupbyId: groupbyId, groupby: groupby, groupId: groupId});
|
||||
}
|
||||
});
|
||||
self.groupsMapping.push({groupId: groupId, group: group, category: 'Group By'});
|
||||
group.updateIntervalMapping(self.intervalMapping);
|
||||
});
|
||||
return new GroupByMenu(this, groupbys, this.fields);
|
||||
},
|
||||
|
||||
|
||||
_fieldIsDate: function (fieldName) {
|
||||
if (_.contains(['date', 'datetime'], this.fields[fieldName].type)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes a fieldsView in place. In particular, parses its arch.
|
||||
*
|
||||
@@ -699,6 +868,154 @@ var SearchView = Widget.extend({
|
||||
fv.arch = utils.xml_to_json(doc, true);
|
||||
return fv;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string[]]} selectedGroupIds
|
||||
*/
|
||||
_unsetUnusedGroupbys: function (selectedGroupIds) {
|
||||
var groupIds = this.selectedGroupIds.groupByCategory.reduce(
|
||||
function (acc, id) {
|
||||
if (!_.contains(selectedGroupIds, id)) {
|
||||
acc.push(id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
this.selectedGroupIds.groupByCategory = selectedGroupIds;
|
||||
this.groupby_menu.unsetGroups(groupIds);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string[]]} selectedGroupIds
|
||||
*/
|
||||
_unsetUnusedFilters: function (selectedGroupIds) {
|
||||
var groupIds = this.selectedGroupIds.filterCategory.reduce(
|
||||
function (acc, id) {
|
||||
if (!_.contains(selectedGroupIds, id)) {
|
||||
acc.push(id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
this.selectedGroupIds.filterCategory = selectedGroupIds;
|
||||
this.filters_menu.unsetGroups(groupIds);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onItemToggled: function (event) {
|
||||
var group;
|
||||
if (event.data.category === 'groupByCategory') {
|
||||
var groupby = _.findWhere(this.groupbysMapping, {groupbyId: event.data.itemId}).groupby;
|
||||
group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group;
|
||||
if (event.data.optionId) {
|
||||
var interval = event.data.optionId;
|
||||
_.findWhere(this.intervalMapping, {groupby: groupby}).interval = interval;
|
||||
group.updateIntervalMapping(this.intervalMapping);
|
||||
}
|
||||
group.toggle(groupby);
|
||||
}
|
||||
if (event.data.category === 'filterCategory') {
|
||||
var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter;
|
||||
group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group;
|
||||
group.toggle(filter);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onNewGroupby: function (event) {
|
||||
var attrs = {
|
||||
context:"{'group_by':'" + event.data.fieldName + "''}",
|
||||
name: event.data.description,
|
||||
fieldName: event.data.fieldName,
|
||||
};
|
||||
|
||||
var groupby = new search_inputs.Filter({attrs: attrs}, this);
|
||||
if (event.data.optionId) {
|
||||
var interval = event.data.optionId;
|
||||
this.intervalMapping.push({groupby: groupby, interval: interval});
|
||||
}
|
||||
var group = new search_inputs.FilterGroup([groupby], this, this.intervalMapping);
|
||||
group.toggle(groupby);
|
||||
this.groupbysMapping.push({
|
||||
groupbyId: event.data.itemId,
|
||||
groupby: groupby,
|
||||
groupId: event.data.groupId,
|
||||
});
|
||||
this.groupsMapping.push({
|
||||
groupId: event.data.groupId,
|
||||
group: group,
|
||||
category: 'Group By',
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onNewFilter: function (event) {
|
||||
var self= this;
|
||||
var filter;
|
||||
var filters = [];
|
||||
var groupId;
|
||||
|
||||
_.each(event.data, function (filterDescription) {
|
||||
filter = new search_inputs.Filter(filterDescription.filter, this);
|
||||
filters.push(filter);
|
||||
self.filtersMapping.push({
|
||||
filterId: filterDescription.itemId,
|
||||
filter: filter,
|
||||
groupId: filterDescription.groupId,
|
||||
});
|
||||
// filters belong to the same group
|
||||
if (!groupId) {
|
||||
groupId = filterDescription.groupId;
|
||||
}
|
||||
});
|
||||
var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping);
|
||||
filters.forEach(function (filter) {
|
||||
group.toggle(filter, {silent: true});
|
||||
});
|
||||
this.query.trigger('reset');
|
||||
|
||||
|
||||
this.groupsMapping.push({
|
||||
groupId: groupId,
|
||||
group: group,
|
||||
category: 'Filters',
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onItemOptionChanged: function (event) {
|
||||
var group;
|
||||
if (event.data.category === 'groupByCategory') {
|
||||
var groupby = _.findWhere(this.groupbysMapping, {groupbyId: event.data.itemId}).groupby;
|
||||
var interval = event.data.optionId;
|
||||
_.findWhere(this.intervalMapping, {groupby: groupby}).interval = interval;
|
||||
group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group;
|
||||
group.updateIntervalMapping(this.intervalMapping);
|
||||
this.query.trigger('reset');
|
||||
}
|
||||
if (event.data.category === 'filterCategory') {
|
||||
var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter;
|
||||
group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group;
|
||||
group.toggle(filter);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
_.extend(SearchView, {
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
odoo.define('web.DropdownMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
// interface IItem {
|
||||
// itemId: string;
|
||||
// description: string;
|
||||
// groupId: string;
|
||||
// isActive: boolean; (optional)
|
||||
// isOpen: boolean; (optional)
|
||||
// isRemovable: boolean; (optional)
|
||||
// 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)
|
||||
// }
|
||||
|
||||
var DropdownMenu = Widget.extend({
|
||||
template: 'web.DropdownMenu',
|
||||
events: {
|
||||
'click li.o_menu_item': '_onItemClick',
|
||||
'click li.o_item_option': '_onOptionClick',
|
||||
'click li.o_menu_item span.o_submenu_switcher': '_openItemOptions',
|
||||
'click span.o_trash_button': '_onTrashClick',
|
||||
'hidden.bs.dropdown': '_onBootstrapClose',
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Widget} parent
|
||||
* @param {Object} dropdowHeader object with keys 'title' and 'icon'
|
||||
* @param {Object} items values are arrays of items (see interface IItems)
|
||||
* @param {Object} itemGenerator
|
||||
*/
|
||||
init: function (parent, dropdownHeader, items) {
|
||||
this._super(parent);
|
||||
this.dropdownCategory = dropdownHeader.category;
|
||||
this.dropdownTitle = dropdownHeader.title;
|
||||
this.dropdownIcon = dropdownHeader.icon;
|
||||
this.dropdownSymbol = dropdownHeader.symbol || 'caret';
|
||||
// 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-sm btn-default dropdown-toggle'},
|
||||
};
|
||||
this.items = _.sortBy(items, 'groupId');
|
||||
_.each(this.items, this._prepareItem.bind(this));
|
||||
},
|
||||
|
||||
start: function () {
|
||||
this.$menu = this.$('.o_dropdown_menu');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {Array[number]} groupIds
|
||||
*/
|
||||
unsetGroups: function (groupIds) {
|
||||
var self = this;
|
||||
_.each(self.items, function (item) {
|
||||
if (_.contains(groupIds, item.groupId)) {
|
||||
item.isActive = false;
|
||||
item.currentOptionId = 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('DropdownMenu.MenuItems', {widget: this});
|
||||
this.$el.find('.o_menu_item,.divider[data-removable="1"]').remove();
|
||||
this.$('ul.o_dropdown_menu').prepend($(newMenuItems));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} item
|
||||
*/
|
||||
_prepareItem: function (item) {
|
||||
item.isOpen = item.isOpen || false;
|
||||
item.isRemovable = item.isRemovable || false;
|
||||
item.hasOptions = item.options && item.options.length !== 0 ? true : false;
|
||||
item.defaultOptionId = item.hasOptions ?
|
||||
(item.defaultOptionId || item.options[0].optionId) :
|
||||
false;
|
||||
item.currentOptionId = item.isActive ? (item.currentOptionId || item.defaultOptionId ) : false;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_toggleMenuItem: function (itemId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
item.isActive = !item.isActive;
|
||||
var eventData = {
|
||||
category: this.dropdownCategory,
|
||||
itemId: itemId,
|
||||
isActive: item.isActive,
|
||||
groupId: item.groupId,
|
||||
};
|
||||
if (item.hasOptions) {
|
||||
if (item.isActive) {
|
||||
item.currentOptionId = item.defaultOptionId;
|
||||
eventData.optionId = item.currentOptionId;
|
||||
} else {
|
||||
item.currentOptionId = false;
|
||||
}
|
||||
}
|
||||
this._renderMenuItems();
|
||||
this.trigger_up('menu_item_toggled', eventData);
|
||||
},
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} itemId
|
||||
*/
|
||||
_toggleMenuOptions: function (itemId) {
|
||||
var item = _.findWhere(this.items, {itemId: itemId});
|
||||
item.isOpen = !item.isOpen;
|
||||
this._renderMenuItems();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_openItemOptions: function (event) {
|
||||
// we preventdefault the event here to avoid changing the hash # in the
|
||||
// url.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var itemId = $(event.currentTarget).data('id');
|
||||
this._toggleMenuOptions(itemId);
|
||||
},
|
||||
});
|
||||
|
||||
return DropdownMenu;
|
||||
|
||||
});
|
||||
@@ -45,7 +45,7 @@
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
&:hover > .dropdown-menu {
|
||||
&:hover .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.o_dropdown_menu {
|
||||
|
||||
> li.divider:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
.o_submenu_switcher {
|
||||
float:right;
|
||||
width: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
.o_trash_button {
|
||||
float:right;
|
||||
+ .o_submenu_switcher {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li.o_menu_item {
|
||||
position: relative;
|
||||
> a {
|
||||
color: $o-main-text-color;
|
||||
padding-right: 25px;
|
||||
&:hover {
|
||||
color: darken($o-main-text-color, 10%);
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
> a {
|
||||
color: darken($o-main-text-color, 10%);
|
||||
font-weight: bold;
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
content: "\f00c";
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_item_option {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,6 @@
|
||||
// ------------------------------------------------------------------
|
||||
// Control panel buttons
|
||||
// ------------------------------------------------------------------
|
||||
.o_graph_measures_list {
|
||||
.o_graph_measures_list, .o_graph_groupbys_list {
|
||||
@include o-selected-li;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Group By
|
||||
.o_group_by_menu {
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.o_add_group { // add button
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
|
||||
.o_submenu_switcher {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -122,105 +122,92 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Searchview dropdowns
|
||||
.o_search_options {
|
||||
// Open and closed carets in dropdowns
|
||||
.o_closed_menu {
|
||||
position: relative;
|
||||
a:before {
|
||||
@include o-caret-right;
|
||||
@include o-position-absolute($top: 50%, $left: 10px);
|
||||
margin-top: (-$caret-width-base);
|
||||
}
|
||||
}
|
||||
.o_open_menu {
|
||||
position: relative;
|
||||
a:before {
|
||||
@include o-caret-down;
|
||||
@include o-position-absolute($top: 50%, $left: 10px);
|
||||
margin-top: (-$caret-width-base);
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
.o_filters_menu {
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.o_filter_condition {
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
margin-bottom: 8px;
|
||||
.o_or_filter { // or between conditions
|
||||
display: none; // hidden for the first condition
|
||||
@include o-position-absolute($top: 7px, $left: -18px);
|
||||
}
|
||||
}
|
||||
.o_filter_condition + .o_filter_condition {
|
||||
.o_or_filter {
|
||||
display: block; // displayed from second condition
|
||||
}
|
||||
}
|
||||
.o_searchview_extended_prop_op {
|
||||
margin: 3px 0px; // vertical gap between selects
|
||||
}
|
||||
.o_searchview_extended_prop_value { // date fields: datepicker position
|
||||
.datepickerbutton {
|
||||
@include o-position-absolute(3px, -20px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o_searchview_extended_delete_prop { // delete condition button position
|
||||
@include o-position-absolute(3px, -18px);
|
||||
cursor: pointer;
|
||||
}
|
||||
.o_add_filter_menu { // apply and add buttons
|
||||
display: none; // Needed by the webclient
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
}
|
||||
|
||||
// Group By
|
||||
.o_group_by_menu {
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.divider {display: none;} // hide the divider by default (no group)
|
||||
.o_add_group { // add button
|
||||
display: none; // Needed by the webclient
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites
|
||||
.o_favorites_menu {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.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);
|
||||
margin-top: -6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o-searchview-custom-public a:after {
|
||||
font-family: FontAwesome;
|
||||
content: "\f0c0";
|
||||
margin-left: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.o_save_name { // save button
|
||||
display: none; // hidden by default
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
// Open and closed carets in dropdowns
|
||||
.o_closed_menu {
|
||||
position: relative;
|
||||
a:before {
|
||||
@include o-caret-right;
|
||||
@include o-position-absolute($top: 50%, $left: 10px);
|
||||
margin-top: (-$caret-width-base);
|
||||
}
|
||||
}
|
||||
.o_open_menu {
|
||||
position: relative;
|
||||
a:before {
|
||||
@include o-caret-down;
|
||||
@include o-position-absolute($top: 50%, $left: 10px);
|
||||
margin-top: (-$caret-width-base);
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
.o_filters_menu {
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.o_filter_condition {
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
margin-bottom: 8px;
|
||||
.o_or_filter { // or between conditions
|
||||
display: none; // hidden for the first condition
|
||||
@include o-position-absolute($top: 7px, $left: -18px);
|
||||
}
|
||||
}
|
||||
.o_filter_condition + .o_filter_condition {
|
||||
.o_or_filter {
|
||||
display: block; // displayed from second condition
|
||||
}
|
||||
}
|
||||
.o_searchview_extended_prop_op {
|
||||
margin: 3px 0px; // vertical gap between selects
|
||||
}
|
||||
.o_searchview_extended_prop_value { // date fields: datepicker position
|
||||
.datepickerbutton {
|
||||
@include o-position-absolute(3px, -20px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o_searchview_extended_delete_prop { // delete condition button position
|
||||
@include o-position-absolute(3px, -18px);
|
||||
cursor: pointer;
|
||||
}
|
||||
.o_add_filter_menu { // apply and add buttons
|
||||
display: none; // Needed by the webclient
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Favorites
|
||||
.o_favorites_menu {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
@include o-selected-li;
|
||||
|
||||
max-height: calc(100vh - 125px);
|
||||
overflow: auto;
|
||||
|
||||
.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);
|
||||
margin-top: -6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o-searchview-custom-public a:after {
|
||||
font-family: FontAwesome;
|
||||
content: "\f0c0";
|
||||
margin-left: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.o_save_name { // save button
|
||||
display: none; // hidden by default
|
||||
@include o-search-options-dropdown-custom-li;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,6 +1018,75 @@
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-name="DropdownMenu.MenuItems">
|
||||
<t t-if="widget.items.length !== 0">
|
||||
<t t-set="group" t-value="0"/>
|
||||
<t t-foreach="widget.items" t-as="item">
|
||||
<li t-if="group !== item.groupId" class="divider" data-removable="1"/>
|
||||
<li t-att-class="'o_menu_item ' + (item.isActive ? 'selected ' : '')"
|
||||
t-att-data-id="item.itemId">
|
||||
<a href="#">
|
||||
<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 class="fa fa-caret-down"></span> </span>
|
||||
</a>
|
||||
<ul t-if="item.hasOptions && item.isOpen">
|
||||
<t t-foreach="_.values(item.options)" t-as="option">
|
||||
<li t-att-class="'o_item_option ' + (item.currentOptionId === option.optionId ? 'selected': '')"
|
||||
t-att-data-item_id="item.itemId" t-att-data-option_id="option.optionId">
|
||||
<a href="#"><t t-esc="option.description"/></a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
<t t-set="group" t-value="item.groupId"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<div t-name="web.DropdownMenu" t-att-class="widget.dropdownStyle.el.class" t-att="widget.dropdownStyle.el.attrs" >
|
||||
<button t-att-class="widget.dropdownStyle.mainButton.class" data-toggle="dropdown" aria-expanded="false" tabindex="-1">
|
||||
<span t-att-class="widget.dropdownIcon"/> <t t-esc="widget.dropdownTitle"/> <span t-att-class="widget.dropdownSymbol"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_dropdown_menu" role="menu">
|
||||
<t t-call="DropdownMenu.MenuItems"/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<t t-name="GroupbyMenuGenerator">
|
||||
<li t-if="Object.keys(widget.presentedFields).length !== 0 && widget.items.length !== 0" class="o_generator_menu divider"/>
|
||||
<li t-if="Object.keys(widget.presentedFields).length !== 0" class="o_generator_menu o_add_custom_group ">
|
||||
<a href="#">Add Custom Group</a>
|
||||
</li>
|
||||
<li t-if="widget.generatorMenuIsOpen" class="o_generator_menu">
|
||||
<select class="o_input o_group_selector o_add_group">
|
||||
<t t-foreach="widget.presentedFields" t-as="field">
|
||||
<option t-att-value="field.name"><t t-esc="field.string"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</li>
|
||||
<li t-if="widget.generatorMenuIsOpen" class="o_generator_menu">
|
||||
<button type="button" class="btn btn-primary btn-sm o_apply_group o_add_group" tabindex="-1">Apply</button>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-name="FiltersMenuGenerator">
|
||||
<li class="divider"/>
|
||||
<li t-if="Object.keys(widget.fields).length !== 0" class="o_add_custom_filter o_closed_menu">
|
||||
<a href="#">Add Custom Filter</a>
|
||||
</li>
|
||||
<li class="o_add_filter_menu">
|
||||
<button class="btn btn-primary btn-sm o_apply_filter" type="button">Apply</button>
|
||||
<button class="btn btn-default btn-sm o_add_condition" type="button">
|
||||
<span class="fa fa-plus-circle"/>
|
||||
Add a condition
|
||||
</button>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
|
||||
|
||||
<div t-name="SearchView" class="o_searchview">
|
||||
<span class="o_searchview_more fa" title="Advanced Search..."/>
|
||||
<div class="o_searchview_input_container"/>
|
||||
@@ -1050,25 +1119,12 @@
|
||||
<div class="fa fa-sm fa-remove o_facet_remove"/>
|
||||
</div>
|
||||
<span t-name="SearchView.FacetView.Value">
|
||||
<t t-esc="widget.model.get('label')"/>
|
||||
<t t-esc="widget.model.get('label') + (widget.intervalLabel ? (': ' + widget.intervalLabel) : '')"/>
|
||||
</span>
|
||||
<t t-name="SearchView.autocomplete">
|
||||
<ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul>
|
||||
</t>
|
||||
<div t-name="SearchView.FilterMenu" class="btn-group o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1">
|
||||
<span class="fa fa-filter"/> Filters <span t-attf-class="#{widget.isMobile ? 'fa fa-chevron-right pull-right mt4' : 'caret'}"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_filters_menu" role="menu">
|
||||
<li class="o_add_filter o_closed_menu">
|
||||
<a href="#">Add Custom Filter</a>
|
||||
</li>
|
||||
<li class="o_add_filter_menu">
|
||||
<button class="btn btn-primary btn-sm o_apply_filter" type="button">Apply</button>
|
||||
<button class="btn btn-default btn-sm o_add_condition" type="button"><span class="fa fa-plus-circle"/> Add a condition</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<t t-name="SearchView.filters">
|
||||
<li t-foreach="widget.filters" t-as="filter" t-if="!filter.visible || filter.visible()"
|
||||
t-att-title="filter.attrs.string ? filter.attrs.help : undefined"
|
||||
@@ -1167,27 +1223,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</t>
|
||||
<div t-name="SearchView.GroupByMenu" class="btn-group o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1">
|
||||
<span class="fa fa-bars"/> Group By <span t-attf-class="#{widget.isMobile ? 'fa fa-chevron-right pull-right mt4' : 'caret'}"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_group_by_menu" role="menu">
|
||||
<li class="divider"/>
|
||||
<li class="o_add_custom_group o_closed_menu">
|
||||
<a href="#">Add custom group</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-name="GroupByMenuSelector">
|
||||
<li><select class="o_input o_add_group o_group_selector">
|
||||
<t t-foreach="groupableFields" t-as="field">
|
||||
<option t-att-data-name="field.name"><t t-esc="field.string"/></option>
|
||||
</t>
|
||||
</select></li>
|
||||
<li>
|
||||
<button type="button" class="btn btn-primary btn-sm o_add_group o_select_group" tabindex="-1">Apply</button>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown">
|
||||
<button class="o_dropdown_toggler_btn btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false" tabindex="-1">
|
||||
<span class="fa fa-star"/> Favorites <span t-attf-class="#{widget.isMobile ? 'fa fa-chevron-right pull-right mt4' : 'caret'}"/>
|
||||
|
||||
@@ -425,9 +425,8 @@ QUnit.module('relational_fields', {
|
||||
|
||||
assert.strictEqual($('tr.o_data_row').length, 9, "should display 9 records");
|
||||
|
||||
$('.o_searchview_more').click(); // Magnifying class for more filters
|
||||
$('button:contains(Filters)').click();
|
||||
$('.o_add_filter').click(); // Add a custom filter, datetime field is selected
|
||||
$('.o_add_custom_filter:visible').click(); // Add a custom filter, datetime field is selected
|
||||
assert.strictEqual($('li.o_filter_condition select.o_searchview_extended_prop_field').val(), 'datetime',
|
||||
"datetime field should be selected");
|
||||
$('.o_apply_filter').click();
|
||||
@@ -11132,11 +11131,12 @@ QUnit.module('relational_fields', {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
|
||||
form.$('.o_field_many2one input').click();
|
||||
$dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click(); // Open Search More
|
||||
|
||||
$('.modal .o_searchview_more').click(); // Magnifying class for more filters
|
||||
$('.modal .o_search_options .fa-bars').click();
|
||||
$('.modal .o_search_options .o_group_by_menu a:contains(Bar)').click(); // group by 'Bar'
|
||||
|
||||
$('.modal .o_group_header:first').click();
|
||||
|
||||
@@ -248,7 +248,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('getContext correctly returns mode, measure and groupbys', function (assert) {
|
||||
QUnit.test('getContext correctly returns mode, measure, groupbys and interval mapping', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
@@ -265,6 +265,7 @@ QUnit.module('Views', {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: '__count__',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
}, "context should be correct");
|
||||
|
||||
graph.$buttons.find('li[data-field="foo"] a').click(); // change measure
|
||||
@@ -275,6 +276,7 @@ QUnit.module('Views', {
|
||||
graph_mode: 'bar',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
}, "context should be correct");
|
||||
|
||||
graph.$buttons.find('button[data-mode="line"]').click(); // change mode
|
||||
@@ -285,6 +287,7 @@ QUnit.module('Views', {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id'],
|
||||
graph_intervalMapping: {},
|
||||
}, "context should be correct");
|
||||
|
||||
graph.update({groupBy: ['product_id', 'color_id']}); // change groupbys
|
||||
@@ -295,6 +298,7 @@ QUnit.module('Views', {
|
||||
graph_mode: 'line',
|
||||
graph_measure: 'foo',
|
||||
graph_groupbys: ['product_id', 'color_id'],
|
||||
graph_intervalMapping: {},
|
||||
}, "context should be correct");
|
||||
|
||||
graph.destroy();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
odoo.define('web.search_filters_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FiltersMenu = require('web.FiltersMenu');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
function createFiltersMenu(filters, fields, params) {
|
||||
params = params || {};
|
||||
var target = params.debug ? document.body : $('#qunit-fixture');
|
||||
var menu = new FiltersMenu(null, filters, fields);
|
||||
testUtils.addMockEnvironment(menu, params);
|
||||
menu.appendTo(target);
|
||||
return menu;
|
||||
}
|
||||
|
||||
QUnit.module('FiltersMenu', {
|
||||
beforeEach: function () {
|
||||
this.filters = [
|
||||
{
|
||||
isActive: false,
|
||||
description: 'some filter',
|
||||
domain: '',
|
||||
name: 'red',
|
||||
groupId: 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(4);
|
||||
|
||||
var filterMenu = createFiltersMenu([], this.fields);
|
||||
filterMenu.$('span.fa-filter').click();
|
||||
assert.strictEqual(filterMenu.$('.divider').length, 1);
|
||||
assert.ok(!filterMenu.$('.divider').is(':visible'));
|
||||
assert.strictEqual(filterMenu.$('li').length, 3,
|
||||
'should have 3 li: a hidden divider, a add custom filter item, a apply button + add condition button');
|
||||
assert.strictEqual(filterMenu.$('li.o_add_custom_filter.o_closed_menu').length, 1);
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with a filter', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var filterMenu = createFiltersMenu(this.filters, this.fields);
|
||||
assert.strictEqual(filterMenu.$('li').length, 5,
|
||||
'should have 4 li: a hidden, separator, a filter, a separator, a add custom filter item, a apply button + add condition button');
|
||||
assert.strictEqual(filterMenu.$('li.o_add_custom_filter.o_closed_menu').length, 1);
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on add custom filter opens the submenu', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var filterMenu = createFiltersMenu([], this.fields);
|
||||
// open menu dropdown
|
||||
filterMenu.$('span.fa-filter').click();
|
||||
// open add custom filter submenu
|
||||
filterMenu.$('li.o_add_custom_filter').click();
|
||||
assert.ok(filterMenu.$('li.o_add_custom_filter').hasClass('o_open_menu'));
|
||||
assert.ok(filterMenu.$('li.o_add_filter_menu').is(':visible'));
|
||||
assert.strictEqual(filterMenu.$('li').length, 4,
|
||||
'should have 3 li: a hidden divider, 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 = createFiltersMenu([], this.fields);
|
||||
// open menu dropdown and custom filter submenu
|
||||
filterMenu.$('span.fa-filter').click();
|
||||
filterMenu.$('li.o_add_custom_filter').click();
|
||||
|
||||
// remove the current unique proposition
|
||||
filterMenu.$('.o_searchview_extended_delete_prop').click();
|
||||
|
||||
assert.ok(filterMenu.$('.o_apply_filter').attr('disabled'));
|
||||
|
||||
assert.strictEqual(filterMenu.$('li').length, 3,
|
||||
'should have 3 li: a hidden separator, 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 = createFiltersMenu([], this.fields);
|
||||
|
||||
// open menu dropdown and custom filter submenu, remove existing prop
|
||||
filterMenu.$('span.fa-filter').click();
|
||||
filterMenu.$('li.o_add_custom_filter').click();
|
||||
filterMenu.$('.o_searchview_extended_delete_prop').click();
|
||||
|
||||
// read a proposition
|
||||
filterMenu.$('.o_add_condition').click();
|
||||
|
||||
assert.ok(!filterMenu.$('.o_apply_filter').attr('disabled'));
|
||||
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('adding a simple filter works', function (assert) {
|
||||
assert.expect(3);
|
||||
delete this.fields.date_field;
|
||||
|
||||
var filterMenu = createFiltersMenu([], this.fields);
|
||||
|
||||
// open menu dropdown and custom filter submenu, remove existing prop
|
||||
filterMenu.$('span.fa-filter').click();
|
||||
filterMenu.$('li.o_add_custom_filter').click();
|
||||
|
||||
// click on apply to activate filter
|
||||
filterMenu.$('.o_apply_filter').click();
|
||||
|
||||
assert.ok(filterMenu.$('.o_add_custom_filter').hasClass('o_closed_menu'));
|
||||
assert.strictEqual(filterMenu.$('.o_filter_condition').length, 0);
|
||||
assert.strictEqual(filterMenu.$('.divider:visible').length, 1,
|
||||
'there should be a separator between filters and add custom filter menu');
|
||||
|
||||
filterMenu.destroy();
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
odoo.define('web.search_groupby_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var GroupByMenu = require('web.GroupByMenu');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
function createGroupByMenu(groupbys, fields, params) {
|
||||
params = params || {};
|
||||
var target = params.debug ? document.body : $('#qunit-fixture');
|
||||
var menu = new GroupByMenu(null, groupbys, fields);
|
||||
testUtils.addMockEnvironment(menu, params);
|
||||
menu.appendTo(target);
|
||||
return menu;
|
||||
}
|
||||
|
||||
QUnit.module('GroupByMenu', {
|
||||
beforeEach: function () {
|
||||
this.groupbys = [
|
||||
{
|
||||
isActive: false,
|
||||
description: 'some group by',
|
||||
fieldName: 'fieldname',
|
||||
itemId: 'red',
|
||||
groupId: 1,
|
||||
},
|
||||
];
|
||||
this.fields = {
|
||||
fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test('simple rendering', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var groupByMenu = createGroupByMenu(this.groupbys, this.fields);
|
||||
groupByMenu.$('button:first').click();
|
||||
assert.strictEqual(groupByMenu.$('li').length, 2, 'should have 2 li');
|
||||
assert.strictEqual(groupByMenu.$('li:nth-child(2)').text().trim(), 'some group by',
|
||||
'should have proper filter name');
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with no filter and no field', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var groupByMenu = createGroupByMenu([], {});
|
||||
groupByMenu.$('button:first').click();
|
||||
assert.strictEqual(groupByMenu.$('li').length, 0, 'should have 0 li');
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with no filter but fields', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var groupByMenu = createGroupByMenu(
|
||||
[],
|
||||
{fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}
|
||||
);
|
||||
groupByMenu.$('button:first').click();
|
||||
assert.strictEqual(groupByMenu.$('li').length, 1, 'should have 1 li');
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on add custom group toggle group selector', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var groupByMenu = createGroupByMenu([], {fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}});
|
||||
groupByMenu.$('button:first').click();
|
||||
var selector = groupByMenu.$('select.o_group_selector');
|
||||
assert.ok(!selector.is(":visible"), 'should be invisible');
|
||||
groupByMenu.$('.o_add_custom_group a').click();
|
||||
selector = groupByMenu.$('select.o_group_selector');
|
||||
assert.ok(selector.is(":visible"), 'should be visible');
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('select a group using the group selector add properly add that group to menu', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var groupByMenu = createGroupByMenu(
|
||||
[],
|
||||
{
|
||||
fieldName: {sortable: true, name: 'candlelight', string: 'Candlelight', type: 'boolean'},
|
||||
}
|
||||
);
|
||||
groupByMenu.$('button:first').click();
|
||||
groupByMenu.$('li.o_add_custom_group a').click();
|
||||
assert.strictEqual(groupByMenu.$('select').val(), 'fieldName',
|
||||
'the select value should be "fieldName"');
|
||||
groupByMenu.$('button.o_apply_group').click();
|
||||
assert.strictEqual(groupByMenu.$('li.o_menu_item.selected').length, 1, 'there should be a groupby selected');
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on groupby filter should activate it', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var groupByMenu = createGroupByMenu(this.groupbys,
|
||||
{fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}, {
|
||||
intercepts: {
|
||||
menu_item_toggled: function(ev) {
|
||||
assert.strictEqual(ev.data.itemId, 'red');
|
||||
assert.strictEqual(ev.data.isActive, true);
|
||||
},
|
||||
},
|
||||
});
|
||||
groupByMenu.$('button:first').click();
|
||||
assert.ok(!groupByMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
groupByMenu.$('.o_menu_item a').first().click();
|
||||
assert.ok(groupByMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(groupByMenu.$('.o_menu_item:first').is(':visible'),
|
||||
'group by filter should still be visible');
|
||||
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on groupby filter should not change url', function (assert) {
|
||||
assert.expect(0);
|
||||
|
||||
var groupByMenu = createGroupByMenu(this.groupbys,
|
||||
{fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}
|
||||
);
|
||||
groupByMenu.$el.click(function(event) {
|
||||
// we do not want a click to get out and change the url, for example
|
||||
throw new Error('No click should get out of the groupby menu');
|
||||
});
|
||||
groupByMenu.$('.o_menu_item a').first().click();
|
||||
|
||||
groupByMenu.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
odoo.define('web.search_view_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
|
||||
QUnit.module('Search View', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
date_field: {string: "Date", type: "date", store: true, sortable: true},
|
||||
Birthday: {string: "Birthday", type: "date", store: true, sortable: true},
|
||||
foo: {string: "Foo", type: "char"},
|
||||
bar: {string: "Bar", type: "many2one", relation: 'partner'},
|
||||
float_field: {string: "Float", type: "float"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", Birthday: "1983-07-15", float_field: 1},
|
||||
{id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", Birthday: "1982-06-04",float_field: 2},
|
||||
{id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", Birthday: "1985-09-13",float_field: 1.618},
|
||||
{id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", Birthday: "1983-05-05",float_field: -1},
|
||||
{id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", Birthday: "1800-01-01",float_field: 13},
|
||||
],
|
||||
},
|
||||
pony: {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
},
|
||||
records: [
|
||||
{id: 4, name: 'Twilight Sparkle'},
|
||||
{id: 6, name: 'Applejack'},
|
||||
{id: 9, name: 'Fluttershy'}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
this.actions = [{
|
||||
id: 1,
|
||||
name: 'Partners Action 1',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Partners Action 2',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
search_view_id: [2, 'search'],
|
||||
}, {
|
||||
id: 3,
|
||||
name: 'Partners Action 3',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'kanban']],
|
||||
search_view_id: [2, 'search'],
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Partners Action 4',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'graph']],
|
||||
search_view_id: [3, 'search'],
|
||||
}, {
|
||||
id: 5,
|
||||
name: 'Partners Action 5',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[2, 'list']],
|
||||
search_view_id: [4, 'search'],
|
||||
}
|
||||
];
|
||||
|
||||
this.archs = {
|
||||
// list views
|
||||
'partner,false,list': '<tree><field name="foo"/></tree>',
|
||||
'partner,2,list': '<tree><field name="foo"/></tree>',
|
||||
|
||||
// kanban views
|
||||
'partner,false,kanban': '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click"><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
|
||||
// graph views
|
||||
'partner,false,graph': '<graph>' +
|
||||
'<field name="date_field" type="row" interval="day"/>' +
|
||||
'<field name="float_field" type="measure"/>' +
|
||||
'</graph>',
|
||||
|
||||
// search views
|
||||
'partner,false,search': '<search>'+
|
||||
'<field name="foo" string="Foo"/>' +
|
||||
'<filter string="candle" name="itsName" context="{\'group_by\': \'foo\'}"/>' +
|
||||
'</search>',
|
||||
'partner,2,search': '<search>'+
|
||||
'<field name="date_field" string="Date"/>' +
|
||||
'<filter string="Date" name="coolName" context="{\'group_by\': \'date_field\'}"/>' +
|
||||
'</search>',
|
||||
'partner,3,search': '<search>'+
|
||||
'<field name="date_field" string="Date"/>' +
|
||||
'<filter string="float" name="positive" domain="[(\'float_field\', \'>=\', 0)]"/>' +
|
||||
'</search>',
|
||||
'partner,4,search': '<search>'+
|
||||
'<field name="date_field" string="Date"/>' +
|
||||
'<filter string="Date" name="coolName" context="{\'group_by\': \'date_field:day\'}"/>' +
|
||||
'</search>',
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('Groupby Menu');
|
||||
|
||||
QUnit.test('click on groupby filter adds a facet', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
$('span.fa-bars').prev().click();
|
||||
$('li.o_menu_item a').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').text().trim(), 'candle',
|
||||
'should have a facet with candle name');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('remove a "Group By" facet properly unchecks groupbys in groupby menu', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
$('span.fa-bars').prev().click();
|
||||
$('li.o_menu_item a').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').text().trim(), 'candle',
|
||||
'should have a facet with candle name');
|
||||
$('.o_facet_remove:first').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0,
|
||||
'there should be no facet');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('change option of a "Group By" does not remove groupy in facet "Group By"', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(2);
|
||||
$('span.fa-bars').click();
|
||||
$('.o_submenu_switcher').click();
|
||||
$('.o_item_option:first').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1,
|
||||
'should have a facet');
|
||||
$('.o_item_option:nth-child(2)').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1,
|
||||
'should have a facet');
|
||||
$('.o_item_option:nth-child(2)').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0,
|
||||
'should have no facet');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('select and unselect quickly groupby does not crash', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(3);
|
||||
$('span.fa-bars').click();
|
||||
$('.o_menu_item:first').click();
|
||||
$('.o_menu_item:first').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0,
|
||||
'should have a facet');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('groupby selected within graph subview are not deleted when modifying search view content', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.actions[3].flags = {isEmbedded: true};
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(4);
|
||||
$('.o_graph_buttons div.o_graph_groupbys_menu > button').click();
|
||||
$('.o_graph_buttons div.o_graph_groupbys_menu .o_menu_item').click();
|
||||
assert.ok(!$('.o_graph_buttons div.o_graph_groupbys_menu .o_menu_item').hasClass('selected'),
|
||||
'groupby should be unselected');
|
||||
$('.o_search_options button span.fa-filter').click();
|
||||
$('.o_filters_menu .o_menu_item a').click();
|
||||
assert.ok(!$('.o_graph_buttons div.o_graph_groupbys_menu .o_menu_item').hasClass('selected'),
|
||||
'groupby should be still unselected');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('group by a date field using interval works', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(5);
|
||||
// open menu 'Group By'
|
||||
$('.o_search_options .fa-bars').click();
|
||||
// Activate the groupby 'Date'
|
||||
$('.o_group_by_menu .o_menu_item').click();
|
||||
// by default, data should be grouped by the field 'Date' using the interval 'day'
|
||||
assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Day');
|
||||
assert.strictEqual($('.o_content tr.o_group_header').length, 5);
|
||||
// open submenu with interval options
|
||||
$('.o_group_by_menu .o_menu_item .o_submenu_switcher').click();
|
||||
// select option 'month'
|
||||
$('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click();
|
||||
// data should be grouped by the field 'Date' using the interval 'month'
|
||||
assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Month');
|
||||
assert.strictEqual($('.o_content tr.o_group_header').length, 3);
|
||||
// deactivate option 'month'
|
||||
$('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click();
|
||||
// no groupby is applied
|
||||
assert.strictEqual($('div.o_facet_values span').length, 0);
|
||||
// open 'Add custom Groupby' menu
|
||||
$('.o_group_by_menu .o_add_custom_group a').click();
|
||||
// click on 'Apply' button
|
||||
$('.o_group_by_menu .o_generator_menu button').click();
|
||||
// data should be grouped by the field 'Birthday' using the interval 'month'
|
||||
assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Month');
|
||||
assert.strictEqual($('.o_content tr.o_group_header').length, 5);
|
||||
// open submenu with interval options
|
||||
$('.o_group_by_menu .o_menu_item .o_submenu_switcher').eq(1).click();
|
||||
// select option 'year'
|
||||
$('.o_group_by_menu .o_menu_item .o_item_option').eq(9).click();
|
||||
// data should be grouped by the field 'Birthday' using the interval 'year'
|
||||
assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Year');
|
||||
assert.strictEqual($('.o_content tr.o_group_header').length, 4);
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('Filters Menu');
|
||||
|
||||
QUnit.test('add a custom filter works', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
$('span.fa-filter').click();
|
||||
$('li.o_add_custom_filter').click();
|
||||
$('.o_apply_filter').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').text().trim(), 'ID is \"0\"',
|
||||
'should have a facet with candle name');
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('deactivate a new custom filter works', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
$('span.fa-filter').click();
|
||||
$('li.o_add_custom_filter').click();
|
||||
$('.o_apply_filter').click();
|
||||
$('li.o_menu_item').click();
|
||||
assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0,
|
||||
'no facet should be in the search view');
|
||||
actionManager.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
odoo.define('web.dropdown_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var DropdownMenu = require('web.DropdownMenu');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
function createDropdownMenu(dropdownTitle, groups, params) {
|
||||
params = params || {};
|
||||
var target = params.debug ? document.body : $('#qunit-fixture');
|
||||
var menu = new DropdownMenu(null, dropdownTitle, groups);
|
||||
testUtils.addMockEnvironment(menu, params);
|
||||
menu.appendTo(target);
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
||||
QUnit.module('Web', {
|
||||
beforeEach: function () {
|
||||
this.items = [
|
||||
{
|
||||
isActive: false,
|
||||
isOpen: false,
|
||||
description: 'Some Item',
|
||||
itemId: 1,
|
||||
groupId: 1,
|
||||
},
|
||||
{
|
||||
isActive: true,
|
||||
description: 'Some other Item',
|
||||
itemId: 2,
|
||||
options: [],
|
||||
isRemovable: true,
|
||||
groupId: 2,
|
||||
},
|
||||
];
|
||||
this.dropdownHeader = {
|
||||
title: "Menu",
|
||||
icon: "fa fa-bars",
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('DropdownMenu');
|
||||
|
||||
QUnit.test('simple rendering', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items);
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 4, 'should have 4 li counting the dividers');
|
||||
assert.strictEqual(dropdownMenu.$('li:nth-child(2)').text().trim(), 'Some Item',
|
||||
'first li should have "Some Item" description');
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on an item should toggle item', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var eventNumber = 0;
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items, {
|
||||
intercepts: {
|
||||
menu_item_toggled: function(ev) {
|
||||
eventNumber++;
|
||||
assert.strictEqual(ev.data.itemId, 1);
|
||||
if (eventNumber === 1) {
|
||||
assert.strictEqual(ev.data.isActive, true);
|
||||
} else {
|
||||
assert.strictEqual(ev.data.isActive, false);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
dropdownMenu.$('button:first').click();
|
||||
assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
dropdownMenu.$('.o_menu_item a').first().click();
|
||||
assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(dropdownMenu.$('.o_menu_item:first').is(':visible'),
|
||||
'item should still be visible');
|
||||
dropdownMenu.$('.o_menu_item a').first().click();
|
||||
assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(dropdownMenu.$('.o_menu_item:first').is(':visible'),
|
||||
'item should still be visible');
|
||||
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on an item should not change url', function (assert) {
|
||||
assert.expect(0);
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items);
|
||||
dropdownMenu.$el.click(function() {
|
||||
// we do not want a click to get out and change the url, for example
|
||||
throw new Error('No click should get out of the dropdown menu');
|
||||
});
|
||||
dropdownMenu.$('.o_menu_item a').first().click();
|
||||
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('options rendering', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.items[0].options = [{optionId: 1, description: "First Option"}, {optionId: 2, description: "Second Option"}];
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items);
|
||||
// open dropdown
|
||||
dropdownMenu.$('button:first').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 4);
|
||||
dropdownMenu.$('span.fa-caret-down').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 6);
|
||||
dropdownMenu.$('span.fa-caret-down').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 4);
|
||||
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('close menu closes also submenu', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.items[0].options = [{optionId: 1, description: "First Option"}, {optionId: 2, description: "Second Option"}];
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items);
|
||||
dropdownMenu.$('button:first').click();
|
||||
dropdownMenu.$('span.fa-caret-down').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 6);
|
||||
dropdownMenu.$('button:first').click();
|
||||
dropdownMenu.$('button:first').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 4);
|
||||
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on an option should toggle options and item states properly', function (assert) {
|
||||
assert.expect(22);
|
||||
|
||||
this.items[0].options = [{optionId: 1, description: "First Option"}, {optionId: 2, description: "Second Option"}];
|
||||
|
||||
var eventNumber = 0;
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items, {
|
||||
intercepts: {
|
||||
menu_item_toggled: function(ev) {
|
||||
eventNumber++;
|
||||
if (eventNumber === 1) {
|
||||
assert.strictEqual(ev.data.itemId, 1);
|
||||
assert.strictEqual(ev.data.isActive, true);
|
||||
assert.strictEqual(ev.data.optionId, 1);
|
||||
} else {
|
||||
assert.strictEqual(ev.data.itemId, 1);
|
||||
assert.strictEqual(ev.data.isActive, false);
|
||||
assert.strictEqual(ev.data.optionId, false);
|
||||
}
|
||||
},
|
||||
item_option_changed: function(ev) {
|
||||
if (eventNumber === 1) {
|
||||
assert.strictEqual(ev.data.itemId, 1);
|
||||
assert.strictEqual(ev.data.isActive, true);
|
||||
assert.strictEqual(ev.data.optionId, 2);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
dropdownMenu.$('button:first').click();
|
||||
dropdownMenu.$('span.fa-caret-down').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 6);
|
||||
assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected'));
|
||||
dropdownMenu.$('li.o_item_option:first').click();
|
||||
assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(dropdownMenu.$('.o_item_option:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected'));
|
||||
dropdownMenu.$('li.o_item_option:nth-child(2)').click();
|
||||
assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected'));
|
||||
assert.ok(dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected'));
|
||||
dropdownMenu.$('li.o_item_option:nth-child(2)').click();
|
||||
assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected'));
|
||||
assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected'));
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('trash an item should be possible', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items, {
|
||||
intercepts: {
|
||||
menu_item_deleted: function(ev) {
|
||||
assert.strictEqual(ev.data.itemId, 2);
|
||||
},
|
||||
},
|
||||
});
|
||||
dropdownMenu.$('button:first').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 4);
|
||||
dropdownMenu.$('span.o_trash_button').click();
|
||||
assert.strictEqual(dropdownMenu.$('li').length, 2);
|
||||
dropdownMenu.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -156,6 +156,8 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/web_calendar.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view_mobile.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_groupby_menu.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/dropdown_menu.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view_extra.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/data_export.scss"/>
|
||||
|
||||
@@ -212,11 +214,13 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/user_menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/search_view.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/search_view_mobile.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/search_view_mobile.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/search_filters.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/search_inputs.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/favorites_menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/groupby_menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/groupby_menu_interface_mixin.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/dropdown_menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/search/filters_menu.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/apps.js"></script>
|
||||
|
||||
@@ -522,6 +526,10 @@
|
||||
<script type="text/javascript" src="/web/static/tests/views/basic_model_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/kanban_model_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/view_dialogs_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/search_filters_menu_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/search_groupby_menu_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/widgets/dropdown_menu_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/search_view_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/registry_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/pyeval_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/core/class_tests.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user