mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
802 lines
31 KiB
TypeScript
802 lines
31 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { format } from 'date-fns'
|
|
import { ExternalLink, Trash, Upload } from 'lucide-react'
|
|
import { useRef, useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import {
|
|
Button,
|
|
Calendar,
|
|
Checkbox,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormInputGroupInput,
|
|
Input,
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupText,
|
|
RadioGroupStacked,
|
|
RadioGroupStackedItem,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Separator,
|
|
Sheet,
|
|
SheetContent,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetSection,
|
|
SheetTitle,
|
|
Switch,
|
|
Textarea,
|
|
} from 'ui'
|
|
import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
|
|
import {
|
|
DatePicker,
|
|
DatePickerButton,
|
|
DatePickerContent,
|
|
DatePickerTrigger,
|
|
} from 'ui-patterns/DatePicker'
|
|
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
|
|
.union([
|
|
z.literal(''),
|
|
z.coerce
|
|
.number()
|
|
.gte(1000, 'Max connections should be at least 1000')
|
|
.lte(10000, 'Max connections should not exceed 10000'),
|
|
])
|
|
.refine((value) => value !== '', 'Max connections is required'),
|
|
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(),
|
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
duration: z
|
|
.union([
|
|
z.literal(''),
|
|
z.coerce
|
|
.number()
|
|
.gte(1000, 'Duration should be at least 5ms')
|
|
.lte(10000, 'Duration should not exceed 30ms'),
|
|
])
|
|
.refine((value) => value !== '', 'Duration is required'),
|
|
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 variant="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 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 {...field} placeholder="Enter text" />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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 {...field} type="password" placeholder="Enter password" />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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">
|
|
<PasswordInput
|
|
copy
|
|
readOnly
|
|
className="input-mono"
|
|
value={form.getValues('apiKey') || ''}
|
|
onChange={() => {}}
|
|
onCopy={() => console.log('Copied to clipboard')}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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 {...field} type="number" min={1} max={1000} />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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>ms</InputGroupText>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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="w-full" />
|
|
|
|
{/* 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
|
|
tabIndex={0}
|
|
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 focus-ring"
|
|
style={{
|
|
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
|
|
}}
|
|
>
|
|
{!logoUrl && <Upload size={14} />}
|
|
</button>
|
|
<div className="flex gap-2 items-center">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="tiny"
|
|
icon={<Upload size={14} />}
|
|
onClick={() => uploadButtonRef.current?.click()}
|
|
>
|
|
Upload
|
|
</Button>
|
|
{logoUrl && (
|
|
<Button
|
|
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="w-full" />
|
|
|
|
{/* 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
|
|
tabIndex={0}
|
|
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-sm border"
|
|
>
|
|
<span className="text-sm text-foreground-light truncate flex-1">
|
|
{file.name}
|
|
</span>
|
|
<Button
|
|
size="tiny"
|
|
icon={<Trash size={12} />}
|
|
onClick={() => {
|
|
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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
|
|
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
|
|
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
|
|
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="w-full" />
|
|
|
|
{/* Select */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="region"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Select (Dropdown)"
|
|
description="Single selection from a list of options"
|
|
>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<FormControl className="col-span-6">
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select an option" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="us-east-1">US East (N. Virginia)</SelectItem>
|
|
<SelectItem value="us-west-2">US West (Oregon)</SelectItem>
|
|
<SelectItem value="eu-west-1">EU West (Ireland)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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"
|
|
>
|
|
<FormControl>
|
|
<MultiSelectorTrigger
|
|
mode="inline-combobox"
|
|
label="Select options..."
|
|
badgeLimit="wrap"
|
|
showIcon={false}
|
|
deletableBadge
|
|
className="w-full"
|
|
/>
|
|
</FormControl>
|
|
<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="w-full" />
|
|
|
|
{/* 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="w-full" />
|
|
|
|
{/* Date Picker */}
|
|
<SheetSection>
|
|
<FormField
|
|
control={form.control}
|
|
name="expiryDate"
|
|
render={({ field, fieldState }) => (
|
|
<FormItemLayout
|
|
layout="horizontal"
|
|
label="Date Picker"
|
|
description="Date selection with calendar popover"
|
|
>
|
|
<DatePicker>
|
|
<FormControl className="col-span-6">
|
|
<DatePickerTrigger asChild>
|
|
<DatePickerButton block isInvalid={fieldState.invalid}>
|
|
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
|
</DatePickerButton>
|
|
</DatePickerTrigger>
|
|
</FormControl>
|
|
<DatePickerContent>
|
|
<Calendar
|
|
mode="single"
|
|
selected={field.value}
|
|
onSelect={field.onChange}
|
|
initialFocus
|
|
/>
|
|
</DatePickerContent>
|
|
</DatePicker>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</SheetSection>
|
|
|
|
<Separator className="w-full" />
|
|
|
|
{/* 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="w-full" />
|
|
|
|
{/* 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="w-full" />
|
|
|
|
{/* 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
|
|
icon={<ExternalLink size={14} />}
|
|
onClick={() => console.log('Action performed')}
|
|
>
|
|
View documentation
|
|
</Button>
|
|
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
|
|
</div>
|
|
</FormItemLayout>
|
|
</SheetSection>
|
|
</form>
|
|
</Form>
|
|
<SheetFooter>
|
|
<Button
|
|
onClick={() => {
|
|
form.reset()
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" form={formId} type="submit">
|
|
Create
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</>
|
|
)
|
|
}
|