Skip to content

Commit

Permalink
docs: add documentation for DataTable (medusajs#11095)
Browse files Browse the repository at this point in the history
* docs: add documentation for DataTable

* update package versions
  • Loading branch information
shahednasser authored and jimrarras committed Jan 28, 2025
1 parent 279ff69 commit 8c64a24
Show file tree
Hide file tree
Showing 55 changed files with 3,452 additions and 1,548 deletions.
4 changes: 2 additions & 2 deletions www/apps/api-reference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"dependencies": {
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@medusajs/icons": "~2.0.0",
"@medusajs/ui": "~3.0.0",
"@medusajs/icons": "~2.4.0",
"@medusajs/ui": "~4.0.4",
"@next/mdx": "15.0.4",
"@react-hook/resize-observer": "^2.0.2",
"@readme/openapi-parser": "^2.5.0",
Expand Down
257 changes: 97 additions & 160 deletions www/apps/book/app/learn/customization/customize-admin/route/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ export const uiRouteHighlights = [
```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { TagSolid } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
import {
Container,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { useMemo, useState } from "react"
Expand All @@ -214,11 +216,6 @@ const BrandsPage = () => {

return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<div>
<Heading level="h2">Brands</Heading>
</div>
</div>
{/* TODO show brands */}
</Container>
)
Expand All @@ -234,164 +231,90 @@ export default BrandsPage

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.

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).
So far, you only show a container. In admin customizations, use components from the [Medusa UI package](!ui!) to maintain a consistent user interface and design in the dashboard.

Create the `Table` component in the file `src/admin/components/table.tsx`:
### Retrieve Brands From API Route

![Directory structure of the Medusa application after adding the table component.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472527/Medusa%20Book/brands-admin-dir-overview-4_avosrf.jpg)
You'll now update the UI route to retrieve the brands from the API route you added earlier.

```tsx title="src/admin/components/table.tsx"
import { useMemo } from "react"
import { Table as UiTable } from "@medusajs/ui"
First, add the following type in `src/admin/routes/brands/page.tsx`:

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
```tsx title="src/admin/routes/brands/page.tsx"
type Brand = {
id: string
name: string
}

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)
}
}

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>
)
type BrandsResponse = {
brands: Brand[]
count: number
limit: number
offset: number
}
```
This component accepts the following props:
You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route.
- `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.
To display the brands, you'll use Medusa UI's [DataTable](!ui!/components/data-table) component. So, add the following imports in `src/admin/routes/brands/page.tsx`:
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.
```tsx title="src/admin/routes/brands/page.tsx"
import {
// ...
Heading,
createDataTableColumnHelper,
DataTable,
DataTablePaginationState,
useDataTable,
} from "@medusajs/ui"
```

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.
You import the `DataTable` component and the following utilities:

### Retrieve Brands From API Route
- `createDataTableColumnHelper`: A utility to create columns for the data table.
- `DataTablePaginationState`: A type that holds the pagination state of the data table.
- `useDataTable`: A hook to initialize and configure the data table.

You'll now update the UI route to retrieve the brands from the API route you added earlier.
You also import the `Heading` component to show a heading above the data table.

First, add the following type in `src/admin/routes/brands/page.tsx`:
Next, you'll define the table's columns. Add the following before the `BrandsPage` component:

```tsx title="src/admin/routes/brands/page.tsx"
type BrandsResponse = {
brands: {
id: string
name: string
}[]
count: number
limit: number
offset: number
}
const columnHelper = createDataTableColumnHelper<Brand>()

const columns = [
columnHelper.accessor("id", {
header: "ID",
}),
columnHelper.accessor("name", {
header: "Name",
})
]
```

This is the type of expected response from the `GET /admin/brands` API route.
You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands.

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"]
["1", "limit", "The maximum number of items per page."],
["2", "pagination", "Define a pagination state to be passed to the data table."],
["6", "offset", "The number of items to skip before retrieving the page's items."],
["10", "useQuery", "Retrieve brands using Tanstack Query"],
["11", "fetch", "Send a request to a custom API route."],
["11", "`/admin/brands`", "The API route's path."],
["12", "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 [pagination, setPagination] = useState<DataTablePaginationState>({
pageSize: limit,
pageIndex: 0,
})
const offset = useMemo(() => {
return currentPage * limit
}, [currentPage])
return pagination.pageIndex * limit
}, [pagination])

const { data } = useQuery<BrandsResponse>({
const { data, isLoading } = useQuery<BrandsResponse>({
queryFn: () => sdk.client.fetch(`/admin/brands`, {
query: {
limit,
Expand All @@ -400,13 +323,16 @@ const { data } = useQuery<BrandsResponse>({
}),
queryKey: [["brands", limit, offset]],
})

// TODO configure data table
```

You first define pagination-related variables:
To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties:

- `pageSize`: The maximum number of items per page. You set it to `15`.
- `pageIndex`: A zero-based index of the current page of items.

- `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.
You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items.

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.

Expand All @@ -422,35 +348,46 @@ This sends a request to the [Get Brands API route](#1-get-brands-api-route), pas

### 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`:
Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following:

```tsx title="src/admin/routes/brands/page.tsx"
import { Table } from "../../components/table"
const table = useDataTable({
columns,
data: data?.brands || [],
getRowId: (row) => row.id,
rowCount: data?.count || 0,
isLoading,
pagination: {
state: pagination,
onPaginationChange: setPagination,
},
})
```

You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties:

- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility.
- `data`: The brands to display in the table.
- `getRowId`: A function that returns a unique identifier for a row.
- `rowCount`: The total count of items. This is used to determine the number of pages.
- `isLoading`: A boolean indicating whether the data is loading.
- `pagination`: An object to configure pagination. It accepts the following properties:
- `state`: The pagination state of the data table.
- `onPaginationChange`: A function to update the pagination state.

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}
/>
<DataTable instance={table}>
<DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
<Heading>Brands</Heading>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>
```

This renders a table that shows the ID and name of the brands.
This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook.

---

Expand Down
2 changes: 1 addition & 1 deletion www/apps/book/generated/edit-dates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const generatedEditDates = {
"app/learn/customization/extend-features/extend-create-product/page.mdx": "2025-01-06T11:18:58.250Z",
"app/learn/customization/custom-features/page.mdx": "2024-12-09T10:46:28.593Z",
"app/learn/customization/customize-admin/page.mdx": "2024-12-09T11:02:38.801Z",
"app/learn/customization/customize-admin/route/page.mdx": "2024-12-24T15:08:46.095Z",
"app/learn/customization/customize-admin/route/page.mdx": "2025-01-22T16:23:31.772Z",
"app/learn/customization/customize-admin/widget/page.mdx": "2024-12-09T11:02:39.108Z",
"app/learn/customization/extend-features/define-link/page.mdx": "2024-12-09T11:02:39.346Z",
"app/learn/customization/extend-features/page.mdx": "2024-12-09T11:02:39.244Z",
Expand Down
Loading

0 comments on commit 8c64a24

Please sign in to comment.