From bf5e59ddaf073af27b08fa8175c4be87a3521ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Wed, 7 Sep 2022 07:38:40 +0000 Subject: [PATCH] [MOV] spreadsheet: move filter components to community Part-of: odoo/odoo#97606 --- .../filter_date_value/filter_date_value.js | 65 +++++++ .../filter_date_value/filter_date_value.xml | 21 ++ .../components/filter_value/filter_value.js | 39 ++++ .../components/filter_value/filter_value.xml | 48 +++++ .../components/tag_selector_widget.js | 183 ++++++++++++++++++ .../global_filters/components/year_picker.js | 109 +++++++++++ 6 files changed, 465 insertions(+) create mode 100644 addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js create mode 100644 addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml create mode 100644 addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.js create mode 100644 addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.xml create mode 100644 addons/spreadsheet/static/src/global_filters/components/tag_selector_widget.js create mode 100644 addons/spreadsheet/static/src/global_filters/components/year_picker.js diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js new file mode 100644 index 00000000000..cc48e6cde04 --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js @@ -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 }, +}; diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml new file mode 100644 index 00000000000..61766a1675c --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml @@ -0,0 +1,21 @@ + + +
+ + +
+
diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.js b/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.js new file mode 100644 index 00000000000..537abb30767 --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.js @@ -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, +}; diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.xml b/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.xml new file mode 100644 index 00000000000..3cabe509e92 --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/filter_value/filter_value.xml @@ -0,0 +1,48 @@ + + + + + +
+ +
+ + + +
+ + + + +
+ +
+
diff --git a/addons/spreadsheet/static/src/global_filters/components/tag_selector_widget.js b/addons/spreadsheet/static/src/global_filters/components/tag_selector_widget.js new file mode 100644 index 00000000000..42371d7c85b --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/tag_selector_widget.js @@ -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} 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 = $("
"); + 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 = $("
"); + 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*/ ` + `; +X2ManyTagSelector.props = { + onValueChanged: Function, + relatedModel: String, + selectedValues: { + optional: true, + type: Array, + }, + placeholder: { + optional: true, + type: String, + }, +}; +X2ManyTagSelector.defaultProps = { + selectedValues: [], +}; diff --git a/addons/spreadsheet/static/src/global_filters/components/year_picker.js b/addons/spreadsheet/static/src/global_filters/components/year_picker.js new file mode 100644 index 00000000000..af4cfb0591b --- /dev/null +++ b/addons/spreadsheet/static/src/global_filters/components/year_picker.js @@ -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, +};