[MOV] spreadsheet: move filter components to community

Part-of: odoo/odoo#97606
This commit is contained in:
Lucas Lefèvre
2022-09-10 02:54:52 +02:00
parent 5f826b2b69
commit bf5e59ddaf
6 changed files with 465 additions and 0 deletions
@@ -0,0 +1,65 @@
/** @odoo-module */
import { YearPicker } from "../year_picker";
import { dateOptions } from "@spreadsheet/global_filters/helpers";
const { DateTime } = luxon;
const { Component, onWillUpdateProps } = owl;
export class DateFilterValue extends Component {
setup() {
this._setStateFromProps(this.props);
onWillUpdateProps(this._setStateFromProps);
}
_setStateFromProps(props) {
this.period = props.period;
/** @type {number|undefined} */
this.yearOffset = props.yearOffset;
// date should be undefined if we don't have the yearOffset
/** @type {DateTime|undefined} */
this.date =
this.yearOffset !== undefined
? DateTime.local().plus({ year: this.yearOffset })
: undefined;
}
dateOptions(type) {
return type ? dateOptions(type) : [];
}
isYear() {
return this.props.type === "year";
}
isSelected(periodId) {
return this.period === periodId;
}
onPeriodChanged(ev) {
this.period = ev.target.value;
this._updateFilter();
}
onYearChanged(date) {
this.date = date;
this.yearOffset = date.year - DateTime.now().year;
this._updateFilter();
}
_updateFilter() {
this.props.onTimeRangeChanged({
yearOffset: this.yearOffset || 0,
period: this.period,
});
}
}
DateFilterValue.template = "spreadsheet_edition.DateFilterValue";
DateFilterValue.components = { YearPicker };
DateFilterValue.props = {
// See @spreadsheet_edition/bundle/global_filters/filters_plugin.RangeType
type: { validate: (t) => ["year", "month", "quarter"].includes(t) },
onTimeRangeChanged: Function,
yearOffset: { type: Number, optional: true },
period: { type: String, optional: true },
};
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<div t-name="spreadsheet_edition.DateFilterValue" class="date_filter_values" owl="1">
<select t-if="!isYear()" class="o_input" t-on-change="onPeriodChanged">
<option value="empty"/>
<t t-set="type" t-value="props.type"/>
<t t-foreach="dateOptions(type)" t-as="periodOption" t-key="periodOption.id">
<option t-if="isSelected(periodOption.id)" selected="1" t-att-value="periodOption.id">
<t t-esc="periodOption.description"/>
</option>
<option t-else="" t-att-value="periodOption.id">
<t t-esc="periodOption.description"/>
</option>
</t>
</select>
<YearPicker
date="date"
onDateTimeChanged.bind="onYearChanged"
/>
</div>
</templates>
@@ -0,0 +1,39 @@
/** @odoo-module */
import { X2ManyTagSelector } from "../tag_selector_widget";
import { RELATIVE_DATE_RANGE_TYPES } from "@spreadsheet/helpers/constants";
import { DateFilterValue } from "../filter_date_value/filter_date_value";
const { Component } = owl;
export class FilterValue extends Component {
setup() {
this.getters = this.props.model.getters;
this.relativeDateRangesTypes = RELATIVE_DATE_RANGE_TYPES;
}
onDateInput(id, value) {
this.props.model.dispatch("SET_GLOBAL_FILTER_VALUE", { id, value });
}
onTextInput(id, value) {
this.props.model.dispatch("SET_GLOBAL_FILTER_VALUE", { id, value });
}
onTagSelected(id, values) {
this.props.model.dispatch("SET_GLOBAL_FILTER_VALUE", {
id,
value: values.map((record) => record.id),
displayNames: values.map((record) => record.display_name),
});
}
onClear(id) {
this.props.model.dispatch("CLEAR_GLOBAL_FILTER_VALUE", { id });
}
}
FilterValue.template = "spreadsheet_edition.FilterValue";
FilterValue.components = { X2ManyTagSelector, DateFilterValue };
FilterValue.props = {
filter: Object,
model: Object,
};
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="spreadsheet_edition.FilterValue" owl="1">
<t t-set="filter" t-value="props.filter"/>
<t t-set="filterValue" t-value="getters.getGlobalFilterValue(filter.id)"/>
<div>
<input t-if="filter.type === 'text'"
type="text"
class="o_input"
t-att-placeholder="filter.label"
t-att-value="filterValue"
t-on-change="(e) => this.onTextInput(filter.id, e.target.value)"/>
</div>
<span t-if="filter.type === 'relation'" t-on-click.stop="">
<X2ManyTagSelector
placeholder="filter.label"
relatedModel="filter.modelName"
selectedValues="filterValue"
onValueChanged="(value) => this.onTagSelected(filter.id, value)"
/>
</span>
<div class="d-flex align-items-center">
<DateFilterValue
t-if="filter.type === 'date' and filter.rangeType !== 'relative'"
period="filterValue &amp;&amp; filterValue.period"
yearOffset="filterValue &amp;&amp; filterValue.yearOffset"
type="filter.rangeType"
onTimeRangeChanged="(value) => this.onDateInput(filter.id, value)" />
<select
t-if="filter.type === 'date' and filter.rangeType === 'relative'"
t-on-change="(e) => this.onDateInput(filter.id, e.target.value || undefined)"
class="o_input pe-3">
<option value=""/>
<t t-foreach="relativeDateRangesTypes" t-as="range" t-key="range.type">
<option t-att-selected="range.type === filterValue" t-att-value="range.type">
<t t-esc="range.description"/>
</option>
</t>
</select>
<i t-if="getters.isFilterActive(filter.id)"
class="fa fa-times btn btn-link text-muted o_side_panel_filter_icon"
title="Clear"
t-on-click="() => this.onClear(filter.id)"/>
</div>
</t>
</templates>
@@ -0,0 +1,183 @@
/** @odoo-module **/
"use strict";
import { ComponentAdapter } from "web.OwlCompatibility";
import { FieldMany2ManyTags } from "web.relational_fields";
import StandaloneFieldManagerMixin from "web.StandaloneFieldManagerMixin";
import Widget from "web.Widget";
const { Component, xml } = owl;
/**
* This widget is used in the global filters to select a value for a
* relation filter
* It uses a FieldMany2ManyTags widget.
*/
const TagSelectorWidget = Widget.extend(StandaloneFieldManagerMixin, {
/**
* @constructor
*
* @param {string} relatedModel Name of the related model
* @param {Array<number>} selectedValues Values already selected
* @param {string | undefined} placeholder
*/
init: function (parent, relatedModel, selectedValues, placeholder) {
this._super.apply(this, arguments);
StandaloneFieldManagerMixin.init.call(this);
this.relatedModel = relatedModel;
this.selectedValues = selectedValues;
this.placeholder = placeholder;
this.widget = undefined;
},
updateWidgetState: async function (state) {
this.selectedValues = state.selectedValues;
this.relatedModel = state.relatedModel;
await this._makeM2MWidget();
const $content = $("<div/>");
this.$el.empty().append($content);
this.widget.appendTo($content);
},
/**
* @override
*/
willStart: async function () {
await this._super.apply(this, arguments);
await this._makeM2MWidget();
},
/**
* @override
*/
start: function () {
const $content = $("<div/>");
this.$el.append($content);
this.widget.appendTo($content);
return this._super.apply(this, arguments);
},
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @override
*/
_confirmChange: async function () {
const result = await StandaloneFieldManagerMixin._confirmChange.apply(this, arguments);
this.trigger_up("value_changed", {
value: this.widget.value.data.map((record) => record.data),
});
return result;
},
/**
* Create a record for the related model and a FieldMany2ManyTags linked
* to this record
*/
_makeM2MWidget: async function () {
const options = {};
options[this.relatedModel] = {
options: {
no_create_edit: true,
no_create: true,
},
};
const recordID = await this.model.makeRecord(
this.relatedModel,
[
{
fields: [
{
name: "id",
type: "integer",
},
{
name: "display_name",
type: "char",
},
],
name: this.relatedModel,
relation: this.relatedModel,
type: "many2many",
value: this.selectedValues,
},
],
options
);
this.widget = new FieldMany2ManyTags(this, this.relatedModel, this.model.get(recordID), {
mode: "edit",
attrs: { placeholder: this.placeholder },
});
this._registerWidget(recordID, this.relatedModel, this.widget);
},
});
class TagSelectorWidgetAdapter extends ComponentAdapter {
setup() {
super.setup();
this.env = Component.env;
}
_trigger_up(ev) {
if (ev.name === "value_changed") {
const { value } = ev.data;
return this.props.onValueChanged(value);
}
super._trigger_up(ev);
}
/**
* @override
*/
get widgetArgs() {
return [this.props.relatedModel, this.props.selectedValues, this.props.placeholder];
}
/**
* @override
*/
async updateWidget(nextProps) {
if (
nextProps.relatedModel !== this.props.relatedModel ||
JSON.stringify(nextProps.selectedValues) !== JSON.stringify(this.props.selectedValues)
) {
const payload = {
relatedModel: nextProps.relatedModel,
selectedValues: nextProps.selectedValues,
};
return this.widget.updateWidgetState(payload);
}
}
/**
* @override
*/
renderWidget() {}
}
export class X2ManyTagSelector extends Component {
setup() {
this.TagSelectorWidget = TagSelectorWidget;
}
}
X2ManyTagSelector.components = { TagSelectorWidgetAdapter };
X2ManyTagSelector.template = xml/*xml*/ `
<TagSelectorWidgetAdapter
Component="TagSelectorWidget"
relatedModel="props.relatedModel"
selectedValues="props.selectedValues"
onValueChanged="props.onValueChanged"
placeholder="props.placeholder"
/>`;
X2ManyTagSelector.props = {
onValueChanged: Function,
relatedModel: String,
selectedValues: {
optional: true,
type: Array,
},
placeholder: {
optional: true,
type: String,
},
};
X2ManyTagSelector.defaultProps = {
selectedValues: [],
};
@@ -0,0 +1,109 @@
/** @odoo-module */
import { DatePicker } from "@web/core/datepicker/datepicker";
const { DateTime } = luxon;
/**
* @param {string} format
* @returns {boolean}
*/
const isValidStaticFormat = (format) => {
try {
return /^[\d\s/:-]+$/.test(DateTime.local().toFormat(format));
} catch (_err) {
return false;
}
};
const DEFAULT_DATE = DateTime.local();
export class YearPicker extends DatePicker {
/**
* @override
*/
initFormat() {
super.initFormat();
// moment.js format
this.defaultFormat = "yyyy";
this.staticFormat = "yyyy";
}
/**
* @override
*/
getOptions(useStatic = false) {
return {
format:
!useStatic || isValidStaticFormat(this.format) ? this.format : this.staticFormat,
locale: DEFAULT_DATE.locale,
};
}
/**
* @override
*/
bootstrapDateTimePicker(commandOrParams) {
if (typeof commandOrParams === "object") {
const widgetParent = window.$(this.rootRef.el);
commandOrParams = { ...commandOrParams, widgetParent };
}
super.bootstrapDateTimePicker(commandOrParams);
}
/**
* @override
*/
onWillUpdateProps(nextProps) {
const pickerParams = {};
let shouldUpdateInput = false;
for (const prop in nextProps) {
const prev = this.props[prop];
const next = nextProps[prop];
if (
(prev instanceof DateTime && next instanceof DateTime && !prev.equals(next)) ||
prev !== next
) {
pickerParams[prop] = nextProps[prop];
if (prop === "date") {
this.setDateAndFormat(nextProps);
shouldUpdateInput = true;
}
}
}
if (shouldUpdateInput) {
this.updateInput();
}
this.bootstrapDateTimePicker(pickerParams);
}
/**
* @override: allow displaying empty dates
*/
updateInput({ useStatic } = {}) {
const [formattedValue] = this.formatValue(this.date, this.getOptions(useStatic));
this.inputRef.el.value = formattedValue || "";
}
/**
* @override
*/
onDateChange({ useStatic } = {}) {
const [date] = this.parseValue(this.inputRef.el.value, this.getOptions(useStatic));
if (!date || (this.date instanceof DateTime && date.equals(this.date))) {
this.updateInput();
} else {
this.state.warning = date > DateTime.local();
this.props.onDateTimeChanged(date);
}
}
}
const props = {
...DatePicker.props,
date: { type: DateTime, optional: true },
};
delete props["format"];
YearPicker.props = props;
YearPicker.defaultProps = {
...DatePicker.defaultProps,
};