From 5be8fbab5618761ccb0800177313f88c04c689a6 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:24:03 +0800 Subject: [PATCH] feat: refactor date-and-time-picker to use custom dayjs utility and add timezone support (#15101) --- .../date-and-time-picker/calendar/item.tsx | 2 +- .../date-picker/header.tsx | 12 +-- .../date-picker/index.tsx | 73 +++++++++++++------ .../base/date-and-time-picker/hooks.ts | 2 +- .../time-picker/index.tsx | 29 ++++++-- .../base/date-and-time-picker/types.ts | 2 + .../{utils.ts => utils/dayjs.ts} | 20 ++++- 7 files changed, 102 insertions(+), 38 deletions(-) rename web/app/components/base/date-and-time-picker/{utils.ts => utils/dayjs.ts} (79%) diff --git a/web/app/components/base/date-and-time-picker/calendar/item.tsx b/web/app/components/base/date-and-time-picker/calendar/item.tsx index 166cc2eb23..7c2cecd438 100644 --- a/web/app/components/base/date-and-time-picker/calendar/item.tsx +++ b/web/app/components/base/date-and-time-picker/calendar/item.tsx @@ -1,7 +1,7 @@ import React, { type FC } from 'react' import type { CalendarItemProps } from '../types' import cn from '@/utils/classnames' -import dayjs from 'dayjs' +import dayjs from '../utils/dayjs' const Item: FC = ({ day, diff --git a/web/app/components/base/date-and-time-picker/date-picker/header.tsx b/web/app/components/base/date-and-time-picker/date-picker/header.tsx index 009e1eaec5..7855c50fab 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/header.tsx +++ b/web/app/components/base/date-and-time-picker/date-picker/header.tsx @@ -22,18 +22,18 @@ const Header: FC = ({ - + ) } diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx index 06dd459ece..67dbec3b90 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx @@ -1,10 +1,16 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import dayjs, { type Dayjs } from 'dayjs' import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react' import cn from '@/utils/classnames' import type { DatePickerProps, Period } from '../types' import { ViewType } from '../types' -import { cloneTime, getDaysInMonth, getHourIn12Hour } from '../utils' +import type { Dayjs } from 'dayjs' +import dayjs, { + clearMonthMapCache, + cloneTime, + getDateWithTimezone, + getDaysInMonth, + getHourIn12Hour, +} from '../utils/dayjs' import { PortalToFollowElem, PortalToFollowElemContent, @@ -22,6 +28,7 @@ import { useTranslation } from 'react-i18next' const DatePicker = ({ value, + timezone, onChange, onClear, placeholder, @@ -32,12 +39,15 @@ const DatePicker = ({ const [isOpen, setIsOpen] = useState(false) const [view, setView] = useState(ViewType.date) const containerRef = useRef(null) + const isInitial = useRef(true) + const inputValue = useRef(value ? value.tz(timezone) : undefined).current + const defaultValue = useRef(getDateWithTimezone({ timezone })).current - const [currentDate, setCurrentDate] = useState(value || dayjs()) - const [selectedDate, setSelectedDate] = useState(value) + const [currentDate, setCurrentDate] = useState(inputValue || defaultValue) + const [selectedDate, setSelectedDate] = useState(inputValue) - const [selectedMonth, setSelectedMonth] = useState((value || dayjs()).month()) - const [selectedYear, setSelectedYear] = useState((value || dayjs()).year()) + const [selectedMonth, setSelectedMonth] = useState((inputValue || defaultValue).month()) + const [selectedYear, setSelectedYear] = useState((inputValue || defaultValue).year()) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -50,6 +60,25 @@ const DatePicker = ({ return () => document.removeEventListener('mousedown', handleClickOutside) }, []) + useEffect(() => { + if (isInitial.current) { + isInitial.current = false + return + } + clearMonthMapCache() + if (value) { + const newValue = getDateWithTimezone({ date: value, timezone }) + setCurrentDate(newValue) + setSelectedDate(newValue) + onChange(newValue) + } + else { + setCurrentDate(prev => getDateWithTimezone({ date: prev, timezone })) + setSelectedDate(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timezone]) + const handleClickTrigger = (e: React.MouseEvent) => { e.stopPropagation() if (isOpen) { @@ -58,15 +87,15 @@ const DatePicker = ({ } setView(ViewType.date) setIsOpen(true) + if (value) { + setCurrentDate(value) + setSelectedDate(value) + } } const handleClear = (e: React.MouseEvent) => { - const newDate = dayjs() e.stopPropagation() setSelectedDate(undefined) - setCurrentDate(prev => prev || newDate) - setSelectedMonth(prev => prev || newDate.month()) - setSelectedYear(prev => prev || newDate.year()) if (!isOpen) onClear() } @@ -84,13 +113,13 @@ const DatePicker = ({ }, [currentDate]) const handleDateSelect = useCallback((day: Dayjs) => { - const newDate = cloneTime(day, selectedDate || dayjs()) + const newDate = cloneTime(day, selectedDate || getDateWithTimezone({ timezone })) setCurrentDate(newDate) setSelectedDate(newDate) - }, [selectedDate]) + }, [selectedDate, timezone]) const handleSelectCurrentDate = () => { - const newDate = dayjs() + const newDate = getDateWithTimezone({ timezone }) setCurrentDate(newDate) setSelectedDate(newDate) onChange(newDate) @@ -119,19 +148,19 @@ const DatePicker = ({ } const handleSelectHour = useCallback((hour: string) => { - const selectedTime = selectedDate || dayjs() + const selectedTime = selectedDate || getDateWithTimezone({ timezone }) handleTimeSelect(hour, selectedTime.minute().toString().padStart(2, '0'), selectedTime.format('A') as Period) - }, [selectedDate]) + }, [selectedDate, timezone]) const handleSelectMinute = useCallback((minute: string) => { - const selectedTime = selectedDate || dayjs() + const selectedTime = selectedDate || getDateWithTimezone({ timezone }) handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), minute, selectedTime.format('A') as Period) - }, [selectedDate]) + }, [selectedDate, timezone]) const handleSelectPeriod = useCallback((period: Period) => { - const selectedTime = selectedDate || dayjs() + const selectedTime = selectedDate || getDateWithTimezone({ timezone }) handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), selectedTime.minute().toString().padStart(2, '0'), period) - }, [selectedDate]) + }, [selectedDate, timezone]) const handleOpenYearMonthPicker = () => { setSelectedMonth(currentDate.month()) @@ -156,15 +185,13 @@ const DatePicker = ({ }, []) const handleYearMonthConfirm = () => { - setCurrentDate((prev) => { - return prev ? prev.clone().month(selectedMonth).year(selectedYear) : dayjs().month(selectedMonth).year(selectedYear) - }) + setCurrentDate(prev => prev.clone().month(selectedMonth).year(selectedYear)) setView(ViewType.date) } const timeFormat = needTimePicker ? 'MMMM D, YYYY hh:mm A' : 'MMMM D, YYYY' const displayValue = value?.format(timeFormat) || '' - const displayTime = (selectedDate || dayjs().startOf('day')).format('hh:mm A') + const displayTime = selectedDate?.format('hh:mm A') || '--:-- --' const placeholderDate = isOpen && selectedDate ? selectedDate.format(timeFormat) : (placeholder || t('time.defaultPlaceholder')) return ( diff --git a/web/app/components/base/date-and-time-picker/hooks.ts b/web/app/components/base/date-and-time-picker/hooks.ts index 0976234b4d..b92a8e24c8 100644 --- a/web/app/components/base/date-and-time-picker/hooks.ts +++ b/web/app/components/base/date-and-time-picker/hooks.ts @@ -1,4 +1,4 @@ -import dayjs from 'dayjs' +import dayjs from './utils/dayjs' import { Period } from './types' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx index 348079e535..1fecda00b7 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' -import dayjs from 'dayjs' import type { Period, TimePickerProps } from '../types' -import { cloneTime, getHourIn12Hour } from '../utils' +import dayjs, { cloneTime, getDateWithTimezone, getHourIn12Hour } from '../utils/dayjs' import { PortalToFollowElem, PortalToFollowElemContent, @@ -16,6 +15,7 @@ import cn from '@/utils/classnames' const TimePicker = ({ value, + timezone, placeholder, onChange, onClear, @@ -24,7 +24,8 @@ const TimePicker = ({ const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) - const [selectedTime, setSelectedTime] = useState(value) + const isInitial = useRef(true) + const [selectedTime, setSelectedTime] = useState(value ? getDateWithTimezone({ timezone, date: value }) : undefined) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -35,6 +36,22 @@ const TimePicker = ({ return () => document.removeEventListener('mousedown', handleClickOutside) }, []) + useEffect(() => { + if (isInitial.current) { + isInitial.current = false + return + } + if (value) { + const newValue = getDateWithTimezone({ date: value, timezone }) + setSelectedTime(newValue) + onChange(newValue) + } + else { + setSelectedTime(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timezone]) + const handleClickTrigger = (e: React.MouseEvent) => { e.stopPropagation() if (isOpen) { @@ -42,6 +59,8 @@ const TimePicker = ({ return } setIsOpen(true) + if (value) + setSelectedTime(value) } const handleClear = (e: React.MouseEvent) => { @@ -74,11 +93,11 @@ const TimePicker = ({ }, [selectedTime]) const handleSelectCurrentTime = useCallback(() => { - const newDate = dayjs() + const newDate = getDateWithTimezone({ timezone }) setSelectedTime(newDate) onChange(newDate) setIsOpen(false) - }, [onChange]) + }, [onChange, timezone]) const handleConfirm = useCallback(() => { onChange(selectedTime) diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 03fc103739..56e0ef6d50 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -21,6 +21,7 @@ type TriggerProps = { export type DatePickerProps = { value: Dayjs | undefined + timezone?: string placeholder?: string needTimePicker?: boolean onChange: (date: Dayjs | undefined) => void @@ -46,6 +47,7 @@ export type DatePickerFooterProps = { export type TimePickerProps = { value: Dayjs | undefined + timezone?: string placeholder?: string onChange: (date: Dayjs | undefined) => void onClear: () => void diff --git a/web/app/components/base/date-and-time-picker/utils.ts b/web/app/components/base/date-and-time-picker/utils/dayjs.ts similarity index 79% rename from web/app/components/base/date-and-time-picker/utils.ts rename to web/app/components/base/date-and-time-picker/utils/dayjs.ts index 6d1258c8df..0928fa5d58 100644 --- a/web/app/components/base/date-and-time-picker/utils.ts +++ b/web/app/components/base/date-and-time-picker/utils/dayjs.ts @@ -1,5 +1,12 @@ -import type { Dayjs } from 'dayjs' -import type { Day } from './types' +import dayjs, { type Dayjs } from 'dayjs' +import type { Day } from '../types' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' + +dayjs.extend(utc) +dayjs.extend(timezone) + +export default dayjs const monthMaps: Record = {} @@ -58,7 +65,16 @@ export const getDaysInMonth = (currentDate: Dayjs) => { return days } +export const clearMonthMapCache = () => { + for (const key in monthMaps) + delete monthMaps[key] +} + export const getHourIn12Hour = (date: Dayjs) => { const hour = date.hour() return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour } + +export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => { + return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone) +}