[IMP] web: new TimeRangeMenu widget in search view

This commit introduces a new widget 'TimeRangeMenu' used in
the search view. It allows a user to easily:

	- search data with a given date or datetime field value
		in a specified period of time

	- compare the data of interest with data of the previous
		period or the previous year.

The time range menu is available in the search view when one
of the four following view types is instantiated:
graph, pivot, cohort, or dashboard.

Task Id: 1835644
This commit is contained in:
Mathieu Duckerts-Antoine
2018-08-14 12:30:51 +02:00
parent 3523694b6c
commit 333ac27cd5
15 changed files with 676 additions and 64 deletions
@@ -528,15 +528,22 @@ ActionManager.include({
group_by_seq: groupbys || [],
eval_context: this.userContext,
});
var groupBy = results.group_by.length ?
results.group_by :
(action.context.group_by || []);
groupBy = (typeof groupBy === 'string') ? [groupBy] : groupBy;
if (results.error) {
throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
var groupBy = results.group_by.length ? results.group_by : (action.context.group_by || []);
var context = _.omit(results.context, 'time_ranges');
return {
context: results.context,
context: context,
domain: results.domain,
groupBy: (typeof groupBy === 'string') ? [groupBy] : groupBy,
groupBy: groupBy,
};
},
/**
@@ -170,7 +170,7 @@ var ControlPanel = Widget.extend({
// Update the searchview and switch buttons
if (status.searchview || options.clear) {
this._update_search_view(status.searchview, status.search_view_hidden, status.groupable);
this._update_search_view(status.searchview, status.search_view_hidden, status.groupable, status.enableTimeRangeMenu);
}
if (status.active_view_selector) {
this._update_switch_buttons(status.active_view_selector);
@@ -266,12 +266,15 @@ var ControlPanel = Widget.extend({
* @param {boolean} [groupable] visibility of the groupable menu (only
* relevant if searchview is visible)
*/
_update_search_view: function (searchview, isHidden, groupable) {
_update_search_view: function (searchview, isHidden, groupable, enableTimeRangeMenu) {
if (searchview) {
searchview.toggle_visibility(!isHidden);
if (groupable !== undefined){
searchview.groupby_menu.do_toggle(groupable);
}
if (enableTimeRangeMenu !== undefined){
searchview.displayTimeRangeMenu(enableTimeRangeMenu);
}
}
this.nodes.$searchview.toggle(!isHidden);
this.$el.toggleClass('o_breadcrumb_full', !!isHidden);
@@ -0,0 +1,83 @@
odoo.define('web.dataComparisonUtils', function (require) {
"use strict";
var fieldUtils = require('web.field_utils');
/**
* @param {Number} value
* @param {Number} comparisonValue
* @returns {Object}
*/
function computeVariation (value, comparisonValue) {
var magnitude;
var signClass;
if (!isNaN(value) && !isNaN(comparisonValue)) {
if (comparisonValue === 0) {
if (value === 0) {
magnitude = 0;
} else if (value > 0){
magnitude = 1;
} else {
magnitude = -1;
}
} else {
magnitude = Math.sign(comparisonValue) * (value - comparisonValue) / comparisonValue;
}
if (magnitude > 0) {
signClass = ' o_positive';
} else if (magnitude < 0) {
signClass = ' o_negative';
} else if (magnitude === 0) {
signClass = ' o_null';
}
return {magnitude: magnitude, signClass: signClass};
} else {
return {magnitude: NaN};
}
}
/**
* @param {Object} variation
* @param {Object} field
* @param {Object} options
* @returns {Object}
*/
function renderVariation (variation, field, options) {
var $variation;
if (!isNaN(variation.magnitude)) {
$variation = $('<div>', {class: 'o_variation' + variation.signClass}).html(
fieldUtils.format.percentage(variation.magnitude, field, options
));
} else {
$variation = $('<div>', {class: 'o_variation'}).html('-');
}
return $variation;
}
/**
* @param {JQuery} $node
* @param {Number} value
* @param {Number} comparisonValue
* @param {Object} variation (with key 'magnitude' and 'signClass')
* @param {function} formatter
* @param {Object} field
* @param {Object} options
* @returns {Object}
*/
function renderComparison ($node, value, comparisonValue, variation, formatter, field, options) {
var $variation = renderVariation(variation, field, options);
$node.append($variation);
if (!isNaN(variation.magnitude)) {
$node.append(
$('<div>', {class: 'o_comparison'})
.html(formatter(value, field, options) + ' vs ' + formatter(comparisonValue, field, options))
);
}
}
return {
computeVariation: computeVariation,
renderComparison: renderComparison
};
});
+80 -31
View File
@@ -234,78 +234,127 @@ var Domain = collections.Tree.extend({
* @param {string} period
* @param {string} type ('date' or 'datetime')
* @param {boolean} forTooltip indicates if domain is used to print a tooltip
* @param {string} comparisonPeriod
* @returns {string} a domain in string form
*/
constructDomain: function (fieldName, period, type, forTooltip) {
constructDomain: function (fieldName, period, type, forTooltip, comparisonPeriod) {
var leftBoundaryParams, rightBoundaryParams;
var offsetPeriodParams;
var t = forTooltip || false;
function makeInterval () {
switch (comparisonPeriod) {
case 'previous_period':
_.map(offsetPeriodParams, function (value, key) {
if (_.isNumber(value)) {
leftBoundaryParams[key] = value + (leftBoundaryParams[key] || 0);
} else {
leftBoundaryParams[key] = value + ' + ' + (leftBoundaryParams[key] || 0);
}
});
_.map(offsetPeriodParams, function (value, key) {
if (_.isNumber(value)) {
rightBoundaryParams[key] = value + (rightBoundaryParams[key] || 0);
} else {
rightBoundaryParams[key] = value + ' + ' + (rightBoundaryParams[key] || 0);
}
});
break;
case 'previous_year':
leftBoundaryParams.years = leftBoundaryParams.years ? leftBoundaryParams.years-- : -1;
rightBoundaryParams.years = rightBoundaryParams.years ? rightBoundaryParams.years-- : -1;
break;
}
var stringifyParams = function (value, key) {
return key + '=' + value;
};
var leftBoundaryStringifyParams = _.map(leftBoundaryParams, stringifyParams).join(', ');
var rightBoundaryStringifyParams = _.map(rightBoundaryParams, stringifyParams).join(', ');
if (type === 'date') {
return "['&'," +
"('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryParams + ")).strftime('%Y-%m-%d'))," +
"('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryParams + ")).strftime('%Y-%m-%d'))"+
"('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))," +
"('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))"+
"]";
}
else {
return "['&'," +
"('" + fieldName + "', '>=', " +
"(datetime.datetime.combine(context_today() + relativedelta(" + leftBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," +
"(datetime.datetime.combine(context_today() + relativedelta(" + leftBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," +
"('" + fieldName + "', '<', " +
"(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+
"(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+
"]";
}
}
switch (period) {
case 'today':
leftBoundaryParams = "";
rightBoundaryParams = t ? "" : "days=1";
leftBoundaryParams = {};
rightBoundaryParams = t ? {} : {days: 1};
offsetPeriodParams = {days: -1};
return makeInterval();
case 'this_week':
leftBoundaryParams = "weeks=-1, days=1, weekday=0";
rightBoundaryParams = t ? "weekday=6" : "days=1, weekday=0";
leftBoundaryParams = {weeks: -1, days: 1, weekday: 0};
rightBoundaryParams = t ? {weekday: 6} : {days: 1, weekday: 0};
offsetPeriodParams = {weeks: -1};
return makeInterval();
case 'this_month':
leftBoundaryParams = "day=1";
rightBoundaryParams = (t ? "days=-1, " : "") + "day=1, months=1";
leftBoundaryParams = {day: 1};
rightBoundaryParams = _.extend(t ? {days: -1} : {},
{day: 1, months: 1});
offsetPeriodParams = {months: -1};
return makeInterval();
case 'this_quarter':
leftBoundaryParams = "months= - (context_today().month - 1) % 3, day=1";
rightBoundaryParams = (t ? "days=-1, " : "") + "months= 3 - (context_today().month -1 ) % 3, day=1";
leftBoundaryParams = {months: '- (context_today().month - 1) % 3', day: 1};
rightBoundaryParams = _.extend(t ? {days: -1} : {},
{months: '3 - (context_today().month - 1) % 3', day: 1});
offsetPeriodParams = {months: -3};
return makeInterval();
case 'this_year':
leftBoundaryParams = "month=1, day=1";
rightBoundaryParams = (t ? "days=-1, " : "") + "month=1, day=1, years=1";
leftBoundaryParams = {month: 1, day: 1};
rightBoundaryParams = _.extend(t ? {days: -1} : {},
{month: 1, day: 1, years: 1});
offsetPeriodParams = {years: -1};
return makeInterval();
case 'yesterday':
leftBoundaryParams = "days=-1";
rightBoundaryParams = (t ? "days=-1" : "");
leftBoundaryParams = {days: -1};
rightBoundaryParams = (t ? {days: -1} : {});
offsetPeriodParams = {days: -1};
return makeInterval();
case 'last_week':
leftBoundaryParams = "weeks=-2, days=1, weekday=0";
rightBoundaryParams = t ? "weeks=-1, weekday=6" : "weeks=-1, days=1, weekday=0";
leftBoundaryParams = {weeks: -2, days: 1, weekday: 0};
rightBoundaryParams = t ? {weeks: -1, weekday: 6} : {weeks: -1, days: 1, weekday: 0};
offsetPeriodParams = {weeks: -1};
return makeInterval();
case 'last_month':
leftBoundaryParams = "months=-1, day=1";
rightBoundaryParams = (t ? "days=-1 , " : "") + "day=1";
leftBoundaryParams = {months: -1, day: 1};
rightBoundaryParams = _.extend(t ? {days: -1} : {}, {day: 1});
offsetPeriodParams = {months: -1};
return makeInterval();
case 'last_quarter':
leftBoundaryParams = "months= - 3 - (context_today().month - 1) % 3, day=1";
rightBoundaryParams = (t ? "days=-1 , " : "") + "months= - (context_today().month - 1) % 3, day=1";
leftBoundaryParams = {months: '- 3 - (context_today().month - 1) % 3', day: 1};
rightBoundaryParams = _.extend(t ? {days: -1} : {},
{months: '- (context_today().month - 1) % 3', day: 1});
offsetPeriodParams = {months: -3};
return makeInterval();
case 'last_year':
leftBoundaryParams = "month=1, day=1, years=-1";
rightBoundaryParams = (t ? "days=-1 , " : "") + "month=1, day=1";
leftBoundaryParams = {month: 1, day: 1, years: -1};
rightBoundaryParams = _.extend(t ? {days: -1} : {}, {month: 1, day: 1});
offsetPeriodParams = {years: -1};
return makeInterval();
case 'last_7_days':
leftBoundaryParams = "days=-7";
rightBoundaryParams = t ? "days=-1" : "";
leftBoundaryParams = {days: -7};
rightBoundaryParams = t ? {days: -1} : {};
offsetPeriodParams = {days: -7};
return makeInterval();
case 'last_30_days':
leftBoundaryParams = "days=-30";
rightBoundaryParams = t ? "days=-1" : "";
leftBoundaryParams = {days: -30};
rightBoundaryParams = t ? {days: -1} : {};
offsetPeriodParams = {days: -30};
return makeInterval();
case 'last_365_days':
leftBoundaryParams = "days=-365";
rightBoundaryParams = t ? "days=-1" : "";
leftBoundaryParams = {days: -365};
rightBoundaryParams = t ? {days: -1} : {};
offsetPeriodParams = {days: -365};
return makeInterval();
}
},
@@ -67,6 +67,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this.searchView = params.searchView;
this.searchViewHidden = params.searchViewHidden;
this.groupable = params.groupable;
this.enableTimeRangeMenu = params.enableTimeRangeMenu;
this.actionViews = params.actionViews;
this.viewType = params.viewType;
this.withControlPanel = params.withControlPanel !== false;
@@ -428,6 +429,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
searchview: this.searchView,
search_view_hidden: !this.searchable || this.searchviewHidden,
groupable: this.groupable,
enableTimeRangeMenu: this.enableTimeRangeMenu,
});
this._pushState();
@@ -50,7 +50,7 @@ var AbstractView = Class.extend({
// groupby menu or not. This is useful for the views which do not support
// grouping data.
groupable: true,
enableTimeRangeMenu: false,
config: {
Model: AbstractModel,
Renderer: AbstractRenderer,
@@ -74,6 +74,11 @@ var AbstractView = Class.extend({
* @param {number} [params.count]
* @param {string} [params.controllerID]
* @param {string[]} params.domain
* @param {string[][]} params.timeRange
* @param {string[][]} params.comparisonTimeRange
* @param {string} params.timeRangeDescription
* @param {string} params.comparisonTimeRangeDescription
* @param {boolean} params.compare
* @param {string[]} params.groupBy
* @param {number} [params.currentId]
* @param {boolean} params.isEmbedded
@@ -110,6 +115,22 @@ var AbstractView = Class.extend({
noContentHelp: params.action && params.action.help,
};
var timeRangeMenuData = params.context.timeRangeMenuData;
var timeRange = [];
var comparisonTimeRange = [];
var compare = false;
var timeRangeDescription = "";
var comparisonTimeRangeDescription = "";
if (this.enableTimeRangeMenu && timeRangeMenuData) {
timeRange = timeRangeMenuData.timeRange;
comparisonTimeRange = timeRangeMenuData.comparisonTimeRange;
compare = comparisonTimeRange.length > 0;
timeRangeDescription = timeRangeMenuData.timeRangeDescription;
comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription;
this.rendererParams.timeRangeDescription = timeRangeDescription;
this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
}
this.controllerParams = {
modelName: params.modelName,
activeActions: {
@@ -119,6 +140,7 @@ var AbstractView = Class.extend({
duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true,
},
groupable: this.groupable,
enableTimeRangeMenu: this.enableTimeRangeMenu,
isEmbedded: this.isEmbedded,
controllerID: params.controllerID,
bannerRoute: this.arch.attrs.banner_route,
@@ -144,6 +166,11 @@ var AbstractView = Class.extend({
count: params.count || ((this.controllerParams.ids !== undefined) &&
this.controllerParams.ids.length) || 0,
domain: params.domain,
timeRange: timeRange,
timeRangeDescription: timeRangeDescription,
comparisonTimeRange: comparisonTimeRange,
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
compare: compare,
groupedBy: params.groupBy,
modelName: params.modelName,
res_id: params.currentId,
@@ -18,10 +18,13 @@ odoo.define('web.PivotModel', function (require) {
var AbstractModel = require('web.AbstractModel');
var concurrency = require('web.concurrency');
var dataComparisonUtils = require('web.dataComparisonUtils');
var core = require('web.core');
var session = require('web.session');
var utils = require('web.utils');
var computeVariation = dataComparisonUtils.computeVariation;
var _t = core._t;
var PivotModel = AbstractModel.extend({
@@ -7,10 +7,12 @@ var Domain = require('web.Domain');
var DropdownMenu = require('web.DropdownMenu');
var search_filters = require('web.search_filters');
var time = require('web.time');
var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions');
var QWeb = core.qweb;
var _t = core._t;
var PERIOD_OPTIONS = TimeRangeMenuOptions.PeriodOptions;
var FiltersMenu = DropdownMenu.extend({
custom_events: {
@@ -43,6 +45,7 @@ var FiltersMenu = DropdownMenu.extend({
* defaultOptionId: string refers to an optionId (optional)
* currentOptionId: string refers to an optionId that is activated if item is active (optional)
* }
* @param {Object[]} periodOptions determines the options available for filter with date attribute
* @param {Object} fields 'field_get' of a model: mapping from field name
* to an object with its properties
*/
@@ -50,21 +53,7 @@ var FiltersMenu = DropdownMenu.extend({
// determines where the filter menu is displayed and its style
this.isMobile = config.device.isMobile;
// determines list of options used by filter of type 'date'
this.periodOptions = [
{description: _t('Today'), optionId: 'today', groupId: 1},
{description: _t('This Week'), optionId: 'this_week', groupId: 1},
{description: _t('This Month'), optionId: 'this_month', groupId: 1},
{description: _t('This Quarter'), optionId: 'this_quarter', groupId: 1},
{description: _t('This Year'), optionId: 'this_year', groupId: 1},
{description: _t('Yesterday'), optionId: 'yesterday', groupId: 2},
{description: _t('Last Week'), optionId: 'last_week', groupId: 2},
{description: _t('Last Month'), optionId: 'last_month', groupId: 2},
{description: _t('Last Quarter'), optionId: 'last_quarter', groupId: 2},
{description: _t('Last Year'), optionId: 'last_year', groupId: 2},
{description: _t('Last 7 Days'), optionId: 'last_7_days', groupId: 3},
{description: _t('Last 30 Days'), optionId: 'last_30_days', groupId: 3},
{description: _t('Last 365 Days'), optionId: 'last_365_days', groupId: 3},
];
this.periodOptions = PERIOD_OPTIONS;
// determines when the 'Add custom filter' submenu is open
this.generatorMenuIsOpen = false;
this.propositions = [];
@@ -10,9 +10,14 @@ var FiltersMenu = require('web.FiltersMenu');
var GroupByMenu = require('web.GroupByMenu');
var pyUtils = require('web.py_utils');
var search_inputs = require('web.search_inputs');
var TimeRangeMenu = require('web.TimeRangeMenu');
var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions');
var utils = require('web.utils');
var Widget = require('web.Widget');
var _t = core._t;
var ComparisonOptions = TimeRangeMenuOptions.ComparisonOptions;
var PeriodOptions = TimeRangeMenuOptions.PeriodOptions;
var Backbone = window.Backbone;
@@ -198,7 +203,7 @@ var FacetView = Widget.extend({
* is the current interval used
* (necessarily the field is of type 'date' or 'datetime')
* @param {Object} periodMapping, a key is a field name and the corresponding value
* is the current interval used
* is the current period used
* (necessarily the field is of type 'date' or 'datetime')
*/
init: function (parent, model, intervalMapping, periodMapping) {
@@ -248,9 +253,11 @@ var FacetValueView = Widget.extend({
/*
* @param {Widget} parent
* @param {Object} model
* @param {Object} interval (optional) is used in case the facet value
* @param {Object} option (optional) is used in case the facet value
* corresponds to a groupby with an associated 'date'
* 'datetime' field.
* @param {Object} comparison (optional) is used in case the facet value
* comes from the time range menu and comparison is active
*/
init: function (parent, model, option) {
this._super(parent);
@@ -326,13 +333,16 @@ var SearchView = Widget.extend({
item_option_changed: '_onItemOptionChanged',
new_groupby: '_onNewGroupby',
new_filters: '_onNewFilters',
time_range_modified: '_onTimeRangeModified',
time_range_removed: '_onTimeRangeRemoved',
},
defaults: _.extend({}, Widget.prototype.defaults, {
hidden: false,
disable_filters: false,
disable_custom_filters: false,
disable_groupby: false,
disable_favorites: false,
disable_custom_filters: false,
disable_filters: false,
disableTimeRangeMenu: true,
}),
template: "SearchView",
@@ -361,6 +371,15 @@ var SearchView = Widget.extend({
this.hasFavorites = false;
this.noDateFields = true;
var field;
for (var key in this.fields) {
field = this.fields[key];
if (_.contains(['date', 'datetime'], field.type) && field.sortable) {
this.noDateFields = false;
break;
}
}
this.selectedGroupIds = {
groupByCategory: [],
filterCategory: []
@@ -373,6 +392,8 @@ var SearchView = Widget.extend({
this.filters = [];
this.groupbys = [];
this.timeRanges = options.action && options.action.context ?
options.action.context.time_ranges : undefined;
var visibleSearchMenu = this.call('local_storage', 'getItem', 'visible_search_menu');
this.visible_filters = (visibleSearchMenu !== 'false');
this.input_subviews = []; // for user input in searchbar
@@ -421,6 +442,12 @@ var SearchView = Widget.extend({
.then(this.set_default_filters.bind(this))
.then(function () {
var menu_defs = [];
self.timeRangeMenu = self._createTimeRangeMenu();
menu_defs.push(self.timeRangeMenu.prependTo($buttons));
self.timeRangeMenu.do_hide();
var display = self.options.disableTimeRangeMenu !== undefined &&
!self.options.disableTimeRangeMenu;
self.displayTimeRangeMenu(display);
if (!self.options.disable_groupby) {
self.groupby_menu = self._createGroupByMenu();
menu_defs.push(self.groupby_menu.prependTo($buttons));
@@ -432,6 +459,17 @@ var SearchView = Widget.extend({
return $.when.apply($, menu_defs);
});
},
/*
*
* @param {boolean}
*/
displayTimeRangeMenu: function (b) {
if (!b || this.noDateFields) {
this.timeRangeMenu.do_hide();
} else {
this.timeRangeMenu.do_show();
}
},
on_attach_callback: function () {
this._focusInput();
},
@@ -448,6 +486,8 @@ var SearchView = Widget.extend({
if (!_.isEmpty(this.search_defaults)) {
var inputs = this.search_fields.concat(this.filters, this.groupbys);
var search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults);
var defaultTimeRange = this._searchDefaultTimeRange();
search_defaults.push(defaultTimeRange);
return $.when.apply(null, search_defaults).then(function () {
var facets = _.compact(arguments);
self.query.reset(facets, {preventSearch: true});
@@ -502,7 +542,7 @@ var SearchView = Widget.extend({
if (domain) {
domains.push(domain);
}
var context = field.get_context(facet);
var context = field.get_context(facet, noDomainEvaluation);
if (context) {
contexts.push(context);
}
@@ -630,8 +670,8 @@ var SearchView = Widget.extend({
groupByCategory: [],
filterCategory: [],
};
var timeRangeMenuIsActive;
this.query.each(function (facet) {
var values = facet.get('values');
if (facet.attributes.cat === "groupByCategory") {
selectedGroupIds.groupByCategory = _.uniq(
@@ -665,7 +705,9 @@ var SearchView = Widget.extend({
)
);
}
if (facet.attributes.cat === "timeRangeCategory") {
timeRangeMenuIsActive = true;
}
var f = new FacetView(this, facet, this.intervalMapping, this.periodMapping);
started.push(f.appendTo(self.$('.o_searchview_input_container')));
self.input_subviews.push(f);
@@ -692,6 +734,9 @@ var SearchView = Widget.extend({
if (self.filters_menu) {
self._unsetUnusedFilters(selectedGroupIds.filterCategory);
}
if (self.timeRangeMenu && !timeRangeMenuIsActive) {
self.timeRangeMenu.deactivate();
}
});
},
childFocused: function () {
@@ -747,7 +792,7 @@ var SearchView = Widget.extend({
}
var current_group = [],
current_category = 'filters',
categories = {filters: this.filters, group_by: this.groupbys};
categories = {filters: this.filters, group_by: this.groupbys, timeRanges: this.timeRanges};
_.each(filters.concat({category:'filters', item: 'separator'}), function (filter) {
if (filter.item.tag === 'filter' && filter.category === current_category) {
@@ -921,6 +966,15 @@ var SearchView = Widget.extend({
});
return new GroupByMenu(this, groupbys, this.fields);
},
/**
* Create a time range menu.
*
* @private
* @returns {Widget} the range menu
*/
_createTimeRangeMenu: function () {
return new TimeRangeMenu(this, this.fields, this.timeRanges);
},
/**
* Processes a fieldsView in place. In particular, parses its arch.
*
@@ -936,6 +990,81 @@ var SearchView = Widget.extend({
fv.arch = utils.xml_to_json(doc, true);
return fv;
},
_searchDefaultTimeRange: function () {
if (this.timeRanges) {
var timeRange = "[]";
var timeRangeDescription;
var comparisonTimeRange = "[]";
var comparisonTimeRangeDescription;
var dateField = {
name: this.timeRanges.field,
type: this.fields[this.timeRanges.field].type,
description: this.fields[this.timeRanges.field].string,
};
timeRange = Domain.prototype.constructDomain(
dateField.name,
this.timeRanges.range,
dateField.type
);
timeRangeDescription = _.findWhere(
PeriodOptions,
{optionId: this.timeRanges.range}
).description;
if (this.timeRanges.comparison_range) {
comparisonTimeRange = Domain.prototype.constructDomain(
dateField.name,
this.timeRanges.range,
dateField.type,
null,
this.timeRanges.comparison_range
);
comparisonTimeRangeDescription = _.findWhere(
ComparisonOptions,
{optionId: this.timeRanges.comparison_range}
).description;
}
return $.when({
cat: 'timeRangeCategory',
category: _t("Time Range"),
icon: 'fa fa-calendar',
field: {
get_context: function (facet, noDomainEvaluation) {
if (!noDomainEvaluation) {
timeRange = Domain.prototype.stringToArray(timeRange);
comparisonTimeRange = Domain.prototype.stringToArray(comparisonTimeRange);
}
return {
timeRangeMenuData: {
timeRange: timeRange,
timeRangeDescription: timeRangeDescription,
comparisonTimeRange: comparisonTimeRange,
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
}
};
},
get_groupby: function () {},
get_domain: function () {}
},
isRange: true,
values: [{
label: dateField.description + ': ' + timeRangeDescription +
(
comparisonTimeRangeDescription ?
(' / ' + comparisonTimeRangeDescription) :
''
),
value: null,
}],
});
} else {
return $.when();
}
},
/**
* This function ask the groupby menu to deactive all groupbys if no
* groupby is used.
@@ -1115,6 +1244,31 @@ var SearchView = Widget.extend({
this.query.trigger('reset');
}
},
/*
* @private
* @param {JQueryEvent} event
*/
_onTimeRangeModified: function () {
var facet = this.timeRangeMenu.facetFor();
var current = this.query.find(function (facet) {
return facet.get('cat') === 'timeRangeCategory';
});
if (current) {
this.query.remove(current, {silent: true});
}
this.query.add(facet);
},
/*
* @private
*/
_onTimeRangeRemoved: function () {
var current = this.query.find(function (facet) {
return facet.get('cat') === 'timeRangeCategory';
});
if (current) {
this.query.remove(current);
}
},
});
_.extend(SearchView, {
@@ -0,0 +1,185 @@
odoo.define('web.TimeRangeMenu', function (require) {
"use strict";
var core = require('web.core');
var Domain = require('web.Domain');
var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions');
var Widget = require('web.Widget');
var _t = core._t;
var ComparisonOptions = TimeRangeMenuOptions.ComparisonOptions;
var PeriodOptions = TimeRangeMenuOptions.PeriodOptions;
var TimeRangeMenu = Widget.extend({
template: 'web.TimeRangeMenu',
events: {
'click .o_apply_range': '_onApplyButtonClick',
'click .o_comparison_checkbox': '_onCheckBoxClick',
},
/**
* override
* @param {Widget} parent
* @param {Object} fields
* @param {Object} configuration
*
*/
init: function(parent, fields, configuration) {
var self = this;
this._super(parent);
this.dateFields = [];
_.each(fields, function (field, name) {
if (field.sortable && _.contains(['date', 'datetime'], field.type)) {
self.dateFields.push(_.extend({}, field, {
name: name,
}));
}
});
this.periodOptions = PeriodOptions;
this.comparisonOptions = ComparisonOptions;
// Following steps determine initial configuration
this.isActive = false;
this.timeRangeId = undefined;
this.comparisonIsSelected = false;
this.comparisonTimeRangeId = undefined;
this.dateField = {};
if (configuration && configuration.field && configuration.range) {
this.isActive = true;
var dateField = _.findWhere(this.dateFields, {name: configuration.field});
this.dateField = {
name: dateField.name,
description: dateField.string,
type: dateField.type,
};
this.timeRangeId = configuration.range;
if (configuration.comparison_range) {
this.comparisonIsSelected = true;
this.comparisonTimeRangeId = configuration.comparison_range;
}
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
deactivate: function () {
this.isActive = false;
this.comparisonIsSelected = false;
this.renderElement();
},
/**
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
* filter descriptor
*
* @returns {Object}
*/
facetFor: function () {
var fieldDescription;
var timeRange = "[]";
var timeRangeDescription;
var comparisonTimeRange = "[]";
var comparisonTimeRangeDescription;
if (this.isActive) {
fieldDescription = this.dateField.description;
if (this.timeRangeId !== 'custom') {
timeRange = Domain.prototype.constructDomain(
this.dateField.name,
this.timeRangeId,
this.dateField.type
);
timeRangeDescription = _.findWhere(
this.periodOptions,
{optionId: this.timeRangeId}
).description;
}
if (this.comparisonIsSelected) {
comparisonTimeRange = Domain.prototype.constructDomain(
this.dateField.name,
this.timeRangeId,
this.dateField.type,
null,
this.comparisonTimeRangeId
);
comparisonTimeRangeDescription = _.findWhere(
this.comparisonOptions,
{optionId: this.comparisonTimeRangeId}
).description;
}
}
return {
cat: 'timeRangeCategory',
category: _t("Time Range"),
icon: 'fa fa-calendar',
field: {
get_context: function (facet, noDomainEvaluation) {
if (!noDomainEvaluation) {
timeRange = Domain.prototype.stringToArray(timeRange);
comparisonTimeRange = Domain.prototype.stringToArray(comparisonTimeRange);
}
return {
timeRangeMenuData: {
timeRange: timeRange,
timeRangeDescription: timeRangeDescription,
comparisonTimeRange: comparisonTimeRange,
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
}
};
},
get_groupby: function () {},
get_domain: function () {}
},
isRange: true,
values: [{
label: fieldDescription + ': ' + timeRangeDescription +
(
comparisonTimeRangeDescription ?
(' / ' + comparisonTimeRangeDescription) :
''
),
value: null,
}],
};
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onApplyButtonClick: function () {
this.isActive = true;
var dateFieldName = this.$('.o_date_field_selector').val();
this.timeRangeId = this.$('.o_time_range_selector').val();
if (this.comparisonIsSelected) {
this.comparisonTimeRangeId = this.$('.o_comparison_time_range_selector').val();
}
this.dateField = {
name: dateFieldName,
type: _.findWhere(this.dateFields, {name: dateFieldName}).type,
description: _.findWhere(this.dateFields, {name: dateFieldName}).string,
};
this.renderElement();
this.trigger_up('time_range_modified');
},
/**
* @private
*
* @param {JQueryEvent} ev
*/
_onCheckBoxClick: function (ev) {
ev.stopPropagation();
this.comparisonIsSelected = this.$('.o_comparison_checkbox').prop('checked');
this.$('.o_comparison_time_range_selector').toggleClass('o_hidden');
this.$el.addClass('open');
}
});
return TimeRangeMenu;
});
@@ -0,0 +1,33 @@
odoo.define('web.TimeRangeMenuOptions', function (require) {
"use strict";
var core = require('web.core');
var _lt = core._lt;
var PeriodOptions = [
{description: _lt('Today'), optionId: 'today', groupId: 1},
{description: _lt('This Week'), optionId: 'this_week', groupId: 1},
{description: _lt('This Month'), optionId: 'this_month', groupId: 1},
{description: _lt('This Quarter'), optionId: 'this_quarter', groupId: 1},
{description: _lt('This Year'), optionId: 'this_year', groupId: 1},
{description: _lt('Yesterday'), optionId: 'yesterday', groupId: 2},
{description: _lt('Last Week'), optionId: 'last_week', groupId: 2},
{description: _lt('Last Month'), optionId: 'last_month', groupId: 2},
{description: _lt('Last Quarter'), optionId: 'last_quarter', groupId: 2},
{description: _lt('Last Year'), optionId: 'last_year', groupId: 2},
{description: _lt('Last 7 Days'), optionId: 'last_7_days', groupId: 3},
{description: _lt('Last 30 Days'), optionId: 'last_30_days', groupId: 3},
{description: _lt('Last 365 Days'), optionId: 'last_365_days', groupId: 3},
];
var ComparisonOptions = [
{description: _lt('Previous Period'), optionId: 'previous_period'},
{description: _lt('Previous Year'), optionId: 'previous_year'}
];
return {
PeriodOptions: PeriodOptions,
ComparisonOptions: ComparisonOptions,
};
});
@@ -3,6 +3,7 @@
display: none;
}
min-width: 150px;
max-height: calc(100vh - 125px); // FIXME
overflow: auto;
+11 -4
View File
@@ -142,11 +142,13 @@
}
}
// Filters
.o_filters_menu {
.o_filters_menu, .o_group_by_menu, .o_time_range_menu {
max-height: calc(100vh - 125px);
overflow: auto;
}
// Filters
.o_filters_menu {
.o_filter_condition {
@include o-search-options-dropdown-custom-item;
margin-bottom: 8px;
@@ -182,8 +184,6 @@
}
}
// Favorites
.o_favorites_menu {
max-height: 75vh;
@@ -211,3 +211,10 @@
@include o-search-options-dropdown-custom-item;
}
}
// Time Ranges
.o_time_range_menu {
.dropdown-item-text > label, .custom-control-label {
font-weight: bold;
}
}
+66
View File
@@ -1127,6 +1127,72 @@
</div>
</t>
<div t-name="web.TimeRangeMenu" class="btn-group o_dropdown">
<button class="o_dropdown_toggler_btn btn btn-secondary dropdown-toggle o_time_range_menu_button" data-toggle="dropdown"
aria-expanded="false" tabindex="-1">
<span class="fa fa-calendar"/> Time Ranges <span class="caret"/>
</button>
<div class="dropdown-menu o_dropdown_menu o_time_range_menu" role="menu">
<div class="dropdown-item-text">
<label for="date_field_selector">Date Field</label>
<select id="date_field_selector" class="o_input o_date_field_selector">
<t t-foreach="widget.dateFields" t-as="field">
<t t-if="widget.dateField.name === field.name">
<option t-att-value="field.name" selected="selected">
<t t-esc="field.string"/>
</option>
</t>
<t t-else="">
<option t-att-value="field.name">
<t t-esc="field.string"/>
</option>
</t>
</t>
</select>
</div>
<div class="dropdown-item-text">
<label for="time_range_selector">Date Range</label>
<select id="time_range_selector" class="o_input o_time_range_selector">
<t t-foreach="widget.periodOptions" t-as="option">
<t t-if="widget.timeRangeId === option.optionId">
<option t-att-value="option.optionId" selected="selected">
<t t-esc="option.description"/>
</option>
</t>
<t t-else="">
<option t-att-value="option.optionId">
<t t-esc="option.description"/>
</option>
</t>
</t>
</select>
</div>
<div class="dropdown-item-text">
<t t-set="isSelected" t-value="widget.comparisonIsSelected"/>
<div class="custom-control custom-checkbox">
<input type="checkbox" id="comparison_checkbox" class="custom-control-input o_comparison_checkbox" t-att-checked="isSelected ? 'checked' : undefined"/>
<label for="comparison_checkbox" class="custom-control-label">Compare To</label>
</div>
<select t-att-class="'o_input o_comparison_time_range_selector' + (widget.comparisonIsSelected ? '' : ' o_hidden')">
<t t-foreach="widget.comparisonOptions" t-as="option">
<t t-if="widget.comparisonTimeRangeId === option.optionId">
<option t-att-value="option.optionId" selected="selected">
<t t-esc="option.description"/>
</option>
</t>
<t t-else="">
<option t-att-value="option.optionId">
<t t-esc="option.description"/>
</option>
</t>
</t>
</select>
</div>
<div class="dropdown-item-text">
<button class="btn btn-primary o_apply_range" type="button">Apply</button>
</div>
</div>
</div>
<div t-name="SearchView" class="o_searchview" role="search" aria-autocomplete="list">
+3
View File
@@ -110,6 +110,7 @@
<script type="text/javascript" src="/web/static/src/js/services/config.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/abstract_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/abstract_storage_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/data_comparison_utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/class.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/collections.js"/>
<script type="text/javascript" src="/web/static/src/js/core/translation.js"></script>
@@ -239,6 +240,8 @@
<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/time_range_menu.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/search/time_range_menu_options.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>