fix(dashboard): edit promotion campaign w/wo currrency (#13404)

**What**
- a promotion couldn't be added to a campaign without a currency budget (on promotion details screen)
- fix fetching campaigns pagination issue
- move select to Combobox
- fix currency restriction and move warning description to the label hint
This commit is contained in:
Frane Polić
2025-09-24 12:24:11 +02:00
committed by GitHub
parent c4c2231a31
commit 294c37564c
5 changed files with 56 additions and 74 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
fix(dashboard): add campaign without currency to promotion

View File

@@ -130,6 +130,7 @@ export const useAddOrRemoveCampaignPromotions = (
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.details() })
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context)
},
...options,

View File

@@ -2234,7 +2234,7 @@
"total_used": "Budget used",
"budget_limit": "Budget limit",
"campaign_id": {
"hint": "Only campaigns with the same currency code as the promotion are shown in this list."
"hint": "Disabled campaigns have budget in a different currency than the promotion."
}
},
"budget": {

View File

@@ -1,9 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminCampaign, AdminPromotion } from "@medusajs/types"
import { Button, RadioGroup, Select, Text, toast } from "@medusajs/ui"
import { Button, RadioGroup, toast } from "@medusajs/ui"
import { useEffect } from "react"
import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
@@ -11,11 +11,14 @@ import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
import { CreateCampaignFormFields } from "../../../../campaigns/common/components/create-campaign-form-fields"
import { CampaignDetails } from "./campaign-details"
import { sdk } from "../../../../../lib/client"
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { Combobox } from "../../../../../components/inputs/combobox"
import { useCampaign } from "../../../../../hooks/api/campaigns"
import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
type EditPromotionFormProps = {
promotion: AdminPromotion
campaigns: AdminCampaign[]
}
const EditPromotionSchema = zod.object({
@@ -25,14 +28,16 @@ const EditPromotionSchema = zod.object({
export const AddCampaignPromotionFields = ({
form,
campaigns,
withNewCampaign = true,
promotionCurrencyCode,
}: {
form: any
campaigns: AdminCampaign[]
withNewCampaign?: boolean
promotionCurrencyCode?: string
}) => {
const { t } = useTranslation()
const direction = useDocumentDirection()
const watchCampaignId = useWatch({
control: form.control,
name: "campaign_id",
@@ -43,8 +48,31 @@ export const AddCampaignPromotionFields = ({
name: "campaign_choice",
})
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
const direction = useDocumentDirection()
const campaignsCombobox = useComboboxData({
queryFn: (params) =>
sdk.admin.campaign.list({
...params,
}),
queryKey: ["campaigns"],
getOptions: (data) =>
data.campaigns.map((campaign) => ({
label: campaign.name.toUpperCase(),
value: campaign.id,
disabled:
campaign.budget?.currency_code &&
campaign.budget?.currency_code?.toLowerCase() !==
promotionCurrencyCode?.toLowerCase(), // also cannot add promotion which doesn't have currency defined to a campaign with a currency amount budget
})),
})
const { campaign: selectedCampaign } = useCampaign(
watchCampaignId as string,
undefined,
{
enabled: !!watchCampaignId,
}
)
return (
<div className="flex flex-col gap-y-8">
<Form.Field
@@ -99,58 +127,24 @@ export const AddCampaignPromotionFields = ({
<Form.Field
control={form.control}
name="campaign_id"
render={({ field: { onChange, ref, ...field } }) => {
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label>
<Form.Label tooltip={t("campaigns.fields.campaign_id.hint")}>
{t("promotions.form.campaign.existing.title")}
</Form.Label>
<Form.Control>
<Select
<Combobox
dir={direction}
onValueChange={onChange}
options={campaignsCombobox.options}
searchValue={campaignsCombobox.searchValue}
onSearchValueChange={campaignsCombobox.onSearchValueChange}
onChange={onChange}
{...field}
>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{!campaigns.length && (
<div className="flex h-[120px] flex-col items-center justify-center gap-2 p-2">
<span className="txt-small text-ui-fg-subtle font-medium">
{t(
"promotions.form.campaign.existing.placeholder.title"
)}
</span>
<span className="txt-small text-ui-fg-muted">
{t(
"promotions.form.campaign.existing.placeholder.desc"
)}
</span>
</div>
)}
{campaigns.map((c) => (
<Select.Item key={c.id} value={c.id}>
{c.name?.toUpperCase()}
</Select.Item>
))}
</Select.Content>
</Select>
></Combobox>
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey="campaigns.fields.campaign_id.hint"
components={[<br key="break" />]}
/>
</Text>
<Form.ErrorMessage />
</Form.Item>
)
@@ -162,14 +156,13 @@ export const AddCampaignPromotionFields = ({
<CreateCampaignFormFields form={form} fieldScope="campaign." />
)}
<CampaignDetails campaign={selectedCampaign} />
<CampaignDetails campaign={selectedCampaign as AdminCampaign} />
</div>
)
}
export const AddCampaignPromotionForm = ({
promotion,
campaigns,
}: EditPromotionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
@@ -227,8 +220,8 @@ export const AddCampaignPromotionForm = ({
<RouteDrawer.Body className="size-full overflow-auto">
<AddCampaignPromotionFields
form={form}
campaigns={campaigns}
withNewCampaign={false}
promotionCurrencyCode={promotion.application_method?.currency_code}
/>
</RouteDrawer.Body>

View File

@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/modals"
import { useCampaigns } from "../../../hooks/api/campaigns"
import { usePromotion } from "../../../hooks/api/promotions"
import { AddCampaignPromotionForm } from "./components/add-campaign-promotion-form"
@@ -12,24 +11,8 @@ export const PromotionAddCampaign = () => {
const { t } = useTranslation()
const { promotion, isPending, isError, error } = usePromotion(id!)
let campaignQuery = {}
if (promotion?.application_method?.currency_code) {
campaignQuery = {
budget: {
currency_code: promotion?.application_method?.currency_code,
},
}
}
const {
campaigns,
isPending: areCampaignsLoading,
isError: isCampaignError,
error: campaignError,
} = useCampaigns(campaignQuery)
if (isError || isCampaignError) {
throw error || campaignError
if (isError) {
throw error
}
return (
@@ -38,8 +21,8 @@ export const PromotionAddCampaign = () => {
<Heading>{t("promotions.campaign.edit.header")}</Heading>
</RouteDrawer.Header>
{!isPending && !areCampaignsLoading && promotion && campaigns && (
<AddCampaignPromotionForm promotion={promotion} campaigns={campaigns} />
{!isPending && promotion && (
<AddCampaignPromotionForm promotion={promotion} />
)}
</RouteDrawer>
)