From bb2b041954642e705aa63c21536d90b7ad0766ee Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 15 May 2024 10:43:13 +0200 Subject: [PATCH] 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 --- .../dashboard/src/hooks/api/promotions.tsx | 2 + .../dashboard/src/types/api-payloads.ts | 3 +- .../create-campaign-form.tsx | 322 ++---------------- .../create-campaign-form-fields.tsx | 274 +++++++++++++++ .../create-campaign-form-fields/index.ts | 1 + .../add-campaign-promotion-form.tsx | 91 +++-- .../campaign-details.tsx | 4 +- .../create-promotion-form.tsx | 29 +- .../create-promotion-form/form-schema.ts | 2 + .../src/api-v2/admin/promotions/validators.ts | 4 +- 10 files changed, 394 insertions(+), 338 deletions(-) create mode 100644 packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/index.ts diff --git a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx index 3143d4472e..4be46c1938 100644 --- a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx @@ -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, diff --git a/packages/admin-next/dashboard/src/types/api-payloads.ts b/packages/admin-next/dashboard/src/types/api-payloads.ts index b7a8f5953b..e9f317bbdc 100644 --- a/packages/admin-next/dashboard/src/types/api-payloads.ts +++ b/packages/admin-next/dashboard/src/types/api-payloads.ts @@ -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 export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] } export type BatchRemovePromotionRulesReq = { rule_ids: string[] } export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] } diff --git a/packages/admin-next/dashboard/src/v2-routes/campaigns/campaign-create/components/create-campaign-form/create-campaign-form.tsx b/packages/admin-next/dashboard/src/v2-routes/campaigns/campaign-create/components/create-campaign-form/create-campaign-form.tsx index ce6042c943..b375c77198 100644 --- a/packages/admin-next/dashboard/src/v2-routes/campaigns/campaign-create/components/create-campaign-form/create-campaign-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/campaigns/campaign-create/components/create-campaign-form/create-campaign-form.tsx @@ -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>({ - 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 (
@@ -127,259 +105,7 @@ export const CreateCampaignForm = () => { -
-
- {t("campaigns.create.header")} - - {t("campaigns.create.hint")} - -
- -
- { - return ( - - {t("fields.name")} - - - - - - - - ) - }} - /> - - { - return ( - - {t("fields.description")} - - - - - - - - ) - }} - /> - - { - return ( - - - {t("campaigns.fields.identifier")} - - - - - - - - - ) - }} - /> - - { - return ( - - {t("fields.currency")} - - - - - - ) - }} - /> - - { - return ( - - - {t("campaigns.fields.start_date")} - - - - { - onChange(v ?? null) - }} - {...field} - /> - - - - - ) - }} - /> - - { - return ( - - {t("campaigns.fields.end_date")} - - - onChange(v ?? null)} - {...field} - /> - - - - - ) - }} - /> -
- -
- {t("campaigns.budget.create.header")} - - {t("campaigns.budget.create.hint")} - -
- - { - return ( - - {t("campaigns.budget.fields.type")} - - - - - - - - - - - ) - }} - /> - -
- { - return ( - - - {t("campaigns.budget.fields.limit")} - - - - {isTypeSpend ? ( - - onChange(value ? parseInt(value) : "") - } - code={currencyValue} - symbol={ - currencyValue - ? getCurrencySymbol(currencyValue) - : "" - } - {...field} - value={value} - /> - ) : ( - { - onChange( - e.target.value === "" - ? null - : parseInt(e.target.value) - ) - }} - /> - )} - - - - ) - }} - /> -
-
+
diff --git a/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx b/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx new file mode 100644 index 0000000000..9cbf553b5d --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/create-campaign-form-fields.tsx @@ -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 ( +
+
+ {t("campaigns.create.header")} + + + {t("campaigns.create.hint")} + +
+ +
+ { + return ( + + {t("fields.name")} + + + + + + + + ) + }} + /> + + { + return ( + + {t("fields.description")} + + + + + + + + ) + }} + /> + + { + return ( + + {t("campaigns.fields.identifier")} + + + + + + + + ) + }} + /> + + { + return ( + + {t("fields.currency")} + + + + + + ) + }} + /> + + { + return ( + + {t("campaigns.fields.start_date")} + + + { + onChange(v ?? null) + }} + {...field} + /> + + + + + ) + }} + /> + + { + return ( + + {t("campaigns.fields.end_date")} + + + onChange(v ?? null)} + {...field} + /> + + + + + ) + }} + /> +
+ +
+ {t("campaigns.budget.create.header")} + + {t("campaigns.budget.create.hint")} + +
+ + { + return ( + + {t("campaigns.budget.fields.type")} + + + + + + + + + + + ) + }} + /> + +
+ { + return ( + + {t("campaigns.budget.fields.limit")} + + + {isTypeSpend ? ( + + onChange(value ? parseInt(value) : "") + } + code={currencyValue} + symbol={ + currencyValue ? getCurrencySymbol(currencyValue) : "" + } + {...field} + value={value} + /> + ) : ( + { + onChange( + e.target.value === "" + ? null + : parseInt(e.target.value) + ) + }} + /> + )} + + + + ) + }} + /> +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/index.ts b/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/index.ts new file mode 100644 index 0000000000..28fb2a80df --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/campaigns/common/components/create-campaign-form-fields/index.ts @@ -0,0 +1 @@ +export * from "./create-campaign-form-fields" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx index 752e1e1487..7faff9f5f3 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx @@ -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 ( Method + { 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, })} /> - + {withNewCampaign && ( + + )} @@ -127,6 +140,10 @@ export const AddCampaignPromotionFields = ({ form, campaigns }) => { /> )} + {watchCampaignChoice === "new" && ( + + )} + ) @@ -143,19 +160,12 @@ export const AddCampaignPromotionForm = ({ const form = useForm>({ 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 (
- + diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx index c84a0a2cf0..24e46ed067 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx @@ -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) => { diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx index 3c9ab80493..e7fea7aa95 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx @@ -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 (