From ee71625499dec8282a9ad6abb63610a73a7091eb Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 22 Apr 2025 17:37:04 +0200 Subject: [PATCH] ui: fix timepicker and timezone interactions --- .../DateRangeDropdown/DateRangePopup.tsx | 137 +++++++++++++----- 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/frontend/app/components/shared/DateRangeDropdown/DateRangePopup.tsx b/frontend/app/components/shared/DateRangeDropdown/DateRangePopup.tsx index b2f5c65ee..00d55ca70 100644 --- a/frontend/app/components/shared/DateRangeDropdown/DateRangePopup.tsx +++ b/frontend/app/components/shared/DateRangeDropdown/DateRangePopup.tsx @@ -12,60 +12,123 @@ import { getDateRangeFromValue, getDateRangeLabel, } from 'App/dateRange'; -import { DateTime, Interval } from 'luxon'; +import { DateTime, Interval, Settings } from 'luxon'; import styles from './dateRangePopup.module.css'; import { useTranslation } from 'react-i18next'; function DateRangePopup(props: any) { const { t } = useTranslation(); + const [displayDates, setDisplayDates] = React.useState<[Date, Date]>([new Date(), new Date()]); const [range, setRange] = React.useState( props.selectedDateRange || Interval.fromDateTimes(DateTime.now(), DateTime.now()), ); const [value, setValue] = React.useState(null); - const selectCustomRange = (range) => { - let newRange; - if (props.singleDay) { - newRange = Interval.fromDateTimes( - DateTime.fromJSDate(range), - DateTime.fromJSDate(range), - ); - } else { - newRange = Interval.fromDateTimes( - DateTime.fromJSDate(range[0]), - DateTime.fromJSDate(range[1]), - ); - } - setRange(newRange); + React.useEffect(() => { + if (props.selectedDateRange) { + const start = new Date( + props.selectedDateRange.start.year, + props.selectedDateRange.start.month - 1, // JS months are 0-based + props.selectedDateRange.start.day + ); + const end = new Date( + props.selectedDateRange.end.year, + props.selectedDateRange.end.month - 1, + props.selectedDateRange.end.day + ); + setDisplayDates([start, end]); + } + }, [props.selectedDateRange]); + + const createNaiveTime = (dateTime: DateTime) => { + if (!dateTime) return null; + return DateTime.fromObject({ + hour: dateTime.hour, + minute: dateTime.minute + }); + }; + + + const selectCustomRange = (newDates: [Date, Date]) => { + if (!newDates || !newDates[0] || !newDates[1]) return; + + setDisplayDates(newDates); + + const selectedTzStart = DateTime.fromObject({ + year: newDates[0].getFullYear(), + month: newDates[0].getMonth() + 1, + day: newDates[0].getDate(), + hour: 0, + minute: 0 + }).setZone(Settings.defaultZone); + + const selectedTzEnd = DateTime.fromObject({ + year: newDates[1].getFullYear(), + month: newDates[1].getMonth() + 1, + day: newDates[1].getDate(), + hour: 23, + minute: 59 + }).setZone(Settings.defaultZone); + + const updatedRange = Interval.fromDateTimes(selectedTzStart, selectedTzEnd); + setRange(updatedRange); setValue(CUSTOM_RANGE); }; - const setRangeTimeStart = (value: DateTime) => { - if (!range.end || value > range.end) { - return; - } - const newRange = range.start.set({ - hour: value.hour, - minute: value.minute, + const setRangeTimeStart = (naiveTime: DateTime) => { + if (!range.end || !naiveTime) return; + + const newStart = range.start.set({ + hour: naiveTime.hour, + minute: naiveTime.minute }); - setRange(Interval.fromDateTimes(newRange, range.end)); + + if (newStart > range.end) return; + + setRange(Interval.fromDateTimes(newStart, range.end)); setValue(CUSTOM_RANGE); }; - const setRangeTimeEnd = (value: DateTime) => { - if (!range.start || (value && value < range.start)) { - return; - } - const newRange = range.end.set({ hour: value.hour, minute: value.minute }); - setRange(Interval.fromDateTimes(range.start, newRange)); + const setRangeTimeEnd = (naiveTime: DateTime) => { + if (!range.start || !naiveTime) return; + + const newEnd = range.end.set({ + hour: naiveTime.hour, + minute: naiveTime.minute + }); + + if (newEnd < range.start) return; + + setRange(Interval.fromDateTimes(range.start, newEnd)); setValue(CUSTOM_RANGE); }; const selectValue = (value: string) => { - const range = getDateRangeFromValue(value); - setRange(range); + const newRange = getDateRangeFromValue(value); + + if (!newRange.start || !newRange.end) { + setRange(Interval.fromDateTimes(DateTime.now(), DateTime.now())); + setDisplayDates([new Date(), new Date()]); + setValue(null); + return; + } + const zonedStart = newRange.start.setZone(Settings.defaultZone); + const zonedEnd = newRange.end.setZone(Settings.defaultZone); + setRange(Interval.fromDateTimes(zonedStart, zonedEnd)); + + const start = new Date( + zonedStart.year, + zonedStart.month - 1, + zonedStart.day + ); + const end = new Date( + zonedEnd.year, + zonedEnd.month - 1, + zonedEnd.day + ); + setDisplayDates([start, end]); setValue(value); }; @@ -77,9 +140,9 @@ function DateRangePopup(props: any) { const isUSLocale = navigator.language === 'en-US' || navigator.language.startsWith('en-US'); - const rangeForDisplay = props.singleDay - ? range.start.ts - : [range.start!.startOf('day').ts, range.end!.startOf('day').ts]; + const naiveStartTime = createNaiveTime(range.start); + const naiveEndTime = createNaiveTime(range.end); + return (
@@ -103,7 +166,7 @@ function DateRangePopup(props: any) { shouldCloseCalendar={() => false} isOpen maxDate={new Date()} - value={rangeForDisplay} + value={displayDates} calendarProps={{ tileDisabled: props.isTileDisabled, selectRange: !props.singleDay, @@ -122,7 +185,7 @@ function DateRangePopup(props: any) { {range.start.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} {range.end.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')}