FormField.tsx390 lines · main
1import { format } from 'date-fns'
2import { CalendarIcon, ExternalLink } from 'lucide-react'
3import { useEffect } from 'react'
4import { useFormContext, type Control } from 'react-hook-form'
5import ReactMarkdown from 'react-markdown'
6import {
7 Button,
8 Calendar,
9 FormControl,
10 FormInputGroupInput,
11 Input,
12 InputGroup,
13 InputGroupAddon,
14 Popover,
15 PopoverContent,
16 PopoverTrigger,
17 Select,
18 SelectContent,
19 SelectItem,
20 SelectTrigger,
21 SelectValue,
22 Separator,
23 SheetSection,
24 Switch,
25 Textarea,
26 FormField as UIFormField,
27 useWatch,
28} from 'ui'
29import { Input as DataInput } from 'ui-patterns/DataInputs/Input'
30import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
31
32import type { Enum } from './AuthProvidersForm.types'
33import { Markdown } from '@/components/interfaces/Markdown'
34import { BASE_PATH } from '@/lib/constants'
35
36interface FormFieldProps {
37 projectRef: string | undefined
38 organizationSlug: string | undefined
39 name: string
40 properties: any
41 control: Control
42 hasAccess: boolean
43 disabled?: boolean
44 readOnly?: boolean
45}
46
47const FormField = ({
48 projectRef,
49 name,
50 properties,
51 organizationSlug,
52 control,
53 hasAccess,
54 disabled: disabledProp,
55 readOnly,
56}: FormFieldProps) => {
57 const { setValue } = useFormContext()
58 const { description: originalDescription } = properties
59 let description = originalDescription
60
61 if (originalDescription && projectRef) {
62 description = originalDescription.replace(
63 /\(\.\.\/auth\/(.*?)\)/g,
64 `(/project/${projectRef}/auth/$1)`
65 )
66 }
67
68 const fieldValue = useWatch({ control, name })
69 if (!hasAccess) {
70 const planMessage = organizationSlug
71 ? `Only available on [Pro plan](/org/${organizationSlug}/billing?panel=subscriptionPlan) and above.`
72 : ''
73 description = originalDescription ? `${originalDescription} ${planMessage}` : planMessage
74 }
75 const disabled =
76 disabledProp || (properties.type === 'boolean' ? !hasAccess && !fieldValue : !hasAccess)
77
78 const showValue = useWatch({
79 control,
80 name: properties.show?.key,
81 disabled: properties.show == null,
82 })
83
84 useEffect(() => {
85 if (properties.show?.key != null && !showValue && fieldValue !== '') {
86 setValue(name, '', { shouldDirty: true })
87 }
88 }, [fieldValue, name, properties.show?.key, setValue, showValue])
89
90 if (properties.show) {
91 if (properties.show.matches) {
92 if (!properties.show.matches.includes(showValue)) {
93 return null
94 }
95 } else if (!showValue) {
96 return null
97 }
98 }
99
100 switch (properties.type) {
101 case 'datetime':
102 return (
103 <>
104 <SheetSection>
105 <UIFormField
106 control={control}
107 name={name}
108 disabled={disabled || readOnly}
109 render={({ field }) => (
110 <FormItemLayout
111 layout="horizontal"
112 label={properties.title}
113 description={
114 description ? (
115 <ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
116 {description}
117 </ReactMarkdown>
118 ) : null
119 }
120 >
121 <FormControl>
122 <Popover>
123 <PopoverTrigger asChild>
124 <Button
125 type="outline"
126 className="w-full justify-start text-left font-normal px-3 py-4"
127 icon={<CalendarIcon className="h-4 w-4" />}
128 size="small"
129 >
130 {field.value ? format(new Date(field.value), 'PPP') : 'Pick a date'}
131 </Button>
132 </PopoverTrigger>
133 <PopoverContent className="w-auto p-0" align="start">
134 <Calendar
135 mode="single"
136 selected={field.value}
137 onSelect={(date) => {
138 field.onChange(date?.toISOString())
139 }}
140 initialFocus
141 />
142 </PopoverContent>
143 </Popover>
144 </FormControl>
145 </FormItemLayout>
146 )}
147 />
148 </SheetSection>
149 <Separator className="w-full" />
150 </>
151 )
152
153 case 'string':
154 return (
155 <>
156 <SheetSection>
157 <UIFormField
158 control={control}
159 name={name}
160 disabled={disabled}
161 render={({ field }) => (
162 <FormItemLayout
163 layout="horizontal"
164 label={properties.title}
165 description={
166 description ? (
167 <Markdown content={description} className="text-foreground-lighter" />
168 ) : null
169 }
170 >
171 <FormControl className="col-span-6">
172 {properties.isSecret ? (
173 <DataInput
174 {...field}
175 id={name}
176 size="small"
177 copy
178 reveal
179 readOnly={readOnly}
180 />
181 ) : (
182 <Input {...field} id={name} readOnly={readOnly} />
183 )}
184 </FormControl>
185 </FormItemLayout>
186 )}
187 />
188 </SheetSection>
189 <Separator className="w-full" />
190 </>
191 )
192
193 case 'multiline-string':
194 return (
195 <>
196 <SheetSection>
197 <UIFormField
198 control={control}
199 name={name}
200 disabled={disabled}
201 render={({ field }) => (
202 <FormItemLayout
203 layout="horizontal"
204 label={properties.title}
205 description={
206 description ? (
207 <Markdown content={description} className="text-foreground-lighter" />
208 ) : null
209 }
210 >
211 <FormControl className="col-span-6">
212 <Textarea
213 {...field}
214 id={name}
215 rows={4}
216 placeholder="Enter multi-line text"
217 className="resize-none"
218 readOnly={readOnly}
219 />
220 </FormControl>
221 </FormItemLayout>
222 )}
223 />
224 </SheetSection>
225 <Separator className="w-full" />
226 </>
227 )
228
229 case 'number':
230 return (
231 <>
232 <SheetSection>
233 <UIFormField
234 control={control}
235 name={name}
236 disabled={disabled}
237 render={({ field }) => (
238 <FormItemLayout
239 layout="horizontal"
240 label={properties.title}
241 description={
242 description ? (
243 <Markdown content={description} className="text-foreground-lighter" />
244 ) : null
245 }
246 >
247 <FormControl className="col-span-6">
248 {properties.units ? (
249 <InputGroup>
250 <FormInputGroupInput
251 {...field}
252 id={name}
253 type="number"
254 onChange={(e) =>
255 field.onChange(e.target.value === '' ? '' : Number(e.target.value))
256 }
257 readOnly={readOnly}
258 />
259 <InputGroupAddon align="inline-end">
260 <ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
261 {properties.units}
262 </ReactMarkdown>
263 </InputGroupAddon>
264 </InputGroup>
265 ) : (
266 <Input
267 {...field}
268 id={name}
269 type="number"
270 onChange={(e) =>
271 field.onChange(e.target.value === '' ? '' : Number(e.target.value))
272 }
273 readOnly={readOnly}
274 />
275 )}
276 </FormControl>
277 </FormItemLayout>
278 )}
279 />
280 </SheetSection>
281 <Separator className="w-full" />
282 </>
283 )
284
285 case 'boolean':
286 return (
287 <>
288 <SheetSection>
289 <UIFormField
290 control={control}
291 name={name}
292 disabled={disabled || readOnly}
293 render={({ field }) => (
294 <FormItemLayout
295 layout="horizontal"
296 label={properties.title}
297 description={
298 <div className="flex flex-col gap-1">
299 {description ? <Markdown content={description} /> : null}
300 {properties.link && (
301 <span>
302 <Button asChild type="default" size="tiny" icon={<ExternalLink />}>
303 <a href={properties.link} target="_blank" rel="noreferrer noopener">
304 Documentation
305 </a>
306 </Button>
307 </span>
308 )}
309 </div>
310 }
311 >
312 <FormControl className="col-span-6">
313 <Switch
314 id={name}
315 checked={field.value}
316 onCheckedChange={field.onChange}
317 size="small"
318 />
319 </FormControl>
320 </FormItemLayout>
321 )}
322 />
323 </SheetSection>
324 <Separator className="w-full" />
325 </>
326 )
327
328 case 'select':
329 return (
330 <>
331 <SheetSection>
332 <UIFormField
333 control={control}
334 name={name}
335 disabled={disabled || readOnly}
336 render={({ field }) => (
337 <FormItemLayout
338 layout="horizontal"
339 label={properties.title}
340 description={
341 description ? (
342 <div className="form-field-markdown">
343 <ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
344 {description}
345 </ReactMarkdown>
346 </div>
347 ) : null
348 }
349 >
350 <FormControl className="col-span-6">
351 <Select
352 defaultValue={properties.enum[0]?.value}
353 value={field.value}
354 onValueChange={field.onChange}
355 >
356 <SelectTrigger>
357 <SelectValue placeholder="Select an option" />
358 </SelectTrigger>
359 <SelectContent>
360 {properties.enum.map((option: Enum) => (
361 <SelectItem key={option.value} value={option.value}>
362 <span className="flex gap-2 items-center">
363 {option.icon ? (
364 <img
365 alt={`${option.label} icon`}
366 className="h-6 w-6"
367 src={`${BASE_PATH}/img/icons/${option.icon}`}
368 />
369 ) : null}
370 {option.label}
371 </span>
372 </SelectItem>
373 ))}
374 </SelectContent>
375 </Select>
376 </FormControl>
377 </FormItemLayout>
378 )}
379 />
380 </SheetSection>
381 <Separator className="w-full" />
382 </>
383 )
384
385 default:
386 return null
387 }
388}
389
390export default FormField