feat(dashboard): manage discounts conditions (#6620)
**What** - forms for managing discount conditions --- https://github.com/medusajs/medusa/assets/16856471/bb0b3fe8-fa60-479b-bc42-6db9d422bcdf Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
co-authored by
Kasper Fabricius Kristensen
parent
9288f53327
commit
a1b4aff127
@@ -309,7 +309,7 @@
|
||||
"discounts": {
|
||||
"domain": "Discounts",
|
||||
"startDate": "Start date",
|
||||
"createDiscountTitle": "Create discount",
|
||||
"createDiscountTitle": "Create Discount",
|
||||
"validDuration": "Duration of the discount",
|
||||
"redemptionsLimit": "Redemptions limit",
|
||||
"endDate": "End date",
|
||||
@@ -320,8 +320,8 @@
|
||||
"fixedAmount": "Fixed amount",
|
||||
"validRegions": "Valid regions",
|
||||
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
|
||||
"editDiscountDetails": "Edit discount details",
|
||||
"editDiscountConfiguration": "Edit discount configuration",
|
||||
"editDiscountDetails": "Edit Discount Details",
|
||||
"editDiscountConfiguration": "Edit Discount Configuration",
|
||||
"hasStartDate": "Discount has a start date",
|
||||
"hasEndDate": "Discount has an expiry date",
|
||||
"startDateHint": "Schedule the discount to activate in the future.",
|
||||
@@ -329,38 +329,54 @@
|
||||
"codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.",
|
||||
"hasUsageLimit": "Limit the number of redemptions?",
|
||||
"usageLimitHint": "Limit applies across all customers, not per customer.",
|
||||
"titleHint": "The code your customers will enter during checkout. This will appear on your customer’s invoice.\nUppercase letters and numbers only.",
|
||||
"titleHint": "The code your customers will enter during checkout. This will appear on your customer's invoice.\nUppercase letters and numbers only.",
|
||||
"hasDurationLimit": "Availability duration",
|
||||
"durationHint": "Set the duration of the discount",
|
||||
"chooseValidRegions": "Choose valid regions",
|
||||
"noConditions": "No conditions are defined for this discount.",
|
||||
"editConditions": "Edit conditions",
|
||||
"conditionsHint": "Create conditions to apply on the discount",
|
||||
"isTemplateDiscount": "Is this a template discount?",
|
||||
"percentageDescription": "Discount applied in %",
|
||||
"fixedDescription": "Amount discount",
|
||||
"shippingDescription": "Override delivery amount",
|
||||
"selectRegionFirst": "Select region first",
|
||||
"selectRegionFirst": "Select a region first",
|
||||
"templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.",
|
||||
"conditions": {
|
||||
"editHeader": "Edit Discount Conditions",
|
||||
"editHint": "Specify conditions for when the discount can be applied to a cart.",
|
||||
"manageTypesAction": "Manage condition types",
|
||||
"including": {
|
||||
"products_one": "Discount applies to <0/> product.",
|
||||
"products_other": "Discount applies to <0/> products.",
|
||||
"customer_groups_one": "Discount applies to <0/> customer group.",
|
||||
"customer_groups_other": "Discount applies to <0/> customer groups.",
|
||||
"product_tags_one": "Discount applies to <0/> tag.",
|
||||
"product_tags_other": "Discount applies to <0/> tags.",
|
||||
"product_collections_one": "Discount applies to <0/> product collection.",
|
||||
"product_collections_other": "Discount applies to <0/> product collections.",
|
||||
"product_types_one": "Discount applies to <0/> product type.",
|
||||
"product_types_other": "Discount applies to <0/> product types."
|
||||
"products_one": "Discount applies to <0/> product",
|
||||
"products_other": "Discount applies to <0/> products",
|
||||
"customer_groups_one": "Discount applies to <0/> customer group",
|
||||
"customer_groups_other": "Discount applies to <0/> customer groups",
|
||||
"product_tags_one": "Discount applies to <0/> tag",
|
||||
"product_tags_other": "Discount applies to <0/> tags",
|
||||
"product_collections_one": "Discount applies to <0/> product collection",
|
||||
"product_collections_other": "Discount applies to <0/> product collections",
|
||||
"product_types_one": "Discount applies to <0/> product type",
|
||||
"product_types_other": "Discount applies to <0/> product types"
|
||||
},
|
||||
"excluding": {
|
||||
"products": "Discount applies to <1>all</1> products except <0/>.",
|
||||
"customer_groups": "Discount applies to <1>all</1> customer groups except <0/>.",
|
||||
"product_tags": "Discount applies to <1>all</1> product tags except <0/>.",
|
||||
"product_collections": "Discount applies to <1>all</1> product collections except <0/>.",
|
||||
"product_types": "Discount applies to <1>all</1> product types except <0/>."
|
||||
"products": "Discount applies to <1>all</1> products except <0/>",
|
||||
"customer_groups": "Discount applies to <1>all</1> customer groups except <0/>",
|
||||
"product_tags": "Discount applies to <1>all</1> product tags except <0/>",
|
||||
"product_collections": "Discount applies to <1>all</1> product collections except <0/>",
|
||||
"product_types": "Discount applies to <1>all</1> product types except <0/>"
|
||||
},
|
||||
"edit": {
|
||||
"appliesTo": "Discount applies to",
|
||||
"except": {
|
||||
"products_one": "product, except",
|
||||
"products_other": "products, except",
|
||||
"product_tags_one": "product tag, except",
|
||||
"product_tags_other": "product tags, except",
|
||||
"product_types_one": "product type, except",
|
||||
"product_types_other": "product types, except",
|
||||
"product_collections_one": "product collection, except",
|
||||
"product_collections_other": "product collections, except",
|
||||
"customer_groups_one": "customer group, except",
|
||||
"customer_groups_other": "customer groups, except"
|
||||
}
|
||||
}
|
||||
},
|
||||
"discountStatus": {
|
||||
@@ -692,8 +708,14 @@
|
||||
"limit": "Limit",
|
||||
"tags": "Tags",
|
||||
"type": "Type",
|
||||
"none": "none",
|
||||
"all": "all",
|
||||
"percentage": "Percentage",
|
||||
"sales_channels": "Sales Channels",
|
||||
"customer_groups": "Customer Groups",
|
||||
"product_tags": "Product Tags",
|
||||
"product_types": "Product Types",
|
||||
"product_collections": "Product Collections",
|
||||
"status": "Status",
|
||||
"code": "Code",
|
||||
"value": "Value",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text, Tooltip, clx } from "@medusajs/ui"
|
||||
import { Tooltip, clx } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type ListSummaryProps = {
|
||||
@@ -12,46 +12,54 @@ type ListSummaryProps = {
|
||||
*/
|
||||
list: string[]
|
||||
/**
|
||||
* Is hte summary displayed inline.
|
||||
* Is the summary displayed inline.
|
||||
* Determines whether the center text is truncated if there is no space in the container
|
||||
*/
|
||||
inline?: boolean
|
||||
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ListSummary = ({ list, inline, n = 2 }: ListSummaryProps) => {
|
||||
export const ListSummary = ({
|
||||
list,
|
||||
className,
|
||||
inline,
|
||||
n = 2,
|
||||
}: ListSummaryProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const title = t("general.plusCountMore", {
|
||||
count: list.length - n,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clx("text-ui-fg-subtle gap-x-2", {
|
||||
"inline-flex": inline,
|
||||
flex: !inline,
|
||||
})}
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-small gap-x-1 overflow-hidden",
|
||||
{
|
||||
"inline-flex": inline,
|
||||
flex: !inline,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Text as="span" leading="compact" size="small" className="truncate">
|
||||
{list.slice(0, n).join(", ")}
|
||||
</Text>
|
||||
<div className="flex-1 truncate">
|
||||
<span className="truncate">{list.slice(0, n).join(", ")}</span>
|
||||
</div>
|
||||
{list.length > n && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{list.slice(n).map((c) => (
|
||||
<li key={c}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="cursor-default whitespace-nowrap"
|
||||
<div className="whitespace-nowrap">
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{list.slice(n).map((c) => (
|
||||
<li key={c}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
{t("general.plusCountMore", {
|
||||
count: list.length - n,
|
||||
})}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<span className="cursor-default whitespace-nowrap">{title}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Tooltip } from "@medusajs/ui"
|
||||
import format from "date-fns/format"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type DateCellProps = {
|
||||
date: Date
|
||||
}
|
||||
|
||||
export const CreatedAtCell = ({ date }: DateCellProps) => {
|
||||
const value = new Date(date)
|
||||
value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
|
||||
|
||||
const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12
|
||||
const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM"
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<Tooltip
|
||||
className="z-10"
|
||||
content={
|
||||
<span className="text-pretty">{`${format(
|
||||
value,
|
||||
timestampFormat
|
||||
)}`}</span>
|
||||
}
|
||||
>
|
||||
<span className="truncate">{format(value, "dd MMM yyyy")}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CreatedAtHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.createdAt")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./created-at-cell"
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { Text } from "@medusajs/ui"
|
||||
|
||||
import { NameHeader } from "../../../components/table/table-cells/common/name-cell"
|
||||
import {
|
||||
CreatedAtHeader,
|
||||
CreatedAtCell,
|
||||
} from "../../../components/table/table-cells/common/created-at-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||
|
||||
export const useCustomerGroupTableColumns = () => {
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "name",
|
||||
header: () => <NameHeader />,
|
||||
cell: ({
|
||||
row: {
|
||||
original: { name },
|
||||
},
|
||||
}) => <Text size="small">{name}</Text>,
|
||||
}),
|
||||
|
||||
columnHelper.accessor("created_at", {
|
||||
header: () => <CreatedAtHeader />,
|
||||
cell: ({ getValue }) => <CreatedAtCell date={getValue()} />,
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { AdminGetCustomerGroupsParams } from "@medusajs/medusa"
|
||||
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseCustomerGroupTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useCustomerGroupTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseCustomerGroupTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "has_account", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, q, order } = queryObject
|
||||
|
||||
const searchParams: AdminGetCustomerGroupsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AdminCustomerGroupsRes,
|
||||
AdminCustomersRes,
|
||||
AdminDraftOrdersRes,
|
||||
AdminDiscountsRes,
|
||||
AdminGiftCardsRes,
|
||||
AdminOrdersRes,
|
||||
AdminProductsRes,
|
||||
@@ -430,25 +431,36 @@ export const v1Routes: RouteObject[] = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: () => import("../../routes/discounts/list"),
|
||||
path: "",
|
||||
lazy: () => import("../../routes/discounts/discount-list"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
lazy: () => import("../../routes/discounts/create"),
|
||||
lazy: () => import("../../routes/discounts/discount-create"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/discounts/details"),
|
||||
lazy: () => import("../../routes/discounts/discount-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminDiscountsRes) => data.discount.code,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/discounts/edit-details"),
|
||||
lazy: () =>
|
||||
import("../../routes/discounts/discount-edit-details"),
|
||||
},
|
||||
{
|
||||
path: "configuration",
|
||||
lazy: () =>
|
||||
import("../../routes/discounts/edit-configuration"),
|
||||
import(
|
||||
"../../routes/discounts/discount-edit-configuration"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "conditions",
|
||||
lazy: () =>
|
||||
import("../../routes/discounts/discount-edit-conditions"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
import {
|
||||
CustomerGroup,
|
||||
Product,
|
||||
ProductCollection,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
} from "@medusajs/medusa"
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import {
|
||||
useAdminCollections,
|
||||
useAdminCustomerGroups,
|
||||
useAdminProductTags,
|
||||
useAdminProductTypes,
|
||||
useAdminProducts,
|
||||
} from "medusa-react"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||
import { useProductConditionsTableColumns } from "../../hooks/columns/use-product-conditions-table-columns"
|
||||
|
||||
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||
import { useCustomerGroupConditionsTableColumns } from "../../hooks/columns/use-customer-group-conditions-table-columns"
|
||||
import { useCustomerGroupConditionsTableFilters } from "../../hooks/filters/use-customer-group-conditions-table-filters"
|
||||
|
||||
import { useProductTypeConditionsTableColumns } from "../../hooks/columns/use-product-type-conditions-table-columns"
|
||||
import { useProductTypeConditionsTableFilters } from "../../hooks/filters/use-product-type-conditions-table-filters"
|
||||
import { useProductTypeConditionsTableQuery } from "../../hooks/query/use-product-type-conditions-table-query"
|
||||
|
||||
import { useProductCollectionConditionsTableColumns } from "../../hooks/columns/use-product-collection-conditions-table-columns"
|
||||
import { useProductCollectionConditionsTableFilters } from "../../hooks/filters/use-product-collection-conditions-table-filters"
|
||||
import { useProductCollectionConditionsTableQuery } from "../../hooks/query/use-product-collection-conditions-table-query"
|
||||
|
||||
import { useProductTagConditionsTableColumns } from "../../hooks/columns/use-product-tag-conditions-table-columns"
|
||||
import { useProductTagConditionsTableFilters } from "../../hooks/filters/use-product-tag-conditions-table-filters"
|
||||
import { useProductTagConditionsTableQuery } from "../../hooks/query/use-product-tag-conditions-table-query"
|
||||
|
||||
import { ConditionEntities } from "../../constants"
|
||||
import { ConditionsOption } from "../../types"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
const PRODUCT_PREFIX = "product"
|
||||
const PRODUCT_TYPE_PREFIX = "product_type"
|
||||
const PRODUCT_COLLECTION_PREFIX = "product_collection"
|
||||
const CUSTOMER_GROUP_PREFIX = "customer_group"
|
||||
const PRODUCT_TAG_PREFIX = "customer_group"
|
||||
|
||||
type ConditionsProps = {
|
||||
selected: ConditionsOption[]
|
||||
onSave: (options: ConditionsOption[]) => void
|
||||
}
|
||||
|
||||
const initRowState = (selected: ConditionsOption[] = []): RowSelectionState => {
|
||||
return selected.reduce((acc, { value }) => {
|
||||
acc[value] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState)
|
||||
}
|
||||
|
||||
const ConditionsFooter = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||
<SplitView.Close type="button" asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</SplitView.Close>
|
||||
<Button size="small" type="button" onClick={onSave}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductConditionsTable = ({ selected = [], onSave }: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_PREFIX,
|
||||
})
|
||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedProducts = (products?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as Product[]
|
||||
|
||||
if (addedProducts.length > 0) {
|
||||
const newConditions = addedProducts.map((p) => ({
|
||||
label: p.title,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductConditionsTableColumns()
|
||||
const filters = useProductTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: (products ?? []) as Product[],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
layout="fill"
|
||||
orderBy={["title", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomerGroupConditionsTable = ({
|
||||
selected = [],
|
||||
onSave,
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useCustomerGroupTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_PREFIX,
|
||||
})
|
||||
const { customer_groups, count, isLoading, isError, error } =
|
||||
useAdminCustomerGroups(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedGroups = (customer_groups?.filter((p) =>
|
||||
added.includes(p.id!)
|
||||
) ?? []) as CustomerGroup[]
|
||||
|
||||
if (addedGroups.length > 0) {
|
||||
const newConditions = addedGroups.map((p) => ({
|
||||
label: p.name,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useCustomerGroupConditionsTableColumns()
|
||||
const filters = useCustomerGroupConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: (customer_groups ?? []) as CustomerGroup[],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: CUSTOMER_GROUP_PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
layout="fill"
|
||||
orderBy={["name", "created_at", "updated_at"]}
|
||||
prefix={CUSTOMER_GROUP_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductTypeConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTypeConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_TYPE_PREFIX,
|
||||
})
|
||||
const { product_types, count, isLoading, isError, error } =
|
||||
useAdminProductTypes(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedTypes = (product_types?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as ProductType[]
|
||||
|
||||
if (addedTypes.length > 0) {
|
||||
const newConditions = addedTypes.map((p) => ({
|
||||
label: p.value,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductTypeConditionsTableColumns()
|
||||
const filters = useProductTypeConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: product_types ?? [],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_TYPE_PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
layout="fill"
|
||||
orderBy={["value", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_TYPE_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductCollectionConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductCollectionConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_COLLECTION_PREFIX,
|
||||
})
|
||||
const { collections, count, isLoading, isError, error } = useAdminCollections(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedCollections = (collections?.filter((p) =>
|
||||
added.includes(p.id!)
|
||||
) ?? []) as ProductCollection[]
|
||||
|
||||
if (addedCollections.length > 0) {
|
||||
const newConditions = addedCollections.map((p) => ({
|
||||
label: p.title,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductCollectionConditionsTableColumns()
|
||||
const filters = useProductCollectionConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: collections ?? [],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_COLLECTION_PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
layout="fill"
|
||||
orderBy={["title", "handle", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_COLLECTION_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductTagConditionsTable = ({
|
||||
onSave,
|
||||
selected = [],
|
||||
}: ConditionsProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(selected)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ConditionsOption[]>(selected)
|
||||
|
||||
const { searchParams, raw } = useProductTagConditionsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PRODUCT_TAG_PREFIX,
|
||||
})
|
||||
const { product_tags, count, isLoading, isError, error } =
|
||||
useAdminProductTags(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const added = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (added.length) {
|
||||
const addedTags = (product_tags?.filter((p) => added.includes(p.id!)) ??
|
||||
[]) as ProductTag[]
|
||||
|
||||
if (addedTags.length > 0) {
|
||||
const newConditions = addedTags.map((p) => ({
|
||||
label: p.value,
|
||||
value: p.id!,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) => p.value in newState)
|
||||
return Array.from(new Set([...filteredPrev, ...newConditions]))
|
||||
})
|
||||
}
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const removed = Object.keys(rowSelection).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
if (removed.length) {
|
||||
setIntermediate((prev) => {
|
||||
return prev.filter((p) => !removed.includes(p.value))
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(intermediate)
|
||||
}
|
||||
|
||||
const columns = useProductTagConditionsTableColumns()
|
||||
const filters = useProductTagConditionsTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: product_tags ?? [],
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
prefix: PRODUCT_TAG_PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
search
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
layout="fill"
|
||||
orderBy={["title", "handle", "created_at", "updated_at"]}
|
||||
prefix={PRODUCT_TAG_PREFIX}
|
||||
/>
|
||||
<ConditionsFooter onSave={handleSave} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ConditionsTableProps = {
|
||||
product: ConditionsProps
|
||||
productType: ConditionsProps
|
||||
productTag: ConditionsProps
|
||||
productCollection: ConditionsProps
|
||||
customerGroup: ConditionsProps
|
||||
selected: ConditionEntities | null
|
||||
}
|
||||
|
||||
export const ConditionsDrawer = ({
|
||||
product,
|
||||
productType,
|
||||
customerGroup,
|
||||
productCollection,
|
||||
productTag,
|
||||
selected,
|
||||
}: ConditionsTableProps) => {
|
||||
switch (selected) {
|
||||
case ConditionEntities.PRODUCT:
|
||||
return <ProductConditionsTable {...product} />
|
||||
case ConditionEntities.PRODUCT_TYPE:
|
||||
return <ProductTypeConditionsTable {...productType} />
|
||||
case ConditionEntities.PRODUCT_COLLECTION:
|
||||
return <ProductCollectionConditionsTable {...productCollection} />
|
||||
case ConditionEntities.PRODUCT_TAG:
|
||||
return <ProductTagConditionsTable {...productTag} />
|
||||
case ConditionEntities.CUSTOMER_GROUP:
|
||||
return <CustomerGroupConditionsTable {...customerGroup} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./conditions-drawer"
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum ConditionEntities {
|
||||
PRODUCT = "products",
|
||||
PRODUCT_TYPE = "product_types",
|
||||
PRODUCT_COLLECTION = "product_collections",
|
||||
PRODUCT_TAG = "product_tags",
|
||||
CUSTOMER_GROUP = "customer_groups",
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||
|
||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||
|
||||
export const useCustomerGroupConditionsTableColumns = () => {
|
||||
const base = useCustomerGroupTableColumns()
|
||||
|
||||
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]
|
||||
)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMemo } from "react"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { ProductCollection } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductCollection>()
|
||||
|
||||
export const useProductCollectionConditionsTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
columnHelper.accessor("handle", {
|
||||
header: t("fields.handle"),
|
||||
cell: ({ getValue }) => `/${getValue()}`,
|
||||
}),
|
||||
columnHelper.accessor("products", {
|
||||
header: t("fields.products"),
|
||||
cell: ({ getValue }) => {
|
||||
const count = getValue()?.length
|
||||
|
||||
return <span>{count || "-"}</span>
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
export const useProductConditionsTableColumns = () => {
|
||||
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]
|
||||
)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMemo } from "react"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { ProductTag } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductTag>()
|
||||
|
||||
export const useProductTagConditionsTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("value", {
|
||||
header: t("fields.value"),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { ProductType } from "@medusajs/medusa"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductType>()
|
||||
|
||||
export const useProductTypeConditionsTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("value", {
|
||||
header: t("fields.value"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useCustomerGroupConditionsTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
return filters
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductCollectionConditionsTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
return filters
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductTagConditionsTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
return filters
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useProductTypeConditionsTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
return filters
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { AdminGetCollectionsParams } from "@medusajs/medusa"
|
||||
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductCollectionConditionsTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"order",
|
||||
"title",
|
||||
"handle",
|
||||
"discount_condition_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const searchParams: AdminGetCollectionsParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset ? Number(raw.offset) : 0,
|
||||
q: raw.q,
|
||||
title: raw.title,
|
||||
handle: raw.handle,
|
||||
discount_condition_id: raw.discount_condition_id,
|
||||
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
|
||||
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
|
||||
order: raw.order,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { AdminGetProductTagsParams } from "@medusajs/medusa"
|
||||
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductTagConditionsTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"order",
|
||||
"value",
|
||||
"discount_condition_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const searchParams: AdminGetProductTagsParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset ? Number(raw.offset) : 0,
|
||||
q: raw.q,
|
||||
discount_condition_id: raw.discount_condition_id,
|
||||
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
|
||||
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
|
||||
order: raw.order,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { AdminGetProductTypesParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useProductTypeConditionsTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["offset", "q", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const searchParams: AdminGetProductTypesParams = {
|
||||
limit: pageSize,
|
||||
offset: raw.offset ? Number(raw.offset) : 0,
|
||||
q: raw.q,
|
||||
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
|
||||
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
|
||||
order: raw.order,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ConditionEntities } from "./constants"
|
||||
|
||||
export type ConditionsOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type ConditionsState = {
|
||||
[K in ConditionEntities]: boolean
|
||||
}
|
||||
+23
-14
@@ -1,23 +1,25 @@
|
||||
import { useEffect, useMemo } from "react"
|
||||
import {
|
||||
Checkbox,
|
||||
CurrencyInput,
|
||||
DatePicker,
|
||||
Heading,
|
||||
Input,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
Textarea,
|
||||
RadioGroup,
|
||||
CurrencyInput,
|
||||
} from "@medusajs/ui"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { useAdminRegions } from "medusa-react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { CreateDiscountFormReturn } from "./create-discount-form"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||
import { CreateDiscountFormReturn } from "./create-discount-form"
|
||||
import { DiscountRuleType } from "./types"
|
||||
|
||||
type CreateDiscountPropsProps = {
|
||||
@@ -29,8 +31,14 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
|
||||
const { regions } = useAdminRegions()
|
||||
|
||||
const watchType = form.watch("type")
|
||||
const watchRegion = form.watch("regions")
|
||||
const watchType = useWatch({
|
||||
control: form.control,
|
||||
name: "type",
|
||||
})
|
||||
const watchRegion = useWatch({
|
||||
name: "regions",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const isFixedDiscount = watchType === DiscountRuleType.FIXED
|
||||
const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING
|
||||
@@ -43,10 +51,12 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
return regions.find((r) => r.id === watchRegion[0])
|
||||
}, [regions, watchRegion])
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("regions", [])
|
||||
form.setValue("value", undefined)
|
||||
}, [watchType])
|
||||
setValue("regions", [])
|
||||
setValue("value", undefined)
|
||||
}, [watchType, setValue])
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col items-center overflow-auto p-16">
|
||||
@@ -197,9 +207,8 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
<Input key="placeholder" disabled />
|
||||
)
|
||||
) : (
|
||||
<Input
|
||||
<PercentageInput
|
||||
key="amount"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
@@ -268,7 +277,7 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
||||
{t("discounts.isTemplateDiscount")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
{/*<Form.ErrorMessage />*/}
|
||||
{/* <Form.ErrorMessage />*/}
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
+52
-23
@@ -1,27 +1,64 @@
|
||||
import type { Discount, DiscountCondition } from "@medusajs/medusa"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import type {
|
||||
CustomerGroup,
|
||||
Discount,
|
||||
DiscountCondition,
|
||||
Product,
|
||||
ProductCollection,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
} from "@medusajs/medusa"
|
||||
import { Badge, Container, Heading } from "@medusajs/ui"
|
||||
import { useMemo } from "react"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
|
||||
type ConditionTypeProps = {
|
||||
condition: DiscountCondition
|
||||
}
|
||||
|
||||
const N = 2
|
||||
const N = 1
|
||||
|
||||
const hasTitle = (p: unknown): p is Product | ProductCollection => {
|
||||
return (p as { title: string }).title !== undefined
|
||||
}
|
||||
|
||||
const hasName = (p: unknown): p is CustomerGroup => {
|
||||
return (p as { name: string }).name !== undefined
|
||||
}
|
||||
|
||||
const hasValue = (p: unknown): p is ProductTag | ProductType => {
|
||||
return (p as { value: string }).value !== undefined
|
||||
}
|
||||
|
||||
function ConditionType({ condition }: ConditionTypeProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const operator = condition.operator === "in" ? "including" : "excluding"
|
||||
const entity = condition.type
|
||||
|
||||
const list = condition[entity].map((p) => {
|
||||
if (hasTitle(p)) {
|
||||
return p.title
|
||||
}
|
||||
|
||||
if (hasName(p)) {
|
||||
return p.name
|
||||
}
|
||||
|
||||
if (hasValue(p)) {
|
||||
return p.value
|
||||
}
|
||||
}) as string[]
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-borders-base flex flex-row justify-around rounded-md p-2">
|
||||
<span className="text-ui-fg-subtle txt-small">
|
||||
<div className="bg-ui-bg-subtle shadow-borders-base flex justify-around rounded-md p-2">
|
||||
<span className="text-ui-fg-subtle txt-compact-xsmall whitespace-nowrap">
|
||||
<Trans
|
||||
t={t}
|
||||
count={
|
||||
condition[entity].length > N
|
||||
? N - condition[entity].length
|
||||
@@ -29,22 +66,19 @@ function ConditionType({ condition }: ConditionTypeProps) {
|
||||
}
|
||||
i18nKey={`discounts.conditions.${operator}.${entity}`}
|
||||
components={[
|
||||
<span
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
key="discounts-incl"
|
||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
||||
className="mx-1 max-w-[120px]"
|
||||
>
|
||||
<ListSummary
|
||||
inline
|
||||
n={N}
|
||||
list={condition[entity].map(
|
||||
(p) => p.title || p.name || p.value
|
||||
)}
|
||||
list={list}
|
||||
className="!txt-compact-xsmall-plus"
|
||||
/>
|
||||
</span>,
|
||||
<span
|
||||
key="discounts-excl"
|
||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
||||
/>,
|
||||
</Badge>,
|
||||
<Badge size="2xsmall" key="discounts-excl" className="mx-1" />,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
@@ -88,12 +122,7 @@ export const DiscountConditionsSection = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
||||
{!conditions.length && (
|
||||
<NoRecords
|
||||
className="h-[180px]"
|
||||
action={{ label: t("discounts.editConditions"), to: "conditions" }}
|
||||
/>
|
||||
)}
|
||||
{!conditions.length && <NoRecords className="h-[180px]" />}
|
||||
{conditions.map((condition) => (
|
||||
<ConditionType key={condition.id} condition={condition} />
|
||||
))}
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import { parse } from "iso8601-duration"
|
||||
import { format, formatDuration } from "date-fns"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { format, formatDuration } from "date-fns"
|
||||
import { parse } from "iso8601-duration"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
@@ -13,9 +13,9 @@ type DiscountConfigurationsSection = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
function formatTime(dateTime?: string) {
|
||||
function formatTime(dateTime?: string | Date | null) {
|
||||
if (!dateTime) {
|
||||
return
|
||||
return "-"
|
||||
}
|
||||
return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss")
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export const DiscountConfigurationSection = ({
|
||||
{t("discounts.startDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{formatTime(discount.starts_at as unknown as string)}
|
||||
{formatTime(discount.starts_at)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
@@ -65,7 +65,7 @@ export const DiscountConfigurationSection = ({
|
||||
{t("discounts.endDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{formatTime(discount.ends_at as unknown as string)}
|
||||
{formatTime(discount.ends_at)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
+2
-2
@@ -60,7 +60,7 @@ export const DiscountGeneralSection = ({
|
||||
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
|
||||
}[getDiscountStatus(discount)] as [
|
||||
"grey" | "orange" | "green" | "red",
|
||||
string
|
||||
string,
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -101,7 +101,7 @@ export const DiscountGeneralSection = ({
|
||||
{t("fields.description")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{discount.rule.description}
|
||||
{discount.rule.description || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
+6
-6
@@ -2,16 +2,16 @@ import { useAdminDiscount } from "medusa-react"
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { DiscountGeneralSection } from "./components/discounts-general-section"
|
||||
import { DiscountConfigurationSection } from "./components/discounts-configurations-section"
|
||||
import { DiscountConfigurationSection } from "./components/discount-configurations-section"
|
||||
import { DiscountGeneralSection } from "./components/discount-general-section"
|
||||
|
||||
import { discountLoader, expand } from "./loader"
|
||||
import { RedemptionsSection } from "./components/redemptions-section"
|
||||
import { DetailsSection } from "./components/details-section"
|
||||
import { DiscountConditionsSection } from "./components/discounts-conditions-section"
|
||||
import { DiscountConditionsSection } from "./components/discount-conditions-section"
|
||||
import { RedemptionsSection } from "./components/discount-redemptions-section"
|
||||
import { discountLoader, expand } from "./loader"
|
||||
|
||||
import before from "medusa-admin:widgets/discount/details/before"
|
||||
import after from "medusa-admin:widgets/discount/details/after"
|
||||
import before from "medusa-admin:widgets/discount/details/before"
|
||||
|
||||
export const DiscountDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export { discountLoader as loader } from "./loader"
|
||||
export { DiscountDetail as Component } from "./discount-detail.tsx"
|
||||
export { discountLoader as loader } from "./loader.ts"
|
||||
+583
@@ -0,0 +1,583 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
import {
|
||||
Discount,
|
||||
DiscountCondition,
|
||||
DiscountConditionType,
|
||||
} from "@medusajs/medusa"
|
||||
import { Button, DropdownMenu, Heading, Text } from "@medusajs/ui"
|
||||
import { adminDiscountKeys } from "medusa-react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { medusa, queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import { ConditionsDrawer } from "../../../common/components/conditions-drawer"
|
||||
import { ConditionEntities } from "../../../common/constants"
|
||||
import { ConditionsOption } from "../../../common/types"
|
||||
|
||||
enum DiscountConditionOperator {
|
||||
IN = "in",
|
||||
NOT_IN = "not_in",
|
||||
}
|
||||
|
||||
type ConditionProps = {
|
||||
labels: string[]
|
||||
isInOperator: boolean
|
||||
type: ConditionEntities
|
||||
onClick: (op: DiscountConditionOperator) => void
|
||||
}
|
||||
|
||||
type EditDiscountFormProps = {
|
||||
discount: Discount
|
||||
}
|
||||
|
||||
const N = 1
|
||||
|
||||
const getDefault = (conditionType: ConditionEntities, labelProp: string) => {
|
||||
return (conditions: DiscountCondition[]) => {
|
||||
const condition = conditions.find(
|
||||
(c) => c.type === (conditionType as unknown as DiscountConditionType)
|
||||
)
|
||||
|
||||
if (!condition || !condition[conditionType]) {
|
||||
return []
|
||||
}
|
||||
|
||||
return condition[conditionType].map((p) => ({
|
||||
label: p[labelProp as keyof typeof p] as string,
|
||||
value: p.id,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const getOperator = (conditionType: ConditionEntities) => {
|
||||
return (conditions: DiscountCondition[]) => {
|
||||
return conditions.find((c) => c.type === (conditionType as any))?.operator
|
||||
}
|
||||
}
|
||||
|
||||
const SelectedConditionTypesSchema = zod.object({
|
||||
[ConditionEntities.PRODUCT]: zod.boolean().optional(),
|
||||
[ConditionEntities.PRODUCT_TAG]: zod.boolean().optional(),
|
||||
[ConditionEntities.PRODUCT_TYPE]: zod.boolean().optional(),
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: zod.boolean().optional(),
|
||||
[ConditionEntities.CUSTOMER_GROUP]: zod.boolean().optional(),
|
||||
})
|
||||
|
||||
type SelectedContionTypeKey =
|
||||
(typeof ConditionEntities)[keyof typeof ConditionEntities]
|
||||
|
||||
const EditDiscountConditionsSchema = zod.object({
|
||||
products: zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
),
|
||||
product_types: zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
),
|
||||
customer_groups: zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
),
|
||||
product_collections: zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
),
|
||||
product_tags: zod.array(
|
||||
zod.object({
|
||||
label: zod.string(),
|
||||
value: zod.string(),
|
||||
})
|
||||
),
|
||||
products_operator: zod.nativeEnum(DiscountConditionOperator).optional(),
|
||||
product_tags_operator: zod.nativeEnum(DiscountConditionOperator).optional(),
|
||||
product_collections_operator: zod
|
||||
.nativeEnum(DiscountConditionOperator)
|
||||
.optional(),
|
||||
product_types_operator: zod.nativeEnum(DiscountConditionOperator).optional(),
|
||||
customer_groups_operator: zod
|
||||
.nativeEnum(DiscountConditionOperator)
|
||||
.optional(),
|
||||
selected_condition_types: SelectedConditionTypesSchema,
|
||||
})
|
||||
|
||||
export const EditDiscountConditionsForm = ({
|
||||
discount,
|
||||
}: EditDiscountFormProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
|
||||
const [conditionType, setConditionType] = useState<ConditionEntities | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const [, setSearchParams] = useSearchParams()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const conditions = discount.rule.conditions?.sort((c1, c2) =>
|
||||
c1.type.localeCompare(c2.type)
|
||||
) // TODO: memo
|
||||
|
||||
const form = useForm<zod.infer<typeof EditDiscountConditionsSchema>>({
|
||||
defaultValues: {
|
||||
products: getDefault(
|
||||
ConditionEntities.PRODUCT,
|
||||
"title"
|
||||
)(discount.rule.conditions),
|
||||
products_operator: getOperator(ConditionEntities.PRODUCT)(
|
||||
discount.rule.conditions
|
||||
),
|
||||
product_types: getDefault(
|
||||
ConditionEntities.PRODUCT_TYPE,
|
||||
"value"
|
||||
)(discount.rule.conditions),
|
||||
product_types_operator: getOperator(ConditionEntities.PRODUCT_TYPE)(
|
||||
discount.rule.conditions
|
||||
),
|
||||
product_collections: getDefault(
|
||||
ConditionEntities.PRODUCT_COLLECTION,
|
||||
"title"
|
||||
)(discount.rule.conditions),
|
||||
product_collections_operator: getOperator(
|
||||
ConditionEntities.PRODUCT_COLLECTION
|
||||
)(discount.rule.conditions),
|
||||
product_tags: getDefault(
|
||||
ConditionEntities.PRODUCT_TAG,
|
||||
"value"
|
||||
)(discount.rule.conditions),
|
||||
product_tags_operator: getOperator(ConditionEntities.PRODUCT_TAG)(
|
||||
discount.rule.conditions
|
||||
),
|
||||
customer_groups: getDefault(
|
||||
ConditionEntities.CUSTOMER_GROUP,
|
||||
"name"
|
||||
)(discount.rule.conditions),
|
||||
customer_groups_operator: getOperator(ConditionEntities.CUSTOMER_GROUP)(
|
||||
discount.rule.conditions
|
||||
),
|
||||
selected_condition_types: {
|
||||
[ConditionEntities.CUSTOMER_GROUP]: !!conditions.find(
|
||||
(c) => c.type === (ConditionEntities.CUSTOMER_GROUP as any)
|
||||
),
|
||||
[ConditionEntities.PRODUCT_TYPE]: !!conditions.find(
|
||||
(c) => c.type === (ConditionEntities.PRODUCT_TYPE as any)
|
||||
),
|
||||
[ConditionEntities.PRODUCT_TAG]: !!conditions.find(
|
||||
(c) => c.type === (ConditionEntities.PRODUCT_TAG as any)
|
||||
),
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: !!conditions.find(
|
||||
(c) => c.type === (ConditionEntities.PRODUCT_COLLECTION as any)
|
||||
),
|
||||
[ConditionEntities.PRODUCT]: !!conditions.find(
|
||||
(c) => c.type === (ConditionEntities.PRODUCT as any)
|
||||
),
|
||||
},
|
||||
},
|
||||
resolver: zodResolver(EditDiscountConditionsSchema),
|
||||
})
|
||||
|
||||
const toggleSelectedConditionTypes = (type: ConditionEntities) => {
|
||||
const state = { ...form.getValues().selected_condition_types }
|
||||
if (state[type]) {
|
||||
delete state[type]
|
||||
} else {
|
||||
state[type] = true
|
||||
}
|
||||
|
||||
form.setValue("selected_condition_types", state, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllSelectedConditions = () => {
|
||||
form.setValue(
|
||||
"selected_condition_types",
|
||||
{
|
||||
[ConditionEntities.PRODUCT]: false,
|
||||
[ConditionEntities.PRODUCT_TAG]: false,
|
||||
[ConditionEntities.PRODUCT_TYPE]: false,
|
||||
[ConditionEntities.PRODUCT_COLLECTION]: false,
|
||||
[ConditionEntities.CUSTOMER_GROUP]: false,
|
||||
},
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const selectedConditionTypes = useWatch({
|
||||
name: "selected_condition_types",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const selectedTypes = Object.keys(selectedConditionTypes)
|
||||
.filter(
|
||||
(k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes]
|
||||
)
|
||||
.sort() as ConditionEntities[]
|
||||
|
||||
const selectedProducts = useWatch({
|
||||
control: form.control,
|
||||
name: "products",
|
||||
})
|
||||
|
||||
const selectedProductTypes = useWatch({
|
||||
control: form.control,
|
||||
name: "product_types",
|
||||
})
|
||||
|
||||
const selectedCustomerGroups = useWatch({
|
||||
control: form.control,
|
||||
name: "customer_groups",
|
||||
})
|
||||
|
||||
const selectedProductCollections = useWatch({
|
||||
control: form.control,
|
||||
name: "product_collections",
|
||||
})
|
||||
|
||||
const selectedProductTags = useWatch({
|
||||
control: form.control,
|
||||
name: "product_tags",
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
setIsLoading(true)
|
||||
|
||||
const handleCondition = async (selectedType: string) => {
|
||||
if (
|
||||
!selectedConditionTypes[
|
||||
selectedType as keyof typeof selectedConditionTypes
|
||||
]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentCondition = conditions.find((c) => c.type === selectedType)
|
||||
|
||||
if (currentCondition) {
|
||||
// delete condition first (in case operator changed since we cannot update that trough API)
|
||||
await medusa.admin.discounts.deleteCondition(
|
||||
discount.id,
|
||||
currentCondition.id
|
||||
)
|
||||
}
|
||||
|
||||
const payload = data[selectedType as SelectedContionTypeKey]
|
||||
|
||||
if (payload?.length) {
|
||||
await medusa.admin.discounts.createCondition(discount.id, {
|
||||
operator: data[
|
||||
`${selectedType}_operator` as keyof typeof data
|
||||
] as DiscountConditionOperator,
|
||||
[selectedType]: payload.map((d) => d.value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUnselectedConditions = async () => {
|
||||
const unselectedConditions = conditions.filter(
|
||||
(c) => !selectedConditionTypes[c.type]
|
||||
)
|
||||
const deletePromises = unselectedConditions.map((c) =>
|
||||
medusa.admin.discounts.deleteCondition(discount.id, c.id)
|
||||
)
|
||||
await Promise.all(deletePromises)
|
||||
}
|
||||
|
||||
for (const selectedType of Object.keys(selectedConditionTypes)) {
|
||||
await handleCondition(selectedType)
|
||||
}
|
||||
|
||||
await deleteUnselectedConditions()
|
||||
|
||||
await queryClient.invalidateQueries(adminDiscountKeys.detail(discount.id))
|
||||
|
||||
setIsLoading(false)
|
||||
handleSuccess()
|
||||
})
|
||||
|
||||
const handleOpenDrawer = (
|
||||
type: ConditionEntities,
|
||||
operator: DiscountConditionOperator
|
||||
) => {
|
||||
form.setValue(`${type}_operator`, operator)
|
||||
setConditionType(type)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveConditions = (type: ConditionEntities) => {
|
||||
return (options: ConditionsOption[]) => {
|
||||
form.setValue(type, options, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setConditionType(null)
|
||||
setSearchParams(
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
|
||||
<SplitView open={open} onOpenChange={handleOpenChange}>
|
||||
<SplitView.Content>
|
||||
<div className="flex size-full flex-col items-center px-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
|
||||
<div>
|
||||
<Heading>{t("discounts.conditions.editHeader")}</Heading>
|
||||
<Text
|
||||
size="small"
|
||||
className="text-ui-fg-subtle text-pretty"
|
||||
>
|
||||
{t("discounts.conditions.editHint")}
|
||||
</Text>
|
||||
</div>
|
||||
{selectedTypes.length > 0 && (
|
||||
<div className="flex flex-col items-start gap-y-4">
|
||||
{selectedTypes.map((t) => {
|
||||
if (t in selectedConditionTypes) {
|
||||
const field = form.getValues(t)
|
||||
const operator = form.getValues(`${t}_operator`)
|
||||
|
||||
return field ? (
|
||||
<Condition
|
||||
key={t}
|
||||
type={t}
|
||||
labels={field.map((f) => f.label)}
|
||||
isInOperator={operator === "in"}
|
||||
onClick={(op) => handleOpenDrawer(t, op)}
|
||||
/>
|
||||
) : (
|
||||
<Condition
|
||||
key={t}
|
||||
type={t}
|
||||
isInOperator
|
||||
labels={[]}
|
||||
onClick={(op) => handleOpenDrawer(t, op)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-2">
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
onOpenChange={(v) => {
|
||||
v && setIsDropdownOpen(v)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("discounts.conditions.manageTypesAction")}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onInteractOutside={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
{Object.values(ConditionEntities).map((type) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={type}
|
||||
checked={selectedConditionTypes[type]}
|
||||
onCheckedChange={() =>
|
||||
toggleSelectedConditionTypes(type)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
size="small"
|
||||
weight={
|
||||
selectedConditionTypes[type]
|
||||
? "plus"
|
||||
: "regular"
|
||||
}
|
||||
>
|
||||
{t(`fields.${type}`)}
|
||||
</Text>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
{selectedTypes.length > 0 && (
|
||||
<Button
|
||||
variant="transparent"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={clearAllSelectedConditions}
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||
>
|
||||
{t("actions.clearAll")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SplitView.Content>
|
||||
<SplitView.Drawer>
|
||||
<ConditionsDrawer
|
||||
product={{
|
||||
selected: selectedProducts,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT),
|
||||
}}
|
||||
productType={{
|
||||
selected: selectedProductTypes,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TYPE),
|
||||
}}
|
||||
productCollection={{
|
||||
selected: selectedProductCollections,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.PRODUCT_COLLECTION
|
||||
),
|
||||
}}
|
||||
productTag={{
|
||||
selected: selectedProductTags,
|
||||
onSave: handleSaveConditions(ConditionEntities.PRODUCT_TAG),
|
||||
}}
|
||||
customerGroup={{
|
||||
selected: selectedCustomerGroups,
|
||||
onSave: handleSaveConditions(
|
||||
ConditionEntities.CUSTOMER_GROUP
|
||||
),
|
||||
}}
|
||||
selected={conditionType}
|
||||
/>
|
||||
</SplitView.Drawer>
|
||||
</SplitView>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
function Condition({ labels, isInOperator, type, onClick }: ConditionProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isInButtonDisabled = !!labels.length && !isInOperator
|
||||
const isExButtonDisabled = !!labels.length && isInOperator
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="bg-ui-bg-field shadow-borders-base inline-flex items-center divide-x overflow-hidden rounded-md">
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-muted shrink-0 px-2 py-1"
|
||||
>
|
||||
{t("discounts.conditions.edit.appliesTo")}
|
||||
</Text>
|
||||
<div className="text-ui-fg-subtle max-w-[240px] shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="transparent"
|
||||
size="small"
|
||||
disabled={isInButtonDisabled}
|
||||
onClick={() => onClick("in" as DiscountConditionOperator)}
|
||||
className="txt-compact-small-plus disabled:text-ui-fg-subtle rounded-none"
|
||||
>
|
||||
{isInOperator && labels.length ? (
|
||||
<ListSummary
|
||||
inline
|
||||
n={N}
|
||||
className="!txt-compact-small-plus max-w-[200px]"
|
||||
list={labels}
|
||||
/>
|
||||
) : (
|
||||
t(labels.length ? "fields.all" : "fields.none")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-muted shrink-0 px-2 py-1"
|
||||
>
|
||||
{t(`discounts.conditions.edit.except.${type}`, {
|
||||
count: labels.length > 1 ? labels.length - 1 : labels.length,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<div className="text-ui-fg-subtle shrink-0 overflow-hidden">
|
||||
<Button
|
||||
type="button"
|
||||
variant="transparent"
|
||||
size="small"
|
||||
disabled={isExButtonDisabled}
|
||||
onClick={() => onClick("not_in" as DiscountConditionOperator)}
|
||||
className="txt-compact-small-plus disabled:text-ui-fg-subtle max-w-[200px] rounded-none"
|
||||
>
|
||||
{!isInOperator && labels.length ? (
|
||||
<ListSummary
|
||||
inline
|
||||
n={N}
|
||||
className="!txt-compact-small-plus"
|
||||
list={labels}
|
||||
/>
|
||||
) : (
|
||||
t("fields.none")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-discount-conditions-form"
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { useAdminDiscount } from "medusa-react"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/route-modal/index.ts"
|
||||
import { expand } from "../discount-detail/loader.ts"
|
||||
import { EditDiscountConditionsForm } from "./components/edit-discount-form/index.ts"
|
||||
|
||||
export const DiscountEditConditions = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { discount, isLoading, isError, error } = useAdminDiscount(id!, {
|
||||
expand,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{!isLoading && discount && (
|
||||
<EditDiscountConditionsForm discount={discount} />
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DiscountEditConditions as Component } from "./discount-edit-conditions"
|
||||
+61
-33
@@ -1,13 +1,13 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { formatISODuration } from "date-fns"
|
||||
import { Duration, parse } from "iso8601-duration"
|
||||
import { useMemo } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { parse, Duration } from "iso8601-duration"
|
||||
import { formatISODuration } from "date-fns"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import { Button, Input, Text, Switch, DatePicker } from "@medusajs/ui"
|
||||
import { Button, DatePicker, Input, Switch, Text } from "@medusajs/ui"
|
||||
import { useAdminUpdateDiscount } from "medusa-react"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
@@ -29,15 +29,15 @@ const EditDiscountSchema = zod.object({
|
||||
end_date: zod.date().nullish(),
|
||||
|
||||
enable_usage_limit: zod.boolean(),
|
||||
usage_limit: zod.number().nullish(),
|
||||
usage_limit: zod.union([zod.string(), zod.number()]).optional(),
|
||||
|
||||
enable_duration: zod.boolean(),
|
||||
|
||||
years: zod.number().optional(),
|
||||
months: zod.number().optional(),
|
||||
days: zod.number().optional(),
|
||||
hours: zod.number().optional(),
|
||||
minutes: zod.number().optional(),
|
||||
years: zod.union([zod.string(), zod.number()]).optional(),
|
||||
months: zod.union([zod.string(), zod.number()]).optional(),
|
||||
days: zod.union([zod.string(), zod.number()]).optional(),
|
||||
hours: zod.union([zod.string(), zod.number()]).optional(),
|
||||
minutes: zod.union([zod.string(), zod.number()]).optional(),
|
||||
})
|
||||
|
||||
export const EditDiscountConfigurationForm = ({
|
||||
@@ -46,13 +46,11 @@ export const EditDiscountConfigurationForm = ({
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const duration = useMemo(
|
||||
() =>
|
||||
discount.valid_duration
|
||||
? parse(discount.valid_duration)
|
||||
: ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration),
|
||||
[discount]
|
||||
)
|
||||
const duration = useMemo(() => {
|
||||
return discount.valid_duration
|
||||
? parse(discount.valid_duration)
|
||||
: ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration)
|
||||
}, [discount])
|
||||
|
||||
const form = useForm<zod.infer<typeof EditDiscountSchema>>({
|
||||
defaultValues: {
|
||||
@@ -60,37 +58,66 @@ export const EditDiscountConfigurationForm = ({
|
||||
start_date: new Date(discount.starts_at),
|
||||
|
||||
enable_usage_limit: !!discount.usage_limit,
|
||||
usage_limit: discount.usage_limit,
|
||||
usage_limit: discount.usage_limit || "",
|
||||
|
||||
enable_duration: !!discount.valid_duration,
|
||||
|
||||
end_date_enabled: !!discount.ends_at,
|
||||
end_date: discount.ends_at ? new Date(discount.ends_at) : null,
|
||||
|
||||
years: duration.years,
|
||||
months: duration.months,
|
||||
days: duration.days,
|
||||
hours: duration.hours,
|
||||
minutes: duration.minutes,
|
||||
years: duration.years || "",
|
||||
months: duration.months || "",
|
||||
days: duration.days || "",
|
||||
hours: duration.hours || "",
|
||||
minutes: duration.minutes || "",
|
||||
},
|
||||
resolver: zodResolver(EditDiscountSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
|
||||
|
||||
const formatDuration = (raw: Record<string, number | string>) => {
|
||||
const isEmpty = Object.values(raw).every((v) => !v)
|
||||
|
||||
if (isEmpty) {
|
||||
return null
|
||||
}
|
||||
|
||||
const durationObject = Object.entries(raw).reduce((acc, [key, value]) => {
|
||||
if (value) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: Number(value),
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as Duration)
|
||||
|
||||
return formatISODuration(durationObject)
|
||||
}
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const duration = pick(data, ["years", "months", "days", "hours", "minutes"])
|
||||
const isDurationEmpty = Object.values(duration).every((v) => !v)
|
||||
const rawDuration = pick(data, [
|
||||
"years",
|
||||
"months",
|
||||
"days",
|
||||
"hours",
|
||||
"minutes",
|
||||
]) as Record<string, number | string>
|
||||
|
||||
const duration = formatDuration(rawDuration)
|
||||
|
||||
const usageLimit =
|
||||
data.enable_usage_limit && !isNaN(Number(data.usage_limit))
|
||||
? Number(data.usage_limit)
|
||||
: null
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
starts_at: data.start_date,
|
||||
ends_at: data.end_date_enabled ? data.end_date : null,
|
||||
usage_limit: data.enable_usage_limit ? data.usage_limit : null,
|
||||
valid_duration:
|
||||
data.enable_duration && !isDurationEmpty
|
||||
? formatISODuration(duration)
|
||||
: null,
|
||||
usage_limit: usageLimit,
|
||||
valid_duration: data.enable_duration && duration ? duration : null,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -254,7 +281,7 @@ export const EditDiscountConfigurationForm = ({
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_limit"
|
||||
render={({ field }) => {
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
@@ -262,13 +289,14 @@ export const EditDiscountConfigurationForm = ({
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
field.onChange(null)
|
||||
onChange(null)
|
||||
} else {
|
||||
field.onChange(Number(value))
|
||||
onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
+6
-6
@@ -1,29 +1,30 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Discount } from "@medusajs/medusa"
|
||||
import {
|
||||
CurrencyInput,
|
||||
Button,
|
||||
CurrencyInput,
|
||||
Input,
|
||||
Select,
|
||||
Text,
|
||||
Textarea,
|
||||
Select,
|
||||
} from "@medusajs/ui"
|
||||
import { useAdminRegions, useAdminUpdateDiscount } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||
import {
|
||||
getDbAmount,
|
||||
getPresentationalAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
|
||||
type EditDiscountFormProps = {
|
||||
discount: Discount
|
||||
@@ -186,9 +187,8 @@ export const EditDiscountDetailsForm = ({
|
||||
{...field}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
<PercentageInput
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DiscountsList as Component } from "./discount-list"
|
||||
export { discountsLoader } from "./loader"
|
||||
@@ -1,2 +0,0 @@
|
||||
export { discountsLoader } from "./loader"
|
||||
export { DiscountsList as Component } from "./list"
|
||||
@@ -125,7 +125,7 @@ const DrawerFooter = ({
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"border-ui-border-base flex items-center justify-end space-x-2 overflow-y-scroll border-t px-6 py-4",
|
||||
"border-ui-border-base flex items-center justify-end space-x-2 overflow-y-auto border-t px-6 py-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user