fix(dashboard): Fixes to campaign and promotions domains (#9022)

This commit is contained in:
Kasper Fabricius Kristensen
2024-09-05 22:18:38 +02:00
committed by GitHub
parent c27aa46939
commit e5b90b2d97
29 changed files with 886 additions and 710 deletions

View File

@@ -1,3 +1,5 @@
import { Badge } from "@medusajs/ui"
type CellProps = {
code: string
}
@@ -9,10 +11,9 @@ type HeaderProps = {
export const CodeCell = ({ code }: CellProps) => {
return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
{/* // TODO: border color inversion*/}
<span className="bg-ui-tag-neutral-bg truncate rounded-md border border-neutral-200 p-1 text-xs">
<Badge size="2xsmall" className="truncate">
{code}
</span>
</Badge>
</div>
)
}

View File

@@ -1,13 +0,0 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
export const usePromotionTableFilters = () => {
const { t } = useTranslation()
let filters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at", type: "date" },
{ label: t("fields.updatedAt"), key: "updated_at", type: "date" },
]
return filters
}

View File

@@ -1694,8 +1694,15 @@
"title": "Edit buy rules"
}
},
"addToCampaign": {
"title": "Promotion's Campaign"
"campaign": {
"header": "Campaign",
"edit": {
"header": "Edit Campaign",
"successToast": "Successfully updated the campaign of the promotion."
},
"actions": {
"goToCampaign": "Go to campaign"
}
},
"campaign_currency": {
"tooltip": "This is the promotion's currency. Change it from the Details tab."
@@ -1800,6 +1807,14 @@
"header": "Edit Campaign",
"successToast": "Campaign '{{name}}' was successfully updated."
},
"configuration": {
"header": "Configuration",
"edit": {
"header": "Edit Campaign Configuration",
"description": "Edit the configuration of the campaign.",
"successToast": "Campaign configuration was successfully updated."
}
},
"create": {
"hint": "Create a promotional campaign.",
"header": "Create Campaign",

View File

@@ -1,4 +1,4 @@
import { PromotionDTO } from "@medusajs/types"
import { HttpTypes } from "@medusajs/types"
export enum PromotionStatus {
SCHEDULED = "SCHEDULED",
@@ -7,7 +7,7 @@ export enum PromotionStatus {
DISABLED = "DISABLED",
}
export const getPromotionStatus = (promotion: PromotionDTO) => {
export const getPromotionStatus = (promotion: HttpTypes.AdminPromotion) => {
const date = new Date()
const campaign = promotion.campaign

View File

@@ -355,6 +355,11 @@ export const RouteMap: RouteObject[] = [
path: "edit",
lazy: () => import("../../routes/campaigns/campaign-edit"),
},
{
path: "configuration",
lazy: () =>
import("../../routes/campaigns/campaign-configuration"),
},
{
path: "edit-budget",
lazy: () =>

View File

@@ -15,9 +15,9 @@ import { RouteFocusModal, useRouteModal } from "../../../../components/modals"
import { DataTable } from "../../../../components/table/data-table"
import { useAddOrRemoveCampaignPromotions } from "../../../../hooks/api/campaigns"
import { usePromotions } from "../../../../hooks/api/promotions"
import { usePromotionTableColumns } from "../../../../hooks/table/columns-v2/use-promotion-table-columns"
import { usePromotionTableColumns } from "../../../../hooks/table/columns/use-promotion-table-columns"
import { usePromotionTableFilters } from "../../../../hooks/table/filters/use-promotion-table-filters"
import { usePromotionTableQuery } from "../../../../hooks/table/query-v2/use-promotion-table-query"
import { usePromotionTableQuery } from "../../../../hooks/table/query/use-promotion-table-query"
import { useDataTable } from "../../../../hooks/use-data-table"
type AddCampaignPromotionsFormProps = {

View File

@@ -0,0 +1,33 @@
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/modals"
import { useCampaign } from "../../../hooks/api/campaigns"
import { CampaignConfigurationForm } from "./components/campaign-configuration-form"
export const CampaignConfiguration = () => {
const { t } = useTranslation()
const { id } = useParams()
const { campaign, isLoading, isError, error } = useCampaign(id!)
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<RouteDrawer.Title asChild>
<Heading>{t("campaigns.configuration.edit.header")}</Heading>
</RouteDrawer.Title>
<RouteDrawer.Description className="sr-only">
{t("campaigns.configuration.edit.description")}
</RouteDrawer.Description>
</RouteDrawer.Header>
{!isLoading && campaign && (
<CampaignConfigurationForm campaign={campaign} />
)}
</RouteDrawer>
)
}

View File

@@ -0,0 +1,133 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminCampaign } from "@medusajs/types"
import { Button, DatePicker, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
type CampaignConfigurationFormProps = {
campaign: AdminCampaign
}
const CampaignConfigurationSchema = z.object({
starts_at: z.date().nullable(),
ends_at: z.date().nullable(),
})
export const CampaignConfigurationForm = ({
campaign,
}: CampaignConfigurationFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof CampaignConfigurationSchema>>({
defaultValues: {
starts_at: campaign.starts_at ? new Date(campaign.starts_at) : undefined,
ends_at: campaign.ends_at ? new Date(campaign.ends_at) : undefined,
},
resolver: zodResolver(CampaignConfigurationSchema),
})
const { mutateAsync, isPending } = useUpdateCampaign(campaign.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
starts_at: data.starts_at || null,
ends_at: data.ends_at || null,
},
{
onSuccess: ({ campaign }) => {
toast.success(
t("campaigns.configuration.edit.successToast", {
name: campaign.name,
})
)
handleSuccess()
},
onError: (error) => {
toast.error(error.message)
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<RouteDrawer.Body>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="starts_at"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
<Form.Control>
<DatePicker
granularity="minute"
hourCycle={12}
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="ends_at"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
<Form.Control>
<DatePicker
granularity="minute"
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button
isLoading={isPending}
type="submit"
variant="primary"
size="small"
>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./campaign-configuration-form"

View File

@@ -0,0 +1 @@
export { CampaignConfiguration as Component } from "./campaign-configuration"

View File

@@ -78,8 +78,12 @@ export const CreateCampaignForm = () => {
return (
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit}>
<RouteFocusModal.Header>
<form onSubmit={handleSubmit} className="flex flex-col overflow-hidden">
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex size-full flex-col items-center overflow-auto py-16">
<CreateCampaignFormFields form={form} />
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
@@ -95,11 +99,7 @@ export const CreateCampaignForm = () => {
{t("actions.create")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center py-16">
<CreateCampaignFormFields form={form} />
</RouteFocusModal.Body>
</RouteFocusModal.Footer>
</form>
</RouteFocusModal.Form>
)

View File

@@ -1,6 +1,5 @@
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { useCampaign } from "../../../hooks/api/campaigns"
import { CampaignBudget } from "./components/campaign-budget"
import { CampaignGeneralSection } from "./components/campaign-general-section"
@@ -12,6 +11,9 @@ import after from "virtual:medusa/widgets/campaign/details/after"
import before from "virtual:medusa/widgets/campaign/details/before"
import sideAfter from "virtual:medusa/widgets/campaign/details/side/after"
import sideBefore from "virtual:medusa/widgets/campaign/details/side/before"
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
import { TwoColumnPage } from "../../../components/layout/pages"
import { CampaignConfigurationSection } from "./components/campaign-configuration-section"
export const CampaignDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -26,7 +28,14 @@ export const CampaignDetail = () => {
)
if (isLoading || !campaign) {
return <div>Loading...</div>
return (
<TwoColumnPageSkeleton
mainSections={2}
sidebarSections={3}
showJSON
showMetadata
/>
)
}
if (isError) {
@@ -34,54 +43,27 @@ export const CampaignDetail = () => {
}
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={campaign} />
</div>
)
})}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-3">
<CampaignGeneralSection campaign={campaign} />
<CampaignPromotionSection campaign={campaign} />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={campaign} />
</div>
)
})}
<div className="hidden xl:block">
<JsonViewSection data={campaign} />
</div>
</div>
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={campaign} />
</div>
)
})}
<CampaignSpend campaign={campaign} />
<CampaignBudget campaign={campaign} />
{sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={campaign} />
</div>
)
})}
<div className="xl:hidden">
<JsonViewSection data={campaign} />
</div>
</div>
</div>
<Outlet />
</div>
<TwoColumnPage
widgets={{
after,
before,
sideAfter,
sideBefore,
}}
hasOutlet
showJSON
showMetadata
data={campaign}
>
<TwoColumnPage.Main>
<CampaignGeneralSection campaign={campaign} />
<CampaignPromotionSection campaign={campaign} />
</TwoColumnPage.Main>
<TwoColumnPage.Sidebar>
<CampaignConfigurationSection campaign={campaign} />
<CampaignSpend campaign={campaign} />
<CampaignBudget campaign={campaign} />
</TwoColumnPage.Sidebar>
</TwoColumnPage>
)
}

