feat: order export and upload stream (#14243)

* feat: order export

* Merge branch 'develop' of https://github.com/medusajs/medusa into feat/order-export

* normalize status

* rm util

* serialize totals

* test

* lock

* comments

* configurable order list
This commit is contained in:
Carlos R. L. Rodrigues
2025-12-14 08:02:53 -03:00
committed by GitHub
parent e199f1eb01
commit 9366c6d468
31 changed files with 1041 additions and 37 deletions
@@ -300,6 +300,12 @@ export function getRouteMap({
{
path: "",
lazy: () => import("../../routes/orders/order-list"),
children: [
{
path: "export",
lazy: () => import("../../routes/orders/order-export"),
},
],
},
{
path: ":id",
@@ -10,8 +10,8 @@ import {
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
import { reservationItemsQueryKeys } from "./reservations"
import { inventoryItemsQueryKeys } from "./inventory"
import { reservationItemsQueryKeys } from "./reservations"
const ORDERS_QUERY_KEY = "orders" as const
const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<"orders"> & {
@@ -438,3 +438,20 @@ export const useUpdateOrderChange = (
...options,
})
}
export const useExportOrders = (
query?: HttpTypes.AdminOrderFilters,
options?: UseMutationOptions<
{ transaction_id: string },
FetchError,
HttpTypes.AdminOrderFilters
>
) => {
return useMutation({
mutationFn: () => sdk.admin.order.export(query),
onSuccess: (data, variables, context) => {
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
@@ -4049,6 +4049,45 @@
"required": ["noRecordsMessage"],
"additionalProperties": false
},
"export": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"description": {
"type": "string"
},
"success": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["title", "description"],
"additionalProperties": false
},
"filters": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["title", "description"],
"additionalProperties": false
}
},
"required": ["header", "description", "success", "filters"],
"additionalProperties": false
},
"status": {
"type": "object",
"properties": {
@@ -5740,6 +5779,7 @@
"orderCanceled",
"onDateFromSalesChannel",
"list",
"export",
"status",
"summary",
"transfer",
@@ -1080,6 +1080,18 @@
"list": {
"noRecordsMessage": "Your orders will show up here."
},
"export": {
"header": "Export Order List",
"description": "Export the order list to a CSV file.",
"success": {
"title": "Export started",
"description": "You will be notified when the export is ready."
},
"filters": {
"title": "Filters",
"description": "The following filters will be applied to the export."
}
},
"status": {
"not_paid": "Not paid",
"pending": "Pending",
@@ -0,0 +1,22 @@
import { Heading, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { DataTableFilter } from "../../../../components/table/data-table/data-table-filter"
import { useOrderTableFilters } from "../../order-list/components/order-list-table/use-order-table-filters"
export const ExportFilters = () => {
const { t } = useTranslation()
const filters = useOrderTableFilters()
return (
<div>
<Heading level="h2">{t("orders.export.filters.title")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("orders.export.filters.description")}
</Text>
<div className="mt-4">
<DataTableFilter filters={filters} readonly />
</div>
</div>
)
}
@@ -0,0 +1 @@
export { OrderExport as Component } from "./order-export"
@@ -0,0 +1,66 @@
import { Button, Heading, toast } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { RouteDrawer, useRouteModal } from "../../../components/modals"
import { useExportOrders } from "../../../hooks/api"
import { useOrderTableQuery } from "../../../hooks/table/query"
import { ExportFilters } from "./components/export-filters"
export const OrderExport = () => {
const { t } = useTranslation()
return (
<RouteDrawer>
<RouteDrawer.Header>
<RouteDrawer.Title asChild>
<Heading>{t("orders.export.header")}</Heading>
</RouteDrawer.Title>
<RouteDrawer.Description className="sr-only">
{t("orders.export.description")}
</RouteDrawer.Description>
</RouteDrawer.Header>
<OrderExportContent />
</RouteDrawer>
)
}
const OrderExportContent = () => {
const { t } = useTranslation()
const { searchParams } = useOrderTableQuery({})
const { mutateAsync } = useExportOrders(searchParams)
const { handleSuccess } = useRouteModal()
const handleExportRequest = async () => {
await mutateAsync(searchParams, {
onSuccess: () => {
toast.info(t("orders.export.success.title"), {
description: t("orders.export.success.description"),
})
handleSuccess()
},
onError: (err) => {
toast.error(err.message)
},
})
}
return (
<>
<RouteDrawer.Body>
<ExportFilters />
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button onClick={handleExportRequest} size="small">
{t("actions.export")}
</Button>
</div>
</RouteDrawer.Footer>
</>
)
}
@@ -1,16 +1,24 @@
import { useTranslation } from "react-i18next"
import { Outlet, useLocation } from "react-router-dom"
import { ConfigurableDataTable } from "../../../../../components/table/configurable-data-table"
import { useOrderTableAdapter } from "./order-table-adapter"
export const ConfigurableOrderListTable = () => {
const { t } = useTranslation()
const orderAdapter = useOrderTableAdapter()
const location = useLocation()
const adapter = useOrderTableAdapter()
return (
<ConfigurableDataTable
adapter={orderAdapter}
heading={t("orders.domain")}
layout="fill"
/>
<>
<ConfigurableDataTable
adapter={adapter}
heading={t("orders.domain")}
actions={[
{ label: t("actions.export"), to: `export${location.search}` },
]}
/>
<Outlet />
</>
)
}
@@ -1,15 +1,16 @@
import { Container, Heading } from "@medusajs/ui"
import { Button, Container, Heading } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link, Outlet, useLocation } from "react-router-dom"
import { _DataTable } from "../../../../../components/table/data-table/data-table"
import { useOrders } from "../../../../../hooks/api/orders"
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
import { useOrderTableFilters } from "./use-order-table-filters"
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
import { ConfigurableOrderListTable } from "./configurable-order-list-table"
import { useOrderTableFilters } from "./use-order-table-filters"
import { DEFAULT_FIELDS } from "../../const"
@@ -17,6 +18,7 @@ const PAGE_SIZE = 20
export const OrderListTable = () => {
const { t } = useTranslation()
const location = useLocation()
const isViewConfigEnabled = useFeatureFlag("view_configurations")
// If feature flag is enabled, use the new configurable table
@@ -57,6 +59,9 @@ export const OrderListTable = () => {
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{t("orders.domain")}</Heading>
<Button size="small" variant="secondary" asChild>
<Link to={`export${location.search}`}>{t("actions.export")}</Link>
</Button>
</div>
<_DataTable
columns={columns}
@@ -78,6 +83,7 @@ export const OrderListTable = () => {
message: t("orders.list.noRecordsMessage"),
}}
/>
<Outlet />
</Container>
)
}