feat(dashboard,types,promotion,medusa): hide fields on promotions depending on templates (#7746)

**what:**

- hides different fields depending on the chosen template
- remove operator values API
- fixes to edit promotion rules
- make currency optional for promotion

RESOLVES CORE-2297
This commit is contained in:
Riqwan Thamir
2024-06-18 18:47:42 +02:00
committed by GitHub
parent 1451112f08
commit 0d04c548f5
24 changed files with 523 additions and 389 deletions
@@ -2,7 +2,6 @@ import { AdminGetPromotionsParams } from "@medusajs/medusa"
import {
AdminPromotionRuleListResponse,
AdminRuleAttributeOptionsListResponse,
AdminRuleOperatorOptionsListResponse,
AdminRuleValueOptionsListResponse,
} from "@medusajs/types"
import {
@@ -49,7 +48,6 @@ export const promotionsQueryKeys = {
ruleValue,
query,
],
listRuleOperators: () => [PROMOTIONS_QUERY_KEY],
}
export const usePromotion = (
@@ -107,26 +105,6 @@ export const usePromotions = (
return { ...data, ...rest }
}
export const usePromotionRuleOperators = (
options?: Omit<
UseQueryOptions<
AdminRuleOperatorOptionsListResponse,
Error,
AdminRuleOperatorOptionsListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: promotionsQueryKeys.listRuleOperators(),
queryFn: async () => client.promotions.listRuleOperators(),
...options,
})
return { ...data, ...rest }
}
export const usePromotionRuleAttributes = (
ruleType: string,
promotionType?: string,
@@ -1151,15 +1151,15 @@
"description": "The code your customers will enter during checkout."
},
"value": {
"title": "Value"
"title": "Promotion Value"
},
"value_type": {
"fixed": {
"title": "Fixed amount",
"title": "Promotion Value",
"description": "eg. 100"
},
"percentage": {
"title": "Percentage",
"title": "Promotion Value",
"description": "eg. 8%"
}
}
@@ -15,7 +15,6 @@ import {
PromotionListRes,
PromotionRes,
PromotionRuleAttributesListRes,
PromotionRuleOperatorsListRes,
} from "../../types/api-responses"
import { deleteRequest, getRequest, postRequest } from "./common"
@@ -112,12 +111,6 @@ async function listPromotionRuleValues(
)
}
async function listPromotionRuleOperators() {
return getRequest<PromotionRuleOperatorsListRes>(
`/admin/promotions/rule-operator-options`
)
}
export const promotions = {
retrieve: retrievePromotion,
list: listPromotions,
@@ -129,6 +122,5 @@ export const promotions = {
updateRules: updatePromotionRules,
listRules: listPromotionRules,
listRuleAttributes: listPromotionRuleAttributes,
listRuleOperators: listPromotionRuleOperators,
listRuleValues: listPromotionRuleValues,
}
@@ -1,14 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import i18n from "i18next"
import { useState } from "react"
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 { EditRules, EditRulesType } from "./form-schema"
type EditPromotionFormProps = {
promotion: PromotionDTO
@@ -18,31 +17,6 @@ type EditPromotionFormProps = {
isSubmitting: boolean
}
const EditRules = zod.object({
type: zod.string().optional(),
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(),
disguised: zod.boolean().optional(),
field_type: zod.string().optional(),
})
),
})
export const EditRulesForm = ({
promotion,
ruleType,
@@ -52,7 +26,7 @@ export const EditRulesForm = ({
const { t } = useTranslation()
const [rulesToRemove, setRulesToRemove] = useState([])
const form = useForm<zod.infer<typeof EditRules>>({
const form = useForm<EditRulesType>({
defaultValues: { rules: [], type: promotion.type },
resolver: zodResolver(EditRules),
})
@@ -64,11 +38,11 @@ export const EditRulesForm = ({
<form onSubmit={handleFormSubmit} className="flex h-full flex-col">
<RouteDrawer.Body>
<RulesFormField
form={form}
form={form as any}
ruleType={ruleType}
setRulesToRemove={setRulesToRemove}
rulesToRemove={rulesToRemove}
promotionId={promotion.id}
promotion={promotion}
/>
</RouteDrawer.Body>
@@ -0,0 +1,29 @@
import i18n from "i18next"
import { z } from "zod"
export const EditRules = z.object({
type: z.string().optional(),
rules: z.array(
z.object({
id: z.string().optional(),
attribute: z
.string()
.min(1, { message: i18n.t("promotions.form.required") }),
operator: z
.string()
.min(1, { message: i18n.t("promotions.form.required") }),
values: z.union([
z.number().min(1, { message: i18n.t("promotions.form.required") }),
z.string().min(1, { message: i18n.t("promotions.form.required") }),
z
.array(z.string())
.min(1, { message: i18n.t("promotions.form.required") }),
]),
required: z.boolean().optional(),
disguised: z.boolean().optional(),
field_type: z.string().optional(),
})
),
})
export type EditRulesType = z.infer<typeof EditRules>
@@ -0,0 +1,17 @@
import { PromotionRuleResponse } from "@medusajs/types"
export 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.operator === "eq"
? typeof rule.values === "object"
? rule.values[0]?.value
: rule.values
: rule?.values?.map((v: { value: string }) => v.value!),
}))
@@ -42,11 +42,15 @@ export const EditRulesWrapper = ({
const { mutateAsync: updatePromotionRules, isPending } =
usePromotionUpdateRules(promotion.id, ruleType)
const handleSubmit = (rulesToRemove?: { id: string }[]) => {
const handleSubmit = (
rulesToRemove?: { id: string; disguised: boolean; attribute: string }[]
) => {
return async function (data: { rules: PromotionRuleResponse[] }) {
const applicationMethodData: Record<any, any> = {}
const { rules: allRules = [] } = data
const disguisedRules = allRules.filter((rule) => rule.disguised)
const disguisedRulesToRemove =
rulesToRemove?.filter((r) => r.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
@@ -55,6 +59,10 @@ export const EditRulesWrapper = ({
applicationMethodData[rule.attribute] = getRuleValue(rule)
}
for (const rule of disguisedRulesToRemove) {
applicationMethodData[rule.attribute] = null
}
// This variable will contain the rules that are actual rule objects, without the disguised
// objects
const rulesData = allRules.filter((rule) => !rule.disguised)
@@ -77,14 +85,14 @@ export const EditRulesWrapper = ({
return {
attribute: rule.attribute,
operator: rule.operator,
values: rule.operator === "eq" ? rule.values[0] : rule.values,
values: rule.values,
} as any
}),
}))
rulesToRemove?.length &&
(await removePromotionRules({
rule_ids: rulesToRemove.map((r) => r.id!),
rule_ids: rulesToRemove.map((r) => r.id).filter(Boolean),
}))
rulesToUpdate.length &&
@@ -5,9 +5,5 @@ export const getRuleValue = (rule: PromotionRuleResponse) => {
return parseInt(rule.values as unknown as string)
}
if (rule.field_type === "select") {
return rule.values[0]
}
return rule.values
}
@@ -1,5 +1,5 @@
import { XMarkMini } from "@medusajs/icons"
import { PromotionRuleResponse } from "@medusajs/types"
import { PromotionDTO } from "@medusajs/types"
import { Badge, Button, Heading, Select, Text } from "@medusajs/ui"
import { Fragment, useEffect } from "react"
import { useFieldArray, UseFormReturn, useWatch } from "react-hook-form"
@@ -7,15 +7,15 @@ 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 { generateRuleAttributes } from "../edit-rules-form/utils"
import { RuleValueFormField } from "../rule-value-form-field"
import { requiredProductRule } from "./constants"
type RulesFormFieldType = {
promotionId?: string
promotion?: PromotionDTO
form: UseFormReturn<CreatePromotionSchemaType>
ruleType: "rules" | "target-rules" | "buy-rules"
setRulesToRemove?: any
@@ -26,32 +26,17 @@ type RulesFormFieldType = {
| "application_method.target_rules"
}
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,
setRulesToRemove,
rulesToRemove,
scope = "rules",
promotionId,
promotion,
}: 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,
@@ -59,21 +44,31 @@ export const RulesFormField = ({
keyName: scope,
})
const promotionType: string = useWatch({
const promotionType = useWatch({
control: form.control,
name: "type",
defaultValue: promotion?.type,
})
const applicationMethodType = useWatch({
control: form.control,
name: "application_method.type",
defaultValue: promotion?.application_method?.type,
})
const query: Record<string, string> = promotionType
? { promotion_type: promotionType }
? {
promotion_type: promotionType,
application_method_type: applicationMethodType,
}
: {}
const { rules, isLoading } = usePromotionRules(
promotionId || null,
promotion?.id || null,
ruleType,
query,
{
enabled: !!promotionType,
enabled: !!promotion?.id || (!!promotionType && !!applicationMethodType),
}
)
@@ -104,12 +99,18 @@ export const RulesFormField = ({
promotionType === "buyget"
) {
form.resetField(scope)
const rulesToAppend = promotionId
const rulesToAppend = promotion?.id
? rules
: [...rules, requiredProductRule]
replace(generateRuleAttributes(rulesToAppend) as any)
return
}
form.resetField(scope)
replace(generateRuleAttributes(rules) as any)
}, [promotionType, isLoading])
return (
@@ -165,10 +166,18 @@ export const RulesFormField = ({
<Select
{...field}
onValueChange={(e) => {
update(index, { ...fieldRule, values: [] })
const currentAttributeOption =
attributeOptions.find((ao) => ao.id === e)
update(index, {
...fieldRule,
values: [],
disguised:
currentAttributeOption?.disguised || false,
})
onChange(e)
}}
disabled={fieldRule.disguised}
disabled={fieldRule.required}
>
<Select.Trigger
ref={attributeRef}
@@ -206,14 +215,14 @@ export const RulesFormField = ({
key={`${identifier}.${scope}.${operatorsField.name}`}
{...operatorsField}
render={({ field: { onChange, ref, ...field } }) => {
const currentAttributeOption = attributes.find(
(attr) => attr.value === fieldRule.attribute
)
return (
<Form.Item className="basis-1/2">
<Form.Control>
<Select
{...field}
onValueChange={onChange}
disabled={fieldRule.disguised}
>
<Select {...field} onValueChange={onChange}>
<Select.Trigger
ref={operatorRef}
className="bg-ui-bg-base"
@@ -222,16 +231,18 @@ export const RulesFormField = ({
</Select.Trigger>
<Select.Content>
{operators?.map((c, i) => (
<Select.Item
key={`${identifier}-operator-option-${i}`}
value={c.value}
>
<span className="text-ui-fg-subtle">
{c.label}
</span>
</Select.Item>
))}
{currentAttributeOption?.operators?.map(
(c, i) => (
<Select.Item
key={`${identifier}-operator-option-${i}`}
value={c.value}
>
<span className="text-ui-fg-subtle">
{c.label}
</span>
</Select.Item>
)
)}
</Select.Content>
</Select>
</Form.Control>
@@ -262,8 +273,7 @@ export const RulesFormField = ({
}`}
onClick={() => {
if (!fieldRule.required) {
fieldRule.id &&
setRulesToRemove &&
setRulesToRemove &&
setRulesToRemove([...rulesToRemove, fieldRule])
remove(index)
@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
Alert,
Badge,
Button,
clx,
CurrencyInput,
@@ -15,7 +16,13 @@ import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { z } from "zod"
import { PromotionRuleOperatorValues } from "@medusajs/types"
import {
ApplicationMethodAllocationValues,
ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues,
PromotionRuleOperatorValues,
PromotionTypeValues,
} from "@medusajs/types"
import { Divider } from "../../../../../components/common/divider"
import { Form } from "../../../../../components/common/form"
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
@@ -33,6 +40,25 @@ import { Tab } from "./constants"
import { CreatePromotionSchema } from "./form-schema"
import { templates } from "./templates"
const defaultValues = {
campaign_id: undefined,
template_id: templates[0].id!,
campaign_choice: "none" as "none",
is_automatic: "false",
code: "",
type: "standard" as PromotionTypeValues,
rules: [],
application_method: {
allocation: "each" as ApplicationMethodAllocationValues,
type: "fixed" as ApplicationMethodTypeValues,
target_type: "items" as ApplicationMethodTargetTypeValues,
max_quantity: 1,
target_rules: [],
buy_rules: [],
},
campaign: undefined,
}
export const CreatePromotionForm = () => {
const [tab, setTab] = useState<Tab>(Tab.TYPE)
const [detailsValidated, setDetailsValidated] = useState(false)
@@ -41,24 +67,7 @@ export const CreatePromotionForm = () => {
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof CreatePromotionSchema>>({
defaultValues: {
campaign_id: undefined,
template_id: templates[0].id!,
campaign_choice: "none",
is_automatic: "false",
code: "",
type: "standard",
rules: [],
application_method: {
allocation: "each",
type: "fixed",
target_type: "items",
max_quantity: 1,
target_rules: [],
buy_rules: [],
},
campaign: undefined,
},
defaultValues,
resolver: zodResolver(CreatePromotionSchema),
})
@@ -172,7 +181,7 @@ export const CreatePromotionForm = () => {
name: "template_id",
})
useMemo(() => {
const currentTemplate = useMemo(() => {
const currentTemplate = templates.find(
(template) => template.id === watchTemplateId
)
@@ -181,6 +190,8 @@ export const CreatePromotionForm = () => {
return
}
form.reset({ ...defaultValues, template_id: watchTemplateId })
for (const [key, value] of Object.entries(currentTemplate.defaults)) {
if (typeof value === "object") {
for (const [subKey, subValue] of Object.entries(value)) {
@@ -190,6 +201,8 @@ export const CreatePromotionForm = () => {
form.setValue(key, value)
}
}
return currentTemplate
}, [watchTemplateId])
const watchValueType = useWatch({
@@ -215,6 +228,14 @@ export const CreatePromotionForm = () => {
})
const isTypeStandard = watchType === "standard"
const targetType = useWatch({
control: form.control,
name: "application_method.target_type",
})
const isTargetTypeOrder = targetType === "order"
const formData = form.getValues()
let campaignQuery: object = {}
@@ -385,9 +406,22 @@ export const CreatePromotionForm = () => {
<ProgressTabs.Content
value={Tab.PROMOTION}
className="flex flex-1 flex-col gap-10"
className="flex flex-1 flex-col gap-8"
>
<Heading level="h2">{t(`promotions.sections.details`)}</Heading>
<Heading level="h1" className="text-fg-base">
{t(`promotions.sections.details`)}
{currentTemplate?.title && (
<Badge
className="ml-2 align-middle"
color="grey"
size="2xsmall"
rounded="full"
>
{currentTemplate?.title}
</Badge>
)}
</Heading>
{form.formState.errors.root && (
<Alert
@@ -471,167 +505,14 @@ export const CreatePromotionForm = () => {
/>
</div>
<Form.Field
control={form.control}
name="type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("promotions.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"standard"}
label={t("promotions.form.type.standard.title")}
description={t(
"promotions.form.type.standard.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"buyget"}
label={t("promotions.form.type.buyget.title")}
description={t(
"promotions.form.type.buyget.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Divider />
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
<Form.Field
control={form.control}
name="application_method.type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.value_type")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"fixed"}
label={t("promotions.form.value_type.fixed.title")}
description={t(
"promotions.form.value_type.fixed.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"percentage"}
label={t(
"promotions.form.value_type.percentage.title"
)}
description={t(
"promotions.form.value_type.percentage.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="flex gap-y-4">
{!currentTemplate?.hiddenFields?.includes("type") && (
<Form.Field
control={form.control}
name="application_method.value"
render={({ field: { onChange, value, ...field } }) => {
const currencyCode =
form.getValues().application_method.currency_code
return (
<Form.Item className="basis-1/2">
<Form.Label
tooltip={
currencyCode || !isFixedValueType
? undefined
: t("promotions.fields.amount.tooltip")
}
>
{isFixedValueType
? t("fields.amount")
: t("fields.percentage")}
</Form.Label>
<Form.Control>
{isFixedValueType ? (
<CurrencyInput
{...field}
min={0}
onValueChange={(value) => {
onChange(value ? parseInt(value) : "")
}}
code={currencyCode}
symbol={
currencyCode
? getCurrencySymbol(currencyCode)
: ""
}
value={value}
disabled={!currencyCode}
/>
) : (
<PercentageInput
key="amount"
className="text-right"
min={0}
max={100}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
{isTypeStandard && (
<Form.Field
control={form.control}
name="application_method.allocation"
name="type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.allocation")}
</Form.Label>
<Form.Label>{t("promotions.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
@@ -639,21 +520,19 @@ export const CreatePromotionForm = () => {
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"each"}
label={t("promotions.form.allocation.each.title")}
value={"standard"}
label={t("promotions.form.type.standard.title")}
description={t(
"promotions.form.allocation.each.description"
"promotions.form.type.standard.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"across"}
label={t(
"promotions.form.allocation.across.title"
)}
value={"buyget"}
label={t("promotions.form.type.buyget.title")}
description={t(
"promotions.form.allocation.across.description"
"promotions.form.type.buyget.description"
)}
className={clx("basis-1/2")}
/>
@@ -666,8 +545,141 @@ export const CreatePromotionForm = () => {
/>
)}
{isTypeStandard && watchAllocation === "each" && (
<div className="flex gap-y-4">
<Divider />
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
{!currentTemplate?.hiddenFields?.includes(
"application_method.type"
) && (
<Form.Field
control={form.control}
name="application_method.type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.value_type")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"fixed"}
label={t(
"promotions.form.value_type.fixed.title"
)}
description={t(
"promotions.form.value_type.fixed.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"percentage"}
label={t(
"promotions.form.value_type.percentage.title"
)}
description={t(
"promotions.form.value_type.percentage.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<div className="flex gap-y-4 gap-x-2">
{!currentTemplate?.hiddenFields?.includes(
"application_method.value"
) && (
<Form.Field
control={form.control}
name="application_method.value"
render={({ field: { onChange, value, ...field } }) => {
const currencyCode =
form.getValues().application_method.currency_code
return (
<Form.Item className="basis-1/2">
<Form.Label
tooltip={
currencyCode || !isFixedValueType
? undefined
: t("promotions.fields.amount.tooltip")
}
>
{t("promotions.form.value.title")}
</Form.Label>
<Form.Control>
{isFixedValueType ? (
<CurrencyInput
{...field}
min={0}
onValueChange={(value) => {
onChange(value ? parseInt(value) : "")
}}
code={currencyCode}
symbol={
currencyCode
? getCurrencySymbol(currencyCode)
: ""
}
value={value}
disabled={!currencyCode}
/>
) : (
<PercentageInput
key="amount"
className="text-right"
min={0}
max={100}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey={
isFixedValueType
? "promotions.form.value_type.fixed.description"
: "promotions.form.value_type.percentage.description"
}
components={[<br key="break" />]}
/>
</Text>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{isTypeStandard && watchAllocation === "each" && (
<Form.Field
control={form.control}
name="application_method.max_quantity"
@@ -705,10 +717,58 @@ export const CreatePromotionForm = () => {
)
}}
/>
</div>
)}
)}
</div>
<Divider />
{isTypeStandard &&
!currentTemplate?.hiddenFields?.includes(
"application_method.allocation"
) && (
<Form.Field
control={form.control}
name="application_method.allocation"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.allocation")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"each"}
label={t(
"promotions.form.allocation.each.title"
)}
description={t(
"promotions.form.allocation.each.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"across"}
label={t(
"promotions.form.allocation.across.title"
)}
description={t(
"promotions.form.allocation.across.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{!isTypeStandard && (
<>
@@ -717,16 +777,19 @@ export const CreatePromotionForm = () => {
ruleType={"buy-rules"}
scope="application_method.buy_rules"
/>
<Divider />
</>
)}
<RulesFormField
form={form}
ruleType={"target-rules"}
scope="application_method.target_rules"
/>
{!isTargetTypeOrder && (
<>
<Divider />
<RulesFormField
form={form}
ruleType={"target-rules"}
scope="application_method.target_rules"
/>
</>
)}
</ProgressTabs.Content>
<ProgressTabs.Content
@@ -29,7 +29,7 @@ export const CreatePromotionSchema = z
application_method: z.object({
allocation: z.enum(["each", "across"]),
value: z.number().min(0),
currency_code: z.string(),
currency_code: z.string().optional(),
max_quantity: z.number().optional().nullable(),
target_rules: RuleSchema,
buy_rules: RuleSchema,
@@ -54,3 +54,5 @@ export const CreatePromotionSchema = z
message: `required field`,
}
)
export type CreatePromotionSchemaType = z.infer<typeof CreatePromotionSchema>
@@ -1,9 +1,16 @@
const commonHiddenFields = [
"type",
"application_method.type",
"application_method.allocation",
]
export const templates = [
{
id: "amount_off_products",
type: "standard",
title: "Amount off products",
description: "Discount specific products or collection of products",
hiddenFields: [...commonHiddenFields],
defaults: {
is_automatic: "false",
type: "standard",
@@ -19,6 +26,7 @@ export const templates = [
type: "standard",
title: "Amount off order",
description: "Discounts the total order amount",
hiddenFields: [...commonHiddenFields],
defaults: {
is_automatic: "false",
type: "standard",
@@ -34,6 +42,7 @@ export const templates = [
type: "standard",
title: "Percentage off product",
description: "Discounts a percentage off selected products",
hiddenFields: [...commonHiddenFields],
defaults: {
is_automatic: "false",
type: "standard",
@@ -49,12 +58,13 @@ export const templates = [
type: "standard",
title: "Percentage off order",
description: "Discounts a percentage of the total order amount",
hiddenFields: [...commonHiddenFields],
defaults: {
is_automatic: "false",
type: "standard",
application_method: {
allocation: "across",
target_type: "items",
target_type: "order",
type: "percentage",
},
},
@@ -64,6 +74,7 @@ export const templates = [
type: "buy_get",
title: "Buy X Get Y",
description: "Buy X product(s), get Y product(s)",
hiddenFields: [...commonHiddenFields, "application_method.value"],
defaults: {
is_automatic: "false",
type: "buyget",
@@ -82,7 +82,7 @@ export const PromotionConditionsSection = ({
className="h-[180px]"
title="No records yet."
message="Please check back later or add a target condition today"
action={{ to: "/promotions", label: "Add condition" }}
action={{ to: `${ruleType}/edit`, label: "Add condition" }}
buttonVariant="transparentIconLeft"
/>
)}