View File

@@ -0,0 +1,43 @@
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { PencilSquare } from "@medusajs/icons"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DateRangeDisplay } from "../../../../../components/common/date-range-display"
type CampaignConfigurationSectionProps = {
campaign: HttpTypes.AdminCampaign
}
export const CampaignConfigurationSection = ({
campaign,
}: CampaignConfigurationSectionProps) => {
const { t } = useTranslation()
return (
<Container className="flex flex-col gap-y-4">
<div className="flex items-center justify-between">
<Heading level="h2">{t("campaigns.configuration.header")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: "configuration",
},
],
},
]}
/>
</div>
<DateRangeDisplay
startsAt={campaign.starts_at}
endsAt={campaign.ends_at}
showTime
/>
</Container>
)
}

View File

@@ -0,0 +1 @@
export * from "./campaign-configuration-section";

View File

@@ -12,7 +12,6 @@ import {
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { formatDate } from "../../../../../components/common/date"
import { useDeleteCampaign } from "../../../../../hooks/api/campaigns"
import { currencies } from "../../../../../lib/data/currencies"
import {
@@ -133,26 +132,6 @@ export const CampaignGeneralSection = ({
</div>
</div>
)}
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("campaigns.fields.start_date")}
</Text>
<Text size="small" leading="compact">
{campaign.starts_at ? formatDate(campaign.starts_at) : "-"}
</Text>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("campaigns.fields.end_date")}
</Text>
<Text size="small" leading="compact">
{campaign.ends_at ? formatDate(campaign.ends_at) : "-"}
</Text>
</div>
</Container>
)
}

