Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 16 additions & 128 deletions lib/public/components/common/form/inputs/DateTimeInputComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/
import { h, StatefulComponent } from '/js/src/index.js';
import { iconX } from '/js/src/icons.js';
import { MILLISECONDS_IN_ONE_DAY } from '../../../../utilities/dateUtils.mjs';
import { formatTimestampForDateTimeInput } from '../../../../utilities/formatting/dateTimeInputFormatters.mjs';

/**
Expand All @@ -22,12 +21,11 @@ import { formatTimestampForDateTimeInput } from '../../../../utilities/formattin

/**
* @typedef DateTimeInputComponentAttrs
* @property {DateTimeInputRawData} value the actual inputs values
* @property {string} value the datetime-local input value
* @property {function} onChange function called with the new value when it changes
* @property {Partial<DateTimeInputRawData>} [defaults] the default raw values to use when partially updating inputs
* @property {boolean} [seconds=false] states if the input has granularity up to seconds (if not, granularity is minutes)
* @property {boolean} [milliseconds=false] states if the input has granularity up to milliseconds
* @property {boolean} [required] states if the inputs can be omitted
* @property {boolean} [required] states if the input can be omitted
* @property {number} [min] the timestamp of the minimum date allowed by the input (included)
* @property {number} [max] the timestamp of the maximum date allowed by the input (excluded)
*/
Expand Down Expand Up @@ -71,40 +69,29 @@ export class DateTimeInputComponent extends StatefulComponent {
* @return {Component} the component
*/
view() {
const inputsMin = this._getInputsMin(this._minTimestamp, this._value);
const inputsMax = this._getInputsMax(this._maxTimestamp, this._value);
const inputMin = this._minTimestamp !== null
? formatTimestampForDateTimeInput(this._minTimestamp, this._seconds || this._milliseconds, this._milliseconds)
: '';
const inputMax = this._maxTimestamp !== null
? formatTimestampForDateTimeInput(this._maxTimestamp, this._seconds || this._milliseconds, this._milliseconds)
: '';

return h('.flex-row.items-center.g2', [
h(
'input.form-control',
{
type: 'date',
// Firefox shrinks the date inputs, apply a min-width to it
style: { minWidth: '9rem' },
type: 'datetime-local',
required: this._required,
value: this._value.date,
onchange: (e) => this._patchValue({ date: e.target.value }),
// Mithril do not remove min/max if previously set...
min: inputsMin?.date ?? '',
max: inputsMax?.date ?? '',
},
),
h(
'input.form-control',
{
type: 'time',
required: this._required,
value: this._value.time,
step: this._milliseconds ? 0.001 : this._seconds ? 1 : undefined,
onchange: (e) => this._patchValue({ time: e.target.value }),
// Mithril do not remove min/max if previously set...
min: inputsMin?.time ?? '',
max: inputsMax?.time ?? '',
value: this._value,
step: this._milliseconds ? 0.001 : this._seconds ? 1 : 60,
onchange: (e) => this._onChange(e.target.value),
min: inputMin,
max: inputMax,
},
),
h(
'.btn.btn-pill.f7',
{ disabled: this._value.date === '' && this._value.time === '', onclick: () => this._patchValue({ date: '', time: '' }) },
{ disabled: this._value === '', onclick: () => this._onChange('') },
iconX(),
),
]);
Expand All @@ -118,8 +105,7 @@ export class DateTimeInputComponent extends StatefulComponent {
* @private
*/
_updateAttrs(attrs) {
const { value, onChange, seconds, milliseconds, required = false, defaults = {}, min = null, max = null } = attrs;
const { date: defaultDate = '', time: defaultTime = '' } = defaults;
const { value, onChange, seconds, milliseconds, required = false, min = null, max = null } = attrs;

this._value = value;
this._onChange = onChange;
Expand All @@ -129,105 +115,7 @@ export class DateTimeInputComponent extends StatefulComponent {

this._required = required;

/**
* @type {DateTimeInputRawData}
* @private
*/
this._defaults = {
date: defaultDate,
time: defaultTime,
};

this._minTimestamp = min;
this._maxTimestamp = max;
}

/**
* Update the inputs values
*
* @param {Partial<DateTimeInputRawData>} patch the input values patch
* @return {void}
*/
_patchValue({ time, date }) {
if (date !== undefined) {
this._value.date = date;
if (time === undefined && !this._value.time) {
this._value.time = this._defaults.time;
}
}

if (time !== undefined) {
this._value.time = time;
if (date === undefined && !this._value.date) {
this._value.date = this._defaults.date;
}
}

this._onChange(this._value);
}

/**
* Returns the min values to apply to the inputs
*
* @param {number|null} minTimestamp the minimal timestamp to represent in the inputs
* @param {DateTimeInputRawData} raw the current raw values
* @return {(Partial<{date: string, time: string}>|null)} the min values to apply to date and time inputs
*/
_getInputsMin(minTimestamp, raw) {
if (minTimestamp === null) {
return null;
}

const rawDate = raw.date || null;
const rawTime = raw.time || null;

const minDateAndTime = formatTimestampForDateTimeInput(minTimestamp, this._seconds || this._milliseconds, this._milliseconds);
const inputsMin = {};

if (rawDate !== null && rawDate === minDateAndTime.date) {
inputsMin.time = minDateAndTime.time;
}

if (rawTime !== null && rawTime < minDateAndTime.time) {
inputsMin.date = formatTimestampForDateTimeInput(
minTimestamp + MILLISECONDS_IN_ONE_DAY,
this._seconds || this._milliseconds,
this._milliseconds,
).date;
} else {
inputsMin.date = minDateAndTime.date;
}

return inputsMin;
}

/**
* Returns the max values to apply to the inputs
*
* @param {number|null} maxTimestamp the maximal timestamp to represent in the inputs
* @param {DateTimeInputRawData} raw the current raw values
* @return {(Partial<{date: string, time: string}>|null)} the max values
*/
_getInputsMax(maxTimestamp, raw) {
if (maxTimestamp === null) {
return null;
}

const rawDate = raw.date || null;
const rawTime = raw.time || null;

const maxDateAndTime = formatTimestampForDateTimeInput(maxTimestamp, this._seconds, this._milliseconds);
const inputsMax = {};

if (rawDate !== null && rawDate === maxDateAndTime.date) {
inputsMax.time = maxDateAndTime.time;
}
if (rawTime !== null && rawTime > maxDateAndTime.time) {
inputsMax.date = formatTimestampForDateTimeInput(maxTimestamp - MILLISECONDS_IN_ONE_DAY, this._seconds, this._milliseconds).date;
} else {
inputsMax.date = maxDateAndTime.date;
}

return inputsMax;
}
}
31 changes: 9 additions & 22 deletions lib/public/components/common/form/inputs/DateTimeInputModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,6 @@ import {
} from '../../../../utilities/formatting/dateTimeInputFormatters.mjs';
import { Observable } from '/js/src/index.js';

/**
* @typedef DateTimeInputRawData
* @property {string} date the raw date value
* @property {string} time the raw time value
*/

/**
* @typedef DateTimeInputConfiguration
* @property {Partial<DateTimeInputRawData>} [defaults] the default raw values to use when partially updating inputs
* @property {boolean} [required] states if the input can be null
*/

/**
* Store the state of a date time model
*/
Expand All @@ -47,10 +35,10 @@ export class DateTimeInputModel extends Observable {
this._milliseconds = milliseconds;

/**
* @type {DateTimeInputRawData}
* @type {string}
* @private
*/
this._raw = { date: '', time: '' };
this._raw = '';

/**
* @type {number|null}
Expand All @@ -60,22 +48,21 @@ export class DateTimeInputModel extends Observable {
}

/**
* Update the inputs raw values
* Update the input raw value
*
* @param {DateTimeInputRawData} raw the input raw values
* @param {string} raw the input raw value (datetime-local string)
* @return {void}
*/
update(raw) {
this._raw = raw;
const hasDateAndTime = raw.date && raw.time;

try {
this._value = hasDateAndTime ? extractTimestampFromDateTimeInput(raw) : null;
this._value = raw ? extractTimestampFromDateTimeInput(raw) : null;
} catch {
this._value = null;
}

hasDateAndTime && this.notify();
this.notify();
}

/**
Expand All @@ -96,9 +83,9 @@ export class DateTimeInputModel extends Observable {
}

/**
* Returns the raw input values
* Returns the raw input value
*
* @return {DateTimeInputRawData} the raw values
* @return {string} the raw datetime-local value
*/
get raw() {
return this._raw;
Expand Down Expand Up @@ -132,7 +119,7 @@ export class DateTimeInputModel extends Observable {
this._value = value;
this._raw = value !== null
? formatTimestampForDateTimeInput(value, this._seconds || this._milliseconds, this._milliseconds)
: { date: '', time: '' };
: '';

!silent && this.notify();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ export class TimestampInputComponent extends StatefulComponent {
const { seconds = false, onChange, value = null } = attrs;

/**
* @type {DateTimeInputRawData}
* @type {string} - format YYYY-MM-DDTHH:MM[:SS[.mmm]]
* @private
*/
this._raw = '';

/**
* @type {number|null} - timestamp in ms
* @private
*/
this._raw = { date: '', time: '' };
this._value = value;

this._onChange = onChange;
Expand All @@ -73,17 +78,17 @@ export class TimestampInputComponent extends StatefulComponent {
}

/**
* Handle change of date/time input and trigger the onChange
* Handle change of datetime-local input and trigger the onChange
*
* @param {DateTimeInputRawData} raw the date/time input raw data
* @param {string} raw the datetime-local input value
* @return {void}
* @private
*/
_handleDateTimeChange(raw) {
this._raw = raw;
let value;
try {
value = raw.date && raw.time ? extractTimestampFromDateTimeInput(raw) : null;
value = raw ? extractTimestampFromDateTimeInput(raw) : null;
} catch {
value = null;
}
Expand Down
38 changes: 14 additions & 24 deletions lib/public/utilities/formatting/dateTimeInputFormatters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
*/

/**
* Returns the given date formatted in two parts, YYYY-MM-DD and HH:MM to fill in HTML date and time inputs (in the user's timezone)
* Returns the given timestamp formatted as a datetime-local input value string (YYYY-MM-DDTHH:MM[:SS[.mmm]],
* in the user's timezone)
*
* @param {number} timestamp the timestamp (ms) to format
* @param {boolean} enableSeconds states if the time input granularity is seconds (else, it is minutes)
* @param {boolean} enableMilliseconds states if the time input granularity is milliseconds
* @return {DateTimeInputRawData} the date expression to use as HTML input values
* @param {boolean} enableSeconds states if the input granularity is seconds (else, it is minutes)
* @param {boolean} enableMilliseconds states if the input granularity is milliseconds
* @return {string} the value to use as a datetime-local HTML input value
*/
export const formatTimestampForDateTimeInput = (timestamp, enableSeconds, enableMilliseconds) => {
const date = new Date(timestamp);
Expand All @@ -32,37 +33,26 @@ export const formatTimestampForDateTimeInput = (timestamp, enableSeconds, enable
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const dateExpression = `${year}-${month}-${day}`;

const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
let timeExpression = `${hours}:${minutes}`;
if (enableSeconds) {

let value = `${year}-${month}-${day}T${hours}:${minutes}`;
if (enableSeconds || enableMilliseconds) {
const seconds = pad(date.getSeconds());
timeExpression = `${timeExpression}:${seconds}`;
value = `${value}:${seconds}`;
}
if (enableMilliseconds) {
const milliseconds = `${date.getMilliseconds()}`.padStart(3, '0');
timeExpression = `${timeExpression}.${milliseconds}`;
value = `${value}.${milliseconds}`;
}

return { date: dateExpression, time: timeExpression };
return value;
};

/**
* Convert the given raw date-time (in the user's timezone) input to UNIX timestamp
* Convert the given datetime-local input value (in the user's timezone) to a UNIX timestamp
*
* @param {DateTimeInputRawData} dateTime the date-time input's value
* @param {string} value the datetime-local input value (YYYY-MM-DDTHH:MM[:SS[.mmm]])
* @return {number} the resulting timestamp
*/
export const extractTimestampFromDateTimeInput = ({ date, time }) => {
if (time.length === 5) {
// HH:MM -> HH:MM:SS.mmm
time = `${time}:00.000`;
} else if (time.length === 8) {
// HH:MM:SS -> HH:MM:SS.mmm
time = `${time}.000`;
}

return Date.parse(`${date}T${time}`);
};
export const extractTimestampFromDateTimeInput = (value) => Date.parse(value);
7 changes: 3 additions & 4 deletions test/public/Filters/FilteringModel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ module.exports = () => {
const baseRowCount = 109;
const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger'));

const { fromDateSelector, fromTimeSelector } = getPeriodInputsSelectors(startPopoverSelector);
const { fromDateTimeSelector } = getPeriodInputsSelectors(startPopoverSelector);

await checkSelectionFilters(runSelectionFiltersChecks, baseRowCount);

Expand Down Expand Up @@ -105,10 +105,9 @@ module.exports = () => {
await waitForTableTotalRowsCountToEqual(page, baseRowCount);

// O2 Start Filter:
await fillInput(page, fromTimeSelector, '11:11', ['change']);
await fillInput(page, fromDateSelector, '2021-02-03', ['change']);
await fillInput(page, fromDateTimeSelector, '2021-02-03T11:11', ['change']);
await waitForTableTotalRowsCountToEqual(page, 1);
await fillInput(page, fromDateSelector, '2020-02-03', ['change']);
await fillInput(page, fromDateTimeSelector, '2020-02-03T11:11', ['change']);
await waitForTableTotalRowsCountToEqual(page, 2);
await page.goBack();
await waitForTableTotalRowsCountToEqual(page, 1);
Expand Down
Loading
Loading