sanity-plugin-recurring-dates 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/index.d.ts +29 -0
  4. package/dist/index.esm.js +9173 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +9197 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +93 -0
  9. package/sanity.json +8 -0
  10. package/src/components/CustomRule/CustomRule.tsx +230 -0
  11. package/src/components/CustomRule/Monthly.tsx +67 -0
  12. package/src/components/CustomRule/Weekly.tsx +62 -0
  13. package/src/components/CustomRule/index.tsx +1 -0
  14. package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
  15. package/src/components/DateInputs/DateInput.tsx +81 -0
  16. package/src/components/DateInputs/DateTimeInput.tsx +122 -0
  17. package/src/components/DateInputs/base/DatePicker.tsx +34 -0
  18. package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
  19. package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
  20. package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
  21. package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
  22. package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
  23. package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
  24. package/src/components/DateInputs/base/calendar/constants.ts +33 -0
  25. package/src/components/DateInputs/base/calendar/features.ts +4 -0
  26. package/src/components/DateInputs/base/calendar/utils.ts +34 -0
  27. package/src/components/DateInputs/index.ts +2 -0
  28. package/src/components/DateInputs/types.ts +4 -0
  29. package/src/components/DateInputs/utils.ts +4 -0
  30. package/src/components/RecurringDate.tsx +139 -0
  31. package/src/constants.ts +36 -0
  32. package/src/index.ts +2 -0
  33. package/src/plugin.tsx +19 -0
  34. package/src/schema/recurringDates.tsx +44 -0
  35. package/src/types.ts +27 -0
  36. package/src/utils.ts +16 -0
  37. package/v2-incompatible.js +11 -0
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "sanity-plugin-recurring-dates",
3
+ "version": "1.0.0",
4
+ "description": "Add a custom input component to your Sanity Studio to manage recurring dates (e.g. for events)",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-plugin"
8
+ ],
9
+ "homepage": "https://github.com/thebiggianthead/sanity-plugin-recurring-dates#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/thebiggianthead/sanity-plugin-recurring-dates/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:thebiggianthead/sanity-plugin-recurring-dates.git"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Tom Smith <tom@sanity.io>",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "source": "./src/index.ts",
23
+ "require": "./dist/index.js",
24
+ "import": "./dist/index.esm.js",
25
+ "default": "./dist/index.esm.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.esm.js",
31
+ "source": "./src/index.ts",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "sanity.json",
36
+ "src",
37
+ "v2-incompatible.js"
38
+ ],
39
+ "scripts": {
40
+ "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict",
41
+ "clean": "rimraf dist",
42
+ "format": "prettier --write --cache --ignore-unknown .",
43
+ "link-watch": "plugin-kit link-watch",
44
+ "lint": "eslint .",
45
+ "prepublishOnly": "run-s build",
46
+ "watch": "pkg-utils watch --strict",
47
+ "prepare": "husky install"
48
+ },
49
+ "dependencies": {
50
+ "@sanity/icons": "^2.4.1",
51
+ "@sanity/incompatible-plugin": "^1.0.4",
52
+ "@sanity/ui": "^1.7.4",
53
+ "lodash": "^4.17.21",
54
+ "rrule": "^2.7.2",
55
+ "sanity-plugin-utils": "^1.6.2"
56
+ },
57
+ "devDependencies": {
58
+ "@commitlint/cli": "^17.6.7",
59
+ "@commitlint/config-conventional": "^17.6.7",
60
+ "@sanity/pkg-utils": "^2.3.9",
61
+ "@sanity/plugin-kit": "^3.1.7",
62
+ "@sanity/semantic-release-preset": "^4.1.2",
63
+ "@types/react": "^18.2.17",
64
+ "@typescript-eslint/eslint-plugin": "^6.2.0",
65
+ "@typescript-eslint/parser": "^6.2.0",
66
+ "eslint": "^8.45.0",
67
+ "eslint-config-prettier": "^8.9.0",
68
+ "eslint-config-sanity": "^6.0.0",
69
+ "eslint-plugin-prettier": "^5.0.0",
70
+ "eslint-plugin-react": "^7.33.0",
71
+ "eslint-plugin-react-hooks": "^4.6.0",
72
+ "eslint-plugin-simple-import-sort": "^10.0.0",
73
+ "husky": "^8.0.3",
74
+ "lint-staged": "^13.2.3",
75
+ "npm-run-all": "^4.1.5",
76
+ "prettier": "^3.0.0",
77
+ "prettier-plugin-packagejson": "^2.4.5",
78
+ "react": "^18.2.0",
79
+ "react-dom": "^18.2.0",
80
+ "react-is": "^18.2.0",
81
+ "rimraf": "^5.0.1",
82
+ "sanity": "^3.14.4",
83
+ "styled-components": "^5.3.11",
84
+ "typescript": "^5.1.6"
85
+ },
86
+ "peerDependencies": {
87
+ "react": "^18",
88
+ "sanity": "^3"
89
+ },
90
+ "engines": {
91
+ "node": ">=14"
92
+ }
93
+ }
package/sanity.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "parts": [
3
+ {
4
+ "implements": "part:@sanity/base/sanity-root",
5
+ "path": "./v2-incompatible.js"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,230 @@
1
+ import {Box, Button, Dialog, Flex, Radio, Select, Stack, Text, TextInput} from '@sanity/ui'
2
+ import React, {useCallback, useMemo, useState} from 'react'
3
+ import {Options, RRule, rrulestr, Weekday} from 'rrule'
4
+ import {type ObjectInputProps, set} from 'sanity'
5
+
6
+ import {DEFAULT_COUNTS} from '../../constants'
7
+ import {PluginConfig} from '../../types'
8
+ import {DateInput} from '../DateInputs'
9
+ import {Monthly} from './Monthly'
10
+ import {Weekly} from './Weekly'
11
+
12
+ export function CustomRule({
13
+ open,
14
+ onClose,
15
+ onChange,
16
+ initialValue,
17
+ startDate,
18
+ dateTimeOptions,
19
+ }: {
20
+ open: boolean
21
+ onClose: () => void
22
+ onChange: ObjectInputProps['onChange']
23
+ initialValue: string
24
+ startDate: string | undefined
25
+ dateTimeOptions: PluginConfig['dateTimeOptions']
26
+ }) {
27
+ const initialRule = useMemo(() => {
28
+ return initialValue ? rrulestr(initialValue) : new RRule()
29
+ }, [initialValue])
30
+
31
+ const [frequency, setFrequency] = useState<Options['freq']>(initialRule.origOptions.freq || 1)
32
+ const [interval, setInterval] = useState<Options['interval']>(
33
+ initialRule.origOptions.interval || 1,
34
+ )
35
+ const [count, setCount] = useState<Options['count']>(initialRule.origOptions.count || null)
36
+ const [until, setUntil] = useState<Options['until'] | number>(
37
+ initialRule.origOptions.until || null,
38
+ )
39
+ const [byweekday, setByweekday] = useState<Options['byweekday']>(
40
+ initialRule.origOptions.byweekday || null,
41
+ )
42
+
43
+ const handleChange = useCallback(
44
+ (event: React.FormEvent<HTMLInputElement> | React.FormEvent<HTMLSelectElement>) => {
45
+ const {name, value} = event.currentTarget
46
+
47
+ if (name === 'freq') {
48
+ setFrequency(Number(value))
49
+ } else if (name === 'interval') {
50
+ setInterval(Number(value))
51
+ } else if (name === 'interval') {
52
+ setCount(Number(value))
53
+ } else if (name === 'count') {
54
+ setCount(Number(value))
55
+ }
56
+ },
57
+ [],
58
+ )
59
+
60
+ const getUntilDate = useCallback(() => {
61
+ const fromDate = new Date(startDate ? startDate : Date.now())
62
+
63
+ if (frequency === RRule.YEARLY) {
64
+ fromDate.setFullYear(fromDate.getFullYear() + DEFAULT_COUNTS[frequency])
65
+ } else if (frequency === RRule.MONTHLY) {
66
+ fromDate.setMonth(fromDate.getMonth() + DEFAULT_COUNTS[frequency])
67
+ } else if (frequency === RRule.WEEKLY) {
68
+ fromDate.setDate(fromDate.getDate() + DEFAULT_COUNTS[frequency] * 7)
69
+ } else if (frequency === RRule.DAILY) {
70
+ fromDate.setDate(fromDate.getDate() + DEFAULT_COUNTS[frequency])
71
+ }
72
+
73
+ return fromDate
74
+ }, [frequency, startDate])
75
+
76
+ const handleUntilChange = useCallback((date: string | null) => {
77
+ if (date) {
78
+ setUntil(new Date(date))
79
+ }
80
+ }, [])
81
+
82
+ const handleEndChange = useCallback(
83
+ (event: React.FormEvent<HTMLInputElement>) => {
84
+ const {value} = event.currentTarget
85
+
86
+ if (!value) {
87
+ setUntil(null)
88
+ setCount(null)
89
+ } else if (value == 'count') {
90
+ setCount(DEFAULT_COUNTS[frequency])
91
+ setUntil(null)
92
+ } else if (value == 'until') {
93
+ const untilDate = getUntilDate()
94
+ setUntil(untilDate)
95
+ setCount(null)
96
+ }
97
+ },
98
+ [frequency, getUntilDate],
99
+ )
100
+
101
+ const handleConfirm = useCallback(() => {
102
+ const newOptions = {
103
+ freq: frequency,
104
+ interval,
105
+ count: count || null,
106
+ until: until ? (until as Date) : null,
107
+ byweekday,
108
+ }
109
+
110
+ const newRule = new RRule(newOptions)
111
+
112
+ onClose()
113
+ onChange(set(newRule.toString(), ['rrule']))
114
+ }, [byweekday, count, frequency, interval, onChange, onClose, until])
115
+
116
+ return (
117
+ open && (
118
+ <Dialog
119
+ header="Custom recurrence"
120
+ id="dialog-example"
121
+ onClose={onClose}
122
+ zOffset={1000}
123
+ width={1}
124
+ >
125
+ <Flex direction="column">
126
+ <Box flex={1} overflow="auto" padding={4}>
127
+ <Stack space={4}>
128
+ <Flex gap={2} align="center">
129
+ <Text style={{whiteSpace: 'nowrap'}}>Repeat every</Text>
130
+ <Box style={{width: '75px'}}>
131
+ <TextInput
132
+ name="interval"
133
+ type="number"
134
+ value={interval}
135
+ onChange={handleChange}
136
+ />
137
+ </Box>
138
+ <Box>
139
+ <Select name="freq" value={frequency} onChange={handleChange}>
140
+ <option value={RRule.YEARLY}>years</option>
141
+ <option value={RRule.MONTHLY}>months</option>
142
+ <option value={RRule.WEEKLY}>weeks</option>
143
+ <option value={RRule.DAILY}>days</option>
144
+ </Select>
145
+ </Box>
146
+ </Flex>
147
+
148
+ {frequency === RRule.MONTHLY && (
149
+ <Monthly byweekday={byweekday as Weekday} setByweekday={setByweekday} />
150
+ )}
151
+
152
+ {frequency === RRule.WEEKLY && (
153
+ <Weekly byweekday={byweekday as Weekday} setByweekday={setByweekday} />
154
+ )}
155
+
156
+ <Stack space={2}>
157
+ <Text>Ends</Text>
158
+ <Flex gap={2} paddingY={2} align="center">
159
+ <Radio
160
+ checked={!count && !until}
161
+ name="ends"
162
+ onChange={handleEndChange}
163
+ value=""
164
+ id="ends-never"
165
+ />
166
+ <Text htmlFor="ends-never" as="label">
167
+ Never
168
+ </Text>
169
+ </Flex>
170
+ <Flex gap={2} align="center">
171
+ <Radio
172
+ checked={!!until}
173
+ name="ends"
174
+ onChange={handleEndChange}
175
+ value="until"
176
+ id="ends-until"
177
+ />
178
+ <Text htmlFor="ends-until" as="label" style={{width: '75px'}}>
179
+ On
180
+ </Text>
181
+ <Box style={{width: '200px'}}>
182
+ <DateInput
183
+ id="until"
184
+ onChange={handleUntilChange}
185
+ type={{
186
+ name: 'until',
187
+ title: 'Date',
188
+ options: dateTimeOptions,
189
+ }}
190
+ value={until ? new Date(until) : getUntilDate()}
191
+ disabled={!until}
192
+ />
193
+ </Box>
194
+ </Flex>
195
+ <Flex gap={2} align="center">
196
+ <Radio
197
+ checked={!!count}
198
+ name="ends"
199
+ onChange={handleEndChange}
200
+ value="count"
201
+ id="ends-count"
202
+ />
203
+ <Text htmlFor="ends-count" as="label" style={{width: '75px'}}>
204
+ After
205
+ </Text>
206
+ <Box style={{width: '75px'}}>
207
+ <TextInput
208
+ name="count"
209
+ type="number"
210
+ value={count || DEFAULT_COUNTS[frequency]}
211
+ onChange={handleChange}
212
+ disabled={!count}
213
+ />
214
+ </Box>
215
+ <Text style={{whiteSpace: 'nowrap'}}>occurrences</Text>
216
+ </Flex>
217
+ </Stack>
218
+ </Stack>
219
+ </Box>
220
+ <Box paddingX={4} paddingY={3} style={{borderTop: '1px solid var(--card-border-color)'}}>
221
+ <Flex gap={2} justify="flex-end">
222
+ <Button text="Cancel" mode="ghost" onClick={onClose} />
223
+ <Button text="Done" tone="positive" onClick={handleConfirm} />
224
+ </Flex>
225
+ </Box>
226
+ </Flex>
227
+ </Dialog>
228
+ )
229
+ )
230
+ }
@@ -0,0 +1,67 @@
1
+ import {Box, Flex, Select, Text} from '@sanity/ui'
2
+ import React, {useCallback} from 'react'
3
+ import {Options, Weekday} from 'rrule'
4
+
5
+ import {DAYS} from '../../constants'
6
+
7
+ interface MonthlyProps {
8
+ byweekday: Weekday
9
+ setByweekday: (value: Options['byweekday']) => void
10
+ }
11
+
12
+ export function Monthly(props: MonthlyProps) {
13
+ const {byweekday, setByweekday} = props
14
+
15
+ const {weekday: dayNo, n: weekNo} =
16
+ byweekday && Array.isArray(byweekday) ? byweekday[0] : {weekday: null, n: null}
17
+
18
+ const handleChange = useCallback(
19
+ (event: React.FormEvent<HTMLSelectElement>) => {
20
+ const {value, name} = event.currentTarget
21
+
22
+ if (name == 'week') {
23
+ if (value == '') {
24
+ setByweekday(null)
25
+ } else {
26
+ const newWeekday = new Weekday(dayNo ? dayNo : 0, Number(value))
27
+ setByweekday([newWeekday])
28
+ }
29
+ } else if (name == 'day') {
30
+ const newWeekday = new Weekday(Number(value), weekNo ? weekNo : 1)
31
+ setByweekday([newWeekday])
32
+ }
33
+ },
34
+ [dayNo, setByweekday, weekNo],
35
+ )
36
+
37
+ return (
38
+ <Flex gap={2} align="center">
39
+ <Text style={{whiteSpace: 'nowrap'}}>On the</Text>
40
+ <Box>
41
+ <Select name="week" value={weekNo?.toString()} onChange={handleChange}>
42
+ <option value="">same day</option>
43
+ <option value="1">first</option>
44
+ <option value="2">second</option>
45
+ <option value="3">third</option>
46
+ <option value="4">fourth</option>
47
+ <option value="5">fifth</option>
48
+ <option value="-1">last</option>
49
+ </Select>
50
+ </Box>
51
+ {weekNo && (
52
+ <Box>
53
+ <Select name="day" value={dayNo ? dayNo : 1} onChange={handleChange}>
54
+ {DAYS.map((day: string, i: number) => {
55
+ const weekday = new Weekday(i)
56
+ return (
57
+ <option value={weekday.weekday} key={weekday.weekday}>
58
+ {day}
59
+ </option>
60
+ )
61
+ })}
62
+ </Select>
63
+ </Box>
64
+ )}
65
+ </Flex>
66
+ )
67
+ }
@@ -0,0 +1,62 @@
1
+ import {Button, Grid, Stack, Text} from '@sanity/ui'
2
+ import React, {useCallback, useMemo} from 'react'
3
+ import {type Options, Weekday} from 'rrule'
4
+
5
+ import {DAYS} from '../../constants'
6
+
7
+ interface WeeklyProps {
8
+ byweekday: Weekday
9
+ setByweekday: (value: Options['byweekday']) => void
10
+ }
11
+
12
+ export function Weekly(props: WeeklyProps) {
13
+ const {byweekday, setByweekday} = props
14
+
15
+ const currentWeekdays: number[] = useMemo(() => {
16
+ return Array.isArray(byweekday) ? byweekday.map((weekday) => weekday.weekday) : []
17
+ }, [byweekday])
18
+
19
+ const handleChange = useCallback(
20
+ (event: React.MouseEvent<HTMLButtonElement>) => {
21
+ const value = Number(event.currentTarget.value)
22
+
23
+ const index = currentWeekdays.indexOf(value)
24
+
25
+ if (index === -1) {
26
+ currentWeekdays.push(value)
27
+ } else {
28
+ currentWeekdays.splice(index, 1)
29
+ }
30
+
31
+ setByweekday(
32
+ currentWeekdays.length
33
+ ? currentWeekdays.map((currentWeekday) => new Weekday(Number(currentWeekday)))
34
+ : null,
35
+ )
36
+ },
37
+ [currentWeekdays, setByweekday],
38
+ )
39
+
40
+ return (
41
+ <Stack space={3}>
42
+ <Text style={{whiteSpace: 'nowrap'}}>Repeats on</Text>
43
+ <Grid columns={DAYS.length} gap={1}>
44
+ {DAYS.map((day: string, i: number) => {
45
+ const weekday = new Weekday(i)
46
+
47
+ return (
48
+ <Button
49
+ key={day}
50
+ mode={currentWeekdays && currentWeekdays.includes(i) ? 'default' : 'ghost'}
51
+ tone={currentWeekdays && currentWeekdays.includes(i) ? 'primary' : 'default'}
52
+ text={weekday.toString()}
53
+ value={i}
54
+ style={{cursor: 'pointer'}}
55
+ onClick={handleChange}
56
+ />
57
+ )
58
+ })}
59
+ </Grid>
60
+ </Stack>
61
+ )
62
+ }
@@ -0,0 +1 @@
1
+ export {CustomRule} from './CustomRule'
@@ -0,0 +1,111 @@
1
+ /* eslint-disable no-nested-ternary */
2
+
3
+ import {TextInput, useForwardedRef} from '@sanity/ui'
4
+ import React, {useEffect} from 'react'
5
+
6
+ import {DateTimeInput} from './base/DateTimeInput'
7
+ import {ParseResult} from './types'
8
+
9
+ export interface CommonDateTimeInputProps {
10
+ id: string
11
+ deserialize: (value: string) => ParseResult
12
+ formatInputValue: (date: Date) => string
13
+ onChange: (nextDate: string | null) => void
14
+ parseInputValue: (inputValue: string) => ParseResult
15
+ placeholder?: string
16
+ readOnly: boolean | undefined
17
+ selectTime?: boolean
18
+ serialize: (date: Date) => string
19
+ timeStep?: number
20
+ value: number | Date | string | undefined | null
21
+ }
22
+
23
+ const DEFAULT_PLACEHOLDER_TIME = new Date()
24
+
25
+ export const CommonDateTimeInput = React.forwardRef(function CommonDateTimeInput(
26
+ props: CommonDateTimeInputProps,
27
+ ref: React.ForwardedRef<HTMLInputElement>,
28
+ ) {
29
+ const {
30
+ id,
31
+ deserialize,
32
+ formatInputValue,
33
+ onChange,
34
+ parseInputValue,
35
+ placeholder,
36
+ readOnly,
37
+ selectTime,
38
+ serialize,
39
+ timeStep,
40
+ value,
41
+ ...restProps
42
+ } = props
43
+
44
+ const [localValue, setLocalValue] = React.useState<string | null>(null)
45
+
46
+ useEffect(() => {
47
+ setLocalValue(null)
48
+ }, [value])
49
+
50
+ const handleDatePickerInputChange = React.useCallback(
51
+ (event: any) => {
52
+ const nextInputValue = event.currentTarget.value
53
+ const result = nextInputValue === '' ? null : parseInputValue(nextInputValue)
54
+
55
+ if (result === null) {
56
+ onChange(null)
57
+
58
+ // If the field value is undefined and we are clearing the invalid value
59
+ // the above useEffect won't trigger, so we do some extra clean up here
60
+ if (typeof value === 'undefined' && localValue) {
61
+ setLocalValue(null)
62
+ }
63
+ } else if (result.isValid) {
64
+ onChange(serialize(result.date))
65
+ } else {
66
+ setLocalValue(nextInputValue)
67
+ }
68
+ },
69
+ [parseInputValue, onChange, value, localValue, serialize],
70
+ )
71
+
72
+ const handleDatePickerChange = React.useCallback(
73
+ (nextDate: Date | null) => {
74
+ onChange(nextDate ? serialize(nextDate) : null)
75
+ },
76
+ [serialize, onChange],
77
+ )
78
+
79
+ const forwardedRef = useForwardedRef(ref)
80
+
81
+ const parseResult = localValue
82
+ ? parseInputValue(localValue)
83
+ : value
84
+ ? deserialize(value as string)
85
+ : null
86
+
87
+ const inputValue = localValue
88
+ ? localValue
89
+ : parseResult?.isValid
90
+ ? formatInputValue(parseResult.date)
91
+ : (value as string)
92
+
93
+ return readOnly ? (
94
+ <TextInput value={inputValue} readOnly disabled={readOnly} />
95
+ ) : (
96
+ <DateTimeInput
97
+ {...restProps}
98
+ id={id}
99
+ selectTime={selectTime}
100
+ timeStep={timeStep}
101
+ placeholder={placeholder || `e.g. ${formatInputValue(DEFAULT_PLACEHOLDER_TIME)}`}
102
+ ref={forwardedRef}
103
+ value={parseResult?.date}
104
+ inputValue={inputValue || ''}
105
+ readOnly={Boolean(readOnly)}
106
+ onInputChange={handleDatePickerInputChange}
107
+ onChange={handleDatePickerChange}
108
+ customValidity={parseResult?.error}
109
+ />
110
+ )
111
+ })
@@ -0,0 +1,81 @@
1
+ import {format, parse} from '@sanity/util/legacyDateFormat'
2
+ import React, {useCallback} from 'react'
3
+
4
+ import {CommonDateTimeInput} from './CommonDateTimeInput'
5
+
6
+ interface ParsedOptions {
7
+ dateFormat: string
8
+ calendarTodayLabel: string
9
+ }
10
+
11
+ interface SchemaOptions {
12
+ dateFormat?: string
13
+ calendarTodayLabel?: string
14
+ }
15
+
16
+ type DateInputProps = {
17
+ id: string
18
+ onChange: (date: string | null) => void
19
+ disabled?: boolean
20
+ value: number | Date | null
21
+ type: {
22
+ name: string
23
+ title: string
24
+ description?: string
25
+ options?: SchemaOptions
26
+ placeholder?: string
27
+ }
28
+ }
29
+
30
+ // This is the format dates are stored on
31
+ const VALUE_FORMAT = 'YYYY-MM-DD'
32
+ // default to how they are stored
33
+ const DEFAULT_DATE_FORMAT = VALUE_FORMAT
34
+
35
+ function parseOptions(options: SchemaOptions = {}): ParsedOptions {
36
+ return {
37
+ dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT,
38
+ calendarTodayLabel: options.calendarTodayLabel || 'Today',
39
+ }
40
+ }
41
+
42
+ const deserialize = (value: string) => parse(value, VALUE_FORMAT)
43
+ const serialize = (date: Date) => format(date, VALUE_FORMAT)
44
+
45
+ /**
46
+ * @hidden
47
+ * @beta */
48
+ export function DateInput(props: DateInputProps) {
49
+ const {id, onChange, type, value, disabled, ...rest} = props
50
+
51
+ const {dateFormat} = parseOptions(type.options)
52
+
53
+ const handleChange = useCallback(
54
+ (nextDate: string | null) => {
55
+ onChange(nextDate)
56
+ },
57
+ [onChange],
58
+ )
59
+
60
+ const formatInputValue = useCallback((date: Date) => format(date, dateFormat), [dateFormat])
61
+
62
+ const parseInputValue = useCallback(
63
+ (inputValue: string) => parse(inputValue, dateFormat),
64
+ [dateFormat],
65
+ )
66
+
67
+ return (
68
+ <CommonDateTimeInput
69
+ id={id}
70
+ {...rest}
71
+ deserialize={deserialize}
72
+ formatInputValue={formatInputValue}
73
+ onChange={handleChange}
74
+ parseInputValue={parseInputValue}
75
+ readOnly={disabled}
76
+ selectTime={false}
77
+ serialize={serialize}
78
+ value={value}
79
+ />
80
+ )
81
+ }