Skip to content

Commit fb71d36

Browse files
committed
Add new date field
1 parent 5d2f8ad commit fb71d36

File tree

5 files changed

+317
-0
lines changed

5 files changed

+317
-0
lines changed

src/fields/Date/Component.tsx

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React, { MouseEventHandler, useCallback, useMemo } from 'react'
2+
import { Label, useField } from 'payload/components/forms'
3+
import Error from 'payload/dist/admin/components/forms/Error'
4+
import type { DateField, Option, OptionObject, SelectField } from 'payload/types'
5+
import type { Timezone } from '.'
6+
import DatePicker from 'payload/dist/admin/components/elements/DatePicker'
7+
import ReactSelect from 'payload/dist/admin/components/elements/ReactSelect'
8+
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'
9+
import FieldDescription from 'payload/dist/admin/components/forms/FieldDescription'
10+
import '../../styles/date.scss'
11+
12+
type Props = DateField & {
13+
path: string
14+
readOnly?: boolean
15+
placeholder?: string
16+
className?: string
17+
custom: {
18+
timezone?: Timezone
19+
timezoneField: SelectField
20+
}
21+
}
22+
23+
const DateComponent: React.FC<Props> = ({
24+
readOnly,
25+
className,
26+
required,
27+
path,
28+
label,
29+
admin,
30+
custom,
31+
type,
32+
...others
33+
}) => {
34+
const { timezone, timezoneField: timezoneFieldProps } = custom
35+
const { value, setValue, showError, errorMessage } = useField<Props>({ path })
36+
const placeholder = admin?.placeholder ?? ''
37+
38+
const beforeInput = admin?.components?.beforeInput
39+
const afterInput = admin?.components?.afterInput
40+
41+
// @todo: figure it out later
42+
//const saveWithTimezone = timezone?.saveWithTimezone
43+
const datePickerProps = admin?.date
44+
45+
const timezonePath = path.includes('.')
46+
? path.slice(0, path.lastIndexOf('.')) + '.' + timezoneFieldProps.name
47+
: timezoneFieldProps.name
48+
49+
const timezoneField = useField<Props>({ path: timezonePath })
50+
51+
const timezoneValue = timezoneField.value
52+
53+
const renderedValue = useMemo(() => {
54+
// @ts-expect-error
55+
const modifiedValue = utcToZonedTime(value, timezoneValue)
56+
57+
return modifiedValue
58+
}, [value, timezoneValue])
59+
60+
const visibleTimezone = useMemo(() => {
61+
const options = timezoneFieldProps.options as OptionObject[]
62+
63+
const item = options.find(value => {
64+
// @ts-expect-error
65+
return value.value === timezoneValue
66+
})
67+
68+
return item
69+
}, [timezoneValue, timezoneFieldProps.options])
70+
71+
let dateFormat = datePickerProps?.displayFormat
72+
73+
if (!datePickerProps?.displayFormat) {
74+
// when no displayFormat is provided, determine format based on the picker appearance
75+
if (datePickerProps?.pickerAppearance === 'default') dateFormat = 'MM/dd/yyyy'
76+
else if (datePickerProps?.pickerAppearance === 'dayAndTime') dateFormat = 'MMM d, yyy h:mm a'
77+
else if (datePickerProps?.pickerAppearance === 'timeOnly') dateFormat = 'h:mm a'
78+
else if (datePickerProps?.pickerAppearance === 'dayOnly') dateFormat = 'MMM dd'
79+
else if (datePickerProps?.pickerAppearance === 'monthOnly') dateFormat = 'MMMM'
80+
}
81+
82+
const onChange = useCallback(
83+
(incomingDate: Date) => {
84+
if (!readOnly) {
85+
// @ts-expect-error
86+
const zonedTime = zonedTimeToUtc(incomingDate, timezoneValue)
87+
88+
setValue(zonedTime?.toISOString() || null)
89+
}
90+
},
91+
[timezoneValue],
92+
)
93+
94+
const classes = [
95+
'',
96+
'text',
97+
className,
98+
showError && 'error',
99+
readOnly && 'read-only',
100+
'container',
101+
]
102+
.filter(Boolean)
103+
.join(' ')
104+
105+
const isRequired = required
106+
const isReadonly = readOnly || admin?.readOnly
107+
108+
return (
109+
<div className={`bfDateFieldWrapper field-type`}>
110+
<Error showError={showError} message={errorMessage ?? ''} />
111+
<div>
112+
<div className={classes}>
113+
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
114+
<div className="fieldsWrapper">
115+
<div className="dateField">
116+
<Label
117+
htmlFor={`field-${path.replace(/\./gi, '__')}`}
118+
label={label}
119+
required={isRequired}
120+
/>
121+
122+
<DatePicker
123+
{...datePickerProps}
124+
onChange={onChange}
125+
readOnly={isReadonly}
126+
value={renderedValue}
127+
overrides={{ id: `field-${path.replace(/\./g, '__')}`, locale: undefined }}
128+
/>
129+
</div>
130+
<div className="timezoneField">
131+
<div>
132+
<Label
133+
htmlFor={`field-${timezonePath.replaceAll('.', '-')}`}
134+
label={timezoneFieldProps?.label ?? ''}
135+
/>
136+
</div>
137+
<ReactSelect
138+
inputId={`field-${timezonePath.replaceAll('.', '-')}`}
139+
value={visibleTimezone}
140+
isMulti={false}
141+
onChange={selected => {
142+
console.log('select', selected.value)
143+
timezoneField.setValue(selected.value)
144+
}}
145+
disabled={isReadonly}
146+
// @ts-expect-error
147+
options={timezoneFieldProps.options}
148+
/>
149+
</div>
150+
</div>
151+
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
152+
</div>
153+
</div>
154+
<FieldDescription
155+
className={`field-description-${path.replace(/\./g, '__')}`}
156+
description={admin?.description}
157+
value={value}
158+
/>
159+
</div>
160+
)
161+
}
162+
163+
export default DateComponent

