feat(dashboard): add campaign create to promotion UI (#7306)

* chore(medusa): strict zod versions in workspace

* feat(dashboard): add campaign create to promotion UI

* chore: fix bug with form reset

* chore: address reviews
This commit is contained in:
Riqwan Thamir authored and GitHub committed 2024-05-15 10:43:13 +02:00
1 parent 5c14d283d0
commit bb2b041954
10 files changed
+394 -338

No files matched your search

@@ -25,6 +25,7 @@ import {
PromotionRulesListRes, PromotionRulesListRes,
PromotionRuleValuesListRes, PromotionRuleValuesListRes,
} from "../../types/api-responses" } from "../../types/api-responses"
import { campaignsQueryKeys } from "./campaigns"
const PROMOTIONS_QUERY_KEY = "promotions" as const const PROMOTIONS_QUERY_KEY = "promotions" as const
export const promotionsQueryKeys = { export const promotionsQueryKeys = {
@@ -185,6 +186,7 @@ export const useCreatePromotion = (
mutationFn: (payload) => client.promotions.create(payload), mutationFn: (payload) => client.promotions.create(payload),
onSuccess: (data, variables, context) => { onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
...options, ...options,
@@ -19,7 +19,6 @@ import {
CreateShippingProfileDTO, CreateShippingProfileDTO,
CreateStockLocationInput, CreateStockLocationInput,
InventoryNext, InventoryNext,
ShippingOptionDTO,
UpdateApiKeyDTO, UpdateApiKeyDTO,
UpdateCampaignDTO, UpdateCampaignDTO,
UpdateCustomerDTO, UpdateCustomerDTO,
@@ -102,7 +101,7 @@ export type DeletePriceListPricesReq = { ids: string[] }
// Promotion // Promotion
export type CreatePromotionReq = CreatePromotionDTO export type CreatePromotionReq = CreatePromotionDTO
export type UpdatePromotionReq = UpdatePromotionDTO export type UpdatePromotionReq = Omit<UpdatePromotionDTO, "id">
export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] } export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] }
export type BatchRemovePromotionRulesReq = { rule_ids: string[] } export type BatchRemovePromotionRulesReq = { rule_ids: string[] }
export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] } export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
@@ -1,60 +1,50 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { import { Button, toast } from "@medusajs/ui"
Button, import { useForm } from "react-hook-form"
clx,
CurrencyInput,
DatePicker,
Heading,
Input,
RadioGroup,
Select,
Text,
toast,
} from "@medusajs/ui"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { CampaignBudgetTypeValues } from "@medusajs/types"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { useCreateCampaign } from "../../../../../hooks/api/campaigns" import { useCreateCampaign } from "../../../../../hooks/api/campaigns"
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies" import { CreateCampaignFormFields } from "../../../common/components/create-campaign-form-fields"
const CreateCampaignSchema = zod.object({ export const CreateCampaignSchema = zod.object({
name: zod.string(), name: zod.string().min(1),
description: zod.string().optional(), description: zod.string().optional(),
currency: zod.string(), currency: zod.string().min(1),
campaign_identifier: zod.string(), campaign_identifier: zod.string().min(1),
starts_at: zod.date().optional(), starts_at: zod.date().optional(),
ends_at: zod.date().optional(), ends_at: zod.date().optional(),
budget: zod.object({ budget: zod.object({
limit: zod.number().min(0), limit: zod.number().min(0),
type: zod.enum(["spend", "usage"]).optional(), type: zod.enum(["spend", "usage"]),
}), }),
}) })
export const defaultCampaignValues = {
name: "",
description: "",
currency: "",
campaign_identifier: "",
starts_at: undefined,
ends_at: undefined,
budget: {
type: "spend" as CampaignBudgetTypeValues,
limit: undefined,
},
}
export const CreateCampaignForm = () => { export const CreateCampaignForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const { mutateAsync, isPending } = useCreateCampaign() const { mutateAsync, isPending } = useCreateCampaign()
const form = useForm<zod.infer<typeof CreateCampaignSchema>>({ const form = useForm<zod.infer<typeof CreateCampaignSchema>>({
defaultValues: { defaultValues: defaultCampaignValues,
name: "",
description: "",
currency: "",
campaign_identifier: "",
starts_at: undefined,
ends_at: undefined,
budget: {
type: "spend",
limit: undefined,
},
},
resolver: zodResolver(CreateCampaignSchema), resolver: zodResolver(CreateCampaignSchema),
}) })
@@ -92,18 +82,6 @@ export const CreateCampaignForm = () => {
) )
}) })
const watchValueType = useWatch({
control: form.control,
name: "budget.type",
})
const isTypeSpend = watchValueType === "spend"
const currencyValue = useWatch({
control: form.control,
name: "currency",
})
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -127,259 +105,7 @@ export const CreateCampaignForm = () => {
</RouteFocusModal.Header> </RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center py-16"> <RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8"> <CreateCampaignFormFields form={form} />
<div>
<Heading>{t("campaigns.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("campaigns.create.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="description"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.description")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="campaign_identifier"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("campaigns.fields.identifier")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="currency"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("fields.currency")}</Form.Label>
<Form.Control>
<Select {...field} onValueChange={onChange}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{Object.values(currencies).map((currency) => (
<Select.Item
value={currency.code}
key={currency.code}
>
{currency.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="starts_at"
render={({
field: { value, onChange, ref: _ref, ...field },
}) => {
return (
<Form.Item>
<Form.Label>
{t("campaigns.fields.start_date")}
</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => {
onChange(v ?? null)
}}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="ends_at"
render={({
field: { value, onChange, ref: _ref, ...field },
}) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => onChange(v ?? null)}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<div>
<Heading>{t("campaigns.budget.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("campaigns.budget.create.hint")}
</Text>
</div>
<Form.Field
control={form.control}
name="budget.type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.budget.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"spend" === field.value,
})}
value={"spend"}
label={t("campaigns.budget.type.spend.title")}
description={t(
"campaigns.budget.type.spend.description"
)}
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"usage" === field.value,
})}
value={"usage"}
label={t("campaigns.budget.type.usage.title")}
description={t(
"campaigns.budget.type.usage.description"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="budget.limit"
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>
{t("campaigns.budget.fields.limit")}
</Form.Label>
<Form.Control>
{isTypeSpend ? (
<CurrencyInput
min={0}
onValueChange={(value) =>
onChange(value ? parseInt(value) : "")
}
code={currencyValue}
symbol={
currencyValue
? getCurrencySymbol(currencyValue)
: ""
}
{...field}
value={value}
/>
) : (
<Input
key="usage"
min={0}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</RouteFocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</RouteFocusModal.Form> </RouteFocusModal.Form>
@@ -0,0 +1,274 @@
import {
clx,
CurrencyInput,
DatePicker,
Heading,
Input,
RadioGroup,
Select,
Text,
} from "@medusajs/ui"
import { useEffect } from "react"
import { useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies"
export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
const { t } = useTranslation()
const watchValueType = useWatch({
control: form.control,
name: `${fieldScope}budget.type`,
})
const isTypeSpend = watchValueType === "spend"
const currencyValue = useWatch({
control: form.control,
name: `${fieldScope}currency`,
})
useEffect(() => {
form.setValue(`${fieldScope}budget.limit`, undefined)
}, [watchValueType])
return (
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("campaigns.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("campaigns.create.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name={`${fieldScope}name`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}description`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.description")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}campaign_identifier`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.identifier")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}currency`}
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("fields.currency")}</Form.Label>
<Form.Control>
<Select {...field} onValueChange={onChange}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{Object.values(currencies).map((currency) => (
<Select.Item value={currency.code} key={currency.code}>
{currency.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}starts_at`}
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => {
onChange(v ?? null)
}}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}ends_at`}
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => onChange(v ?? null)}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<div>
<Heading>{t("campaigns.budget.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("campaigns.budget.create.hint")}
</Text>
</div>
<Form.Field
control={form.control}
name={`${fieldScope}budget.type`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.budget.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"spend" === field.value,
})}
value={"spend"}
label={t("campaigns.budget.type.spend.title")}
description={t("campaigns.budget.type.spend.description")}
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"usage" === field.value,
})}
value={"usage"}
label={t("campaigns.budget.type.usage.title")}
description={t("campaigns.budget.type.usage.description")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name={`${fieldScope}budget.limit`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>{t("campaigns.budget.fields.limit")}</Form.Label>
<Form.Control>
{isTypeSpend ? (
<CurrencyInput
min={0}
onValueChange={(value) =>
onChange(value ? parseInt(value) : "")
}
code={currencyValue}
symbol={
currencyValue ? getCurrencySymbol(currencyValue) : ""
}
{...field}
value={value}
/>
) : (
<Input
type="number"
key="usage"
min={0}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
)
}
@@ -0,0 +1 @@
export * from "./create-campaign-form-fields"
@@ -1,29 +1,38 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { CampaignDTO, PromotionDTO } from "@medusajs/types" import { CampaignResponse, PromotionDTO } from "@medusajs/types"
import { Button, clx, RadioGroup, Select } from "@medusajs/ui" import { Button, clx, RadioGroup, Select } from "@medusajs/ui"
import { useEffect } from "react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { CampaignDetails } from "./campaign-details"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { useUpdatePromotion } from "../../../../../hooks/api/promotions" import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
import { CreateCampaignFormFields } from "../../../../campaigns/common/components/create-campaign-form-fields"
import { CampaignDetails } from "./campaign-details"
type EditPromotionFormProps = { type EditPromotionFormProps = {
promotion: PromotionDTO promotion: PromotionDTO
campaigns: CampaignDTO[] campaigns: CampaignResponse[]
} }
const EditPromotionSchema = zod.object({ const EditPromotionSchema = zod.object({
campaign_id: zod.string().optional(), campaign_id: zod.string().optional().nullable(),
existing: zod.string().toLowerCase(), campaign_choice: zod.enum(["none", "existing"]).optional(),
}) })
export const AddCampaignPromotionFields = ({ form, campaigns }) => { export const AddCampaignPromotionFields = ({
form,
campaigns,
withNewCampaign = true,
}: {
form: any
campaigns: CampaignResponse[]
withNewCampaign?: boolean
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const watchCampaignId = useWatch({ const watchCampaignId = useWatch({
control: form.control, control: form.control,
@@ -46,9 +55,10 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
return ( return (
<Form.Item> <Form.Item>
<Form.Label>Method</Form.Label> <Form.Label>Method</Form.Label>
<Form.Control> <Form.Control>
<RadioGroup <RadioGroup
className="flex-col gap-y-3" className="flex gap-y-3"
{...field} {...field}
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
@@ -57,8 +67,8 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
value={"none"} value={"none"}
label={t("promotions.form.campaign.none.title")} label={t("promotions.form.campaign.none.title")}
description={t("promotions.form.campaign.none.description")} description={t("promotions.form.campaign.none.description")}
className={clx("", { className={clx("border", {
"border-2 border-ui-border-interactive": "border border-ui-border-interactive":
"none" === field.value, "none" === field.value,
})} })}
/> />
@@ -69,22 +79,25 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
description={t( description={t(
"promotions.form.campaign.existing.description" "promotions.form.campaign.existing.description"
)} )}
className={clx("", { className={clx("border", {
"border-2 border-ui-border-interactive": "border border-ui-border-interactive":
"existing" === field.value, "existing" === field.value,
})} })}
/> />
<RadioGroup.ChoiceBox {withNewCampaign && (
value={"new"} <RadioGroup.ChoiceBox
label={t("promotions.form.campaign.new.title")} value={"new"}
description={t("promotions.form.campaign.new.description")} label={t("promotions.form.campaign.new.title")}
className={clx("", { description={t(
"border-2 border-ui-border-interactive": "promotions.form.campaign.new.description"
"new" === field.value, )}
})} className={clx("border", {
disabled "border border-ui-border-interactive":
/> "new" === field.value,
})}
/>
)}
</RadioGroup> </RadioGroup>
</Form.Control> </Form.Control>
@@ -127,6 +140,10 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
/> />
)} )}
{watchCampaignChoice === "new" && (
<CreateCampaignFormFields form={form} fieldScope="campaign." />
)}
<CampaignDetails campaign={selectedCampaign} /> <CampaignDetails campaign={selectedCampaign} />
</div> </div>
) )
@@ -143,19 +160,12 @@ export const AddCampaignPromotionForm = ({
const form = useForm<zod.infer<typeof EditPromotionSchema>>({ const form = useForm<zod.infer<typeof EditPromotionSchema>>({
defaultValues: { defaultValues: {
campaign_id: campaign?.id, campaign_id: campaign?.id,
existing: "true", campaign_choice: campaign?.id ? "existing" : "none",
}, },
resolver: zodResolver(EditPromotionSchema), resolver: zodResolver(EditPromotionSchema),
}) })
const watchCampaignId = useWatch({
control: form.control,
name: "campaign_id",
})
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id) const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync( await mutateAsync(
{ campaign_id: data.campaign_id }, { campaign_id: data.campaign_id },
@@ -163,11 +173,30 @@ export const AddCampaignPromotionForm = ({
) )
}) })
const watchCampaignChoice = useWatch({
control: form.control,
name: "campaign_choice",
})
useEffect(() => {
if (watchCampaignChoice === "none") {
form.setValue("campaign_id", null)
}
if (watchCampaignChoice === "existing") {
form.setValue("campaign_id", campaign?.id)
}
}, [watchCampaignChoice])
return ( return (
<RouteDrawer.Form form={form}> <RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col"> <form onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteDrawer.Body> <RouteDrawer.Body>
<AddCampaignPromotionFields form={form} campaigns={campaigns} /> <AddCampaignPromotionFields
form={form}
campaigns={campaigns}
withNewCampaign={false}
/>
</RouteDrawer.Body> </RouteDrawer.Body>
<RouteDrawer.Footer> <RouteDrawer.Footer>
@@ -1,10 +1,10 @@
import { CampaignDTO } from "@medusajs/types" import { CampaignResponse } from "@medusajs/types"
import { Heading, Text } from "@medusajs/ui" import { Heading, Text } from "@medusajs/ui"
import { Fragment } from "react" import { Fragment } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
type CampaignDetailsProps = { type CampaignDetailsProps = {
campaign?: CampaignDTO campaign?: CampaignResponse
} }
export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => { export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => {
@@ -28,6 +28,7 @@ import {
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { useCreatePromotion } from "../../../../../hooks/api/promotions" import { useCreatePromotion } from "../../../../../hooks/api/promotions"
import { getCurrencySymbol } from "../../../../../lib/currencies" import { getCurrencySymbol } from "../../../../../lib/currencies"
import { defaultCampaignValues } from "../../../../campaigns/campaign-create/components/create-campaign-form"
import { RulesFormField } from "../../../common/edit-rules/components/edit-rules-form" import { RulesFormField } from "../../../common/edit-rules/components/edit-rules-form"
import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form" import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form"
import { Tab } from "./constants" import { Tab } from "./constants"
@@ -89,6 +90,7 @@ export const CreatePromotionForm = ({
target_rules: generateRuleAttributes(targetRules), target_rules: generateRuleAttributes(targetRules),
buy_rules: generateRuleAttributes(buyRules), buy_rules: generateRuleAttributes(buyRules),
}, },
campaign: undefined,
}, },
resolver: zodResolver(CreatePromotionSchema), resolver: zodResolver(CreatePromotionSchema),
}) })
@@ -296,6 +298,29 @@ export const CreatePromotionForm = ({
return "not-started" return "not-started"
}, [detailsValidated]) }, [detailsValidated])
const watchCampaignChoice = useWatch({
control: form.control,
name: "campaign_choice",
})
useEffect(() => {
const formData = form.getValues()
if (watchCampaignChoice !== "existing") {
form.setValue("campaign_id", undefined)
}
if (watchCampaignChoice !== "new") {
form.setValue("campaign", undefined)
}
if (watchCampaignChoice === "new") {
if (!formData.campaign || !formData.campaign?.budget?.type) {
form.setValue("campaign", defaultCampaignValues)
}
}
}, [watchCampaignChoice])
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<form <form
@@ -700,9 +725,7 @@ export const CreatePromotionForm = ({
<Input <Input
{...form.register( {...form.register(
"application_method.max_quantity", "application_method.max_quantity",
{ { valueAsNumber: true }
valueAsNumber: true,
}
)} )}
type="number" type="number"
min={1} min={1}
@@ -1,4 +1,5 @@
import { z } from "zod" import { z } from "zod"
import { CreateCampaignSchema } from "../../../../campaigns/campaign-create/components/create-campaign-form"
const RuleSchema = z.array( const RuleSchema = z.array(
z.object({ z.object({
@@ -33,4 +34,5 @@ export const CreatePromotionSchema = z.object({
type: z.enum(["fixed", "percentage"]), type: z.enum(["fixed", "percentage"]),
target_type: z.enum(["order", "shipping_methods", "items"]), target_type: z.enum(["order", "shipping_methods", "items"]),
}), }),
campaign: CreateCampaignSchema.optional(),
}) })
@@ -163,7 +163,7 @@ export const AdminCreatePromotion = z
code: z.string(), code: z.string(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
type: z.nativeEnum(PromotionType), type: z.nativeEnum(PromotionType),
campaign_id: z.string().optional(), campaign_id: z.string().optional().nullable(),
campaign: AdminCreateCampaign.optional(), campaign: AdminCreateCampaign.optional(),
application_method: AdminCreateApplicationMethod, application_method: AdminCreateApplicationMethod,
rules: z.array(AdminCreatePromotionRule).optional(), rules: z.array(AdminCreatePromotionRule).optional(),
@@ -181,7 +181,7 @@ export const AdminUpdatePromotion = z
code: z.string().optional(), code: z.string().optional(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
type: z.nativeEnum(PromotionType).optional(), type: z.nativeEnum(PromotionType).optional(),
campaign_id: z.string().optional(), campaign_id: z.string().optional().nullable(),
campaign: AdminCreateCampaign.optional(), campaign: AdminCreateCampaign.optional(),
application_method: AdminUpdateApplicationMethod.optional(), application_method: AdminUpdateApplicationMethod.optional(),
rules: z.array(AdminCreatePromotionRule).optional(), rules: z.array(AdminCreatePromotionRule).optional(),