[MOV] spreadsheet: move filter components to community
Part-of: odoo/odoo#97606
This commit is contained in:
+65
@@ -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 },
|
||||
};
|
||||
+21
@@ -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 && filterValue.period"
|
||||
yearOffset="filterValue && 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,
|
||||
};
|
||||
Reference in New Issue
Block a user