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:
1 parent
5c14d283d0
commit
bb2b041954
10 files changed
+385
-329
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[] }
|
||||||
|
|||||||
+21
-295
@@ -1,49 +1,31 @@
|
|||||||
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 CreateCampaignForm = () => {
|
export const defaultCampaignValues = {
|
||||||
const { t } = useTranslation()
|
|
||||||
const { handleSuccess } = useRouteModal()
|
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useCreateCampaign()
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateCampaignSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
currency: "",
|
currency: "",
|
||||||
@@ -51,10 +33,18 @@ export const CreateCampaignForm = () => {
|
|||||||
starts_at: undefined,
|
starts_at: undefined,
|
||||||
ends_at: undefined,
|
ends_at: undefined,
|
||||||
budget: {
|
budget: {
|
||||||
type: "spend",
|
type: "spend" as CampaignBudgetTypeValues,
|
||||||
limit: undefined,
|
limit: undefined,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
export const CreateCampaignForm = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const { mutateAsync, isPending } = useCreateCampaign()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof CreateCampaignSchema>>({
|
||||||
|
defaultValues: defaultCampaignValues,
|
||||||
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>
|
||||||
|
|||||||
+274
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-campaign-form-fields"
|
||||||
+54
-25
@@ -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,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{withNewCampaign && (
|
||||||
<RadioGroup.ChoiceBox
|
<RadioGroup.ChoiceBox
|
||||||
value={"new"}
|
value={"new"}
|
||||||
label={t("promotions.form.campaign.new.title")}
|
label={t("promotions.form.campaign.new.title")}
|
||||||
description={t("promotions.form.campaign.new.description")}
|
description={t(
|
||||||
className={clx("", {
|
"promotions.form.campaign.new.description"
|
||||||
"border-2 border-ui-border-interactive":
|
)}
|
||||||
|
className={clx("border", {
|
||||||
|
"border border-ui-border-interactive":
|
||||||
"new" === field.value,
|
"new" === field.value,
|
||||||
})}
|
})}
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</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>
|
||||||
|
|||||||
+2
-2
@@ -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) => {
|
||||||
|
|||||||
+26
-3
@@ -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}
|
||||||
|
|||||||
+2
@@ -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(),
|
||||||
|
|||||||
Reference in new issue
Block a user