Files
WeFlow/src/components/Export/ExportDateRangeDialog.tsx
2026-04-12 07:10:59 +08:00

756 lines
27 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react'
import {
EXPORT_DATE_RANGE_PRESETS,
WEEKDAY_SHORT_LABELS,
addMonths,
buildCalendarCells,
cloneExportDateRangeSelection,
createDateRangeByPreset,
createDefaultDateRange,
formatCalendarMonthTitle,
isSameDay,
parseDateInputValue,
startOfDay,
endOfDay,
toMonthStart,
type ExportDateRangePreset,
type ExportDateRangeSelection
} from '../../utils/exportDateRange'
import './ExportDateRangeDialog.scss'
interface ExportDateRangeDialogProps {
open: boolean
value: ExportDateRangeSelection
title?: string
minDate?: Date | null
maxDate?: Date | null
onClose: () => void
onConfirm: (value: ExportDateRangeSelection) => void
}
type ActiveBoundary = 'start' | 'end'
interface ExportDateRangeDialogDraft extends ExportDateRangeSelection {
panelMonth: Date
}
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, index) => `${index}`.padStart(2, '0'))
const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, index) => `${index}`.padStart(2, '0'))
const QUICK_TIME_OPTIONS = ['00:00', '08:00', '12:00', '18:00', '23:59']
const resolveBounds = (minDate?: Date | null, maxDate?: Date | null): { minDate: Date; maxDate: Date } | null => {
if (!(minDate instanceof Date) || Number.isNaN(minDate.getTime())) return null
if (!(maxDate instanceof Date) || Number.isNaN(maxDate.getTime())) return null
const normalizedMin = startOfDay(minDate)
const normalizedMax = endOfDay(maxDate)
if (normalizedMin.getTime() > normalizedMax.getTime()) return null
return {
minDate: normalizedMin,
maxDate: normalizedMax
}
}
const clampSelectionToBounds = (
value: ExportDateRangeSelection,
minDate?: Date | null,
maxDate?: Date | null
): ExportDateRangeSelection => {
const bounds = resolveBounds(minDate, maxDate)
if (!bounds) return cloneExportDateRangeSelection(value)
// For custom selections, only ensure end >= start, preserve time precision
if (value.preset === 'custom' && !value.useAllTime) {
const { start, end } = value.dateRange
if (end.getTime() < start.getTime()) {
return {
...value,
dateRange: { start, end: start }
}
}
return cloneExportDateRangeSelection(value)
}
// For useAllTime, use bounds directly
if (value.useAllTime) {
return {
preset: value.preset,
useAllTime: true,
dateRange: {
start: bounds.minDate,
end: bounds.maxDate
}
}
}
// For preset selections (not custom), clamp dates to bounds and use default times
const nextStart = new Date(Math.min(Math.max(value.dateRange.start.getTime(), bounds.minDate.getTime()), bounds.maxDate.getTime()))
const nextEndCandidate = new Date(Math.min(Math.max(value.dateRange.end.getTime(), bounds.minDate.getTime()), bounds.maxDate.getTime()))
const nextEnd = nextEndCandidate.getTime() < nextStart.getTime() ? nextStart : nextEndCandidate
// Set default times: start at 00:00:00, end at 23:59:59
nextStart.setHours(0, 0, 0, 0)
nextEnd.setHours(23, 59, 59, 999)
return {
preset: value.preset,
useAllTime: false,
dateRange: {
start: nextStart,
end: nextEnd
}
}
}
const buildDialogDraft = (
value: ExportDateRangeSelection,
minDate?: Date | null,
maxDate?: Date | null
): ExportDateRangeDialogDraft => {
const nextValue = clampSelectionToBounds(value, minDate, maxDate)
return {
...nextValue,
panelMonth: toMonthStart(nextValue.dateRange.start)
}
}
export function ExportDateRangeDialog({
open,
value,
title = '时间范围设置',
minDate,
maxDate,
onClose,
onConfirm
}: ExportDateRangeDialogProps) {
// Helper: Format date only (YYYY-MM-DD) for the date input field
const formatDateOnly = (date: Date): string => {
const y = date.getFullYear()
const m = `${date.getMonth() + 1}`.padStart(2, '0')
const d = `${date.getDate()}`.padStart(2, '0')
return `${y}-${m}-${d}`
}
// Helper: Format time only (HH:mm) for the time input field
const formatTimeOnly = (date: Date): string => {
const h = `${date.getHours()}`.padStart(2, '0')
const m = `${date.getMinutes()}`.padStart(2, '0')
return `${h}:${m}`
}
const [draft, setDraft] = useState<ExportDateRangeDialogDraft>(() => buildDialogDraft(value, minDate, maxDate))
const [activeBoundary, setActiveBoundary] = useState<ActiveBoundary>('start')
const [dateInput, setDateInput] = useState({
start: formatDateOnly(value.dateRange.start),
end: formatDateOnly(value.dateRange.end)
})
const [dateInputError, setDateInputError] = useState({ start: false, end: false })
// Default times: start at 00:00, end at 23:59
const [timeInput, setTimeInput] = useState({
start: '00:00',
end: '23:59'
})
const [openTimeDropdown, setOpenTimeDropdown] = useState<ActiveBoundary | null>(null)
const startTimeSelectRef = useRef<HTMLDivElement>(null)
const endTimeSelectRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
const nextDraft = buildDialogDraft(value, minDate, maxDate)
setDraft(nextDraft)
setActiveBoundary('start')
setDateInput({
start: formatDateOnly(nextDraft.dateRange.start),
end: formatDateOnly(nextDraft.dateRange.end)
})
// For preset-based selections (not custom), use default times 00:00 and 23:59
// For custom selections, preserve the time from value.dateRange
if (nextDraft.useAllTime || nextDraft.preset !== 'custom') {
setTimeInput({
start: '00:00',
end: '23:59'
})
} else {
setTimeInput({
start: formatTimeOnly(nextDraft.dateRange.start),
end: formatTimeOnly(nextDraft.dateRange.end)
})
}
setOpenTimeDropdown(null)
setDateInputError({ start: false, end: false })
}, [maxDate, minDate, open, value])
useEffect(() => {
if (!open) return
setDateInput({
start: formatDateOnly(draft.dateRange.start),
end: formatDateOnly(draft.dateRange.end)
})
// Don't sync timeInput here - it's controlled by the time picker
setDateInputError({ start: false, end: false })
}, [draft.dateRange.end.getTime(), draft.dateRange.start.getTime(), open])
useEffect(() => {
if (!openTimeDropdown) return
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node
const activeContainer = openTimeDropdown === 'start'
? startTimeSelectRef.current
: endTimeSelectRef.current
if (!activeContainer?.contains(target)) {
setOpenTimeDropdown(null)
}
}
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOpenTimeDropdown(null)
}
}
document.addEventListener('mousedown', handlePointerDown)
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('mousedown', handlePointerDown)
document.removeEventListener('keydown', handleEscape)
}
}, [openTimeDropdown])
const bounds = useMemo(() => resolveBounds(minDate, maxDate), [maxDate, minDate])
const clampStartDate = useCallback((targetDate: Date) => {
if (!bounds) return targetDate
const min = bounds.minDate
const max = bounds.maxDate
if (targetDate.getTime() < min.getTime()) return min
if (targetDate.getTime() > max.getTime()) return max
return targetDate
}, [bounds])
const clampEndDate = useCallback((targetDate: Date) => {
if (!bounds) return targetDate
const min = bounds.minDate
const max = bounds.maxDate
if (targetDate.getTime() < min.getTime()) return min
if (targetDate.getTime() > max.getTime()) return max
return targetDate
}, [bounds])
const setRangeStart = useCallback((targetDate: Date) => {
const start = clampStartDate(targetDate)
setDraft(prev => {
return {
...prev,
preset: 'custom',
useAllTime: false,
dateRange: {
start,
end: prev.dateRange.end
},
panelMonth: toMonthStart(start)
}
})
}, [clampStartDate])
const setRangeEnd = useCallback((targetDate: Date) => {
const end = clampEndDate(targetDate)
setDraft(prev => {
const nextStart = prev.useAllTime ? clampStartDate(targetDate) : prev.dateRange.start
return {
...prev,
preset: 'custom',
useAllTime: false,
dateRange: {
start: nextStart,
end: end
},
panelMonth: toMonthStart(targetDate)
}
})
}, [clampEndDate, clampStartDate])
const applyPreset = useCallback((preset: Exclude<ExportDateRangePreset, 'custom'>) => {
if (preset === 'all') {
const previewRange = bounds
? { start: bounds.minDate, end: bounds.maxDate }
: createDefaultDateRange()
setTimeInput({
start: '00:00',
end: '23:59'
})
setOpenTimeDropdown(null)
setDraft(prev => ({
...prev,
preset,
useAllTime: true,
dateRange: previewRange,
panelMonth: toMonthStart(previewRange.start)
}))
setActiveBoundary('start')
return
}
const range = clampSelectionToBounds({
preset,
useAllTime: false,
dateRange: createDateRangeByPreset(preset)
}, minDate, maxDate).dateRange
setTimeInput({
start: '00:00',
end: '23:59'
})
setOpenTimeDropdown(null)
setDraft(prev => ({
...prev,
preset,
useAllTime: false,
dateRange: range,
panelMonth: toMonthStart(range.start)
}))
setActiveBoundary('start')
}, [bounds, maxDate, minDate])
const parseTimeValue = (timeStr: string): { hours: number; minutes: number } | null => {
const matched = /^(\d{1,2}):(\d{2})$/.exec(timeStr.trim())
if (!matched) return null
const hours = Number(matched[1])
const minutes = Number(matched[2])
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null
return { hours, minutes }
}
const updateBoundaryTime = useCallback((boundary: ActiveBoundary, timeStr: string) => {
setTimeInput(prev => ({ ...prev, [boundary]: timeStr }))
const parsedTime = parseTimeValue(timeStr)
if (!parsedTime) return
setDraft(prev => {
const dateObj = boundary === 'start' ? prev.dateRange.start : prev.dateRange.end
const newDate = new Date(dateObj)
newDate.setHours(parsedTime.hours, parsedTime.minutes, 0, 0)
return {
...prev,
preset: 'custom',
useAllTime: false,
dateRange: {
...prev.dateRange,
[boundary]: newDate
}
}
})
}, [])
const toggleTimeDropdown = useCallback((boundary: ActiveBoundary) => {
setActiveBoundary(boundary)
setOpenTimeDropdown(prev => (prev === boundary ? null : boundary))
}, [])
const handleTimeColumnSelect = useCallback((boundary: ActiveBoundary, field: 'hour' | 'minute', value: string) => {
const parsedCurrent = parseTimeValue(timeInput[boundary]) ?? {
hours: boundary === 'start' ? 0 : 23,
minutes: boundary === 'start' ? 0 : 59
}
const nextHours = field === 'hour' ? Number(value) : parsedCurrent.hours
const nextMinutes = field === 'minute' ? Number(value) : parsedCurrent.minutes
updateBoundaryTime(boundary, `${`${nextHours}`.padStart(2, '0')}:${`${nextMinutes}`.padStart(2, '0')}`)
}, [timeInput, updateBoundaryTime])
const renderTimeDropdown = (boundary: ActiveBoundary) => {
const currentTime = timeInput[boundary]
const parsedCurrent = parseTimeValue(currentTime) ?? {
hours: boundary === 'start' ? 0 : 23,
minutes: boundary === 'start' ? 0 : 59
}
return (
<div className="export-date-range-time-dropdown" onClick={(event) => event.stopPropagation()}>
<div className="export-date-range-time-dropdown-header">
<span>{boundary === 'start' ? '开始时间' : '结束时间'}</span>
<strong>{currentTime}</strong>
</div>
<div className="export-date-range-time-quick-list">
{QUICK_TIME_OPTIONS.map(option => (
<button
key={`${boundary}-${option}`}
type="button"
className={`export-date-range-time-quick-item ${currentTime === option ? 'active' : ''}`}
onClick={() => updateBoundaryTime(boundary, option)}
>
{option}
</button>
))}
</div>
<div className="export-date-range-time-columns">
<div className="export-date-range-time-column">
<span className="export-date-range-time-column-label"></span>
<div className="export-date-range-time-column-list">
{HOUR_OPTIONS.map(option => (
<button
key={`${boundary}-hour-${option}`}
type="button"
className={`export-date-range-time-option ${parsedCurrent.hours === Number(option) ? 'active' : ''}`}
onClick={() => handleTimeColumnSelect(boundary, 'hour', option)}
>
{option}
</button>
))}
</div>
</div>
<div className="export-date-range-time-column">
<span className="export-date-range-time-column-label"></span>
<div className="export-date-range-time-column-list">
{MINUTE_OPTIONS.map(option => (
<button
key={`${boundary}-minute-${option}`}
type="button"
className={`export-date-range-time-option ${parsedCurrent.minutes === Number(option) ? 'active' : ''}`}
onClick={() => handleTimeColumnSelect(boundary, 'minute', option)}
>
{option}
</button>
))}
</div>
</div>
</div>
</div>
)
}
// Check if date input string contains time (YYYY-MM-DD HH:mm format)
const dateInputHasTime = (dateStr: string): boolean => /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(dateStr.trim())
const commitStartFromInput = useCallback(() => {
const parsedDate = parseDateInputValue(dateInput.start)
if (!parsedDate) {
setDateInputError(prev => ({ ...prev, start: true }))
return
}
// Only apply time picker value if date input doesn't contain time
if (!dateInputHasTime(dateInput.start)) {
const parsedTime = parseTimeValue(timeInput.start)
if (parsedTime) {
parsedDate.setHours(parsedTime.hours, parsedTime.minutes, 0, 0)
}
}
setDateInputError(prev => ({ ...prev, start: false }))
setRangeStart(parsedDate)
}, [dateInput.start, timeInput.start, setRangeStart])
const commitEndFromInput = useCallback(() => {
const parsedDate = parseDateInputValue(dateInput.end)
if (!parsedDate) {
setDateInputError(prev => ({ ...prev, end: true }))
return
}
// Only apply time picker value if date input doesn't contain time
if (!dateInputHasTime(dateInput.end)) {
const parsedTime = parseTimeValue(timeInput.end)
if (parsedTime) {
parsedDate.setHours(parsedTime.hours, parsedTime.minutes, 0, 0)
}
}
setDateInputError(prev => ({ ...prev, end: false }))
setRangeEnd(parsedDate)
}, [dateInput.end, timeInput.end, setRangeEnd])
const shiftPanelMonth = useCallback((delta: number) => {
setDraft(prev => ({
...prev,
panelMonth: addMonths(prev.panelMonth, delta)
}))
}, [])
const handleCalendarSelect = useCallback((targetDate: Date) => {
// Use time from timeInput state (which is updated by the time picker)
const parseTime = (timeStr: string): { hours: number; minutes: number } => {
const matched = /^(\d{1,2}):(\d{2})$/.exec(timeStr.trim())
if (!matched) return { hours: 0, minutes: 0 }
return { hours: Number(matched[1]), minutes: Number(matched[2]) }
}
if (activeBoundary === 'start') {
const newStart = new Date(targetDate)
const time = parseTime(timeInput.start)
newStart.setHours(time.hours, time.minutes, 0, 0)
setRangeStart(newStart)
setActiveBoundary('end')
setOpenTimeDropdown(null)
return
}
const pickedStart = startOfDay(targetDate)
const start = draft.useAllTime ? startOfDay(targetDate) : draft.dateRange.start
const nextStart = pickedStart <= start ? pickedStart : start
const newEnd = new Date(targetDate)
const time = parseTime(timeInput.end)
// If selecting same day or going backwards, use 23:59:59, otherwise use the time from timeInput
if (pickedStart <= start) {
newEnd.setHours(23, 59, 59, 999)
setTimeInput(prev => ({ ...prev, end: '23:59' }))
} else {
newEnd.setHours(time.hours, time.minutes, 59, 999)
}
setDraft(prev => ({
...prev,
preset: 'custom',
useAllTime: false,
dateRange: {
start: nextStart,
end: newEnd
},
panelMonth: toMonthStart(targetDate)
}))
setActiveBoundary('start')
setOpenTimeDropdown(null)
}, [activeBoundary, draft.dateRange.start, draft.useAllTime, timeInput.end, timeInput.start, setRangeStart])
const isRangeModeActive = !draft.useAllTime
const modeText = isRangeModeActive
? '当前导出模式:按时间范围导出'
: '当前导出模式:全部时间导出,选择下方日期会切换为自定义时间范围'
const isPresetActive = useCallback((preset: ExportDateRangePreset): boolean => {
if (preset === 'all') return draft.useAllTime
return !draft.useAllTime && draft.preset === preset
}, [draft])
const calendarCells = useMemo(() => buildCalendarCells(draft.panelMonth), [draft.panelMonth])
const minPanelMonth = bounds ? toMonthStart(bounds.minDate) : null
const maxPanelMonth = bounds ? toMonthStart(bounds.maxDate) : null
const canShiftPrev = !minPanelMonth || draft.panelMonth.getTime() > minPanelMonth.getTime()
const canShiftNext = !maxPanelMonth || draft.panelMonth.getTime() < maxPanelMonth.getTime()
const isStartSelected = useCallback((date: Date) => (
!draft.useAllTime && isSameDay(date, draft.dateRange.start)
), [draft])
const isEndSelected = useCallback((date: Date) => (
!draft.useAllTime && isSameDay(date, draft.dateRange.end)
), [draft])
const isDateInRange = useCallback((date: Date) => (
!draft.useAllTime &&
startOfDay(date).getTime() >= startOfDay(draft.dateRange.start).getTime() &&
startOfDay(date).getTime() <= startOfDay(draft.dateRange.end).getTime()
), [draft])
const isDateSelectable = useCallback((date: Date) => {
if (!bounds) return true
const target = startOfDay(date).getTime()
return target >= startOfDay(bounds.minDate).getTime() && target <= startOfDay(bounds.maxDate).getTime()
}, [bounds])
const hintText = draft.useAllTime
? '选择开始或结束日期后,会自动切换为自定义时间范围'
: (activeBoundary === 'start' ? '下一次点击将设置开始日期' : '下一次点击将设置结束日期')
if (!open) return null
return createPortal(
<div className="export-date-range-dialog-overlay" onClick={onClose}>
<div className="export-date-range-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
<div className="export-date-range-dialog-header">
<h4>{title}</h4>
<button
type="button"
className="export-date-range-dialog-close-btn"
onClick={onClose}
aria-label="关闭时间范围设置"
>
<X size={14} />
</button>
</div>
<div className="export-date-range-preset-list">
{EXPORT_DATE_RANGE_PRESETS.map((preset) => {
const active = isPresetActive(preset.value)
return (
<button
key={preset.value}
type="button"
className={`export-date-range-preset-item ${active ? 'active' : ''}`}
onClick={() => applyPreset(preset.value)}
>
<span>{preset.label}</span>
{active && <Check size={14} />}
</button>
)
})}
</div>
<div className={`export-date-range-mode-banner ${isRangeModeActive ? 'range' : 'all'}`}>
{modeText}
</div>
<div className="export-date-range-boundary-row">
<div
className={`export-date-range-boundary-card ${activeBoundary === 'start' ? 'active' : ''}`}
onClick={() => setActiveBoundary('start')}
>
<span className="boundary-label"></span>
<input
type="text"
className={`export-date-range-date-input ${dateInputError.start ? 'invalid' : ''}`}
value={dateInput.start}
placeholder="YYYY-MM-DD"
onChange={(event) => {
const nextValue = event.target.value
setDateInput(prev => ({ ...prev, start: nextValue }))
if (dateInputError.start) {
setDateInputError(prev => ({ ...prev, start: false }))
}
}}
onFocus={() => setActiveBoundary('start')}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (event.key !== 'Enter') return
event.preventDefault()
commitStartFromInput()
}}
onBlur={commitStartFromInput}
/>
<div
className={`export-date-range-time-select ${openTimeDropdown === 'start' ? 'open' : ''}`}
ref={startTimeSelectRef}
onClick={(event) => event.stopPropagation()}
>
<button
type="button"
className="export-date-range-time-trigger"
onClick={() => toggleTimeDropdown('start')}
aria-haspopup="dialog"
aria-expanded={openTimeDropdown === 'start'}
>
<span className="export-date-range-time-trigger-value">{timeInput.start}</span>
<ChevronDown size={14} />
</button>
{openTimeDropdown === 'start' && renderTimeDropdown('start')}
</div>
</div>
<div
className={`export-date-range-boundary-card ${activeBoundary === 'end' ? 'active' : ''}`}
onClick={() => setActiveBoundary('end')}
>
<span className="boundary-label"></span>
<input
type="text"
className={`export-date-range-date-input ${dateInputError.end ? 'invalid' : ''}`}
value={dateInput.end}
placeholder="YYYY-MM-DD"
onChange={(event) => {
const nextValue = event.target.value
setDateInput(prev => ({ ...prev, end: nextValue }))
if (dateInputError.end) {
setDateInputError(prev => ({ ...prev, end: false }))
}
}}
onFocus={() => setActiveBoundary('end')}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (event.key !== 'Enter') return
event.preventDefault()
commitEndFromInput()
}}
onBlur={commitEndFromInput}
/>
<div
className={`export-date-range-time-select ${openTimeDropdown === 'end' ? 'open' : ''}`}
ref={endTimeSelectRef}
onClick={(event) => event.stopPropagation()}
>
<button
type="button"
className="export-date-range-time-trigger"
onClick={() => toggleTimeDropdown('end')}
aria-haspopup="dialog"
aria-expanded={openTimeDropdown === 'end'}
>
<span className="export-date-range-time-trigger-value">{timeInput.end}</span>
<ChevronDown size={14} />
</button>
{openTimeDropdown === 'end' && renderTimeDropdown('end')}
</div>
</div>
</div>
<div className="export-date-range-selection-hint">{hintText}</div>
<section className="export-date-range-calendar-panel single">
<div className="export-date-range-calendar-panel-header">
<div className="export-date-range-calendar-date-label">
<span></span>
<strong>{formatCalendarMonthTitle(draft.panelMonth)}</strong>
</div>
<div className="export-date-range-calendar-nav">
<button type="button" onClick={() => shiftPanelMonth(-1)} aria-label="上个月" disabled={!canShiftPrev}>
<ChevronLeft size={14} />
</button>
<button type="button" onClick={() => shiftPanelMonth(1)} aria-label="下个月" disabled={!canShiftNext}>
<ChevronRight size={14} />
</button>
</div>
</div>
<div className="export-date-range-calendar-weekdays">
{WEEKDAY_SHORT_LABELS.map(label => (
<span key={`weekday-${label}`}>{label}</span>
))}
</div>
<div className="export-date-range-calendar-days">
{calendarCells.map((cell) => {
const startSelected = isStartSelected(cell.date)
const endSelected = isEndSelected(cell.date)
const inRange = isDateInRange(cell.date)
const selectable = isDateSelectable(cell.date)
return (
<button
key={cell.date.getTime()}
type="button"
disabled={!selectable}
className={[
'export-date-range-calendar-day',
cell.inCurrentMonth ? '' : 'outside',
selectable ? '' : 'disabled',
inRange ? 'in-range' : '',
startSelected ? 'range-start' : '',
endSelected ? 'range-end' : '',
activeBoundary === 'start' && startSelected ? 'active-boundary' : '',
activeBoundary === 'end' && endSelected ? 'active-boundary' : ''
].filter(Boolean).join(' ')}
onClick={() => handleCalendarSelect(cell.date)}
>
{cell.date.getDate()}
</button>
)
})}
</div>
</section>
<div className="export-date-range-dialog-actions">
<button type="button" className="export-date-range-dialog-btn secondary" onClick={onClose}>
</button>
<button
type="button"
className="export-date-range-dialog-btn primary"
onClick={() => {
// Validate: end time should not be earlier than start time
if (draft.dateRange.end.getTime() < draft.dateRange.start.getTime()) {
setDateInputError({ start: true, end: true })
return
}
onConfirm(cloneExportDateRangeSelection(draft))
}}
>
</button>
</div>
</div>
</div>,
document.body
)
}