From 83d3a7d863b1932aebe4e1cfa0847a45c5eddc10 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 29 Mar 2024 13:10:47 +0100 Subject: [PATCH 1/7] chore: added details page for promotions --- .changeset/quick-jeans-push.md | 6 + .../dashboard/public/locales/$schema.json | 9 + .../public/locales/en-US/translation.json | 28 ++- .../empty-table-content.tsx | 45 +++-- .../dashboard/src/lib/api-v2/promotion.ts | 29 ++++ .../src/providers/router-provider/v2.tsx | 12 +- .../campaign-section/campaign-section.tsx | 110 ++++++++++++ .../components/campaign-section/index.ts | 1 + .../promotion-conditions-section/index.ts | 1 + .../promotion-conditions-section.tsx | 91 ++++++++++ .../promotion-general-section/index.ts | 1 + .../promotion-general-section.tsx | 160 ++++++++++++++++++ .../promotions/promotion-detail/index.ts | 2 + .../promotions/promotion-detail/loader.ts | 20 +++ .../promotion-detail/promotion-detail.tsx | 76 +++++++++ .../promotion-list-table.tsx | 31 ++-- .../promotions/promotion-list/loader.ts | 4 +- .../src/hooks/admin/custom/queries.ts | 16 +- .../api-v2/admin/promotions/query-config.ts | 3 + .../src/api-v2/admin/promotions/types.ts | 4 + 20 files changed, 615 insertions(+), 34 deletions(-) create mode 100644 .changeset/quick-jeans-push.md create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/promotion-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx diff --git a/.changeset/quick-jeans-push.md b/.changeset/quick-jeans-push.md new file mode 100644 index 0000000000000..59ce3bc9c4294 --- /dev/null +++ b/.changeset/quick-jeans-push.md @@ -0,0 +1,6 @@ +--- +"medusa-react": patch +"@medusajs/medusa": patch +--- + +feat(dashboard): added details page for promotions diff --git a/packages/admin-next/dashboard/public/locales/$schema.json b/packages/admin-next/dashboard/public/locales/$schema.json index d391cfc9bd1fc..98af3952a95b5 100644 --- a/packages/admin-next/dashboard/public/locales/$schema.json +++ b/packages/admin-next/dashboard/public/locales/$schema.json @@ -140,6 +140,15 @@ }, "required": ["domain"] }, + "campaigns": { + "type": "object", + "properties": { + "domain": { + "type": "string" + } + }, + "required": ["domain"] + }, "giftCards": { "type": "object", "properties": { diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 457885be4e270..3eb4f4387d7a0 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -506,12 +506,38 @@ "domain": "Promotions", "fields": { "method": "Method", - "campaign": "Campaign" + "type": "Type", + "value": "Value", + "campaign": "Campaign", + "allocation": "Allocation", + "conditions": { + "rules": { + "title": "Who can use this code?", + "description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched. Choose between attributes, operators, and values to set up the conditions." + }, + "target_rules": { + "title": "What needs to be in the cart to unlock the promotion?", + "description": "If these conditions match, we enable a promotion action on the target items. Choose between attributes, operators, and values to set up the conditions." + }, + "buy_rules": { + "title": "What will the promotion be applied to?", + "description": "The promotion will be applied to items that match these conditions" + } + } }, "deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.", "createPromotionTitle": "Create Promotion", "type": "Promotion type" }, + "campaigns": { + "domain": "Campaigns", + "fields": { + "name": "Name", + "identifier": "Identifier", + "start_date": "Start date", + "end_date": "End date" + } + }, "pricing": { "domain": "Pricing", "deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.", diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx index 76a50895c0da3..5aac770ac7224 100644 --- a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -1,4 +1,4 @@ -import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons" +import { ExclamationCircle, MagnifyingGlass, PlusMini } from "@medusajs/icons" import { Button, Text, clx } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" @@ -32,21 +32,44 @@ export const NoResults = ({ title, message, className }: NoResultsProps) => { ) } -type NoRecordsProps = { - title?: string - message?: string +type ActionProps = { action?: { to: string label: string } - className?: string } +type NoRecordsProps = { + title?: string + message?: string + className?: string + buttonVariant?: string +} & ActionProps + +const DefaultButton = ({ action }: ActionProps) => + action && ( + + + + ) + +const TransparentIconLeftButton = ({ action }: ActionProps) => + action && ( + + + + ) + export const NoRecords = ({ title, message, action, className, + buttonVariant = "default", }: NoRecordsProps) => { const { t } = useTranslation() @@ -59,19 +82,19 @@ export const NoRecords = ({ >
+ {title ?? t("general.noRecordsTitle")} + {message ?? t("general.noRecordsMessage")}
- {action && ( - - - + + {buttonVariant === "default" && } + {buttonVariant === "transparentIconLeft" && ( + )} ) diff --git a/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts b/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts index ff28055712d1b..aa0b6cb8935bd 100644 --- a/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts +++ b/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts @@ -1,8 +1,12 @@ import { AdminGetPromotionsParams, + AdminGetPromotionsPromotionParams, + AdminPromotionRes, AdminPromotionsListRes, } from "@medusajs/medusa" +import { useMutation } from "@tanstack/react-query" import { queryKeysFactory, useAdminCustomQuery } from "medusa-react" +import { medusa } from "../medusa" const QUERY_KEY = "admin_promotions" export const adminPromotionKeys = queryKeysFactory< @@ -10,6 +14,12 @@ export const adminPromotionKeys = queryKeysFactory< AdminGetPromotionsParams >(QUERY_KEY) +export const adminPromotionQueryFns = { + list: (query: AdminGetPromotionsParams) => + medusa.admin.custom.get(`/admin/promotions`, query), + detail: (id: string) => medusa.admin.custom.get(`/admin/promotions/${id}`), +} + export const useV2Promotions = ( query?: AdminGetPromotionsParams, options?: object @@ -21,3 +31,22 @@ export const useV2Promotions = ( return { ...data, ...rest } } + +export const useV2Promotion = ( + id: string, + query?: AdminGetPromotionsParams, + options?: object +) => { + const { data, ...rest } = useAdminCustomQuery< + AdminGetPromotionsPromotionParams, + AdminPromotionRes + >(`/admin/promotions/${id}`, adminPromotionKeys.detail(id), query, options) + + return { ...data, ...rest } +} + +export const useV2DeletePromotion = (id: string) => { + return useMutation(() => + medusa.admin.custom.delete(`/admin/promotions/${id}`) + ) +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index 2e8ecac565778..b7560c9b60e0e 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -2,9 +2,9 @@ import { Navigate, RouteObject, useLocation } from "react-router-dom" import { MainLayout } from "../../components/layout-v2/main-layout" import { SettingsLayout } from "../../components/layout/settings-layout" -import { Outlet } from "react-router-dom" - import { Spinner } from "@medusajs/icons" +import { AdminPromotionRes } from "@medusajs/medusa" +import { Outlet } from "react-router-dom" import { ErrorBoundary } from "../../components/error/error-boundary" import { useV2Session } from "../../lib/api-v2" import { SearchProvider } from "../search-provider" @@ -87,6 +87,14 @@ export const v2Routes: RouteObject[] = [ path: "", lazy: () => import("../../v2-routes/promotions/promotion-list"), }, + { + path: ":id", + lazy: () => + import("../../v2-routes/promotions/promotion-detail"), + handle: { + crumb: (data: AdminPromotionRes) => data.promotion?.code, + }, + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx new file mode 100644 index 0000000000000..04ac9d1aa9357 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx @@ -0,0 +1,110 @@ +import { PencilSquare } from "@medusajs/icons" +import { CampaignDTO } from "@medusajs/types" +import { Container, Heading, Text } from "@medusajs/ui" +import { format } from "date-fns" +import { Fragment } from "react" +import { useTranslation } from "react-i18next" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { NoRecords } from "../../../../../components/common/empty-table-content" + +function formatDate(date?: string | Date) { + if (!date) { + return "-" + } + + return format(new Date(date), "dd MMM yyyy") +} + +const CampaignDetailSection = ({ campaign }: { campaign: CampaignDTO }) => { + const { t } = useTranslation() + + return ( + +
+ + {t("campaigns.fields.name")} + + +
+ + {campaign?.name} + +
+
+ +
+ + {t("campaigns.fields.identifier")} + + +
+ + {campaign?.campaign_identifier} + +
+
+ +
+ + {t("campaigns.fields.start_date")} + + +
+ + {formatDate(campaign?.starts_at)} + +
+
+ +
+ + {t("campaigns.fields.end_date") || "-"} + + +
+ + {formatDate(campaign?.ends_at)} + +
+
+
+ ) +} + +export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => { + const { t } = useTranslation() + + return ( + +
+ {t("promotions.fields.campaign")} + , + }, + ], + }, + ]} + /> +
+ + {campaign ? ( + + ) : ( + + )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/index.ts new file mode 100644 index 0000000000000..52b96902ac207 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/index.ts @@ -0,0 +1 @@ +export * from "./campaign-section.tsx" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/index.ts new file mode 100644 index 0000000000000..260e4a4ffb063 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/index.ts @@ -0,0 +1 @@ +export * from "./promotion-conditions-section" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx new file mode 100644 index 0000000000000..78974f82e9fe4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx @@ -0,0 +1,91 @@ +import { PencilSquare } from "@medusajs/icons" +import { PromotionRuleDTO, PromotionRuleTypes } from "@medusajs/types" +import { Badge, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { NoRecords } from "../../../../../components/common/empty-table-content" + +type RuleProps = { + rule: PromotionRuleDTO +} + +function RuleBlock({ rule }: RuleProps) { + return ( +
+ + + {rule.attribute} + + + {rule.operator} + + + {rule.values?.map((v) => v.value).join(",")} + + +
+ ) +} + +type PromotionConditionsSectionProps = { + rules: PromotionRuleDTO[] + ruleType: PromotionRuleTypes +} + +export const PromotionConditionsSection = ({ + rules, + ruleType, +}: PromotionConditionsSectionProps) => { + const { t } = useTranslation() + + return ( + +
+
+ + {t(`promotions.fields.conditions.${ruleType}.title`)} + +
+ + , + label: t("actions.edit"), + to: `conditions`, + }, + ], + }, + ]} + /> +
+ +
+ {!rules.length && ( + + )} + + {rules.map((rule) => ( + + ))} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/index.ts new file mode 100644 index 0000000000000..555494d37afc4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/index.ts @@ -0,0 +1 @@ +export * from "./promotion-general-section.tsx" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/promotion-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/promotion-general-section.tsx new file mode 100644 index 0000000000000..2d05103f2851a --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-general-section/promotion-general-section.tsx @@ -0,0 +1,160 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { PromotionDTO } from "@medusajs/types" +import { + Container, + Copy, + Heading, + StatusBadge, + Text, + usePrompt, +} from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { useV2DeletePromotion } from "../../../../../lib/api-v2" +import { + getPromotionStatus, + PromotionStatus, +} from "../../../../../lib/promotions" + +type PromotionGeneralSectionProps = { + promotion: PromotionDTO +} + +export const PromotionGeneralSection = ({ + promotion, +}: PromotionGeneralSectionProps) => { + const { t } = useTranslation() + const prompt = usePrompt() + const navigate = useNavigate() + const { mutateAsync } = useV2DeletePromotion(promotion.id) + + const handleDelete = async () => { + const confirm = await prompt({ + title: t("general.areYouSure"), + description: t("promotions.deleteWarning", { + code: promotion.code, + }), + verificationInstruction: t("general.typeToConfirm"), + verificationText: promotion.code, + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!confirm) { + return + } + + await mutateAsync(undefined, { + onSuccess: () => { + navigate("/promotions", { replace: true }) + }, + }) + } + + const [color, text] = { + [PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")], + [PromotionStatus.ACTIVE]: ["green", t("statuses.active")], + [PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")], + [PromotionStatus.EXPIRED]: ["red", t("statuses.expired")], + }[getPromotionStatus(promotion)] as [ + "grey" | "orange" | "green" | "red", + string, + ] + + return ( + +
+
+ {promotion.code} +
+ +
+ {text} + , + label: t("actions.edit"), + to: `/promotions/${promotion.id}/edit`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.delete"), + onClick: handleDelete, + }, + ], + }, + ]} + /> +
+
+ +
+ + {t("promotions.fields.method")} + + + + {promotion.is_automatic ? "Promotion code" : "Automatic"} + +
+ +
+ + {t("fields.code")} + + +
+ + {promotion.code} + + + +
+
+ +
+ + {t("promotions.fields.type")} + + + + {promotion.type} + +
+ +
+ + {t("promotions.fields.value")} + + + + {promotion.application_method?.value} + +
+ +
+ + {t("promotions.fields.allocation")} + + + + {promotion.application_method?.allocation!} + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/index.ts new file mode 100644 index 0000000000000..961854b02b176 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/index.ts @@ -0,0 +1,2 @@ +export { promotionLoader as loader } from "./loader" +export { PromotionDetail as Component } from "./promotion-detail.tsx" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/loader.ts new file mode 100644 index 0000000000000..59e1d74ffe10d --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/loader.ts @@ -0,0 +1,20 @@ +import { AdminPromotionRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { LoaderFunctionArgs } from "react-router-dom" +import { adminPromotionKeys, adminPromotionQueryFns } from "../../../lib/api-v2" +import { queryClient } from "../../../lib/medusa" + +const promotionDetailQuery = (id: string) => ({ + queryKey: adminPromotionKeys.detail(id), + queryFn: () => adminPromotionQueryFns.detail(id), +}) + +export const promotionLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = promotionDetailQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx new file mode 100644 index 0000000000000..88b7b5ec25beb --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx @@ -0,0 +1,76 @@ +import { Outlet, useLoaderData, useParams } from "react-router-dom" + +import { JsonViewSection } from "../../../components/common/json-view-section" +import { useV2Promotion } from "../../../lib/api-v2/promotion" +import { CampaignSection } from "./components/campaign-section" +import { PromotionConditionsSection } from "./components/promotion-conditions-section" +import { PromotionGeneralSection } from "./components/promotion-general-section" +import { promotionLoader } from "./loader" + +import after from "medusa-admin:widgets/promotion/details/after" +import before from "medusa-admin:widgets/promotion/details/before" + +export const PromotionDetail = () => { + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { id } = useParams() + const { promotion, isLoading } = useV2Promotion(id!, {}, { initialData }) + + if (isLoading || !promotion) { + return
Loading...
+ } + + return ( +
+ {before.widgets.map((w, i) => { + return ( +
+ +
+ ) + })} + +
+
+ + + + + + + {promotion.type === "buyget" && ( + + )} + +
+ +
+ {after.widgets.map((w, i) => { + return ( +
+ +
+ ) + })} + +
+ +
+ +
+
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/components/promotion-list-table/promotion-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/components/promotion-list-table/promotion-list-table.tsx index 62d07a921038c..2abd20c0b69e4 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/components/promotion-list-table/promotion-list-table.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/components/promotion-list-table/promotion-list-table.tsx @@ -2,10 +2,9 @@ import { PencilSquare, Trash } from "@medusajs/icons" import { PromotionDTO } from "@medusajs/types" import { Button, Container, Heading, usePrompt } from "@medusajs/ui" import { createColumnHelper } from "@tanstack/react-table" -import { useAdminDeleteDiscount } from "medusa-react" import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { Link, Outlet, useLoaderData } from "react-router-dom" +import { Link, Outlet, useLoaderData, useNavigate } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" import { DataTable } from "../../../../../components/table/data-table" @@ -13,7 +12,10 @@ import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/ import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters" import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" -import { useV2Promotions } from "../../../../../lib/api-v2" +import { + useV2DeletePromotion, + useV2Promotions, +} from "../../../../../lib/api-v2" import { promotionsLoader } from "../../loader" const PAGE_SIZE = 20 @@ -80,25 +82,34 @@ export const PromotionListTable = () => { const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => { const { t } = useTranslation() const prompt = usePrompt() - // TODO: change to promotions delete endpoint - const { mutateAsync } = useAdminDeleteDiscount(promotion.id) + const navigate = useNavigate() + const { mutateAsync } = useV2DeletePromotion(promotion.id) const handleDelete = async () => { const res = await prompt({ title: t("general.areYouSure"), - description: t("promotions.deleteWarning", { - code: promotion.code, - }), + description: t("promotions.deleteWarning", { code: promotion.code! }), confirmText: t("actions.delete"), cancelText: t("actions.cancel"), + verificationInstruction: t("general.typeToConfirm"), + verificationText: promotion.code, }) if (!res) { return } - // TODO: handle error scenario here - await mutateAsync() + try { + await mutateAsync(undefined, { + onSuccess: () => { + navigate("/promotions", { replace: true }) + }, + }) + } catch { + throw new Error( + `Promotion with code ${promotion.code} could not be deleted` + ) + } } return ( diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/loader.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/loader.ts index f66e516581d07..52b1fad2b130f 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/loader.ts +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-list/loader.ts @@ -1,12 +1,12 @@ import { AdminPromotionsListRes } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { QueryClient } from "@tanstack/react-query" -import { adminPromotionKeys, useV2Promotions } from "../../../lib/api-v2" +import { adminPromotionKeys, adminPromotionQueryFns } from "../../../lib/api-v2" import { queryClient } from "../../../lib/medusa" const promotionsListQuery = () => ({ queryKey: adminPromotionKeys.list(), - queryFn: async () => useV2Promotions({ limit: 20, offset: 0 }), + queryFn: () => adminPromotionQueryFns.list({ limit: 20, offset: 0 }), }) export const promotionsLoader = (client: QueryClient) => { diff --git a/packages/medusa-react/src/hooks/admin/custom/queries.ts b/packages/medusa-react/src/hooks/admin/custom/queries.ts index d71a44f05f78d..79dda86402436 100644 --- a/packages/medusa-react/src/hooks/admin/custom/queries.ts +++ b/packages/medusa-react/src/hooks/admin/custom/queries.ts @@ -5,25 +5,25 @@ import { UseQueryOptionsWrapper } from "../../../types" /** * This hook sends a `GET` request to a custom API Route. - * + * * @typeParam TQuery - The type of accepted query parameters which defaults to `Record`. * @typeParam TResponse - The type of response which defaults to `any`. * @typeParamDefinition TQuery - The query parameters based on the type specified for `TQuery`. * @typeParamDefinition TResponse - The response based on the type specified for `TResponse`. - * + * * @example * import React from "react" * import { useAdminCustomQuery } from "medusa-react" * import Post from "./models/Post" - * + * * type RequestQuery = { * title: string * } - * + * * type ResponseData = { * posts: Post * } - * + * * const Custom = () => { * const { data, isLoading } = useAdminCustomQuery * ( @@ -33,7 +33,7 @@ import { UseQueryOptionsWrapper } from "../../../types" * title: "My post" * } * ) - * + * * return ( *
* {isLoading && Loading...} @@ -50,9 +50,9 @@ import { UseQueryOptionsWrapper } from "../../../types" *
* ) * } - * + * * export default Custom - * + * * @customNamespace Hooks.Admin.Custom * @category Mutations */ diff --git a/packages/medusa/src/api-v2/admin/promotions/query-config.ts b/packages/medusa/src/api-v2/admin/promotions/query-config.ts index 38d3b65c8aa90..2f77f1f791132 100644 --- a/packages/medusa/src/api-v2/admin/promotions/query-config.ts +++ b/packages/medusa/src/api-v2/admin/promotions/query-config.ts @@ -21,6 +21,9 @@ export const defaultAdminPromotionFields = [ "deleted_at", "campaign.id", "campaign.name", + "campaign.campaign_identifier", + "campaign.starts_at", + "campaign.ends_at", "application_method.value", "application_method.type", "application_method.max_quantity", diff --git a/packages/medusa/src/api-v2/admin/promotions/types.ts b/packages/medusa/src/api-v2/admin/promotions/types.ts index df356c69b9682..15d2608c44372 100644 --- a/packages/medusa/src/api-v2/admin/promotions/types.ts +++ b/packages/medusa/src/api-v2/admin/promotions/types.ts @@ -3,3 +3,7 @@ import { PaginatedResponse, PromotionDTO } from "@medusajs/types" export type AdminPromotionsListRes = PaginatedResponse<{ promotions: PromotionDTO[] }> + +export type AdminPromotionRes = { + promotion: PromotionDTO +} From f5e4a4394af75d3a82f4bd93b9a29242c1f4655d Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 2 Apr 2024 09:36:14 +0200 Subject: [PATCH 2/7] chore: add edit rules, edit details and edit campaign pages --- .../public/locales/en-US/translation.json | 80 +++- .../components/common/combobox/combobox.tsx | 2 +- .../dashboard/src/lib/api-v2/campaign.ts | 57 +++ .../dashboard/src/lib/api-v2/promotion.ts | 7 + .../src/providers/router-provider/v2.tsx | 20 + .../edit-rules-form/edit-rules-form.tsx | 364 ++++++++++++++++++ .../components/edit-rules-form/index.ts | 1 + .../promotions/edit-rules/edit-rules.tsx | 31 ++ .../v2-routes/promotions/edit-rules/index.ts | 1 + .../add-campaign-promotion-form.tsx | 151 ++++++++ .../campaign-details.tsx | 116 ++++++ .../add-campaign-promotion-form/index.ts | 1 + .../promotion-add-campaign/index.ts | 1 + .../promotion-add-campaign.tsx | 40 ++ .../campaign-section/campaign-section.tsx | 10 +- .../promotion-conditions-section.tsx | 2 +- .../promotion-detail/promotion-detail.tsx | 2 + .../edit-promotion-details-form.tsx | 279 ++++++++++++++ .../components/edit-promotion-form/index.ts | 1 + .../promotion-edit-details/index.ts | 1 + .../promotion-edit-details.tsx | 30 ++ .../src/api-v2/admin/campaigns/index.ts | 2 + .../src/api-v2/admin/campaigns/types.ts | 9 + packages/medusa/src/api-v2/admin/index.ts | 1 + 24 files changed, 1204 insertions(+), 5 deletions(-) create mode 100644 packages/admin-next/dashboard/src/lib/api-v2/campaign.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/promotion-edit-details.tsx create mode 100644 packages/medusa/src/api-v2/admin/campaigns/index.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/types.ts diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 3eb4f4387d7a0..47ad2405dd7f4 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -507,6 +507,7 @@ "fields": { "method": "Method", "type": "Type", + "value_type": "Value Type", "value": "Value", "campaign": "Campaign", "allocation": "Allocation", @@ -525,17 +526,94 @@ } } }, + "edit": { + "title": "Edit Promotion Details", + "rules": { + "title": "Edit rules" + }, + "target_rules": { + "title": "Edit target rules" + }, + "buy_rules": { + "title": "Edit buy rules" + } + }, + "addToCampaign": { + "title": "Add Promotion To Campaign" + }, + "form": { + "campaign": { + "existing": { + "title": "Existing Campaign", + "description": "Would you like to add promotion to an existing campaign?" + }, + "new": { + "title": "New Campaign", + "description": "Would you like to create a new campaign with this promotion?" + } + }, + "status": { + "title": "Status" + }, + "method": { + "code": { + "title": "Promotion code", + "description": "Customers must enter this at checkout" + }, + "automatic": { + "title": "Automatic", + "description": "Customers will automatically see this during checkout" + } + }, + "allocation": { + "each": { + "title": "Each", + "description": "Applies value on each product" + }, + "across": { + "title": "Across", + "description": "Applies value proportionally across products" + } + }, + "code": { + "title": "Code", + "description": "The code your customers will enter during checkout." + }, + "value": { + "title": "Value" + }, + "value_type": { + "fixed": { + "title": "Fixed amount", + "description": "eg. 100" + }, + "percentage": { + "title": "Percentage", + "description": "eg. 8%" + } + } + }, "deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.", "createPromotionTitle": "Create Promotion", "type": "Promotion type" }, "campaigns": { "domain": "Campaigns", + "details": "Campaign details", "fields": { "name": "Name", "identifier": "Identifier", "start_date": "Start date", "end_date": "End date" + }, + "budget": { + "details": "Campaign budget", + "fields": { + "type": "Type", + "currency": "Currency", + "limit": "Limit", + "used": "Used" + } } }, "pricing": { @@ -995,4 +1073,4 @@ "seconds_one": "Second", "seconds_other": "Seconds" } -} \ No newline at end of file +} diff --git a/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx index 21378b270b1aa..91269d99d79ce 100644 --- a/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx +++ b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx @@ -238,7 +238,7 @@ const ComboboxImpl = ( (QUERY_KEY) + +export const adminCampaignQueryFns = { + list: (query: AdminGetCampaignsParams) => + medusa.admin.custom.get(`/admin/campaigns`, query), + detail: (id: string) => medusa.admin.custom.get(`/admin/campaigns/${id}`), +} + +export const useV2Campaigns = ( + query?: AdminGetCampaignsParams, + options?: object +) => { + const { data, ...rest } = useAdminCustomQuery< + AdminGetCampaignsParams, + AdminCampaignsListRes + >("/admin/campaigns", adminCampaignKeys.list(query), query, options) + + return { ...data, ...rest } +} + +export const useV2Campaign = ( + id: string, + query?: AdminGetCampaignsParams, + options?: object +) => { + const { data, ...rest } = useAdminCustomQuery< + AdminGetCampaignsCampaignParams, + AdminCampaignRes + >(`/admin/campaigns/${id}`, adminCampaignKeys.detail(id), query, options) + + return { ...data, ...rest } +} + +export const useV2DeleteCampaign = (id: string) => { + return useMutation(() => medusa.admin.custom.delete(`/admin/campaigns/${id}`)) +} + +export const useV2PostCampaign = (id: string) => { + return useMutation((args: AdminPostCampaignsCampaignReq) => + medusa.client.request("POST", `/admin/campaigns/${id}`, args) + ) +} diff --git a/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts b/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts index aa0b6cb8935bd..2d73a96256ca4 100644 --- a/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts +++ b/packages/admin-next/dashboard/src/lib/api-v2/promotion.ts @@ -1,6 +1,7 @@ import { AdminGetPromotionsParams, AdminGetPromotionsPromotionParams, + AdminPostPromotionsPromotionReq, AdminPromotionRes, AdminPromotionsListRes, } from "@medusajs/medusa" @@ -50,3 +51,9 @@ export const useV2DeletePromotion = (id: string) => { medusa.admin.custom.delete(`/admin/promotions/${id}`) ) } + +export const useV2PostPromotion = (id: string) => { + return useMutation((args: AdminPostPromotionsPromotionReq) => + medusa.client.request("POST", `/admin/promotions/${id}`, args) + ) +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index b7560c9b60e0e..0774e3ac15db8 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -94,6 +94,26 @@ export const v2Routes: RouteObject[] = [ handle: { crumb: (data: AdminPromotionRes) => data.promotion?.code, }, + children: [ + { + path: "edit", + lazy: () => + import( + "../../v2-routes/promotions/promotion-edit-details" + ), + }, + { + path: "add-to-campaign", + lazy: () => + import( + "../../v2-routes/promotions/promotion-add-campaign" + ), + }, + { + path: ":ruleType/edit", + lazy: () => import("../../v2-routes/promotions/edit-rules"), + }, + ], }, ], }, diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx new file mode 100644 index 0000000000000..ef5dd65c2c8cc --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx @@ -0,0 +1,364 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { XMarkMini } from "@medusajs/icons" +import { PromotionDTO } from "@medusajs/types" +import { Badge, Button, Heading, Select, Text } from "@medusajs/ui" +import { useFieldArray, useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Combobox } from "../../../../../components/common/combobox" + +import { Fragment } from "react" +import { Form } from "../../../../../components/common/form" +import { + RouteDrawer, + useRouteModal, +} from "../../../../../components/route-modal" +import { useV2PostPromotion } from "../../../../../lib/api-v2" + +type EditPromotionFormProps = { + promotion: PromotionDTO + ruleType: string +} + +const ruleValidation = zod.object({ + id: zod.string().optional(), + attribute: zod.string().min(1, { message: "Required field" }), + operator: zod.string().min(1, { message: "Required field" }), + values: zod.string().array().min(1, { message: "Required field" }), + required: zod.boolean(), + frontend_id: zod.string().optional(), +}) + +const EditRules = zod.object({ + rules: zod.array(ruleValidation), +}) + +export const EditRulesForm = ({ + promotion, + ruleType, +}: EditPromotionFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const { rules = [] } = promotion + + const form = useForm>({ + defaultValues: { + rules: rules.map((rule) => ({ + id: rule.id, + required: rule.attribute === "currency.code", + attribute: rule.attribute!, + operator: rule.operator!, + values: rule.values.map((v) => v.value!), + })), + }, + resolver: zodResolver(EditRules), + }) + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "rules", + }) + + const { mutateAsync, isLoading } = useV2PostPromotion(promotion.id) + + const handleSubmit = form.handleSubmit(async (data) => { + // TODO: update rules + await mutateAsync( + {}, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + // TODO: Replace with an endpoint + const attributes = [ + { + required: true, + title: "Currency Code", + value: "currency.code", + }, + { + title: "Customer Group", + value: "customer_group.id", + }, + { + title: "Region", + value: "region.id", + }, + { + title: "Country", + value: "address.country_code", + }, + { + title: "Sales Channel", + value: "sales_channel_id", + }, + ] + + const operators = [ + { + title: "In", + value: "in", + }, + { + title: "Equals", + value: "eq", + }, + { + title: "Not In", + value: "nin", + }, + ] + + const values = [ + { + title: "France", + value: "region_id", + }, + { + title: "USD", + value: "usd", + }, + { + title: "Customer Group", + value: "cusgro_id", + }, + ] + + const attributesMap = { + rules: attributes, + } + + const operatorMap = { + rules: operators, + } + + const valuesMap = { + rules: values, + } + + return ( + +
+ +
+ + {t(`promotions.fields.conditions.${ruleType}.title`)} + + + + {t(`promotions.fields.conditions.${ruleType}.description`)} + + + {fields.map((rule, index) => { + const { ref: attributeRef, ...attributeFields } = form.register( + `rules.${index}.attribute` + ) + const { ref: operatorRef, ...operatorFields } = form.register( + `rules.${index}.operator` + ) + const { ref: valuesRef, ...valuesFields } = form.register( + `rules.${index}.values` + ) + + return ( + +
+
+ { + return ( + + {rule.required && ( +

+ Required +

+ )} + + + + + +
+ ) + }} + /> + +
+ { + return ( + + + + + + + ) + }} + /> + + { + return ( + + + ({ + label: v.title, + value: v.value, + }))} + placeholder="Select Values" + {...field} + onChange={onChange} + className="bg-ui-bg-base placeholder:text-green-200" + /> + + + + ) + }} + /> +
+
+ +
+ { + if (!rule.required) { + remove(index) + } + }} + /> +
+
+ + {index < fields.length - 1 && ( +
+
+ + + AND + +
+ )} +
+ ) + })} + +
+ + + +
+
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts new file mode 100644 index 0000000000000..b3e29984030d6 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-rules-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx new file mode 100644 index 0000000000000..c7d9eb69117c8 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx @@ -0,0 +1,31 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/route-modal" +import { useV2Promotion } from "../../../lib/api-v2/promotion" +import { EditRulesForm } from "./components/edit-rules-form" + +export const EditRules = () => { + const { id, ruleType: ruleTypeParam } = useParams() + const { t } = useTranslation() + const ruleType = ruleTypeParam!.split("-").join("_") + + const { promotion, isLoading, isError, error } = useV2Promotion(id!) + + if (isError) { + throw error + } + + return ( + + + {t(`promotions.edit.${ruleType}.title`)} + + + {!isLoading && promotion && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts new file mode 100644 index 0000000000000..f9059883053ac --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts @@ -0,0 +1 @@ +export { EditRules as Component } from "./edit-rules" 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 new file mode 100644 index 0000000000000..ec1a01f4b51e4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx @@ -0,0 +1,151 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { CampaignDTO, PromotionDTO } from "@medusajs/types" +import { Button, RadioGroup, Select } from "@medusajs/ui" +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 { useV2PostPromotion } from "../../../../../lib/api-v2" + +type EditPromotionFormProps = { + promotion: PromotionDTO + campaigns: CampaignDTO[] +} + +const EditPromotionSchema = zod.object({ + campaign_id: zod.string().optional(), + existing: zod.string().toLowerCase(), +}) + +export const AddCampaignPromotionForm = ({ + promotion, + campaigns, +}: EditPromotionFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const { campaign } = promotion + + const form = useForm>({ + defaultValues: { + campaign_id: campaign?.id, + existing: "true", + }, + resolver: zodResolver(EditPromotionSchema), + }) + + const watchCampaignId = useWatch({ + control: form.control, + name: "campaign_id", + }) + + const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId) + const { mutateAsync, isLoading } = useV2PostPromotion(promotion.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { campaign_id: data.campaign_id }, + { onSuccess: () => handleSuccess() } + ) + }) + + return ( + +
+ +
+ { + return ( + + Method + + + + + + + + + + ) + }} + /> + + { + return ( + + + {t("promotions.form.campaign.existing.title")} + + + + + + + + ) + }} + /> + + +
+
+ + +
+ + + + + +
+
+
+
+ ) +} 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 new file mode 100644 index 0000000000000..c84a0a2cf0f03 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/campaign-details.tsx @@ -0,0 +1,116 @@ +import { CampaignDTO } from "@medusajs/types" +import { Heading, Text } from "@medusajs/ui" +import { Fragment } from "react" +import { useTranslation } from "react-i18next" + +type CampaignDetailsProps = { + campaign?: CampaignDTO +} + +export const CampaignDetails = ({ campaign }: CampaignDetailsProps) => { + const { t } = useTranslation() + + if (!campaign) { + return + } + + return ( + +
+ + {t("campaigns.details")} + + +
+ + {t("campaigns.fields.identifier")} + + +
+ + {campaign.campaign_identifier || "-"} + +
+
+ +
+ {t("fields.description")} + +
+ {campaign.description || "-"} +
+
+ +
+ + {t("campaigns.fields.start_date")} + + +
+ + {campaign.starts_at?.toString() || "-"} + +
+
+ +
+ + {t("campaigns.fields.end_date")} + + +
+ + {campaign.ends_at?.toString() || "-"} + +
+
+
+ +
+ + {t("campaigns.budget.details")} + + +
+ + {t("campaigns.budget.fields.type")} + + +
+ {campaign.budget?.type || "-"} +
+
+ +
+ + {t("campaigns.budget.fields.currency")} + + +
+ {campaign.currency || "-"} +
+
+ +
+ + {t("campaigns.budget.fields.limit")} + + +
+ {campaign.budget?.limit || "-"} +
+
+ +
+ + {t("campaigns.budget.fields.used")} + + +
+ {campaign.budget?.used || "-"} +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/index.ts new file mode 100644 index 0000000000000..c7fb969a7d720 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/index.ts @@ -0,0 +1 @@ +export * from "./add-campaign-promotion-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/index.ts new file mode 100644 index 0000000000000..3836868e22ed1 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/index.ts @@ -0,0 +1 @@ +export { PromotionAddCampaign as Component } from "./promotion-add-campaign" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx new file mode 100644 index 0000000000000..d67677556dd13 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx @@ -0,0 +1,40 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/route-modal" +import { useV2Campaigns } from "../../../lib/api-v2/campaign" +import { useV2Promotion } from "../../../lib/api-v2/promotion" +import { AddCampaignPromotionForm } from "./components/add-campaign-promotion-form" + +export const PromotionAddCampaign = () => { + const { id } = useParams() + const { t } = useTranslation() + const { promotion, isLoading, isError, error } = useV2Promotion(id!) + const { + campaigns, + isLoading: areCampaignsLoading, + isError: isCampaignError, + error: campaignError, + } = useV2Campaigns() + + if (isError) { + throw error + } + + if (isCampaignError) { + throw campaignError + } + + return ( + + + {t("promotions.addToCampaign.title")} + + + {!isLoading && !areCampaignsLoading && promotion && campaigns && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx index 04ac9d1aa9357..094eb0a719d51 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/campaign-section/campaign-section.tsx @@ -4,6 +4,7 @@ import { Container, Heading, Text } from "@medusajs/ui" import { format } from "date-fns" import { Fragment } from "react" import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" import { NoRecords } from "../../../../../components/common/empty-table-content" @@ -74,18 +75,20 @@ const CampaignDetailSection = ({ campaign }: { campaign: CampaignDTO }) => { export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => { const { t } = useTranslation() + const { id } = useParams() return (
{t("promotions.fields.campaign")} + , }, ], @@ -101,7 +104,10 @@ export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => { className="h-[180px] p-6 text-center" title="Not part of a campaign" message="Add this promotion to an existing campaign" - action={{ to: "/promotions", label: "Add to Campaign" }} + action={{ + to: `/promotions/${id}/add-to-campaign`, + label: "Add to Campaign", + }} buttonVariant="transparentIconLeft" /> )} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx index 78974f82e9fe4..2fa7fbc175130 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx @@ -63,7 +63,7 @@ export const PromotionConditionsSection = ({ { icon: , label: t("actions.edit"), - to: `conditions`, + to: `${ruleType.split("_").join("-")}/edit`, }, ], }, diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx index 88b7b5ec25beb..35be8773cd84f 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx @@ -56,6 +56,7 @@ export const PromotionDetail = () => {
+ {after.widgets.map((w, i) => { return (
@@ -63,6 +64,7 @@ export const PromotionDetail = () => {
) })} +
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx new file mode 100644 index 0000000000000..b1e1499741310 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx @@ -0,0 +1,279 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { PromotionDTO } from "@medusajs/types" +import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui" +import { useForm, useWatch } from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" +import * as zod from "zod" + +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" +import { + RouteDrawer, + useRouteModal, +} from "../../../../../components/route-modal" +import { useV2PostPromotion } from "../../../../../lib/api-v2" +import { getCurrencySymbol } from "../../../../../lib/currencies" + +type EditPromotionFormProps = { + promotion: PromotionDTO +} + +const EditPromotionSchema = zod.object({ + is_automatic: zod.string().toLowerCase(), + code: zod.string().min(1), + value_type: zod.enum(["fixed", "percentage"]), + value: zod.string(), + allocation: zod.enum(["each", "across"]), +}) + +export const EditPromotionDetailsForm = ({ + promotion, +}: EditPromotionFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + is_automatic: promotion.is_automatic!.toString(), + code: promotion.code, + value: promotion.application_method!.value?.toString(), + allocation: promotion.application_method!.allocation, + value_type: promotion.application_method!.type, + }, + resolver: zodResolver(EditPromotionSchema), + }) + + const watchValueType = useWatch({ + control: form.control, + name: "value_type", + }) + + const isFixedValueType = watchValueType === "fixed" + + const { mutateAsync, isLoading } = useV2PostPromotion(promotion.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + is_automatic: Boolean(data.is_automatic), + code: data.code, + application_method: { + value: data.value, + type: data.value_type as any, + allocation: data.allocation as any, + }, + }, + { + onSuccess: () => { + handleSuccess() + }, + } + ) + }) + + return ( + +
+ +
+ { + return ( + + Method + + + + + + + + + ) + }} + /> + +
+ { + return ( + + {t("promotions.form.code.title")} + + + + + + ) + }} + /> + + + ]} + /> + +
+ + { + return ( + + {t("promotions.fields.value_type")} + + + + + + + + + + ) + }} + /> + + { + return ( + + + {isFixedValueType + ? t("fields.amount") + : t("fields.percentage")} + + + {isFixedValueType ? ( + + ) : ( + { + onChange( + e.target.value === "" ? null : e.target.value + ) + }} + /> + )} + + + + ) + }} + /> + + { + return ( + + {t("promotions.fields.allocation")} + + + + + + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/index.ts new file mode 100644 index 0000000000000..94c686f950714 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-promotion-details-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/index.ts new file mode 100644 index 0000000000000..a49ed0b0885d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/index.ts @@ -0,0 +1 @@ +export { PromotionEditDetails as Component } from "./promotion-edit-details" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/promotion-edit-details.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/promotion-edit-details.tsx new file mode 100644 index 0000000000000..91572219672c9 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/promotion-edit-details.tsx @@ -0,0 +1,30 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/route-modal" +import { useV2Promotion } from "../../../lib/api-v2/promotion" +import { EditPromotionDetailsForm } from "./components/edit-promotion-form" + +export const PromotionEditDetails = () => { + const { id } = useParams() + const { t } = useTranslation() + + const { promotion, isLoading, isError, error } = useV2Promotion(id!) + + if (isError) { + throw error + } + + return ( + + + {t("promotions.edit.title")} + + + {!isLoading && promotion && ( + + )} + + ) +} diff --git a/packages/medusa/src/api-v2/admin/campaigns/index.ts b/packages/medusa/src/api-v2/admin/campaigns/index.ts new file mode 100644 index 0000000000000..1bb71ae4743e1 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export * from "./validators" diff --git a/packages/medusa/src/api-v2/admin/campaigns/types.ts b/packages/medusa/src/api-v2/admin/campaigns/types.ts new file mode 100644 index 0000000000000..33bac922d1100 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/types.ts @@ -0,0 +1,9 @@ +import { CampaignDTO, PaginatedResponse } from "@medusajs/types" + +export type AdminCampaignsListRes = PaginatedResponse<{ + campaigns: CampaignDTO[] +}> + +export type AdminCampaignRes = { + campaign: CampaignDTO +} diff --git a/packages/medusa/src/api-v2/admin/index.ts b/packages/medusa/src/api-v2/admin/index.ts index b579f7910fc3b..fe7cd6873d04a 100644 --- a/packages/medusa/src/api-v2/admin/index.ts +++ b/packages/medusa/src/api-v2/admin/index.ts @@ -1 +1,2 @@ +export * from "./campaigns" export * from "./promotions" From c28f6676a8a073f7df65df0e9fe9aca8f4bfeb04 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 2 Apr 2024 12:17:24 +0200 Subject: [PATCH 3/7] chore: change to type button --- .../components/edit-rules-form/edit-rules-form.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx index ef5dd65c2c8cc..57f4c0a5048a9 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx @@ -309,16 +309,16 @@ export const EditRulesForm = ({
-
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts new file mode 100644 index 0000000000000..af13209435c60 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts @@ -0,0 +1,52 @@ +import { PromotionDTO } from "@medusajs/types" +import { RuleType } from "../../edit-rules" + +// We are disguising couple of database columns as rules here, namely +// apply_to_quantity and buy_rules_min_quantity. +// We need to transform the database value into a disugised "rule" shape +// for the form +export function getDisguisedRules( + promotion: PromotionDTO, + requiredAttributes: any[], + ruleType: string +) { + if (ruleType === RuleType.RULES && !requiredAttributes?.length) { + return [] + } + + const applyToQuantityRule = requiredAttributes.find( + (attr) => attr.id === "apply_to_quantity" + ) + + const buyRulesMinQuantityRule = requiredAttributes.find( + (attr) => attr.id === "buy_rules_min_quantity" + ) + + if (ruleType === RuleType.TARGET_RULES) { + return [ + { + id: "apply_to_quantity", + attribute: "apply_to_quantity", + operator: "eq", + required: applyToQuantityRule?.required, + field_type: applyToQuantityRule?.field_type, + values: [{ value: promotion?.application_method?.apply_to_quantity }], + }, + ] + } + + if (ruleType === RuleType.BUY_RULES) { + return [ + { + id: "buy_rules_min_quantity", + attribute: "buy_rules_min_quantity", + operator: "eq", + required: buyRulesMinQuantityRule?.required, + field_type: buyRulesMinQuantityRule?.field_type, + values: [ + { value: promotion?.application_method?.buy_rules_min_quantity }, + ], + }, + ] + } +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx index 29d63a20043ae..2a87feb6dbce1 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx @@ -1,7 +1,7 @@ +import { PromotionRuleDTO } from "@medusajs/types" import { Heading } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" - import { RouteDrawer } from "../../../components/route-modal" import { useV2Promotion, @@ -10,17 +10,53 @@ import { } from "../../../lib/api-v2/promotion" import { EditRulesForm } from "./components/edit-rules-form" +export enum RuleType { + RULES = "rules", + BUY_RULES = "buy-rules", + TARGET_RULES = "target-rules", +} +export type RuleTypeValues = "rules" | "buy-rules" | "target-rules" + export const EditRules = () => { - const { id, ruleType: ruleTypeParam } = useParams() + const params = useParams() + const allowedParams: string[] = [ + RuleType.RULES, + RuleType.BUY_RULES, + RuleType.TARGET_RULES, + ] + + if (!allowedParams.includes(params.ruleType!)) { + throw "invalid page" + } + const { t } = useTranslation() - const ruleType = ruleTypeParam!.split("-").join("_") + const ruleType = params.ruleType as RuleTypeValues + const id = params.id as string + const rules: PromotionRuleDTO[] = [] + const { promotion, isLoading, isError, error } = useV2Promotion(id) + const { + attributes, + isError: isAttributesError, + error: attributesError, + } = useV2PromotionRuleAttributeOptions(ruleType!) + const { + operators, + isError: isOperatorsError, + error: operatorsError, + } = useV2PromotionRuleOperatorOptions() - const { promotion, isLoading, isError, error } = useV2Promotion(id!) - const { attributes } = useV2PromotionRuleAttributeOptions("rules") - const { operators } = useV2PromotionRuleOperatorOptions() + if (promotion) { + if (ruleType === RuleType.RULES) { + rules.push(...(promotion.rules || [])) + } else if (ruleType === RuleType.TARGET_RULES) { + rules.push(...(promotion?.application_method?.target_rules || [])) + } else if (ruleType === RuleType.BUY_RULES) { + rules.push(...(promotion.application_method?.buy_rules || [])) + } + } - if (isError) { - throw error + if (isError || isAttributesError || isOperatorsError) { + throw error || attributesError || operatorsError } return ( @@ -29,9 +65,10 @@ export const EditRules = () => { {t(`promotions.edit.${ruleType}.title`)} - {!isLoading && promotion && ( + {!isLoading && promotion && attributes && operators && ( - +
+
- {rule.attribute} + {rule.attribute_label} - {rule.operator} + + {rule.operator_label} + - {rule.values?.map((v) => v.value).join(",")} + {rule.values?.map((v) => v.label).join(",")} - +
) } type PromotionConditionsSectionProps = { - rules: PromotionRuleDTO[] + rules: AdminGetPromotionRulesRes ruleType: PromotionRuleTypes } @@ -63,7 +65,7 @@ export const PromotionConditionsSection = ({ { icon: , label: t("actions.edit"), - to: `${ruleType.split("_").join("-")}/edit`, + to: `${ruleType}/edit`, }, ], }, diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx index 35be8773cd84f..a25fb2a67b0a5 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/promotion-detail.tsx @@ -1,7 +1,10 @@ import { Outlet, useLoaderData, useParams } from "react-router-dom" import { JsonViewSection } from "../../../components/common/json-view-section" -import { useV2Promotion } from "../../../lib/api-v2/promotion" +import { + useV2Promotion, + useV2PromotionRules, +} from "../../../lib/api-v2/promotion" import { CampaignSection } from "./components/campaign-section" import { PromotionConditionsSection } from "./components/promotion-conditions-section" import { PromotionGeneralSection } from "./components/promotion-general-section" @@ -17,6 +20,9 @@ export const PromotionDetail = () => { const { id } = useParams() const { promotion, isLoading } = useV2Promotion(id!, {}, { initialData }) + const { rules } = useV2PromotionRules(id!, "rules") + const { rules: targetRules } = useV2PromotionRules(id!, "target-rules") + const { rules: buyRules } = useV2PromotionRules(id!, "buy-rules") if (isLoading || !promotion) { return
Loading...
@@ -36,20 +42,17 @@ export const PromotionDetail = () => {
- + {promotion.type === "buyget" && ( )} diff --git a/packages/medusa/src/api-v2/admin/promotions/[id]/[rule_type]/route.ts b/packages/medusa/src/api-v2/admin/promotions/[id]/[rule_type]/route.ts new file mode 100644 index 0000000000000..2159ea5d49754 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/[id]/[rule_type]/route.ts @@ -0,0 +1,115 @@ +import { AdminGetPromotionRulesRes } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, + RuleOperator, + RuleType, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" +import { + operatorsMap, + ruleAttributesMap, + ruleQueryConfigurations, + validateRuleType, +} from "../../utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, rule_type: ruleType } = req.params + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + validateRuleType(ruleType) + + const dasherizedRuleType = ruleType.split("-").join("_") + const queryObject = remoteQueryObjectFromString({ + entryPoint: "promotion", + variables: { id }, + fields: req.remoteQueryConfig.fields, + }) + + const [promotion] = await remoteQuery(queryObject) + const ruleAttributes = ruleAttributesMap[ruleType] + const promotionRules: any[] = [] + + if (dasherizedRuleType === RuleType.RULES) { + promotionRules.push(...(promotion?.rules || [])) + } else if (dasherizedRuleType === RuleType.TARGET_RULES) { + promotionRules.push(...(promotion.application_method?.target_rules || [])) + } else if (dasherizedRuleType === RuleType.BUY_RULES) { + promotionRules.push(...(promotion.application_method?.buy_rules || [])) + } + + const transformedRules: AdminGetPromotionRulesRes = [] + const disguisedRules = ruleAttributes.filter((attr) => !!attr.disguised) + + for (const disguisedRule of disguisedRules) { + const value = promotion.application_method?.[disguisedRule.id] + const values = [{ label: value, value }] + + transformedRules.push({ + id: undefined, + attribute: disguisedRule.id, + attribute_label: disguisedRule.label, + operator: RuleOperator.EQ, + operator_label: operatorsMap[RuleOperator.EQ].label, + values, + disguised: true, + required: true, + }) + + continue + } + + for (const promotionRule of promotionRules) { + const currentRuleAttribute = ruleAttributes.find( + (attr) => attr.value === promotionRule.attribute + ) + + if (!currentRuleAttribute) { + continue + } + + const queryConfig = ruleQueryConfigurations[currentRuleAttribute.id] + const rows = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: queryConfig.entryPoint, + variables: { + filters: { + [queryConfig.valueAttr]: promotionRule.values.map((v) => v.value), + }, + }, + fields: [queryConfig.labelAttr, queryConfig.valueAttr], + }) + ) + + const valueLabelMap = new Map( + rows.map((row) => [ + row[queryConfig.valueAttr], + row[queryConfig.labelAttr], + ]) + ) + + promotionRule.values = promotionRule.values.map((value) => ({ + value: value.value, + label: valueLabelMap.get(value.value) || value.value, + })) + + transformedRules.push({ + ...promotionRule, + attribute_label: currentRuleAttribute.label, + operator_label: + operatorsMap[promotionRule.operator]?.label || promotionRule.operator, + disguised: false, + required: currentRuleAttribute.required || false, + }) + } + + res.json({ + rules: transformedRules, + }) +} diff --git a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts index 742b7d8763835..4eba8e1579d23 100644 --- a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts @@ -45,6 +45,16 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["GET"], + matcher: "/admin/promotions/:id/:rule_type", + middlewares: [ + transformQuery( + AdminGetPromotionsPromotionParams, + QueryConfig.retrieveNewTransformQueryConfig + ), + ], + }, { method: ["POST"], matcher: "/admin/promotions/:id", diff --git a/packages/medusa/src/api-v2/admin/promotions/query-config.ts b/packages/medusa/src/api-v2/admin/promotions/query-config.ts index fbd6bf5beec73..e0144f0f352b8 100644 --- a/packages/medusa/src/api-v2/admin/promotions/query-config.ts +++ b/packages/medusa/src/api-v2/admin/promotions/query-config.ts @@ -27,22 +27,29 @@ export const defaultAdminPromotionFields = [ "application_method.value", "application_method.type", "application_method.max_quantity", + "application_method.apply_to_quantity", + "application_method.buy_rules_min_quantity", "application_method.target_type", "application_method.allocation", "application_method.created_at", "application_method.updated_at", "application_method.deleted_at", + "application_method.buy_rules.id", "application_method.buy_rules.attribute", "application_method.buy_rules.operator", "application_method.buy_rules.values.value", + "application_method.target_rules.id", "application_method.target_rules.attribute", "application_method.target_rules.operator", "application_method.target_rules.values.value", + "rules.id", "rules.attribute", "rules.operator", "rules.values.value", ] +const defaults = [...defaultAdminPromotionFields] + export const retrieveTransformQueryConfig = { defaultFields: defaultAdminPromotionFields, defaultRelations: defaultAdminPromotionRelations, @@ -60,3 +67,9 @@ export const listRuleValueTransformQueryConfig = { allowed: [], isList: true, } + +// TODO: replace this with the old one +export const retrieveNewTransformQueryConfig = { + defaults, + isList: false, +} diff --git a/packages/medusa/src/api-v2/admin/promotions/rule-operator-options/route.ts b/packages/medusa/src/api-v2/admin/promotions/rule-operator-options/route.ts index c5317f6c9b125..7bd25b91fa44d 100644 --- a/packages/medusa/src/api-v2/admin/promotions/rule-operator-options/route.ts +++ b/packages/medusa/src/api-v2/admin/promotions/rule-operator-options/route.ts @@ -1,32 +1,14 @@ -import { RuleOperator } from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../types/routing" - -const operators = [ - { - id: RuleOperator.IN, - value: RuleOperator.IN, - label: "In", - }, - { - id: RuleOperator.EQ, - value: RuleOperator.EQ, - label: "Equals", - }, - { - id: RuleOperator.NE, - value: RuleOperator.NE, - label: "Not In", - }, -] +import { operatorsMap } from "../utils" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { res.json({ - operators, + operators: Object.values(operatorsMap), }) } diff --git a/packages/medusa/src/api-v2/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts b/packages/medusa/src/api-v2/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts index 8f2d009cd765e..794c59e977d5d 100644 --- a/packages/medusa/src/api-v2/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts @@ -6,67 +6,18 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../../../types/routing" -import { validateRuleAttribute, validateRuleType } from "../../../utils" - -const queryConfigurations = { - region: { - entryPoint: "region", - labelAttr: "name", - valueAttr: "id", - }, - currency: { - entryPoint: "currency", - labelAttr: "name", - valueAttr: "code", - }, - customer_group: { - entryPoint: "customer_group", - labelAttr: "name", - valueAttr: "id", - }, - sales_channel: { - entryPoint: "sales_channel", - labelAttr: "name", - valueAttr: "id", - }, - country: { - entryPoint: "country", - labelAttr: "display_name", - valueAttr: "iso_2", - }, - product: { - entryPoint: "product", - labelAttr: "title", - valueAttr: "id", - }, - product_category: { - entryPoint: "product_category", - labelAttr: "name", - valueAttr: "id", - }, - product_collection: { - entryPoint: "product_collection", - labelAttr: "title", - valueAttr: "id", - }, - product_type: { - entryPoint: "product_type", - labelAttr: "value", - valueAttr: "id", - }, - product_tag: { - entryPoint: "product_tag", - labelAttr: "value", - valueAttr: "id", - }, -} +import { + ruleQueryConfigurations, + validateRuleAttribute, + validateRuleType, +} from "../../../utils" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params - const queryConfig = queryConfigurations[ruleAttributeId] + const queryConfig = ruleQueryConfigurations[ruleAttributeId] const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) validateRuleType(ruleType) diff --git a/packages/medusa/src/api-v2/admin/promotions/utils/index.ts b/packages/medusa/src/api-v2/admin/promotions/utils/index.ts index 193881239c49b..b9837dfc396f6 100644 --- a/packages/medusa/src/api-v2/admin/promotions/utils/index.ts +++ b/packages/medusa/src/api-v2/admin/promotions/utils/index.ts @@ -1,3 +1,5 @@ +export * from "./operators-map" export * from "./rule-attributes-map" +export * from "./rule-query-configuration" export * from "./validate-rule-attribute" export * from "./validate-rule-type" diff --git a/packages/medusa/src/api-v2/admin/promotions/utils/operators-map.ts b/packages/medusa/src/api-v2/admin/promotions/utils/operators-map.ts new file mode 100644 index 0000000000000..254f6b6dde9f2 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/utils/operators-map.ts @@ -0,0 +1,19 @@ +import { RuleOperator } from "@medusajs/utils" + +export const operatorsMap = { + [RuleOperator.IN]: { + id: RuleOperator.IN, + value: RuleOperator.IN, + label: "In", + }, + [RuleOperator.EQ]: { + id: RuleOperator.EQ, + value: RuleOperator.EQ, + label: "Equals", + }, + [RuleOperator.NE]: { + id: RuleOperator.NE, + value: RuleOperator.NE, + label: "Not In", + }, +} diff --git a/packages/medusa/src/api-v2/admin/promotions/utils/rule-attributes-map.ts b/packages/medusa/src/api-v2/admin/promotions/utils/rule-attributes-map.ts index 9e41712c0df7b..7fb2e989e0845 100644 --- a/packages/medusa/src/api-v2/admin/promotions/utils/rule-attributes-map.ts +++ b/packages/medusa/src/api-v2/admin/promotions/utils/rule-attributes-map.ts @@ -1,3 +1,17 @@ +export enum DisguisedRule { + APPLY_TO_QUANTITY = "apply_to_quantity", + BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity", +} + +export const disguisedRulesMap = { + [DisguisedRule.APPLY_TO_QUANTITY]: { + relation: "application_method", + }, + [DisguisedRule.BUY_RULES_MIN_QUANTITY]: { + relation: "application_method", + }, +} + const ruleAttributes = [ { id: "currency", @@ -66,20 +80,24 @@ const commonAttributes = [ const buyRuleAttributes = [ { - id: "buy_rules_min_quantity", - value: "buy_rules_min_quantity", + id: DisguisedRule.BUY_RULES_MIN_QUANTITY, + value: DisguisedRule.BUY_RULES_MIN_QUANTITY, label: "Minimum quantity of items", + field_type: "number", required: true, + disguised: true, }, ...commonAttributes, ] const targetRuleAttributes = [ { - id: "apply_to_quantity", - value: "apply_to_quantity", + id: DisguisedRule.APPLY_TO_QUANTITY, + value: DisguisedRule.APPLY_TO_QUANTITY, label: "Quantity of items promotion will apply to", + field_type: "number", required: true, + disguised: true, }, ...commonAttributes, ] diff --git a/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts b/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts new file mode 100644 index 0000000000000..61e9a1de9d90f --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts @@ -0,0 +1,52 @@ +export const ruleQueryConfigurations = { + region: { + entryPoint: "region", + labelAttr: "name", + valueAttr: "id", + }, + currency: { + entryPoint: "currency", + labelAttr: "name", + valueAttr: "code", + }, + customer_group: { + entryPoint: "customer_group", + labelAttr: "name", + valueAttr: "id", + }, + sales_channel: { + entryPoint: "sales_channel", + labelAttr: "name", + valueAttr: "id", + }, + country: { + entryPoint: "country", + labelAttr: "display_name", + valueAttr: "iso_2", + }, + product: { + entryPoint: "product", + labelAttr: "title", + valueAttr: "id", + }, + product_category: { + entryPoint: "product_category", + labelAttr: "name", + valueAttr: "id", + }, + product_collection: { + entryPoint: "product_collection", + labelAttr: "title", + valueAttr: "id", + }, + product_type: { + entryPoint: "product_type", + labelAttr: "value", + valueAttr: "id", + }, + product_tag: { + entryPoint: "product_tag", + labelAttr: "value", + valueAttr: "id", + }, +} diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts index 17906ec6ee662..c3da8a0fd2802 100644 --- a/packages/medusa/src/api-v2/admin/promotions/validators.ts +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -29,6 +29,7 @@ import { XorConstraint } from "../../../types/validators/xor" import { AdminPostCampaignsReq } from "../campaigns/validators" export class AdminGetPromotionsPromotionParams extends FindParams {} +export class AdminGetPromotionRules extends FindParams {} export class AdminGetPromotionsRuleValueParams extends extendedFindParamsMixin({ limit: 100, diff --git a/packages/types/src/promotion/http.ts b/packages/types/src/promotion/http.ts new file mode 100644 index 0000000000000..8942ade390b6a --- /dev/null +++ b/packages/types/src/promotion/http.ts @@ -0,0 +1,10 @@ +export type AdminGetPromotionRulesRes = { + id?: string + attribute: string + attribute_label: string + operator: string + operator_label: string + values: { label?: string; value: string }[] + disguised?: boolean + required?: boolean +}[] diff --git a/packages/types/src/promotion/index.ts b/packages/types/src/promotion/index.ts index dfae9af8a2f1f..897a509125f08 100644 --- a/packages/types/src/promotion/index.ts +++ b/packages/types/src/promotion/index.ts @@ -1,4 +1,5 @@ export * from "./common" +export * from "./http" export * from "./mutations" export * from "./service" export * from "./workflows" From 4b769af6e5fde2f5ba914bd0e5ea3d61a0f79c3a Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Sat, 6 Apr 2024 09:22:35 +0200 Subject: [PATCH 6/7] chore: add badge summary list --- .../public/locales/en-US/translation.json | 1 + .../badge-list-summary/badge-list-summary.tsx | 69 +++++++++++++++++++ .../common/badge-list-summary/index.ts | 1 + .../promotion-conditions-section.tsx | 19 +++-- 4 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/badge-list-summary/index.ts diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index b52124aabafa6..db30989b66d68 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -30,6 +30,7 @@ "items_one": "{{count}} item", "items_other": "{{count}} items", "countSelected": "{{count}} selected", + "plusCount": "+ {{count}}", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", "noRecordsFound": "No records found", diff --git a/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx new file mode 100644 index 0000000000000..09923af000a31 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx @@ -0,0 +1,69 @@ +import { Badge, Tooltip, clx } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +type BadgeListSummaryProps = { + /** + * Number of initial items to display + * @default 2 + */ + n?: number + /** + * List of strings to display as abbreviated list + */ + list: string[] + /** + * Is the summary displayed inline. + * Determines whether the center text is truncated if there is no space in the container + */ + inline?: boolean + + className?: string +} + +export const BadgeListSummary = ({ + list, + className, + inline, + n = 2, +}: BadgeListSummaryProps) => { + const { t } = useTranslation() + + const title = t("general.plusCount", { + count: list.length - n, + }) + + return ( +
+ {list.slice(0, n).map((item) => { + return {item} + })} + + {list.length > n && ( +
+ + {list.slice(n).map((c) => ( +
  • {c}
  • + ))} + + } + > + + {title} + +
    +
    + )} +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/badge-list-summary/index.ts b/packages/admin-next/dashboard/src/components/common/badge-list-summary/index.ts new file mode 100644 index 0000000000000..a156dab659661 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/badge-list-summary/index.ts @@ -0,0 +1 @@ +export * from "./badge-list-summary" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx index f39dc964fb12f..37c2c3d655ed0 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx @@ -4,6 +4,7 @@ import { Badge, Container, Heading } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" +import { BadgeListSummary } from "../../../../../components/common/badge-list-summary" import { NoRecords } from "../../../../../components/common/empty-table-content" type RuleProps = { @@ -15,24 +16,22 @@ function RuleBlock({ rule }: RuleProps) {
    {rule.attribute_label} - + {rule.operator_label} - - {rule.values?.map((v) => v.label).join(",")} - + v.label)} + />
    ) From 0576af6c46074f018d5b5c60fd7d985e23065c93 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Sat, 6 Apr 2024 12:33:33 +0200 Subject: [PATCH 7/7] chore: fix campaigns --- .../pricing/admin/rule-types.spec.ts | 2 +- .../badge-list-summary/badge-list-summary.tsx | 6 +- .../dashboard/src/hooks/api/campaigns.tsx | 96 +++++++++++++++++++ .../dashboard/src/hooks/api/promotions.tsx | 27 +++++- .../dashboard/src/lib/client/campaigns.ts | 35 +++++++ .../dashboard/src/lib/client/client.ts | 2 + .../dashboard/src/types/api-payloads.ts | 6 ++ .../dashboard/src/types/api-responses.ts | 6 ++ .../add-campaign-promotion-form.tsx | 2 +- .../promotion-add-campaign.tsx | 18 ++-- .../promotion-conditions-section.tsx | 2 +- .../edit-promotion-details-form.tsx | 3 +- 12 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 packages/admin-next/dashboard/src/hooks/api/campaigns.tsx create mode 100644 packages/admin-next/dashboard/src/lib/client/campaigns.ts diff --git a/integration-tests/modules/__tests__/pricing/admin/rule-types.spec.ts b/integration-tests/modules/__tests__/pricing/admin/rule-types.spec.ts index 190840cf0cdae..d33c11078dab9 100644 --- a/integration-tests/modules/__tests__/pricing/admin/rule-types.spec.ts +++ b/integration-tests/modules/__tests__/pricing/admin/rule-types.spec.ts @@ -194,7 +194,7 @@ medusaIntegrationTestRunner({ `/admin/pricing/rule-types/${ruleType.id}`, adminHeaders ) - console.log("response.data -- ", response.data) + expect(response.status).toEqual(200) expect(response.data).toEqual({ id: ruleType.id, diff --git a/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx index 09923af000a31..eb0887e21089f 100644 --- a/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx +++ b/packages/admin-next/dashboard/src/components/common/badge-list-summary/badge-list-summary.tsx @@ -44,7 +44,11 @@ export const BadgeListSummary = ({ )} > {list.slice(0, n).map((item) => { - return {item} + return ( + + {item} + + ) })} {list.length > n && ( diff --git a/packages/admin-next/dashboard/src/hooks/api/campaigns.tsx b/packages/admin-next/dashboard/src/hooks/api/campaigns.tsx new file mode 100644 index 0000000000000..53c20c3721ba4 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/api/campaigns.tsx @@ -0,0 +1,96 @@ +import { + QueryKey, + UseMutationOptions, + UseQueryOptions, + useMutation, + useQuery, +} from "@tanstack/react-query" +import { client } from "../../lib/client" +import { queryClient } from "../../lib/medusa" +import { queryKeysFactory } from "../../lib/query-key-factory" +import { CreateCampaignReq, UpdateCampaignReq } from "../../types/api-payloads" +import { + CampaignDeleteRes, + CampaignListRes, + CampaignRes, +} from "../../types/api-responses" + +const REGIONS_QUERY_KEY = "campaigns" as const +const campaignsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY) + +export const useCampaign = ( + id: string, + options?: Omit< + UseQueryOptions, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryKey: campaignsQueryKeys.detail(id), + queryFn: async () => client.campaigns.retrieve(id), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCampaigns = ( + query?: Record, + options?: Omit< + UseQueryOptions, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => client.campaigns.list(query), + queryKey: campaignsQueryKeys.list(query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCreateCampaign = ( + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (payload) => client.campaigns.create(payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateCampaign = ( + id: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (payload) => client.campaigns.update(id, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() }) + queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.detail(id) }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteCampaign = ( + id: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => client.campaigns.delete(id), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.lists() }) + queryClient.invalidateQueries({ queryKey: campaignsQueryKeys.detail(id) }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx index b70b2e57c140f..81b3820b4b64a 100644 --- a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx @@ -27,7 +27,21 @@ import { } from "../../types/api-responses" const PROMOTIONS_QUERY_KEY = "promotions" as const -export const promotionsQueryKeys = queryKeysFactory(PROMOTIONS_QUERY_KEY) +export const promotionsQueryKeys = { + ...queryKeysFactory(PROMOTIONS_QUERY_KEY), + listRules: (id: string, ruleType: string) => [ + PROMOTIONS_QUERY_KEY, + id, + ruleType, + ], + listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType], + listRuleValues: (ruleType: string, ruleValue: string) => [ + PROMOTIONS_QUERY_KEY, + ruleType, + ruleValue, + ], + listRuleOperators: () => [PROMOTIONS_QUERY_KEY], +} export const usePromotion = ( id: string, @@ -59,7 +73,7 @@ export const usePromotionRules = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.detail(id), + queryKey: promotionsQueryKeys.listRules(id, ruleType), queryFn: async () => client.promotions.listRules(id, ruleType), ...options, }) @@ -95,7 +109,7 @@ export const usePromotionRuleOperators = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.list(), + queryKey: promotionsQueryKeys.listRuleOperators(), queryFn: async () => client.promotions.listRuleOperators(), ...options, }) @@ -116,7 +130,7 @@ export const usePromotionRuleAttributes = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.list(), + queryKey: promotionsQueryKeys.listRuleAttributes(ruleType), queryFn: async () => client.promotions.listRuleAttributes(ruleType), ...options, }) @@ -138,7 +152,7 @@ export const usePromotionRuleValues = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: promotionsQueryKeys.list(), + queryKey: promotionsQueryKeys.listRuleValues(ruleType, ruleValue), queryFn: async () => client.promotions.listRuleValues(ruleType, ruleValue), ...options, }) @@ -252,6 +266,9 @@ export const usePromotionUpdateRules = ( client.promotions.updateRules(id, ruleType, payload), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: promotionsQueryKeys.listRules(id, ruleType), + }) queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.detail(id), }) diff --git a/packages/admin-next/dashboard/src/lib/client/campaigns.ts b/packages/admin-next/dashboard/src/lib/client/campaigns.ts new file mode 100644 index 0000000000000..eacb8aee0ab96 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/client/campaigns.ts @@ -0,0 +1,35 @@ +import { CreateCampaignDTO, UpdateCampaignDTO } from "@medusajs/types" +import { + CampaignDeleteRes, + CampaignListRes, + CampaignRes, +} from "../../types/api-responses" +import { deleteRequest, getRequest, postRequest } from "./common" + +async function retrieveCampaign(id: string, query?: Record) { + return getRequest(`/admin/campaigns/${id}`, query) +} + +async function listCampaigns(query?: Record) { + return getRequest(`/admin/campaigns`, query) +} + +async function createCampaign(payload: CreateCampaignDTO) { + return postRequest(`/admin/campaigns`, payload) +} + +async function updateCampaign(id: string, payload: UpdateCampaignDTO) { + return postRequest(`/admin/campaigns/${id}`, payload) +} + +async function deleteCampaign(id: string) { + return deleteRequest(`/admin/campaigns/${id}`) +} + +export const campaigns = { + retrieve: retrieveCampaign, + list: listCampaigns, + create: createCampaign, + update: updateCampaign, + delete: deleteCampaign, +} diff --git a/packages/admin-next/dashboard/src/lib/client/client.ts b/packages/admin-next/dashboard/src/lib/client/client.ts index b659d89e2a73d..37f0e73da012e 100644 --- a/packages/admin-next/dashboard/src/lib/client/client.ts +++ b/packages/admin-next/dashboard/src/lib/client/client.ts @@ -1,5 +1,6 @@ import { apiKeys } from "./api-keys" import { auth } from "./auth" +import { campaigns } from "./campaigns" import { collections } from "./collections" import { currencies } from "./currencies" import { customers } from "./customers" @@ -17,6 +18,7 @@ import { workflowExecutions } from "./workflow-executions" export const client = { auth: auth, apiKeys: apiKeys, + campaigns: campaigns, customers: customers, currencies: currencies, collections: collections, diff --git a/packages/admin-next/dashboard/src/types/api-payloads.ts b/packages/admin-next/dashboard/src/types/api-payloads.ts index 17c1b25c7adf5..7c42a99115fc2 100644 --- a/packages/admin-next/dashboard/src/types/api-payloads.ts +++ b/packages/admin-next/dashboard/src/types/api-payloads.ts @@ -4,6 +4,7 @@ import { CreateApiKeyDTO, + CreateCampaignDTO, CreateCustomerDTO, CreateInviteDTO, CreateProductCollectionDTO, @@ -13,6 +14,7 @@ import { CreateSalesChannelDTO, CreateStockLocationInput, UpdateApiKeyDTO, + UpdateCampaignDTO, UpdateCustomerDTO, UpdateProductCollectionDTO, UpdatePromotionDTO, @@ -68,3 +70,7 @@ export type UpdatePromotionReq = UpdatePromotionDTO export type BatchAddPromotionRulesReq = { rules: CreatePromotionRuleDTO[] } export type BatchRemovePromotionRulesReq = { rule_ids: string[] } export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] } + +// Campaign +export type CreateCampaignReq = CreateCampaignDTO +export type UpdateCampaignReq = UpdateCampaignDTO diff --git a/packages/admin-next/dashboard/src/types/api-responses.ts b/packages/admin-next/dashboard/src/types/api-responses.ts index 20095827938eb..ea07cbbff6672 100644 --- a/packages/admin-next/dashboard/src/types/api-responses.ts +++ b/packages/admin-next/dashboard/src/types/api-responses.ts @@ -4,6 +4,7 @@ import { ApiKeyDTO, + CampaignDTO, CurrencyDTO, CustomerDTO, InviteDTO, @@ -68,6 +69,11 @@ export type RegionRes = { region: RegionDTO } export type RegionListRes = { regions: RegionDTO[] } & ListRes export type RegionDeleteRes = DeleteRes +// Campaigns +export type CampaignRes = { campaign: CampaignDTO } +export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes +export type CampaignDeleteRes = DeleteRes + // API Keys export type ExtendedApiKeyDTO = ApiKeyDTO & { sales_channels: SalesChannelDTO[] | null 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 d741b4af9e6e6..a17bc289f0bd9 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 @@ -49,7 +49,7 @@ export const AddCampaignPromotionForm = ({ const handleSubmit = form.handleSubmit(async (data) => { await mutateAsync( - { id: promotion.id, campaign_id: data.campaign_id }, + { campaign_id: data.campaign_id }, { onSuccess: () => handleSuccess() } ) }) diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx index ce148061bfe26..7c283480e9d4b 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/promotion-add-campaign.tsx @@ -3,27 +3,23 @@ import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" import { RouteDrawer } from "../../../components/route-modal" +import { useCampaigns } from "../../../hooks/api/campaigns" import { usePromotion } from "../../../hooks/api/promotions" -import { useV2Campaigns } from "../../../lib/api-v2/campaign" import { AddCampaignPromotionForm } from "./components/add-campaign-promotion-form" export const PromotionAddCampaign = () => { const { id } = useParams() const { t } = useTranslation() - const { promotion, isLoading, isError, error } = usePromotion(id!) + const { promotion, isPending, isError, error } = usePromotion(id!) const { campaigns, - isLoading: areCampaignsLoading, + isPending: areCampaignsLoading, isError: isCampaignError, error: campaignError, - } = useV2Campaigns() + } = useCampaigns() - if (isError) { - throw error - } - - if (isCampaignError) { - throw campaignError + if (isError || isCampaignError) { + throw error || campaignError } return ( @@ -32,7 +28,7 @@ export const PromotionAddCampaign = () => { {t("promotions.addToCampaign.title")} - {!isLoading && !areCampaignsLoading && promotion && campaigns && ( + {!isPending && !areCampaignsLoading && promotion && campaigns && ( )} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx index 37c2c3d655ed0..f041c3fa5df5d 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-detail/components/promotion-conditions-section/promotion-conditions-section.tsx @@ -84,7 +84,7 @@ export const PromotionConditionsSection = ({ )} {rules.map((rule) => ( - + ))}
    diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx index 8778f05f80d89..7fbe9ec95c2bc 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx @@ -53,9 +53,10 @@ export const EditPromotionDetailsForm = ({ const { mutateAsync, isPending } = useUpdatePromotion(promotion.id) const handleSubmit = form.handleSubmit(async (data) => { + console.log("data ------ ", data) await mutateAsync( { - is_automatic: Boolean(data.is_automatic), + is_automatic: data.is_automatic === "true", code: data.code, application_method: { value: data.value,