docs: revise admin customization pages (#10466)
* docs: revise admin customization pages * fix prerequisites link * apply lint
This commit is contained in:
@@ -29,12 +29,12 @@ export const checks1Highlights = [
|
||||
```ts highlights={checks1Highlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const CustomProduct = model.define('custom_product', {
|
||||
const CustomProduct = model.define("custom_product", {
|
||||
// ...
|
||||
price: model.bigNumber(),
|
||||
})
|
||||
.checks([
|
||||
(columns) => `${columns.price} >= 0`
|
||||
(columns) => `${columns.price} >= 0`,
|
||||
])
|
||||
```
|
||||
|
||||
@@ -53,15 +53,15 @@ export const checks2Highlights = [
|
||||
```ts highlights={checks2Highlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const CustomProduct = model.define('custom_product', {
|
||||
const CustomProduct = model.define("custom_product", {
|
||||
// ...
|
||||
price: model.bigNumber(),
|
||||
})
|
||||
.checks([
|
||||
{
|
||||
name: 'custom_product_price_check',
|
||||
expression: (columns) => `${columns.price} >= 0`
|
||||
}
|
||||
name: "custom_product_price_check",
|
||||
expression: (columns) => `${columns.price} >= 0`,
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ const Order = model.define("order", {
|
||||
mappedBy: "orders",
|
||||
pivotTable: "order_product",
|
||||
joinColumn: "order_id",
|
||||
inverseJoinColumn: "product_id"
|
||||
inverseJoinColumn: "product_id",
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
+1
-1
@@ -236,7 +236,7 @@ const step1 = createStep(
|
||||
)
|
||||
|
||||
return new StepResponse(ids, prevData)
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ You create a validation schema in a TypeScript or JavaScript file under a sub-di
|
||||
import { z } from "zod"
|
||||
|
||||
export const PostAdminCreateBrand = z.object({
|
||||
name: z.string()
|
||||
name: z.string(),
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Customize Admin to Add Brands`,
|
||||
title: `${pageNumber} Customize Medusa Admin Dashboard`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll continue with the brands example to learn how to customize the Medusa Admin to:
|
||||
In the previous chapters, you've customized your Medusa application to [add brands](../custom-features/module/page.mdx), [expose an API route to create brands](../custom-features/api-route/page.mdx), and [linked brands to products](../extend-features/define-link/page.mdx).
|
||||
|
||||
- Show a product's brand on its details page using a widget.
|
||||
- Add a page showing the list of brands in your application using a UI route.
|
||||
After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to:
|
||||
|
||||
- Insert components, called [widgets](../../advanced-development/admin/widgets/page.mdx), on existing pages.
|
||||
- Add new pages, called [UI Routes](../../advanced-development/admin/ui-routes/page.mdx).
|
||||
|
||||
From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard
|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: View Brands in Dashboard
|
||||
|
||||
In the next chapters, you'll continue with the brands example to:
|
||||
|
||||
- Add a new section to the product details page that shows the product's brand.
|
||||
- Add a new page in the dashboard that shows all brands in the store.
|
||||
|
||||
@@ -1,196 +1,480 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Brands List UI Route in Admin`,
|
||||
title: `${pageNumber} Create Brands UI Route in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a UI route (or page) that shows your brands as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is a UI Route?
|
||||
|
||||
A UI route is a React Component that adds a new page to your admin dashboard.
|
||||
|
||||
The UI Route can be shown in the sidebar or added as a nested page.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisite: Add Retrieve Brand API Route
|
||||
In this chapter, you'll add a UI route to the admin dashboard that shows the all [brands](../../custom-features/module/page.mdx) in a new page. You'll retrieve the brands from the server and display them in a table with pagination.
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/learn/customization/custom-features/module"
|
||||
text: "Brands Module",
|
||||
link: "/learn/customization/custom-features/modules"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
Before adding the UI route, you need an API route that retrieves all brands.
|
||||
## 1. Get Brands API Route
|
||||
|
||||
Create the file `src/api/admin/brands/route.ts` with the following content:
|
||||
In a [previous chapter](../../extend-features/query-linked-records/page.mdx), you learned how to add an API route that retrieves brands and their products using [Query](../../../advanced-development/module-links/query/page.mdx). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table.
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-7" expandMoreButton="Show Imports"
|
||||
Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following:
|
||||
|
||||
export const apiRouteHighlights = [
|
||||
["15", "metadata", "Pagination details, such as the total count or how many items were skipped."],
|
||||
["18", "remoteQueryConfig", "Query configurations parsed from the request."],
|
||||
]
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights}
|
||||
// other imports...
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const brandModuleService: BrandModuleService = req.scope.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const limit = req.query.limit || 15
|
||||
const offset = req.query.offset || 0
|
||||
|
||||
const [brands, count] = await brandModuleService.listAndCountBrands({}, {
|
||||
skip: offset as number,
|
||||
take: limit as number,
|
||||
const query = req.scope.resolve("query")
|
||||
|
||||
const {
|
||||
data: brands,
|
||||
metadata: { count, take, skip },
|
||||
} = await query.graph({
|
||||
entity: "brand",
|
||||
...req.remoteQueryConfig,
|
||||
})
|
||||
|
||||
res.json({
|
||||
res.json({
|
||||
brands,
|
||||
count,
|
||||
limit,
|
||||
offset,
|
||||
limit: take,
|
||||
offset: skip,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `GET` API route at `/admin/brands`.
|
||||
In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `remoteQueryConfig` property of the request object. This property holds configurations for pagination and retrieved fields.
|
||||
|
||||
In the API route, you resolve the Brand Module's main service and use its `listAndCountBrands` method to retrieve the list of brands with their total count.
|
||||
The query configurations are combined from default configurations, which you'll add next, and the request's query parameters:
|
||||
|
||||
This method accepts as a first parameter filters to apply on the retrieved data, and as a second parameter configurations for pagination.
|
||||
- `fields`: The fields to retrieve in the brands.
|
||||
- `limit`: The maximum number of items to retrieve.
|
||||
- `offset`: The number of items to skip before retrieving the returned items.
|
||||
|
||||
When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties:
|
||||
|
||||
- `count`: The total count of items.
|
||||
- `take`: The maximum number of items returned in the `data` array.
|
||||
- `skip`: The number of items skipped before retrieving the returned items.
|
||||
|
||||
You return in the response the retrieved brands and the pagination configurations.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the `listAndCount` method and its parameters in [this reference](!resources!/service-factory-reference/methods/listAndCount).
|
||||
Learn more about pagination with Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#apply-pagination).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Add a UI Route to Show Brands
|
||||
## 2. Add Default Query Configurations
|
||||
|
||||
A UI route is created in a file named `page.tsx` under subdirectories of the `src/admin/routes` directory. The file’s default export must be the UI route’s React component.
|
||||
Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations.
|
||||
|
||||
To create a UI route that shows the list of brands, create the file `src/admin/routes/brands/page.tsx` with the following content:
|
||||
Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import {
|
||||
defineMiddlewares,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework/http"
|
||||
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
|
||||
// other imports...
|
||||
|
||||
export const GetBrandsSchema = createFindParams()
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
// ...
|
||||
{
|
||||
matcher: "/admin/brands",
|
||||
method: "GET",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
GetBrandsSchema,
|
||||
{
|
||||
defaults: [
|
||||
"id",
|
||||
"name",
|
||||
"products.*",
|
||||
],
|
||||
isList: true,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters:
|
||||
|
||||
- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides a `createFindParams` utility that generates a Zod schema with the following properties:
|
||||
- `fields`: A comma-separated string indicating the fields to retrieve.
|
||||
- `limit`: The maximum number of items to retrieve.
|
||||
- `offset`: The number of items to skip before retrieving the returned items.
|
||||
- `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](!api!/admin#sort-order)
|
||||
- An object of Query configurations having the following properties:
|
||||
- `defaults`: An array of default fields and relations to retrieve.
|
||||
- `isList`: Whether the API route returns a list of items.
|
||||
|
||||
By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#request-query-configurations).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 3. Initialize JS SDK
|
||||
|
||||
In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](!resources!/js-sdk) that simplifies sending requests to the core API route.
|
||||
|
||||
If you didn't follow the [previous chapter](../widget/page.mdx), create the file `src/admin/lib/sdk.ts` with the following content:
|
||||
|
||||

|
||||
|
||||
```ts title="src/admin/lib/sdk.ts"
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
|
||||
export const sdk = new Medusa({
|
||||
baseUrl: "http://localhost:9000",
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
auth: {
|
||||
type: "session",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You initialize the SDK passing it the following options:
|
||||
|
||||
- `baseUrl`: The URL to the Medusa server.
|
||||
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
|
||||
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.
|
||||
|
||||
You can now use the SDK to send requests to the Medusa server.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the JS SDK and its options in [this reference](!resources!/js-sdk).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 4. Add a UI Route to Show Brands
|
||||
|
||||
You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about UI routes in [this chapter](../../../advanced-development/admin/ui-routes/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content:
|
||||
|
||||

|
||||
|
||||
export const uiRouteHighlights = [
|
||||
["7", "brands", "State variable to store the brands."],
|
||||
["12", "fetch", "Retrieve the brands from the custom API route."]
|
||||
["8", "BrandsPage", "The UI route that displays a new page."],
|
||||
["23", "defineRouteConfig", "Export config to add a link for the UI route in the sidebar."],
|
||||
["24", "label", "The sidebar item's label."],
|
||||
["25", "icon", "The sidebar item's icon."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
|
||||
import { Table, Container, Heading } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { TagSolid } from "@medusajs/icons"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { sdk } from "../../lib/sdk"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
const BrandsPage = () => {
|
||||
const [brands, setBrands] = useState<
|
||||
Record<string, string>[]
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/admin/brands`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ brands: brandsData }) => {
|
||||
setBrands(brandsData)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// TODO retrieve brands
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Brands</Heading>
|
||||
</div>
|
||||
<div className="flex h-full flex-col overflow-hidden !border-t-0">
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{brands.map((brand) => (
|
||||
<Table.Row key={brand.id}>
|
||||
<Table.Cell>{brand.id}</Table.Cell>
|
||||
<Table.Cell>{brand.name}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<div>
|
||||
<Heading level="h2">Brands</Heading>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO show brands */}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandsPage
|
||||
|
||||
// TODO export configuration
|
||||
```
|
||||
|
||||
This adds a new page in the admin at `http://localhost:9000/app/brands`.
|
||||
|
||||
In the UI route's component, you retrieve the brands from the `/admin/brands` API route. You show the brands in a table.
|
||||
|
||||
<Note>
|
||||
|
||||
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin.
|
||||
|
||||
</Note>
|
||||
|
||||
### Add UI Route to the Sidebar
|
||||
|
||||
To add the UI route to the sidebar, replace the `TODO` at the end of the file with the following:
|
||||
|
||||
```ts title="src/admin/routes/brands/page.tsx"
|
||||
export const config = defineRouteConfig({
|
||||
label: "Brands",
|
||||
icon: TagSolid,
|
||||
})
|
||||
|
||||
export default BrandsPage
|
||||
```
|
||||
|
||||
You export a `config` variable defined using the `defineRouteConfig` utility.
|
||||
A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK.
|
||||
|
||||
This indicates that a new item should be added to the sidebar with the title `Brands` and an icon from the [Medusa Icons package](!ui!/icons/overview).
|
||||
So far, you only show a "Brands" header. In admin customizations, use components from the [Medusa UI package](!ui!) to maintain a consistent user interface and design in the dashboard.
|
||||
|
||||
### Add Table Component
|
||||
|
||||
To show the brands with pagination functionalities, you'll create a new `Table` component that uses the UI package's [Table](!ui!/components/table) component with some alterations to match the design of the Medusa Admin. This new component is taken from the [Admin Components guide](!resources!/admin-components/components/table).
|
||||
|
||||
Create the `Table` component in the file `src/admin/components/table.tsx`:
|
||||
|
||||

|
||||
|
||||
```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(count / pageSize)
|
||||
}, [count, 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)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(pageCount, canNextPage, canPreviousPage, currentPage)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This component accepts the following props:
|
||||
|
||||
- `columns`: An array of the table's columns.
|
||||
- `data`: The rows in the table.
|
||||
- `pageSize`: The maximum number of items shown in a page.
|
||||
- `count`: The total number of items.
|
||||
- `currentPage`: A zero-based index of the current page.
|
||||
- `setCurrentPage`: A function to change the current page.
|
||||
|
||||
In the component, you use the UI package's [Table](!ui!/components/table) component to display the data received as a prop in a table that supports pagination.
|
||||
|
||||
You can learn more about this component's implementation and how it works in the [Admin Components guide](!resources!/admin-components), which provides more examples of how to build common components in the Medusa Admin dashboard.
|
||||
|
||||
### Retrieve Brands From API Route
|
||||
|
||||
You'll now update the UI route to retrieve the brands from the API route you added earlier.
|
||||
|
||||
First, add the following type in `src/admin/routes/brands/page.tsx`:
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx"
|
||||
type BrandsResponse = {
|
||||
brands: {
|
||||
id: string
|
||||
name: string
|
||||
}[]
|
||||
count: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
```
|
||||
|
||||
This is the type of expected response from the `GET /admin/brands` API route.
|
||||
|
||||
Then, replace the `// TODO retrieve brands` in the component with the following:
|
||||
|
||||
export const queryHighlights = [
|
||||
["1", "currentPage", "A zero-based index of the current page of items."],
|
||||
["2", "limit", "The maximum number of items per page."],
|
||||
["3", "offset", "The number of items to skip before retrieving the page's items."],
|
||||
["7", "useQuery", "Retrieve brands using Tanstack Query"],
|
||||
["8", "fetch", "Send a request to a custom API route."],
|
||||
["8", "`/admin/brands`", "The API route's path."],
|
||||
["9", "query", "Query parameters to pass in the request"]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights}
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const limit = 15
|
||||
const offset = useMemo(() => {
|
||||
return currentPage * limit
|
||||
}, [currentPage])
|
||||
|
||||
const { data } = useQuery<BrandsResponse>({
|
||||
queryFn: () => sdk.client.fetch(`/admin/brands`, {
|
||||
query: {
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
}),
|
||||
queryKey: [["brands", limit, offset]],
|
||||
})
|
||||
```
|
||||
|
||||
You first define pagination-related variables:
|
||||
|
||||
- `currentPage`: A zero-based index of the current page of items.
|
||||
- `limit`: The maximum number of items per page.
|
||||
- `offset`: The number of items to skip before retrieving the page's items. This is calculated from the `currentPage` and `limit` variables.
|
||||
|
||||
Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching.
|
||||
|
||||
In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property.
|
||||
|
||||
This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page.
|
||||
|
||||
### Display Brands Table
|
||||
|
||||
Finally, you'll display the brands in a table using the component you created earlier. Import the component at the top of `src/admin/routes/brands/page.tsx`:
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx"
|
||||
import { Table } from "../../components/table"
|
||||
```
|
||||
|
||||
Then, replace the `{/* TODO show brands */}` in the return statement with the following:
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx"
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: "id",
|
||||
label: "#",
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
},
|
||||
]}
|
||||
data={data?.brands || []}
|
||||
pageSize={data?.limit || limit}
|
||||
count={data?.count || 0}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
```
|
||||
|
||||
This renders a table that shows the ID and name of the brands.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application and login into the Medusa Admin.
|
||||
To test out the UI route, start the Medusa application:
|
||||
|
||||
You'll find a new "Brands" sidebar item. If you click on it, a new page opens showing the list of brands in your store.
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the examples of the previous chapters, you:
|
||||
By following the previous chapters, you:
|
||||
|
||||
- Created a widget that showed the brand of a product in the Medusa Admin.
|
||||
- Created a UI route that showed the list of brands in the Medusa Admin.
|
||||
- Injected a widget into the product details page to show the product's brand.
|
||||
- Created a UI route in the Medusa Admin that shows the list of brands.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
## Next Steps: Integrate Third-Party Systems
|
||||
|
||||
In the next chapters, you'll learn how to integrate third-party systems into your Medusa application to sync brands.
|
||||
Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system.
|
||||
|
||||
In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application.
|
||||
|
||||
@@ -1,64 +1,132 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Show Brand of Product in Admin`,
|
||||
title: `${pageNumber} Guide: Add Product's Brand Widget in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](../../custom-features/module/page.mdx). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it.
|
||||
|
||||
This chapter covers how to show the brand of a product in the Medusa Admin using a widget as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brands linked to products",
|
||||
link: "/learn/customization/extend-features/define-link"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## 1. Initialize JS SDK
|
||||
|
||||
In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](!resources!/js-sdk) that simplifies sending requests to the server's API routes.
|
||||
|
||||
So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content:
|
||||
|
||||

|
||||
|
||||
```ts title="src/admin/lib/sdk.ts"
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
|
||||
export const sdk = new Medusa({
|
||||
baseUrl: "http://localhost:9000",
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
auth: {
|
||||
type: "session",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You initialize the SDK passing it the following options:
|
||||
|
||||
- `baseUrl`: The URL to the Medusa server.
|
||||
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
|
||||
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.
|
||||
|
||||
You can now use the SDK to send requests to the Medusa server.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the JS SDK and its options in [this reference](!resources!/js-sdk).
|
||||
|
||||
</Note>
|
||||
|
||||
## Widget to Show Brand in Product Details
|
||||
---
|
||||
|
||||
## 2. Add Widget to Product Details Page
|
||||
|
||||
You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about widgets in [this documentation](../../../advanced-development/admin/widgets/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content:
|
||||
|
||||

|
||||
|
||||
export const highlights = [
|
||||
["7", "data", "Receive the product's details as a prop"],
|
||||
["9", "brand", "A state variable to store the brand"],
|
||||
["19", "fetch", "Retrieve the brand of a product using the custom API route"],
|
||||
["41", "zone", "Show the widget at the top of the product details page."]
|
||||
["14", "ProductBrandWidget", "Widget to inject into the page"],
|
||||
["15", "data", "Receive the product's details as a prop"],
|
||||
["17", "useQuery", "Use Tanstack Query to send the request to the server with the JS SDK."],
|
||||
["18", "sdk", "Send the request to retrieve the product with the JS SDK."],
|
||||
["19", "fields", "Specify the product's brand to be retrieved."],
|
||||
["23", "brandName", "Get brand name from the query request."],
|
||||
["53", "defineWidgetConfig", "Export the widget's configurations"],
|
||||
["54", "zone", "Show the widget at the top of the product details page."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { clx, Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { sdk } from "../lib/sdk"
|
||||
|
||||
type AdminProductBrand = AdminProduct & {
|
||||
brand?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
const ProductBrandWidget = ({
|
||||
data,
|
||||
data: product,
|
||||
}: DetailWidgetProps<AdminProduct>) => {
|
||||
const [brand, setBrand] = useState<
|
||||
Record<string, string> | undefined
|
||||
>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { data: queryResult } = useQuery({
|
||||
queryFn: () => sdk.admin.product.retrieve(product.id, {
|
||||
fields: "+brand.*",
|
||||
}),
|
||||
queryKey: [["product", product.id]],
|
||||
})
|
||||
const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/admin/products/${data.id}?fields=+brand.*`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ product }) => {
|
||||
setBrand(product.brand)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Brand</Heading>
|
||||
<div>
|
||||
<Heading level="h2">Brand</Heading>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clx(
|
||||
`text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
Name
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="whitespace-pre-line text-pretty"
|
||||
>
|
||||
{brandName || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{brand && <span>Name: {brand.name}</span>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -70,32 +138,41 @@ export const config = defineWidgetConfig({
|
||||
export default ProductBrandWidget
|
||||
```
|
||||
|
||||
This adds a widget at the top of the product's details page.
|
||||
A widget's file must export:
|
||||
|
||||
<Note>
|
||||
- A React component to be rendered in the specified injection zone. The component must be the file's default export.
|
||||
- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to.
|
||||
|
||||
Learn more about widgets [in this guide](../../../basics/admin-customizations/page.mdx).
|
||||
Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter.
|
||||
|
||||
</Note>
|
||||
In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](!api!/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand.
|
||||
|
||||
Widgets created in a details page receive the targetted item in a `data` prop. So, the `ProductBrandWidget` receives the product's details in the `data` prop.
|
||||
|
||||
In the widget, you fetch the product's brand using the [Get Product API route](!api!/admin#products_getproductsid), passing it the query parameter `fields=+brand.*` to retrieve the product's brand.
|
||||
|
||||
<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>
|
||||
You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](!ui!) to maintain a consistent user interface and design in the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
Start your Medusa application and go to a product's details page in the Medusa Admin, you'll find a new block at the top of the page showing the product's brand.
|
||||
To test out your widget, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Add List of Brands Page
|
||||
## Admin Components Guides
|
||||
|
||||
In the next chapter, you'll add a new page or UI route that displays the list of brands in your application.
|
||||
When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand.
|
||||
|
||||
The [Admin Components guides](!resources!/admin-components) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Add UI Route for Brands
|
||||
|
||||
In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users.
|
||||
|
||||
@@ -94,9 +94,9 @@ export const generatedEditDates = {
|
||||
"app/learn/customization/custom-features/workflow/page.mdx": "2024-11-28T10:47:28.084Z",
|
||||
"app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-05T09:26:15.796Z",
|
||||
"app/learn/customization/custom-features/page.mdx": "2024-11-28T08:21:55.207Z",
|
||||
"app/learn/customization/customize-admin/page.mdx": "2024-09-12T12:25:29.853Z",
|
||||
"app/learn/customization/customize-admin/route/page.mdx": "2024-10-07T12:43:11.335Z",
|
||||
"app/learn/customization/customize-admin/widget/page.mdx": "2024-12-05T10:40:56.611Z",
|
||||
"app/learn/customization/customize-admin/page.mdx": "2024-12-06T07:21:02.303Z",
|
||||
"app/learn/customization/customize-admin/route/page.mdx": "2024-12-06T08:29:57.834Z",
|
||||
"app/learn/customization/customize-admin/widget/page.mdx": "2024-12-06T08:15:11.426Z",
|
||||
"app/learn/customization/extend-features/define-link/page.mdx": "2024-12-04T17:15:16.004Z",
|
||||
"app/learn/customization/extend-features/page.mdx": "2024-09-12T12:38:57.394Z",
|
||||
"app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-05T10:36:32.357Z",
|
||||
|
||||
@@ -142,7 +142,7 @@ export const sidebar = numberSidebarItems(
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Add Widgets",
|
||||
title: "Add Widget",
|
||||
path: "/learn/customization/customize-admin/widget",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user