View File

@@ -10,9 +10,9 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useAddOrRemoveCampaignPromotions } from "../../../../../hooks/api/campaigns"
import { usePromotions } from "../../../../../hooks/api/promotions"
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
import { usePromotionTableColumns } from "../../../../../hooks/table/columns/use-promotion-table-columns"
import { usePromotionTableFilters } from "../../../../../hooks/table/filters/use-promotion-table-filters"
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
import { usePromotionTableQuery } from "../../../../../hooks/table/query/use-promotion-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type CampaignPromotionSectionProps = {

View File

@@ -74,13 +74,10 @@ export const ProductCreateForm = ({
return {}
}
return regions.reduce(
(acc, reg) => {
acc[reg.id] = reg.currency_code
return acc
},
{} as Record<string, string>
)
return regions.reduce((acc, reg) => {
acc[reg.id] = reg.currency_code
return acc
}, {} as Record<string, string>)
}, [regions])
/**

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminCampaign, AdminPromotion } from "@medusajs/types"
import { Button, RadioGroup, Select, Text } from "@medusajs/ui"
import { Button, RadioGroup, Select, Text, toast } from "@medusajs/ui"
import { useEffect } from "react"
import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
@@ -44,7 +44,7 @@ export const AddCampaignPromotionFields = ({
const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId)
return (
<div className="flex h-full flex-col gap-y-8">
<div className="flex flex-col gap-y-8">
<Form.Field
control={form.control}
name="campaign_choice"
@@ -55,7 +55,7 @@ export const AddCampaignPromotionFields = ({
<Form.Control>
<RadioGroup
className="flex gap-y-3"
className="grid grid-cols-1 gap-3"
{...field}
value={field.value}
onValueChange={field.onChange}
@@ -154,6 +154,8 @@ export const AddCampaignPromotionForm = ({
const { handleSuccess } = useRouteModal()
const { campaign } = promotion
const originalId = campaign?.id
const form = useForm<zod.infer<typeof EditPromotionSchema>>({
defaultValues: {
campaign_id: campaign?.id,
@@ -162,11 +164,21 @@ export const AddCampaignPromotionForm = ({
resolver: zodResolver(EditPromotionSchema),
})
const { setValue } = form
const { mutateAsync, isPending } = useUpdatePromotion(promotion.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{ campaign_id: data.campaign_id },
{ onSuccess: () => handleSuccess() }
{
onSuccess: () => {
toast.success(t("promotions.campaign.edit.successToast"))
handleSuccess()
},
onError: (e) => {
toast.error(e.message)
},
}
)
})
@@ -177,18 +189,21 @@ export const AddCampaignPromotionForm = ({
useEffect(() => {
if (watchCampaignChoice === "none") {
form.setValue("campaign_id", null)
setValue("campaign_id", null)
}
if (watchCampaignChoice === "existing") {
form.setValue("campaign_id", campaign?.id)
setValue("campaign_id", originalId)
}
}, [watchCampaignChoice])
}, [watchCampaignChoice, setValue, originalId])
return (
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteDrawer.Body>
<form
onSubmit={handleSubmit}
className="flex size-full flex-col overflow-hidden"
>
<RouteDrawer.Body className="size-full overflow-auto">
<AddCampaignPromotionFields
form={form}
campaigns={campaigns}

View File

@@ -35,7 +35,7 @@ export const PromotionAddCampaign = () => {
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("promotions.addToCampaign.title")}</Heading>
<Heading>{t("promotions.campaign.edit.header")}</Heading>
</RouteDrawer.Header>
{!isPending && !areCampaignsLoading && promotion && campaigns && (

View File

@@ -326,17 +326,15 @@ export const CreatePromotionForm = () => {
return (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-scroll"
onSubmit={handleSubmit}
>
<form className="flex h-full flex-col" onSubmit={handleSubmit}>
<ProgressTabs
value={tab}
onValueChange={(tab) => handleTabChange(tab as Tab)}
className="flex h-full flex-col overflow-hidden"
>
<RouteFocusModal.Header>
<div className="flex w-full items-center justify-between gap-x-4">
<div className="-my-2 w-full max-w-[400px] border-l">
<div className="-my-2 w-full max-w-[600px] border-l">
<ProgressTabs.List className="grid w-full grid-cols-3">
<ProgressTabs.Trigger
className="w-full"
@@ -358,427 +356,114 @@ export const CreatePromotionForm = () => {
</ProgressTabs.Trigger>
</ProgressTabs.List>
</div>
<div className="flex items-center gap-x-2">
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
{tab === Tab.CAMPAIGN ? (
<Button
key="save-btn"
type="submit"
size="small"
isLoading={false}
>
{t("actions.save")}
</Button>
) : (
<Button
key="continue-btn"
type="button"
onClick={handleContinue}
size="small"
>
{t("actions.continue")}
</Button>
)}
</div>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="mx-auto my-20 w-[800px]">
<ProgressTabs.Content value={Tab.TYPE}>
<Form.Field
control={form.control}
name="template_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("promotions.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
key={"template_id"}
className="flex-col gap-y-3"
{...field}
onValueChange={field.onChange}
>
{templates.map((template) => {
return (
<RadioGroup.ChoiceBox
key={template.id}
value={template.id}
label={template.title}
description={template.description}
/>
)
})}
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</ProgressTabs.Content>
<RouteFocusModal.Body className="size-full overflow-hidden">
<ProgressTabs.Content
value={Tab.PROMOTION}
className="flex flex-1 flex-col gap-8"
value={Tab.TYPE}
className="size-full overflow-y-auto"
>
<Heading level="h1" className="text-fg-base">
{t(`promotions.sections.details`)}
{currentTemplate?.title && (
<Badge
className="ml-2 align-middle"
color="grey"
size="2xsmall"
rounded="full"
>
{currentTemplate?.title}
</Badge>
)}
</Heading>
{form.formState.errors.root && (
<Alert
variant="error"
dismissible={false}
className="text-balance"
>
{form.formState.errors.root.message}
</Alert>
)}
<Form.Field
control={form.control}
name="is_automatic"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>Method</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
value={field.value}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"false"}
label={t("promotions.form.method.code.title")}
description={t(
"promotions.form.method.code.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"true"}
label={t("promotions.form.method.automatic.title")}
description={t(
"promotions.form.method.automatic.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="flex gap-y-4">
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>
{t("promotions.form.code.title")}
</Form.Label>
<Form.Control>
<Input {...field} placeholder="SUMMER15" />
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey="promotions.form.code.description"
components={[<br key="break" />]}
/>
</Text>
</Form.Item>
)
}}
/>
</div>
{!currentTemplate?.hiddenFields?.includes("type") && (
<Form.Field
control={form.control}
name="type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("promotions.fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"standard"}
label={t("promotions.form.type.standard.title")}
description={t(
"promotions.form.type.standard.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"buyget"}
label={t("promotions.form.type.buyget.title")}
description={t(
"promotions.form.type.buyget.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<Divider />
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
{!currentTemplate?.hiddenFields?.includes(
"application_method.type"
) && (
<Form.Field
control={form.control}
name="application_method.type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.value_type")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"fixed"}
label={t(
"promotions.form.value_type.fixed.title"
)}
description={t(
"promotions.form.value_type.fixed.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"percentage"}
label={t(
"promotions.form.value_type.percentage.title"
)}
description={t(
"promotions.form.value_type.percentage.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<div className="flex gap-x-2 gap-y-4">
{!currentTemplate?.hiddenFields?.includes(
"application_method.value"
) && (
<div className="flex size-full flex-col items-center">
<div className="w-full max-w-[720px] py-16">
<Form.Field
control={form.control}
name="application_method.value"
render={({ field: { onChange, value, ...field } }) => {
const currencyCode =
form.getValues().application_method.currency_code
name="template_id"
render={({ field }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label
tooltip={
currencyCode || !isFixedValueType
? undefined
: t("promotions.fields.amount.tooltip")
}
>
{t("promotions.form.value.title")}
</Form.Label>
<Form.Item>
<Form.Label>{t("promotions.fields.type")}</Form.Label>
<Form.Control>
{isFixedValueType ? (
<CurrencyInput
{...field}
min={0}
onValueChange={(value) => {
onChange(value ? parseInt(value) : "")
}}
code={currencyCode}
symbol={
currencyCode
? getCurrencySymbol(currencyCode)
: ""
}
value={value}
disabled={!currencyCode}
/>
) : (
<DeprecatedPercentageInput
key="amount"
className="text-right"
min={0}
max={100}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
<RadioGroup
key={"template_id"}
className="flex-col gap-y-3"
{...field}
onValueChange={field.onChange}
>
{templates.map((template) => {
return (
<RadioGroup.ChoiceBox
key={template.id}
value={template.id}
label={template.title}
description={template.description}
/>
)
})}
</RadioGroup>
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey={
isFixedValueType
? "promotions.form.value_type.fixed.description"
: "promotions.form.value_type.percentage.description"
}
components={[<br key="break" />]}
/>
</Text>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{isTypeStandard && watchAllocation === "each" && (
<Form.Field
control={form.control}
name="application_method.max_quantity"
render={({ field }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>
{t("promotions.form.max_quantity.title")}
</Form.Label>
<Form.Control>
<Input
{...form.register(
"application_method.max_quantity",
{ valueAsNumber: true }
)}
type="number"
min={1}
placeholder="3"
/>
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey="promotions.form.max_quantity.description"
components={[<br key="break" />]}
/>
</Text>
</Form.Item>
)
}}
/>
)}
</div>
</div>
</ProgressTabs.Content>
<ProgressTabs.Content
value={Tab.PROMOTION}
className="size-full overflow-y-auto"
>
<div className="flex size-full flex-col items-center">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
<Heading level="h1" className="text-fg-base">
{t(`promotions.sections.details`)}
{currentTemplate?.title && (
<Badge
className="ml-2 align-middle"
color="grey"
size="2xsmall"
rounded="full"
>
{currentTemplate?.title}
</Badge>
)}
</Heading>
{form.formState.errors.root && (
<Alert
variant="error"
dismissible={false}
className="text-balance"
>
{form.formState.errors.root.message}
</Alert>
)}
{isTypeStandard &&
!currentTemplate?.hiddenFields?.includes(
"application_method.allocation"
) && (
<Form.Field
control={form.control}
name="application_method.allocation"
name="is_automatic"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.allocation")}
</Form.Label>
<Form.Label>Method</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
value={field.value}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"each"}
label={t(
"promotions.form.allocation.each.title"
)}
value={"false"}
label={t("promotions.form.method.code.title")}
description={t(
"promotions.form.allocation.each.description"
"promotions.form.method.code.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"across"}
value={"true"}
label={t(
"promotions.form.allocation.across.title"
"promotions.form.method.automatic.title"
)}
description={t(
"promotions.form.allocation.across.description"
"promotions.form.method.automatic.description"
)}
className={clx("basis-1/2")}
/>
@@ -789,41 +474,376 @@ export const CreatePromotionForm = () => {
)
}}
/>
)}
{!isTypeStandard && (
<>
<RulesFormField
form={form}
ruleType={"buy-rules"}
scope="application_method.buy_rules"
/>
</>
)}
<div className="flex gap-y-4">
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>
{t("promotions.form.code.title")}
</Form.Label>
<Form.Control>
<Input {...field} placeholder="SUMMER15" />
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey="promotions.form.code.description"
components={[<br key="break" />]}
/>
</Text>
</Form.Item>
)
}}
/>
</div>
{!currentTemplate?.hiddenFields?.includes("type") && (
<Form.Field
control={form.control}
name="type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.type")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"standard"}
label={t(
"promotions.form.type.standard.title"
)}
description={t(
"promotions.form.type.standard.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"buyget"}
label={t("promotions.form.type.buyget.title")}
description={t(
"promotions.form.type.buyget.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{!isTargetTypeOrder && (
<>
<Divider />
<RulesFormField
form={form}
ruleType={"target-rules"}
scope="application_method.target_rules"
/>
</>
)}
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
{!currentTemplate?.hiddenFields?.includes(
"application_method.type"
) && (
<Form.Field
control={form.control}
name="application_method.type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.value_type")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"fixed"}
label={t(
"promotions.form.value_type.fixed.title"
)}
description={t(
"promotions.form.value_type.fixed.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"percentage"}
label={t(
"promotions.form.value_type.percentage.title"
)}
description={t(
"promotions.form.value_type.percentage.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<div className="flex gap-x-2 gap-y-4">
{!currentTemplate?.hiddenFields?.includes(
"application_method.value"
) && (
<Form.Field
control={form.control}
name="application_method.value"
render={({ field: { onChange, value, ...field } }) => {
const currencyCode =
form.getValues().application_method.currency_code
return (
<Form.Item className="basis-1/2">
<Form.Label
tooltip={
currencyCode || !isFixedValueType
? undefined
: t("promotions.fields.amount.tooltip")
}
>
{t("promotions.form.value.title")}
</Form.Label>
<Form.Control>
{isFixedValueType ? (
<CurrencyInput
{...field}
min={0}
onValueChange={(value) => {
onChange(value ? parseInt(value) : "")
}}
code={currencyCode || "USD"}
symbol={
currencyCode
? getCurrencySymbol(currencyCode)
: "$"
}
value={value}
disabled={!currencyCode}
/>
) : (
<DeprecatedPercentageInput
key="amount"
className="text-right"
min={0}
max={100}
{...field}
value={value}
onChange={(e) => {
onChange(
e.target.value === ""
? null
: parseInt(e.target.value)
)
}}
/>
)}
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey={
isFixedValueType
? "promotions.form.value_type.fixed.description"
: "promotions.form.value_type.percentage.description"
}
components={[<br key="break" />]}
/>
</Text>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{isTypeStandard && watchAllocation === "each" && (
<Form.Field
control={form.control}
name="application_method.max_quantity"
render={({ field }) => {
return (
<Form.Item className="basis-1/2">
<Form.Label>
{t("promotions.form.max_quantity.title")}
</Form.Label>
<Form.Control>
<Input
{...form.register(
"application_method.max_quantity",
{ valueAsNumber: true }
)}
type="number"
min={1}
placeholder="3"
/>
</Form.Control>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Trans
t={t}
i18nKey="promotions.form.max_quantity.description"
components={[<br key="break" />]}
/>
</Text>
</Form.Item>
)
}}
/>
)}
</div>
{isTypeStandard &&
!currentTemplate?.hiddenFields?.includes(
"application_method.allocation"
) && (
<Form.Field
control={form.control}
name="application_method.allocation"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("promotions.fields.allocation")}
</Form.Label>
<Form.Control>
<RadioGroup
className="flex gap-y-3"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
value={"each"}
label={t(
"promotions.form.allocation.each.title"
)}
description={t(
"promotions.form.allocation.each.description"
)}
className={clx("basis-1/2")}
/>
<RadioGroup.ChoiceBox
value={"across"}
label={t(
"promotions.form.allocation.across.title"
)}
description={t(
"promotions.form.allocation.across.description"
)}
className={clx("basis-1/2")}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
{!isTypeStandard && (
<>
<RulesFormField
form={form}
ruleType={"buy-rules"}
scope="application_method.buy_rules"
/>
</>
)}
{!isTargetTypeOrder && (
<>
<Divider />
<RulesFormField
form={form}
ruleType={"target-rules"}
scope="application_method.target_rules"
/>
</>
)}
</div>
</div>
</ProgressTabs.Content>
<ProgressTabs.Content
value={Tab.CAMPAIGN}
className="flex flex-col items-center"
className="size-full overflow-auto"
>
<AddCampaignPromotionFields
form={form}
campaigns={campaigns || []}
/>
<div className="flex flex-col items-center">
<div className="flex w-full max-w-[720px] flex-col gap-y-8 py-16">
<AddCampaignPromotionFields
form={form}
campaigns={campaigns || []}
/>
</div>
</div>
</ProgressTabs.Content>
</RouteFocusModal.Body>
</ProgressTabs>
<RouteFocusModal.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
{tab === Tab.CAMPAIGN ? (
<Button
key="save-btn"
type="submit"
size="small"
isLoading={false}
>
{t("actions.save")}
</Button>
) : (
<Button
key="continue-btn"
type="button"
onClick={handleContinue}
size="small"
>
{t("actions.continue")}
</Button>
)}
</div>
</RouteFocusModal.Footer>
</form>
</RouteFocusModal.Form>
)

View File

@@ -2,5 +2,9 @@ import { RouteFocusModal } from "../../../components/modals"
import { CreatePromotionForm } from "./components/create-promotion-form/create-promotion-form"
export const PromotionCreate = () => {
return <RouteFocusModal>{<CreatePromotionForm />}</RouteFocusModal>
return (
<RouteFocusModal>
<CreatePromotionForm />
</RouteFocusModal>
)
}

View File

@@ -1,97 +1,73 @@
import { PencilSquare } from "@medusajs/icons"
import { CampaignDTO } from "@medusajs/types"
import { ArrowUpRightOnBox, PencilSquare } from "@medusajs/icons"
import { HttpTypes } 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 { useParams } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DateRangeDisplay } from "../../../../../components/common/date-range-display"
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()
const CampaignDetailSection = ({
campaign,
}: {
campaign: HttpTypes.AdminCampaign
}) => {
return (
<Fragment>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("campaigns.fields.name")}
<div className="flex flex-col gap-y-3">
<div className="text-ui-fg-muted flex items-center gap-x-1.5">
<Text size="small" weight="plus" className="text-ui-fg-base">
{campaign.name}
</Text>
<div className="flex items-center gap-1">
<Text size="small" leading="compact" className="text-pretty">
{campaign?.name}
</Text>
</div>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("campaigns.fields.identifier")}
<Text size="small" weight="plus">
·
</Text>
<div className="flex items-center gap-1">
<Text size="small" leading="compact" className="text-pretty">
{campaign?.campaign_identifier}
</Text>
</div>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("campaigns.fields.start_date")}
<Text size="small" weight="plus">
{campaign.campaign_identifier}
</Text>
<div className="flex items-center gap-1">
<Text size="small" leading="compact" className="text-pretty">
{formatDate(campaign?.starts_at)}
</Text>
</div>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" weight="plus" leading="compact">
{t("campaigns.fields.end_date") || "-"}
</Text>
<div className="flex items-center gap-1">
<Text size="small" leading="compact" className="text-pretty">
{formatDate(campaign?.ends_at)}
</Text>
</div>
</div>
</Fragment>
<DateRangeDisplay
startsAt={campaign.starts_at}
endsAt={campaign.ends_at}
showTime
/>
</div>
)
}
export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
export const CampaignSection = ({
campaign,
}: {
campaign: HttpTypes.AdminCampaign | null
}) => {
const { t } = useTranslation()
const { id } = useParams()
const actions = [
{
label: t("actions.edit"),
to: "add-to-campaign",
icon: <PencilSquare />,
},
]
if (campaign) {
actions.unshift({
label: t("promotions.campaign.actions.goToCampaign"),
to: `/campaigns/${campaign.id}`,
icon: <ArrowUpRightOnBox />,
})
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Container>
<div className="flex items-center justify-between">
<Heading level="h2">{t("promotions.fields.campaign")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: "add-to-campaign",
icon: <PencilSquare />,
},
],
actions,
},
]}
/>
@@ -101,7 +77,7 @@ export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
<CampaignDetailSection campaign={campaign} />
) : (
<NoRecords
className="h-[180px] p-6 text-center"
className="h-[180px] pt-4 text-center"
title="Not part of a campaign"
message="Add this promotion to an existing campaign"
action={{

View File

@@ -1,5 +1,5 @@
import { PencilSquare } from "@medusajs/icons"
import { AdminGetPromotionRulesRes, PromotionRuleTypes } from "@medusajs/types"
import { HttpTypes, PromotionRuleTypes } from "@medusajs/types"
import { Badge, Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
@@ -8,7 +8,7 @@ import { BadgeListSummary } from "../../../../../components/common/badge-list-su
import { NoRecords } from "../../../../../components/common/empty-table-content"
type RuleProps = {
rule: AdminGetPromotionRulesRes
rule: HttpTypes.AdminPromotionRule
}
function RuleBlock({ rule }: RuleProps) {
@@ -42,7 +42,7 @@ function RuleBlock({ rule }: RuleProps) {
}
type PromotionConditionsSectionProps = {
rules: AdminGetPromotionRulesRes
rules: HttpTypes.AdminPromotionRule[]
ruleType: PromotionRuleTypes
}

View File

@@ -1,5 +1,5 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { PromotionDTO } from "@medusajs/types"
import { HttpTypes } from "@medusajs/types"
import {
Badge,
Container,
@@ -14,13 +14,37 @@ import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeletePromotion } from "../../../../../hooks/api/promotions"
import { formatCurrency } from "../../../../../lib/format-currency"
import { formatPercentage } from "../../../../../lib/percentage-helpers"
import {
getPromotionStatus,
PromotionStatus,
} from "../../../../../lib/promotions"
type PromotionGeneralSectionProps = {
promotion: PromotionDTO
promotion: HttpTypes.AdminPromotion
}
function getDisplayValue(promotion: HttpTypes.AdminPromotion) {
const value = promotion.application_method?.value
if (!value) {
return null
}
if (promotion.application_method?.type === "fixed") {
const currency = promotion.application_method?.currency_code
if (!currency) {
return null
}
return formatCurrency(value, currency)
} else if (promotion.application_method?.type === "percentage") {
return formatPercentage(value)
}
return null
}
export const PromotionGeneralSection = ({
@@ -64,6 +88,8 @@ export const PromotionGeneralSection = ({
string
]
const displayValue = getDisplayValue(promotion)
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
@@ -113,18 +139,15 @@ export const PromotionGeneralSection = ({
{t("fields.code")}
</Text>
<div className="flex items-center gap-1">
<Text
size="small"
weight="plus"
leading="compact"
className="text-pretty"
<Copy content={promotion.code!} asChild>
<Badge
size="2xsmall"
rounded="full"
className="cursor-pointer text-pretty"
>
{promotion.code}
</Text>
<Copy content={promotion.code!} variant="mini" />
</div>
</Badge>
</Copy>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
@@ -142,17 +165,16 @@ export const PromotionGeneralSection = ({
{t("promotions.fields.value")}
</Text>
<Text size="small" leading="compact" className="text-pretty">
<Text className="inline pr-3" size="small" leading="compact">
{promotion.application_method?.value}
<div className="flex items-center gap-x-2">
<Text className="inline" size="small" leading="compact">
{displayValue || "-"}
</Text>
{promotion?.application_method?.type === "fixed" && (
<Badge size="xsmall">
{promotion?.application_method?.currency_code}
<Badge size="2xsmall" rounded="full">
{promotion?.application_method?.currency_code?.toUpperCase()}
</Badge>
)}
</Text>
</div>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">

View File

@@ -1,6 +1,7 @@
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
import { TwoColumnPage } from "../../../components/layout/pages"
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
import { CampaignSection } from "./components/campaign-section"
import { PromotionConditionsSection } from "./components/promotion-conditions-section"
@@ -9,6 +10,8 @@ import { promotionLoader } from "./loader"
import after from "virtual:medusa/widgets/promotion/details/after"
import before from "virtual:medusa/widgets/promotion/details/before"
import sideAfter from "virtual:medusa/widgets/promotion/details/side/after"
import sideBefore from "virtual:medusa/widgets/promotion/details/side/before"
export const PromotionDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -28,51 +31,40 @@ export const PromotionDetail = () => {
const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query)
if (isLoading || !promotion) {
return <div>Loading...</div>
return (
<TwoColumnPageSkeleton mainSections={3} sidebarSections={1} showJSON />
)
}
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={promotion} />
</div>
)
})}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-3">
<PromotionGeneralSection promotion={promotion} />
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
<TwoColumnPage
data={promotion}
widgets={{
after,
before,
sideAfter,
sideBefore,
}}
hasOutlet
showJSON
>
<TwoColumnPage.Main>
<PromotionGeneralSection promotion={promotion} />
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
<PromotionConditionsSection
rules={targetRules || []}
ruleType={"target-rules"}
/>
{promotion.type === "buyget" && (
<PromotionConditionsSection
rules={targetRules || []}
ruleType={"target-rules"}
rules={buyRules || []}
ruleType={"buy-rules"}
/>
{promotion.type === "buyget" && (
<PromotionConditionsSection
rules={buyRules || []}
ruleType={"buy-rules"}
/>
)}
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={promotion} />
</div>
)
})}
<div className="hidden xl:block">
<JsonViewSection data={promotion} />
</div>
</div>
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
<CampaignSection campaign={promotion.campaign!} />
<div className="xl:hidden">
<JsonViewSection data={promotion} />
</div>
</div>
</div>
<Outlet />
</div>
)}
</TwoColumnPage.Main>
<TwoColumnPage.Sidebar>
<CampaignSection campaign={promotion.campaign!} />
</TwoColumnPage.Sidebar>
</TwoColumnPage>
)
}

View File

@@ -1,23 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { PromotionDTO } from "@medusajs/types"
import {
Button,
clx,
CurrencyInput,
Input,
RadioGroup,
Text,
} from "@medusajs/ui"
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 { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/modals"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
@@ -80,9 +70,12 @@ export const EditPromotionDetailsForm = ({
return (
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteDrawer.Body>
<div className="flex h-full flex-col gap-y-8">
<form
onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden"
>
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
<div className="flex flex-col gap-y-8">
<Form.Field
control={form.control}
name="is_automatic"
@@ -98,10 +91,6 @@ export const EditPromotionDetailsForm = ({
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"false" === field.value,
})}
value={"false"}
label={t("promotions.form.method.code.title")}
description={t(
@@ -109,10 +98,6 @@ export const EditPromotionDetailsForm = ({
)}
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"true" === field.value,
})}
value={"true"}
label={t("promotions.form.method.automatic.title")}
description={t(
@@ -171,10 +156,6 @@ export const EditPromotionDetailsForm = ({
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"fixed" === field.value,
})}
value={"fixed"}
label={t("promotions.form.value_type.fixed.title")}
description={t(
@@ -183,10 +164,6 @@ export const EditPromotionDetailsForm = ({
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"percentage" === field.value,
})}
value={"percentage"}
label={t(
"promotions.form.value_type.percentage.title"
@@ -263,10 +240,6 @@ export const EditPromotionDetailsForm = ({
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"each" === field.value,
})}
value={"each"}
label={t("promotions.form.allocation.each.title")}
description={t(
@@ -275,10 +248,6 @@ export const EditPromotionDetailsForm = ({
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-ui-border-interactive border-2":
"across" === field.value,
})}
value={"across"}
label={t("promotions.form.allocation.across.title")}
description={t(

View File

@@ -13,9 +13,9 @@ import {
useDeletePromotion,
usePromotions,
} from "../../../../../hooks/api/promotions"
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters"
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
import { usePromotionTableColumns } from "../../../../../hooks/table/columns/use-promotion-table-columns"
import { usePromotionTableFilters } from "../../../../../hooks/table/filters/use-promotion-table-filters"
import { usePromotionTableQuery } from "../../../../../hooks/table/query/use-promotion-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { promotionsLoader } from "../../loader"