fix: set date to 12UTC for default, dayOnly and monthOnly fields (#3887)

This commit is contained in:
Jarrod Flesch
2023-10-27 14:43:36 -04:00
committed by GitHub
parent a4f36aa8a0
commit d393225289
5 changed files with 215 additions and 46 deletions

View File

@@ -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',

View File

@@ -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>
)
}

View File

@@ -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}
/>
)
}

View File

@@ -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'

View File

@@ -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', () => {