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:
@@ -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 dashboard’s 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 product’s 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 product’s details page in the Medusa Admin. You’ll 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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

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

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

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

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

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

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

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

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