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.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +9173 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +9197 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/sanity.json +8 -0
- package/src/components/CustomRule/CustomRule.tsx +230 -0
- package/src/components/CustomRule/Monthly.tsx +67 -0
- package/src/components/CustomRule/Weekly.tsx +62 -0
- package/src/components/CustomRule/index.tsx +1 -0
- package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
- package/src/components/DateInputs/DateInput.tsx +81 -0
- package/src/components/DateInputs/DateTimeInput.tsx +122 -0
- package/src/components/DateInputs/base/DatePicker.tsx +34 -0
- package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
- package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
- package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
- package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
- package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
- package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
- package/src/components/DateInputs/base/calendar/constants.ts +33 -0
- package/src/components/DateInputs/base/calendar/features.ts +4 -0
- package/src/components/DateInputs/base/calendar/utils.ts +34 -0
- package/src/components/DateInputs/index.ts +2 -0
- package/src/components/DateInputs/types.ts +4 -0
- package/src/components/DateInputs/utils.ts +4 -0
- package/src/components/RecurringDate.tsx +139 -0
- package/src/constants.ts +36 -0
- package/src/index.ts +2 -0
- package/src/plugin.tsx +19 -0
- package/src/schema/recurringDates.tsx +44 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +16 -0
- 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,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
|
+
}
|