Skip to content

Commit a1b937c

Browse files
committed
Fix filter date round-trip: parse calendar day, ignore stored tz marker
The respect=false wire format ('YYYY-MM-DDT00:00:00Z') made the filter's display round-trip drop a day in browsers west of UTC: dayjs(stored).startOf('day') parses the Z-marked instant in browser-local time, which moves the picker's value to the previous calendar day after any re-render. Adds parseFilterDate as the inverse of formatFilterDate — slices the calendar prefix from the stored value and rebuilds a browser-local midnight timestamp, so the picker re-shows the same day the user clicked regardless of host TZ. Locked down with round-trip tests against both wire formats (Z marker and signed offset).
1 parent 80db9c8 commit a1b937c

3 files changed

Lines changed: 42 additions & 6 deletions

File tree

assets/js/src/core/components/date-picker/utils/date-picker-utils.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import dayjs from 'dayjs'
1212
import utc from 'dayjs/plugin/utc'
1313
import timezone from 'dayjs/plugin/timezone'
14-
import { toDayJs, fromDayJs, toServerWallClock, formatFilterDate } from './date-picker-utils'
14+
import { toDayJs, fromDayJs, toServerWallClock, formatFilterDate, parseFilterDate } from './date-picker-utils'
1515

1616
dayjs.extend(utc)
1717
dayjs.extend(timezone)
@@ -118,6 +118,34 @@ describe('formatFilterDate', () => {
118118
})
119119
})
120120

121+
describe('parseFilterDate (read-back round-trip)', () => {
122+
// Pins the regression: stored UTC ISO must not shift the picker day in browsers west of UTC.
123+
const tsBrowserLocalMidnight = new Date(2026, 2, 15).getTime() / 1000
124+
125+
it('returns null for null', () => {
126+
expect(parseFilterDate(null)).toBeNull()
127+
})
128+
129+
it('round-trips a respect=false stored value back to the originally picked day', () => {
130+
const stored = formatFilterDate(tsBrowserLocalMidnight, false)!
131+
expect(stored).toBe('2026-03-15T00:00:00Z')
132+
const recovered = parseFilterDate(stored)!
133+
expect(dayjs.unix(recovered).format('YYYY-MM-DD')).toBe('2026-03-15')
134+
})
135+
136+
it('round-trips a respect=true stored value back to the originally picked day', () => {
137+
const stored = formatFilterDate(tsBrowserLocalMidnight, true)!
138+
const recovered = parseFilterDate(stored)!
139+
expect(dayjs.unix(recovered).format('YYYY-MM-DD')).toBe('2026-03-15')
140+
})
141+
142+
it('ignores any offset or Z marker on the stored value (calendar day only)', () => {
143+
expect(dayjs.unix(parseFilterDate('2026-03-15T00:00:00Z')!).format('YYYY-MM-DD')).toBe('2026-03-15')
144+
expect(dayjs.unix(parseFilterDate('2026-03-15T00:00:00-04:00')!).format('YYYY-MM-DD')).toBe('2026-03-15')
145+
expect(dayjs.unix(parseFilterDate('2026-03-15T00:00:00+09:00')!).format('YYYY-MM-DD')).toBe('2026-03-15')
146+
})
147+
})
148+
121149
describe('fromDayJs — unchanged save behaviour', () => {
122150
it('formats a dateString with the supplied output format', () => {
123151
expect(fromDayJs(dayjs('2024-01-15 13:30'), 'dateString', 'YYYY-MM-DD HH:mm')).toBe('2024-01-15 13:30')

assets/js/src/core/components/date-picker/utils/date-picker-utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ export const formatFilterDate = (timestamp: number | null, respectTimezone: bool
6060
return `${dj.format('YYYY-MM-DD')}T00:00:00Z`
6161
}
6262

63+
/**
64+
* Inverse of `formatFilterDate`. Slices the calendar prefix so the read-back picker shows the
65+
* picked day regardless of any offset / `Z` marker on the stored value.
66+
*/
67+
export const parseFilterDate = (dateStr: string | null): number | null => {
68+
if (dateStr === null) {
69+
return null
70+
}
71+
return dayjs(dateStr.slice(0, 10), 'YYYY-MM-DD').unix()
72+
}
73+
6374
export const toDayJs = (value?: unknown, format?: string, options?: ToDayJsOptions): Dayjs | null => {
6475
if (dayjs.isDayjs(value)) {
6576
return value

assets/js/src/core/modules/element/dynamic-types/definitions/field-filters/components/dynamic-type-field-filter-date-component.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99
*/
1010

1111
import React from 'react'
12-
import dayjs from 'dayjs'
1312
import { Select } from '@Pimcore/components/select/select'
1413
import { Flex } from '@Pimcore/components/flex/flex'
1514
import { DatePicker } from '@Pimcore/components/date-picker/date-picker'
1615
import { DateRangePicker } from '@Pimcore/components/date-picker/date-range-picker'
1716
import { useDynamicFilter } from '@Pimcore/components/dynamic-filter/provider/use-dynamic-filter'
1817
import { t } from 'i18next'
1918
import { type AbstractFieldFilterDefinition } from '../dynamic-type-field-filter-abstract'
20-
import { formatFilterDate } from '@Pimcore/components/date-picker/utils/date-picker-utils'
19+
import { formatFilterDate, parseFilterDate } from '@Pimcore/components/date-picker/utils/date-picker-utils'
2120
import { isFieldRespectTimezone } from '../../objects/data-related/types/abstract/dynamic-type-object-data-abstract-date'
2221

2322
export enum DatePickerSettingValue {
@@ -97,9 +96,7 @@ export const DynamicTypeFieldFilterDateComponent = (props: DynamicTypeFieldFilte
9796
}
9897

9998
const convertISOToTimestamp = (dateStr: string | null): number | null => {
100-
if (dateStr === null) return null
101-
102-
return dayjs(dateStr).startOf('day').unix()
99+
return parseFilterDate(dateStr)
103100
}
104101

105102
const handleDateChange = (field: 'on' | 'from' | 'to', value: string | null): void => {

0 commit comments

Comments
 (0)