mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 03:36:36 +08:00
Signed-off-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Poojan <poojan@infocusp.com> Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Pandaaaa906 <ye.pandaaaa906@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ijas <ijas.ahmd.ap@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: 木之本澪 <kinomotomiovo@gmail.com> Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com> Co-authored-by: 不做了睡大觉 <64798754+stakeswky@users.noreply.github.com> Co-authored-by: User <user@example.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Leilei <138381132+Inlei@users.noreply.github.com> Co-authored-by: HaKu <104669497+haku-ink@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Varun Chawla <34209028+veeceey@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: tda <95275462+tda1017@users.noreply.github.com> Co-authored-by: root <root@DESKTOP-KQLO90N> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: slegarraga <64795732+slegarraga@users.noreply.github.com> Co-authored-by: 99 <wh2099@pm.me> Co-authored-by: Br1an <932039080@qq.com> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: akkoaya <151345394+akkoaya@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: weiguang li <codingpunk@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: HanWenbo <124024253+hwb96@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Stable Genius <stablegenius043@gmail.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Co-authored-by: ふるい <46769295+Echo0ff@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
import type { Dayjs } from 'dayjs'
|
|
import type { DatePickerProps, Period } from '../types'
|
|
import * as React from 'react'
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
PortalToFollowElem,
|
|
PortalToFollowElemContent,
|
|
PortalToFollowElemTrigger,
|
|
} from '@/app/components/base/portal-to-follow-elem'
|
|
import { cn } from '@/utils/classnames'
|
|
import Calendar from '../calendar'
|
|
import TimePickerHeader from '../time-picker/header'
|
|
import TimePickerOptions from '../time-picker/options'
|
|
import { ViewType } from '../types'
|
|
import dayjs, {
|
|
clearMonthMapCache,
|
|
cloneTime,
|
|
getDateWithTimezone,
|
|
getDaysInMonth,
|
|
getHourIn12Hour,
|
|
} from '../utils/dayjs'
|
|
import YearAndMonthPickerFooter from '../year-and-month-picker/footer'
|
|
import YearAndMonthPickerHeader from '../year-and-month-picker/header'
|
|
import YearAndMonthPickerOptions from '../year-and-month-picker/options'
|
|
import DatePickerFooter from './footer'
|
|
import DatePickerHeader from './header'
|
|
|
|
const DatePicker = ({
|
|
value,
|
|
timezone,
|
|
onChange,
|
|
onClear,
|
|
placeholder,
|
|
needTimePicker = true,
|
|
renderTrigger,
|
|
triggerWrapClassName,
|
|
popupZIndexClassname = 'z-[11]',
|
|
noConfirm,
|
|
getIsDateDisabled,
|
|
}: DatePickerProps) => {
|
|
const { t } = useTranslation()
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [view, setView] = useState(ViewType.date)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const isInitial = useRef(true)
|
|
|
|
// Normalize the value to ensure that all subsequent uses are Day.js objects.
|
|
const normalizedValue = useMemo(() => {
|
|
if (!value)
|
|
return undefined
|
|
return dayjs.isDayjs(value) ? value.tz(timezone) : dayjs(value).tz(timezone)
|
|
}, [value, timezone])
|
|
|
|
const inputValue = useRef(normalizedValue).current
|
|
const defaultValue = useRef(getDateWithTimezone({ timezone })).current
|
|
|
|
const [currentDate, setCurrentDate] = useState(inputValue || defaultValue)
|
|
const [selectedDate, setSelectedDate] = useState(inputValue)
|
|
|
|
const [selectedMonth, setSelectedMonth] = useState(() => (inputValue || defaultValue).month())
|
|
const [selectedYear, setSelectedYear] = useState(() => (inputValue || defaultValue).year())
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false)
|
|
setView(ViewType.date)
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (isInitial.current) {
|
|
isInitial.current = false
|
|
return
|
|
}
|
|
clearMonthMapCache()
|
|
if (normalizedValue) {
|
|
const newValue = getDateWithTimezone({ date: normalizedValue, timezone })
|
|
setCurrentDate(newValue)
|
|
setSelectedDate(newValue)
|
|
onChange(newValue)
|
|
}
|
|
else {
|
|
setCurrentDate(prev => getDateWithTimezone({ date: prev, timezone }))
|
|
setSelectedDate(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined)
|
|
}
|
|
}, [timezone])
|
|
|
|
const handleClickTrigger = (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
if (isOpen) {
|
|
setIsOpen(false)
|
|
return
|
|
}
|
|
setView(ViewType.date)
|
|
setIsOpen(true)
|
|
if (normalizedValue) {
|
|
setCurrentDate(normalizedValue)
|
|
setSelectedDate(normalizedValue)
|
|
}
|
|
}
|
|
|
|
const handleClear = (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
setSelectedDate(undefined)
|
|
if (!isOpen)
|
|
onClear()
|
|
}
|
|
|
|
const days = useMemo(() => {
|
|
return getDaysInMonth(currentDate)
|
|
}, [currentDate])
|
|
|
|
const handleClickNextMonth = useCallback(() => {
|
|
setCurrentDate(currentDate.clone().add(1, 'month'))
|
|
}, [currentDate])
|
|
|
|
const handleClickPrevMonth = useCallback(() => {
|
|
setCurrentDate(currentDate.clone().subtract(1, 'month'))
|
|
}, [currentDate])
|
|
|
|
const handleConfirmDate = useCallback((passedInSelectedDate?: Dayjs) => {
|
|
// passedInSelectedDate may be a click event when noConfirm is false
|
|
const nextDate = (dayjs.isDayjs(passedInSelectedDate) ? passedInSelectedDate : selectedDate)
|
|
onChange(nextDate ? nextDate.tz(timezone) : undefined)
|
|
setIsOpen(false)
|
|
}, [selectedDate, onChange, timezone])
|
|
|
|
const handleDateSelect = useCallback((day: Dayjs) => {
|
|
const newDate = cloneTime(day, selectedDate || getDateWithTimezone({ timezone }))
|
|
setCurrentDate(newDate)
|
|
setSelectedDate(newDate)
|
|
if (noConfirm)
|
|
handleConfirmDate(newDate)
|
|
}, [selectedDate, timezone, noConfirm, handleConfirmDate])
|
|
|
|
const handleSelectCurrentDate = () => {
|
|
const newDate = getDateWithTimezone({ timezone })
|
|
setCurrentDate(newDate)
|
|
setSelectedDate(newDate)
|
|
onChange(newDate)
|
|
setIsOpen(false)
|
|
}
|
|
|
|
const handleClickTimePicker = () => {
|
|
if (view === ViewType.date) {
|
|
setView(ViewType.time)
|
|
return
|
|
}
|
|
if (view === ViewType.time)
|
|
setView(ViewType.date)
|
|
}
|
|
|
|
const handleTimeSelect = (hour: string, minute: string, period: Period) => {
|
|
const newTime = cloneTime(dayjs(), dayjs(`1/1/2000 ${hour}:${minute} ${period}`))
|
|
setSelectedDate((prev) => {
|
|
return prev ? cloneTime(prev, newTime) : newTime
|
|
})
|
|
}
|
|
|
|
const handleSelectHour = useCallback((hour: string) => {
|
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
|
handleTimeSelect(hour, selectedTime.minute().toString().padStart(2, '0'), selectedTime.format('A') as Period)
|
|
}, [selectedDate, timezone])
|
|
|
|
const handleSelectMinute = useCallback((minute: string) => {
|
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
|
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), minute, selectedTime.format('A') as Period)
|
|
}, [selectedDate, timezone])
|
|
|
|
const handleSelectPeriod = useCallback((period: Period) => {
|
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
|
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), selectedTime.minute().toString().padStart(2, '0'), period)
|
|
}, [selectedDate, timezone])
|
|
|
|
const handleOpenYearMonthPicker = () => {
|
|
setSelectedMonth(currentDate.month())
|
|
setSelectedYear(currentDate.year())
|
|
setView(ViewType.yearMonth)
|
|
}
|
|
|
|
const handleCloseYearMonthPicker = useCallback(() => {
|
|
setView(ViewType.date)
|
|
}, [])
|
|
|
|
const handleMonthSelect = useCallback((month: number) => {
|
|
setSelectedMonth(month)
|
|
}, [])
|
|
|
|
const handleYearSelect = useCallback((year: number) => {
|
|
setSelectedYear(year)
|
|
}, [])
|
|
|
|
const handleYearMonthCancel = useCallback(() => {
|
|
setView(ViewType.date)
|
|
}, [])
|
|
|
|
const handleYearMonthConfirm = () => {
|
|
setCurrentDate(prev => prev.clone().month(selectedMonth).year(selectedYear))
|
|
setView(ViewType.date)
|
|
}
|
|
|
|
const timeFormat = needTimePicker ? t('dateFormats.displayWithTime', { ns: 'time' }) : t('dateFormats.display', { ns: 'time' })
|
|
const displayValue = normalizedValue?.format(timeFormat) || ''
|
|
const displayTime = selectedDate?.format('hh:mm A') || '--:-- --'
|
|
const placeholderDate = isOpen && selectedDate ? selectedDate.format(timeFormat) : (placeholder || t('defaultPlaceholder', { ns: 'time' }))
|
|
|
|
return (
|
|
<PortalToFollowElem
|
|
open={isOpen}
|
|
onOpenChange={setIsOpen}
|
|
placement="bottom-end"
|
|
>
|
|
<PortalToFollowElemTrigger className={triggerWrapClassName}>
|
|
{renderTrigger
|
|
? (
|
|
renderTrigger({
|
|
value: normalizedValue,
|
|
selectedDate,
|
|
isOpen,
|
|
handleClear,
|
|
handleClickTrigger,
|
|
}))
|
|
: (
|
|
<div
|
|
className="group flex w-[252px] cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt"
|
|
onClick={handleClickTrigger}
|
|
data-testid="date-picker-trigger"
|
|
>
|
|
<input
|
|
className="flex-1 cursor-pointer appearance-none truncate bg-transparent p-1 text-components-input-text-filled
|
|
outline-none system-xs-regular placeholder:text-components-input-text-placeholder"
|
|
readOnly
|
|
value={isOpen ? '' : displayValue}
|
|
placeholder={placeholderDate}
|
|
/>
|
|
<span className={cn('i-ri-calendar-line h-4 w-4 shrink-0 text-text-quaternary', isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', (displayValue || (isOpen && selectedDate)) && 'group-hover:hidden')} />
|
|
<span className={cn('i-ri-close-circle-fill hidden h-4 w-4 shrink-0 text-text-quaternary', (displayValue || (isOpen && selectedDate)) && 'hover:text-text-secondary group-hover:inline-block')} onClick={handleClear} data-testid="date-picker-clear-button" />
|
|
</div>
|
|
)}
|
|
</PortalToFollowElemTrigger>
|
|
<PortalToFollowElemContent className={popupZIndexClassname}>
|
|
<div className="mt-1 w-[252px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5">
|
|
{/* Header */}
|
|
{view === ViewType.date
|
|
? (
|
|
<DatePickerHeader
|
|
handleOpenYearMonthPicker={handleOpenYearMonthPicker}
|
|
currentDate={currentDate}
|
|
onClickNextMonth={handleClickNextMonth}
|
|
onClickPrevMonth={handleClickPrevMonth}
|
|
/>
|
|
)
|
|
: view === ViewType.yearMonth
|
|
? (
|
|
<YearAndMonthPickerHeader
|
|
selectedYear={selectedYear}
|
|
selectedMonth={selectedMonth}
|
|
onClick={handleCloseYearMonthPicker}
|
|
/>
|
|
)
|
|
: (
|
|
<TimePickerHeader />
|
|
)}
|
|
|
|
{/* Content */}
|
|
{
|
|
view === ViewType.date
|
|
? (
|
|
<Calendar
|
|
days={days}
|
|
selectedDate={selectedDate}
|
|
onDateClick={handleDateSelect}
|
|
getIsDateDisabled={getIsDateDisabled}
|
|
/>
|
|
)
|
|
: view === ViewType.yearMonth
|
|
? (
|
|
<YearAndMonthPickerOptions
|
|
selectedMonth={selectedMonth}
|
|
selectedYear={selectedYear}
|
|
handleMonthSelect={handleMonthSelect}
|
|
handleYearSelect={handleYearSelect}
|
|
/>
|
|
)
|
|
: (
|
|
<TimePickerOptions
|
|
selectedTime={selectedDate}
|
|
handleSelectHour={handleSelectHour}
|
|
handleSelectMinute={handleSelectMinute}
|
|
handleSelectPeriod={handleSelectPeriod}
|
|
/>
|
|
)
|
|
}
|
|
|
|
{/* Footer */}
|
|
{
|
|
[ViewType.date, ViewType.time].includes(view) && !noConfirm && (
|
|
<DatePickerFooter
|
|
needTimePicker={needTimePicker}
|
|
displayTime={displayTime}
|
|
view={view}
|
|
handleClickTimePicker={handleClickTimePicker}
|
|
handleSelectCurrentDate={handleSelectCurrentDate}
|
|
handleConfirmDate={handleConfirmDate}
|
|
/>
|
|
)
|
|
}
|
|
{
|
|
![ViewType.date, ViewType.time].includes(view) && (
|
|
<YearAndMonthPickerFooter
|
|
handleYearMonthCancel={handleYearMonthCancel}
|
|
handleYearMonthConfirm={handleYearMonthConfirm}
|
|
/>
|
|
)
|
|
}
|
|
</div>
|
|
</PortalToFollowElemContent>
|
|
</PortalToFollowElem>
|
|
)
|
|
}
|
|
|
|
export default DatePicker
|