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:
1 parent
9288f53327
commit
a1b4aff127
61 files changed
+2078
-140
No files matched your search
@@ -309,7 +309,7 @@
|
|||||||
"discounts": {
|
"discounts": {
|
||||||
"domain": "Discounts",
|
"domain": "Discounts",
|
||||||
"startDate": "Start date",
|
"startDate": "Start date",
|
||||||
"createDiscountTitle": "Create discount",
|
"createDiscountTitle": "Create Discount",
|
||||||
"validDuration": "Duration of the discount",
|
"validDuration": "Duration of the discount",
|
||||||
"redemptionsLimit": "Redemptions limit",
|
"redemptionsLimit": "Redemptions limit",
|
||||||
"endDate": "End date",
|
"endDate": "End date",
|
||||||
@@ -320,8 +320,8 @@
|
|||||||
"fixedAmount": "Fixed amount",
|
"fixedAmount": "Fixed amount",
|
||||||
"validRegions": "Valid regions",
|
"validRegions": "Valid regions",
|
||||||
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
|
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
|
||||||
"editDiscountDetails": "Edit discount details",
|
"editDiscountDetails": "Edit Discount Details",
|
||||||
"editDiscountConfiguration": "Edit discount configuration",
|
"editDiscountConfiguration": "Edit Discount Configuration",
|
||||||
"hasStartDate": "Discount has a start date",
|
"hasStartDate": "Discount has a start date",
|
||||||
"hasEndDate": "Discount has an expiry date",
|
"hasEndDate": "Discount has an expiry date",
|
||||||
"startDateHint": "Schedule the discount to activate in the future.",
|
"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.",
|
"codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.",
|
||||||
"hasUsageLimit": "Limit the number of redemptions?",
|
"hasUsageLimit": "Limit the number of redemptions?",
|
||||||
"usageLimitHint": "Limit applies across all customers, not per customer.",
|
"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",
|
"hasDurationLimit": "Availability duration",
|
||||||
"durationHint": "Set the duration of the discount",
|
"durationHint": "Set the duration of the discount",
|
||||||
"chooseValidRegions": "Choose valid regions",
|
"chooseValidRegions": "Choose valid regions",
|
||||||
"noConditions": "No conditions are defined for this discount.",
|
|
||||||
"editConditions": "Edit conditions",
|
|
||||||
"conditionsHint": "Create conditions to apply on the discount",
|
"conditionsHint": "Create conditions to apply on the discount",
|
||||||
"isTemplateDiscount": "Is this a template discount?",
|
"isTemplateDiscount": "Is this a template discount?",
|
||||||
"percentageDescription": "Discount applied in %",
|
"percentageDescription": "Discount applied in %",
|
||||||
"fixedDescription": "Amount discount",
|
"fixedDescription": "Amount discount",
|
||||||
"shippingDescription": "Override delivery amount",
|
"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.",
|
"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": {
|
"conditions": {
|
||||||
|
"editHeader": "Edit Discount Conditions",
|
||||||
|
"editHint": "Specify conditions for when the discount can be applied to a cart.",
|
||||||
|
"manageTypesAction": "Manage condition types",
|
||||||
"including": {
|
"including": {
|
||||||
"products_one": "Discount applies to <0/> product.",
|
"products_one": "Discount applies to <0/> product",
|
||||||
"products_other": "Discount applies to <0/> products.",
|
"products_other": "Discount applies to <0/> products",
|
||||||
"customer_groups_one": "Discount applies to <0/> customer group.",
|
"customer_groups_one": "Discount applies to <0/> customer group",
|
||||||
"customer_groups_other": "Discount applies to <0/> customer groups.",
|
"customer_groups_other": "Discount applies to <0/> customer groups",
|
||||||
"product_tags_one": "Discount applies to <0/> tag.",
|
"product_tags_one": "Discount applies to <0/> tag",
|
||||||
"product_tags_other": "Discount applies to <0/> tags.",
|
"product_tags_other": "Discount applies to <0/> tags",
|
||||||
"product_collections_one": "Discount applies to <0/> product collection.",
|
"product_collections_one": "Discount applies to <0/> product collection",
|
||||||
"product_collections_other": "Discount applies to <0/> product collections.",
|
"product_collections_other": "Discount applies to <0/> product collections",
|
||||||
"product_types_one": "Discount applies to <0/> product type.",
|
"product_types_one": "Discount applies to <0/> product type",
|
||||||
"product_types_other": "Discount applies to <0/> product types."
|
"product_types_other": "Discount applies to <0/> product types"
|
||||||
},
|
},
|
||||||
"excluding": {
|
"excluding": {
|
||||||
"products": "Discount applies to <1>all</1> products except <0/>.",
|
"products": "Discount applies to <1>all</1> products except <0/>",
|
||||||
"customer_groups": "Discount applies to <1>all</1> customer groups 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_tags": "Discount applies to <1>all</1> product tags except <0/>",
|
||||||
"product_collections": "Discount applies to <1>all</1> product collections 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/>."
|
"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": {
|
"discountStatus": {
|
||||||
@@ -692,8 +708,14 @@
|
|||||||
"limit": "Limit",
|
"limit": "Limit",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
"none": "none",
|
||||||
|
"all": "all",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
"sales_channels": "Sales Channels",
|
"sales_channels": "Sales Channels",
|
||||||
|
"customer_groups": "Customer Groups",
|
||||||
|
"product_tags": "Product Tags",
|
||||||
|
"product_types": "Product Types",
|
||||||
|
"product_collections": "Product Collections",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Text, Tooltip, clx } from "@medusajs/ui"
|
import { Tooltip, clx } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
type ListSummaryProps = {
|
type ListSummaryProps = {
|
||||||
@@ -12,25 +12,42 @@ type ListSummaryProps = {
|
|||||||
*/
|
*/
|
||||||
list: string[]
|
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
|
* Determines whether the center text is truncated if there is no space in the container
|
||||||
*/
|
*/
|
||||||
inline?: boolean
|
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 { t } = useTranslation()
|
||||||
|
|
||||||
|
const title = t("general.plusCountMore", {
|
||||||
|
count: list.length - n,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx("text-ui-fg-subtle gap-x-2", {
|
className={clx(
|
||||||
|
"text-ui-fg-subtle txt-compact-small gap-x-1 overflow-hidden",
|
||||||
|
{
|
||||||
"inline-flex": inline,
|
"inline-flex": inline,
|
||||||
flex: !inline,
|
flex: !inline,
|
||||||
})}
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Text as="span" leading="compact" size="small" className="truncate">
|
<div className="flex-1 truncate">
|
||||||
{list.slice(0, n).join(", ")}
|
<span className="truncate">{list.slice(0, n).join(", ")}</span>
|
||||||
</Text>
|
</div>
|
||||||
{list.length > n && (
|
{list.length > n && (
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
<ul>
|
<ul>
|
||||||
@@ -40,18 +57,9 @@ export const ListSummary = ({ list, inline, n = 2 }: ListSummaryProps) => {
|
|||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text
|
<span className="cursor-default whitespace-nowrap">{title}</span>
|
||||||
as="span"
|
|
||||||
size="small"
|
|
||||||
weight="plus"
|
|
||||||
leading="compact"
|
|
||||||
className="cursor-default whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: list.length - n,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</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,
|
AdminCustomerGroupsRes,
|
||||||
AdminCustomersRes,
|
AdminCustomersRes,
|
||||||
AdminDraftOrdersRes,
|
AdminDraftOrdersRes,
|
||||||
|
AdminDiscountsRes,
|
||||||
AdminGiftCardsRes,
|
AdminGiftCardsRes,
|
||||||
AdminOrdersRes,
|
AdminOrdersRes,
|
||||||
AdminProductsRes,
|
AdminProductsRes,
|
||||||
@@ -430,25 +431,36 @@ export const v1Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () => import("../../routes/discounts/list"),
|
lazy: () => import("../../routes/discounts/discount-list"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "create",
|
path: "create",
|
||||||
lazy: () => import("../../routes/discounts/create"),
|
lazy: () => import("../../routes/discounts/discount-create"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () => import("../../routes/discounts/details"),
|
lazy: () => import("../../routes/discounts/discount-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminDiscountsRes) => data.discount.code,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () => import("../../routes/discounts/edit-details"),
|
lazy: () =>
|
||||||
|
import("../../routes/discounts/discount-edit-details"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "configuration",
|
path: "configuration",
|
||||||
lazy: () =>
|
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 {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
CurrencyInput,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
|
RadioGroup,
|
||||||
Select,
|
Select,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
RadioGroup,
|
|
||||||
CurrencyInput,
|
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
|
||||||
import { useAdminRegions } from "medusa-react"
|
import { useAdminRegions } from "medusa-react"
|
||||||
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { CreateDiscountFormReturn } from "./create-discount-form"
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
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 { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
|
import { CreateDiscountFormReturn } from "./create-discount-form"
|
||||||
import { DiscountRuleType } from "./types"
|
import { DiscountRuleType } from "./types"
|
||||||
|
|
||||||
type CreateDiscountPropsProps = {
|
type CreateDiscountPropsProps = {
|
||||||
@@ -29,8 +31,14 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
|||||||
|
|
||||||
const { regions } = useAdminRegions()
|
const { regions } = useAdminRegions()
|
||||||
|
|
||||||
const watchType = form.watch("type")
|
const watchType = useWatch({
|
||||||
const watchRegion = form.watch("regions")
|
control: form.control,
|
||||||
|
name: "type",
|
||||||
|
})
|
||||||
|
const watchRegion = useWatch({
|
||||||
|
name: "regions",
|
||||||
|
control: form.control,
|
||||||
|
})
|
||||||
|
|
||||||
const isFixedDiscount = watchType === DiscountRuleType.FIXED
|
const isFixedDiscount = watchType === DiscountRuleType.FIXED
|
||||||
const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING
|
const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING
|
||||||
@@ -43,10 +51,12 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
|||||||
return regions.find((r) => r.id === watchRegion[0])
|
return regions.find((r) => r.id === watchRegion[0])
|
||||||
}, [regions, watchRegion])
|
}, [regions, watchRegion])
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setValue("regions", [])
|
setValue("regions", [])
|
||||||
form.setValue("value", undefined)
|
setValue("value", undefined)
|
||||||
}, [watchType])
|
}, [watchType, setValue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col items-center overflow-auto p-16">
|
<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 key="placeholder" disabled />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<PercentageInput
|
||||||
key="amount"
|
key="amount"
|
||||||
type="number"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -268,7 +277,7 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
|
|||||||
{t("discounts.isTemplateDiscount")}
|
{t("discounts.isTemplateDiscount")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
</div>
|
</div>
|
||||||
{/*<Form.ErrorMessage />*/}
|
{/* <Form.ErrorMessage />*/}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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 { 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 { useMemo } from "react"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
|
||||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
|
||||||
type ConditionTypeProps = {
|
type ConditionTypeProps = {
|
||||||
condition: DiscountCondition
|
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) {
|
function ConditionType({ condition }: ConditionTypeProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const operator = condition.operator === "in" ? "including" : "excluding"
|
const operator = condition.operator === "in" ? "including" : "excluding"
|
||||||
const entity = condition.type
|
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 (
|
return (
|
||||||
<div className="bg-ui-bg-subtle shadow-borders-base flex flex-row justify-around rounded-md p-2">
|
<div className="bg-ui-bg-subtle shadow-borders-base flex justify-around rounded-md p-2">
|
||||||
<span className="text-ui-fg-subtle txt-small">
|
<span className="text-ui-fg-subtle txt-compact-xsmall whitespace-nowrap">
|
||||||
<Trans
|
<Trans
|
||||||
|
t={t}
|
||||||
count={
|
count={
|
||||||
condition[entity].length > N
|
condition[entity].length > N
|
||||||
? N - condition[entity].length
|
? N - condition[entity].length
|
||||||
@@ -29,22 +66,19 @@ function ConditionType({ condition }: ConditionTypeProps) {
|
|||||||
}
|
}
|
||||||
i18nKey={`discounts.conditions.${operator}.${entity}`}
|
i18nKey={`discounts.conditions.${operator}.${entity}`}
|
||||||
components={[
|
components={[
|
||||||
<span
|
<Badge
|
||||||
|
size="2xsmall"
|
||||||
key="discounts-incl"
|
key="discounts-incl"
|
||||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
className="mx-1 max-w-[120px]"
|
||||||
>
|
>
|
||||||
<ListSummary
|
<ListSummary
|
||||||
inline
|
inline
|
||||||
n={N}
|
n={N}
|
||||||
list={condition[entity].map(
|
list={list}
|
||||||
(p) => p.title || p.name || p.value
|
className="!txt-compact-xsmall-plus"
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</span>,
|
</Badge>,
|
||||||
<span
|
<Badge size="2xsmall" key="discounts-excl" className="mx-1" />,
|
||||||
key="discounts-excl"
|
|
||||||
className="bg-ui-tag-neutral-bg mx-1 rounded-md border p-1"
|
|
||||||
/>,
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -88,12 +122,7 @@ export const DiscountConditionsSection = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
||||||
{!conditions.length && (
|
{!conditions.length && <NoRecords className="h-[180px]" />}
|
||||||
<NoRecords
|
|
||||||
className="h-[180px]"
|
|
||||||
action={{ label: t("discounts.editConditions"), to: "conditions" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{conditions.map((condition) => (
|
{conditions.map((condition) => (
|
||||||
<ConditionType key={condition.id} condition={condition} />
|
<ConditionType key={condition.id} condition={condition} />
|
||||||
))}
|
))}
|
||||||
File renamed without changes.
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
import { parse } from "iso8601-duration"
|
|
||||||
import { format, formatDuration } from "date-fns"
|
|
||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { format, formatDuration } from "date-fns"
|
||||||
|
import { parse } from "iso8601-duration"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
import { Discount } from "@medusajs/medusa"
|
import { Discount } from "@medusajs/medusa"
|
||||||
@@ -13,9 +13,9 @@ type DiscountConfigurationsSection = {
|
|||||||
discount: Discount
|
discount: Discount
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(dateTime?: string) {
|
function formatTime(dateTime?: string | Date | null) {
|
||||||
if (!dateTime) {
|
if (!dateTime) {
|
||||||
return
|
return "-"
|
||||||
}
|
}
|
||||||
return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss")
|
return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss")
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ export const DiscountConfigurationSection = ({
|
|||||||
{t("discounts.startDate")}
|
{t("discounts.startDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
{formatTime(discount.starts_at as unknown as string)}
|
{formatTime(discount.starts_at)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
<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")}
|
{t("discounts.endDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
{formatTime(discount.ends_at as unknown as string)}
|
{formatTime(discount.ends_at)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
File renamed without changes.
+2
-2
@@ -60,7 +60,7 @@ export const DiscountGeneralSection = ({
|
|||||||
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
|
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
|
||||||
}[getDiscountStatus(discount)] as [
|
}[getDiscountStatus(discount)] as [
|
||||||
"grey" | "orange" | "green" | "red",
|
"grey" | "orange" | "green" | "red",
|
||||||
string
|
string,
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,7 +101,7 @@ export const DiscountGeneralSection = ({
|
|||||||
{t("fields.description")}
|
{t("fields.description")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
{discount.rule.description}
|
{discount.rule.description || "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+6
-6
@@ -2,16 +2,16 @@ import { useAdminDiscount } from "medusa-react"
|
|||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
import { DiscountGeneralSection } from "./components/discounts-general-section"
|
import { DiscountConfigurationSection } from "./components/discount-configurations-section"
|
||||||
import { DiscountConfigurationSection } from "./components/discounts-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 { 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 after from "medusa-admin:widgets/discount/details/after"
|
||||||
|
import before from "medusa-admin:widgets/discount/details/before"
|
||||||
|
|
||||||
export const DiscountDetail = () => {
|
export const DiscountDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
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 { DiscountDetail as Component } from "./discount-detail.tsx"
|
||||||
|
export { discountLoader as loader } from "./loader.ts"
|
||||||
File renamed without changes.
+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"
|
||||||
+60
-32
@@ -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 { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import { parse, Duration } from "iso8601-duration"
|
|
||||||
import { formatISODuration } from "date-fns"
|
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Discount } from "@medusajs/medusa"
|
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 { useAdminUpdateDiscount } from "medusa-react"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
@@ -29,15 +29,15 @@ const EditDiscountSchema = zod.object({
|
|||||||
end_date: zod.date().nullish(),
|
end_date: zod.date().nullish(),
|
||||||
|
|
||||||
enable_usage_limit: zod.boolean(),
|
enable_usage_limit: zod.boolean(),
|
||||||
usage_limit: zod.number().nullish(),
|
usage_limit: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
|
|
||||||
enable_duration: zod.boolean(),
|
enable_duration: zod.boolean(),
|
||||||
|
|
||||||
years: zod.number().optional(),
|
years: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
months: zod.number().optional(),
|
months: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
days: zod.number().optional(),
|
days: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
hours: zod.number().optional(),
|
hours: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
minutes: zod.number().optional(),
|
minutes: zod.union([zod.string(), zod.number()]).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const EditDiscountConfigurationForm = ({
|
export const EditDiscountConfigurationForm = ({
|
||||||
@@ -46,13 +46,11 @@ export const EditDiscountConfigurationForm = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const duration = useMemo(
|
const duration = useMemo(() => {
|
||||||
() =>
|
return discount.valid_duration
|
||||||
discount.valid_duration
|
|
||||||
? parse(discount.valid_duration)
|
? parse(discount.valid_duration)
|
||||||
: ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration),
|
: ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration)
|
||||||
[discount]
|
}, [discount])
|
||||||
)
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditDiscountSchema>>({
|
const form = useForm<zod.infer<typeof EditDiscountSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -60,37 +58,66 @@ export const EditDiscountConfigurationForm = ({
|
|||||||
start_date: new Date(discount.starts_at),
|
start_date: new Date(discount.starts_at),
|
||||||
|
|
||||||
enable_usage_limit: !!discount.usage_limit,
|
enable_usage_limit: !!discount.usage_limit,
|
||||||
usage_limit: discount.usage_limit,
|
usage_limit: discount.usage_limit || "",
|
||||||
|
|
||||||
enable_duration: !!discount.valid_duration,
|
enable_duration: !!discount.valid_duration,
|
||||||
|
|
||||||
end_date_enabled: !!discount.ends_at,
|
end_date_enabled: !!discount.ends_at,
|
||||||
end_date: discount.ends_at ? new Date(discount.ends_at) : null,
|
end_date: discount.ends_at ? new Date(discount.ends_at) : null,
|
||||||
|
|
||||||
years: duration.years,
|
years: duration.years || "",
|
||||||
months: duration.months,
|
months: duration.months || "",
|
||||||
days: duration.days,
|
days: duration.days || "",
|
||||||
hours: duration.hours,
|
hours: duration.hours || "",
|
||||||
minutes: duration.minutes,
|
minutes: duration.minutes || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(EditDiscountSchema),
|
resolver: zodResolver(EditDiscountSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
|
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 handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const duration = pick(data, ["years", "months", "days", "hours", "minutes"])
|
const rawDuration = pick(data, [
|
||||||
const isDurationEmpty = Object.values(duration).every((v) => !v)
|
"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(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
starts_at: data.start_date,
|
starts_at: data.start_date,
|
||||||
ends_at: data.end_date_enabled ? data.end_date : null,
|
ends_at: data.end_date_enabled ? data.end_date : null,
|
||||||
usage_limit: data.enable_usage_limit ? data.usage_limit : null,
|
usage_limit: usageLimit,
|
||||||
valid_duration:
|
valid_duration: data.enable_duration && duration ? duration : null,
|
||||||
data.enable_duration && !isDurationEmpty
|
|
||||||
? formatISODuration(duration)
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -254,7 +281,7 @@ export const EditDiscountConfigurationForm = ({
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="usage_limit"
|
name="usage_limit"
|
||||||
render={({ field }) => {
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
@@ -262,13 +289,14 @@ export const EditDiscountConfigurationForm = ({
|
|||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
value={value ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
|
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
field.onChange(null)
|
onChange(null)
|
||||||
} else {
|
} else {
|
||||||
field.onChange(Number(value))
|
onChange(Number(value))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+6
-6
@@ -1,29 +1,30 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Discount } from "@medusajs/medusa"
|
import { Discount } from "@medusajs/medusa"
|
||||||
import {
|
import {
|
||||||
CurrencyInput,
|
|
||||||
Button,
|
Button,
|
||||||
|
CurrencyInput,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
Select,
|
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { useAdminRegions, useAdminUpdateDiscount } from "medusa-react"
|
import { useAdminRegions, useAdminUpdateDiscount } from "medusa-react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
import {
|
import {
|
||||||
getDbAmount,
|
getDbAmount,
|
||||||
getPresentationalAmount,
|
getPresentationalAmount,
|
||||||
} from "../../../../../lib/money-amount-helpers"
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
|
|
||||||
type EditDiscountFormProps = {
|
type EditDiscountFormProps = {
|
||||||
discount: Discount
|
discount: Discount
|
||||||
@@ -186,9 +187,8 @@ export const EditDiscountDetailsForm = ({
|
|||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<PercentageInput
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
type="number"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
{...field}
|
{...field}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,2 @@
|
|||||||
|
export { DiscountsList as Component } from "./discount-list"
|
||||||
|
export { discountsLoader } from "./loader"
|
||||||
File renamed without changes.
@@ -1,2 +0,0 @@
|
|||||||
export { discountsLoader } from "./loader"
|
|
||||||
export { DiscountsList as Component } from "./list"
|
|
||||||
@@ -125,7 +125,7 @@ const DrawerFooter = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in new issue
Block a user