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:
Kasper Fabricius Kristensen
2024-03-11 14:22:35 +01:00
committed by GitHub
parent c51a67a421
commit 78e5ec459a
21 changed files with 489 additions and 58 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/client-types": patch
"@medusajs/medusa": patch
---
fix(medusa): Add missing query params to draft order list endpoint

View File

@@ -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",

View File

@@ -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"),
},
],
},

View File

@@ -1,3 +0,0 @@
export const DraftOrderDetails = () => {
return <div>Draft Order Details</div>;
};

View File

@@ -1 +0,0 @@
export { DraftOrderDetails as Component } from "./details";

View File

@@ -0,0 +1,3 @@
export const DraftOrderDetails = () => {
return <div>Draft Order Details</div>
}

View File

@@ -0,0 +1 @@
export { DraftOrderDetails as Component } from "./details"

View File

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

View File

@@ -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 />,
},
],
},
]}
/>
)
}

View File

@@ -0,0 +1 @@
export * from "./draft-order-list-table"

View File

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

View File

@@ -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]
}

View File

@@ -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,
}
}

View File

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

View File

@@ -0,0 +1 @@
export { DraftOrderList as Component } from "./draft-order-list"

View File

@@ -1 +0,0 @@
export { DraftOrderList as Component } from "./list";

View File

@@ -1,9 +0,0 @@
import { Container, Heading } from "@medusajs/ui";
export const DraftOrderList = () => {
return (
<Container>
<Heading>Draft Orders</Heading>
</Container>
);
};

View File

@@ -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">
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import { AddressPayload } from "./common"
export type DraftOrderStatusValue = "open" | "completed"
export type DraftOrderListSelector = { q?: string }
export type DraftOrderCreateProps = {