feat(medusa,dahsboard): Initial work on pricing domain (#6633)
**What** - Initial work on pricing domain. - List page - Details page - Partial edit form - Fixes admin/price-lists/:id/products endpoint to allow filtering products by multiple ids. **Note** Pushing this now as the current design need a bit of tweaking so we display all relevant data to users. Future PR will include completed Edit moda, Create modal, Edit prices modal and Add prices modal.
This commit is contained in:
committed by
GitHub
parent
b8bedb84cf
commit
e124762873
6
.changeset/calm-singers-return.md
Normal file
6
.changeset/calm-singers-return.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/client-types": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Allows to filter price list products by multiple ids.
|
||||
@@ -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",
|
||||
|
||||
@@ -487,14 +487,13 @@ export const DataGridRoot = <
|
||||
}, [handleMouseUp, handleCopy, handlePaste, handleCommandHistory])
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<div className="border-b p-4"></div>
|
||||
<div className="size-full overflow-hidden">
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
style={{
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
height: "600px",
|
||||
height: "100%",
|
||||
userSelect: isSelecting || isDragging ? "none" : "auto",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -11,13 +11,9 @@ export const DataGrid = <TData, TFieldValues extends FieldValues = any>({
|
||||
isLoading,
|
||||
...props
|
||||
}: DataGridProps<TData, TFieldValues>) => {
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<DataGridSkeleton columns={props.columns} rowCount={10} />
|
||||
) : (
|
||||
<DataGridRoot {...props} />
|
||||
)}
|
||||
</div>
|
||||
return isLoading ? (
|
||||
<DataGridSkeleton columns={props.columns} rowCount={10} />
|
||||
) : (
|
||||
<DataGridRoot {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const useProductTableQuery = ({
|
||||
"tags",
|
||||
"type_id",
|
||||
"status",
|
||||
"id",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const PricingDetails = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Pricing</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { PricingDetails as Component } from "./details";
|
||||
@@ -1 +0,0 @@
|
||||
export { PricingList as Component } from "./list";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const PricingList = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Pricing</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./pricing-general-section"
|
||||
@@ -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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{priceList.name}</Heading>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<StatusBadge color={color}>{text}</StatusBadge>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "edit",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text leading="compact" size="small" weight="plus">
|
||||
{t("fields.type")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
{type}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text leading="compact" size="small" weight="plus">
|
||||
{t("fields.description")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
{priceList.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text leading="compact" size="small" weight="plus">
|
||||
{t("pricing.settings.customerGroupsLabel")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
<span>
|
||||
{firstCustomerGroups.length > 0
|
||||
? firstCustomerGroups.join(", ")
|
||||
: "-"}
|
||||
</span>
|
||||
{remainingCustomerGroups.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{remainingCustomerGroups.map((cg) => (
|
||||
<li key={cg.id}>{cg.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="text-ui-fg-muted">
|
||||
{" "}
|
||||
{t("general.plusCountMore", {
|
||||
count: remainingCustomerGroups.length,
|
||||
})}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text leading="compact" size="small" weight="plus">
|
||||
{t("pricing.settings.priceOverridesLabel")}
|
||||
</Text>
|
||||
<Text size="small" className="text-pretty">
|
||||
{overrideCount || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./pricing-product-section"
|
||||
@@ -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<RowSelectionState>({})
|
||||
|
||||
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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("products.domain")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("pricing.prices.addPrices"),
|
||||
to: "products/add",
|
||||
icon: <Plus />,
|
||||
},
|
||||
{
|
||||
label: t("pricing.prices.editPrices"),
|
||||
to: "products/edit",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
filters={filters}
|
||||
columns={columns}
|
||||
count={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
isLoading={isLoading}
|
||||
navigateTo={(row) => `/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}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useColumns = () => {
|
||||
const base = useProductTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
],
|
||||
[base]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PricingDetail as Component } from "./pricing-detail"
|
||||
@@ -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 <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<PricingGeneralSection priceList={price_list} />
|
||||
<PricingProductSection priceList={price_list} />
|
||||
<JsonViewSection data={price_list} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<z.infer<typeof EditPriceListFormSchema>>({
|
||||
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 (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div>
|
||||
<Form.Label>{t("fields.type")}</Form.Label>
|
||||
<Form.Hint>{t("pricing.settings.typeHint")}</Form.Hint>
|
||||
</div>
|
||||
<Form.Control>
|
||||
<RadioGroup {...field} onValueChange={onChange}>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={PriceListType.SALE}
|
||||
label={t("pricing.type.sale")}
|
||||
description={t("pricing.settings.saleTypeHint")}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={PriceListType.OVERRIDE}
|
||||
label={t("pricing.type.override")}
|
||||
description={t("pricing.settings.overrideTypeHint")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.name")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.description")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="includes_tax"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<Form.Label>{t("fields.taxInclusivePricing")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t("pricing.settings.taxInclusivePricingHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer className="shrink-0">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit">
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-price-list-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { PricingEdit as Component } from "./pricing-edit"
|
||||
@@ -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 (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("pricing.settings.editPriceListTitle")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{ready && <EditPriceListForm priceList={price_list} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./pricing-list-table"
|
||||
@@ -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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("pricing.domain")}</Heading>
|
||||
<Button size="small" variant="secondary" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pageSize={PAGE_SIZE}
|
||||
navigateTo={(row) => row.original.id}
|
||||
isLoading={isLoading}
|
||||
pagination
|
||||
search
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `${priceList.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<PriceList>()
|
||||
|
||||
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 (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ row }) => {
|
||||
const { color, text } = getPriceListStatus(t, row.original)
|
||||
|
||||
return <StatusCell color={color}>{text}</StatusCell>
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("customer_groups", {
|
||||
header: t("customerGroups.domain"),
|
||||
cell: (info) => info.getValue()?.length || "-",
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <PricingTableActions priceList={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PricingList as Component } from "./pricing-list"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { PricingListTable } from "./components/pricing-list-table"
|
||||
|
||||
export const PricingList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<PricingListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { PricingProductsAdd as Component } from "./pricing-products-add"
|
||||
@@ -0,0 +1,5 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
|
||||
export const PricingProductsAdd = () => {
|
||||
return <RouteFocusModal></RouteFocusModal>
|
||||
}
|
||||
@@ -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<typeof ProductEditorSchema>
|
||||
|
||||
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 (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form className="flex size-full flex-col">
|
||||
<RouteFocusModal.Header></RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-col overflow-hidden">
|
||||
<DataGrid
|
||||
isLoading={isLoading}
|
||||
data={products as Product[]}
|
||||
columns={columns}
|
||||
state={form}
|
||||
getSubRows={getVariantRows}
|
||||
/>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Product | ProductVariant>()
|
||||
|
||||
const createRegionColum = (region: Region) => {
|
||||
return columnHelper.display({
|
||||
id: region.id,
|
||||
header: region.name,
|
||||
cell: ({ row, table }) => {
|
||||
const entity = row.original
|
||||
|
||||
if (isProduct(entity)) {
|
||||
return <DisplayField />
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
field={`products.${entity.id}.variants.${entity.id}.prices.regions.${region.id}.amount`}
|
||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 <DisplayField />
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
field={`products.${entity.id}.variants.${entity.id}.prices.currencies.${currency.code}.amount`}
|
||||
meta={table.options.meta as DataGridMeta<ProductEditorSchemaType>}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 (
|
||||
<DisplayField>
|
||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||
<Thumbnail src={entity.thumbnail} />
|
||||
<span className="truncate">{entity.title}</span>
|
||||
</div>
|
||||
</DisplayField>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DisplayField>
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">{entity.title}</span>
|
||||
</div>
|
||||
</DisplayField>
|
||||
)
|
||||
},
|
||||
size: 350,
|
||||
}),
|
||||
...regionColumns,
|
||||
...currencyColumns,
|
||||
],
|
||||
[t, regionColumns, currencyColumns]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-product-prices-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { PricingProductsEdit as Component } from "./pricing-products-edit"
|
||||
@@ -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 (
|
||||
<RouteFocusModal>
|
||||
{ready && (
|
||||
<EditProductPricesForm
|
||||
ids={ids}
|
||||
regions={regions}
|
||||
currencies={currencies}
|
||||
/>
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -9,9 +9,9 @@ export interface AdminGetPriceListsPriceListProductsParams {
|
||||
*/
|
||||
q?: string
|
||||
/**
|
||||
* Filter by product ID
|
||||
* Filter by product IDs.
|
||||
*/
|
||||
id?: string
|
||||
id?: string | Array<string>
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user