docs: added documentation for admin components (#9491)
- Documented common admin components with design that matches the admin. - Updated existing admin customization snippets to match the admin's design. Closes DOCS-964
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
---
|
||||
sidebar_label: "Action Menu"
|
||||
---
|
||||
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Action Menu - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon.
|
||||
|
||||

|
||||
|
||||
To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/components/action-menu.tsx"
|
||||
import {
|
||||
DropdownMenu,
|
||||
IconButton,
|
||||
clx
|
||||
} from "@medusajs/ui"
|
||||
import { EllipsisHorizontal } from "@medusajs/icons"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
export type Action = {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
disabled?: boolean
|
||||
} & (
|
||||
| {
|
||||
to: string
|
||||
onClick?: never
|
||||
}
|
||||
| {
|
||||
onClick: () => void
|
||||
to?: never
|
||||
}
|
||||
)
|
||||
|
||||
export type ActionGroup = {
|
||||
actions: Action[]
|
||||
}
|
||||
|
||||
export type ActionMenuProps = {
|
||||
groups: ActionGroup[]
|
||||
}
|
||||
|
||||
export const ActionMenu = ({ groups }: ActionMenuProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{groups.map((group, index) => {
|
||||
if (!group.actions.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isLast = index === groups.length - 1
|
||||
|
||||
return (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{group.actions.map((action, index) => {
|
||||
if (action.onClick) {
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
disabled={action.disabled}
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
action.onClick()
|
||||
}}
|
||||
className={clx(
|
||||
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
|
||||
{
|
||||
"[&_svg]:text-ui-fg-disabled": action.disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{action.icon}
|
||||
<span>{action.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<DropdownMenu.Item
|
||||
className={clx(
|
||||
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
|
||||
{
|
||||
"[&_svg]:text-ui-fg-disabled": action.disabled,
|
||||
}
|
||||
)}
|
||||
asChild
|
||||
disabled={action.disabled}
|
||||
>
|
||||
<Link to={action.to} onClick={(e) => e.stopPropagation()}>
|
||||
{action.icon}
|
||||
<span>{action.label}</span>
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{!isLast && <DropdownMenu.Separator />}
|
||||
</DropdownMenu.Group>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](!ui!/icons/overview) in a button.
|
||||
|
||||
When the button is clicked, a dropdown menu is shown with the actions passed in the props.
|
||||
|
||||
The component accepts the following props:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "groups",
|
||||
type: "`object[]`",
|
||||
optional: false,
|
||||
description: "Groups of actions to be shown in the dropdown. Each group is separated by a divider.",
|
||||
children: [
|
||||
{
|
||||
name: "actions",
|
||||
type: "`object[]`",
|
||||
optional: false,
|
||||
description: "Actions in the group.",
|
||||
children: [
|
||||
{
|
||||
name: "icon",
|
||||
type: "`React.ReactNode`",
|
||||
optional: false,
|
||||
description: `The icon of the action. You can use icons from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview).`
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The action's text."
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
type: "`boolean`",
|
||||
optional: true,
|
||||
defaultValue: false,
|
||||
description: "Whether the action is shown as disabled."
|
||||
},
|
||||
{
|
||||
name: "`to`",
|
||||
type: "`string`",
|
||||
optional: true,
|
||||
description: "The link to take the user to when they click the action. This is required if `onClick` isn't provided."
|
||||
},
|
||||
{
|
||||
name: "`onClick`",
|
||||
type: "`() => void`",
|
||||
optional: true,
|
||||
description: "The function to execute when the action is clicked. This is required if `to` isn't provided."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `ActionMenu` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Pencil } from "@medusajs/icons"
|
||||
import { Container } from "../components/container"
|
||||
import { ActionMenu } from "../components/action-menu"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<ActionMenu groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Pencil />,
|
||||
label: "Edit",
|
||||
onClick: () => {
|
||||
alert("You clicked the edit action!")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This widget also uses a [Container](../container/page.mdx) custom component.
|
||||
|
||||
### Use in Header
|
||||
|
||||
You can also use the action menu in the [Header](../header/page.mdx) component as part of its actions.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Pencil } from "@medusajs/icons"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header
|
||||
title="Product Widget"
|
||||
subtitle="This is my custom product widget"
|
||||
actions={[
|
||||
{
|
||||
type: "action-menu",
|
||||
props: {
|
||||
groups: [
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Pencil />,
|
||||
label: "Edit",
|
||||
onClick: () => {
|
||||
alert("You clicked the edit action!")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
sidebar_label: "Container"
|
||||
---
|
||||
|
||||
export const metadata = {
|
||||
title: `Container - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin wraps each section of a page in a container.
|
||||
|
||||

|
||||
|
||||
To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Container as UiContainer,
|
||||
clx
|
||||
} from "@medusajs/ui"
|
||||
|
||||
type ContainerProps = React.ComponentProps<typeof UiContainer>
|
||||
|
||||
export const Container = (props: ContainerProps) => {
|
||||
return (
|
||||
<UiContainer {...props} className={clx(
|
||||
"divide-y p-0",
|
||||
props.className
|
||||
)} />
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `Container` component re-uses the component from the [Medusa UI package](!ui!/components/container) and applies to it classes to match the Medusa Admin's design conventions.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use that `Container` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header title="Product Widget" />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This widget also uses a [Header](../header/page.mdx) custom component.
|
||||
@@ -0,0 +1,584 @@
|
||||
---
|
||||
sidebar_label: "Forms"
|
||||
---
|
||||
|
||||
export const metadata = {
|
||||
title: `Forms - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin has two types of forms:
|
||||
|
||||
1. Create forms, created using the [FocusModal UI component](!ui!/components/focus-modal).
|
||||
2. Edit or update forms, created using the [Drawer UI component](!ui!/ui/components/drawer).
|
||||
|
||||
This guide explains how to create these two form types following the Medusa Admin's conventions.
|
||||
|
||||
## Form Tooling
|
||||
|
||||
The Medusa Admin uses the following tools to build the forms:
|
||||
|
||||
1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states.
|
||||
2. [Zod](https://zod.dev/) to validate the form's fields.
|
||||
|
||||
Both of these libraries are available in your project, so you don't have to install them to use them.
|
||||
|
||||
---
|
||||
|
||||
## Create Form
|
||||
|
||||
In this section, you'll build a form component to create an item of a resource.
|
||||
|
||||
<Details summaryContent="Full Component">
|
||||
|
||||
```tsx title="src/admin/components/create-form.tsx"
|
||||
import {
|
||||
FocusModal,
|
||||
Heading,
|
||||
Label,
|
||||
Input,
|
||||
Button
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
useForm,
|
||||
FormProvider,
|
||||
Controller
|
||||
} from "react-hook-form"
|
||||
import * as zod from "zod"
|
||||
|
||||
const schema = zod.object({
|
||||
name: zod.string()
|
||||
})
|
||||
|
||||
export const CreateForm = () => {
|
||||
const form = useForm<zod.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(({ name }) => {
|
||||
// TODO submit to backend
|
||||
console.log(name)
|
||||
})
|
||||
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Create</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 type="submit" size="small">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body>
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading className="capitalize">
|
||||
Create Item
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Label size="small" weight="plus">
|
||||
Name
|
||||
</Label>
|
||||
</div>
|
||||
<Input {...field} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin.
|
||||
|
||||
Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in.
|
||||
|
||||
### Create Validation Schema
|
||||
|
||||
In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields:
|
||||
|
||||
```tsx title="src/admin/components/create-form.tsx"
|
||||
import * as zod from "zod"
|
||||
|
||||
const schema = zod.object({
|
||||
name: zod.string()
|
||||
})
|
||||
```
|
||||
|
||||
The form in this guide is simple, it only has a required `name` field, which is a string.
|
||||
|
||||
### Initialize Form
|
||||
|
||||
Next, you'll initialize the form using `react-hook-form`.
|
||||
|
||||
Add to `src/admin/components/create-form.tsx` the following:
|
||||
|
||||
```tsx title="src/admin/components/create-form.tsx"
|
||||
// other imports...
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
// validation schema...
|
||||
|
||||
export const CreateForm = () => {
|
||||
const form = useForm<zod.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(({ name }) => {
|
||||
// TODO submit to backend
|
||||
console.log(name)
|
||||
})
|
||||
|
||||
// TODO render form
|
||||
}
|
||||
```
|
||||
|
||||
You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.
|
||||
|
||||
You also define a `handleSubmit` function to perform an action when the form is submitted.
|
||||
|
||||
You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/advanced-development/admin/tips#send-requests-to-api-routes) for more details on how to do that.
|
||||
|
||||
### Render Components
|
||||
|
||||
You'll now add a `return` statement that renders the focus modal where the form is shown.
|
||||
|
||||
Replace `// TODO render form` with the following:
|
||||
|
||||
```tsx title="src/admin/components/create-form.tsx"
|
||||
// other imports...
|
||||
import {
|
||||
FocusModal,
|
||||
Heading,
|
||||
Label,
|
||||
Input,
|
||||
Button
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
FormProvider,
|
||||
Controller
|
||||
} from "react-hook-form"
|
||||
|
||||
export const CreateForm = () => {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Create</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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 type="submit" size="small">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body>
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading className="capitalize">
|
||||
Create Item
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Label size="small" weight="plus">
|
||||
Name
|
||||
</Label>
|
||||
</div>
|
||||
<Input {...field} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
You render a focus modal, with a trigger button to open it.
|
||||
|
||||
In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.
|
||||
|
||||
In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.
|
||||
|
||||
In the `FocusModal.Header` component, you add buttons to save or cancel the form submission.
|
||||
|
||||
Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.
|
||||
|
||||
### Use Create Form Component
|
||||
|
||||
You can use the `CreateForm` component in your widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { CreateForm } from "../components/create-form"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header
|
||||
title="Items"
|
||||
actions={[
|
||||
{
|
||||
type: "custom",
|
||||
children: <CreateForm />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This component uses the [Container](../container/page.mdx) and [Header](../header/page.mdx) custom components.
|
||||
|
||||
It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form.
|
||||
|
||||
---
|
||||
|
||||
## Edit Form
|
||||
|
||||
In this section, you'll build a form component to edit an item of a resource.
|
||||
|
||||
<Details summaryContent="Full Component">
|
||||
|
||||
```tsx title="src/admin/components/edit-form.tsx"
|
||||
import {
|
||||
Drawer,
|
||||
Heading,
|
||||
Label,
|
||||
Input,
|
||||
Button
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
useForm,
|
||||
FormProvider,
|
||||
Controller
|
||||
} from "react-hook-form"
|
||||
import * as zod from "zod"
|
||||
|
||||
const schema = zod.object({
|
||||
name: zod.string()
|
||||
})
|
||||
|
||||
export const EditForm = () => {
|
||||
const form = useForm<zod.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(({ name }) => {
|
||||
// TODO submit to backend
|
||||
console.log(name)
|
||||
})
|
||||
|
||||
return (
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
<Button>Edit Item</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Drawer.Header>
|
||||
<Heading className="capitalize">
|
||||
Edit Item
|
||||
</Heading>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Label size="small" weight="plus">
|
||||
Name
|
||||
</Label>
|
||||
</div>
|
||||
<Input {...field} />
|
||||
</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" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin.
|
||||
|
||||
Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in.
|
||||
|
||||
### Create Validation Schema
|
||||
|
||||
In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields:
|
||||
|
||||
```tsx title="src/admin/components/edit-form.tsx"
|
||||
import * as zod from "zod"
|
||||
|
||||
const schema = zod.object({
|
||||
name: zod.string()
|
||||
})
|
||||
```
|
||||
|
||||
The form in this guide is simple, it only has a required `name` field, which is a string.
|
||||
|
||||
### Initialize Form
|
||||
|
||||
Next, you'll initialize the form using `react-hook-form`.
|
||||
|
||||
Add to `src/admin/components/edit-form.tsx` the following:
|
||||
|
||||
```tsx title="src/admin/components/edit-form.tsx"
|
||||
// other imports...
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
// validation schema...
|
||||
|
||||
export const EditForm = () => {
|
||||
const form = useForm<zod.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(({ name }) => {
|
||||
// TODO submit to backend
|
||||
console.log(name)
|
||||
})
|
||||
|
||||
// TODO render form
|
||||
}
|
||||
```
|
||||
|
||||
You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.
|
||||
|
||||
You also define a `handleSubmit` function to perform an action when the form is submitted.
|
||||
|
||||
You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/advanced-development/admin/tips#send-requests-to-api-routes) for more details on how to do that.
|
||||
|
||||
### Render Components
|
||||
|
||||
You'll now add a `return` statement that renders the drawer where the form is shown.
|
||||
|
||||
Replace `// TODO render form` with the following:
|
||||
|
||||
```tsx title="src/admin/components/edit-form.tsx"
|
||||
// other imports...
|
||||
import {
|
||||
Drawer,
|
||||
Heading,
|
||||
Label,
|
||||
Input,
|
||||
Button
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
FormProvider,
|
||||
Controller
|
||||
} from "react-hook-form"
|
||||
|
||||
export const EditForm = () => {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
<Button>Edit Item</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Drawer.Header>
|
||||
<Heading className="capitalize">
|
||||
Edit Item
|
||||
</Heading>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Label size="small" weight="plus">
|
||||
Name
|
||||
</Label>
|
||||
</div>
|
||||
<Input {...field} />
|
||||
</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" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
You render a drawer, with a trigger button to open it.
|
||||
|
||||
In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.
|
||||
|
||||
In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.
|
||||
|
||||
You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.
|
||||
|
||||
Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission.
|
||||
|
||||
### Use Edit Form Component
|
||||
|
||||
You can use the `EditForm` component in your widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
import { EditForm } from "../components/edit-form"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header
|
||||
title="Items"
|
||||
actions={[
|
||||
{
|
||||
type: "custom",
|
||||
children: <EditForm />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This component uses the [Container](../container/page.mdx) and [Header](../header/page.mdx) custom components.
|
||||
|
||||
It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
sidebar_label: "Header"
|
||||
---
|
||||
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Header - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action.
|
||||
|
||||

|
||||
|
||||
To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/components/header.tsx"
|
||||
import { Heading, Button, Text } from "@medusajs/ui"
|
||||
import React from "react"
|
||||
import { Link, LinkProps } from "react-router-dom"
|
||||
import { ActionMenu, ActionMenuProps } from "./action-menu"
|
||||
|
||||
export type HeadingProps = {
|
||||
title: string
|
||||
subtitle?: string
|
||||
actions?: (
|
||||
{
|
||||
type: "button",
|
||||
props: React.ComponentProps<typeof Button>
|
||||
link?: LinkProps
|
||||
} |
|
||||
{
|
||||
type: "action-menu"
|
||||
props: ActionMenuProps
|
||||
} |
|
||||
{
|
||||
type: "custom"
|
||||
children: React.ReactNode
|
||||
}
|
||||
)[]
|
||||
}
|
||||
|
||||
export const Header = ({
|
||||
title,
|
||||
subtitle,
|
||||
actions = []
|
||||
}: HeadingProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading level="h2">{title}</Heading>
|
||||
{subtitle && (
|
||||
<Text className="text-ui-fg-subtle" size="small">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{actions.length > 0 && (
|
||||
<div className="flex items-center justify-center gap-x-2">
|
||||
{actions.map((action, index) => (
|
||||
<>
|
||||
{action.type === "button" && (
|
||||
<Button
|
||||
{...action.props}
|
||||
size={action.props.size || "small"}
|
||||
key={index}
|
||||
>
|
||||
<>
|
||||
{action.props.children}
|
||||
{action.link && <Link {...action.link} />}
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
{action.type === "action-menu" && (
|
||||
<ActionMenu {...action.props} />
|
||||
)}
|
||||
{action.type === "custom" && action.children}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `Header` component shows a title, and optionally a subtitle and action buttons.
|
||||
|
||||
<Note>
|
||||
|
||||
The component also uses the [Action Menu](../action-menu/page.mdx) custom component.
|
||||
|
||||
</Note>
|
||||
|
||||
It accepts the following props:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "title",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The section's title."
|
||||
},
|
||||
{
|
||||
name: "subtitle",
|
||||
type: "`string`",
|
||||
optional: true,
|
||||
description: "The section's subtitle."
|
||||
},
|
||||
{
|
||||
name: "actions",
|
||||
type: "`object[]`",
|
||||
optional: true,
|
||||
description: "An array of actions to show.",
|
||||
children: [
|
||||
{
|
||||
name: "type",
|
||||
type: "`button` \\| `action-menu` \\| `custom`",
|
||||
optional: false,
|
||||
description: "The type of action to add.\n\n- If its value is `button`, it'll show a button that can have a link or an on-click action.\n\n- If its value is `action-menu`, it'll show a three dot icon with a dropdown of actions.\n\n- If its value is `custom`, you can pass any React nodes to render.",
|
||||
},
|
||||
{
|
||||
name: "props",
|
||||
type: "object",
|
||||
optional: false,
|
||||
description: `This property is only accepted if \`type\` is \`button\` or \`action-menu\`. If \`type\` is \`button\`, it accepts the [props to pass to the UI Button component](https://docs.medusajs.com/components/button). If \`type\` is \`action-menu\`, it accepts the props to pass to the action menu, explaind in [this guide](../action-menu/page.mdx).`,
|
||||
},
|
||||
{
|
||||
name: "link",
|
||||
type: `[LinkProps](https://reactrouter.com/en/main/components/link)`,
|
||||
optional: true,
|
||||
description: "This property is only accepted if `type` is `button`. If provided, a link is rendered inside the button. Its value is the props to pass the `Link` component of `react-router-dom`."
|
||||
},
|
||||
{
|
||||
name: "children",
|
||||
type: "React.ReactNode",
|
||||
optional: true,
|
||||
description: "This property is only accepted if `type` is `custom`. Its content is rendered as part of the actions."
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `Header` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header
|
||||
title="Product Widget"
|
||||
subtitle="This is my custom product widget"
|
||||
actions={[
|
||||
{
|
||||
type: "button",
|
||||
props: {
|
||||
children: "Click me",
|
||||
variant: "secondary",
|
||||
onClick: () => {
|
||||
alert("You clicked the button.")
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This widget also uses a [Container](../container/page.mdx) custom component.
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
sidebar_label: "JSON View"
|
||||
---
|
||||
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `JSON View - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.
|
||||
|
||||

|
||||
|
||||
To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/components/json-view-section.tsx"
|
||||
import {
|
||||
ArrowUpRightOnBox,
|
||||
Check,
|
||||
SquareTwoStack,
|
||||
TriangleDownMini,
|
||||
XMarkMini,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
Badge,
|
||||
Container,
|
||||
Drawer,
|
||||
Heading,
|
||||
IconButton,
|
||||
Kbd,
|
||||
} from "@medusajs/ui"
|
||||
import Primitive from "@uiw/react-json-view"
|
||||
import { CSSProperties, MouseEvent, Suspense, useState } from "react"
|
||||
|
||||
type JsonViewSectionProps = {
|
||||
data: object
|
||||
title?: string
|
||||
}
|
||||
|
||||
export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
|
||||
const numberOfKeys = Object.keys(data).length
|
||||
|
||||
return (
|
||||
<Container className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Heading level="h2">JSON</Heading>
|
||||
<Badge size="2xsmall" rounded="full">
|
||||
{numberOfKeys} keys
|
||||
</Badge>
|
||||
</div>
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="transparent"
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||
>
|
||||
<ArrowUpRightOnBox />
|
||||
</IconButton>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]">
|
||||
<div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Drawer.Title asChild>
|
||||
<Heading className="text-ui-contrast-fg-primary">
|
||||
<span className="text-ui-fg-subtle">
|
||||
{numberOfKeys}
|
||||
</span>
|
||||
</Heading>
|
||||
</Drawer.Title>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary">
|
||||
esc
|
||||
</Kbd>
|
||||
<Drawer.Close asChild>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="transparent"
|
||||
className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active"
|
||||
>
|
||||
<XMarkMini />
|
||||
</IconButton>
|
||||
</Drawer.Close>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]">
|
||||
<div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3">
|
||||
<Suspense
|
||||
fallback={<div className="flex size-full flex-col"></div>}
|
||||
>
|
||||
<Primitive
|
||||
value={data}
|
||||
displayDataTypes={false}
|
||||
style={
|
||||
{
|
||||
"--w-rjv-font-family": "Roboto Mono, monospace",
|
||||
"--w-rjv-line-color": "var(--contrast-border-base)",
|
||||
"--w-rjv-curlybraces-color":
|
||||
"var(--contrast-fg-secondary)",
|
||||
"--w-rjv-brackets-color": "var(--contrast-fg-secondary)",
|
||||
"--w-rjv-key-string": "var(--contrast-fg-primary)",
|
||||
"--w-rjv-info-color": "var(--contrast-fg-secondary)",
|
||||
"--w-rjv-type-string-color": "var(--tag-green-icon)",
|
||||
"--w-rjv-quotes-string-color": "var(--tag-green-icon)",
|
||||
"--w-rjv-type-boolean-color": "var(--tag-orange-icon)",
|
||||
"--w-rjv-type-int-color": "var(--tag-orange-icon)",
|
||||
"--w-rjv-type-float-color": "var(--tag-orange-icon)",
|
||||
"--w-rjv-type-bigint-color": "var(--tag-orange-icon)",
|
||||
"--w-rjv-key-number": "var(--contrast-fg-secondary)",
|
||||
"--w-rjv-arrow-color": "var(--contrast-fg-secondary)",
|
||||
"--w-rjv-copied-color": "var(--contrast-fg-secondary)",
|
||||
"--w-rjv-copied-success-color":
|
||||
"var(--contrast-fg-primary)",
|
||||
"--w-rjv-colon-color": "var(--contrast-fg-primary)",
|
||||
"--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)",
|
||||
} as CSSProperties
|
||||
}
|
||||
collapsed={1}
|
||||
>
|
||||
<Primitive.Quote render={() => <span />} />
|
||||
<Primitive.Null
|
||||
render={() => (
|
||||
<span className="text-ui-tag-red-icon">null</span>
|
||||
)}
|
||||
/>
|
||||
<Primitive.Undefined
|
||||
render={() => (
|
||||
<span className="text-ui-tag-blue-icon">undefined</span>
|
||||
)}
|
||||
/>
|
||||
<Primitive.CountInfo
|
||||
render={(_props, { value }) => {
|
||||
return (
|
||||
<span className="text-ui-contrast-fg-secondary ml-2">
|
||||
{Object.keys(value as object).length} items
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Primitive.Arrow>
|
||||
<TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" />
|
||||
</Primitive.Arrow>
|
||||
<Primitive.Colon>
|
||||
<span className="mr-1">:</span>
|
||||
</Primitive.Colon>
|
||||
<Primitive.Copied
|
||||
render={({ style }, { value }) => {
|
||||
return <Copied style={style} value={value} />
|
||||
}}
|
||||
/>
|
||||
</Primitive>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Drawer.Body>
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
type CopiedProps = {
|
||||
style?: CSSProperties
|
||||
value: object | undefined
|
||||
}
|
||||
|
||||
const Copied = ({ style, value }: CopiedProps) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handler = (e: MouseEvent<HTMLSpanElement>) => {
|
||||
e.stopPropagation()
|
||||
setCopied(true)
|
||||
|
||||
if (typeof value === "string") {
|
||||
navigator.clipboard.writeText(value)
|
||||
} else {
|
||||
const json = JSON.stringify(value, null, 2)
|
||||
navigator.clipboard.writeText(json)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const styl = { whiteSpace: "nowrap", width: "20px" }
|
||||
|
||||
if (copied) {
|
||||
return (
|
||||
<span style={{ ...style, ...styl }}>
|
||||
<Check className="text-ui-contrast-fg-primary" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ ...style, ...styl }} onClick={handler}>
|
||||
<SquareTwoStack className="text-ui-contrast-fg-secondary" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window.
|
||||
|
||||
The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `JsonViewSection` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { JsonViewSection } from "../components/json-view-section"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return <JsonViewSection data={{
|
||||
name: "John"
|
||||
}} />
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
sidebar_label: "Section Row"
|
||||
---
|
||||
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Section Row - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin often shows information in rows of label-values, such as when showing a product's details.
|
||||
|
||||

|
||||
|
||||
To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/components/section-row.tsx"
|
||||
import { Text, clx } from "@medusajs/ui"
|
||||
|
||||
export type SectionRowProps = {
|
||||
title: string
|
||||
value?: React.ReactNode | string | null
|
||||
actions?: React.ReactNode
|
||||
}
|
||||
|
||||
export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
|
||||
const isValueString = typeof value === "string" || !value
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
`text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
|
||||
{
|
||||
"grid-cols-[1fr_1fr_28px]": !!actions,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{isValueString ? (
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="whitespace-pre-line text-pretty"
|
||||
>
|
||||
{value ?? "-"}
|
||||
</Text>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">{value}</div>
|
||||
)}
|
||||
|
||||
{actions && <div>{actions}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `SectionRow` component shows a title and a value in the same row.
|
||||
|
||||
It accepts the following props:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "title",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The title to show on the left side."
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "`React.ReactNode` \\| `string` \\| `null`",
|
||||
optional: true,
|
||||
description: "The value to show on the right side."
|
||||
},
|
||||
{
|
||||
name: "actions",
|
||||
type: "`React.ReactNode`",
|
||||
optional: true,
|
||||
description: "The actions to show at the end of the row."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `SectionRow` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "../components/container"
|
||||
import { Header } from "../components/header"
|
||||
import { SectionRow } from "../components/section-row"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Header title="Product Widget" />
|
||||
<SectionRow title="Name" value="John" />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This widget also uses the [Container](../container/page.mdx) and [Header](../header/page.mdx) custom component.
|
||||
@@ -0,0 +1,245 @@
|
||||
---
|
||||
sidebar_label: "Table"
|
||||
---
|
||||
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Table - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The listing pages in the Admin show a table with pagination.
|
||||
|
||||

|
||||
|
||||
To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/components/table.tsx"
|
||||
import { useMemo } from "react"
|
||||
import { Table as UiTable } from "@medusajs/ui"
|
||||
|
||||
export type TableProps = {
|
||||
columns: {
|
||||
key: string
|
||||
label?: string
|
||||
render?: (value: unknown) => React.ReactNode
|
||||
}[]
|
||||
data: Record<string, unknown>[]
|
||||
pageSize: number
|
||||
count: number
|
||||
currentPage: number
|
||||
setCurrentPage: (value: number) => void
|
||||
}
|
||||
|
||||
export const Table = ({
|
||||
columns,
|
||||
data,
|
||||
pageSize,
|
||||
count,
|
||||
currentPage,
|
||||
setCurrentPage
|
||||
}: TableProps) => {
|
||||
const pageCount = useMemo(() => {
|
||||
return Math.ceil(data.length / pageSize)
|
||||
}, [data, pageSize])
|
||||
|
||||
const canNextPage = useMemo(() => {
|
||||
return currentPage < pageCount - 1
|
||||
}, [currentPage, pageCount])
|
||||
const canPreviousPage = useMemo(() => {
|
||||
return currentPage - 1 >= 0
|
||||
}, [currentPage])
|
||||
|
||||
const nextPage = () => {
|
||||
if (canNextPage) {
|
||||
setCurrentPage(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const previousPage = () => {
|
||||
if (canPreviousPage) {
|
||||
setCurrentPage(currentPage - 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden !border-t-0">
|
||||
<UiTable>
|
||||
<UiTable.Header>
|
||||
<UiTable.Row>
|
||||
{columns.map((column, index) => (
|
||||
<UiTable.HeaderCell key={index}>
|
||||
{column.label || column.key}
|
||||
</UiTable.HeaderCell>
|
||||
))}
|
||||
</UiTable.Row>
|
||||
</UiTable.Header>
|
||||
<UiTable.Body>
|
||||
{data.map((item, index) => {
|
||||
const rowIndex = "id" in item ? item.id as string : index
|
||||
return (
|
||||
<UiTable.Row key={rowIndex}>
|
||||
{columns.map((column, index) => (
|
||||
<UiTable.Cell key={`${rowIndex}-${index}`}>
|
||||
<>
|
||||
{column.render && column.render(item[column.key])}
|
||||
{!column.render && (
|
||||
<>{item[column.key] as string}</>
|
||||
)}
|
||||
</>
|
||||
</UiTable.Cell>
|
||||
))}
|
||||
</UiTable.Row>
|
||||
)
|
||||
})}
|
||||
</UiTable.Body>
|
||||
</UiTable>
|
||||
<UiTable.Pagination
|
||||
count={count}
|
||||
pageSize={pageSize}
|
||||
pageIndex={currentPage}
|
||||
pageCount={pageCount}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `Table` component uses the component from the [UI package](!ui!/components/table), with additional styling and rendering of data.
|
||||
|
||||
It accepts the following props:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "columns",
|
||||
type: "`object[]`",
|
||||
optional: false,
|
||||
description: "The table's columns.",
|
||||
children: [
|
||||
{
|
||||
name: "key",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The column's key in the passed `data`"
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
type: "`string`",
|
||||
optional: true,
|
||||
description: "The column's label shown in the table. If not provided, the `key` is used."
|
||||
},
|
||||
{
|
||||
name: "render",
|
||||
type: "`(value: unknown) => React.ReactNode`",
|
||||
optional: true,
|
||||
description: "By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "data",
|
||||
type: "`Record<string, unknown>[]`",
|
||||
optional: false,
|
||||
description: "The data to show in the table for the current page. The keys of each object should be in the `columns` array."
|
||||
},
|
||||
{
|
||||
name: "pageSize",
|
||||
type: "`number`",
|
||||
optional: false,
|
||||
description: "The number of items to show per page."
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
type: "`number`",
|
||||
optional: false,
|
||||
description: "The total number of items."
|
||||
},
|
||||
{
|
||||
name: "currentPage",
|
||||
type: "`number`",
|
||||
optional: false,
|
||||
description: "A zero-based index indicating the current page's number."
|
||||
},
|
||||
{
|
||||
name: "setCurrentPage",
|
||||
type: "`(value: number) => void`",
|
||||
optional: false,
|
||||
description: "A function used to change the current page."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `Table` component in any widget or UI route.
|
||||
|
||||
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { StatusBadge } from "@medusajs/ui"
|
||||
import { Table } from "../components/table"
|
||||
import { useState } from "react"
|
||||
import { Container } from "../components/container"
|
||||
|
||||
const ProductWidget = () => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: "name",
|
||||
label: "Name"
|
||||
},
|
||||
{
|
||||
key: "is_enabled",
|
||||
label: "Status",
|
||||
render: (value: unknown) => {
|
||||
const isEnabled = value as boolean
|
||||
|
||||
return (
|
||||
<StatusBadge color={isEnabled ? "green" : "grey"}>
|
||||
{isEnabled ? "Enabled" : "Disabled"}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
}
|
||||
]}
|
||||
data={[
|
||||
{
|
||||
name: "John",
|
||||
is_enabled: true
|
||||
},
|
||||
{
|
||||
name: "Jane",
|
||||
is_enabled: false
|
||||
}
|
||||
]}
|
||||
pageSize={2}
|
||||
count={2}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This widget also uses the [Container](../container.mdx) custom component.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
sidebar_label: "Single Column"
|
||||
---
|
||||
|
||||
export const metadata = {
|
||||
title: `Single Column Layout - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin has pages with a single column of content.
|
||||
|
||||
<Note>
|
||||
|
||||
This doesn't include the sidebar, only the main content.
|
||||
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/layouts/single-column.tsx"
|
||||
export type SingleColumnLayoutProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `SingleColumnLayout` accepts the content in the `children` props.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `SingleColumnLayout` component in your UI routes that have a single column. For example:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
import { Container } from "../../components/container"
|
||||
import { SingleColumnLayout } from "../../layouts/single-column"
|
||||
import { Header } from "../../components/header"
|
||||
|
||||
const CustomPage = () => {
|
||||
return (
|
||||
<SingleColumnLayout>
|
||||
<Container>
|
||||
<Header title="Custom Page" />
|
||||
</Container>
|
||||
</SingleColumnLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Custom",
|
||||
icon: ChatBubbleLeftRight,
|
||||
})
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
This UI route also uses a [Container](../../components/container/page.mdx) and a [Header]() custom components.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
sidebar_label: "Two Column"
|
||||
---
|
||||
|
||||
export const metadata = {
|
||||
title: `Two Column Layout - Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The Medusa Admin has pages with two columns of content.
|
||||
|
||||
<Note>
|
||||
|
||||
This doesn't include the sidebar, only the main content.
|
||||
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/layouts/two-column.tsx"
|
||||
export type TwoColumnLayoutProps = {
|
||||
firstCol: React.ReactNode
|
||||
secondCol: React.ReactNode
|
||||
}
|
||||
|
||||
export const TwoColumnLayout = ({
|
||||
firstCol,
|
||||
secondCol
|
||||
}: TwoColumnLayoutProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{firstCol}
|
||||
</div>
|
||||
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
|
||||
{secondCol}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `TwoColumnLayout` accepts two props:
|
||||
|
||||
- `firstCol` indicating the content of the first column.
|
||||
- `secondCol` indicating the content of the second column.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Use the `TwoColumnLayout` component in your UI routes that have a single column. For example:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
import { Container } from "../../components/container"
|
||||
import { Header } from "../../components/header"
|
||||
import { TwoColumnLayout } from "../../layouts/two-column"
|
||||
|
||||
const CustomPage = () => {
|
||||
return (
|
||||
<TwoColumnLayout
|
||||
firstCol={
|
||||
<Container>
|
||||
<Header title="First Column" />
|
||||
</Container>
|
||||
}
|
||||
secondCol={
|
||||
<Container>
|
||||
<Header title="Second Column" />
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Custom",
|
||||
icon: ChatBubbleLeftRight,
|
||||
})
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
This UI route also uses [Container](../../components/container/page.mdx) and [Header]() custom components.
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ChildDocs } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Admin Components`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this section, you'll find examples of implementing common Medusa Admin components and layouts.
|
||||
|
||||
These components are useful to follow the same design conventions as the Medusa Admin, and are build on top of the [Medusa UI package](!ui!).
|
||||
|
||||
Refer to the [Medusa UI documentation](!ui!) for a full list of components.
|
||||
|
||||
## Layouts
|
||||
|
||||
Use these components to set the layout of your UI route.
|
||||
|
||||
<ChildDocs showItems={["Layouts"]} onlyTopLevel={false} />
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
Use these components in your widgets and UI routes.
|
||||
|
||||
<ChildDocs showItems={["Components"]} onlyTopLevel={false} />
|
||||
Reference in New Issue
Block a user