diff --git a/.changeset/seven-files-begin.md b/.changeset/seven-files-begin.md
new file mode 100644
index 0000000000..0111ea78e5
--- /dev/null
+++ b/.changeset/seven-files-begin.md
@@ -0,0 +1,6 @@
+---
+"@medusajs/medusa": patch
+"@medusajs/types": patch
+---
+
+feat(medusa,types): create promotion flows
diff --git a/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts b/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts
index 97b29e1304..5f64aa0039 100644
--- a/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts
+++ b/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts
@@ -1,8 +1,8 @@
-import { IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import { IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
-import { createAdminUser } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
+import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -65,7 +65,7 @@ medusaIntegrationTestRunner({
target_type: "items",
type: "fixed",
allocation: "each",
- value: "100",
+ value: 100,
max_quantity: 100,
target_rules: [
{
@@ -144,7 +144,7 @@ medusaIntegrationTestRunner({
target_type: "items",
type: "fixed",
allocation: "each",
- value: "100",
+ value: 100,
max_quantity: 100,
target_rules: [
{
@@ -184,7 +184,7 @@ medusaIntegrationTestRunner({
target_type: "items",
type: "fixed",
allocation: "each",
- value: "100",
+ value: 100,
max_quantity: 100,
buy_rules: [
{
@@ -231,7 +231,7 @@ medusaIntegrationTestRunner({
target_type: "items",
type: "fixed",
allocation: "each",
- value: "100",
+ value: 100,
max_quantity: 100,
apply_to_quantity: 1,
buy_rules_min_quantity: 1,
diff --git a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts
index 2158088f31..99e1f367a2 100644
--- a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts
+++ b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts
@@ -834,8 +834,8 @@ medusaIntegrationTestRunner({
expect(response.data.values.length).toEqual(2)
expect(response.data.values).toEqual(
expect.arrayContaining([
- { label: "Afghan Afghani", value: "afn" },
- { label: "Albanian Lek", value: "all" },
+ { label: "afn", value: "afn" },
+ { label: "all", value: "all" },
])
)
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 35e739c5b8..0ff378766f 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -824,6 +824,8 @@
"value": "Value",
"campaign": "Campaign",
"allocation": "Allocation",
+ "addCondition": "Add condition",
+ "clearAll": "Clear all",
"conditions": {
"rules": {
"title": "Who can use this code?",
@@ -839,6 +841,10 @@
}
}
},
+ "errors": {
+ "requiredField": "Required field"
+ },
+ "create": {},
"edit": {
"title": "Edit Promotion Details",
"rules": {
@@ -855,6 +861,9 @@
"title": "Add Promotion To Campaign"
},
"form": {
+ "required": "Required",
+ "and": "AND",
+ "selectAttribute": "Select Attribute",
"campaign": {
"existing": {
"title": "Existing Campaign",
@@ -875,17 +884,31 @@
},
"automatic": {
"title": "Automatic",
- "description": "Customers will automatically see this during checkout"
+ "description": "Customers will see this at checkout"
+ }
+ },
+ "max_quantity": {
+ "title": "Maximum Quantity",
+ "description": "Maximum quantity of items this promotion applies to"
+ },
+ "type": {
+ "standard": {
+ "title": "Standard",
+ "description": "A standard promotion"
+ },
+ "buyget": {
+ "title": "Buy Get",
+ "description": "Buy X get Y promotion"
}
},
"allocation": {
"each": {
"title": "Each",
- "description": "Applies value on each product"
+ "description": "Applies value on each item"
},
"across": {
"title": "Across",
- "description": "Applies value proportionally across products"
+ "description": "Applies value across items"
}
},
"code": {
diff --git a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx
index 81b3820b4b..3143d4472e 100644
--- a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx
+++ b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx
@@ -29,7 +29,7 @@ import {
const PROMOTIONS_QUERY_KEY = "promotions" as const
export const promotionsQueryKeys = {
...queryKeysFactory(PROMOTIONS_QUERY_KEY),
- listRules: (id: string, ruleType: string) => [
+ listRules: (id: string | null, ruleType: string) => [
PROMOTIONS_QUERY_KEY,
id,
ruleType,
@@ -60,7 +60,7 @@ export const usePromotion = (
}
export const usePromotionRules = (
- id: string,
+ id: string | null,
ruleType: string,
options?: Omit<
UseQueryOptions<
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
index c63e856544..138a27e61c 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
@@ -172,6 +172,11 @@ export const v2Routes: RouteObject[] = [
path: "",
lazy: () => import("../../v2-routes/promotions/promotion-list"),
},
+ {
+ path: "create",
+ lazy: () =>
+ import("../../v2-routes/promotions/promotion-create"),
+ },
{
path: ":id",
lazy: () =>
@@ -196,7 +201,8 @@ export const v2Routes: RouteObject[] = [
},
{
path: ":ruleType/edit",
- lazy: () => import("../../v2-routes/promotions/edit-rules"),
+ lazy: () =>
+ import("../../v2-routes/promotions/common/edit-rules"),
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/invite/invite.tsx b/packages/admin-next/dashboard/src/routes/invite/invite.tsx
index 7a688c6d46..2da35b372a 100644
--- a/packages/admin-next/dashboard/src/routes/invite/invite.tsx
+++ b/packages/admin-next/dashboard/src/routes/invite/invite.tsx
@@ -2,11 +2,11 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { UserRoles } from "@medusajs/medusa"
import { Alert, Button, Heading, Input, Text, Tooltip } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion"
+import i18n from "i18next"
import { useAdminAcceptInvite } from "medusa-react"
import { Trans, useTranslation } from "react-i18next"
import { Link, useSearchParams } from "react-router-dom"
import * as z from "zod"
-import i18n from "i18next"
import { useState } from "react"
import { useForm } from "react-hook-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx
new file mode 100644
index 0000000000..7b33dde445
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx
@@ -0,0 +1,423 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { XMarkMini } from "@medusajs/icons"
+import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
+import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui"
+import i18n from "i18next"
+import { Fragment, useState } from "react"
+import { useFieldArray, useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+import { Combobox } from "../../../../../../components/common/combobox"
+import { Form } from "../../../../../../components/common/form"
+import { RouteDrawer } from "../../../../../../components/route-modal"
+import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
+import { RuleTypeValues } from "../../edit-rules"
+import { getDisguisedRules } from "./utils"
+
+type EditPromotionFormProps = {
+ promotion: PromotionDTO
+ rules: PromotionRuleDTO[]
+ ruleType: RuleTypeValues
+ attributes: any[]
+ operators: any[]
+ handleSubmit: any
+ isSubmitting: boolean
+}
+
+const EditRules = zod.object({
+ rules: zod.array(
+ zod.object({
+ id: zod.string().optional(),
+ attribute: zod
+ .string()
+ .min(1, { message: i18n.t("promotions.form.required") }),
+ operator: zod
+ .string()
+ .min(1, { message: i18n.t("promotions.form.required") }),
+ values: zod.union([
+ zod.number().min(1, { message: i18n.t("promotions.form.required") }),
+ zod.string().min(1, { message: i18n.t("promotions.form.required") }),
+ zod
+ .array(zod.string())
+ .min(1, { message: i18n.t("promotions.form.required") }),
+ ]),
+ required: zod.boolean().optional(),
+ field_type: zod.string().optional(),
+ })
+ ),
+})
+
+const RuleValueFormField = ({
+ identifier,
+ scope,
+ valuesFields,
+ valuesRef,
+ fieldRule,
+ attributes,
+ ruleType,
+}) => {
+ const attribute = attributes?.find(
+ (attr) => attr.value === fieldRule.attribute
+ )
+ const { values: options = [] } = usePromotionRuleValues(
+ ruleType,
+ attribute?.id,
+ {
+ enabled: !!attribute?.id && !attribute.disguised,
+ }
+ )
+
+ return (
+
{
+ if (fieldRule.field_type === "number") {
+ return (
+
+
+
+
+
+
+ )
+ } else if (fieldRule.field_type === "text") {
+ return (
+
+
+
+
+
+
+ )
+ } else {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+ }}
+ />
+ )
+}
+
+export const RulesFormField = ({
+ form,
+ ruleType,
+ fields,
+ attributes,
+ operators,
+ removeRule,
+ updateRule,
+ appendRule,
+ setRulesToRemove,
+ rulesToRemove,
+ scope = "rules",
+}) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t(`promotions.fields.conditions.${ruleType}.title`)}
+
+
+
+ {t(`promotions.fields.conditions.${ruleType}.description`)}
+
+
+ {fields.map((fieldRule, index) => {
+ const identifier = fieldRule.id
+ const { ref: attributeRef, ...attributeFields } = form.register(
+ `${scope}.${index}.attribute`
+ )
+ const { ref: operatorRef, ...operatorFields } = form.register(
+ `${scope}.${index}.operator`
+ )
+ const { ref: valuesRef, ...valuesFields } = form.register(
+ `${scope}.${index}.values`
+ )
+
+ return (
+
+
+
+
{
+ const existingAttributes =
+ fields?.map((field) => field.attribute) || []
+ const attributeOptions =
+ attributes?.filter((attr) => {
+ if (attr.value === fieldRule.attribute) {
+ return true
+ }
+
+ return !existingAttributes.includes(attr.value)
+ }) || []
+
+ return (
+
+ {fieldRule.required && (
+
+ {t("promotions.form.required")}
+
+ )}
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
{
+ return (
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+ {
+ if (!fieldRule.required) {
+ fieldRule.id &&
+ setRulesToRemove &&
+ setRulesToRemove([...rulesToRemove, fieldRule])
+
+ removeRule(index)
+ }
+ }}
+ />
+
+
+
+ {index < fields.length - 1 && (
+
+
+
+
+ {t("promotions.form.and")}
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+
+
+
+ )
+}
+
+export const EditRulesForm = ({
+ promotion,
+ rules,
+ ruleType,
+ attributes,
+ operators,
+ handleSubmit,
+ isSubmitting,
+}: EditPromotionFormProps) => {
+ const { t } = useTranslation()
+ const requiredAttributes = attributes?.filter((ra) => ra.required) || []
+ const requiredAttributeValues = requiredAttributes?.map((ra) => ra.value)
+ const disguisedRules =
+ getDisguisedRules(promotion, requiredAttributes, ruleType) || []
+ const [rulesToRemove, setRulesToRemove] = useState([])
+
+ const form = useForm>({
+ defaultValues: {
+ rules: [...disguisedRules, ...rules].map((rule) => ({
+ id: rule.id,
+ required: requiredAttributeValues.includes(rule.attribute),
+ field_type: rule.field_type,
+ attribute: rule.attribute!,
+ operator: rule.operator!,
+ values: rule?.values?.map((v: { value: string }) => v.value!),
+ })),
+ },
+ resolver: zodResolver(EditRules),
+ })
+
+ const { fields, append, remove, update } = useFieldArray({
+ control: form.control,
+ name: "rules",
+ keyName: "rules_id",
+ })
+
+ const handleFormSubmit = form.handleSubmit(handleSubmit(rulesToRemove))
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts
rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/index.ts
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts
rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx
new file mode 100644
index 0000000000..1458990fc4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx
@@ -0,0 +1,137 @@
+import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
+import { useRouteModal } from "../../../../../../components/route-modal"
+import {
+ usePromotionAddRules,
+ usePromotionRemoveRules,
+ usePromotionRuleAttributes,
+ usePromotionRuleOperators,
+ usePromotionUpdateRules,
+ useUpdatePromotion,
+} from "../../../../../../hooks/api/promotions"
+import { RuleTypeValues } from "../../edit-rules"
+import { EditRulesForm } from "../edit-rules-form"
+import { getDisguisedRules } from "../edit-rules-form/utils"
+
+type EditPromotionFormProps = {
+ promotion: PromotionDTO
+ rules: PromotionRuleDTO[]
+ ruleType: RuleTypeValues
+}
+
+export const EditRulesWrapper = ({
+ promotion,
+ rules,
+ ruleType,
+}: EditPromotionFormProps) => {
+ const { handleSuccess } = useRouteModal()
+ const {
+ attributes,
+ isError: isAttributesError,
+ error: attributesError,
+ } = usePromotionRuleAttributes(ruleType!)
+
+ const {
+ operators,
+ isError: isOperatorsError,
+ error: operatorsError,
+ } = usePromotionRuleOperators()
+
+ if (isAttributesError || isOperatorsError) {
+ throw attributesError || operatorsError
+ }
+
+ const requiredAttributes = attributes?.filter((ra) => ra.required) || []
+ const disguisedRules =
+ getDisguisedRules(promotion, requiredAttributes, ruleType) || []
+
+ const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id)
+ const { mutateAsync: addPromotionRules } = usePromotionAddRules(
+ promotion.id,
+ ruleType
+ )
+
+ const { mutateAsync: removePromotionRules } = usePromotionRemoveRules(
+ promotion.id,
+ ruleType
+ )
+
+ const { mutateAsync: updatePromotionRules, isPending } =
+ usePromotionUpdateRules(promotion.id, ruleType)
+
+ const handleSubmit = (rulesToRemove?: any[]) => {
+ return async function (data) {
+ const applicationMethodData: Record = {}
+ const { rules: allRules = [] } = data
+
+ const disguisedRulesData = allRules.filter((rule) =>
+ disguisedRules.map((rule) => rule.id).includes(rule.id!)
+ )
+
+ // For all the rules that were disguised, convert them to actual values in the
+ // database, they are currently all under application_method. If more of these are coming
+ // up, abstract this away.
+ for (const rule of disguisedRulesData) {
+ applicationMethodData[rule.id!] = parseInt(rule.values as string)
+ }
+
+ // This variable will contain the rules that are actual rule objects, without the disguised
+ // objects
+ const rulesData = allRules.filter(
+ (rule) => !disguisedRules.map((rule) => rule.id).includes(rule.id!)
+ )
+
+ const rulesToCreate = rulesData.filter((rule) => !("id" in rule))
+ const rulesToUpdate = rulesData.filter(
+ (rule) => typeof rule.id === "string"
+ )
+
+ if (Object.keys(applicationMethodData).length) {
+ await updatePromotion({ application_method: applicationMethodData })
+ }
+
+ rulesToCreate.length &&
+ (await addPromotionRules({
+ rules: rulesToCreate.map((rule) => {
+ return {
+ attribute: rule.attribute,
+ operator: rule.operator,
+ values: rule.values,
+ } as any
+ }),
+ }))
+
+ rulesToRemove?.length &&
+ (await removePromotionRules({
+ rule_ids: rulesToRemove.map((r) => r.id!),
+ }))
+
+ rulesToUpdate.length &&
+ (await updatePromotionRules({
+ rules: rulesToUpdate.map((rule) => {
+ return {
+ id: rule.id!,
+ attribute: rule.attribute,
+ operator: rule.operator,
+ values: rule.values,
+ } as any
+ }),
+ }))
+
+ handleSuccess()
+ }
+ }
+
+ if (attributes && operators) {
+ return (
+
+ )
+ }
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts
new file mode 100644
index 0000000000..f8a67aac9f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts
@@ -0,0 +1 @@
+export * from "./edit-rules-wrapper"
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx
similarity index 63%
rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx
rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx
index 969dabb1ba..437d571592 100644
--- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx
@@ -2,13 +2,9 @@ import { PromotionRuleDTO } from "@medusajs/types"
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
-import { RouteDrawer } from "../../../components/route-modal"
-import {
- usePromotion,
- usePromotionRuleAttributes,
- usePromotionRuleOperators,
-} from "../../../hooks/api/promotions"
-import { EditRulesForm } from "./components/edit-rules-form"
+import { RouteDrawer } from "../../../../components/route-modal"
+import { usePromotion } from "../../../../hooks/api/promotions"
+import { EditRulesWrapper } from "./components/edit-rules-wrapper"
export enum RuleType {
RULES = "rules",
@@ -33,17 +29,7 @@ export const EditRules = () => {
const ruleType = params.ruleType as RuleTypeValues
const id = params.id as string
const rules: PromotionRuleDTO[] = []
- const { promotion, isLoading, isError, error } = usePromotion(id)
- const {
- attributes,
- isError: isAttributesError,
- error: attributesError,
- } = usePromotionRuleAttributes(ruleType!)
- const {
- operators,
- isError: isOperatorsError,
- error: operatorsError,
- } = usePromotionRuleOperators()
+ const { promotion, isPending: isLoading, isError, error } = usePromotion(id)
if (promotion) {
if (ruleType === RuleType.RULES) {
@@ -55,8 +41,8 @@ export const EditRules = () => {
}
}
- if (isError || isAttributesError || isOperatorsError) {
- throw error || attributesError || operatorsError
+ if (isError) {
+ throw error
}
return (
@@ -65,13 +51,11 @@ export const EditRules = () => {
{t(`promotions.edit.${ruleType}.title`)}
- {!isLoading && promotion && attributes && operators && (
-
)}
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/index.ts
similarity index 100%
rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts
rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/index.ts
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx
deleted file mode 100644
index c93a3eb04f..0000000000
--- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx
+++ /dev/null
@@ -1,442 +0,0 @@
-import { zodResolver } from "@hookform/resolvers/zod"
-import { XMarkMini } from "@medusajs/icons"
-import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
-import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui"
-import { Fragment, useState } from "react"
-import { useFieldArray, useForm } from "react-hook-form"
-import { useTranslation } from "react-i18next"
-import * as zod from "zod"
-import { Combobox } from "../../../../../components/common/combobox"
-import { Form } from "../../../../../components/common/form"
-import {
- RouteDrawer,
- useRouteModal,
-} from "../../../../../components/route-modal"
-import {
- usePromotionAddRules,
- usePromotionRemoveRules,
- usePromotionRuleValues,
- usePromotionUpdateRules,
- useUpdatePromotion,
-} from "../../../../../hooks/api/promotions"
-import { RuleTypeValues } from "../../edit-rules"
-import { getDisguisedRules } from "./utils"
-
-type EditPromotionFormProps = {
- promotion: PromotionDTO
- rules: PromotionRuleDTO[]
- ruleType: RuleTypeValues
- attributes: any[]
- operators: any[]
-}
-
-const EditRules = zod.object({
- rules: zod.array(
- zod.object({
- id: zod.string().optional(),
- attribute: zod.string().min(1, { message: "Required field" }),
- operator: zod.string().min(1, { message: "Required field" }),
- values: zod.union([
- zod.number().min(1, { message: "Required field" }),
- zod.string().min(1, { message: "Required field" }),
- zod.array(zod.string()).min(1, { message: "Required field" }),
- ]),
- required: zod.boolean().optional(),
- field_type: zod.string().optional(),
- })
- ),
-})
-
-const fetchOptionsForRule = (ruleType: string, attribute: string) => {
- const { values } = usePromotionRuleValues(ruleType, attribute)
-
- return values || []
-}
-
-export const EditRulesForm = ({
- promotion,
- rules,
- ruleType,
- attributes,
- operators,
-}: EditPromotionFormProps) => {
- const { t } = useTranslation()
- const { handleSuccess } = useRouteModal()
- const requiredAttributes = attributes?.filter((ra) => ra.required) || []
- const requiredAttributeValues = requiredAttributes?.map((ra) => ra.value)
- const disguisedRules =
- getDisguisedRules(promotion, requiredAttributes, ruleType) || []
- const [rulesToRemove, setRulesToRemove] = useState([])
-
- const form = useForm>({
- defaultValues: {
- rules: [...disguisedRules, ...rules].map((rule) => ({
- id: rule.id,
- required: requiredAttributeValues.includes(rule.attribute),
- field_type: rule.field_type,
- attribute: rule.attribute!,
- operator: rule.operator!,
- values: rule?.values?.map((v: { value: string }) => v.value!),
- })),
- },
- resolver: zodResolver(EditRules),
- })
-
- const { fields, append, remove, update } = useFieldArray({
- control: form.control,
- name: "rules",
- keyName: "rules_id",
- })
-
- const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id)
- const { mutateAsync: addPromotionRules } = usePromotionAddRules(
- promotion.id,
- ruleType
- )
-
- const { mutateAsync: removePromotionRules } = usePromotionRemoveRules(
- promotion.id,
- ruleType
- )
-
- const { mutateAsync: updatePromotionRules } = usePromotionUpdateRules(
- promotion.id,
- ruleType
- )
-
- const handleSubmit = form.handleSubmit(async (data) => {
- const applicationMethodData: Record = {}
- const { rules: allRules = [] } = data
-
- const disguisedRulesData = allRules.filter((rule) =>
- disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
- )
-
- // For all the rules that were disguised, convert them to actual values in the
- // database, they are currently all under application_method. If more of these are coming
- // up, abstract this away.
- for (const rule of disguisedRulesData) {
- applicationMethodData[rule.id!] = parseInt(rule.values as string)
- }
-
- // This variable will contain the rules that are actual rule objects, without the disguised
- // objects
- const rulesData = allRules.filter(
- (rule) =>
- !disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
- )
-
- const rulesToCreate = rulesData.filter((rule) => !("id" in rule))
- const rulesToUpdate = rulesData.filter(
- (rule) => typeof rule.id === "string"
- )
-
- if (Object.keys(applicationMethodData).length) {
- await updatePromotion({ application_method: applicationMethodData })
- }
-
- rulesToCreate.length &&
- (await addPromotionRules({
- rules: rulesToCreate.map((rule) => {
- return {
- attribute: rule.attribute,
- operator: rule.operator,
- values: rule.values,
- } as any
- }),
- }))
-
- rulesToRemove.length &&
- (await removePromotionRules({
- rule_ids: rulesToRemove.map((r) => r.id!),
- }))
-
- rulesToUpdate.length &&
- (await updatePromotionRules({
- rules: rulesToUpdate.map((rule) => {
- return {
- id: rule.id!,
- attribute: rule.attribute,
- operator: rule.operator,
- values: rule.values,
- } as any
- }),
- }))
-
- handleSuccess()
- })
-
- return (
-
-
-
- )
-}
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx
index a17bc289f0..adaeb1acad 100644
--- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { CampaignDTO, PromotionDTO } from "@medusajs/types"
-import { Button, RadioGroup, Select } from "@medusajs/ui"
+import { Button, clx, RadioGroup, Select } from "@medusajs/ui"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -23,6 +23,97 @@ const EditPromotionSchema = zod.object({
existing: zod.string().toLowerCase(),
})
+export const AddCampaignPromotionFields = ({ form, campaigns }) => {
+ const { t } = useTranslation()
+ const watchCampaignId = useWatch({
+ control: form.control,
+ name: "campaign_id",
+ })
+
+ const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
+
+ return (
+
+
{
+ return (
+
+ Method
+
+
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+ {
+ return (
+
+
+ {t("promotions.form.campaign.existing.title")}
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+ )
+}
+
export const AddCampaignPromotionForm = ({
promotion,
campaigns,
@@ -58,78 +149,7 @@ export const AddCampaignPromotionForm = ({