diff --git a/addons/web/static/src/core/datetime/datetime_picker.js b/addons/web/static/src/core/datetime/datetime_picker.js index 0edfa5c5e1b..c5a5013ff13 100644 --- a/addons/web/static/src/core/datetime/datetime_picker.js +++ b/addons/web/static/src/core/datetime/datetime_picker.js @@ -37,7 +37,8 @@ import { ensureArray } from "../utils/arrays"; * @property {PrecisionLevel} [minPrecision="days"] * @property {(value: DateTime) => any} [onSelect] * @property {boolean} [range] - * @property {number} [rounding=5] + * @property {number} [rounding=5] the rounding in minutes, pass 0 to show seconds, pass 1 to avoid + * rounding minutes without displaying seconds. * @property {{ buttons?: any }} [slots] * @property {"date" | "datetime"} [type] * @property {NullableDateTime | NullableDateRange} value @@ -143,6 +144,7 @@ const toWeekItem = (weekDayItems) => ({ // Time constants const HOURS = numberRange(0, 24).map((hour) => [hour, String(hour)]); const MINUTES = numberRange(0, 60).map((minute) => [minute, String(minute || 0).padStart(2, "0")]); +const SECONDS = [...MINUTES]; const MERIDIEMS = ["AM", "PM"]; /** @@ -380,14 +382,19 @@ export class DateTimePicker extends Component { ); this.availableHours = HOURS; this.availableMinutes = MINUTES.filter((minute) => !(minute[0] % props.rounding)); + this.availableSeconds = props.rounding ? [] : SECONDS; this.allowedPrecisionLevels = this.filterPrecisionLevels( props.minPrecision, props.maxPrecision ); this.additionalMonth = props.range && !this.env.isSmall; - this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE).endOf("day"); - this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE).startOf("day"); + this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE); + this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE); + if (this.props.type === "date") { + this.maxDate = this.maxDate.endOf("day"); + this.minDate = this.minDate.startOf("day"); + } if (this.maxDate < this.minDate) { throw new Error(`DateTimePicker error: given "maxDate" comes before "minDate".`); diff --git a/addons/web/static/src/core/datetime/datetime_picker.xml b/addons/web/static/src/core/datetime/datetime_picker.xml index 4d99bb29b0c..63555eeda9a 100644 --- a/addons/web/static/src/core/datetime/datetime_picker.xml +++ b/addons/web/static/src/core/datetime/datetime_picker.xml @@ -66,6 +66,13 @@ + + : + + + + + diff --git a/addons/web/static/tests/core/datetime/datetime_picker_tests.js b/addons/web/static/tests/core/datetime/datetime_picker_tests.js index 4710c4dcb3b..9968bce95cd 100644 --- a/addons/web/static/tests/core/datetime/datetime_picker_tests.js +++ b/addons/web/static/tests/core/datetime/datetime_picker_tests.js @@ -872,6 +872,23 @@ QUnit.module("Components", ({ beforeEach }) => { ); }); + QUnit.test("rounding=0 enables seconds picker", async (assert) => { + await mountPicker({ + rounding: 0, + }); + + const [hourSelect, minuteSelect, secondsSelect] = getTimePickers().at(0); + assert.deepEqual(getTexts(hourSelect, "option"), range(24, String)); + assert.deepEqual( + getTexts(minuteSelect, "option"), + range(60, (i) => pad2(i)) + ); + assert.deepEqual( + getTexts(secondsSelect, "option"), + range(60, (i) => pad2(i)) + ); + }); + QUnit.test("no value, select date without handler", async (assert) => { await mountPicker(); @@ -903,6 +920,86 @@ QUnit.module("Components", ({ beforeEach }) => { assert.verifySteps(["2023-04-25T18:00:00", "2023-04-25T18:05:00"]); }); + QUnit.test("minDate with time: selecting out-of-range and in-range times", async (assert) => { + await mountPicker({ + onSelect: (value) => assert.step(formatForStep(value)), + minDate: DateTime.fromISO("2023-04-25T16:00:00.000"), + }); + + const [hourSelect] = getTimePickers().at(0); + await editSelect(hourSelect, null, "15"); + assert.verifySteps([]); + await editSelect(hourSelect, null, "16"); + assert.verifySteps(["2023-04-25T16:00:00"]); + }); + + QUnit.test("maxDate with time: selecting out-of-range and in-range times", async (assert) => { + await mountPicker({ + onSelect: (value) => assert.step(formatForStep(value)), + maxDate: DateTime.fromISO("2023-04-25T16:00:00.000"), + }); + + const [hourSelect] = getTimePickers().at(0); + await editSelect(hourSelect, null, "17"); + assert.verifySteps([]); + await editSelect(hourSelect, null, "16"); + assert.verifySteps(["2023-04-25T16:00:00"]); + }); + + QUnit.test( + "max and min date with time: selecting out-of-range and in-range times", + async (assert) => { + await mountPicker({ + onSelect: (value) => assert.step(formatForStep(value)), + minDate: DateTime.fromISO("2023-04-25T16:00:00.000"), + maxDate: DateTime.fromISO("2023-04-25T16:00:00.000"), + }); + + const [hourSelect] = getTimePickers().at(0); + await editSelect(hourSelect, null, "15"); + await editSelect(hourSelect, null, "17"); + assert.verifySteps([]); + await editSelect(hourSelect, null, "16"); + assert.verifySteps(["2023-04-25T16:00:00"]); + } + ); + + QUnit.test( + "max and min date with time: selecting invalid minutes and making it valid by selecting hours", + async (assert) => { + await mountPicker({ + onSelect: (value) => assert.step(formatForStep(value)), + minDate: DateTime.fromISO("2023-04-25T16:10:00.000"), + maxDate: DateTime.fromISO("2023-04-25T16:50:00.000"), + }); + + const [hourSelect, minuteSelect] = getTimePickers().at(0); + await editSelect(hourSelect, null, "13"); + await editSelect(minuteSelect, null, "30"); + assert.verifySteps([]); + await editSelect(hourSelect, null, "16"); + assert.verifySteps(["2023-04-25T16:30:00"]); + } + ); + + QUnit.test( + "max and min date with time: valid time on invalid day becomes valid when selecting day", + async (assert) => { + await mountPicker({ + onSelect: (value) => assert.step(formatForStep(value)), + minDate: DateTime.fromISO("2023-04-24T16:10:00.000"), + maxDate: DateTime.fromISO("2023-04-24T16:50:00.000"), + }); + + const [hourSelect, minuteSelect] = getTimePickers().at(0); + await editSelect(hourSelect, null, "16"); + await editSelect(minuteSelect, null, "30"); + assert.verifySteps([]); + await click(getPickerCell("24")); + assert.verifySteps(["2023-04-24T16:30:00"]); + } + ); + QUnit.test("single value, select date", async (assert) => { await mountPicker({ value: DateTime.fromObject({ day: 30, hour: 8, minute: 43 }),