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:
1 parent
e3dc9eaf0c
commit
74b286b701
22 files changed
+2362
-218
No files matched your search
@@ -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.
|
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.
|
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])
|
}, [loading])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
{loading && <span>Loading...</span>}
|
{loading && <span>Loading...</span>}
|
||||||
{!loading && <span>You have {productsCount} Product(s).</span>}
|
{!loading && <span>You have {productsCount} Product(s).</span>}
|
||||||
</Container>
|
</Container>
|
||||||
@@ -78,7 +78,7 @@ import { Link } from "react-router-dom"
|
|||||||
// The widget
|
// The widget
|
||||||
const ProductWidget = () => {
|
const ProductWidget = () => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
<Link to={"/orders"}>View Orders</Link>
|
<Link to={"/orders"}>View Orders</Link>
|
||||||
</Container>
|
</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:
|
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
|
||||||
|
|
||||||
```tsx title="src/admin/routes/custom/page.tsx"
|
```tsx title="src/admin/routes/custom/page.tsx"
|
||||||
import { Container } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
const CustomPage = () => {
|
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
|
export default CustomPage
|
||||||
@@ -59,17 +65,23 @@ A UI route file can export a configuration object that indicates a new item must
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
export const highlights = [
|
export const highlights = [
|
||||||
["14", "label", "The label of the UI route's sidebar item."],
|
["16", "label", "The label of the UI route's sidebar item."],
|
||||||
["15", "icon", "The icon 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 { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||||
import { Container } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
const CustomPage = () => {
|
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({
|
export const config = defineRouteConfig({
|
||||||
@@ -101,8 +113,10 @@ import { Container, Heading } from "@medusajs/ui"
|
|||||||
|
|
||||||
const CustomSettingPage = () => {
|
const CustomSettingPage = () => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h1">Custom Setting Page</Heading>
|
<Heading level="h1">Custom Setting Page</Heading>
|
||||||
|
</div>
|
||||||
</Container>
|
</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:
|
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 { useParams } from "react-router-dom"
|
||||||
import { Container } from "@medusajs/ui"
|
import { Container } from "@medusajs/ui"
|
||||||
|
|
||||||
const CustomPage = () => {
|
const CustomPage = () => {
|
||||||
const { id } = useParams()
|
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
|
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).
|
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.
|
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 = [
|
export const widgetHighlights = [
|
||||||
["5", "ProductWidget", "The React component of the product widget."],
|
["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}
|
```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
|
||||||
@@ -34,8 +34,10 @@ import { Container, Heading } from "@medusajs/ui"
|
|||||||
// The widget
|
// The widget
|
||||||
const ProductWidget = () => {
|
const ProductWidget = () => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">Product Widget</Heading>
|
<Heading level="h2">Product Widget</Heading>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -85,7 +87,7 @@ For example:
|
|||||||
export const detailHighlights = [
|
export const detailHighlights = [
|
||||||
["10", "data", "Receive the data as a prop."],
|
["10", "data", "Receive the data as a prop."],
|
||||||
["11", "AdminProduct", "Pass the expected type of `data` as a type argument."],
|
["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}
|
```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
|
||||||
@@ -101,10 +103,12 @@ const ProductWidget = ({
|
|||||||
data,
|
data,
|
||||||
}: DetailWidgetProps<AdminProduct>) => {
|
}: DetailWidgetProps<AdminProduct>) => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">
|
<Heading level="h2">
|
||||||
Product Widget {data.title}
|
Product Widget {data.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
</div>
|
||||||
</Container>
|
</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"
|
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
const ProductWidget = () => {
|
const ProductWidget = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Container className="divide-y p-0">
|
||||||
<h2>Product Widget</h2>
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">Product Widget</Heading>
|
||||||
</div>
|
</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.
|
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
|
### Test the Widget
|
||||||
|
|
||||||
To test out the widget, start the Medusa application:
|
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.
|
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,8 +116,11 @@ const BrandsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">Brands</Heading>
|
<Heading level="h2">Brands</Heading>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full flex-col overflow-hidden !border-t-0">
|
||||||
<Table>
|
<Table>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
@@ -134,6 +137,7 @@ const BrandsPage = () => {
|
|||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table>
|
</Table>
|
||||||
|
</div>
|
||||||
</Container>
|
</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.
|
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
|
### 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:
|
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"],
|
["7", "data", "Receive the product's details as a prop"],
|
||||||
["9", "brand", "A state variable to store the brand"],
|
["9", "brand", "A state variable to store the brand"],
|
||||||
["19", "fetch", "Retrieve the brand of a product using the custom API route"],
|
["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}
|
```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
|
||||||
@@ -62,8 +62,10 @@ const ProductBrandWidget = ({
|
|||||||
}, [loading])
|
}, [loading])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">Brand</Heading>
|
<Heading level="h2">Brand</Heading>
|
||||||
|
</div>
|
||||||
{loading && <span>Loading...</span>}
|
{loading && <span>Loading...</span>}
|
||||||
{brand && <span>Name: {brand.name}</span>}
|
{brand && <span>Name: {brand.name}</span>}
|
||||||
</Container>
|
</Container>
|
||||||
@@ -91,7 +93,7 @@ In the widget, you fetch the product's brand from the `/admin/products/:id/brand
|
|||||||
|
|
||||||
<Note>
|
<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>
|
</Note>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const generatedEditDates = {
|
|||||||
"app/storefront-development/page.mdx": "2024-09-11T10:58:59.290Z",
|
"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/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/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/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/workflows/parallel-steps/page.mdx": "2024-09-30T08:43:53.130Z",
|
||||||
"app/advanced-development/page.mdx": "2024-07-04T17:26:03+03:00",
|
"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/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/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/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/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/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",
|
"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/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/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/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/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/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",
|
"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/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/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/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/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/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/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",
|
"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/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/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/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/route/page.mdx": "2024-10-07T12:43:11.335Z",
|
||||||
"app/customization/customize-admin/widget/page.mdx": "2024-09-30T08:43:53.133Z",
|
"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/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/page.mdx": "2024-09-12T12:38:57.394Z",
|
||||||
"app/customization/extend-models/query-linked-records/page.mdx": "2024-09-30T08:43:53.134Z",
|
"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.IModuleService/page.mdx": "2024-10-03T00:12:20.657Z",
|
||||||
"references/user/interfaces/user.MessageAggregatorFormat/page.mdx": "2024-10-03T00:12:20.662Z",
|
"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/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/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/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",
|
"app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2024-10-08T08:05:36.596Z",
|
||||||
|
|||||||
@@ -1,4 +1,44 @@
|
|||||||
export const filesMap = [
|
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",
|
"filePath": "/www/apps/resources/app/admin-widget-injection-zones/page.mdx",
|
||||||
"pathname": "/admin-widget-injection-zones"
|
"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,
|
"loaded": true,
|
||||||
"isPathHref": 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,
|
"loaded": true,
|
||||||
"isPathHref": 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"
|
"type": "separator"
|
||||||
},
|
},
|
||||||
@@ -8540,6 +8532,14 @@ export const generatedSidebar = [
|
|||||||
"type": "category",
|
"type": "category",
|
||||||
"title": "General",
|
"title": "General",
|
||||||
"children": [
|
"children": [
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/references/medusa-config",
|
||||||
|
"title": "Medusa Configurations",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"loaded": true,
|
"loaded": true,
|
||||||
"isPathHref": 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,
|
"loaded": true,
|
||||||
"isPathHref": true,
|
"isPathHref": true,
|
||||||
@@ -8779,14 +8898,6 @@ export const generatedSidebar = [
|
|||||||
"path": "/events-reference",
|
"path": "/events-reference",
|
||||||
"title": "Events List",
|
"title": "Events List",
|
||||||
"children": []
|
"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",
|
type: "link",
|
||||||
path: "/architectural-modules",
|
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",
|
type: "link",
|
||||||
path: "/storefront-development",
|
path: "/storefront-development",
|
||||||
@@ -2067,11 +2067,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "link",
|
|
||||||
path: "/references/medusa-config",
|
|
||||||
title: "Medusa Configurations",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
@@ -2079,6 +2074,11 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
|||||||
type: "category",
|
type: "category",
|
||||||
title: "General",
|
title: "General",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
path: "/references/medusa-config",
|
||||||
|
title: "Medusa Configurations",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
path: "/upgrade-guides",
|
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",
|
type: "category",
|
||||||
title: "Lists",
|
title: "Lists",
|
||||||
@@ -2223,11 +2255,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
|||||||
path: "/events-reference",
|
path: "/events-reference",
|
||||||
title: "Events List",
|
title: "Events List",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "link",
|
|
||||||
path: "/admin-widget-injection-zones",
|
|
||||||
title: "Admin Widget Injection Zones",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user