feat: support relative date view filters

This commit is contained in:
Test
2026-04-07 23:02:55 +02:00
parent c19cefe989
commit 58dfb039ca
7 changed files with 335 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { FilterBuilder } from './FilterBuilder'
import type { FilterGroup } from '../types'
@@ -10,6 +10,10 @@ describe('FilterBuilder value inputs', () => {
vi.clearAllMocks()
})
afterEach(() => {
vi.useRealTimers()
})
function renderBuilder(group?: FilterGroup) {
const defaultGroup: FilterGroup = {
all: [{ field: 'title', op: 'contains', value: '' }],
@@ -97,10 +101,10 @@ describe('FilterBuilder value inputs', () => {
all: [{ field: 'created', op: 'before', value: '2024-06-01' }],
})
expect(screen.getByTestId('date-value-input')).toHaveValue('2024-06-01')
const dateButton = screen.getByTestId('date-picker-trigger')
expect(dateButton).toBeInTheDocument()
expect(dateButton).toHaveTextContent('Jun 1, 2024')
expect(screen.queryByDisplayValue('2024-06-01')).not.toBeInTheDocument()
expect(dateButton).toHaveAttribute('title', 'Jun 1, 2024')
})
it('renders date picker placeholder when no date is selected', () => {
@@ -108,10 +112,23 @@ describe('FilterBuilder value inputs', () => {
all: [{ field: 'created', op: 'after', value: '' }],
})
expect(screen.getByTestId('date-picker-trigger')).toHaveTextContent('Pick a date')
expect(screen.getByTestId('date-value-input')).toHaveValue('')
expect(screen.getByTestId('date-picker-trigger')).toHaveAttribute('title', 'Pick a date')
expect(screen.queryByTestId('filter-regex-toggle')).not.toBeInTheDocument()
})
it('allows free-text relative date phrases for date operators', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-07T12:00:00Z'))
renderBuilder({
all: [{ field: 'created', op: 'after', value: '10 days ago' }],
})
expect(screen.getByTestId('date-value-input')).toHaveValue('10 days ago')
expect(screen.getByTestId('date-picker-trigger')).toHaveAttribute('title', 'Mar 28, 2026')
})
it('shows body field in field dropdown separated from property fields', () => {
render(
<FilterBuilder

View File

@@ -1,5 +1,5 @@
import { Plus, X, CalendarBlank, WarningCircle } from '@phosphor-icons/react'
import { format, parseISO } from 'date-fns'
import { format } from 'date-fns'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Calendar } from '@/components/ui/calendar'
@@ -7,6 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import type { FilterCondition, FilterOp, FilterGroup, FilterNode } from '../types'
import { parseDateFilterInput } from '../utils/filterDates'
const OPERATORS: { value: FilterOp; label: string }[] = [
{ value: 'equals', label: 'equals' },
@@ -116,28 +117,39 @@ function OperatorSelect({ value, onChange }: {
}
function DateValueInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const parsed = value ? parseISO(value) : undefined
const selected = parsed && !isNaN(parsed.getTime()) ? parsed : undefined
const selected = value ? parseDateFilterInput(value) ?? undefined : undefined
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
data-testid="date-picker-trigger"
className="h-8 flex-1 min-w-0 justify-start gap-2 px-2 text-sm font-normal"
>
<CalendarBlank size={14} className="shrink-0 text-muted-foreground" />
{selected ? format(selected, 'MMM d, yyyy') : <span className="text-muted-foreground">Pick a date</span>}
</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 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>
)
}