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:
@@ -25,6 +25,7 @@ import {
|
||||
PromotionRulesListRes,
|
||||
PromotionRuleValuesListRes,
|
||||
} from "../../types/api-responses"
|
||||
import { campaignsQueryKeys } from "./campaigns"
|
||||
|
||||
const PROMOTIONS_QUERY_KEY = "promotions" as const
|
||||
export const promotionsQueryKeys = {
|
||||
@@ -185,6 +186,7 @@ export const useCreatePromotion = (
|
||||
mutationFn: (payload) => client.promotions.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
CreateShippingProfileDTO,
|
||||
CreateStockLocationInput,
|
||||
InventoryNext,
|
||||
ShippingOptionDTO,
|
||||
UpdateApiKeyDTO,
|
||||
UpdateCampaignDTO,
|
||||
UpdateCustomerDTO,
|
||||
@@ -102,7 +101,7 @@ export type DeletePriceListPricesReq = { ids: string[] }
|
||||
|
||||
// Promotion
|
||||
export type CreatePromotionReq = CreatePromotionDTO
|
||||
export type UpdatePromotionReq = UpdatePromotionDTO
|
||||
export type UpdatePromotionReq = Omit<UpdatePromotionDTO, "id">
|
||||
export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] }
|
||||
export type BatchRemovePromotionRulesReq = { rule_ids: string[] }
|
||||
export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
|
||||
|
||||
@@ -1,60 +1,50 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Button,
|
||||
clx,
|
||||
CurrencyInput,
|
||||
DatePicker,
|
||||
Heading,
|
||||
Input,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Text,
|
||||
toast,
|
||||
} from "@medusajs/ui"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { Button, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { CampaignBudgetTypeValues } from "@medusajs/types"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
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({
|
||||
name: zod.string(),
|
||||
export const CreateCampaignSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
description: zod.string().optional(),
|
||||
currency: zod.string(),
|
||||
campaign_identifier: zod.string(),
|
||||
currency: zod.string().min(1),
|
||||
campaign_identifier: zod.string().min(1),
|
||||
starts_at: zod.date().optional(),
|
||||
ends_at: zod.date().optional(),
|
||||
budget: zod.object({
|
||||
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 = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { mutateAsync, isPending } = useCreateCampaign()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateCampaignSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
currency: "",
|
||||
campaign_identifier: "",
|
||||
starts_at: undefined,
|
||||
ends_at: undefined,
|
||||
budget: {
|
||||
type: "spend",
|
||||
limit: undefined,
|
||||
},
|
||||
},
|
||||
defaultValues: defaultCampaignValues,
|
||||
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 (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -127,259 +105,7 @@ export const CreateCampaignForm = () => {
|
||||
</RouteFocusModal.Header>
|
||||
|
||||
<RouteFocusModal.Body className="flex flex-col items-center py-16">
|
||||
<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="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>
|
||||
<CreateCampaignFormFields form={form} />
|
||||
</RouteFocusModal.Body>
|
||||
</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 { CampaignDTO, PromotionDTO } from "@medusajs/types"
|
||||
import { CampaignResponse, PromotionDTO } from "@medusajs/types"
|
||||
import { Button, clx, RadioGroup, Select } from "@medusajs/ui"
|
||||
import { useEffect } from "react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { CampaignDetails } from "./campaign-details"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||
import { CreateCampaignFormFields } from "../../../../campaigns/common/components/create-campaign-form-fields"
|
||||
import { CampaignDetails } from "./campaign-details"
|
||||
|
||||
type EditPromotionFormProps = {
|
||||
promotion: PromotionDTO
|
||||
campaigns: CampaignDTO[]
|
||||
campaigns: CampaignResponse[]
|
||||
}
|
||||
|
||||
const EditPromotionSchema = zod.object({
|
||||
campaign_id: zod.string().optional(),
|
||||
existing: zod.string().toLowerCase(),
|
||||
campaign_id: zod.string().optional().nullable(),
|
||||
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 watchCampaignId = useWatch({
|
||||
control: form.control,
|
||||
@@ -46,9 +55,10 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>Method</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col gap-y-3"
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
@@ -57,8 +67,8 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
|
||||
value={"none"}
|
||||
label={t("promotions.form.campaign.none.title")}
|
||||
description={t("promotions.form.campaign.none.description")}
|
||||
className={clx("", {
|
||||
"border-2 border-ui-border-interactive":
|
||||
className={clx("border", {
|
||||
"border border-ui-border-interactive":
|
||||
"none" === field.value,
|
||||
})}
|
||||
/>
|
||||
@@ -69,22 +79,25 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
|
||||
description={t(
|
||||
"promotions.form.campaign.existing.description"
|
||||
)}
|
||||
className={clx("", {
|
||||
"border-2 border-ui-border-interactive":
|
||||
className={clx("border", {
|
||||
"border border-ui-border-interactive":
|
||||
"existing" === field.value,
|
||||
})}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"new"}
|
||||
label={t("promotions.form.campaign.new.title")}
|
||||
description={t("promotions.form.campaign.new.description")}
|
||||
className={clx("", {
|
||||
"border-2 border-ui-border-interactive":
|
||||
"new" === field.value,
|
||||
})}
|
||||
disabled
|
||||
/>
|
||||
{withNewCampaign && (
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"new"}
|
||||
label={t("promotions.form.campaign.new.title")}
|
||||
description={t(
|
||||
"promotions.form.campaign.new.description"
|
||||
)}
|
||||
className={clx("border", {
|
||||
"border border-ui-border-interactive":
|
||||
"new" === field.value,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
|
||||
@@ -127,6 +140,10 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchCampaignChoice === "new" && (
|
||||
<CreateCampaignFormFields form={form} fieldScope="campaign." />
|
||||
)}
|
||||
|
||||
<CampaignDetails campaign={selectedCampaign} />
|
||||
</div>
|
||||
)
|
||||
@@ -143,19 +160,12 @@ export const AddCampaignPromotionForm = ({
|
||||
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
|
||||
defaultValues: {
|
||||
campaign_id: campaign?.id,
|
||||
existing: "true",
|
||||
campaign_choice: campaign?.id ? "existing" : "none",
|
||||
},
|
||||
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 handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{ 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 (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<AddCampaignPromotionFields form={form} campaigns={campaigns} />
|
||||
<AddCampaignPromotionFields
|
||||
form={form}
|
||||
campaigns={campaigns}
|
||||
withNewCampaign={false}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
<RouteDrawer.Footer>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CampaignDTO } from "@medusajs/types"
|
||||
import { CampaignResponse } from "@medusajs/types"
|
||||
import { Heading, Text } from "@medusajs/ui"
|
||||
import { Fragment } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type CampaignDetailsProps = {
|
||||
campaign?: CampaignDTO
|
||||
campaign?: CampaignResponse
|
||||
}
|
||||
|
||||
export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useCreatePromotion } from "../../../../../hooks/api/promotions"
|
||||
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 { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form"
|
||||
import { Tab } from "./constants"
|
||||
@@ -89,6 +90,7 @@ export const CreatePromotionForm = ({
|
||||
target_rules: generateRuleAttributes(targetRules),
|
||||
buy_rules: generateRuleAttributes(buyRules),
|
||||
},
|
||||
campaign: undefined,
|
||||
},
|
||||
resolver: zodResolver(CreatePromotionSchema),
|
||||
})
|
||||
@@ -296,6 +298,29 @@ export const CreatePromotionForm = ({
|
||||
return "not-started"
|
||||
}, [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 (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
@@ -700,9 +725,7 @@ export const CreatePromotionForm = ({
|
||||
<Input
|
||||
{...form.register(
|
||||
"application_method.max_quantity",
|
||||
{
|
||||
valueAsNumber: true,
|
||||
}
|
||||
{ valueAsNumber: true }
|
||||
)}
|
||||
type="number"
|
||||
min={1}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { CreateCampaignSchema } from "../../../../campaigns/campaign-create/components/create-campaign-form"
|
||||
|
||||
const RuleSchema = z.array(
|
||||
z.object({
|
||||
@@ -33,4 +34,5 @@ export const CreatePromotionSchema = z.object({
|
||||
type: z.enum(["fixed", "percentage"]),
|
||||
target_type: z.enum(["order", "shipping_methods", "items"]),
|
||||
}),
|
||||
campaign: CreateCampaignSchema.optional(),
|
||||
})
|
||||
|
||||
@@ -163,7 +163,7 @@ export const AdminCreatePromotion = z
|
||||
code: z.string(),
|
||||
is_automatic: z.boolean().optional(),
|
||||
type: z.nativeEnum(PromotionType),
|
||||
campaign_id: z.string().optional(),
|
||||
campaign_id: z.string().optional().nullable(),
|
||||
campaign: AdminCreateCampaign.optional(),
|
||||
application_method: AdminCreateApplicationMethod,
|
||||
rules: z.array(AdminCreatePromotionRule).optional(),
|
||||
@@ -181,7 +181,7 @@ export const AdminUpdatePromotion = z
|
||||
code: z.string().optional(),
|
||||
is_automatic: z.boolean().optional(),
|
||||
type: z.nativeEnum(PromotionType).optional(),
|
||||
campaign_id: z.string().optional(),
|
||||
campaign_id: z.string().optional().nullable(),
|
||||
campaign: AdminCreateCampaign.optional(),
|
||||
application_method: AdminUpdateApplicationMethod.optional(),
|
||||
rules: z.array(AdminCreatePromotionRule).optional(),
|
||||
|
||||
Reference in New Issue
Block a user