diff --git a/.changeset/calm-singers-return.md b/.changeset/calm-singers-return.md
new file mode 100644
index 0000000000..e9230beb01
--- /dev/null
+++ b/.changeset/calm-singers-return.md
@@ -0,0 +1,6 @@
+---
+"@medusajs/client-types": patch
+"@medusajs/medusa": patch
+---
+
+fix(medusa): Allows to filter price list products by multiple ids.
diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index effe2c4628..77d7c84a1e 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -322,7 +322,35 @@
}
},
"pricing": {
- "domain": "Pricing"
+ "domain": "Pricing",
+ "deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
+ "status": {
+ "draft": "Draft",
+ "expired": "Expired",
+ "active": "Active",
+ "scheduled": "Scheduled"
+ },
+ "type": {
+ "sale": "Sale",
+ "override": "Override"
+ },
+ "settings": {
+ "typeHint": "Choose the type of price list you want to create.",
+ "saleTypeHint": "Sale prices are temporary price changes for products.",
+ "overrideTypeHint": "Overrides are usually used to create customer-specific prices.",
+ "editPriceListTitle": "Edit Price List",
+ "customerGroupsLabel": "Customer groups",
+ "priceOverridesLabel": "Price overrides",
+ "taxInclusivePricingHint": "When enabled all prices in the price list will be tax inclusive."
+ },
+ "products": {
+ "deleteProductsPricesWarning_one": "You are about to delete {{count}} product price. This action cannot be undone.",
+ "deleteProductsPricesWarning_other": "You are about to delete {{count}} product prices. This action cannot be undone."
+ },
+ "prices": {
+ "addPrices": "Add prices",
+ "editPrices": "Edit prices"
+ }
},
"profile": {
"domain": "Profile",
diff --git a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx
index 3d7cf16b8f..c9ba29d500 100644
--- a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx
+++ b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid-root/data-grid-root.tsx
@@ -487,14 +487,13 @@ export const DataGridRoot = <
}, [handleMouseUp, handleCopy, handlePaste, handleCommandHistory])
return (
-
-
+
diff --git a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid.tsx b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid.tsx
index 88d9e4b4a7..54445c2d58 100644
--- a/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid.tsx
+++ b/packages/admin-next/dashboard/src/components/grid/data-grid/data-grid.tsx
@@ -11,13 +11,9 @@ export const DataGrid =
({
isLoading,
...props
}: DataGridProps) => {
- return (
-
- {isLoading ? (
-
- ) : (
-
- )}
-
+ return isLoading ? (
+
+ ) : (
+
)
}
diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
index 5e0720dda5..1d813f94f0 100644
--- a/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
@@ -26,6 +26,7 @@ export const useProductTableQuery = ({
"tags",
"type_id",
"status",
+ "id",
],
prefix
)
diff --git a/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx b/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx
deleted file mode 100644
index 206df07caf..0000000000
--- a/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { usePrompt } from "@medusajs/ui"
-import { useTranslation } from "react-i18next"
-
-export const useFormPrompt = () => {
- const { t } = useTranslation()
- const fn = usePrompt()
-
- const promptValues = {
- title: t("general.unsavedChangesTitle"),
- description: t("general.unsavedChangesDescription"),
- cancelText: t("actions.cancel"),
- confirmText: t("actions.continue"),
- }
-
- const prompt = async () => {
- return await fn(promptValues)
- }
-
- return prompt
-}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
index a0a8d76262..557deb37e5 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
@@ -405,12 +405,34 @@ const router = createBrowserRouter([
},
children: [
{
- index: true,
- lazy: () => import("../../routes/pricing/list"),
+ path: "",
+ lazy: () => import("../../routes/pricing/pricing-list"),
+ children: [
+ // {
+ // path: "create",
+ // lazy: () => import("../../routes/pricing/pricing-create"),
+ // },
+ ],
},
{
path: ":id",
- lazy: () => import("../../routes/pricing/details"),
+ lazy: () => import("../../routes/pricing/pricing-detail"),
+ children: [
+ {
+ path: "edit",
+ lazy: () => import("../../routes/pricing/pricing-edit"),
+ },
+ {
+ path: "products/add",
+ lazy: () =>
+ import("../../routes/pricing/pricing-products-add"),
+ },
+ {
+ path: "products/edit",
+ lazy: () =>
+ import("../../routes/pricing/pricing-products-edit"),
+ },
+ ],
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/pricing/common/constants.ts b/packages/admin-next/dashboard/src/routes/pricing/common/constants.ts
new file mode 100644
index 0000000000..24218e56e8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/common/constants.ts
@@ -0,0 +1,15 @@
+/**
+ * Re-implementation of enum from `@medusajs/medusa` as it cannot be imported
+ */
+export enum PriceListStatus {
+ ACTIVE = "active",
+ DRAFT = "draft",
+}
+
+/**
+ * Re-implementation of enum from `@medusajs/medusa` as it cannot be imported
+ */
+export enum PriceListType {
+ SALE = "sale",
+ OVERRIDE = "override",
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/common/utils.ts b/packages/admin-next/dashboard/src/routes/pricing/common/utils.ts
new file mode 100644
index 0000000000..c5ec9a28f5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/common/utils.ts
@@ -0,0 +1,48 @@
+import { PriceList } from "@medusajs/medusa"
+import { TFunction } from "i18next"
+import { PriceListStatus } from "./constants"
+
+const getValues = (priceList: PriceList) => {
+ const startsAt = priceList.starts_at
+ const endsAt = priceList.ends_at
+
+ const isExpired = endsAt ? new Date(endsAt) < new Date() : false
+ const isScheduled = startsAt ? new Date(startsAt) > new Date() : false
+ const isDraft = priceList.status === PriceListStatus.DRAFT
+
+ return {
+ isExpired,
+ isScheduled,
+ isDraft,
+ }
+}
+
+export const getPriceListStatus = (
+ t: TFunction<"translation">,
+ priceList: PriceList
+) => {
+ const { isExpired, isScheduled, isDraft } = getValues(priceList)
+
+ let text = t("pricing.status.active")
+ let color: "red" | "grey" | "orange" | "green" = "green"
+
+ if (isScheduled) {
+ color = "orange"
+ text = t("pricing.status.scheduled")
+ }
+
+ if (isDraft) {
+ color = "grey"
+ text = t("pricing.status.draft")
+ }
+
+ if (isExpired) {
+ color = "red"
+ text = t("pricing.status.expired")
+ }
+
+ return {
+ color,
+ text,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/details/details.tsx b/packages/admin-next/dashboard/src/routes/pricing/details/details.tsx
deleted file mode 100644
index 6e41789447..0000000000
--- a/packages/admin-next/dashboard/src/routes/pricing/details/details.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Container, Heading } from "@medusajs/ui";
-
-export const PricingDetails = () => {
- return (
-
-
- Pricing
-
-
- );
-};
diff --git a/packages/admin-next/dashboard/src/routes/pricing/details/index.ts b/packages/admin-next/dashboard/src/routes/pricing/details/index.ts
deleted file mode 100644
index 720fdcf6f8..0000000000
--- a/packages/admin-next/dashboard/src/routes/pricing/details/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { PricingDetails as Component } from "./details";
diff --git a/packages/admin-next/dashboard/src/routes/pricing/list/index.ts b/packages/admin-next/dashboard/src/routes/pricing/list/index.ts
deleted file mode 100644
index b2b32b4611..0000000000
--- a/packages/admin-next/dashboard/src/routes/pricing/list/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { PricingList as Component } from "./list";
diff --git a/packages/admin-next/dashboard/src/routes/pricing/list/list.tsx b/packages/admin-next/dashboard/src/routes/pricing/list/list.tsx
deleted file mode 100644
index fe3df51b7e..0000000000
--- a/packages/admin-next/dashboard/src/routes/pricing/list/list.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Container, Heading } from "@medusajs/ui";
-
-export const PricingList = () => {
- return (
-
-
- Pricing
-
-
- );
-};
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/index.ts
new file mode 100644
index 0000000000..6f5f081067
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./pricing-general-section"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/pricing-general-section.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/pricing-general-section.tsx
new file mode 100644
index 0000000000..898cb13d0c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-general-section/pricing-general-section.tsx
@@ -0,0 +1,148 @@
+import { PencilSquare, Trash } from "@medusajs/icons"
+import { PriceList } from "@medusajs/medusa"
+import {
+ Container,
+ Heading,
+ StatusBadge,
+ Text,
+ Tooltip,
+ usePrompt,
+} from "@medusajs/ui"
+import { useAdminDeletePriceList } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { useNavigate } from "react-router-dom"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import { getPriceListStatus } from "../../../common/utils"
+
+type PricingGeneralSectionProps = {
+ priceList: PriceList
+}
+
+export const PricingGeneralSection = ({
+ priceList,
+}: PricingGeneralSectionProps) => {
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+ const prompt = usePrompt()
+
+ const { mutateAsync } = useAdminDeletePriceList(priceList.id)
+
+ const overrideCount = priceList.prices?.length || 0
+ const firstCustomerGroups = priceList.customer_groups?.slice(0, 3)
+ const remainingCustomerGroups = priceList.customer_groups?.slice(3)
+
+ const { color, text } = getPriceListStatus(t, priceList)
+
+ const handleDelete = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("pricing.deletePriceListWarning", {
+ name: priceList.name,
+ }),
+ confirmText: t("actions.delete"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync(undefined, {
+ onSuccess: () => {
+ navigate("..", { replace: true })
+ },
+ })
+ }
+
+ const type =
+ priceList.type === "sale"
+ ? t("pricing.type.sale")
+ : t("pricing.type.override")
+
+ return (
+
+
+
{priceList.name}
+
+
{text}
+
,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("actions.delete"),
+ onClick: handleDelete,
+ icon:
,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+
+ {t("fields.type")}
+
+
+ {type}
+
+
+
+
+ {t("fields.description")}
+
+
+ {priceList.description}
+
+
+
+
+ {t("pricing.settings.customerGroupsLabel")}
+
+
+
+ {firstCustomerGroups.length > 0
+ ? firstCustomerGroups.join(", ")
+ : "-"}
+
+ {remainingCustomerGroups.length > 0 && (
+
+ {remainingCustomerGroups.map((cg) => (
+ {cg.name}
+ ))}
+
+ }
+ >
+
+ {" "}
+ {t("general.plusCountMore", {
+ count: remainingCustomerGroups.length,
+ })}
+
+
+ )}
+
+
+
+
+ {t("pricing.settings.priceOverridesLabel")}
+
+
+ {overrideCount || "-"}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/index.ts
new file mode 100644
index 0000000000..39141c59a2
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/index.ts
@@ -0,0 +1 @@
+export * from "./pricing-product-section"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/pricing-product-section.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/pricing-product-section.tsx
new file mode 100644
index 0000000000..49919df59d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/components/pricing-product-section/pricing-product-section.tsx
@@ -0,0 +1,191 @@
+import { PencilSquare, Plus } from "@medusajs/icons"
+import { PriceList, Product } from "@medusajs/medusa"
+import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
+import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
+import {
+ useAdminDeletePriceListProductsPrices,
+ useAdminPriceListProducts,
+} from "medusa-react"
+import { useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { useNavigate } from "react-router-dom"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import { DataTable } from "../../../../../components/table/data-table"
+import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
+import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
+import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
+import { useDataTable } from "../../../../../hooks/use-data-table"
+
+type PricingProductSectionProps = {
+ priceList: PriceList
+}
+
+const PAGE_SIZE = 10
+const PREFIX = "p"
+
+export const PricingProductSection = ({
+ priceList,
+}: PricingProductSectionProps) => {
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+ const prompt = usePrompt()
+
+ const [rowSelection, setRowSelection] = useState({})
+
+ const { searchParams, raw } = useProductTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PREFIX,
+ })
+ const { products, count, isLoading, isError, error } =
+ useAdminPriceListProducts(
+ priceList.id,
+ {
+ ...searchParams,
+ expand: "variants,sales_channels,collection",
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const filters = useProductTableFilters()
+ const columns = useColumns()
+
+ const { table } = useDataTable({
+ data: (products || []) as Product[],
+ count,
+ columns,
+ enablePagination: true,
+ enableRowSelection: true,
+ pageSize: PAGE_SIZE,
+ getRowId: (row) => row.id,
+ rowSelection: {
+ state: rowSelection,
+ updater: setRowSelection,
+ },
+ prefix: PREFIX,
+ })
+
+ const { mutateAsync } = useAdminDeletePriceListProductsPrices(priceList.id)
+ const handleDelete = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("pricing.products.deleteProductsPricesWarning", {
+ count: Object.keys(rowSelection).length,
+ }),
+ confirmText: t("actions.delete"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync({
+ product_ids: Object.keys(rowSelection),
+ })
+ }
+
+ const handleEdit = async () => {
+ const ids = Object.keys(rowSelection).join(",")
+
+ navigate(`/pricing/${priceList.id}/products/edit?ids[]=${ids}`)
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
{t("products.domain")}
+
,
+ },
+ {
+ label: t("pricing.prices.editPrices"),
+ to: "products/edit",
+ icon:
,
+ },
+ ],
+ },
+ ]}
+ />
+
+ `/products/${row.original.id}`}
+ orderBy={["title", "created_at", "updated_at"]}
+ commands={[
+ {
+ action: handleEdit,
+ label: t("actions.edit"),
+ shortcut: "e",
+ },
+ {
+ action: handleDelete,
+ label: t("actions.delete"),
+ shortcut: "d",
+ },
+ ]}
+ pagination
+ search
+ prefix={PREFIX}
+ queryObject={raw}
+ />
+
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const base = useProductTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/index.ts
new file mode 100644
index 0000000000..aa7e1461c8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/index.ts
@@ -0,0 +1 @@
+export { PricingDetail as Component } from "./pricing-detail"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/pricing-detail.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/pricing-detail.tsx
new file mode 100644
index 0000000000..91d06a9dd5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-detail/pricing-detail.tsx
@@ -0,0 +1,28 @@
+import { useAdminPriceList } from "medusa-react"
+import { Outlet, useParams } from "react-router-dom"
+import { JsonViewSection } from "../../../components/common/json-view-section"
+import { PricingGeneralSection } from "./components/pricing-general-section"
+import { PricingProductSection } from "./components/pricing-product-section"
+
+export const PricingDetail = () => {
+ const { id } = useParams()
+
+ const { price_list, isLoading, isError, error } = useAdminPriceList(id!)
+
+ if (isLoading || !price_list) {
+ return Loading...
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/edit-price-list-form.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/edit-price-list-form.tsx
new file mode 100644
index 0000000000..6abde24142
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/edit-price-list-form.tsx
@@ -0,0 +1,159 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { PriceList } from "@medusajs/medusa"
+import { Button, Input, RadioGroup, Switch, Textarea } from "@medusajs/ui"
+import { useAdminUpdatePriceList } from "medusa-react"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import { z } from "zod"
+import { Form } from "../../../../../components/common/form"
+import {
+ RouteDrawer,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { PriceListType } from "../../../common/constants"
+
+type EditPriceListFormProps = {
+ priceList: PriceList
+}
+
+const EditPriceListFormSchema = z.object({
+ type: z.nativeEnum(PriceListType),
+ name: z.string().min(1),
+ description: z.string().min(1),
+ includes_tax: z.boolean(),
+})
+
+export const EditPriceListForm = ({ priceList }: EditPriceListFormProps) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ type: priceList.type,
+ name: priceList.name,
+ description: priceList.description,
+ includes_tax: priceList.includes_tax,
+ },
+ resolver: zodResolver(EditPriceListFormSchema),
+ })
+
+ const { mutateAsync } = useAdminUpdatePriceList(priceList.id)
+
+ const handleSubmit = form.handleSubmit(async (values) => {
+ await mutateAsync(values, {
+ onSuccess: () => {
+ handleSuccess()
+ },
+ })
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/index.ts
new file mode 100644
index 0000000000..23de2b1936
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/components/edit-price-list-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-price-list-form"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/index.ts
new file mode 100644
index 0000000000..a8bc1355ce
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/index.ts
@@ -0,0 +1 @@
+export { PricingEdit as Component } from "./pricing-edit"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/pricing-edit.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/pricing-edit.tsx
new file mode 100644
index 0000000000..9fc8972ce3
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-edit/pricing-edit.tsx
@@ -0,0 +1,28 @@
+import { Heading } from "@medusajs/ui"
+import { useAdminPriceList } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { useParams } from "react-router-dom"
+import { RouteDrawer } from "../../../components/route-modal"
+import { EditPriceListForm } from "./components/edit-price-list-form"
+
+export const PricingEdit = () => {
+ const { t } = useTranslation()
+ const { id } = useParams()
+
+ const { price_list, isLoading, isError, error } = useAdminPriceList(id!)
+
+ const ready = !isLoading && price_list
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("pricing.settings.editPriceListTitle")}
+
+ {ready && }
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/index.ts
new file mode 100644
index 0000000000..13fe4e07ee
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/index.ts
@@ -0,0 +1 @@
+export * from "./pricing-list-table"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-list-table.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-list-table.tsx
new file mode 100644
index 0000000000..6fe2d2c6ea
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-list-table.tsx
@@ -0,0 +1,63 @@
+import { Button, Container, Heading } from "@medusajs/ui"
+import { useAdminPriceLists } 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 { usePricingTableColumns } from "./use-pricing-table-columns"
+import { usePricingTableQuery } from "./use-pricing-table-query"
+
+const PAGE_SIZE = 20
+
+export const PricingListTable = () => {
+ const { t } = useTranslation()
+
+ const { searchParams, raw } = usePricingTableQuery({
+ pageSize: PAGE_SIZE,
+ })
+ const { price_lists, count, isLoading, isError, error } = useAdminPriceLists(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const columns = usePricingTableColumns()
+
+ const { table } = useDataTable({
+ data: price_lists || [],
+ columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("pricing.domain")}
+
+
+ row.original.id}
+ isLoading={isLoading}
+ pagination
+ search
+ />
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-table-actions.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-table-actions.tsx
new file mode 100644
index 0000000000..5e616314ca
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/pricing-table-actions.tsx
@@ -0,0 +1,61 @@
+import { PencilSquare, Trash } from "@medusajs/icons"
+import { PriceList } from "@medusajs/medusa"
+import { usePrompt } from "@medusajs/ui"
+import { useAdminDeletePriceList } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+
+type PricingTableActionsProps = {
+ priceList: PriceList
+}
+
+export const PricingTableActions = ({
+ priceList,
+}: PricingTableActionsProps) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const { mutateAsync } = useAdminDeletePriceList(priceList.id)
+
+ const handleDelete = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("pricing.deletePriceListWarning", {
+ name: priceList.name,
+ }),
+ confirmText: t("actions.delete"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync()
+ }
+
+ return (
+ ,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("actions.delete"),
+ onClick: handleDelete,
+ icon: ,
+ },
+ ],
+ },
+ ]}
+ />
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-columns.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-columns.tsx
new file mode 100644
index 0000000000..f54c87b24e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-columns.tsx
@@ -0,0 +1,54 @@
+import { PriceList } from "@medusajs/medusa"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useTranslation } from "react-i18next"
+import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
+import { getPriceListStatus } from "../../../common/utils"
+import { PricingTableActions } from "./pricing-table-actions"
+
+const columnHelper = createColumnHelper()
+
+export const usePricingTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.accessor("name", {
+ header: t("fields.name"),
+ cell: (info) => info.getValue(),
+ }),
+ columnHelper.accessor("type", {
+ header: t("fields.type"),
+ cell: ({ getValue }) => {
+ const label =
+ getValue() === "sale"
+ ? t("pricing.type.sale")
+ : t("pricing.type.override")
+
+ return (
+
+ {label}
+
+ )
+ },
+ }),
+ columnHelper.accessor("status", {
+ header: t("fields.status"),
+ cell: ({ row }) => {
+ const { color, text } = getPriceListStatus(t, row.original)
+
+ return {text}
+ },
+ }),
+ columnHelper.accessor("customer_groups", {
+ header: t("customerGroups.domain"),
+ cell: (info) => info.getValue()?.length || "-",
+ }),
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row }) => ,
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-query.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-query.tsx
new file mode 100644
index 0000000000..47c4c1e810
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/components/pricing-list-table/use-pricing-table-query.tsx
@@ -0,0 +1,23 @@
+import { AdminGetPriceListPaginationParams } from "@medusajs/medusa"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+export const usePricingTableQuery = ({
+ pageSize = 20,
+ prefix,
+}: {
+ pageSize?: number
+ prefix?: string
+}) => {
+ const raw = useQueryParams(["offset", "q"], prefix)
+
+ const searchParams: AdminGetPriceListPaginationParams = {
+ limit: pageSize,
+ offset: raw.offset ? Number(raw.offset) : 0,
+ q: raw.q,
+ }
+
+ return {
+ searchParams,
+ raw,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/index.ts
new file mode 100644
index 0000000000..6b53600118
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/index.ts
@@ -0,0 +1 @@
+export { PricingList as Component } from "./pricing-list"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-list/pricing-list.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/pricing-list.tsx
new file mode 100644
index 0000000000..c9b402c84c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-list/pricing-list.tsx
@@ -0,0 +1,11 @@
+import { Outlet } from "react-router-dom"
+import { PricingListTable } from "./components/pricing-list-table"
+
+export const PricingList = () => {
+ return (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/index.ts
new file mode 100644
index 0000000000..a9b776ba7a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/index.ts
@@ -0,0 +1 @@
+export { PricingProductsAdd as Component } from "./pricing-products-add"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/pricing-products-add.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/pricing-products-add.tsx
new file mode 100644
index 0000000000..8c620e18fa
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-add/pricing-products-add.tsx
@@ -0,0 +1,5 @@
+import { RouteFocusModal } from "../../../components/route-modal"
+
+export const PricingProductsAdd = () => {
+ return
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/edit-product-prices-form.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/edit-product-prices-form.tsx
new file mode 100644
index 0000000000..5669462084
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/edit-product-prices-form.tsx
@@ -0,0 +1,202 @@
+import { Currency, Product, ProductVariant, Region } from "@medusajs/medusa"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useAdminPriceListProducts } from "medusa-react"
+import { useMemo } from "react"
+import { useForm } from "react-hook-form"
+import { useParams } from "react-router-dom"
+import { z } from "zod"
+
+import { useTranslation } from "react-i18next"
+import { Thumbnail } from "../../../../../components/common/thumbnail"
+import { DataGrid } from "../../../../../components/grid/data-grid"
+import { TextField } from "../../../../../components/grid/grid-fields/common/text-field"
+import { DisplayField } from "../../../../../components/grid/grid-fields/non-interactive/display-field"
+import { DataGridMeta } from "../../../../../components/grid/types"
+import { RouteFocusModal } from "../../../../../components/route-modal"
+
+type EditProductPricesFormProps = {
+ regions: Region[]
+ currencies: Currency[]
+ ids: string | null
+}
+
+const ProductEditorSchema = z.object({
+ products: z.record(
+ z.object({
+ variants: z.record(
+ z.object({
+ prices: z.object({
+ regions: z.record(
+ z.object({
+ amount: z.number(),
+ })
+ ),
+ currencies: z.record(
+ z.object({
+ amount: z.number(),
+ })
+ ),
+ }),
+ })
+ ),
+ })
+ ),
+})
+
+type ProductEditorSchemaType = z.infer
+
+export const EditProductPricesForm = ({
+ ids,
+ regions,
+ currencies,
+}: EditProductPricesFormProps) => {
+ const { id } = useParams()
+
+ const form = useForm()
+
+ const { products, count, isLoading, isError, error } =
+ useAdminPriceListProducts(
+ id!,
+ {
+ id: ids?.split(",") || undefined,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const columns = useColumns({ regions, currencies })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+ )
+}
+
+/**
+ * Helper function to determine if a row is a product or a variant.
+ */
+const isProduct = (row: Product | ProductVariant): row is Product => {
+ return "variants" in row
+}
+
+const getVariantRows = (row: Product | ProductVariant) => {
+ if ("variants" in row) {
+ return row.variants
+ }
+
+ return undefined
+}
+
+const columnHelper = createColumnHelper()
+
+const createRegionColum = (region: Region) => {
+ return columnHelper.display({
+ id: region.id,
+ header: region.name,
+ cell: ({ row, table }) => {
+ const entity = row.original
+
+ if (isProduct(entity)) {
+ return
+ }
+
+ return (
+ }
+ />
+ )
+ },
+ })
+}
+
+const createCurrencyColumn = (currency: Currency) => {
+ return columnHelper.display({
+ id: currency.code,
+ header: currency.code.toUpperCase(),
+ cell: ({ row, table }) => {
+ const entity = row.original
+
+ if (isProduct(entity)) {
+ return
+ }
+
+ return (
+ }
+ />
+ )
+ },
+ })
+}
+
+const useColumns = ({
+ regions,
+ currencies,
+}: {
+ regions: Region[]
+ currencies: Currency[]
+}) => {
+ const { t } = useTranslation()
+
+ const regionColumns = useMemo(() => {
+ return regions.map(createRegionColum)
+ }, [regions])
+
+ const currencyColumns = useMemo(() => {
+ return currencies.map(createCurrencyColumn)
+ }, [currencies])
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "product-display",
+ header: t("fields.product"),
+ cell: ({ row }) => {
+ const entity = row.original
+
+ if (isProduct(entity)) {
+ return (
+
+
+
+ {entity.title}
+
+
+ )
+ }
+
+ return (
+
+
+ {entity.title}
+
+
+ )
+ },
+ size: 350,
+ }),
+ ...regionColumns,
+ ...currencyColumns,
+ ],
+ [t, regionColumns, currencyColumns]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/index.ts
new file mode 100644
index 0000000000..9ab3a194af
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/components/edit-product-prices-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-product-prices-form"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/index.ts b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/index.ts
new file mode 100644
index 0000000000..d3d036ef45
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/index.ts
@@ -0,0 +1 @@
+export { PricingProductsEdit as Component } from "./pricing-products-edit"
diff --git a/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/pricing-products-edit.tsx b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/pricing-products-edit.tsx
new file mode 100644
index 0000000000..f38b4ab4db
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/pricing/pricing-products-edit/pricing-products-edit.tsx
@@ -0,0 +1,45 @@
+import { useAdminRegions, useAdminStore } from "medusa-react"
+import { useSearchParams } from "react-router-dom"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { EditProductPricesForm } from "./components/edit-product-prices-form"
+
+export const PricingProductsEdit = () => {
+ const [searchParams] = useSearchParams()
+
+ const { regions, isLoading, isError, error } = useAdminRegions({
+ limit: 1000,
+ fields: "id,name,includes_tax,currency.code,currency.symbol_native",
+ expand: "currency",
+ })
+
+ const {
+ store,
+ isLoading: isLoadingStore,
+ isError: isStoreError,
+ error: storeError,
+ } = useAdminStore()
+
+ const ids = searchParams.get("ids[]")
+ const currencies = store?.currencies || []
+ const ready = !isLoading && regions && !isLoadingStore && store
+
+ if (isError) {
+ throw error
+ }
+
+ if (isStoreError) {
+ throw storeError
+ }
+
+ return (
+
+ {ready && (
+
+ )}
+
+ )
+}
diff --git a/packages/generated/client-types/src/lib/models/AdminGetPriceListsPriceListProductsParams.ts b/packages/generated/client-types/src/lib/models/AdminGetPriceListsPriceListProductsParams.ts
index 24552c84ef..e704a95914 100644
--- a/packages/generated/client-types/src/lib/models/AdminGetPriceListsPriceListProductsParams.ts
+++ b/packages/generated/client-types/src/lib/models/AdminGetPriceListsPriceListProductsParams.ts
@@ -9,9 +9,9 @@ export interface AdminGetPriceListsPriceListProductsParams {
*/
q?: string
/**
- * Filter by product ID
+ * Filter by product IDs.
*/
- id?: string
+ id?: string | Array
/**
* Filter by product status
*/
@@ -39,7 +39,7 @@ export interface AdminGetPriceListsPriceListProductsParams {
/**
* A boolean value to filter by whether the product is a gift card or not.
*/
- is_giftcard?: string
+ is_giftcard?: boolean
/**
* Filter product type.
*/
diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts
index 70547c9fcb..46f5321453 100644
--- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts
+++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts
@@ -6,7 +6,6 @@ import {
IsString,
ValidateNested,
} from "class-validator"
-import { isDefined } from "medusa-core-utils"
import {
DateComparisonOperator,
extendedFindParamsMixin,
@@ -15,11 +14,10 @@ import {
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { Type } from "class-transformer"
import { Request } from "express"
-import { pickBy } from "lodash"
import { ProductStatus } from "../../../../models"
import PriceListService from "../../../../services/price-list"
import { FilterableProductProps } from "../../../../types/product"
-import { listProducts } from "../../../../utils"
+import { IsType, listProducts } from "../../../../utils"
/**
* @oas [get] /admin/price-lists/{id}/products
@@ -30,7 +28,19 @@ import { listProducts } from "../../../../utils"
* parameters:
* - (path) id=* {string} ID of the price list.
* - (query) q {string} term used to search products' title, description, product variant's title and sku, and product collection's title.
- * - (query) id {string} Filter by product ID
+ * - in: query
+ * name: id
+ * style: form
+ * explode: false
+ * description: Filter by product IDs.
+ * schema:
+ * oneOf:
+ * - type: string
+ * description: ID of the product.
+ * - type: array
+ * items:
+ * type: string
+ * description: ID of a product.
* - in: query
* name: status
* description: Filter by product status
@@ -62,7 +72,7 @@ import { listProducts } from "../../../../utils"
* - (query) title {string} Filter by title
* - (query) description {string} Filter by description
* - (query) handle {string} Filter by handle
- * - (query) is_giftcard {string} A boolean value to filter by whether the product is a gift card or not.
+ * - (query) is_giftcard {boolean} A boolean value to filter by whether the product is a gift card or not.
* - (query) type {string} Filter product type.
* - (query) order {string} A product field to sort-order the retrieved products by.
* - in: query
@@ -239,7 +249,7 @@ export default async (req: Request, res) => {
} else {
;[products, count] = await priceListService.listProducts(
id,
- pickBy(filterableFields, (val) => isDefined(val)),
+ filterableFields,
req.listConfig
)
}
@@ -264,9 +274,9 @@ export class AdminGetPriceListsPriceListProductsParams extends extendedFindParam
/**
* ID to filter products by.
*/
- @IsString()
@IsOptional()
- id?: string
+ @IsType([String, [String]])
+ id?: string | string[]
/**
* Search term to search products' title, description, product variant's title and sku, and product collection's title.
@@ -323,7 +333,7 @@ export class AdminGetPriceListsPriceListProductsParams extends extendedFindParam
@IsBoolean()
@IsOptional()
@Type(() => Boolean)
- is_giftcard?: string
+ is_giftcard?: boolean
/**
* Type to filter products by.
diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts
index 21f2f8b458..4e79cfe00c 100644
--- a/packages/medusa/src/services/price-list.ts
+++ b/packages/medusa/src/services/price-list.ts
@@ -1,3 +1,7 @@
+import { MedusaError, isDefined } from "medusa-core-utils"
+import { DeepPartial, EntityManager } from "typeorm"
+import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
+import { FindConfig, Selector } from "../types/common"
import {
CreatePriceListInput,
FilterablePriceListProps,
@@ -5,24 +9,20 @@ import {
PriceListPriceUpdateInput,
UpdatePriceListInput,
} from "../types/price-list"
-import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
-import { DeepPartial, EntityManager } from "typeorm"
-import { FindConfig, Selector } from "../types/common"
-import { isDefined, MedusaError } from "medusa-core-utils"
-import { CustomerGroupService } from "."
-import { FilterableProductProps } from "../types/product"
-import { FilterableProductVariantProps } from "../types/product-variant"
import { FlagRouter, promiseAll } from "@medusajs/utils"
+import { CustomerGroupService } from "."
+import { TransactionBaseService } from "../interfaces"
+import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { MoneyAmountRepository } from "../repositories/money-amount"
import { PriceListRepository } from "../repositories/price-list"
-import ProductService from "./product"
import { ProductVariantRepository } from "../repositories/product-variant"
+import { FilterableProductProps } from "../types/product"
+import { FilterableProductVariantProps } from "../types/product-variant"
+import { buildQuery } from "../utils"
+import ProductService from "./product"
import ProductVariantService from "./product-variant"
import RegionService from "./region"
-import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
-import { TransactionBaseService } from "../interfaces"
-import { buildQuery } from "../utils"
type PriceListConstructorProps = {
manager: EntityManager
@@ -378,6 +378,7 @@ class PriceListService extends TransactionBaseService {
const productVariantRepo = manager.withRepository(
this.productVariantRepo_
)
+
const [products, count] = await this.productService_
.withTransaction(manager)
.listAndCount(selector, config)