src/fields/Date/index.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { Field, Option } from 'payload/types'
2+
import deepMerge from '../../utilities/deepMerge'
3+
import DateComponent from './Component'
4+
import { DateField as DateFieldType, SelectField } from 'payload/types'
5+
import { PartialRequired } from '../../utilities/partialRequired'
6+
import { timezones } from './timezones'
7+
8+
/**
9+
* Config for the timezone functionality
10+
*/
11+
export type Timezone = {
12+
/**
13+
* @default true
14+
*/
15+
enable: boolean
16+
/**
17+
* Saves the date with timezone information stored
18+
* @default false
19+
*/
20+
//saveWithTimezone: boolean
21+
/**
22+
* Array of options for timezones
23+
*/
24+
timezones?: Option[]
25+
/**
26+
* Timezone select field overrides. Add timezones to the 'timezones' property not the field options
27+
*/
28+
overrides: Omit<PartialRequired<SelectField, 'name'>, 'type' | 'options'>
29+
}
30+
31+
type Date = (
32+
/**
33+
* Field overrides
34+
*/
35+
dateOverrides: Omit<PartialRequired<DateFieldType, 'name'>, 'type'>,
36+
timezone?: Timezone,
37+
) => Field[]
38+
39+
export const DateField: Date = (
40+
dateOverrides,
41+
timezone = {
42+
enable: true,
43+
timezones,
44+
overrides: { name: 'timezoneSelect' },
45+
},
46+
) => {
47+
const timezoneField = deepMerge<SelectField, Omit<Partial<SelectField>, 'type'>>(
48+
{
49+
name: 'timezoneSelect',
50+
label: 'Select Timezone',
51+
type: 'select',
52+
options: timezone.timezones ?? timezones,
53+
admin: {
54+
hidden: true,
55+
},
56+
},
57+
timezone.overrides,
58+
)
59+
60+
const dateField = deepMerge<DateFieldType, Omit<Partial<DateFieldType>, 'type'>>(
61+
{
62+
name: 'date',
63+
type: 'date',
64+
admin: {
65+
components: {
66+
Field: DateComponent,
67+
},
68+
},
69+
custom: {
70+
config: { ...timezone, timezones: timezoneField.options },
71+
timezoneField,
72+
},
73+
},
74+
dateOverrides,
75+
)
76+
77+
const fields = [dateField, timezoneField]
78+
79+
return fields
80+
}

