Skip to content

Commit a3df837

Browse files
authored
feat: timezone config can now be added on a per field basis (#14410)
This PR enables you to add timezone specific config on a per field basis so each field can support its own list of timezones ```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' }, ], }, } ``` You can also enforce a specific timezone by specifying just one with a default value: ```ts { name: 'date', type: 'date', timezone: { defaultTimezone: 'Europe/London', supportedTimezones: [ { label: 'London', value: 'Europe/London' }, ], }, } ``` It also fixes a bug with date fields in list view not showing the date in the formatted timezone correctly.
1 parent ffb9a2e commit a3df837

File tree

15 files changed

+502
-783
lines changed

15 files changed

+502
-783
lines changed

docs/fields/date.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,28 @@ To enable timezone selection on a Date field, set the `timezone` property to `tr
236236

237237
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`.
238238

239-
You can customise the available list of timezones in the [global admin config](../admin/overview#timezones).
239+
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:
240+
241+
| Property | Description |
242+
| -------------------- | ------------------------------------------------------------------------- |
243+
| `defaultTimezone` | A value for the default timezone to be set. |
244+
| `supportedTimezones` | An array of supported timezones with label and value object. |
245+
| `required` | If true, the timezone selection will be required even if the date is not. |
246+
247+
```ts
248+
{
249+
name: 'date',
250+
type: 'date',
251+
timezone: {
252+
defaultTimezone: 'America/New_York',
253+
supportedTimezones: [
254+
{ label: 'New York', value: 'America/New_York' },
255+
{ label: 'Los Angeles', value: 'America/Los_Angeles' },
256+
{ label: 'London', value: 'Europe/London' },
257+
],
258+
},
259+
}
260+
```
240261

241262
<Banner type="info">
242263
**Good to know:**

packages/payload/src/config/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export type Timezone = {
440440

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

443-
type TimezonesConfig = {
443+
export type TimezonesConfig = {
444444
/**
445445
* The default timezone to use for the admin panel.
446446
*/

packages/payload/src/exports/shared.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,31 @@ export { isImage } from '../uploads/isImage.js'
6262
export { appendUploadSelectFields } from '../utilities/appendUploadSelectFields.js'
6363
export { applyLocaleFiltering } from '../utilities/applyLocaleFiltering.js'
6464
export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js'
65-
6665
export {
6766
deepCopyObject,
6867
deepCopyObjectComplex,
6968
deepCopyObjectSimple,
7069
deepCopyObjectSimpleWithoutReactComponents,
7170
} from '../utilities/deepCopyObject.js'
71+
7272
export {
7373
deepMerge,
7474
deepMergeWithCombinedArrays,
7575
deepMergeWithReactComponents,
7676
deepMergeWithSourceArrays,
7777
} from '../utilities/deepMerge.js'
78-
7978
export { extractID } from '../utilities/extractID.js'
8079

8180
export { flattenAllFields } from '../utilities/flattenAllFields.js'
81+
8282
export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
8383
export { formatAdminURL } from '../utilities/formatAdminURL.js'
8484
export { formatLabels, toWords } from '../utilities/formatLabels.js'
85-
8685
export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js'
86+
8787
export { getDataByPath } from '../utilities/getDataByPath.js'
8888
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
89+
export { getObjectDotNotation } from '../utilities/getObjectDotNotation.js'
8990
export { getSafeRedirect } from '../utilities/getSafeRedirect.js'
9091

9192
export { getSelectMode } from '../utilities/getSelectMode.js'

packages/payload/src/fields/config/sanitize.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,20 @@ export const sanitizeFields = async ({
407407
// Insert our field after assignment
408408
if (field.type === 'date' && field.timezone) {
409409
const name = field.name + '_tz'
410-
const defaultTimezone = config.admin?.timezones?.defaultTimezone
411410

412-
const supportedTimezones = config.admin?.timezones?.supportedTimezones
411+
const defaultTimezone =
412+
field.timezone && typeof field.timezone === 'object'
413+
? field.timezone.defaultTimezone
414+
: config.admin?.timezones?.defaultTimezone
415+
416+
const required =
417+
field.required ||
418+
(field.timezone && typeof field.timezone === 'object' && field.timezone.required)
419+
420+
const supportedTimezones =
421+
field.timezone && typeof field.timezone === 'object' && field.timezone.supportedTimezones
422+
? field.timezone.supportedTimezones
423+
: config.admin?.timezones?.supportedTimezones
413424

414425
const options =
415426
typeof supportedTimezones === 'function'
@@ -422,7 +433,7 @@ export const sanitizeFields = async ({
422433
name,
423434
defaultValue: defaultTimezone,
424435
options,
425-
required: field.required,
436+
required,
426437
})
427438

428439
fields.splice(++i, 0, timezoneField)

packages/payload/src/fields/config/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ import type {
116116
LabelFunction,
117117
PayloadComponent,
118118
StaticLabel,
119+
Timezone,
120+
TimezonesConfig,
119121
} from '../../config/types.js'
120122
import type { DBIdentifierName } from '../../database/types.js'
121123
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
@@ -723,6 +725,14 @@ export type CheckboxFieldClient = {
723725
} & FieldBaseClient &
724726
Pick<CheckboxField, 'type'>
725727

728+
type DateFieldTimezoneConfig = {
729+
/**
730+
* Make only the timezone required in the admin interface. This means a timezone is always required to be selected.
731+
*/
732+
required?: boolean
733+
supportedTimezones?: Timezone[]
734+
} & Pick<TimezonesConfig, 'defaultTimezone'>
735+
726736
export type DateField = {
727737
admin?: {
728738
components?: {
@@ -737,7 +747,7 @@ export type DateField = {
737747
/**
738748
* Enable timezone selection in the admin interface.
739749
*/
740-
timezone?: true
750+
timezone?: DateFieldTimezoneConfig | true
741751
type: 'date'
742752
validate?: DateFieldValidation
743753
} & Omit<FieldBase, 'validate'>

packages/payload/src/utilities/getObjectDotNotation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/**
2+
*
3+
* @deprecated use getObjectDotNotation from `'payload/shared'` instead of `'payload'`
4+
*
5+
* @example
6+
*
7+
* ```ts
8+
* import { getObjectDotNotation } from 'payload/shared'
9+
*
10+
* const obj = { a: { b: { c: 42 } } }
11+
* const value = getObjectDotNotation<number>(obj, 'a.b.c', 0) // value is 42
12+
* const defaultValue = getObjectDotNotation<number>(obj, 'a.b.x', 0) // defaultValue is 0
13+
* ```
14+
*/
115
export const getObjectDotNotation = <T>(
216
obj: Record<string, unknown>,
317
path: string,
Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
'use client'
22
import type { DateFieldClient, DefaultCellComponentProps } from 'payload'
33

4+
import { getObjectDotNotation } from 'payload/shared'
45
import React from 'react'
56

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

10-
export const DateCell: React.FC<DefaultCellComponentProps<DateFieldClient>> = ({
11-
cellData,
12-
field: { admin: { date } = {} },
13-
}) => {
11+
export const DateCell: React.FC<
12+
DefaultCellComponentProps<{ accessor?: string } & DateFieldClient>
13+
> = (props) => {
14+
const {
15+
cellData,
16+
field: { name, accessor, admin: { date } = {}, timezone: timezoneFromField },
17+
rowData,
18+
} = props
19+
1420
const {
1521
config: {
1622
admin: { dateFormat: dateFormatFromRoot },
1723
},
1824
} = useConfig()
25+
const { i18n } = useTranslation()
1926

20-
const dateFormat = date?.displayFormat || dateFormatFromRoot
27+
const fieldPath = accessor || name
2128

22-
const { i18n } = useTranslation()
29+
const timezoneFieldName = `${fieldPath}_tz`
30+
const timezone =
31+
Boolean(timezoneFromField) && rowData
32+
? getObjectDotNotation(rowData, timezoneFieldName, undefined)
33+
: undefined
34+
35+
const dateFormat = date?.displayFormat || dateFormatFromRoot
2336

24-
return <span>{cellData && formatDate({ date: cellData, i18n, pattern: dateFormat })}</span>
37+
return (
38+
<span>
39+
{Boolean(cellData) && formatDate({ date: cellData, i18n, pattern: dateFormat, timezone })}
40+
</span>
41+
)
2542
}

packages/ui/src/elements/TimezonePicker/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
background: none;
2929
border: none;
3030
padding: 0;
31+
padding-left: calc(var(--base) * 0.25);
3132
min-height: auto !important;
3233
position: relative;
3334
box-shadow: unset;

packages/ui/src/elements/TimezonePicker/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const TimezonePicker: React.FC<Props> = (props) => {
1818
id,
1919
onChange: onChangeFromProps,
2020
options: optionsFromProps,
21+
readOnly: readOnlyFromProps,
2122
required,
2223
selectedTimezone: selectedTimezoneFromProps,
2324
} = props
@@ -33,6 +34,8 @@ export const TimezonePicker: React.FC<Props> = (props) => {
3334
})
3435
}, [options, selectedTimezoneFromProps])
3536

37+
const readOnly = Boolean(readOnlyFromProps) || options.length === 1
38+
3639
return (
3740
<div className="timezone-picker-wrapper">
3841
<FieldLabel
@@ -43,8 +46,9 @@ export const TimezonePicker: React.FC<Props> = (props) => {
4346
/>
4447
<ReactSelect
4548
className="timezone-picker"
49+
disabled={readOnly}
4650
inputId={id}
47-
isClearable={true}
51+
isClearable={!required}
4852
isCreatable={false}
4953
onChange={(val: OptionObject) => {
5054
if (onChangeFromProps) {

packages/ui/src/elements/TimezonePicker/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { SelectFieldClient } from 'payload'
33
export type Props = {
44
id: string
55
onChange?: (val: string) => void
6+
readOnly?: boolean
67
required?: boolean
78
selectedTimezone?: string
89
} & Pick<SelectFieldClient, 'options'>

0 commit comments

Comments
 (0)