feat: preview relative date filters

This commit is contained in:
Test
2026-04-08 21:44:20 +02:00
parent 70e7e72120
commit 13edf894fb
4 changed files with 145 additions and 42 deletions

View File

@@ -1,14 +1,11 @@
import { Plus, X, CalendarBlank, WarningCircle } from '@phosphor-icons/react'
import { format } from 'date-fns'
import { Plus, X, WarningCircle } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import type { FilterCondition, FilterOp, FilterGroup, FilterNode } from '../types'
import { parseDateFilterInput } from '../utils/filterDates'
import { FilterFieldCombobox } from './FilterFieldCombobox'
import { DateValueInput } from './filter-builder/DateValueInput'
const OPERATORS: { value: FilterOp; label: string }[] = [
{ value: 'equals', label: 'equals' },
@@ -81,43 +78,6 @@ function OperatorSelect({ value, onChange }: {
)
}
function DateValueInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const selected = value ? parseDateFilterInput(value) ?? undefined : undefined
return (
<div className="flex flex-1 min-w-0 items-center gap-1">
<Input
className="h-8 flex-1 min-w-0 text-sm"
placeholder='YYYY-MM-DD or "10 days ago"'
value={value}
onChange={(e) => onChange(e.target.value)}
data-testid="date-value-input"
/>
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
data-testid="date-picker-trigger"
className="h-8 w-8 shrink-0 px-0"
title={selected ? format(selected, 'MMM d, yyyy') : 'Pick a date'}
aria-label={selected ? `Open date picker (${format(selected, 'MMM d, yyyy')})` : 'Open date picker'}
>
<CalendarBlank size={14} className="shrink-0 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selected}
onSelect={(day) => onChange(day ? format(day, 'yyyy-MM-dd') : '')}
/>
</PopoverContent>
</Popover>
</div>
)
}
function TextValueInput({ value, onChange, regexEnabled, regexSupported, invalidRegex, onToggleRegex }: {
value: string
onChange: (v: string) => void

View File

@@ -0,0 +1,59 @@
import { useState } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { DateValueInput } from './DateValueInput'
describe('DateValueInput', () => {
afterEach(() => {
vi.useRealTimers()
})
function renderControlledDateValueInput(initialValue = '') {
function ControlledDateValueInput() {
const [value, setValue] = useState(initialValue)
return <DateValueInput value={value} onChange={setValue} />
}
return render(<ControlledDateValueInput />)
}
it('shows a debounced resolved-date preview while focused', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-08T12:00:00Z'))
renderControlledDateValueInput()
const input = screen.getByTestId('date-value-input')
fireEvent.focus(input)
fireEvent.change(input, { target: { value: '10 days ago' } })
expect(screen.queryByTestId('date-value-preview')).not.toBeInTheDocument()
await act(async () => {
vi.advanceTimersByTime(250)
})
expect(screen.getByTestId('date-value-preview')).toHaveTextContent('Resolves to March 29, 2026')
})
it('shows a neutral hint for unrecognized input and hides the preview on blur', async () => {
vi.useFakeTimers()
renderControlledDateValueInput()
const input = screen.getByTestId('date-value-input')
fireEvent.focus(input)
fireEvent.change(input, { target: { value: 'eventually maybe' } })
await act(async () => {
vi.advanceTimersByTime(250)
})
expect(screen.getByTestId('date-value-preview-unrecognized')).toHaveTextContent('Not recognized')
fireEvent.blur(input)
expect(screen.queryByTestId('date-value-preview')).not.toBeInTheDocument()
expect(screen.queryByTestId('date-value-preview-unrecognized')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,78 @@
import { useEffect, useState } from 'react'
import { CalendarBlank } from '@phosphor-icons/react'
import { format } from 'date-fns'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { parseDateFilterInput } from '@/utils/filterDates'
const DATE_PREVIEW_DEBOUNCE_MS = 250
export function DateValueInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const selected = value ? parseDateFilterInput(value) ?? undefined : undefined
const [showPreview, setShowPreview] = useState(false)
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timeoutId = window.setTimeout(() => setDebouncedValue(value), DATE_PREVIEW_DEBOUNCE_MS)
return () => window.clearTimeout(timeoutId)
}, [value])
const previewValue = showPreview ? debouncedValue.trim() : ''
const resolvedPreview = previewValue ? parseDateFilterInput(previewValue) : null
const previewLabel = resolvedPreview
? format(resolvedPreview, 'MMMM d, yyyy')
: previewValue
? 'Not recognized'
: null
return (
<div className="flex flex-1 min-w-0 flex-col gap-1">
<div className="flex min-w-0 items-center gap-1">
<Input
className="h-8 flex-1 min-w-0 text-sm"
placeholder='YYYY-MM-DD or "10 days ago"'
value={value}
onChange={(e) => {
setShowPreview(true)
onChange(e.target.value)
}}
onFocus={() => setShowPreview(true)}
onBlur={() => setShowPreview(false)}
data-testid="date-value-input"
/>
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
data-testid="date-picker-trigger"
className="h-8 w-8 shrink-0 px-0"
title={selected ? format(selected, 'MMM d, yyyy') : 'Pick a date'}
aria-label={selected ? `Open date picker (${format(selected, 'MMM d, yyyy')})` : 'Open date picker'}
>
<CalendarBlank size={14} className="shrink-0 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selected}
onSelect={(day) => onChange(day ? format(day, 'yyyy-MM-dd') : '')}
/>
</PopoverContent>
</Popover>
</div>
{previewLabel && (
<div
className="pl-1 text-[11px] text-muted-foreground"
data-testid={resolvedPreview ? 'date-value-preview' : 'date-value-preview-unrecognized'}
>
{resolvedPreview ? `Resolves to ${previewLabel}` : previewLabel}
</div>
)}
</div>
)
}

View File

@@ -18,6 +18,12 @@ describe('filterDates', () => {
expect(parsed && format(parsed, 'yyyy-MM-dd')).toBe('2026-03-28')
})
it('parses numeric relative year phrases', () => {
const reference = new Date('2026-04-08T12:00:00Z')
const parsed = parseDateFilterInput('10 years ago', reference)
expect(parsed && format(parsed, 'yyyy-MM-dd')).toBe('2016-04-08')
})
it('parses word-based relative week phrases', () => {
const reference = new Date('2026-04-07T12:00:00Z')
const parsed = parseDateFilterInput('one week ago', reference)