src/fields/Date/timezones.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const timezones = [
2+
{ label: 'International Date Line West (UTC-12:00)', value: 'Pacific/Midway' },
3+
{ label: 'Samoa Standard Time (UTC-11:00)', value: 'Pacific/Samoa' },
4+
{ label: 'Hawaii-Aleutian Standard Time (UTC-10:00)', value: 'Pacific/Honolulu' },
5+
{ label: 'Alaska Standard Time (UTC-09:00)', value: 'America/Anchorage' },
6+
{ label: 'Pacific Standard Time (UTC-08:00)', value: 'America/Los_Angeles' },
7+
{ label: 'Mountain Standard Time (UTC-07:00)', value: 'America/Denver' },
8+
{ label: 'Central Standard Time (UTC-06:00)', value: 'America/Chicago' },
9+
{ label: 'Eastern Standard Time (UTC-05:00)', value: 'America/New_York' },
10+
{ label: 'Atlantic Standard Time (UTC-04:00)', value: 'America/Halifax' },
11+
{ label: 'Newfoundland Standard Time (UTC-03:30)', value: 'America/St_Johns' },
12+
{ label: 'Brasília Standard Time (UTC-03:00)', value: 'America/Sao_Paulo' },
13+
{ label: 'Argentina Standard Time (UTC-03:00)', value: 'America/Argentina/Buenos_Aires' },
14+
{ label: 'Greenland Standard Time (UTC-03:00)', value: 'America/Godthab' },
15+
{ label: 'Mid-Atlantic Standard Time (UTC-02:00)', value: 'Atlantic/South_Georgia' },
16+
{ label: 'Azores Standard Time (UTC-01:00)', value: 'Atlantic/Azores' },
17+
{ label: 'Greenwich Mean Time (UTC+00:00)', value: 'Etc/GMT' },
18+
{ label: 'Central European Standard Time (UTC+01:00)', value: 'Europe/Berlin' },
19+
{ label: 'Eastern European Standard Time (UTC+02:00)', value: 'Europe/Helsinki' },
20+
{ label: 'Moscow Standard Time (UTC+03:00)', value: 'Europe/Moscow' },
21+
{ label: 'Pakistan Standard Time (UTC+05:00)', value: 'Asia/Karachi' },
22+
{ label: 'India Standard Time (UTC+05:30)', value: 'Asia/Kolkata' },
23+
{ label: 'Nepal Standard Time (UTC+05:45)', value: 'Asia/Kathmandu' },
24+
{ label: 'Bangladesh Standard Time (UTC+06:00)', value: 'Asia/Dhaka' },
25+
{ label: 'Myanmar Standard Time (UTC+06:30)', value: 'Asia/Yangon' },
26+
{ label: 'Indochina Time (UTC+07:00)', value: 'Asia/Bangkok' },
27+
{ label: 'China Standard Time (UTC+08:00)', value: 'Asia/Shanghai' },
28+
{ label: 'Japan Standard Time (UTC+09:00)', value: 'Asia/Tokyo' },
29+
{ label: 'Australian Central Standard Time (UTC+09:30)', value: 'Australia/Darwin' },
30+
{ label: 'Australian Eastern Standard Time (UTC+10:00)', value: 'Australia/Sydney' },
31+
{ label: 'Lord Howe Standard Time (UTC+10:30)', value: 'Australia/Lord_Howe' },
32+
{ label: 'New Zealand Standard Time (UTC+12:00)', value: 'Pacific/Auckland' },
33+
{ label: 'Tonga Standard Time (UTC+13:00)', value: 'Pacific/Tongatapu' },
34+
]

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { RangeField } from './fields/Range'
77
export { TelephoneField } from './fields/Telephone'
88
export { AlertBoxField } from './fields/AlertBox'
99
export { ColourPickerField } from './fields/ColourPicker'
10+
export { DateField } from './fields/Date'

src/styles/date.scss

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.bfDateFieldWrapper {
2+
position: relative;
3+
4+
.container {
5+
display: flex;
6+
}
7+
8+
.fieldsWrapper {
9+
display: flex;
10+
}
11+
12+
.dateField {
13+
min-width: 18rem;
14+
}
15+
16+
.timezoneField {
17+
min-width: 18rem;
18+
}
19+
20+
// Hacky fix
21+
.react-select .rs__control {
22+
padding-top: 0.45rem;
23+
padding-bottom: 0.38rem;
24+
}
25+
26+
.srOnly {
27+
border: 0 !important;
28+
clip: rect(1px, 1px, 1px, 1px) !important;
29+
-webkit-clip-path: inset(50%) !important;
30+
clip-path: inset(50%) !important;
31+
height: 1px !important;
32+
margin: -1px !important;
33+
overflow: hidden !important;
34+
padding: 0 !important;
35+
position: absolute !important;
36+
width: 1px !important;
37+
white-space: nowrap !important;
38+
}
39+
}

0 commit comments

Comments
 (0)