fix: set date to 12UTC for default, dayOnly and monthOnly fields (#3887)
This commit is contained in:
@@ -21,7 +21,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
minDate,
|
||||
minTime,
|
||||
monthsToShow = 1,
|
||||
onChange,
|
||||
onChange: onChangeFromProps,
|
||||
pickerAppearance = 'default',
|
||||
placeholder: placeholderText,
|
||||
readOnly,
|
||||
@@ -51,6 +51,15 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
else if (pickerAppearance === 'monthOnly') dateFormat = 'MMMM'
|
||||
}
|
||||
|
||||
const onChange = (incomingDate: Date) => {
|
||||
const newDate = incomingDate
|
||||
if (newDate instanceof Date && ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
|
||||
const tzOffset = incomingDate.getTimezoneOffset() / 60
|
||||
newDate.setHours(12 - tzOffset, 0)
|
||||
}
|
||||
if (typeof onChangeFromProps === 'function') onChangeFromProps(newDate)
|
||||
}
|
||||
|
||||
const dateTimePickerProps = {
|
||||
customInputRef: 'ref',
|
||||
dateFormat,
|
||||
@@ -93,7 +102,6 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
{...dateTimePickerProps}
|
||||
dropdownMode="select"
|
||||
locale={locale}
|
||||
onChange={(val) => onChange(val)}
|
||||
popperModifiers={[
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { DateField } from '../../../../../exports/types'
|
||||
import type { Description } from '../../FieldDescription/types'
|
||||
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import DatePicker from '../../../elements/DatePicker'
|
||||
import Error from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'date-time-field'
|
||||
|
||||
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
className?: string
|
||||
datePickerProps?: DateField['admin']['date']
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
onChange?: (e: Date) => void
|
||||
path: string
|
||||
placeholder?: Record<string, string> | string
|
||||
readOnly?: boolean
|
||||
required?: boolean
|
||||
showError?: boolean
|
||||
style?: React.CSSProperties
|
||||
value?: Date
|
||||
width?: string
|
||||
}
|
||||
|
||||
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
datePickerProps,
|
||||
description,
|
||||
errorMessage,
|
||||
label,
|
||||
onChange,
|
||||
path,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
showError,
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
baseClass,
|
||||
className,
|
||||
showError && `${baseClass}--has-error`,
|
||||
readOnly && 'read-only',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<Label htmlFor={path} label={label} required={required} />
|
||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
onChange={onChange}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
readOnly={readOnly}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { Props } from './types'
|
||||
|
||||
import { date as dateValidation } from '../../../../../fields/validations'
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import DatePicker from '../../../elements/DatePicker'
|
||||
import Error from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import { DateTimeInput } from './Input'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const baseClass = 'date-time-field'
|
||||
|
||||
const DateTime: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -26,8 +18,6 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
validate = dateValidation,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const path = pathFromProps || name
|
||||
|
||||
const memoizedValidate = useCallback(
|
||||
@@ -37,45 +27,31 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
[validate, required],
|
||||
)
|
||||
|
||||
const { errorMessage, setValue, showError, value } = useField({
|
||||
const { errorMessage, setValue, showError, value } = useField<Date>({
|
||||
condition,
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
baseClass,
|
||||
className,
|
||||
showError && `${baseClass}--has-error`,
|
||||
readOnly && 'read-only',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
<DateTimeInput
|
||||
className={className}
|
||||
datePickerProps={date}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
label={label}
|
||||
onChange={(incomingDate) => {
|
||||
if (!readOnly) setValue(incomingDate?.toISOString() || null)
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<Label htmlFor={path} label={label} required={required} />
|
||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
<DatePicker
|
||||
{...date}
|
||||
onChange={(incomingDate) => {
|
||||
if (!readOnly) setValue(incomingDate?.toISOString() || null)
|
||||
}}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
readOnly={readOnly}
|
||||
value={value as Date}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
path={path}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
showError={showError}
|
||||
style={style}
|
||||
value={value}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,11 @@ export { fieldTypes } from '../../admin/components/forms/field-types'
|
||||
export { default as Checkbox } from '../../admin/components/forms/field-types/Checkbox'
|
||||
|
||||
export { default as Collapsible } from '../../admin/components/forms/field-types/Collapsible'
|
||||
export { default as Date } from '../../admin/components/forms/field-types/DateTime'
|
||||
export { DateTimeInput } from '../../admin/components/forms/field-types/DateTime/Input'
|
||||
|
||||
export { default as Group } from '../../admin/components/forms/field-types/Group'
|
||||
export { default as HiddenInput } from '../../admin/components/forms/field-types/HiddenInput'
|
||||
|
||||
export { default as Select } from '../../admin/components/forms/field-types/Select'
|
||||
export { default as SelectInput } from '../../admin/components/forms/field-types/Select/Input'
|
||||
export { default as Text } from '../../admin/components/forms/field-types/Text'
|
||||
|
||||
@@ -8,6 +8,7 @@ import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
import { collapsibleFieldsSlug } from './collections/Collapsible/shared'
|
||||
import { jsonDoc } from './collections/JSON'
|
||||
import { numberDoc } from './collections/Number'
|
||||
@@ -18,6 +19,7 @@ import { textDoc, textFieldsSlug } from './collections/Text'
|
||||
|
||||
const { afterEach, beforeAll, describe } = test
|
||||
|
||||
let client: RESTClient
|
||||
let page: Page
|
||||
let serverURL
|
||||
|
||||
@@ -25,6 +27,8 @@ describe('fields', () => {
|
||||
beforeAll(async ({ browser }) => {
|
||||
const config = await initPayloadE2E(__dirname)
|
||||
serverURL = config.serverURL
|
||||
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
|
||||
await client.login()
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -1115,6 +1119,100 @@ describe('fields', () => {
|
||||
await clearButton.click()
|
||||
await expect(dateField).toHaveValue('')
|
||||
})
|
||||
|
||||
describe('localized dates', async () => {
|
||||
describe('EST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -83.0458,
|
||||
latitude: 42.3314,
|
||||
},
|
||||
timezoneId: 'America/Detroit',
|
||||
})
|
||||
test('create EST day only date', async () => {
|
||||
await page.goto(url.create)
|
||||
const dateField = page.locator('#field-default input')
|
||||
|
||||
// enter date in default date field
|
||||
await dateField.fill('02/07/2023')
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
// wait for navigation to update route
|
||||
await wait(500)
|
||||
|
||||
// get the ID of the doc
|
||||
const routeSegments = page.url().split('/')
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -122.419416,
|
||||
latitude: 37.774929,
|
||||
},
|
||||
timezoneId: 'America/Los_Angeles',
|
||||
})
|
||||
|
||||
test('create PDT day only date', async () => {
|
||||
await page.goto(url.create)
|
||||
const dateField = page.locator('#field-default input')
|
||||
|
||||
// enter date in default date field
|
||||
await dateField.fill('02/07/2023')
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
// wait for navigation to update route
|
||||
await wait(500)
|
||||
|
||||
// get the ID of the doc
|
||||
const routeSegments = page.url().split('/')
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -171.857,
|
||||
latitude: -14.5994,
|
||||
},
|
||||
timezoneId: 'Pacific/Apia',
|
||||
})
|
||||
|
||||
test('create ST day only date', async () => {
|
||||
await page.goto(url.create)
|
||||
const dateField = page.locator('#field-default input')
|
||||
|
||||
// enter date in default date field
|
||||
await dateField.fill('02/07/2023')
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
// wait for navigation to update route
|
||||
await wait(500)
|
||||
|
||||
// get the ID of the doc
|
||||
const routeSegments = page.url().split('/')
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationship', () => {
|
||||
|
||||
Reference in New Issue
Block a user