Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/fields/date.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,28 @@ To enable timezone selection on a Date field, set the `timezone` property to `tr

This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`.

You can customise the available list of timezones in the [global admin config](../admin/overview#timezones).
You can customise the available list of timezones in the [global admin config](../admin/overview#timezones) or on the field config itself which accepts the following config as well:

| Property | Description |
| -------------------- | ------------------------------------------------------------------------- |
| `defaultTimezone` | A value for the default timezone to be set. |
| `supportedTimezones` | An array of supported timezones with label and value object. |
| `required` | If true, the timezone selection will be required even if the date is not. |

```ts
{
name: 'date',
type: 'date',
timezone: {
defaultTimezone: 'America/New_York',
supportedTimezones: [
{ label: 'New York', value: 'America/New_York' },
{ label: 'Los Angeles', value: 'America/Los_Angeles' },
{ label: 'London', value: 'Europe/London' },
],
},
}
```

<Banner type="info">
**Good to know:**
Expand Down
2 changes: 1 addition & 1 deletion packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ export type Timezone = {

type SupportedTimezonesFn = (args: { defaultTimezones: Timezone[] }) => Timezone[]

type TimezonesConfig = {
export type TimezonesConfig = {
/**
* The default timezone to use for the admin panel.
*/
Expand Down
7 changes: 4 additions & 3 deletions packages/payload/src/exports/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,31 @@ export { isImage } from '../uploads/isImage.js'
export { appendUploadSelectFields } from '../utilities/appendUploadSelectFields.js'
export { applyLocaleFiltering } from '../utilities/applyLocaleFiltering.js'
export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js'

export {
deepCopyObject,
deepCopyObjectComplex,
deepCopyObjectSimple,
deepCopyObjectSimpleWithoutReactComponents,
} from '../utilities/deepCopyObject.js'

export {
deepMerge,
deepMergeWithCombinedArrays,
deepMergeWithReactComponents,
deepMergeWithSourceArrays,
} from '../utilities/deepMerge.js'

export { extractID } from '../utilities/extractID.js'

export { flattenAllFields } from '../utilities/flattenAllFields.js'

export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
export { formatAdminURL } from '../utilities/formatAdminURL.js'
export { formatLabels, toWords } from '../utilities/formatLabels.js'

export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js'

export { getDataByPath } from '../utilities/getDataByPath.js'
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
export { getObjectDotNotation } from '../utilities/getObjectDotNotation.js'
export { getSafeRedirect } from '../utilities/getSafeRedirect.js'

export { getSelectMode } from '../utilities/getSelectMode.js'
Expand Down
17 changes: 14 additions & 3 deletions packages/payload/src/fields/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,20 @@ export const sanitizeFields = async ({
// Insert our field after assignment
if (field.type === 'date' && field.timezone) {
const name = field.name + '_tz'
const defaultTimezone = config.admin?.timezones?.defaultTimezone

const supportedTimezones = config.admin?.timezones?.supportedTimezones
const defaultTimezone =
field.timezone && typeof field.timezone === 'object'
? field.timezone.defaultTimezone
: config.admin?.timezones?.defaultTimezone

const required =
field.required ||
(field.timezone && typeof field.timezone === 'object' && field.timezone.required)

const supportedTimezones =
field.timezone && typeof field.timezone === 'object' && field.timezone.supportedTimezones
? field.timezone.supportedTimezones
: config.admin?.timezones?.supportedTimezones

const options =
typeof supportedTimezones === 'function'
Expand All @@ -422,7 +433,7 @@ export const sanitizeFields = async ({
name,
defaultValue: defaultTimezone,
options,
required: field.required,
required,
})

fields.splice(++i, 0, timezoneField)
Expand Down
12 changes: 11 additions & 1 deletion packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ import type {
LabelFunction,
PayloadComponent,
StaticLabel,
Timezone,
TimezonesConfig,
} from '../../config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
Expand Down Expand Up @@ -723,6 +725,14 @@ export type CheckboxFieldClient = {
} & FieldBaseClient &
Pick<CheckboxField, 'type'>

type DateFieldTimezoneConfig = {
/**
* Make only the timezone required in the admin interface. This means a timezone is always required to be selected.
*/
required?: boolean
supportedTimezones?: Timezone[]
} & Pick<TimezonesConfig, 'defaultTimezone'>

export type DateField = {
admin?: {
components?: {
Expand All @@ -737,7 +747,7 @@ export type DateField = {
/**
* Enable timezone selection in the admin interface.
*/
timezone?: true
timezone?: DateFieldTimezoneConfig | true
type: 'date'
validate?: DateFieldValidation
} & Omit<FieldBase, 'validate'>
Expand Down
14 changes: 14 additions & 0 deletions packages/payload/src/utilities/getObjectDotNotation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/**
*
* @deprecated use getObjectDotNotation from `'payload/shared'` instead of `'payload'`
*
* @example
*
* ```ts
* import { getObjectDotNotation } from 'payload/shared'
*
* const obj = { a: { b: { c: 42 } } }
* const value = getObjectDotNotation<number>(obj, 'a.b.c', 0) // value is 42
* const defaultValue = getObjectDotNotation<number>(obj, 'a.b.x', 0) // defaultValue is 0
* ```
*/
export const getObjectDotNotation = <T>(
obj: Record<string, unknown>,
path: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
'use client'
import type { DateFieldClient, DefaultCellComponentProps } from 'payload'

import { getObjectDotNotation } from 'payload/shared'
import React from 'react'

import { useConfig } from '../../../../../providers/Config/index.js'
import { useTranslation } from '../../../../../providers/Translation/index.js'
import { formatDate } from '../../../../../utilities/formatDocTitle/formatDateTitle.js'

export const DateCell: React.FC<DefaultCellComponentProps<DateFieldClient>> = ({
cellData,
field: { admin: { date } = {} },
}) => {
export const DateCell: React.FC<
DefaultCellComponentProps<{ accessor?: string } & DateFieldClient>
> = (props) => {
const {
cellData,
field: { name, accessor, admin: { date } = {}, timezone: timezoneFromField },
rowData,
} = props

const {
config: {
admin: { dateFormat: dateFormatFromRoot },
},
} = useConfig()
const { i18n } = useTranslation()

const dateFormat = date?.displayFormat || dateFormatFromRoot
const fieldPath = accessor || name

const { i18n } = useTranslation()
const timezoneFieldName = `${fieldPath}_tz`
const timezone =
Boolean(timezoneFromField) && rowData
? getObjectDotNotation(rowData, timezoneFieldName, undefined)
: undefined

const dateFormat = date?.displayFormat || dateFormatFromRoot

return <span>{cellData && formatDate({ date: cellData, i18n, pattern: dateFormat })}</span>
return (
<span>
{Boolean(cellData) && formatDate({ date: cellData, i18n, pattern: dateFormat, timezone })}
</span>
)
}
1 change: 1 addition & 0 deletions packages/ui/src/elements/TimezonePicker/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
background: none;
border: none;
padding: 0;
padding-left: calc(var(--base) * 0.25);
min-height: auto !important;
position: relative;
box-shadow: unset;
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/src/elements/TimezonePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const TimezonePicker: React.FC<Props> = (props) => {
id,
onChange: onChangeFromProps,
options: optionsFromProps,
readOnly: readOnlyFromProps,
required,
selectedTimezone: selectedTimezoneFromProps,
} = props
Expand All @@ -33,6 +34,8 @@ export const TimezonePicker: React.FC<Props> = (props) => {
})
}, [options, selectedTimezoneFromProps])

const readOnly = Boolean(readOnlyFromProps) || options.length === 1

return (
<div className="timezone-picker-wrapper">
<FieldLabel
Expand All @@ -43,8 +46,9 @@ export const TimezonePicker: React.FC<Props> = (props) => {
/>
<ReactSelect
className="timezone-picker"
disabled={readOnly}
inputId={id}
isClearable={true}
isClearable={!required}
isCreatable={false}
onChange={(val: OptionObject) => {
if (onChangeFromProps) {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/elements/TimezonePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { SelectFieldClient } from 'payload'
export type Props = {
id: string
onChange?: (val: string) => void
readOnly?: boolean
required?: boolean
selectedTimezone?: string
} & Pick<SelectFieldClient, 'options'>
15 changes: 13 additions & 2 deletions packages/ui/src/fields/DateTime/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,23 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {

const timezonePath = path + '_tz'
const timezoneField = useFormFields(([fields, _]) => fields?.[timezonePath])
const supportedTimezones = config.admin.timezones.supportedTimezones

const supportedTimezones = useMemo(() => {
if (timezone && typeof timezone === 'object' && timezone.supportedTimezones) {
return timezone.supportedTimezones
}

return config.admin.timezones.supportedTimezones
}, [config.admin.timezones.supportedTimezones, timezone])

/**
* Date appearance doesn't include timestamps,
* which means we need to pin the time to always 12:00 for the selected date
*/
const isDateOnly = ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)
const selectedTimezone = timezoneField?.value as string
const timezoneRequired =
required || (timezone && typeof timezone === 'object' && timezone.required)

// The displayed value should be the original value, adjusted to the user's timezone
const displayedValue = useMemo(() => {
Expand Down Expand Up @@ -192,7 +202,8 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
id={`${path}-timezone-picker`}
onChange={onChangeTimezone}
options={supportedTimezones}
required={required}
readOnly={readOnly || disabled}
required={timezoneRequired}
selectedTimezone={selectedTimezone}
/>
)}
Expand Down
Loading
Loading