mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
329 lines
9.7 KiB
TypeScript
329 lines
9.7 KiB
TypeScript
import { format } from 'date-fns'
|
|
import dayjs from 'dayjs'
|
|
import { ArrowRight, Calendar } from 'lucide-react'
|
|
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
Button,
|
|
ButtonProps,
|
|
Calendar as CalendarPicker,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverSeparator,
|
|
PopoverTrigger,
|
|
} from 'ui'
|
|
|
|
import { TimeSplitInput } from './TimeSplitInput'
|
|
import type { DatePickerToFrom } from '@/components/interfaces/Settings/Logs/Logs.types'
|
|
|
|
interface DatePickerProps {
|
|
onChange?: (args: DatePickerToFrom) => void
|
|
to?: string // ISO string
|
|
from?: string // ISO string
|
|
triggerButtonVariant?: ButtonProps['variant']
|
|
triggerButtonClassName?: string
|
|
triggerButtonTitle?: string
|
|
triggerButtonSize?: 'tiny' | 'small'
|
|
contentSide?: 'bottom' | 'top'
|
|
minDate?: Date
|
|
maxDate?: Date
|
|
hideTime?: boolean
|
|
hideClear?: boolean
|
|
selectsRange?: boolean
|
|
renderFooter?: (args: DatePickerToFrom) => ReactNode | void
|
|
children?: ReactNode | ReactNode[] | null
|
|
}
|
|
|
|
const START_DATE_DEFAULT = new Date()
|
|
const END_DATE_DEFAULT = new Date()
|
|
|
|
const START_TIME_DEFAULT = { HH: '00', mm: '00', ss: '00' }
|
|
const END_TIME_DEFAULT = { HH: '23', mm: '59', ss: '59' }
|
|
|
|
const calculateDisabledDays = (minDate?: Date, maxDate?: Date) => {
|
|
const rules: Array<{ before: Date } | { after: Date }> = []
|
|
|
|
if (minDate) {
|
|
rules.push({ before: dayjs(minDate).startOf('day').toDate() })
|
|
}
|
|
|
|
if (maxDate) {
|
|
rules.push({ after: dayjs(maxDate).endOf('day').toDate() })
|
|
}
|
|
|
|
if (rules.length === 0) return undefined
|
|
if (rules.length === 1) return rules[0]
|
|
return rules
|
|
}
|
|
|
|
export function DatePicker({
|
|
to,
|
|
from,
|
|
onChange,
|
|
triggerButtonVariant = 'default',
|
|
triggerButtonClassName = '',
|
|
triggerButtonTitle,
|
|
triggerButtonSize,
|
|
contentSide = 'bottom',
|
|
minDate,
|
|
maxDate,
|
|
hideTime = false,
|
|
hideClear = false,
|
|
selectsRange = true,
|
|
renderFooter = () => null,
|
|
children,
|
|
}: DatePickerProps) {
|
|
const [open, setOpen] = useState<boolean>(false)
|
|
const [appliedStartDate, setAppliedStartDate] = useState<null | Date>(null)
|
|
const [appliedEndDate, setAppliedEndDate] = useState<null | Date>(null)
|
|
const [startDate, setStartDate] = useState<Date | null>(START_DATE_DEFAULT)
|
|
const [endDate, setEndDate] = useState<Date | null>(END_DATE_DEFAULT)
|
|
const [startTime, setStartTime] = useState<any>(START_TIME_DEFAULT)
|
|
const [endTime, setEndTime] = useState<any>(END_TIME_DEFAULT)
|
|
|
|
const disabledDays = useMemo(() => calculateDisabledDays(minDate, maxDate), [minDate, maxDate])
|
|
|
|
const startMonth = minDate ? dayjs(minDate).startOf('month').toDate() : undefined
|
|
const endMonth = maxDate ? dayjs(maxDate).endOf('month').toDate() : undefined
|
|
|
|
const clampDateToRange = useCallback(
|
|
(date: Date | null) => {
|
|
if (!date) return date
|
|
|
|
let nextDate = date
|
|
|
|
if (minDate && dayjs(nextDate).isBefore(minDate, 'day')) {
|
|
nextDate = dayjs(minDate).toDate()
|
|
}
|
|
|
|
if (maxDate && dayjs(nextDate).isAfter(maxDate, 'day')) {
|
|
nextDate = dayjs(maxDate).toDate()
|
|
}
|
|
|
|
return nextDate
|
|
},
|
|
[minDate, maxDate]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!minDate && !maxDate) return
|
|
|
|
setStartDate((prev) => {
|
|
if (!prev) return prev
|
|
const clamped = clampDateToRange(prev)
|
|
return dayjs(clamped).isSame(prev) ? prev : clamped
|
|
})
|
|
|
|
setEndDate((prev) => {
|
|
if (!prev) return prev
|
|
const clamped = clampDateToRange(prev)
|
|
return dayjs(clamped).isSame(prev) ? prev : clamped
|
|
})
|
|
|
|
setAppliedStartDate((prev) => {
|
|
if (!prev) return prev
|
|
const clamped = clampDateToRange(prev)
|
|
return dayjs(clamped).isSame(prev) ? prev : clamped
|
|
})
|
|
|
|
setAppliedEndDate((prev) => {
|
|
if (!prev) return prev
|
|
const clamped = clampDateToRange(prev)
|
|
return dayjs(clamped).isSame(prev) ? prev : clamped
|
|
})
|
|
}, [minDate, maxDate, clampDateToRange])
|
|
|
|
useEffect(() => {
|
|
if (!from) {
|
|
setAppliedStartDate(null)
|
|
} else if (from !== appliedStartDate?.toISOString()) {
|
|
const start = dayjs(from)
|
|
const startDate = start.toDate()
|
|
setAppliedStartDate(startDate)
|
|
setStartDate(startDate)
|
|
setStartTime({
|
|
HH: start.format('HH'),
|
|
mm: start.format('mm'),
|
|
ss: start.format('ss'),
|
|
})
|
|
}
|
|
|
|
if (!to) {
|
|
setAppliedEndDate(null)
|
|
} else if (to !== appliedEndDate?.toISOString()) {
|
|
const end = dayjs(to)
|
|
const endDate = end.toDate()
|
|
setAppliedEndDate(endDate)
|
|
setEndDate(endDate)
|
|
setEndTime({
|
|
HH: end.format('HH'),
|
|
mm: end.format('mm'),
|
|
ss: end.format('ss'),
|
|
})
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [to, from])
|
|
|
|
function handleDatePickerChange(dates: Date | [from: Date | null, to: Date | null] | null) {
|
|
if (!dates) {
|
|
setStartDate(null)
|
|
setEndDate(null)
|
|
} else if (dates instanceof Date) {
|
|
setStartDate(dates)
|
|
setEndDate(dates)
|
|
} else {
|
|
const [from, to] = dates
|
|
setStartDate(from)
|
|
setEndDate(to)
|
|
}
|
|
}
|
|
|
|
function handleSubmit() {
|
|
setOpen(false)
|
|
|
|
setAppliedStartDate(startDate)
|
|
setAppliedEndDate(endDate)
|
|
|
|
const payload = {
|
|
from: dayjs(startDate)
|
|
.second(Number(startTime.ss))
|
|
.minute(Number(startTime.mm))
|
|
.hour(Number(startTime.HH))
|
|
.toISOString(),
|
|
to: dayjs(endDate || startDate)
|
|
.second(Number(endTime.ss))
|
|
.minute(Number(endTime.mm))
|
|
.hour(Number(endTime.HH))
|
|
.toISOString(),
|
|
}
|
|
if (onChange) onChange(payload)
|
|
}
|
|
|
|
function handleClear() {
|
|
setOpen(false)
|
|
setStartDate(START_DATE_DEFAULT)
|
|
setEndDate(END_DATE_DEFAULT)
|
|
|
|
setStartTime(START_TIME_DEFAULT)
|
|
setEndTime(END_TIME_DEFAULT)
|
|
|
|
setAppliedStartDate(null)
|
|
setAppliedEndDate(null)
|
|
|
|
if (onChange) onChange({ from: null, to: null })
|
|
}
|
|
|
|
return (
|
|
<Popover modal open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
title={triggerButtonTitle}
|
|
variant={triggerButtonVariant}
|
|
icon={<Calendar />}
|
|
size={triggerButtonSize}
|
|
className={triggerButtonClassName}
|
|
>
|
|
{children !== undefined ? (
|
|
children
|
|
) : (
|
|
<>
|
|
{/* Custom */}
|
|
{selectsRange &&
|
|
appliedStartDate &&
|
|
appliedEndDate &&
|
|
appliedStartDate !== appliedEndDate ? (
|
|
<>
|
|
{format(new Date(appliedStartDate), 'dd MMM')} -{' '}
|
|
{format(new Date(appliedEndDate), 'dd MMM')}
|
|
</>
|
|
) : appliedStartDate || appliedEndDate ? (
|
|
format(new Date((appliedStartDate || appliedEndDate)!), 'dd MMM')
|
|
) : (
|
|
'Custom'
|
|
)}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="center" side={contentSide} className="p-0">
|
|
<>
|
|
{hideTime ? null : (
|
|
<>
|
|
<div className="flex items-stretch justify-between p-2">
|
|
{!selectsRange ? null : (
|
|
<>
|
|
<div className="flex grow flex-col gap-1">
|
|
<TimeSplitInput
|
|
type="start"
|
|
startTime={startTime}
|
|
endTime={endTime}
|
|
time={startTime}
|
|
setTime={setStartTime}
|
|
setStartTime={setStartTime}
|
|
setEndTime={setEndTime}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-center w-12 text-foreground-lighter">
|
|
<ArrowRight strokeWidth={1.5} size={14} />
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="flex grow flex-col gap-1">
|
|
<TimeSplitInput
|
|
type="end"
|
|
startTime={startTime}
|
|
endTime={endTime}
|
|
time={endTime}
|
|
setTime={setEndTime}
|
|
setStartTime={setStartTime}
|
|
setEndTime={setEndTime}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="p-2">
|
|
{selectsRange ? (
|
|
<CalendarPicker
|
|
mode="range"
|
|
startMonth={startMonth}
|
|
endMonth={endMonth}
|
|
disabled={disabledDays}
|
|
selected={{ from: startDate ?? undefined, to: endDate ?? undefined }}
|
|
onSelect={(range) => {
|
|
handleDatePickerChange([range?.from ?? null, range?.to ?? null])
|
|
}}
|
|
/>
|
|
) : (
|
|
<CalendarPicker
|
|
mode="single"
|
|
startMonth={startMonth}
|
|
endMonth={endMonth}
|
|
disabled={disabledDays}
|
|
selected={endDate ?? undefined}
|
|
onSelect={(date) => {
|
|
handleDatePickerChange(date ?? null)
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
{renderFooter({
|
|
from: startDate?.toISOString() || null,
|
|
to: endDate?.toISOString() || null,
|
|
})}
|
|
<PopoverSeparator />
|
|
<div className="flex items-center justify-end gap-2 py-2 px-3 pb-4">
|
|
{!hideClear && <Button onClick={() => handleClear()}>Clear</Button>}
|
|
<Button variant="primary" onClick={() => handleSubmit()}>
|
|
Apply
|
|
</Button>
|
|
</div>
|
|
</>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|