diff --git a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx index ed31acf72e..88710649d7 100644 --- a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx @@ -1,5 +1,10 @@ import { AdminGetPromotionsParams } from "@medusajs/medusa" -import { AdminRuleValueOptionsListResponse } from "@medusajs/types" +import { + AdminPromotionRuleListResponse, + AdminRuleAttributeOptionsListResponse, + AdminRuleOperatorOptionsListResponse, + AdminRuleValueOptionsListResponse, +} from "@medusajs/types" import { QueryKey, useMutation, @@ -21,21 +26,23 @@ import { PromotionDeleteRes, PromotionListRes, PromotionRes, - PromotionRuleAttributesListRes, - PromotionRuleOperatorsListRes, - PromotionRulesListRes, } from "../../types/api-responses" import { campaignsQueryKeys } from "./campaigns" const PROMOTIONS_QUERY_KEY = "promotions" as const export const promotionsQueryKeys = { ...queryKeysFactory(PROMOTIONS_QUERY_KEY), - listRules: (id: string | null, ruleType: string) => [ + // TODO: handle invalidations properly + listRules: ( + id: string | null, + ruleType: string, + query?: Record + ) => [PROMOTIONS_QUERY_KEY, id, ruleType, query], + listRuleAttributes: (ruleType: string, promotionType?: string) => [ PROMOTIONS_QUERY_KEY, - id, ruleType, + promotionType, ], - listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType], listRuleValues: (ruleType: string, ruleValue: string, query: object) => [ PROMOTIONS_QUERY_KEY, ruleType, @@ -64,19 +71,20 @@ export const usePromotion = ( export const usePromotionRules = ( id: string | null, ruleType: string, + query?: Record, options?: Omit< UseQueryOptions< - PromotionRulesListRes, + AdminPromotionRuleListResponse, Error, - PromotionRulesListRes, + AdminPromotionRuleListResponse, QueryKey >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.listRules(id, ruleType), - queryFn: async () => client.promotions.listRules(id, ruleType), + queryKey: promotionsQueryKeys.listRules(id, ruleType, query), + queryFn: async () => client.promotions.listRules(id, ruleType, query), ...options, }) @@ -102,9 +110,9 @@ export const usePromotions = ( export const usePromotionRuleOperators = ( options?: Omit< UseQueryOptions< - PromotionListRes, + AdminRuleOperatorOptionsListResponse, Error, - PromotionRuleOperatorsListRes, + AdminRuleOperatorOptionsListResponse, QueryKey >, "queryFn" | "queryKey" @@ -121,19 +129,21 @@ export const usePromotionRuleOperators = ( export const usePromotionRuleAttributes = ( ruleType: string, + promotionType?: string, options?: Omit< UseQueryOptions< - PromotionListRes, + AdminRuleAttributeOptionsListResponse, Error, - PromotionRuleAttributesListRes, + AdminRuleAttributeOptionsListResponse, QueryKey >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.listRuleAttributes(ruleType), - queryFn: async () => client.promotions.listRuleAttributes(ruleType), + queryKey: promotionsQueryKeys.listRuleAttributes(ruleType, promotionType), + queryFn: async () => + client.promotions.listRuleAttributes(ruleType, promotionType), ...options, }) @@ -207,10 +217,7 @@ export const useUpdatePromotion = ( return useMutation({ mutationFn: (payload) => client.promotions.update(id, payload), onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) - queryClient.invalidateQueries({ - queryKey: promotionsQueryKeys.detail(id), - }) + queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all }) options?.onSuccess?.(data, variables, context) }, @@ -226,10 +233,7 @@ export const usePromotionAddRules = ( return useMutation({ mutationFn: (payload) => client.promotions.addRules(id, ruleType, payload), onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) - queryClient.invalidateQueries({ - queryKey: promotionsQueryKeys.detail(id), - }) + queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all }) options?.onSuccess?.(data, variables, context) }, @@ -250,10 +254,7 @@ export const usePromotionRemoveRules = ( mutationFn: (payload) => client.promotions.removeRules(id, ruleType, payload), onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) - queryClient.invalidateQueries({ - queryKey: promotionsQueryKeys.detail(id), - }) + queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all }) options?.onSuccess?.(data, variables, context) }, @@ -274,13 +275,7 @@ export const usePromotionUpdateRules = ( mutationFn: (payload) => client.promotions.updateRules(id, ruleType, payload), onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) - queryClient.invalidateQueries({ - queryKey: promotionsQueryKeys.listRules(id, ruleType), - }) - queryClient.invalidateQueries({ - queryKey: promotionsQueryKeys.detail(id), - }) + queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all }) options?.onSuccess?.(data, variables, context) }, diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 0343312168..79e95175a1 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -981,15 +981,15 @@ "conditions": { "rules": { "title": "Who can use this code?", - "description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched. Choose between attributes, operators, and values to set up the conditions." + "description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched." }, "target-rules": { - "title": "What needs to be in the cart to unlock the promotion?", - "description": "If these conditions match, we enable a promotion action on the target items. Choose between attributes, operators, and values to set up the conditions." + "title": "What will the promotion be applied to?", + "description": "The promotion will be applied to items that match the following conditions" }, "buy-rules": { - "title": "What will the promotion be applied to?", - "description": "The promotion will be applied to items that match these conditions" + "title": "What needs to be in the cart to unlock the promotion?", + "description": "If these conditions match, we enable the promotion on the target items." } } }, diff --git a/packages/admin-next/dashboard/src/lib/client/promotions.ts b/packages/admin-next/dashboard/src/lib/client/promotions.ts index f1beea7bc3..deac50b069 100644 --- a/packages/admin-next/dashboard/src/lib/client/promotions.ts +++ b/packages/admin-next/dashboard/src/lib/client/promotions.ts @@ -81,15 +81,23 @@ async function removePromotionRules( ) } -async function listPromotionRules(id: string | null, ruleType: string) { +async function listPromotionRules( + id: string | null, + ruleType: string, + query?: Record +) { return getRequest( - `/admin/promotions/${id}/${ruleType}` + `/admin/promotions/${id}/${ruleType}`, + query ) } -async function listPromotionRuleAttributes(ruleType: string) { +async function listPromotionRuleAttributes( + ruleType: string, + promotionType?: string +) { return getRequest( - `/admin/promotions/rule-attribute-options/${ruleType}` + `/admin/promotions/rule-attribute-options/${ruleType}?promotion_type=${promotionType}` ) } diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx index 7cf022cb54..68089c9903 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx @@ -3,25 +3,23 @@ import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types" import { Button } from "@medusajs/ui" import i18n from "i18next" import { useState } from "react" -import { useFieldArray, useForm } from "react-hook-form" +import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" import { RouteDrawer } from "../../../../../../components/route-modal" import { RuleTypeValues } from "../../edit-rules" import { RulesFormField } from "../rules-form-field" -import { getDisguisedRules } from "./utils" type EditPromotionFormProps = { promotion: PromotionDTO rules: PromotionRuleDTO[] ruleType: RuleTypeValues - attributes: any[] - operators: any[] handleSubmit: any isSubmitting: boolean } const EditRules = zod.object({ + type: zod.string().optional(), rules: zod.array( zod.object({ id: zod.string().optional(), @@ -39,6 +37,7 @@ const EditRules = zod.object({ .min(1, { message: i18n.t("promotions.form.required") }), ]), required: zod.boolean().optional(), + disguised: zod.boolean().optional(), field_type: zod.string().optional(), }) ), @@ -46,41 +45,18 @@ const EditRules = zod.object({ 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), - attribute: rule.attribute!, - operator: rule.operator!, - values: Array.isArray(rule?.values) - ? rule?.values?.map((v: any) => v.value!) - : rule.values!, - })), - }, + defaultValues: { rules: [], type: promotion.type }, resolver: zodResolver(EditRules), }) - const { fields, append, remove, update } = useFieldArray({ - control: form.control, - name: "rules", - keyName: "rules_id", - }) - const handleFormSubmit = form.handleSubmit(handleSubmit(rulesToRemove)) return ( @@ -90,14 +66,9 @@ export const EditRulesForm = ({ diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts deleted file mode 100644 index 5cfe71c437..0000000000 --- a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { PromotionDTO } from "@medusajs/types" -import { RuleType } from "../../edit-rules" - -// We are disguising couple of database columns as rules here, namely -// apply_to_quantity and buy_rules_min_quantity. -// We need to transform the database value into a disugised "rule" shape -// for the form -export function getDisguisedRules( - promotion: PromotionDTO, - requiredAttributes: any[], - ruleType: string -) { - if (ruleType === RuleType.RULES && !requiredAttributes?.length) { - return [] - } - - const applyToQuantityRule = requiredAttributes.find( - (attr) => attr.id === "apply_to_quantity" - ) - - const buyRulesMinQuantityRule = requiredAttributes.find( - (attr) => attr.id === "buy_rules_min_quantity" - ) - - const currencyCodeRule = requiredAttributes.find( - (attr) => attr.id === "currency_code" - ) - - if (ruleType === RuleType.RULES) { - return [ - { - id: "currency_code", - attribute: "currency_code", - operator: "eq", - required: currencyCodeRule?.required, - values: promotion?.application_method?.currency_code?.toLowerCase(), - }, - ] - } - - if (ruleType === RuleType.TARGET_RULES) { - return [ - { - id: "apply_to_quantity", - attribute: "apply_to_quantity", - operator: "eq", - required: applyToQuantityRule?.required, - values: promotion?.application_method?.apply_to_quantity, - }, - ] - } - - if (ruleType === RuleType.BUY_RULES) { - return [ - { - id: "buy_rules_min_quantity", - attribute: "buy_rules_min_quantity", - operator: "eq", - required: buyRulesMinQuantityRule?.required, - values: [ - { value: promotion?.application_method?.buy_rules_min_quantity }, - ], - }, - ] - } -} diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx index 875b1dd7d4..655561ff7a 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx @@ -2,19 +2,19 @@ import { CreatePromotionRuleDTO, PromotionDTO, PromotionRuleDTO, + PromotionRuleOperatorValues, + PromotionRuleResponse, } 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" +import { getRuleValue } from "./utils" type EditPromotionFormProps = { promotion: PromotionDTO @@ -28,26 +28,6 @@ export const EditRulesWrapper = ({ 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, @@ -63,33 +43,21 @@ export const EditRulesWrapper = ({ usePromotionUpdateRules(promotion.id, ruleType) const handleSubmit = (rulesToRemove?: { id: string }[]) => { - return async function (data: { rules: PromotionRuleDTO[] }) { + return async function (data: { rules: PromotionRuleResponse[] }) { const applicationMethodData: Record = {} const { rules: allRules = [] } = data - const disguisedRulesData = allRules.filter((rule) => - disguisedRules.map((rule) => rule.id).includes(rule.id!) - ) + const disguisedRules = allRules.filter((rule) => rule.disguised) // 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) { - const currentAttribute = attributes?.find( - (attr) => attr.value === rule.attribute - ) - - applicationMethodData[rule.id!] = - currentAttribute?.field_type === "number" - ? parseInt(rule.values as unknown as string) - : rule.values + for (const rule of disguisedRules) { + applicationMethodData[rule.attribute] = getRuleValue(rule) } // 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 rulesData = allRules.filter((rule) => !rule.disguised) const rulesToCreate: CreatePromotionRuleDTO[] = rulesData.filter( (rule) => !("id" in rule) ) @@ -121,11 +89,11 @@ export const EditRulesWrapper = ({ rulesToUpdate.length && (await updatePromotionRules({ - rules: rulesToUpdate.map((rule: PromotionRuleDTO) => { + rules: rulesToUpdate.map((rule: PromotionRuleResponse) => { return { id: rule.id!, attribute: rule.attribute, - operator: rule.operator, + operator: rule.operator as PromotionRuleOperatorValues, values: rule.values as unknown as string | string[], } }), @@ -135,17 +103,13 @@ export const EditRulesWrapper = ({ } } - if (attributes && operators) { - return ( - - ) - } + return ( + + ) } diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/utils.ts b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/utils.ts new file mode 100644 index 0000000000..1916a36232 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/edit-rules-wrapper/utils.ts @@ -0,0 +1,13 @@ +import { PromotionRuleResponse } from "@medusajs/types" + +export const getRuleValue = (rule: PromotionRuleResponse) => { + if (rule.field_type === "number") { + return parseInt(rule.values as unknown as string) + } + + if (rule.field_type === "select") { + return rule.values[0] + } + + return rule.values +} diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/constants.ts b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/constants.ts new file mode 100644 index 0000000000..21c70423e6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/constants.ts @@ -0,0 +1,11 @@ +export const requiredProductRule = { + id: "product", + attribute: "items.product.id", + attribute_label: "Product", + operator: "eq", + operator_label: "Equal", + values: [], + required: true, + field_type: "select", + disguised: false, +} diff --git a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/rules-form-field.tsx b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/rules-form-field.tsx index 82941fa558..82681e1a16 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/rules-form-field.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/common/edit-rules/components/rules-form-field/rules-form-field.tsx @@ -1,31 +1,23 @@ import { XMarkMini } from "@medusajs/icons" -import { - RuleAttributeOptionsResponse, - RuleOperatorOptionsResponse, -} from "@medusajs/types" +import { PromotionRuleResponse } from "@medusajs/types" import { Badge, Button, Heading, Select, Text } from "@medusajs/ui" -import { Fragment } from "react" -import { - FieldValues, - Path, - UseFieldArrayAppend, - UseFieldArrayRemove, - UseFieldArrayUpdate, - UseFormReturn, -} from "react-hook-form" +import { Fragment, useEffect } from "react" +import { useFieldArray, UseFormReturn, useWatch } from "react-hook-form" import { useTranslation } from "react-i18next" import { Form } from "../../../../../../components/common/form" +import { + usePromotionRuleAttributes, + usePromotionRuleOperators, + usePromotionRules, +} from "../../../../../../hooks/api/promotions" +import { CreatePromotionSchemaType } from "../../../../promotion-create/components/create-promotion-form/form-schema" import { RuleValueFormField } from "../rule-value-form-field" +import { requiredProductRule } from "./constants" -type RulesFormFieldType = { - form: UseFormReturn +type RulesFormFieldType = { + promotionId?: string + form: UseFormReturn ruleType: "rules" | "target-rules" | "buy-rules" - fields: any[] - attributes: RuleAttributeOptionsResponse[] - operators: RuleOperatorOptionsResponse[] - removeRule: UseFieldArrayRemove - updateRule: UseFieldArrayUpdate - appendRule: UseFieldArrayAppend setRulesToRemove?: any rulesToRemove?: any scope?: @@ -34,20 +26,91 @@ type RulesFormFieldType = { | "application_method.target_rules" } -export const RulesFormField = ({ +const generateRuleAttributes = (rules?: PromotionRuleResponse[]) => + (rules || []).map((rule) => ({ + id: rule.id, + required: rule.required, + field_type: rule.field_type, + disguised: rule.disguised, + attribute: rule.attribute!, + operator: rule.operator!, + values: + rule.field_type === "number" + ? rule.values + : rule?.values?.map((v: { value: string }) => v.value!), + })) + +export const RulesFormField = ({ form, ruleType, - fields, - attributes, - operators, - removeRule, - updateRule, - appendRule, setRulesToRemove, rulesToRemove, scope = "rules", -}: RulesFormFieldType) => { + promotionId, +}: RulesFormFieldType) => { const { t } = useTranslation() + const formData = form.getValues() + const { attributes } = usePromotionRuleAttributes(ruleType, formData.type) + const { operators } = usePromotionRuleOperators() + + const { fields, append, remove, update, replace } = useFieldArray({ + control: form.control, + name: scope, + keyName: scope, + }) + + const promotionType: string = useWatch({ + control: form.control, + name: "type", + }) + + const query: Record = promotionType + ? { promotion_type: promotionType } + : {} + + const { rules, isLoading } = usePromotionRules( + promotionId || null, + ruleType, + query, + { + enabled: !!promotionType, + } + ) + + useEffect(() => { + if (isLoading) { + return + } + + if (ruleType === "rules" && !fields.length) { + replace(generateRuleAttributes(rules) as any) + } + + if (ruleType === "rules" && promotionType === "standard") { + form.resetField("application_method.buy_rules") + form.resetField("application_method.target_rules") + } + + if ( + ["buy-rules", "target-rules"].includes(ruleType) && + promotionType === "standard" + ) { + form.resetField(scope) + replace([]) + } + + if ( + ["buy-rules", "target-rules"].includes(ruleType) && + promotionType === "buyget" + ) { + form.resetField(scope) + const rulesToAppend = promotionId + ? rules + : [...rules, requiredProductRule] + + replace(generateRuleAttributes(rulesToAppend) as any) + } + }, [promotionType, isLoading]) return (
@@ -62,17 +125,17 @@ export const RulesFormField = ({ {fields.map((fieldRule: any, index) => { const identifier = fieldRule.id const { ref: attributeRef, ...attributeField } = form.register( - `${scope}.${index}.attribute` as Path + `${scope}.${index}.attribute` ) const { ref: operatorRef, ...operatorsField } = form.register( - `${scope}.${index}.operator` as Path + `${scope}.${index}.operator` ) const { ref: valuesRef, ...valuesField } = form.register( - `${scope}.${index}.values` as Path + `${scope}.${index}.values` ) return ( - +
({ ({ setRulesToRemove && setRulesToRemove([...rulesToRemove, fieldRule]) - removeRule(index) + remove(index) } }} /> @@ -229,7 +292,7 @@ export const RulesFormField = ({ variant="secondary" className="inline-block" onClick={() => { - appendRule({ + append({ attribute: "", operator: "", values: [], @@ -251,7 +314,7 @@ export const RulesFormField = ({ setRulesToRemove && setRulesToRemove(fields.filter((field: any) => !field.required)) - removeRule(indicesToRemove) + remove(indicesToRemove) }} > {t("promotions.fields.clearAll")} diff --git a/packages/admin-next/dashboard/src/routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx b/packages/admin-next/dashboard/src/routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx index 50f9cb23a1..7640f3316b 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx @@ -11,16 +11,11 @@ import { Text, } from "@medusajs/ui" import { useEffect, useMemo, useState } from "react" -import { useFieldArray, useForm, useWatch } from "react-hook-form" +import { useForm, useWatch } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" import { z } from "zod" -import { - PromotionRuleOperatorValues, - PromotionRuleResponse, - RuleAttributeOptionsResponse, - RuleOperatorOptionsResponse, -} from "@medusajs/types" +import { PromotionRuleOperatorValues } from "@medusajs/types" import { Divider } from "../../../../../components/common/divider" import { Form } from "../../../../../components/common/form" import { PercentageInput } from "../../../../../components/inputs/percentage-input" @@ -38,42 +33,13 @@ import { Tab } from "./constants" import { CreatePromotionSchema } from "./form-schema" import { templates } from "./templates" -type CreatePromotionFormProps = { - ruleAttributes: RuleAttributeOptionsResponse[] - targetRuleAttributes: RuleAttributeOptionsResponse[] - buyRuleAttributes: RuleAttributeOptionsResponse[] - operators: RuleOperatorOptionsResponse[] - rules: PromotionRuleResponse[] - targetRules: PromotionRuleResponse[] - buyRules: PromotionRuleResponse[] -} - -export const CreatePromotionForm = ({ - ruleAttributes, - targetRuleAttributes, - buyRuleAttributes, - operators, - rules, - targetRules, - buyRules, -}: CreatePromotionFormProps) => { +export const CreatePromotionForm = () => { const [tab, setTab] = useState(Tab.TYPE) const [detailsValidated, setDetailsValidated] = useState(false) const { t } = useTranslation() const { handleSuccess } = useRouteModal() - const generateRuleAttributes = (rules: PromotionRuleResponse[]) => - rules.map((rule) => ({ - id: rule.id, - required: rule.required, - field_type: rule.field_type, - disguised: rule.disguised, - attribute: rule.attribute!, - operator: rule.operator!, - values: rule?.values?.map((v: { value: string }) => v.value!), - })) - const form = useForm>({ defaultValues: { campaign_id: undefined, @@ -82,53 +48,20 @@ export const CreatePromotionForm = ({ is_automatic: "false", code: "", type: "standard", - rules: generateRuleAttributes(rules), + rules: [], application_method: { allocation: "each", type: "fixed", target_type: "items", max_quantity: 1, - target_rules: generateRuleAttributes(targetRules), - buy_rules: generateRuleAttributes(buyRules), + target_rules: [], + buy_rules: [], }, campaign: undefined, }, resolver: zodResolver(CreatePromotionSchema), }) - const { - fields: ruleFields, - append: appendRule, - remove: removeRule, - update: updateRule, - } = useFieldArray({ - control: form.control, - name: "rules", - keyName: "rules_id", - }) - - const { - fields: targetRuleFields, - append: appendTargetRule, - remove: removeTargetRule, - update: updateTargetRule, - } = useFieldArray({ - control: form.control, - name: "application_method.target_rules", - keyName: "target_rules_id", - }) - - const { - fields: buyRuleFields, - append: appendBuyRule, - remove: removeBuyRule, - update: updateBuyRule, - } = useFieldArray({ - control: form.control, - name: "application_method.buy_rules", - keyName: "buy_rules_id", - }) - const { mutateAsync: createPromotion } = useCreatePromotion() const handleSubmit = form.handleSubmit( @@ -265,14 +198,11 @@ export const CreatePromotionForm = ({ }) const isFixedValueType = watchValueType === "fixed" - const watchAllocation = useWatch({ control: form.control, name: "application_method.allocation", }) - const isAllocationEach = watchAllocation === "each" - useEffect(() => { if (watchAllocation === "across") { form.setValue("application_method.max_quantity", null) @@ -296,17 +226,6 @@ export const CreatePromotionForm = ({ const { campaigns } = useCampaigns(campaignQuery) - useEffect(() => { - if (isTypeStandard) { - form.setValue("application_method.buy_rules", undefined) - } else { - form.setValue( - "application_method.buy_rules", - generateRuleAttributes(buyRules) - ) - } - }, [isTypeStandard]) - const detailsProgress = useMemo(() => { if (detailsValidated) { return "completed" @@ -592,16 +511,7 @@ export const CreatePromotionForm = ({ - + @@ -756,7 +666,7 @@ export const CreatePromotionForm = ({ /> )} - {isTypeStandard && isAllocationEach && ( + {isTypeStandard && watchAllocation === "each" && (
+ {!isTypeStandard && ( + <> + + + + + )} + - - - - {!isTypeStandard && ( - - )} { - const { attributes: ruleAttributes } = usePromotionRuleAttributes("rules") - const { attributes: targetRuleAttributes } = - usePromotionRuleAttributes("target-rules") - const { attributes: buyRuleAttributes } = - usePromotionRuleAttributes("buy-rules") - - const { rules } = usePromotionRules(null, "rules") - const { rules: targetRules } = usePromotionRules(null, "target-rules") - const { rules: buyRules } = usePromotionRules(null, "buy-rules") - const { operators } = usePromotionRuleOperators() - - return ( - - {rules && - buyRules && - targetRules && - operators && - ruleAttributes && - targetRuleAttributes && - buyRuleAttributes && ( - - )} - - ) + return {} } diff --git a/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx b/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx index f041c3fa5d..f41519d652 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx @@ -30,7 +30,11 @@ function RuleBlock({ rule }: RuleProps) { v.label)} + list={ + rule.field_type === "number" + ? [rule.values] + : rule.values?.map((v) => v.label) + } />
diff --git a/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/promotion-detail.tsx b/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/promotion-detail.tsx index 87381ff9cd..cd57e5e543 100644 --- a/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/promotion-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/promotions/promotion-detail/promotion-detail.tsx @@ -17,9 +17,15 @@ export const PromotionDetail = () => { const { id } = useParams() const { promotion, isLoading } = usePromotion(id!, { initialData }) - const { rules } = usePromotionRules(id!, "rules") - const { rules: targetRules } = usePromotionRules(id!, "target-rules") - const { rules: buyRules } = usePromotionRules(id!, "buy-rules") + const query: Record = {} + + if (promotion?.type === "buyget") { + query.promotion_type = promotion.type + } + + const { rules } = usePromotionRules(id!, "rules", query) + const { rules: targetRules } = usePromotionRules(id!, "target-rules", query) + const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query) if (isLoading || !promotion) { return
Loading...
diff --git a/packages/core/types/src/http/promotion/admin/promotion-rule.ts b/packages/core/types/src/http/promotion/admin/promotion-rule.ts index b2b06cfd6f..2bfcb2ec9d 100644 --- a/packages/core/types/src/http/promotion/admin/promotion-rule.ts +++ b/packages/core/types/src/http/promotion/admin/promotion-rule.ts @@ -11,5 +11,5 @@ export interface PromotionRuleResponse { } export interface AdminPromotionRuleListResponse { - attributes: PromotionRuleResponse[] + rules: PromotionRuleResponse[] } diff --git a/packages/core/types/src/promotion/common/promotion.ts b/packages/core/types/src/promotion/common/promotion.ts index 80107c42c3..dbfb306784 100644 --- a/packages/core/types/src/promotion/common/promotion.ts +++ b/packages/core/types/src/promotion/common/promotion.ts @@ -13,6 +13,11 @@ import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule" */ export type PromotionTypeValues = "standard" | "buyget" +/** + * The promotion's possible rule types. + */ +export type RuleTypeValues = "rules" | "buy-rules" | "target-rules" + /** * The promotion details. */ diff --git a/packages/medusa/src/api/admin/promotions/[id]/[rule_type]/route.ts b/packages/medusa/src/api/admin/promotions/[id]/[rule_type]/route.ts index f7e9f9a7ba..5097499b12 100644 --- a/packages/medusa/src/api/admin/promotions/[id]/[rule_type]/route.ts +++ b/packages/medusa/src/api/admin/promotions/[id]/[rule_type]/route.ts @@ -10,8 +10,8 @@ import { MedusaResponse, } from "../../../../../types/routing" import { + getRuleAttributesMap, operatorsMap, - ruleAttributesMap, ruleQueryConfigurations, validateRuleType, } from "../../utils" @@ -33,7 +33,9 @@ export const GET = async ( }) const [promotion] = await remoteQuery(queryObject) - const ruleAttributes = ruleAttributesMap[ruleType] + const ruleAttributes = getRuleAttributesMap( + promotion?.type || req.query.promotion_type + )[ruleType] const promotionRules: any[] = [] if (dasherizedRuleType === RuleType.RULES) { @@ -49,8 +51,19 @@ export const GET = async ( const requiredRules = ruleAttributes.filter((attr) => !!attr.required) for (const disguisedRule of disguisedRules) { - const value = promotion?.application_method?.[disguisedRule.id] - const values = value ? [{ label: value, value }] : [] + const getValues = () => { + const value = promotion?.application_method?.[disguisedRule.id] + + if (disguisedRule.field_type === "number") { + return value + } + + if (value) { + return [{ label: value, value }] + } + + return [] + } transformedRules.push({ id: undefined, @@ -60,7 +73,7 @@ export const GET = async ( hydrate: disguisedRule.hydrate || false, operator: RuleOperator.EQ, operator_label: operatorsMap[RuleOperator.EQ].label, - values, + values: getValues(), disguised: true, required: true, }) @@ -90,7 +103,7 @@ export const GET = async ( entryPoint: queryConfig.entryPoint, variables: { filters: { - [queryConfig.valueAttr]: promotionRule.values.map((v) => v.value), + [queryConfig.valueAttr]: promotionRule.values?.map((v) => v.value), }, }, fields: [queryConfig.labelAttr, queryConfig.valueAttr], @@ -104,10 +117,11 @@ export const GET = async ( ]) ) - promotionRule.values = promotionRule.values.map((value) => ({ - value: value.value, - label: valueLabelMap.get(value.value) || value.value, - })) + promotionRule.values = + promotionRule.values?.map((value) => ({ + value: value.value, + label: valueLabelMap.get(value.value) || value.value, + })) || promotionRule.values if (!currentRuleAttribute.hydrate) { transformedRules.push({ diff --git a/packages/medusa/src/api/admin/promotions/middlewares.ts b/packages/medusa/src/api/admin/promotions/middlewares.ts index 312a8d1baf..5c47243fb7 100644 --- a/packages/medusa/src/api/admin/promotions/middlewares.ts +++ b/packages/medusa/src/api/admin/promotions/middlewares.ts @@ -1,4 +1,5 @@ import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { unlessPath } from "../../utils/unless-path" import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformQuery } from "../../utils/validate-query" import { createBatchBody } from "../../utils/validators" @@ -62,9 +63,12 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/admin/promotions/:id/:rule_type", middlewares: [ - validateAndTransformQuery( - AdminGetPromotionRuleTypeParams, - QueryConfig.retrieveTransformQueryConfig + unlessPath( + /.*\/promotions\/rule-attribute-options/, + validateAndTransformQuery( + AdminGetPromotionRuleTypeParams, + QueryConfig.retrieveTransformQueryConfig + ) ), ], }, @@ -118,4 +122,14 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["GET"], + matcher: "/admin/promotions/rule-attribute-options/:rule_type", + middlewares: [ + validateAndTransformQuery( + AdminGetPromotionRuleParams, + QueryConfig.listRuleTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api/admin/promotions/rule-attribute-options/[rule_type]/route.ts b/packages/medusa/src/api/admin/promotions/rule-attribute-options/[rule_type]/route.ts index 802db97d42..de5d3bdd71 100644 --- a/packages/medusa/src/api/admin/promotions/rule-attribute-options/[rule_type]/route.ts +++ b/packages/medusa/src/api/admin/promotions/rule-attribute-options/[rule_type]/route.ts @@ -2,17 +2,19 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../../types/routing" -import { ruleAttributesMap, validateRuleType } from "../../utils" +import { getRuleAttributesMap, validateRuleType } from "../../utils" +import { AdminGetPromotionRuleParamsType } from "../../validators" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const { rule_type: ruleType } = req.params validateRuleType(ruleType) - const attributes = ruleAttributesMap[ruleType] || [] + const attributes = + getRuleAttributesMap(req.query.promotion_type as string)[ruleType] || [] res.json({ attributes, diff --git a/packages/medusa/src/api/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts b/packages/medusa/src/api/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts index f0bf7bd0a5..f1623f7b11 100644 --- a/packages/medusa/src/api/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts +++ b/packages/medusa/src/api/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts @@ -11,12 +11,17 @@ import { validateRuleAttribute, validateRuleType, } from "../../../utils" +import { AdminGetPromotionRuleParamsType } from "../../../validators" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params + const { + rule_type: ruleType, + rule_attribute_id: ruleAttributeId, + promotion_type: promotionType, + } = req.params const queryConfig = ruleQueryConfigurations[ruleAttributeId] const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const filterableFields = req.filterableFields @@ -28,7 +33,7 @@ export const GET = async ( } validateRuleType(ruleType) - validateRuleAttribute(ruleType, ruleAttributeId) + validateRuleAttribute(promotionType, ruleType, ruleAttributeId) const { rows } = await remoteQuery( remoteQueryObjectFromString({ diff --git a/packages/medusa/src/api/admin/promotions/utils/rule-attributes-map.ts b/packages/medusa/src/api/admin/promotions/utils/rule-attributes-map.ts index 362a5355ca..b7942d0484 100644 --- a/packages/medusa/src/api/admin/promotions/utils/rule-attributes-map.ts +++ b/packages/medusa/src/api/admin/promotions/utils/rule-attributes-map.ts @@ -1,21 +1,11 @@ +import { PromotionType } from "@medusajs/utils" + export enum DisguisedRule { APPLY_TO_QUANTITY = "apply_to_quantity", BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity", CURRENCY_CODE = "currency_code", } -export const disguisedRulesMap = { - [DisguisedRule.APPLY_TO_QUANTITY]: { - relation: "application_method", - }, - [DisguisedRule.BUY_RULES_MIN_QUANTITY]: { - relation: "application_method", - }, - [DisguisedRule.CURRENCY_CODE]: { - relation: "application_method", - }, -} - const ruleAttributes = [ { id: DisguisedRule.CURRENCY_CODE, @@ -94,7 +84,7 @@ const commonAttributes = [ }, ] -const buyRuleAttributes = [ +const buyGetBuyRules = [ { id: DisguisedRule.BUY_RULES_MIN_QUANTITY, value: DisguisedRule.BUY_RULES_MIN_QUANTITY, @@ -103,10 +93,9 @@ const buyRuleAttributes = [ required: true, disguised: true, }, - ...commonAttributes, ] -const targetRuleAttributes = [ +const buyGetTargetRules = [ { id: DisguisedRule.APPLY_TO_QUANTITY, value: DisguisedRule.APPLY_TO_QUANTITY, @@ -115,11 +104,19 @@ const targetRuleAttributes = [ required: true, disguised: true, }, - ...commonAttributes, ] -export const ruleAttributesMap = { - rules: ruleAttributes, - "target-rules": targetRuleAttributes, - "buy-rules": buyRuleAttributes, +export const getRuleAttributesMap = (promotionType?: string) => { + const map = { + rules: [...ruleAttributes], + "target-rules": [...commonAttributes], + "buy-rules": [...commonAttributes], + } + + if (promotionType === PromotionType.BUYGET) { + map["buy-rules"].push(...buyGetBuyRules) + map["target-rules"].push(...buyGetTargetRules) + } + + return map } diff --git a/packages/medusa/src/api/admin/promotions/utils/validate-rule-attribute.ts b/packages/medusa/src/api/admin/promotions/utils/validate-rule-attribute.ts index f6e4211cc9..1b944bff86 100644 --- a/packages/medusa/src/api/admin/promotions/utils/validate-rule-attribute.ts +++ b/packages/medusa/src/api/admin/promotions/utils/validate-rule-attribute.ts @@ -1,11 +1,12 @@ import { MedusaError } from "@medusajs/utils" -import { ruleAttributesMap } from "./rule-attributes-map" +import { getRuleAttributesMap } from "./rule-attributes-map" export function validateRuleAttribute( + promotionType: string | undefined, ruleType: string, ruleAttributeId: string ) { - const ruleAttributes = ruleAttributesMap[ruleType] || [] + const ruleAttributes = getRuleAttributesMap(promotionType)[ruleType] || [] const ruleAttribute = ruleAttributes.find((obj) => obj.id === ruleAttributeId) if (!ruleAttribute) { diff --git a/packages/medusa/src/api/admin/promotions/validators.ts b/packages/medusa/src/api/admin/promotions/validators.ts index 900daf09f7..769744c4fb 100644 --- a/packages/medusa/src/api/admin/promotions/validators.ts +++ b/packages/medusa/src/api/admin/promotions/validators.ts @@ -47,12 +47,18 @@ export const AdminGetPromotionsParams = createFindParams({ export type AdminGetPromotionRuleParamsType = z.infer< typeof AdminGetPromotionRuleParams > -export const AdminGetPromotionRuleParams = createSelectParams() +export const AdminGetPromotionRuleParams = z.object({ + promotion_type: z.string().optional(), +}) export type AdminGetPromotionRuleTypeParamsType = z.infer< typeof AdminGetPromotionRuleTypeParams > -export const AdminGetPromotionRuleTypeParams = createSelectParams() +export const AdminGetPromotionRuleTypeParams = createSelectParams().merge( + z.object({ + promotion_type: z.string().optional(), + }) +) export type AdminGetPromotionsRuleValueParamsType = z.infer< typeof AdminGetPromotionsRuleValueParams