mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
0facd341a6
## Problem We used to have a `_Shadcn_` suffix for all the shadcn form components because we also had `formik` form components. This is not needed anymore. ## Solution - Remove the suffix - Update all usages
801 lines
32 KiB
TypeScript
801 lines
32 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { format } from 'date-fns'
|
|
import { CalendarIcon, ExternalLink, Trash, Upload } from 'lucide-react'
|
|
import { useRef, useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import {
|
|
Button,
|
|
Calendar,
|
|
Checkbox_Shadcn_,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormInputGroupInput,
|
|
Input_Shadcn_,
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupInput,
|
|
InputGroupText,
|
|
Popover_Shadcn_,
|
|
PopoverContent_Shadcn_,
|
|
PopoverTrigger_Shadcn_,
|
|
RadioGroupStacked,
|
|
RadioGroupStackedItem,
|
|
Select_Shadcn_,
|
|
SelectContent_Shadcn_,
|
|
SelectItem_Shadcn_,
|
|
SelectTrigger_Shadcn_,
|
|
SelectValue_Shadcn_,
|
|
Separator,
|
|
Sheet,
|
|
SheetContent,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetSection,
|
|
SheetTitle,
|
|
Switch,
|
|
Textarea,
|
|
} from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
|
|
import { getKeyValueFieldArrayValidationIssues } from 'ui-patterns/form/KeyValueFieldArray/validation'
|
|
import { SingleValueFieldArray } from 'ui-patterns/form/SingleValueFieldArray/SingleValueFieldArray'
|
|
import {
|
|
MultiSelector,
|
|
MultiSelectorContent,
|
|
MultiSelectorItem,
|
|
MultiSelectorList,
|
|
MultiSelectorTrigger,
|
|
} from 'ui-patterns/multi-select'
|
|
import * as z from 'zod'
|
|
|
|
const formSchema = z
|
|
.object({
|
|
name: z.string().min(1, 'Name is required'),
|
|
description: z.string().optional(),
|
|
maxConnections: z.number().min(1).max(1000),
|
|
enableFeature: z.boolean(),
|
|
enableRls: z.boolean(),
|
|
enableNotifications: z.boolean(),
|
|
enableAnalytics: z.boolean(),
|
|
region: z.string().min(1, 'Region is required'),
|
|
schemas: z.array(z.string()).min(1, 'At least one schema is required'),
|
|
queueType: z.enum(['basic', 'partitioned']),
|
|
expiryDate: z.date().optional(),
|
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
duration: z.number().min(5).max(30),
|
|
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
|
|
httpHeaders: z.array(z.object({ key: z.string().trim(), value: z.string().trim() })),
|
|
apiKey: z.string().optional(),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
getKeyValueFieldArrayValidationIssues({
|
|
rows: data.httpHeaders,
|
|
keyFieldName: 'key',
|
|
valueFieldName: 'value',
|
|
keyRequiredMessage: 'Header name is required',
|
|
valueRequiredMessage: 'Header value is required',
|
|
}).forEach((issue) => {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: issue.message,
|
|
path: ['httpHeaders', ...issue.path],
|
|
})
|
|
})
|
|
})
|
|
|
|
const fakeApiKey = 'sk_live_51H3x4mpl3_4nd_53cur3_k3y_1234567890'
|
|
|
|
export default function FormPatternsSidePanel() {
|
|
const [open, setOpen] = useState(false)
|
|
const uploadButtonRef = useRef<HTMLInputElement>(null)
|
|
const fileUploadRef = useRef<HTMLInputElement>(null)
|
|
const [logoFile, setLogoFile] = useState<File>()
|
|
const [logoUrl, setLogoUrl] = useState<string>()
|
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
name: '',
|
|
description: '',
|
|
maxConnections: 10,
|
|
enableFeature: false,
|
|
enableRls: true,
|
|
enableNotifications: false,
|
|
enableAnalytics: true,
|
|
region: '',
|
|
schemas: ['public'],
|
|
queueType: 'basic',
|
|
expiryDate: undefined,
|
|
password: '',
|
|
duration: 10,
|
|
redirectUris: [{ value: '' }],
|
|
httpHeaders: [{ key: '', value: '' }],
|
|
apiKey: fakeApiKey,
|
|
},
|
|
})
|
|
|
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
console.log(values)
|
|
setOpen(false)
|
|
}
|
|
|
|
const formId = 'sidepanel-form'
|
|
|
|
return (
|
|
<>
|
|
<Button type="primary" onClick={() => setOpen(true)}>
|
|
Open form panel
|
|
</Button>
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetContent size="lg" className="flex flex-col gap-0">
|
|
<SheetHeader>
|
|
<SheetTitle>Create Configuration</SheetTitle>
|
|
</SheetHeader>
|
|
<Form {...form}>
|
|
<form
|
|
id={formId}
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="overflow-auto flex-grow px-0"
|
|
>
|
|
{/* Text Input */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Text Input"
|
|
description="Single-line text entry for short values"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Input_Shadcn_ {...field} placeholder="Enter text" />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Password Input */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="password"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Password Input"
|
|
description="Masked input for secure text entry"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Input_Shadcn_ {...field} type="password" placeholder="Enter password" />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Copyable Input */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="apiKey"
|
|
render={() => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Copyable Input"
|
|
description="Read-only input with copy-to-clipboard functionality"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Input
|
|
copy
|
|
readOnly
|
|
className="input-mono"
|
|
value={form.getValues('apiKey') || ''}
|
|
onChange={() => {}}
|
|
onCopy={() => console.log('Copied to clipboard')}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Number Input */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="maxConnections"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Number Input"
|
|
description="Numeric input with min/max validation"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Input_Shadcn_
|
|
{...field}
|
|
type="number"
|
|
min={1}
|
|
max={1000}
|
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Input with Units */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="duration"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Input with Units"
|
|
description="Input with additional unit label"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<InputGroup>
|
|
<FormInputGroupInput {...field} type="number" min={5} max={30} />
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>MB</InputGroupText>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Textarea */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Textarea"
|
|
description="Multi-line text input for longer content"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Textarea
|
|
{...field}
|
|
rows={3}
|
|
placeholder="Enter multi-line text"
|
|
className="resize-none"
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Icon Upload */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={() => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Icon upload"
|
|
description="For icons, avatars, or small images with preview"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<div className="flex gap-4 items-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => uploadButtonRef.current?.click()}
|
|
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong"
|
|
style={{
|
|
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
|
|
}}
|
|
>
|
|
{!logoUrl && <Upload size={14} />}
|
|
</button>
|
|
<div className="flex gap-2 items-center">
|
|
<Button
|
|
type="default"
|
|
size="tiny"
|
|
icon={<Upload size={14} />}
|
|
onClick={() => uploadButtonRef.current?.click()}
|
|
>
|
|
Upload
|
|
</Button>
|
|
{logoUrl && (
|
|
<Button
|
|
type="default"
|
|
size="tiny"
|
|
icon={<Trash size={12} />}
|
|
onClick={() => {
|
|
setLogoFile(undefined)
|
|
setLogoUrl(undefined)
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref={uploadButtonRef}
|
|
className="hidden"
|
|
accept="image/png, image/jpeg"
|
|
onChange={(e) => {
|
|
const files = e.target.files
|
|
if (files && files.length > 0) {
|
|
const file = files[0]
|
|
setLogoFile(file)
|
|
setLogoUrl(URL.createObjectURL(file))
|
|
e.target.value = ''
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* File Upload */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={() => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="File Upload"
|
|
description="Drag-and-drop or select files for upload"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<div
|
|
className={`border-2 rounded-lg p-6 text-center bg-muted transition-colors duration-300 ${
|
|
isDragging
|
|
? 'border-strong border-dashed bg-muted'
|
|
: 'border-border border-dashed'
|
|
}`}
|
|
onDragOver={(e) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
onDrop={(e) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
const files = Array.from(e.dataTransfer.files)
|
|
setUploadedFiles((prev) => [...prev, ...files])
|
|
}}
|
|
>
|
|
<input
|
|
type="file"
|
|
ref={fileUploadRef}
|
|
className="hidden"
|
|
multiple
|
|
onChange={(e) => {
|
|
const files = e.target.files
|
|
if (files) {
|
|
setUploadedFiles((prev) => [...prev, ...Array.from(files)])
|
|
}
|
|
e.target.value = ''
|
|
}}
|
|
/>
|
|
<div className="flex flex-col items-center gap-y-2">
|
|
<Upload size={20} className="text-foreground-lighter" />
|
|
<p className="text-sm text-foreground-light">
|
|
{uploadedFiles.length > 0
|
|
? `${uploadedFiles.length} file${uploadedFiles.length > 1 ? 's' : ''} selected`
|
|
: 'Upload files'}
|
|
</p>
|
|
<p className="text-xs text-foreground-lighter">
|
|
Drag and drop or{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => fileUploadRef.current?.click()}
|
|
className="underline cursor-pointer hover:text-foreground-light"
|
|
>
|
|
select files
|
|
</button>{' '}
|
|
to upload
|
|
</p>
|
|
{uploadedFiles.length > 0 && (
|
|
<div className="mt-4 w-full space-y-2">
|
|
{uploadedFiles.map((file, idx) => (
|
|
<div
|
|
key={`${file.name}-${idx}`}
|
|
className="flex items-center justify-between gap-2 p-2 bg rounded border"
|
|
>
|
|
<span className="text-sm text-foreground-light truncate flex-1">
|
|
{file.name}
|
|
</span>
|
|
<Button
|
|
type="default"
|
|
size="tiny"
|
|
icon={<Trash size={12} />}
|
|
onClick={() => {
|
|
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Switch */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="enableFeature"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Switch"
|
|
description="Toggle for boolean on/off states"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
{/* Checkbox */}
|
|
<SheetSection>
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Checkbox"
|
|
description="Boolean values or multiple selections"
|
|
>
|
|
<div className="col-span-6 w-full flex flex-col gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="enableRls"
|
|
render={({ field }) => (
|
|
<div className="flex items-center w-full justify-start space-x-2">
|
|
<FormControl>
|
|
<Checkbox_Shadcn_
|
|
id="enable-rls"
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
<label
|
|
htmlFor="enable-rls"
|
|
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
|
>
|
|
Enable Row Level Security
|
|
</label>
|
|
</div>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="enableNotifications"
|
|
render={({ field }) => (
|
|
<div className="flex items-center w-full justify-start space-x-2">
|
|
<FormControl>
|
|
<Checkbox_Shadcn_
|
|
id="enable-notifications"
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
<label
|
|
htmlFor="enable-notifications"
|
|
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
|
>
|
|
Enable email notifications
|
|
</label>
|
|
</div>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="enableAnalytics"
|
|
render={({ field }) => (
|
|
<div className="flex items-center w-full justify-start space-x-2">
|
|
<FormControl>
|
|
<Checkbox_Shadcn_
|
|
id="enable-analytics"
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
<label
|
|
htmlFor="enable-analytics"
|
|
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
|
>
|
|
Enable analytics tracking
|
|
</label>
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
</FormItemLayout>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Select */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="region"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Select (Dropdown)"
|
|
description="Single selection from a list of options"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Select_Shadcn_ value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger_Shadcn_>
|
|
<SelectValue_Shadcn_ placeholder="Select an option" />
|
|
</SelectTrigger_Shadcn_>
|
|
<SelectContent_Shadcn_>
|
|
<SelectItem_Shadcn_ value="us-east-1">
|
|
US East (N. Virginia)
|
|
</SelectItem_Shadcn_>
|
|
<SelectItem_Shadcn_ value="us-west-2">
|
|
US West (Oregon)
|
|
</SelectItem_Shadcn_>
|
|
<SelectItem_Shadcn_ value="eu-west-1">
|
|
EU West (Ireland)
|
|
</SelectItem_Shadcn_>
|
|
</SelectContent_Shadcn_>
|
|
</Select_Shadcn_>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Multi-Select */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="schemas"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Multi-Select"
|
|
description="Multiple selection from a list"
|
|
>
|
|
<div className="col-span-6">
|
|
<MultiSelector
|
|
onValuesChange={field.onChange}
|
|
values={field.value}
|
|
size="small"
|
|
className="w-full"
|
|
>
|
|
<MultiSelectorTrigger
|
|
mode="inline-combobox"
|
|
label="Select options..."
|
|
badgeLimit="wrap"
|
|
showIcon={false}
|
|
deletableBadge
|
|
className="w-full !min-w-lg"
|
|
/>
|
|
<MultiSelectorContent>
|
|
<MultiSelectorList>
|
|
<MultiSelectorItem value="public">public</MultiSelectorItem>
|
|
<MultiSelectorItem value="auth">auth</MultiSelectorItem>
|
|
<MultiSelectorItem value="storage">storage</MultiSelectorItem>
|
|
</MultiSelectorList>
|
|
</MultiSelectorContent>
|
|
</MultiSelector>
|
|
</div>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Radio Group */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="queueType"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Radio Group"
|
|
description="Single selection from multiple options"
|
|
>
|
|
<div className="col-span-6">
|
|
<RadioGroupStacked value={field.value} onValueChange={field.onChange}>
|
|
<RadioGroupStackedItem
|
|
value="basic"
|
|
label="Option 1"
|
|
description="First option description"
|
|
/>
|
|
<RadioGroupStackedItem
|
|
value="partitioned"
|
|
label="Option 2"
|
|
description="Second option description"
|
|
/>
|
|
</RadioGroupStacked>
|
|
</div>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Date Picker */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="expiryDate"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Date Picker"
|
|
description="Date selection with calendar popover"
|
|
>
|
|
<FormControl className="col-span-6">
|
|
<Popover_Shadcn_>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button
|
|
type="outline"
|
|
className="bg-control w-full justify-start text-left font-normal px-3 py-4"
|
|
icon={<CalendarIcon className="h-4 w-4" />}
|
|
>
|
|
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="w-auto p-0" align="start">
|
|
<Calendar
|
|
mode="single"
|
|
selected={field.value}
|
|
onSelect={field.onChange}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Field Array */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="redirectUris"
|
|
render={() => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Field Array"
|
|
description="Dynamic list for adding/removing items"
|
|
>
|
|
<div className="col-span-6">
|
|
<SingleValueFieldArray
|
|
control={form.control}
|
|
name="redirectUris"
|
|
valueFieldName="value"
|
|
createEmptyRow={() => ({ value: '' })}
|
|
placeholder="https://example.com/callback"
|
|
addLabel="Add redirect URI"
|
|
removeLabel="Remove redirect URI"
|
|
/>
|
|
</div>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Key/Value Field Array */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="httpHeaders"
|
|
render={() => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Key/Value Field Array"
|
|
description="Repeated text pairs for headers, parameters, and config entries"
|
|
>
|
|
<div className="col-span-6">
|
|
<KeyValueFieldArray
|
|
control={form.control}
|
|
name="httpHeaders"
|
|
keyFieldName="key"
|
|
valueFieldName="value"
|
|
createEmptyRow={() => ({ key: '', value: '' })}
|
|
keyPlaceholder="Header name"
|
|
valuePlaceholder="Header value"
|
|
addLabel="Add header"
|
|
removeLabel="Remove header"
|
|
/>
|
|
</div>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
|
|
|
{/* Action Field */}
|
|
<SheetSection>
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Action Field"
|
|
description="Button or link for navigation or performable actions"
|
|
>
|
|
<div className="col-span-6 flex gap-2 items-center">
|
|
<Button
|
|
type="default"
|
|
icon={<ExternalLink size={14} />}
|
|
onClick={() => console.log('Action performed')}
|
|
>
|
|
View documentation
|
|
</Button>
|
|
<Button type="default" onClick={() => console.log('Reset action')}>
|
|
Reset API key
|
|
</Button>
|
|
</div>
|
|
</FormItemLayout>
|
|
</SheetSection>
|
|
</form>
|
|
</Form>
|
|
<SheetFooter>
|
|
<Button
|
|
type="default"
|
|
onClick={() => {
|
|
form.reset()
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="primary" form={formId} htmlType="submit">
|
|
Create
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</>
|
|
)
|
|
}
|