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:
Shahed Nasser
2024-10-14 07:18:38 +00:00
committed by GitHub
parent e3dc9eaf0c
commit 74b286b701
22 changed files with 2385 additions and 241 deletions
@@ -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.
![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png)
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.
![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png)
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.
![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png)
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.
![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png)
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.
![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png)
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.
![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png)
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>
![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png)
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>
![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png)
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} />