chore: buyget templates add default target + buy rules (#7500)

* chore: buyget templates add default target + buy rules

* chore: reposition

* chore: address comments

* chore: added fixes

* chore: fix typo

* chore: fix strictness checks
This commit is contained in:
Riqwan Thamir
2024-06-07 09:47:31 +02:00
committed by GitHub
parent bd302e678e
commit 1f1b996f63
23 changed files with 322 additions and 445 deletions
@@ -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<string, string>
) => [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<string, string>,
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)
},
@@ -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."
}
}
},
@@ -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<string, string>
) {
return getRequest<PromotionRuleAttributesListRes>(
`/admin/promotions/${id}/${ruleType}`
`/admin/promotions/${id}/${ruleType}`,
query
)
}
async function listPromotionRuleAttributes(ruleType: string) {
async function listPromotionRuleAttributes(
ruleType: string,
promotionType?: string
) {
return getRequest<PromotionRuleAttributesListRes>(
`/admin/promotions/rule-attribute-options/${ruleType}`
`/admin/promotions/rule-attribute-options/${ruleType}?promotion_type=${promotionType}`
)
}
@@ -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<zod.infer<typeof EditRules>>({
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 = ({
<RulesFormField
form={form}
ruleType={ruleType}
attributes={attributes}
operators={operators}
fields={fields}
setRulesToRemove={setRulesToRemove}
rulesToRemove={rulesToRemove}
appendRule={append}
removeRule={remove}
updateRule={update}
promotionId={promotion.id}
/>
</RouteDrawer.Body>
@@ -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 },
],
},
]
}
}
@@ -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<any, any> = {}
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 (
<EditRulesForm
promotion={promotion}
rules={rules}
ruleType={ruleType}
attributes={attributes}
operators={operators}
handleSubmit={handleSubmit}
isSubmitting={isPending}
/>
)
}
return (
<EditRulesForm
promotion={promotion}
rules={rules}
ruleType={ruleType}
handleSubmit={handleSubmit}
isSubmitting={isPending}
/>
)
}
@@ -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
}
@@ -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,
}
@@ -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<TSchema extends FieldValues> = {
form: UseFormReturn<TSchema>
type RulesFormFieldType = {
promotionId?: string
form: UseFormReturn<CreatePromotionSchemaType>
ruleType: "rules" | "target-rules" | "buy-rules"
fields: any[]
attributes: RuleAttributeOptionsResponse[]
operators: RuleOperatorOptionsResponse[]
removeRule: UseFieldArrayRemove
updateRule: UseFieldArrayUpdate<TSchema>
appendRule: UseFieldArrayAppend<TSchema>
setRulesToRemove?: any
rulesToRemove?: any
scope?:
@@ -34,20 +26,91 @@ type RulesFormFieldType<TSchema extends FieldValues> = {
| "application_method.target_rules"
}
export const RulesFormField = <TSchema extends FieldValues>({
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<TSchema>) => {
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<string, string> = 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 (
<div className="flex flex-col">
@@ -62,17 +125,17 @@ export const RulesFormField = <TSchema extends FieldValues>({
{fields.map((fieldRule: any, index) => {
const identifier = fieldRule.id
const { ref: attributeRef, ...attributeField } = form.register(
`${scope}.${index}.attribute` as Path<TSchema>
`${scope}.${index}.attribute`
)
const { ref: operatorRef, ...operatorsField } = form.register(
`${scope}.${index}.operator` as Path<TSchema>
`${scope}.${index}.operator`
)
const { ref: valuesRef, ...valuesField } = form.register(
`${scope}.${index}.values` as Path<TSchema>
`${scope}.${index}.values`
)
return (
<Fragment key={`${fieldRule.id}.${index}`}>
<Fragment key={`${fieldRule.id}.${index}.${fieldRule.attribute}`}>
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
<div className="grow">
<Form.Field
@@ -102,10 +165,10 @@ export const RulesFormField = <TSchema extends FieldValues>({
<Select
{...field}
onValueChange={(e) => {
updateRule(index, { ...fieldRule, values: [] })
update(index, { ...fieldRule, values: [] })
onChange(e)
}}
disabled={fieldRule.required}
disabled={fieldRule.disguised}
>
<Select.Trigger
ref={attributeRef}
@@ -149,7 +212,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
<Select
{...field}
onValueChange={onChange}
disabled={fieldRule.required}
disabled={fieldRule.disguised}
>
<Select.Trigger
ref={operatorRef}
@@ -203,7 +266,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
setRulesToRemove &&
setRulesToRemove([...rulesToRemove, fieldRule])
removeRule(index)
remove(index)
}
}}
/>
@@ -229,7 +292,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
variant="secondary"
className="inline-block"
onClick={() => {
appendRule({
append({
attribute: "",
operator: "",
values: [],
@@ -251,7 +314,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
setRulesToRemove &&
setRulesToRemove(fields.filter((field: any) => !field.required))
removeRule(indicesToRemove)
remove(indicesToRemove)
}}
>
{t("promotions.fields.clearAll")}
@@ -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>(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<z.infer<typeof CreatePromotionSchema>>({
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 = ({
<Divider />
<RulesFormField
form={form}
ruleType={"rules"}
attributes={ruleAttributes}
operators={operators}
fields={ruleFields}
appendRule={appendRule}
removeRule={removeRule}
updateRule={updateRule}
/>
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
@@ -756,7 +666,7 @@ export const CreatePromotionForm = ({
/>
)}
{isTypeStandard && isAllocationEach && (
{isTypeStandard && watchAllocation === "each" && (
<div className="flex gap-y-4">
<Form.Field
control={form.control}
@@ -800,33 +710,23 @@ export const CreatePromotionForm = ({
<Divider />
{!isTypeStandard && (
<>
<RulesFormField
form={form}
ruleType={"buy-rules"}
scope="application_method.buy_rules"
/>
<Divider />
</>
)}
<RulesFormField
form={form}
ruleType={"target-rules"}
attributes={targetRuleAttributes}
operators={operators}
fields={targetRuleFields}
appendRule={appendTargetRule}
removeRule={removeTargetRule}
updateRule={updateTargetRule}
scope="application_method.target_rules"
/>
<Divider />
{!isTypeStandard && (
<RulesFormField
form={form}
ruleType={"buy-rules"}
attributes={buyRuleAttributes}
operators={operators}
fields={buyRuleFields}
appendRule={appendBuyRule}
removeRule={removeBuyRule}
updateRule={updateBuyRule}
scope="application_method.buy_rules"
/>
)}
</ProgressTabs.Content>
<ProgressTabs.Content
@@ -32,7 +32,7 @@ export const CreatePromotionSchema = z
currency_code: z.string(),
max_quantity: z.number().optional().nullable(),
target_rules: RuleSchema,
buy_rules: RuleSchema.min(2).optional(),
buy_rules: RuleSchema,
type: z.enum(["fixed", "percentage"]),
target_type: z.enum(["order", "shipping_methods", "items"]),
}),
@@ -1,42 +1,6 @@
import { RouteFocusModal } from "../../../components/route-modal"
import {
usePromotionRuleAttributes,
usePromotionRuleOperators,
usePromotionRules,
} from "../../../hooks/api/promotions"
import { CreatePromotionForm } from "./components/create-promotion-form/create-promotion-form"
export const PromotionCreate = () => {
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 (
<RouteFocusModal>
{rules &&
buyRules &&
targetRules &&
operators &&
ruleAttributes &&
targetRuleAttributes &&
buyRuleAttributes && (
<CreatePromotionForm
ruleAttributes={ruleAttributes}
targetRuleAttributes={targetRuleAttributes}
buyRuleAttributes={buyRuleAttributes}
operators={operators}
rules={rules}
targetRules={targetRules}
buyRules={buyRules}
/>
)}
</RouteFocusModal>
)
return <RouteFocusModal>{<CreatePromotionForm />}</RouteFocusModal>
}
@@ -30,7 +30,11 @@ function RuleBlock({ rule }: RuleProps) {
<BadgeListSummary
inline
className="!txt-compact-small-plus"
list={rule.values.map((v) => v.label)}
list={
rule.field_type === "number"
? [rule.values]
: rule.values?.map((v) => v.label)
}
/>
</div>
</div>
@@ -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<string, string> = {}
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 <div>Loading...</div>