[IMP] web: Optional start date in daterange fields
Before this commit, the date/datetime/daterange fields only allowed for an optional end date field. This meant that the primary date was always the start date. This commit allows the field to do the opposite: with the primary date being the end date, and having a `start_date_field` option for an optional start date. closes odoo/odoo#120695 Signed-off-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import { standardFieldProps } from "../standard_field_props";
|
||||
* placeholder?: string;
|
||||
* required?: boolean;
|
||||
* rounding?: number;
|
||||
* startDateField?: string;
|
||||
* warnFuture?: boolean;
|
||||
* }} DateTimeFieldProps
|
||||
*
|
||||
@@ -42,6 +43,7 @@ export class DateTimeField extends Component {
|
||||
placeholder: { type: String, optional: true },
|
||||
required: { type: Boolean, optional: true },
|
||||
rounding: { type: Number, optional: true },
|
||||
startDateField: { type: String, optional: true },
|
||||
warnFuture: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
@@ -51,6 +53,10 @@ export class DateTimeField extends Component {
|
||||
// Getters
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
get endDateField() {
|
||||
return this.props.endDateField || this.props.name;
|
||||
}
|
||||
|
||||
get hasEmptyField() {
|
||||
return this.values.length < 2 || !this.values.every(Boolean);
|
||||
}
|
||||
@@ -60,8 +66,16 @@ export class DateTimeField extends Component {
|
||||
return this.inputRefs.map((ref) => ref.el?.value);
|
||||
}
|
||||
|
||||
get showEndDateInput() {
|
||||
return this.props.endDateField && this.values.filter(Boolean).length;
|
||||
get relatedField() {
|
||||
return this.props.startDateField || this.props.endDateField;
|
||||
}
|
||||
|
||||
get showRange() {
|
||||
return this.relatedField && this.values.filter(Boolean).length;
|
||||
}
|
||||
|
||||
get startDateField() {
|
||||
return this.props.startDateField || this.props.name;
|
||||
}
|
||||
|
||||
get values() {
|
||||
@@ -73,7 +87,7 @@ export class DateTimeField extends Component {
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
setup() {
|
||||
const { endDateField, name } = this.props; // should not change
|
||||
const { name } = this.props; // should not change
|
||||
|
||||
this.rootRef = useRef("root");
|
||||
this.inputRefs = [useRef("start-date"), useRef("end-date")];
|
||||
@@ -84,9 +98,9 @@ export class DateTimeField extends Component {
|
||||
onChange: (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.every(Boolean)) {
|
||||
this.emptyField = false;
|
||||
this.emptyField = null;
|
||||
} else {
|
||||
this.emptyField = value[0] ? endDateField : name;
|
||||
this.emptyField = value[1] ? this.startDateField : this.endDateField;
|
||||
this.state.value = value.find(Boolean);
|
||||
}
|
||||
}
|
||||
@@ -96,10 +110,10 @@ export class DateTimeField extends Component {
|
||||
const toUpdate = {};
|
||||
if (Array.isArray(value)) {
|
||||
// Value is already a range
|
||||
[toUpdate[name], toUpdate[endDateField]] = value;
|
||||
[toUpdate[this.startDateField], toUpdate[this.endDateField]] = value;
|
||||
} else {
|
||||
toUpdate[this.emptyField === name ? endDateField : name] = value;
|
||||
if (endDateField && this.emptyField) {
|
||||
toUpdate[this.emptyField === name ? this.relatedField : name] = value;
|
||||
if (this.relatedField && this.emptyField) {
|
||||
toUpdate[this.emptyField] = false;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +138,7 @@ export class DateTimeField extends Component {
|
||||
|
||||
async addDate() {
|
||||
const [value] = this.values;
|
||||
this.state.focusedDateIndex = this.emptyField === this.props.name ? 0 : 1;
|
||||
this.state.focusedDateIndex = this.emptyField === this.startDateField ? 0 : 1;
|
||||
this.state.value = [value, value];
|
||||
}
|
||||
|
||||
@@ -139,14 +153,15 @@ export class DateTimeField extends Component {
|
||||
* @param {DateTimeFieldProps} props
|
||||
*/
|
||||
getPickerProps(props) {
|
||||
const { endDateField, record, name, maxDate, minDate, rounding } = props;
|
||||
const { record, name, maxDate, minDate, rounding } = props;
|
||||
const value = this.getValueFromProps(props);
|
||||
|
||||
// Compute own props
|
||||
this.type = record.fields[name].type;
|
||||
if (endDateField) {
|
||||
if (this.relatedField) {
|
||||
this.emptyField =
|
||||
!Array.isArray(value) && (record.data[endDateField] ? name : endDateField);
|
||||
!Array.isArray(value) &&
|
||||
(record.data[this.relatedField] ? name : this.relatedField);
|
||||
}
|
||||
|
||||
// Compute picker props
|
||||
@@ -168,14 +183,17 @@ export class DateTimeField extends Component {
|
||||
* @param {DateTimeFieldProps} props
|
||||
* @returns {DateTimePickerProps["value"]}
|
||||
*/
|
||||
getValueFromProps({ endDateField, record, required, name }) {
|
||||
getValueFromProps({ endDateField, name, record, required, startDateField }) {
|
||||
const value = record.data[name];
|
||||
if (endDateField) {
|
||||
const endValue = record.data[endDateField];
|
||||
if (required || (value && endValue)) {
|
||||
return [value, endValue];
|
||||
const relatedField = startDateField || endDateField;
|
||||
if (relatedField) {
|
||||
const relatedValue = record.data[relatedField];
|
||||
if (required || (value && relatedValue)) {
|
||||
const range = [value, value];
|
||||
range[startDateField ? 0 : 1] = relatedValue;
|
||||
return range;
|
||||
} else if (!value) {
|
||||
return endValue;
|
||||
return relatedValue;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
@@ -217,6 +235,7 @@ export class DateTimeField extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const START_DATE_FIELD_OPTION = "start_date_field";
|
||||
const END_DATE_FIELD_OPTION = "end_date_field";
|
||||
|
||||
export const dateField = {
|
||||
@@ -250,12 +269,23 @@ export const dateField = {
|
||||
placeholder: attrs.placeholder,
|
||||
required: Boolean(modifiers.required),
|
||||
rounding: parseInt(options.rounding),
|
||||
startDateField: options[START_DATE_FIELD_OPTION],
|
||||
warnFuture: archParseBoolean(options.warn_future),
|
||||
}),
|
||||
fieldDependencies: ({ type, modifiers, options }) =>
|
||||
options[END_DATE_FIELD_OPTION] && [
|
||||
{ name: options[END_DATE_FIELD_OPTION], type, modifiers },
|
||||
],
|
||||
fieldDependencies: ({ type, modifiers, options }) => {
|
||||
const deps = [];
|
||||
if (options[START_DATE_FIELD_OPTION]) {
|
||||
deps.push({ name: options[START_DATE_FIELD_OPTION], type, modifiers });
|
||||
if (options[END_DATE_FIELD_OPTION]) {
|
||||
console.warn(
|
||||
`A field cannot have both ${START_DATE_FIELD_OPTION} and ${END_DATE_FIELD_OPTION} options at the same time`
|
||||
);
|
||||
}
|
||||
} else if (options[END_DATE_FIELD_OPTION]) {
|
||||
deps.push({ name: options[END_DATE_FIELD_OPTION], type, modifiers });
|
||||
}
|
||||
return deps;
|
||||
},
|
||||
};
|
||||
|
||||
export const dateTimeField = {
|
||||
@@ -279,9 +309,15 @@ export const dateRangeField = {
|
||||
displayName: _lt("Date Range"),
|
||||
supportedOptions: [
|
||||
...dateTimeField.supportedOptions,
|
||||
{
|
||||
label: _lt("Start date field"),
|
||||
name: START_DATE_FIELD_OPTION,
|
||||
type: "field",
|
||||
availableTypes: ["date", "datetime"],
|
||||
},
|
||||
{
|
||||
label: _lt("End date field"),
|
||||
name: "end_date_field",
|
||||
name: END_DATE_FIELD_OPTION,
|
||||
type: "field",
|
||||
availableTypes: ["date", "datetime"],
|
||||
},
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<input
|
||||
t-ref="start-date"
|
||||
type="text"
|
||||
t-att-id="props.id"
|
||||
t-att-id="showRange ? props.endDateField and props.id : props.id"
|
||||
class="o_input cursor-pointer"
|
||||
autocomplete="off"
|
||||
t-att-placeholder="props.placeholder"
|
||||
t-att-data-field="props.name"
|
||||
t-att-data-field="emptyField !== startDateField ? startDateField : endDateField"
|
||||
t-on-input="onInput"
|
||||
/>
|
||||
<span
|
||||
@@ -23,12 +23,12 @@
|
||||
title="This date is on the future. Make sure it is what you expected."
|
||||
/>
|
||||
</t>
|
||||
<t t-if="showEndDateInput">
|
||||
<t t-if="showRange">
|
||||
<t t-if="emptyField and !props.readonly and !props.required">
|
||||
<!-- Add date button -->
|
||||
<button class="o_add_date btn btn-secondary btn-sm w-100" t-on-click="addDate">
|
||||
<i class="fa fa-plus me-2" />
|
||||
<t t-if="emptyField === props.name">Add start date</t>
|
||||
<t t-if="emptyField === startDateField">Add start date</t>
|
||||
<t t-else="">Add end date</t>
|
||||
</button>
|
||||
</t>
|
||||
@@ -47,7 +47,7 @@
|
||||
class="o_input cursor-pointer"
|
||||
autocomplete="off"
|
||||
t-att-placeholder="props.placeholder"
|
||||
t-att-data-field="props.endDateField"
|
||||
t-att-data-field="endDateField"
|
||||
t-on-input="onInput"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -580,7 +580,89 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("initial empty start date", async (assert) => {
|
||||
QUnit.test("Render with initial empty value and optional start date", async (assert) => {
|
||||
patchDate(2014, 7, 14, 12, 34, 56);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime_end" widget="daterange" options="{'start_date_field': 'datetime'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, "input[data-field=datetime_end]");
|
||||
|
||||
assert.containsOnce(target, ".o_datetime_picker", "check that the datepicker is opened");
|
||||
assert.containsNone(target, ".o_add_date");
|
||||
|
||||
// Select a value (today)
|
||||
await click(target.querySelector(".o_today"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_daterange input")[0].value,
|
||||
"08/14/2014 12:00:00",
|
||||
"end date should be set properly"
|
||||
);
|
||||
assert.notOk(isHiddenByCSS(target.querySelector(".o_add_date")));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_add_date").innerText.trim().toLowerCase(),
|
||||
"add start date"
|
||||
);
|
||||
|
||||
// Add an end date
|
||||
await click(target.querySelector(".o_add_date"));
|
||||
|
||||
const [startInput, endInput] = target.querySelectorAll(".o_field_daterange input");
|
||||
assert.strictEqual(
|
||||
startInput.value,
|
||||
endInput.value,
|
||||
"the end date should be set to the same value as the start date"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("initial empty date with optional start date", async (assert) => {
|
||||
patchDate(2014, 7, 14, 12, 34, 56);
|
||||
|
||||
serverData.models.partner.records[0].datetime = "2017-03-13 00:00:00";
|
||||
serverData.models.partner.records[0].datetime_end = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime_end" widget="daterange" options="{'start_date_field': 'datetime'}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.ok(isHiddenByCSS(target.querySelector(".o_add_date")));
|
||||
|
||||
target.querySelector(".o_field_daterange input").focus();
|
||||
await nextTick();
|
||||
|
||||
assert.notOk(isHiddenByCSS(target.querySelector(".o_add_date")));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_add_date").innerText.trim().toLowerCase(),
|
||||
"add end date"
|
||||
);
|
||||
|
||||
// Add an end date
|
||||
await click(target.querySelector(".o_add_date"));
|
||||
|
||||
const [startInput, endInput] = target.querySelectorAll(".o_field_daterange input");
|
||||
assert.strictEqual(
|
||||
startInput.value,
|
||||
endInput.value,
|
||||
"the end date should be set to the same value as the start date"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("initial empty date with optional end date", async (assert) => {
|
||||
// 2014-08-14 12:34:56 -> the day E. Zuckerman, who invented pop-up ads, has apologised.
|
||||
patchDate(2014, 7, 14, 12, 34, 56);
|
||||
|
||||
@@ -609,14 +691,14 @@ QUnit.module("Fields", (hooks) => {
|
||||
"add start date"
|
||||
);
|
||||
|
||||
// Add an start date
|
||||
// Add a start date
|
||||
await click(target.querySelector(".o_add_date"));
|
||||
|
||||
const [startInput, endInput] = target.querySelectorAll(".o_field_daterange input");
|
||||
assert.strictEqual(
|
||||
startInput.value,
|
||||
endInput.value,
|
||||
"the end date should be set to the same value as the start date"
|
||||
"the start date should be set to the same value as the end date"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+4
-3
@@ -233,15 +233,16 @@ class Form:
|
||||
field_info['type'] = node.get('widget')
|
||||
elif node.get('widget') == 'daterange':
|
||||
options = ast.literal_eval(node.get('options', '{}'))
|
||||
daterange_field_names[options.get('end_date_field')] = field_name
|
||||
related_field = options.get('start_date_field') or options.get('end_date_field')
|
||||
daterange_field_names[related_field] = field_name
|
||||
|
||||
# determine subview to use for edition
|
||||
if level and field_info['type'] == 'one2many':
|
||||
field_info['invisible'] = field_modifiers.get('invisible') == [TRUE_LEAF]
|
||||
field_info['edition_view'] = self._get_one2many_edition_view(field_info, node, level)
|
||||
|
||||
for end_field, start_field in daterange_field_names.items():
|
||||
modifiers[end_field]['invisible'] = modifiers[start_field].get('invisible', False)
|
||||
for related_field, start_field in daterange_field_names.items():
|
||||
modifiers[related_field]['invisible'] = modifiers[start_field].get('invisible', False)
|
||||
|
||||
view['onchange'] = model._onchange_spec({'arch': etree.tostring(tree)})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user