FormField.tsx 12 KB

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