Files
suplement/.agents/skills/building-admin-dashboard-customizations/references/forms.md
2026-03-07 11:07:45 -03:00

401 lines
11 KiB
Markdown

# Forms and Modal Patterns
## Contents
- [FocusModal vs Drawer](#focusmodal-vs-drawer)
- [Edit Button Patterns](#edit-button-patterns)
- [Simple Edit Button (top right corner)](#simple-edit-button-top-right-corner)
- [Dropdown Menu with Actions](#dropdown-menu-with-actions)
- [Select Component for Small Datasets](#select-component-for-small-datasets)
- [FocusModal Example](#focusmodal-example)
- [Drawer Example](#drawer-example)
- [Form with Validation and Loading States](#form-with-validation-and-loading-states)
- [Key Form Patterns](#key-form-patterns)
## FocusModal vs Drawer
**FocusModal** - Use for creating new entities:
- Full-screen modal
- More space for complex forms
- Better for multi-step flows
**Drawer** - Use for editing existing entities:
- Side panel that slides in from right
- Quick edits without losing context
- Better for single-field updates
**Rule of thumb:** FocusModal for creating, Drawer for editing.
## Edit Button Patterns
Data displayed in a container should not be editable directly. Instead, use an "Edit" button. This can be:
### Simple Edit Button (top right corner)
```tsx
import { Button } from "@medusajs/ui"
import { PencilSquare } from "@medusajs/icons"
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Section Title</Heading>
<Button
size="small"
variant="secondary"
onClick={() => setOpen(true)}
>
<PencilSquare />
</Button>
</div>
```
### Dropdown Menu with Actions
```tsx
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"
export function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="small" variant="transparent">
<EllipsisHorizontal />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item className="gap-x-2">
<PencilSquare className="text-ui-fg-subtle" />
Edit
</DropdownMenu.Item>
<DropdownMenu.Item className="gap-x-2">
<Plus className="text-ui-fg-subtle" />
Add
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item className="gap-x-2">
<Trash className="text-ui-fg-subtle" />
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
)
}
```
## Select Component for Small Datasets
For selecting from 2-10 options (statuses, types, etc.), use the Select component:
```tsx
import { Select } from "@medusajs/ui"
<Select>
<Select.Trigger>
<Select.Value placeholder="Select status" />
</Select.Trigger>
<Select.Content>
{items.map((item) => (
<Select.Item key={item.value} value={item.value}>
{item.label}
</Select.Item>
))}
</Select.Content>
</Select>
```
**For larger datasets** (Products, Categories, Regions, etc.), use DataTable with FocusModal for search and pagination. See [table-selection.md](table-selection.md) for the complete pattern.
## FocusModal Example
```tsx
import { FocusModal, Button, Input, Label } from "@medusajs/ui"
import { useState } from "react"
const MyWidget = () => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({ title: "" })
const handleSubmit = () => {
// Handle form submission
console.log(formData)
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Create New
</Button>
<FocusModal open={open} onOpenChange={setOpen}>
<FocusModal.Content>
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
Cancel
</Button>
</FocusModal.Close>
<Button size="small" onClick={handleSubmit}>
Save
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex-1 overflow-auto">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title</Label>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
{/* More form fields */}
</div>
</FocusModal.Body>
</div>
</FocusModal.Content>
</FocusModal>
</>
)
}
```
## Drawer Example
```tsx
import { Drawer, Button, Input, Label } from "@medusajs/ui"
import { useState } from "react"
const MyWidget = ({ data }) => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({ title: data.title })
const handleSubmit = () => {
// Handle form submission
console.log(formData)
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Edit
</Button>
<Drawer open={open} onOpenChange={setOpen}>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Edit Settings</Drawer.Title>
</Drawer.Header>
<Drawer.Body className="flex-1 overflow-auto p-4">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title</Label>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
{/* More form fields */}
</div>
</Drawer.Body>
<Drawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<Button size="small" variant="secondary">
Cancel
</Button>
</Drawer.Close>
<Button size="small" onClick={handleSubmit}>
Save
</Button>
</div>
</Drawer.Footer>
</Drawer.Content>
</Drawer>
</>
)
}
```
## Form with Validation and Loading States
```tsx
import { FocusModal, Button, Input, Label, Text, toast } from "@medusajs/ui"
import { useState } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/client"
const CreateProductWidget = () => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({
title: "",
description: "",
})
const [errors, setErrors] = useState({})
const queryClient = useQueryClient()
const createProduct = useMutation({
mutationFn: (data) => sdk.admin.product.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product created successfully")
setOpen(false)
setFormData({ title: "", description: "" })
setErrors({})
},
onError: (error) => {
toast.error(error.message || "Failed to create product")
},
})
const handleSubmit = () => {
// Validate
const newErrors = {}
if (!formData.title) newErrors.title = "Title is required"
if (!formData.description) newErrors.description = "Description is required"
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
createProduct.mutate(formData)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Create Product
</Button>
<FocusModal open={open} onOpenChange={setOpen}>
<FocusModal.Content>
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button
size="small"
variant="secondary"
disabled={createProduct.isPending}
>
Cancel
</Button>
</FocusModal.Close>
<Button
size="small"
onClick={handleSubmit}
isLoading={createProduct.isPending}
>
Save
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex-1 overflow-auto">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title *</Label>
<Input
value={formData.title}
onChange={(e) => {
setFormData({ ...formData, title: e.target.value })
setErrors({ ...errors, title: undefined })
}}
/>
{errors.title && (
<Text size="small" className="text-ui-fg-error">
{errors.title}
</Text>
)}
</div>
<div className="flex flex-col gap-y-2">
<Label>Description *</Label>
<Input
value={formData.description}
onChange={(e) => {
setFormData({ ...formData, description: e.target.value })
setErrors({ ...errors, description: undefined })
}}
/>
{errors.description && (
<Text size="small" className="text-ui-fg-error">
{errors.description}
</Text>
)}
</div>
</div>
</FocusModal.Body>
</div>
</FocusModal.Content>
</FocusModal>
</>
)
}
```
## Key Form Patterns
### Always Disable Actions During Mutations
```tsx
<Button
disabled={mutation.isPending}
onClick={handleAction}
>
Action
</Button>
```
### Show Loading State on Submit Button
```tsx
<Button
isLoading={mutation.isPending}
onClick={handleSubmit}
>
Save
</Button>
```
### Clear Form After Success
```tsx
onSuccess: () => {
setFormData(initialState)
setErrors({})
setOpen(false)
}
```
### Validate Before Submitting
```tsx
const handleSubmit = () => {
const errors = validateForm(formData)
if (Object.keys(errors).length > 0) {
setErrors(errors)
return
}
mutation.mutate(formData)
}
```
### Clear Field Errors on Input Change
```tsx
<Input
value={formData.field}
onChange={(e) => {
setFormData({ ...formData, field: e.target.value })
setErrors({ ...errors, field: undefined }) // Clear error
}}
/>
```