feat(dashboard,medusa): Draft order list page (#6658)
**What** - (dashboard) Adds list page for Draft Order domain - (medusa) Adds the ability to filter draft orders
This commit is contained in:
committed by
GitHub
parent
c51a67a421
commit
78e5ec459a
6
.changeset/purple-sloths-pump.md
Normal file
6
.changeset/purple-sloths-pump.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/client-types": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Add missing query params to draft order list endpoint
|
||||
@@ -255,7 +255,12 @@
|
||||
}
|
||||
},
|
||||
"draftOrders": {
|
||||
"domain": "Draft Orders"
|
||||
"domain": "Draft Orders",
|
||||
"deleteWarning": "You are about to delete the draft order {{id}}. This action cannot be undone.",
|
||||
"status": {
|
||||
"open": "Open",
|
||||
"completed": "Completed"
|
||||
}
|
||||
},
|
||||
"discounts": {
|
||||
"domain": "Discounts",
|
||||
|
||||
@@ -109,11 +109,13 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: () => import("../../routes/draft-orders/list"),
|
||||
lazy: () =>
|
||||
import("../../routes/draft-orders/draft-order-list"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/draft-orders/details"),
|
||||
lazy: () =>
|
||||
import("../../routes/draft-orders/draft-order-detail"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const DraftOrderDetails = () => {
|
||||
return <div>Draft Order Details</div>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { DraftOrderDetails as Component } from "./details";
|
||||
@@ -0,0 +1,3 @@
|
||||
export const DraftOrderDetails = () => {
|
||||
return <div>Draft Order Details</div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DraftOrderDetails as Component } from "./details"
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminDraftOrders } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useDraftOrderTableColumns } from "./use-draft-order-table-columns"
|
||||
import { useDraftOrderTableFilters } from "./use-draft-order-table-filters"
|
||||
import { useDraftOrderTableQuery } from "./use-draft-order-table-query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const DraftOrderListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useDraftOrderTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { draft_orders, count, isLoading, isError, error } =
|
||||
useAdminDraftOrders(
|
||||
{
|
||||
...searchParams,
|
||||
expand: "cart,cart.customer",
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useDraftOrderTableColumns()
|
||||
const filters = useDraftOrderTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: draft_orders || [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("draftOrders.domain")}</Heading>
|
||||
<Button variant="secondary" size="small" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
search
|
||||
pagination
|
||||
navigateTo={(row) => row.original.id}
|
||||
orderBy={["status", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { DraftOrder } from "@medusajs/medusa"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteDraftOrder } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
export const DraftOrderTableActions = ({
|
||||
draftOrder,
|
||||
}: {
|
||||
draftOrder: DraftOrder
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteDraftOrder(draftOrder.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("draftOrders.deleteWarning", {
|
||||
id: `#${draftOrder.display_id}`,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `${draftOrder.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./draft-order-list-table"
|
||||
@@ -0,0 +1,81 @@
|
||||
import { DraftOrder } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||
import {
|
||||
CustomerCell,
|
||||
CustomerHeader,
|
||||
} from "../../../../../components/table/table-cells/order/customer-cell"
|
||||
import { DraftOrderTableActions } from "./draft-order-table-actions"
|
||||
|
||||
const columnHelper = createColumnHelper<DraftOrder>()
|
||||
|
||||
export const useDraftOrderTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("display_id", {
|
||||
header: t("fields.id"),
|
||||
cell: ({ getValue }) => (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">#{getValue()}</span>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
|
||||
const { color, label } = {
|
||||
open: { color: "orange", label: t("draftOrders.status.open") },
|
||||
completed: {
|
||||
color: "green",
|
||||
label: t("draftOrders.status.completed"),
|
||||
},
|
||||
}[status] as { color: "green" | "orange"; label: string }
|
||||
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("order", {
|
||||
header: t("fields.order"),
|
||||
cell: ({ getValue }) => {
|
||||
const displayId = getValue()?.display_id
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">
|
||||
{displayId ? `#${displayId}` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("cart.customer", {
|
||||
header: () => <CustomerHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const customer = getValue()
|
||||
|
||||
return <CustomerCell customer={customer} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: t("fields.createdAt"),
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue()
|
||||
|
||||
return <DateCell date={date} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <DraftOrderTableActions draftOrder={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useDraftOrderTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
type: "select",
|
||||
multiple: true,
|
||||
key: "status",
|
||||
label: t("fields.status"),
|
||||
options: [
|
||||
{
|
||||
label: t("draftOrders.status.open"),
|
||||
value: "open",
|
||||
},
|
||||
{
|
||||
label: t("draftOrders.status.completed"),
|
||||
value: "completed",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
return [...filters, ...dateFilters]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AdminGetDraftOrdersParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useDraftOrderTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["offset", "q", "order", "status", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { status, offset, created_at, updated_at, ...rest } = raw
|
||||
|
||||
const searchParams: AdminGetDraftOrdersParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
status: status ? (status.split(",") as ["open" | "completed"]) : undefined,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { DraftOrderListTable } from "./components/draft-order-list-table"
|
||||
|
||||
export const DraftOrderList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DraftOrderListTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DraftOrderList as Component } from "./draft-order-list"
|
||||
@@ -1 +0,0 @@
|
||||
export { DraftOrderList as Component } from "./list";
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const DraftOrderList = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Draft Orders</Heading>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -16,4 +16,62 @@ export interface AdminGetDraftOrdersParams {
|
||||
* a term to search draft orders' display IDs and emails in the draft order's cart
|
||||
*/
|
||||
q?: string
|
||||
/**
|
||||
* Field to sort retrieved draft orders by.
|
||||
*/
|
||||
order?: string
|
||||
/**
|
||||
* A comma-separated list of fields to expand.
|
||||
*/
|
||||
expand?: string
|
||||
/**
|
||||
* A comma-separated list of fields to include in the response.
|
||||
*/
|
||||
fields?: string
|
||||
/**
|
||||
* Filter by a creation date range.
|
||||
*/
|
||||
created_at?: {
|
||||
/**
|
||||
* filter by dates less than this date
|
||||
*/
|
||||
lt?: string
|
||||
/**
|
||||
* filter by dates greater than this date
|
||||
*/
|
||||
gt?: string
|
||||
/**
|
||||
* filter by dates less than or equal to this date
|
||||
*/
|
||||
lte?: string
|
||||
/**
|
||||
* filter by dates greater than or equal to this date
|
||||
*/
|
||||
gte?: string
|
||||
}
|
||||
/**
|
||||
* Filter by an update date range.
|
||||
*/
|
||||
updated_at?: {
|
||||
/**
|
||||
* filter by dates less than this date
|
||||
*/
|
||||
lt?: string
|
||||
/**
|
||||
* filter by dates greater than this date
|
||||
*/
|
||||
gt?: string
|
||||
/**
|
||||
* filter by dates less than or equal to this date
|
||||
*/
|
||||
lte?: string
|
||||
/**
|
||||
* filter by dates greater than or equal to this date
|
||||
*/
|
||||
gte?: string
|
||||
}
|
||||
/**
|
||||
* Filter by status
|
||||
*/
|
||||
status?: Array<"open" | "completed">
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Router } from "express"
|
||||
import { Cart, DraftOrder, Order } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import middlewares, { transformQuery } from "../../../middlewares"
|
||||
import { AdminGetDraftOrdersParams } from "./list-draft-orders"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/draft-orders", route)
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-draft-orders").default))
|
||||
route.get(
|
||||
"/",
|
||||
transformQuery(AdminGetDraftOrdersParams, {
|
||||
defaultFields: defaultAdminDraftOrdersFields,
|
||||
defaultRelations: defaultAdminDraftOrdersRelations,
|
||||
isList: true,
|
||||
}),
|
||||
middlewares.wrap(require("./list-draft-orders").default)
|
||||
)
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-draft-order").default))
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
defaultAdminDraftOrdersFields,
|
||||
defaultAdminDraftOrdersRelations,
|
||||
} from "."
|
||||
|
||||
import { DraftOrder } from "../../../../models"
|
||||
import { DraftOrderListSelector } from "../../../../types/draft-orders"
|
||||
import { DraftOrderService } from "../../../../services"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { Type } from "class-transformer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { DraftOrderStatus } from "../../../../models"
|
||||
import { DraftOrderService } from "../../../../services"
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
extendedFindParamsMixin,
|
||||
} from "../../../../types/common"
|
||||
import { DraftOrderStatusValue } from "../../../../types/draft-orders"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/draft-orders
|
||||
@@ -21,6 +25,63 @@ import { validator } from "../../../../utils/validator"
|
||||
* - (query) offset=0 {number} The number of draft orders to skip when retrieving the draft orders.
|
||||
* - (query) limit=50 {number} Limit the number of draft orders returned.
|
||||
* - (query) q {string} a term to search draft orders' display IDs and emails in the draft order's cart
|
||||
* - (query) order {string} Field to sort retrieved draft orders by.
|
||||
* - (query) expand {string} A comma-separated list of fields to expand.
|
||||
* - (query) fields {string} A comma-separated list of fields to include in the response.
|
||||
* - in: query
|
||||
* name: created_at
|
||||
* description: Filter by a creation date range.
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* lt:
|
||||
* type: string
|
||||
* description: filter by dates less than this date
|
||||
* format: date
|
||||
* gt:
|
||||
* type: string
|
||||
* description: filter by dates greater than this date
|
||||
* format: date
|
||||
* lte:
|
||||
* type: string
|
||||
* description: filter by dates less than or equal to this date
|
||||
* format: date
|
||||
* gte:
|
||||
* type: string
|
||||
* description: filter by dates greater than or equal to this date
|
||||
* format: date
|
||||
* - in: query
|
||||
* name: updated_at
|
||||
* description: Filter by an update date range.
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* lt:
|
||||
* type: string
|
||||
* description: filter by dates less than this date
|
||||
* format: date
|
||||
* gt:
|
||||
* type: string
|
||||
* description: filter by dates greater than this date
|
||||
* format: date
|
||||
* lte:
|
||||
* type: string
|
||||
* description: filter by dates less than or equal to this date
|
||||
* format: date
|
||||
* gte:
|
||||
* type: string
|
||||
* description: filter by dates greater than or equal to this date
|
||||
* format: date
|
||||
* - in: query
|
||||
* name: status
|
||||
* style: form
|
||||
* explode: false
|
||||
* description: Filter by status
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* enum: [open, completed]
|
||||
* x-codegen:
|
||||
* method: list
|
||||
* queryParams: AdminGetDraftOrdersParams
|
||||
@@ -98,39 +159,27 @@ export default async (req, res) => {
|
||||
const draftOrderService: DraftOrderService =
|
||||
req.scope.resolve("draftOrderService")
|
||||
|
||||
const validated = await validator(AdminGetDraftOrdersParams, req.query)
|
||||
|
||||
const selector: DraftOrderListSelector = {}
|
||||
|
||||
if (validated.q) {
|
||||
selector.q = validated.q
|
||||
}
|
||||
|
||||
const listConfig: FindConfig<DraftOrder> = {
|
||||
select: defaultAdminDraftOrdersFields,
|
||||
relations: defaultAdminDraftOrdersRelations,
|
||||
skip: validated.offset ?? 0,
|
||||
take: validated.limit ?? 50,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
const { skip, take } = req.listConfig
|
||||
|
||||
const [draftOrders, count] = await draftOrderService.listAndCount(
|
||||
selector,
|
||||
listConfig
|
||||
req.filterableFields,
|
||||
req.listConfig
|
||||
)
|
||||
|
||||
res.json({
|
||||
draft_orders: draftOrders,
|
||||
count,
|
||||
offset: validated.offset,
|
||||
limit: validated.limit,
|
||||
offset: skip,
|
||||
limit: take,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters used to filter and configure the pagination of the retrieved draft orders.
|
||||
*/
|
||||
export class AdminGetDraftOrdersParams {
|
||||
export class AdminGetDraftOrdersParams extends extendedFindParamsMixin({
|
||||
limit: 50,
|
||||
}) {
|
||||
/**
|
||||
* Search term to search draft orders by their display IDs and emails.
|
||||
*/
|
||||
@@ -139,18 +188,41 @@ export class AdminGetDraftOrdersParams {
|
||||
q?: string
|
||||
|
||||
/**
|
||||
* {@inheritDoc FindPaginationParams.limit}
|
||||
* {@inheritDoc FindParams.expand}
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number = 50
|
||||
@IsString()
|
||||
expand?: string
|
||||
|
||||
/**
|
||||
* {@inheritDoc FindPaginationParams.offset}
|
||||
* {@inheritDoc FindPaginationParams.limit}
|
||||
* @defaultValue 50
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number = 0
|
||||
@IsNumber()
|
||||
fields?: string
|
||||
|
||||
/**
|
||||
* Statuses to filter draft orders by.
|
||||
*/
|
||||
@IsArray()
|
||||
@IsEnum(DraftOrderStatus, { each: true })
|
||||
@IsOptional()
|
||||
status?: DraftOrderStatusValue[]
|
||||
|
||||
/**
|
||||
* Date filters to apply on the draft orders' `created_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
/**
|
||||
* Date filters to apply on the draft orders' `updated_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AddressPayload } from "./common"
|
||||
|
||||
export type DraftOrderStatusValue = "open" | "completed"
|
||||
|
||||
export type DraftOrderListSelector = { q?: string }
|
||||
|
||||
export type DraftOrderCreateProps = {
|
||||
|
||||
Reference in New Issue
Block a user