5 changed files with 135 additions and 75 deletions
@ -0,0 +1,47 @@ |
|||
import type { Dayjs } from 'dayjs'; |
|||
|
|||
import dayjs from 'dayjs'; |
|||
import { describe, expect, it } from 'vitest'; |
|||
|
|||
import { createDateRangeCodec } from '../../src/utils/date-range-codec'; |
|||
|
|||
interface SearchFormValues extends Record<string, unknown> { |
|||
createdAt?: [Dayjs, Dayjs]; |
|||
keyword?: string; |
|||
} |
|||
|
|||
const codec = createDateRangeCodec<SearchFormValues>()({ |
|||
endField: 'finishedAt', |
|||
rangeField: 'createdAt', |
|||
startField: 'startedAt', |
|||
}); |
|||
|
|||
describe('date range codec', () => { |
|||
it('encodes and decodes configurable date range fields', () => { |
|||
const submitValues = codec.encode({ |
|||
createdAt: [dayjs('2026-07-01'), dayjs('2026-07-23')], |
|||
keyword: 'admin', |
|||
}); |
|||
|
|||
expect(submitValues).toEqual({ |
|||
finishedAt: '2026-07-23', |
|||
keyword: 'admin', |
|||
startedAt: '2026-07-01', |
|||
}); |
|||
const formValues = codec.decode(submitValues); |
|||
expect(formValues.keyword).toBe('admin'); |
|||
expect( |
|||
formValues.createdAt?.map((value) => value.format('YYYY-MM-DD')), |
|||
).toEqual(['2026-07-01', '2026-07-23']); |
|||
}); |
|||
|
|||
it('does not reconstruct a range with a missing bound', () => { |
|||
expect( |
|||
codec.decode({ |
|||
finishedAt: undefined, |
|||
keyword: 'admin', |
|||
startedAt: '2026-07-01', |
|||
}), |
|||
).toEqual({ keyword: 'admin' }); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,64 @@ |
|||
import type { Dayjs } from 'dayjs'; |
|||
|
|||
import dayjs from 'dayjs'; |
|||
|
|||
type DateRange = [Dayjs, Dayjs]; |
|||
|
|||
type DateRangeSubmitValues< |
|||
TFormValues extends Record<string, unknown>, |
|||
TRangeField extends keyof TFormValues, |
|||
TStartField extends string, |
|||
TEndField extends string, |
|||
> = Omit<TFormValues, TRangeField> & |
|||
Record<TEndField | TStartField, string | undefined>; |
|||
|
|||
interface DateRangeCodecOptions< |
|||
TFormValues extends Record<string, unknown>, |
|||
TRangeField extends keyof TFormValues & string, |
|||
TStartField extends string, |
|||
TEndField extends string, |
|||
> { |
|||
endField: TEndField; |
|||
rangeField: TRangeField; |
|||
startField: TStartField; |
|||
} |
|||
|
|||
export function createDateRangeCodec< |
|||
TFormValues extends Record<string, unknown>, |
|||
>() { |
|||
return function createCodec< |
|||
TRangeField extends keyof TFormValues & string, |
|||
TStartField extends string, |
|||
TEndField extends string, |
|||
>({ |
|||
endField, |
|||
rangeField, |
|||
startField, |
|||
}: DateRangeCodecOptions<TFormValues, TRangeField, TStartField, TEndField>) { |
|||
type SubmitValues = DateRangeSubmitValues< |
|||
TFormValues, |
|||
TRangeField, |
|||
TStartField, |
|||
TEndField |
|||
>; |
|||
|
|||
return { |
|||
decode(values: Readonly<SubmitValues>): TFormValues { |
|||
const { [endField]: end, [startField]: start, ...formValues } = values; |
|||
return { |
|||
...formValues, |
|||
...(start && end ? { [rangeField]: [dayjs(start), dayjs(end)] } : {}), |
|||
} as TFormValues; |
|||
}, |
|||
encode(values: Readonly<TFormValues>): SubmitValues { |
|||
const { [rangeField]: value, ...formValues } = values; |
|||
const range = value as DateRange | undefined; |
|||
return { |
|||
...formValues, |
|||
[endField]: range?.[1]?.format('YYYY-MM-DD'), |
|||
[startField]: range?.[0]?.format('YYYY-MM-DD'), |
|||
} as SubmitValues; |
|||
}, |
|||
}; |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue