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 10:18:38 +03:00
committed by GitHub
parent e3dc9eaf0c
commit 74b286b701
22 changed files with 2385 additions and 241 deletions
@@ -18,3 +18,9 @@ You can customize the admin dashboard by:
Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboards design.
Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui) to learn how to install it and use its components.
---
## Admin Components List
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
@@ -41,7 +41,7 @@ const ProductWidget = () => {
}, [loading])
return (
<Container>
<Container className="divide-y p-0">
{loading && <span>Loading...</span>}
{!loading && <span>You have {productsCount} Product(s).</span>}
</Container>
@@ -78,7 +78,7 @@ import { Link } from "react-router-dom"
// The widget
const ProductWidget = () => {
return (
<Container>
<Container className="divide-y p-0">
<Link to={"/orders"}>View Orders</Link>
</Container>
)
@@ -21,10 +21,16 @@ A UI route is created in a file named `page.tsx` under the `src/admin/routes` di
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/page.tsx"
import { Container } from "@medusajs/ui"
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return <Container>This is my custom route</Container>
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export default CustomPage
@@ -59,17 +65,23 @@ A UI route file can export a configuration object that indicates a new item must
For example:
export const highlights = [
["14", "label", "The label of the UI route's sidebar item."],
["15", "icon", "The icon of the UI route's sidebar item."]
["16", "label", "The label of the UI route's sidebar item."],
["17", "icon", "The icon of the UI route's sidebar item."]
]
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "@medusajs/ui"
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return <Container>This is my custom route</Container>
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
@@ -101,8 +113,10 @@ import { Container, Heading } from "@medusajs/ui"
const CustomSettingPage = () => {
return (
<Container>
<Heading level="h1">Custom Setting Page</Heading>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Custom Setting Page</Heading>
</div>
</Container>
)
}
@@ -124,14 +138,20 @@ A UI route can accept path parameters if the name of any of the directories in i
For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["7", "{id}", "Show the path parameter."]]}
```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["10", "{id}", "Show the path parameter."]]}
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"
const CustomPage = () => {
const { id } = useParams()
return <Container>Passed ID: {id}</Container>
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Passed ID: {id}</Heading>
</div>
</Container>
)
}
export default CustomPage
@@ -140,3 +160,9 @@ export default CustomPage
You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params).
If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page.
---
## Admin Components List
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
@@ -24,7 +24,7 @@ For example, create the file `src/admin/widgets/product-widget.tsx` with the fol
export const widgetHighlights = [
["5", "ProductWidget", "The React component of the product widget."],
["15", "zone", "The zone to inject the widget to."]
["17", "zone", "The zone to inject the widget to."]
]
```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
@@ -34,8 +34,10 @@ import { Container, Heading } from "@medusajs/ui"
// The widget
const ProductWidget = () => {
return (
<Container>
<Heading level="h2">Product Widget</Heading>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Product Widget</Heading>
</div>
</Container>
)
}
@@ -85,7 +87,7 @@ For example:
export const detailHighlights = [
["10", "data", "Receive the data as a prop."],
["11", "AdminProduct", "Pass the expected type of `data` as a type argument."],
["15", "data.title"]
["16", "data.title", "Show the product's title."]
]
```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
@@ -101,10 +103,12 @@ const ProductWidget = ({
data,
}: DetailWidgetProps<AdminProduct>) => {
return (
<Container>
<Heading level="h2">
Product Widget {data.title}
</Heading>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">
Product Widget {data.title}
</Heading>
</div>
</Container>
)
}
@@ -24,12 +24,15 @@ For example, create the file `src/admin/widgets/product-widget.tsx` with the fol
```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const ProductWidget = () => {
return (
<div>
<h2>Product Widget</h2>
</div>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Product Widget</Heading>
</div>
</Container>
)
}
@@ -42,6 +45,8 @@ export default ProductWidget
This inserts a widget with the text “Product Widget” at the beginning of a products details page.
In your widget, use custom components from the [Medusa UI package](https://docs.medusajs.com/ui).
### Test the Widget
To test out the widget, start the Medusa application:
@@ -51,3 +56,9 @@ npm run dev
```
Then, open a products details page in the Medusa Admin. Youll find your custom widget at the top of the page.
---
## Admin Components List
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
@@ -116,24 +116,28 @@ const BrandsPage = () => {
return (
<Container>
<Heading level="h2">Brands</Heading>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>ID</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{brands.map((brand) => (
<Table.Row key={brand.id}>
<Table.Cell>{brand.id}</Table.Cell>
<Table.Cell>{brand.name}</Table.Cell>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Brands</Heading>
</div>
<div className="flex h-full flex-col overflow-hidden !border-t-0">
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>ID</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
))}
</Table.Body>
</Table>
</Table.Header>
<Table.Body>
{brands.map((brand) => (
<Table.Row key={brand.id}>
<Table.Cell>{brand.id}</Table.Cell>
<Table.Cell>{brand.name}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
</Container>
)
}
@@ -147,6 +151,12 @@ This adds a new page in the admin at `http://localhost:9000/app/brands`.
In the UI route's component, you retrieve the brands from the `/admin/brands` API route. You show the brands in a table.
<Note>
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin.
</Note>
### Add UI Route to the Sidebar
To add the UI route to the sidebar, replace the `TODO` at the end of the file with the following:
@@ -29,7 +29,7 @@ export const highlights = [
["7", "data", "Receive the product's details as a prop"],
["9", "brand", "A state variable to store the brand"],
["19", "fetch", "Retrieve the brand of a product using the custom API route"],
["39", "zone", "Show the widget at the top of the product details page."]
["41", "zone", "Show the widget at the top of the product details page."]
]
```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
@@ -62,8 +62,10 @@ const ProductBrandWidget = ({
}, [loading])
return (
<Container>
<Heading level="h2">Brand</Heading>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Brand</Heading>
</div>
{loading && <span>Loading...</span>}
{brand && <span>Name: {brand.name}</span>}
</Container>
@@ -91,7 +93,7 @@ In the widget, you fetch the product's brand from the `/admin/products/:id/brand
<Note>
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design.
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin.
</Note>
+7 -7
View File
@@ -12,7 +12,7 @@ export const generatedEditDates = {
"app/storefront-development/page.mdx": "2024-09-11T10:58:59.290Z",
"app/storefront-development/nextjs-starter/page.mdx": "2024-07-04T17:26:03+03:00",
"app/basics/page.mdx": "2024-09-03T07:11:06.879Z",
"app/basics/admin-customizations/page.mdx": "2024-09-03T08:07:35.584Z",
"app/basics/admin-customizations/page.mdx": "2024-10-07T12:41:39.218Z",
"app/advanced-development/workflows/workflow-timeout/page.mdx": "2024-09-30T08:43:53.131Z",
"app/advanced-development/workflows/parallel-steps/page.mdx": "2024-09-30T08:43:53.130Z",
"app/advanced-development/page.mdx": "2024-07-04T17:26:03+03:00",
@@ -27,7 +27,7 @@ export const generatedEditDates = {
"app/advanced-development/modules/container/page.mdx": "2024-09-30T08:43:53.125Z",
"app/advanced-development/workflows/execute-another-workflow/page.mdx": "2024-09-30T08:43:53.129Z",
"app/basics/loaders/page.mdx": "2024-09-03T08:00:45.993Z",
"app/advanced-development/admin/widgets/page.mdx": "2024-09-30T08:43:53.120Z",
"app/advanced-development/admin/widgets/page.mdx": "2024-10-07T12:51:09.969Z",
"app/advanced-development/data-models/page.mdx": "2024-09-19T07:26:43.535Z",
"app/advanced-development/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z",
"app/advanced-development/api-routes/protected-routes/page.mdx": "2024-09-30T08:43:53.121Z",
@@ -38,7 +38,7 @@ export const generatedEditDates = {
"app/advanced-development/events-and-subscribers/emit-event/page.mdx": "2024-09-30T08:43:53.125Z",
"app/advanced-development/workflows/conditions/page.mdx": "2024-09-30T08:43:53.128Z",
"app/advanced-development/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00",
"app/advanced-development/admin/page.mdx": "2024-05-29T13:50:19+03:00",
"app/advanced-development/admin/page.mdx": "2024-10-07T12:39:13.178Z",
"app/advanced-development/workflows/long-running-workflow/page.mdx": "2024-09-30T08:43:53.129Z",
"app/advanced-development/workflows/constructor-constraints/page.mdx": "2024-10-04T08:40:14.867Z",
"app/advanced-development/data-models/write-migration/page.mdx": "2024-07-15T17:46:10+02:00",
@@ -54,9 +54,9 @@ export const generatedEditDates = {
"app/advanced-development/scheduled-jobs/execution-number/page.mdx": "2024-07-02T09:41:15+00:00",
"app/advanced-development/api-routes/parameters/page.mdx": "2024-09-11T10:44:13.491Z",
"app/advanced-development/api-routes/http-methods/page.mdx": "2024-09-11T10:43:33.169Z",
"app/advanced-development/admin/tips/page.mdx": "2024-09-10T11:39:51.165Z",
"app/advanced-development/admin/tips/page.mdx": "2024-10-07T12:50:36.335Z",
"app/advanced-development/api-routes/cors/page.mdx": "2024-09-30T08:43:53.121Z",
"app/advanced-development/admin/ui-routes/page.mdx": "2024-08-06T09:44:22+02:00",
"app/advanced-development/admin/ui-routes/page.mdx": "2024-10-07T12:52:37.509Z",
"app/advanced-development/api-routes/middlewares/page.mdx": "2024-09-11T10:45:31.861Z",
"app/advanced-development/modules/isolation/page.mdx": "2024-07-04T17:26:03+03:00",
"app/advanced-development/data-models/configure-properties/page.mdx": "2024-09-30T08:43:53.122Z",
@@ -96,8 +96,8 @@ export const generatedEditDates = {
"app/customization/extend-models/extend-create-product/page.mdx": "2024-09-30T08:43:53.134Z",
"app/customization/custom-features/page.mdx": "2024-09-12T11:18:13.271Z",
"app/customization/customize-admin/page.mdx": "2024-09-12T12:25:29.853Z",
"app/customization/customize-admin/route/page.mdx": "2024-09-12T12:45:39.258Z",
"app/customization/customize-admin/widget/page.mdx": "2024-09-30T08:43:53.133Z",
"app/customization/customize-admin/route/page.mdx": "2024-10-07T12:43:11.335Z",
"app/customization/customize-admin/widget/page.mdx": "2024-10-07T12:44:24.538Z",
"app/customization/extend-models/define-link/page.mdx": "2024-09-30T08:43:53.134Z",
"app/customization/extend-models/page.mdx": "2024-09-12T12:38:57.394Z",
"app/customization/extend-models/query-linked-records/page.mdx": "2024-09-30T08:43:53.134Z",
@@ -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} />
@@ -2218,6 +2218,16 @@ export const generatedEditDates = {
"references/user/interfaces/user.IModuleService/page.mdx": "2024-10-03T00:12:20.657Z",
"references/user/interfaces/user.MessageAggregatorFormat/page.mdx": "2024-10-03T00:12:20.662Z",
"app/troubleshooting/dist-imports/page.mdx": "2024-10-03T09:19:37.639Z",
"app/admin-components/components/action-menu/page.mdx": "2024-10-07T11:16:26.178Z",
"app/admin-components/components/container/page.mdx": "2024-10-07T11:15:58.824Z",
"app/admin-components/components/header/page.mdx": "2024-10-07T11:16:47.407Z",
"app/admin-components/components/json-view-section/page.mdx": "2024-10-07T11:15:58.833Z",
"app/admin-components/components/section-row/page.mdx": "2024-10-07T11:15:58.832Z",
"app/admin-components/components/table/page.mdx": "2024-10-07T11:15:58.833Z",
"app/admin-components/page.mdx": "2024-10-07T11:09:49.493Z",
"app/admin-components/layouts/single-column/page.mdx": "2024-10-07T11:16:06.435Z",
"app/admin-components/layouts/two-column/page.mdx": "2024-10-07T11:16:10.092Z",
"app/admin-components/components/forms/page.mdx": "2024-10-09T12:48:04.229Z",
"app/commerce-modules/auth/reset-password/page.mdx": "2024-10-08T07:34:08.488Z",
"app/storefront-development/customers/reset-password/page.mdx": "2024-09-25T10:21:46.647Z",
"app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2024-10-08T08:05:36.596Z",
@@ -1,4 +1,44 @@
export const filesMap = [
{
"filePath": "/www/apps/resources/app/admin-components/components/action-menu/page.mdx",
"pathname": "/admin-components/components/action-menu"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/container/page.mdx",
"pathname": "/admin-components/components/container"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/forms/page.mdx",
"pathname": "/admin-components/components/forms"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/header/page.mdx",
"pathname": "/admin-components/components/header"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/json-view-section/page.mdx",
"pathname": "/admin-components/components/json-view-section"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/section-row/page.mdx",
"pathname": "/admin-components/components/section-row"
},
{
"filePath": "/www/apps/resources/app/admin-components/components/table/page.mdx",
"pathname": "/admin-components/components/table"
},
{
"filePath": "/www/apps/resources/app/admin-components/layouts/single-column/page.mdx",
"pathname": "/admin-components/layouts/single-column"
},
{
"filePath": "/www/apps/resources/app/admin-components/layouts/two-column/page.mdx",
"pathname": "/admin-components/layouts/two-column"
},
{
"filePath": "/www/apps/resources/app/admin-components/page.mdx",
"pathname": "/admin-components"
},
{
"filePath": "/www/apps/resources/app/admin-widget-injection-zones/page.mdx",
"pathname": "/admin-widget-injection-zones"
+249 -138
View File
@@ -7725,128 +7725,6 @@ export const generatedSidebar = [
}
]
},
{
"type": "separator"
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "SDKs and Tools",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/create-medusa-app",
"title": "create-medusa-app",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli",
"title": "Medusa CLI",
"isChildSidebar": true,
"childSidebarTitle": "Medusa CLI Reference",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli",
"title": "Overview",
"children": []
},
{
"type": "separator"
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "Commands",
"autogenerate_path": "medusa-cli/commands",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/new",
"title": "new",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/develop",
"title": "develop",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/start",
"title": "start",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/user",
"title": "user",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/db",
"title": "db",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/exec",
"title": "exec",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/start-cluster",
"title": "start-cluster",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/telemtry",
"title": "telemetry",
"children": []
}
]
}
]
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/nextjs-starter",
"title": "Next.js Starter",
"children": []
}
]
},
{
"loaded": true,
"isPathHref": true,
@@ -8089,6 +7967,128 @@ export const generatedSidebar = [
}
]
},
{
"type": "separator"
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "SDKs and Tools",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/create-medusa-app",
"title": "create-medusa-app",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli",
"title": "Medusa CLI",
"isChildSidebar": true,
"childSidebarTitle": "Medusa CLI Reference",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli",
"title": "Overview",
"children": []
},
{
"type": "separator"
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "Commands",
"autogenerate_path": "medusa-cli/commands",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/new",
"title": "new",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/develop",
"title": "develop",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/start",
"title": "start",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/user",
"title": "user",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/db",
"title": "db",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/exec",
"title": "exec",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/start-cluster",
"title": "start-cluster",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/medusa-cli/commands/telemtry",
"title": "telemetry",
"children": []
}
]
}
]
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/nextjs-starter",
"title": "Next.js Starter",
"children": []
}
]
},
{
"loaded": true,
"isPathHref": true,
@@ -8523,14 +8523,6 @@ export const generatedSidebar = [
}
]
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/references/medusa-config",
"title": "Medusa Configurations",
"children": []
},
{
"type": "separator"
},
@@ -8540,6 +8532,14 @@ export const generatedSidebar = [
"type": "category",
"title": "General",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/references/medusa-config",
"title": "Medusa Configurations",
"children": []
},
{
"loaded": true,
"isPathHref": true,
@@ -8758,6 +8758,125 @@ export const generatedSidebar = [
}
]
},
{
"type": "separator"
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "Admin",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-widget-injection-zones",
"title": "Admin Widget Injection Zones",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components",
"title": "Admin Components",
"isChildSidebar": true,
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "Layouts",
"autogenerate_path": "/admin-components/layouts",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/layouts/single-column",
"title": "Single Column",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/layouts/two-column",
"title": "Two Column",
"children": []
}
]
},
{
"loaded": true,
"isPathHref": true,
"type": "category",
"title": "Components",
"autogenerate_path": "/admin-components/components",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/action-menu",
"title": "Action Menu",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/container",
"title": "Container",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/forms",
"title": "Forms",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/header",
"title": "Header",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/json-view-section",
"title": "JSON View",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/section-row",
"title": "Section Row",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-components/components/table",
"title": "Table",
"children": []
}
]
}
]
}
]
},
{
"loaded": true,
"isPathHref": true,
@@ -8779,14 +8898,6 @@ export const generatedSidebar = [
"path": "/events-reference",
"title": "Events List",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/admin-widget-injection-zones",
"title": "Admin Widget Injection Zones",
"children": []
}
]
},
+78 -51
View File
@@ -1596,47 +1596,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
],
},
{
type: "separator",
},
{
type: "category",
title: "SDKs and Tools",
children: [
{
type: "link",
path: "/create-medusa-app",
title: "create-medusa-app",
},
{
type: "link",
path: "/medusa-cli",
title: "Medusa CLI",
isChildSidebar: true,
childSidebarTitle: "Medusa CLI Reference",
children: [
{
type: "link",
path: "/medusa-cli",
title: "Overview",
},
{
type: "separator",
},
{
type: "category",
title: "Commands",
autogenerate_path: "medusa-cli/commands",
},
],
},
{
type: "link",
path: "/nextjs-starter",
title: "Next.js Starter",
},
],
},
{
type: "link",
path: "/architectural-modules",
@@ -1799,6 +1758,47 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
],
},
{
type: "separator",
},
{
type: "category",
title: "SDKs and Tools",
children: [
{
type: "link",
path: "/create-medusa-app",
title: "create-medusa-app",
},
{
type: "link",
path: "/medusa-cli",
title: "Medusa CLI",
isChildSidebar: true,
childSidebarTitle: "Medusa CLI Reference",
children: [
{
type: "link",
path: "/medusa-cli",
title: "Overview",
},
{
type: "separator",
},
{
type: "category",
title: "Commands",
autogenerate_path: "medusa-cli/commands",
},
],
},
{
type: "link",
path: "/nextjs-starter",
title: "Next.js Starter",
},
],
},
{
type: "link",
path: "/storefront-development",
@@ -2067,11 +2067,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
],
},
{
type: "link",
path: "/references/medusa-config",
title: "Medusa Configurations",
},
{
type: "separator",
},
@@ -2079,6 +2074,11 @@ export const sidebar = sidebarAttachHrefCommonOptions([
type: "category",
title: "General",
children: [
{
type: "link",
path: "/references/medusa-config",
title: "Medusa Configurations",
},
{
type: "link",
path: "/upgrade-guides",
@@ -2209,6 +2209,38 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
],
},
{
type: "separator",
},
{
type: "category",
title: "Admin",
children: [
{
type: "link",
path: "/admin-widget-injection-zones",
title: "Admin Widget Injection Zones",
},
{
type: "link",
path: "/admin-components",
title: "Admin Components",
isChildSidebar: true,
children: [
{
type: "category",
title: "Layouts",
autogenerate_path: "/admin-components/layouts",
},
{
type: "category",
title: "Components",
autogenerate_path: "/admin-components/components",
},
],
},
],
},
{
type: "category",
title: "Lists",
@@ -2223,11 +2255,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
path: "/events-reference",
title: "Events List",
},
{
type: "link",
path: "/admin-widget-injection-zones",
title: "Admin Widget Injection Zones",
},
],
},
{