diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index 63dffaa0e3..055025a412 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -309,7 +309,7 @@
"discounts": {
"domain": "Discounts",
"startDate": "Start date",
- "createDiscountTitle": "Create discount",
+ "createDiscountTitle": "Create Discount",
"validDuration": "Duration of the discount",
"redemptionsLimit": "Redemptions limit",
"endDate": "End date",
@@ -320,8 +320,8 @@
"fixedAmount": "Fixed amount",
"validRegions": "Valid regions",
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
- "editDiscountDetails": "Edit discount details",
- "editDiscountConfiguration": "Edit discount configuration",
+ "editDiscountDetails": "Edit Discount Details",
+ "editDiscountConfiguration": "Edit Discount Configuration",
"hasStartDate": "Discount has a start date",
"hasEndDate": "Discount has an expiry date",
"startDateHint": "Schedule the discount to activate in the future.",
@@ -329,38 +329,54 @@
"codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.",
"hasUsageLimit": "Limit the number of redemptions?",
"usageLimitHint": "Limit applies across all customers, not per customer.",
- "titleHint": "The code your customers will enter during checkout. This will appear on your customer’s invoice.\nUppercase letters and numbers only.",
+ "titleHint": "The code your customers will enter during checkout. This will appear on your customer's invoice.\nUppercase letters and numbers only.",
"hasDurationLimit": "Availability duration",
"durationHint": "Set the duration of the discount",
"chooseValidRegions": "Choose valid regions",
- "noConditions": "No conditions are defined for this discount.",
- "editConditions": "Edit conditions",
"conditionsHint": "Create conditions to apply on the discount",
"isTemplateDiscount": "Is this a template discount?",
"percentageDescription": "Discount applied in %",
"fixedDescription": "Amount discount",
"shippingDescription": "Override delivery amount",
- "selectRegionFirst": "Select region first",
+ "selectRegionFirst": "Select a region first",
"templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.",
"conditions": {
+ "editHeader": "Edit Discount Conditions",
+ "editHint": "Specify conditions for when the discount can be applied to a cart.",
+ "manageTypesAction": "Manage condition types",
"including": {
- "products_one": "Discount applies to <0/> product.",
- "products_other": "Discount applies to <0/> products.",
- "customer_groups_one": "Discount applies to <0/> customer group.",
- "customer_groups_other": "Discount applies to <0/> customer groups.",
- "product_tags_one": "Discount applies to <0/> tag.",
- "product_tags_other": "Discount applies to <0/> tags.",
- "product_collections_one": "Discount applies to <0/> product collection.",
- "product_collections_other": "Discount applies to <0/> product collections.",
- "product_types_one": "Discount applies to <0/> product type.",
- "product_types_other": "Discount applies to <0/> product types."
+ "products_one": "Discount applies to <0/> product",
+ "products_other": "Discount applies to <0/> products",
+ "customer_groups_one": "Discount applies to <0/> customer group",
+ "customer_groups_other": "Discount applies to <0/> customer groups",
+ "product_tags_one": "Discount applies to <0/> tag",
+ "product_tags_other": "Discount applies to <0/> tags",
+ "product_collections_one": "Discount applies to <0/> product collection",
+ "product_collections_other": "Discount applies to <0/> product collections",
+ "product_types_one": "Discount applies to <0/> product type",
+ "product_types_other": "Discount applies to <0/> product types"
},
"excluding": {
- "products": "Discount applies to <1>all1> products except <0/>.",
- "customer_groups": "Discount applies to <1>all1> customer groups except <0/>.",
- "product_tags": "Discount applies to <1>all1> product tags except <0/>.",
- "product_collections": "Discount applies to <1>all1> product collections except <0/>.",
- "product_types": "Discount applies to <1>all1> product types except <0/>."
+ "products": "Discount applies to <1>all1> products except <0/>",
+ "customer_groups": "Discount applies to <1>all1> customer groups except <0/>",
+ "product_tags": "Discount applies to <1>all1> product tags except <0/>",
+ "product_collections": "Discount applies to <1>all1> product collections except <0/>",
+ "product_types": "Discount applies to <1>all1> product types except <0/>"
+ },
+ "edit": {
+ "appliesTo": "Discount applies to",
+ "except": {
+ "products_one": "product, except",
+ "products_other": "products, except",
+ "product_tags_one": "product tag, except",
+ "product_tags_other": "product tags, except",
+ "product_types_one": "product type, except",
+ "product_types_other": "product types, except",
+ "product_collections_one": "product collection, except",
+ "product_collections_other": "product collections, except",
+ "customer_groups_one": "customer group, except",
+ "customer_groups_other": "customer groups, except"
+ }
}
},
"discountStatus": {
@@ -692,8 +708,14 @@
"limit": "Limit",
"tags": "Tags",
"type": "Type",
+ "none": "none",
+ "all": "all",
"percentage": "Percentage",
"sales_channels": "Sales Channels",
+ "customer_groups": "Customer Groups",
+ "product_tags": "Product Tags",
+ "product_types": "Product Types",
+ "product_collections": "Product Collections",
"status": "Status",
"code": "Code",
"value": "Value",
diff --git a/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
index 57d79598b7..ab18f60b1f 100644
--- a/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
+++ b/packages/admin-next/dashboard/src/components/common/list-summary/list-summary.tsx
@@ -1,4 +1,4 @@
-import { Text, Tooltip, clx } from "@medusajs/ui"
+import { Tooltip, clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
type ListSummaryProps = {
@@ -12,46 +12,54 @@ type ListSummaryProps = {
*/
list: string[]
/**
- * Is hte summary displayed inline.
+ * Is the summary displayed inline.
* Determines whether the center text is truncated if there is no space in the container
*/
inline?: boolean
+
+ className?: string
}
-export const ListSummary = ({ list, inline, n = 2 }: ListSummaryProps) => {
+export const ListSummary = ({
+ list,
+ className,
+ inline,
+ n = 2,
+}: ListSummaryProps) => {
const { t } = useTranslation()
+
+ const title = t("general.plusCountMore", {
+ count: list.length - n,
+ })
+
return (
-
- {list.slice(0, n).join(", ")}
-
+
+ {list.slice(0, n).join(", ")}
+
{list.length > n && (
-
- {list.slice(n).map((c) => (
- {c}
- ))}
-
- }
- >
-
+
+ {list.slice(n).map((c) => (
+ {c}
+ ))}
+
+ }
>
- {t("general.plusCountMore", {
- count: list.length - n,
- })}
-
-
+
{title}
+
+
)}
)
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx
new file mode 100644
index 0000000000..c36a04782b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx
@@ -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 (
+
+ {`${format(
+ value,
+ timestampFormat
+ )}`}
+ }
+ >
+ {format(value, "dd MMM yyyy")}
+
+
+ )
+}
+
+export const CreatedAtHeader = () => {
+ const { t } = useTranslation()
+
+ return (
+
+ {t("fields.createdAt")}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts
new file mode 100644
index 0000000000..2a3ce946f5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/created-at-cell/index.ts
@@ -0,0 +1 @@
+export * from "./created-at-cell"
diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx
new file mode 100644
index 0000000000..779d65252e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-group-table-columns.tsx
@@ -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()
+
+export const useCustomerGroupTableColumns = () => {
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "name",
+ header: () => ,
+ cell: ({
+ row: {
+ original: { name },
+ },
+ }) => {name},
+ }),
+
+ columnHelper.accessor("created_at", {
+ header: () => ,
+ cell: ({ getValue }) => ,
+ }),
+ ],
+ []
+ )
+}
diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx
new file mode 100644
index 0000000000..46fd056783
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-group-table-query.tsx
@@ -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,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
index 1d5ec8b053..442cd2a6a2 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx
@@ -3,6 +3,7 @@ import type {
AdminCustomerGroupsRes,
AdminCustomersRes,
AdminDraftOrdersRes,
+ AdminDiscountsRes,
AdminGiftCardsRes,
AdminOrdersRes,
AdminProductsRes,
@@ -430,25 +431,36 @@ export const v1Routes: RouteObject[] = [
},
children: [
{
- index: true,
- lazy: () => import("../../routes/discounts/list"),
+ path: "",
+ lazy: () => import("../../routes/discounts/discount-list"),
},
{
path: "create",
- lazy: () => import("../../routes/discounts/create"),
+ lazy: () => import("../../routes/discounts/discount-create"),
},
{
path: ":id",
- lazy: () => import("../../routes/discounts/details"),
+ lazy: () => import("../../routes/discounts/discount-detail"),
+ handle: {
+ crumb: (data: AdminDiscountsRes) => data.discount.code,
+ },
children: [
{
path: "edit",
- lazy: () => import("../../routes/discounts/edit-details"),
+ lazy: () =>
+ import("../../routes/discounts/discount-edit-details"),
},
{
path: "configuration",
lazy: () =>
- import("../../routes/discounts/edit-configuration"),
+ import(
+ "../../routes/discounts/discount-edit-configuration"
+ ),
+ },
+ {
+ path: "conditions",
+ lazy: () =>
+ import("../../routes/discounts/discount-edit-conditions"),
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx
new file mode 100644
index 0000000000..c2c1e6bd07
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/conditions-drawer.tsx
@@ -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 (
+
+
+
+
+
+
+ )
+}
+
+const ProductConditionsTable = ({ selected = [], onSave }: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useProductTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_PREFIX,
+ })
+ const { products, count, isLoading, isError, error } = useAdminProducts(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (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 (
+
+
+
+
+ )
+}
+
+const CustomerGroupConditionsTable = ({
+ selected = [],
+ onSave,
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+
+ const [intermediate, setIntermediate] = useState(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 = (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 (
+
+
+
+
+ )
+}
+
+const ProductTypeConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(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 = (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 (
+
+
+
+
+ )
+}
+
+const ProductCollectionConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(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 = (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 (
+
+
+
+
+ )
+}
+
+const ProductTagConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(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 = (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 (
+
+
+
+
+ )
+}
+
+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
+ case ConditionEntities.PRODUCT_TYPE:
+ return
+ case ConditionEntities.PRODUCT_COLLECTION:
+ return
+ case ConditionEntities.PRODUCT_TAG:
+ return
+ case ConditionEntities.CUSTOMER_GROUP:
+ return
+ default:
+ return null
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts
new file mode 100644
index 0000000000..b65a27e54d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/components/conditions-drawer/index.ts
@@ -0,0 +1 @@
+export * from "./conditions-drawer"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts b/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts
new file mode 100644
index 0000000000..186bbe49e7
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/constants.ts
@@ -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",
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx
new file mode 100644
index 0000000000..5ed653dad6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-customer-group-conditions-table-columns.tsx
@@ -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()
+
+export const useCustomerGroupConditionsTableColumns = () => {
+ const base = useCustomerGroupTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx
new file mode 100644
index 0000000000..049e8998e1
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-collection-conditions-table-columns.tsx
@@ -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()
+
+export const useProductCollectionConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ 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 {count || "-"}
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx
new file mode 100644
index 0000000000..9fc6850729
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-conditions-table-columns.tsx
@@ -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()
+
+export const useProductConditionsTableColumns = () => {
+ const base = useProductTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx
new file mode 100644
index 0000000000..540c8d1354
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-tag-conditions-table-columns.tsx
@@ -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()
+
+export const useProductTagConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("value", {
+ header: t("fields.value"),
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx
new file mode 100644
index 0000000000..9066f5b02f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/columns/use-product-type-conditions-table-columns.tsx
@@ -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()
+
+export const useProductTypeConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("value", {
+ header: t("fields.value"),
+ cell: ({ getValue }) => getValue(),
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx
new file mode 100644
index 0000000000..50859c8e24
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-customer-group-conditions-table-filters.tsx
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx
new file mode 100644
index 0000000000..45de4cc975
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-collection-conditions-table-filters.tsx
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx
new file mode 100644
index 0000000000..b79dad59a0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-tag-conditions-table-filters.tsx
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx
new file mode 100644
index 0000000000..7c9a74018c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/filters/use-product-type-conditions-table-filters.tsx
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx
new file mode 100644
index 0000000000..159ab0a8bd
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-collection-conditions-table-query.tsx
@@ -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,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx
new file mode 100644
index 0000000000..717cab17f6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-tag-conditions-table-query.tsx
@@ -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,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx
new file mode 100644
index 0000000000..737d00d143
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/hooks/query/use-product-type-conditions-table-query.tsx
@@ -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,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/common/types.ts b/packages/admin-next/dashboard/src/routes/discounts/common/types.ts
new file mode 100644
index 0000000000..411329ebef
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/common/types.ts
@@ -0,0 +1,10 @@
+import { ConditionEntities } from "./constants"
+
+export type ConditionsOption = {
+ value: string
+ label: string
+}
+
+export type ConditionsState = {
+ [K in ConditionEntities]: boolean
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-details.tsx
similarity index 97%
rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-details.tsx
index 937922e6aa..fd064a7184 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-details.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-details.tsx
@@ -1,23 +1,25 @@
-import { useEffect, useMemo } from "react"
import {
Checkbox,
+ CurrencyInput,
DatePicker,
Heading,
Input,
+ RadioGroup,
Select,
Switch,
Text,
Textarea,
- RadioGroup,
- CurrencyInput,
} from "@medusajs/ui"
-import { Trans, useTranslation } from "react-i18next"
import { useAdminRegions } from "medusa-react"
+import { useEffect, useMemo } from "react"
+import { Trans, useTranslation } from "react-i18next"
-import { Form } from "../../../../../components/common/form"
-import { CreateDiscountFormReturn } from "./create-discount-form"
+import { useWatch } from "react-hook-form"
import { Combobox } from "../../../../../components/common/combobox"
+import { Form } from "../../../../../components/common/form"
+import { PercentageInput } from "../../../../../components/common/percentage-input"
import { getCurrencySymbol } from "../../../../../lib/currencies"
+import { CreateDiscountFormReturn } from "./create-discount-form"
import { DiscountRuleType } from "./types"
type CreateDiscountPropsProps = {
@@ -29,8 +31,14 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
const { regions } = useAdminRegions()
- const watchType = form.watch("type")
- const watchRegion = form.watch("regions")
+ const watchType = useWatch({
+ control: form.control,
+ name: "type",
+ })
+ const watchRegion = useWatch({
+ name: "regions",
+ control: form.control,
+ })
const isFixedDiscount = watchType === DiscountRuleType.FIXED
const isFreeShipping = watchType === DiscountRuleType.FREE_SHIPPING
@@ -43,10 +51,12 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
return regions.find((r) => r.id === watchRegion[0])
}, [regions, watchRegion])
+ const { setValue } = form
+
useEffect(() => {
- form.setValue("regions", [])
- form.setValue("value", undefined)
- }, [watchType])
+ setValue("regions", [])
+ setValue("value", undefined)
+ }, [watchType, setValue])
return (
@@ -197,9 +207,8 @@ export const CreateDiscountDetails = ({ form }: CreateDiscountPropsProps) => {
)
) : (
- {
{t("discounts.isTemplateDiscount")}
- {/**/}
+ {/* */}
)
}}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-form.tsx
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/create-discount-form.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/create-discount-form.tsx
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/types.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/types.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/create/components/create-discount-form/types.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/components/create-discount-form/types.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-create/discount-create.tsx
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/create/discount-create.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/discount-create.tsx
diff --git a/packages/admin-next/dashboard/src/routes/discounts/create/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-create/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/create/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-create/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/details-section.tsx
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/details-section.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/details-section.tsx
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/details-section/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/details-section/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx
similarity index 61%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx
index 32fbc233cc..cac593ef7e 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/discount-conditions-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/discount-conditions-section.tsx
@@ -1,27 +1,64 @@
-import type { Discount, DiscountCondition } from "@medusajs/medusa"
-import { Container, Heading } from "@medusajs/ui"
-import { Trans, useTranslation } from "react-i18next"
import { PencilSquare } from "@medusajs/icons"
+import type {
+ CustomerGroup,
+ Discount,
+ DiscountCondition,
+ Product,
+ ProductCollection,
+ ProductTag,
+ ProductType,
+} from "@medusajs/medusa"
+import { Badge, Container, Heading } from "@medusajs/ui"
import { useMemo } from "react"
+import { Trans, useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
-import { ListSummary } from "../../../../../components/common/list-summary"
import { NoRecords } from "../../../../../components/common/empty-table-content"
+import { ListSummary } from "../../../../../components/common/list-summary"
type ConditionTypeProps = {
condition: DiscountCondition
}
-const N = 2
+const N = 1
+
+const hasTitle = (p: unknown): p is Product | ProductCollection => {
+ return (p as { title: string }).title !== undefined
+}
+
+const hasName = (p: unknown): p is CustomerGroup => {
+ return (p as { name: string }).name !== undefined
+}
+
+const hasValue = (p: unknown): p is ProductTag | ProductType => {
+ return (p as { value: string }).value !== undefined
+}
function ConditionType({ condition }: ConditionTypeProps) {
+ const { t } = useTranslation()
+
const operator = condition.operator === "in" ? "including" : "excluding"
const entity = condition.type
+ const list = condition[entity].map((p) => {
+ if (hasTitle(p)) {
+ return p.title
+ }
+
+ if (hasName(p)) {
+ return p.name
+ }
+
+ if (hasValue(p)) {
+ return p.value
+ }
+ }) as string[]
+
return (
-
-
+
+
N
? N - condition[entity].length
@@ -29,22 +66,19 @@ function ConditionType({ condition }: ConditionTypeProps) {
}
i18nKey={`discounts.conditions.${operator}.${entity}`}
components={[
-
p.title || p.name || p.value
- )}
+ list={list}
+ className="!txt-compact-xsmall-plus"
/>
- ,
- ,
+ ,
+ ,
]}
/>
@@ -88,12 +122,7 @@ export const DiscountConditionsSection = ({
/>
- {!conditions.length && (
-
- )}
+ {!conditions.length && }
{conditions.map((condition) => (
))}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-conditions-section/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-conditions-section/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx
similarity index 93%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx
index 86d3777c3d..cd9b262ca1 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/discount-configurations-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/discount-configurations-section.tsx
@@ -1,6 +1,6 @@
-import { parse } from "iso8601-duration"
-import { format, formatDuration } from "date-fns"
import { PencilSquare } from "@medusajs/icons"
+import { format, formatDuration } from "date-fns"
+import { parse } from "iso8601-duration"
import { useMemo } from "react"
import { Discount } from "@medusajs/medusa"
@@ -13,9 +13,9 @@ type DiscountConfigurationsSection = {
discount: Discount
}
-function formatTime(dateTime?: string) {
+function formatTime(dateTime?: string | Date | null) {
if (!dateTime) {
- return
+ return "-"
}
return format(new Date(dateTime), "dd MMM, yyyy, HH:mm:ss")
}
@@ -57,7 +57,7 @@ export const DiscountConfigurationSection = ({
{t("discounts.startDate")}
- {formatTime(discount.starts_at as unknown as string)}
+ {formatTime(discount.starts_at)}
@@ -65,7 +65,7 @@ export const DiscountConfigurationSection = ({
{t("discounts.endDate")}
- {formatTime(discount.ends_at as unknown as string)}
+ {formatTime(discount.ends_at)}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-configurations-section/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-configurations-section/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/discount-general-section.tsx
similarity index 98%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/discount-general-section.tsx
index f2ce0e6b39..bf6b3981c8 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/discount-general-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/discount-general-section.tsx
@@ -60,7 +60,7 @@ export const DiscountGeneralSection = ({
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
}[getDiscountStatus(discount)] as [
"grey" | "orange" | "green" | "red",
- string
+ string,
]
return (
@@ -101,7 +101,7 @@ export const DiscountGeneralSection = ({
{t("fields.description")}
- {discount.rule.description}
+ {discount.rule.description || "-"}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/discounts-general-section/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-general-section/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/redemptions-section.tsx
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/components/redemptions-section/redemptions-section.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/components/discount-redemptions-section/redemptions-section.tsx
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx
similarity index 90%
rename from packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx
index f2bedf3542..ececf11ff4 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/discount-detail.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/discount-detail.tsx
@@ -2,16 +2,16 @@ import { useAdminDiscount } from "medusa-react"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
-import { DiscountGeneralSection } from "./components/discounts-general-section"
-import { DiscountConfigurationSection } from "./components/discounts-configurations-section"
+import { DiscountConfigurationSection } from "./components/discount-configurations-section"
+import { DiscountGeneralSection } from "./components/discount-general-section"
-import { discountLoader, expand } from "./loader"
-import { RedemptionsSection } from "./components/redemptions-section"
import { DetailsSection } from "./components/details-section"
-import { DiscountConditionsSection } from "./components/discounts-conditions-section"
+import { DiscountConditionsSection } from "./components/discount-conditions-section"
+import { RedemptionsSection } from "./components/discount-redemptions-section"
+import { discountLoader, expand } from "./loader"
-import before from "medusa-admin:widgets/discount/details/before"
import after from "medusa-admin:widgets/discount/details/after"
+import before from "medusa-admin:widgets/discount/details/before"
export const DiscountDetail = () => {
const initialData = useLoaderData() as Awaited<
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts
similarity index 55%
rename from packages/admin-next/dashboard/src/routes/discounts/details/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts
index 2debe10554..aeedb8f7fc 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/details/index.ts
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/index.ts
@@ -1,2 +1,2 @@
-export { discountLoader as loader } from "./loader"
export { DiscountDetail as Component } from "./discount-detail.tsx"
+export { discountLoader as loader } from "./loader.ts"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/details/loader.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-detail/loader.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/details/loader.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-detail/loader.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx
new file mode 100644
index 0000000000..17aa2e4e5d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/edit-discount-conditions-form.tsx
@@ -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(
+ 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>({
+ 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 (
+
+
+
+ )
+}
+
+function Condition({ labels, isInOperator, type, onClick }: ConditionProps) {
+ const { t } = useTranslation()
+
+ const isInButtonDisabled = !!labels.length && !isInOperator
+ const isExButtonDisabled = !!labels.length && isInOperator
+
+ return (
+
+
+
+ {t("discounts.conditions.edit.appliesTo")}
+
+
+
+
+
+
+ {t(`discounts.conditions.edit.except.${type}`, {
+ count: labels.length > 1 ? labels.length - 1 : labels.length,
+ })}
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts
new file mode 100644
index 0000000000..d8b9de67e0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/components/edit-discount-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-discount-conditions-form"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx
new file mode 100644
index 0000000000..2bf1156357
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/discount-edit-conditions.tsx
@@ -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 (
+
+ {!isLoading && discount && (
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts
new file mode 100644
index 0000000000..922672dcff
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-conditions/index.ts
@@ -0,0 +1 @@
+export { DiscountEditConditions as Component } from "./discount-edit-conditions"
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
similarity index 88%
rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
index 1c7808b819..b5da6c8938 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/edit-discount-configuration-form.tsx
@@ -1,13 +1,13 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { formatISODuration } from "date-fns"
+import { Duration, parse } from "iso8601-duration"
import { useMemo } from "react"
import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
import { Trans, useTranslation } from "react-i18next"
-import { parse, Duration } from "iso8601-duration"
-import { formatISODuration } from "date-fns"
import * as zod from "zod"
import { Discount } from "@medusajs/medusa"
-import { Button, Input, Text, Switch, DatePicker } from "@medusajs/ui"
+import { Button, DatePicker, Input, Switch, Text } from "@medusajs/ui"
import { useAdminUpdateDiscount } from "medusa-react"
import { Form } from "../../../../../components/common/form"
@@ -29,15 +29,15 @@ const EditDiscountSchema = zod.object({
end_date: zod.date().nullish(),
enable_usage_limit: zod.boolean(),
- usage_limit: zod.number().nullish(),
+ usage_limit: zod.union([zod.string(), zod.number()]).optional(),
enable_duration: zod.boolean(),
- years: zod.number().optional(),
- months: zod.number().optional(),
- days: zod.number().optional(),
- hours: zod.number().optional(),
- minutes: zod.number().optional(),
+ years: zod.union([zod.string(), zod.number()]).optional(),
+ months: zod.union([zod.string(), zod.number()]).optional(),
+ days: zod.union([zod.string(), zod.number()]).optional(),
+ hours: zod.union([zod.string(), zod.number()]).optional(),
+ minutes: zod.union([zod.string(), zod.number()]).optional(),
})
export const EditDiscountConfigurationForm = ({
@@ -46,13 +46,11 @@ export const EditDiscountConfigurationForm = ({
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
- const duration = useMemo(
- () =>
- discount.valid_duration
- ? parse(discount.valid_duration)
- : ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration),
- [discount]
- )
+ const duration = useMemo(() => {
+ return discount.valid_duration
+ ? parse(discount.valid_duration)
+ : ({ years: 0, months: 0, days: 0, hours: 0, minutes: 0 } as Duration)
+ }, [discount])
const form = useForm>({
defaultValues: {
@@ -60,37 +58,66 @@ export const EditDiscountConfigurationForm = ({
start_date: new Date(discount.starts_at),
enable_usage_limit: !!discount.usage_limit,
- usage_limit: discount.usage_limit,
+ usage_limit: discount.usage_limit || "",
enable_duration: !!discount.valid_duration,
end_date_enabled: !!discount.ends_at,
end_date: discount.ends_at ? new Date(discount.ends_at) : null,
- years: duration.years,
- months: duration.months,
- days: duration.days,
- hours: duration.hours,
- minutes: duration.minutes,
+ years: duration.years || "",
+ months: duration.months || "",
+ days: duration.days || "",
+ hours: duration.hours || "",
+ minutes: duration.minutes || "",
},
resolver: zodResolver(EditDiscountSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateDiscount(discount.id)
+ const formatDuration = (raw: Record) => {
+ const isEmpty = Object.values(raw).every((v) => !v)
+
+ if (isEmpty) {
+ return null
+ }
+
+ const durationObject = Object.entries(raw).reduce((acc, [key, value]) => {
+ if (value) {
+ return {
+ ...acc,
+ [key]: Number(value),
+ }
+ }
+ return acc
+ }, {} as Duration)
+
+ return formatISODuration(durationObject)
+ }
+
const handleSubmit = form.handleSubmit(async (data) => {
- const duration = pick(data, ["years", "months", "days", "hours", "minutes"])
- const isDurationEmpty = Object.values(duration).every((v) => !v)
+ const rawDuration = pick(data, [
+ "years",
+ "months",
+ "days",
+ "hours",
+ "minutes",
+ ]) as Record
+
+ const duration = formatDuration(rawDuration)
+
+ const usageLimit =
+ data.enable_usage_limit && !isNaN(Number(data.usage_limit))
+ ? Number(data.usage_limit)
+ : null
await mutateAsync(
{
starts_at: data.start_date,
ends_at: data.end_date_enabled ? data.end_date : null,
- usage_limit: data.enable_usage_limit ? data.usage_limit : null,
- valid_duration:
- data.enable_duration && !isDurationEmpty
- ? formatISODuration(duration)
- : null,
+ usage_limit: usageLimit,
+ valid_duration: data.enable_duration && duration ? duration : null,
},
{
onSuccess: () => {
@@ -254,7 +281,7 @@ export const EditDiscountConfigurationForm = ({
{
+ render={({ field: { value, onChange, ...field } }) => {
return (
@@ -262,13 +289,14 @@ export const EditDiscountConfigurationForm = ({
{...field}
type="number"
min={0}
+ value={value ?? ""}
onChange={(e) => {
const value = e.target.value
if (value === "") {
- field.onChange(null)
+ onChange(null)
} else {
- field.onChange(Number(value))
+ onChange(Number(value))
}
}}
/>
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/components/edit-discount-form/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/components/edit-discount-form/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/discount-edit-configuration.tsx
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/discount-edit-configuration.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/discount-edit-configuration.tsx
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/routes/discounts/edit-configuration/index.ts
rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-configuration/index.ts
diff --git a/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx
similarity index 98%
rename from packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx
rename to packages/admin-next/dashboard/src/routes/discounts/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx
index 181e6d54b4..c0752053d2 100644
--- a/packages/admin-next/dashboard/src/routes/discounts/edit-details/components/edit-discount-form/edit-discount-details-form.tsx
+++ b/packages/admin-next/dashboard/src/routes/discounts/discount-edit-details/components/edit-discount-form/edit-discount-details-form.tsx
@@ -1,29 +1,30 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Discount } from "@medusajs/medusa"
import {
- CurrencyInput,
Button,
+ CurrencyInput,
Input,
+ Select,
Text,
Textarea,
- Select,
} from "@medusajs/ui"
import { useAdminRegions, useAdminUpdateDiscount } from "medusa-react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
+import { Combobox } from "../../../../../components/common/combobox"
import { Form } from "../../../../../components/common/form"
+import { PercentageInput } from "../../../../../components/common/percentage-input"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
+import { getCurrencySymbol } from "../../../../../lib/currencies"
import {
getDbAmount,
getPresentationalAmount,
} from "../../../../../lib/money-amount-helpers"
-import { getCurrencySymbol } from "../../../../../lib/currencies"
-import { Combobox } from "../../../../../components/common/combobox"
type EditDiscountFormProps = {
discount: Discount
@@ -186,9 +187,8 @@ export const EditDiscountDetailsForm = ({
{...field}
/>
) : (
-