feat(dashboard): added details page for promotions + edit sliders (#6882)
* chore: added details page for promotions * chore: add edit rules, edit details and edit campaign pages * chore: change to type button * chore: connection for rules * chore: listing rule labels of multiple modules * chore: add badge summary list * chore: fix campaigns
This commit is contained in:
1 parent
07fb058d96
commit
5e30b8cce6
60 files changed
+2803
-153
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"medusa-react": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(dashboard): added details page for promotions
|
||||
@@ -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,
|
||||
|
||||
@@ -140,6 +140,15 @@
|
||||
},
|
||||
"required": ["domain"]
|
||||
},
|
||||
"campaigns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
},
|
||||
"giftCards": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -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",
|
||||
@@ -611,12 +612,116 @@
|
||||
"domain": "Promotions",
|
||||
"fields": {
|
||||
"method": "Method",
|
||||
"campaign": "Campaign"
|
||||
"type": "Type",
|
||||
"value_type": "Value 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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"domain": "Pricing",
|
||||
"deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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 (
|
||||
<div
|
||||
className={clx(
|
||||
"ml-2 text-ui-fg-subtle txt-compact-small gap-x-2 overflow-hidden",
|
||||
{
|
||||
"inline-flex": inline,
|
||||
flex: !inline,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{list.slice(0, n).map((item) => {
|
||||
return (
|
||||
<Badge key={item} size="2xsmall">
|
||||
{item}
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
|
||||
{list.length > n && (
|
||||
<div className="whitespace-nowrap">
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{list.slice(n).map((c) => (
|
||||
<li key={c}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<Badge size="2xsmall" className="cursor-default whitespace-nowrap">
|
||||
{title}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./badge-list-summary"
|
||||
+34
-11
@@ -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 && (
|
||||
<Link to={action.to}>
|
||||
<Button variant="secondary" size="small">
|
||||
{action.label}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
const TransparentIconLeftButton = ({ action }: ActionProps) =>
|
||||
action && (
|
||||
<Link to={action.to}>
|
||||
<Button variant="transparent" className="text-ui-fg-interactive">
|
||||
<PlusMini /> {action.label}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export const NoRecords = ({
|
||||
title,
|
||||
message,
|
||||
action,
|
||||
className,
|
||||
buttonVariant = "default",
|
||||
}: NoRecordsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -59,19 +82,19 @@ export const NoRecords = ({
|
||||
>
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<ExclamationCircle />
|
||||
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title ?? t("general.noRecordsTitle")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" className="text-ui-fg-muted">
|
||||
{message ?? t("general.noRecordsMessage")}
|
||||
</Text>
|
||||
</div>
|
||||
{action && (
|
||||
<Link to={action.to}>
|
||||
<Button variant="secondary" size="small">
|
||||
{action.label}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{buttonVariant === "default" && <DefaultButton action={action} />}
|
||||
{buttonVariant === "transparentIconLeft" && (
|
||||
<TransparentIconLeftButton action={action} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<CampaignRes, Error, CampaignRes, QueryKey>,
|
||||
"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<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<CampaignListRes, Error, CampaignListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.campaigns.list(query),
|
||||
queryKey: campaignsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateCampaign = (
|
||||
options?: UseMutationOptions<CampaignRes, Error, CreateCampaignReq>
|
||||
) => {
|
||||
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<CampaignRes, Error, UpdateCampaignReq>
|
||||
) => {
|
||||
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<CampaignDeleteRes, Error, void>
|
||||
) => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,47 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
||||
import {
|
||||
BatchAddPromotionRulesReq,
|
||||
BatchRemovePromotionRulesReq,
|
||||
BatchUpdatePromotionRulesReq,
|
||||
CreatePromotionReq,
|
||||
UpdatePromotionReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
PromotionDeleteRes,
|
||||
PromotionListRes,
|
||||
PromotionRes,
|
||||
PromotionRuleAttributesListRes,
|
||||
PromotionRuleOperatorsListRes,
|
||||
PromotionRulesListRes,
|
||||
PromotionRuleValuesListRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const PROMOTIONS_QUERY_KEY = "promotions" as const
|
||||
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,
|
||||
@@ -24,6 +59,28 @@ export const usePromotion = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRules = (
|
||||
id: string,
|
||||
ruleType: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
PromotionRulesListRes,
|
||||
Error,
|
||||
PromotionRulesListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRules(id, ruleType),
|
||||
queryFn: async () => client.promotions.listRules(id, ruleType),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotions = (
|
||||
query?: AdminGetPromotionsParams,
|
||||
options?: Omit<
|
||||
@@ -39,3 +96,185 @@ export const usePromotions = (
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRuleOperators = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
PromotionListRes,
|
||||
Error,
|
||||
PromotionRuleOperatorsListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRuleOperators(),
|
||||
queryFn: async () => client.promotions.listRuleOperators(),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRuleAttributes = (
|
||||
ruleType: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
PromotionListRes,
|
||||
Error,
|
||||
PromotionRuleAttributesListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRuleAttributes(ruleType),
|
||||
queryFn: async () => client.promotions.listRuleAttributes(ruleType),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRuleValues = (
|
||||
ruleType: string,
|
||||
ruleValue: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
PromotionListRes,
|
||||
Error,
|
||||
PromotionRuleValuesListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRuleValues(ruleType, ruleValue),
|
||||
queryFn: async () => client.promotions.listRuleValues(ruleType, ruleValue),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useDeletePromotion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<PromotionDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.promotions.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreatePromotion = (
|
||||
options?: UseMutationOptions<PromotionRes, Error, CreatePromotionReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.promotions.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdatePromotion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<PromotionRes, Error, UpdatePromotionReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.promotions.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePromotionAddRules = (
|
||||
id: string,
|
||||
ruleType: string,
|
||||
options?: UseMutationOptions<PromotionRes, Error, BatchAddPromotionRulesReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.promotions.addRules(id, ruleType, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePromotionRemoveRules = (
|
||||
id: string,
|
||||
ruleType: string,
|
||||
options?: UseMutationOptions<
|
||||
PromotionRes,
|
||||
Error,
|
||||
BatchRemovePromotionRulesReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
client.promotions.removeRules(id, ruleType, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePromotionUpdateRules = (
|
||||
id: string,
|
||||
ruleType: string,
|
||||
options?: UseMutationOptions<
|
||||
PromotionRes,
|
||||
Error,
|
||||
BatchUpdatePromotionRulesReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
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),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
AdminCampaignRes,
|
||||
AdminCampaignsListRes,
|
||||
AdminGetCampaignsCampaignParams,
|
||||
AdminGetCampaignsParams,
|
||||
AdminPostCampaignsCampaignReq,
|
||||
} from "@medusajs/medusa"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { queryKeysFactory, useAdminCustomQuery } from "medusa-react"
|
||||
import { medusa } from "../medusa"
|
||||
|
||||
const QUERY_KEY = "admin_campaigns"
|
||||
export const adminCampaignKeys = queryKeysFactory<
|
||||
typeof QUERY_KEY,
|
||||
AdminGetCampaignsParams
|
||||
>(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)
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./auth"
|
||||
export * from "./promotion"
|
||||
export * from "./store"
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
AdminGetPromotionsParams,
|
||||
AdminPromotionsListRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import {
|
||||
UseQueryOptionsWrapper,
|
||||
queryKeysFactory,
|
||||
useAdminCustomQuery,
|
||||
} from "medusa-react"
|
||||
|
||||
const ADMIN_PROMOTIONS_QUERY_KEY = "admin_promotions"
|
||||
|
||||
export const adminPromotionKeys = queryKeysFactory<
|
||||
typeof ADMIN_PROMOTIONS_QUERY_KEY,
|
||||
AdminGetPromotionsParams
|
||||
>(ADMIN_PROMOTIONS_QUERY_KEY)
|
||||
|
||||
type PromotionQueryKey = typeof adminPromotionKeys
|
||||
|
||||
export const useV2Promotions = (
|
||||
query?: AdminGetPromotionsParams,
|
||||
options?: UseQueryOptionsWrapper<
|
||||
Response<AdminPromotionsListRes>,
|
||||
Error,
|
||||
ReturnType<PromotionQueryKey["list"]>
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useAdminCustomQuery<
|
||||
AdminGetPromotionsParams,
|
||||
AdminPromotionsListRes
|
||||
>("/promotions", adminPromotionKeys.list(query), query, options as any)
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -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<string, any>) {
|
||||
return getRequest<CampaignRes>(`/admin/campaigns/${id}`, query)
|
||||
}
|
||||
|
||||
async function listCampaigns(query?: Record<string, any>) {
|
||||
return getRequest<CampaignListRes>(`/admin/campaigns`, query)
|
||||
}
|
||||
|
||||
async function createCampaign(payload: CreateCampaignDTO) {
|
||||
return postRequest<CampaignRes>(`/admin/campaigns`, payload)
|
||||
}
|
||||
|
||||
async function updateCampaign(id: string, payload: UpdateCampaignDTO) {
|
||||
return postRequest<CampaignRes>(`/admin/campaigns/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteCampaign(id: string) {
|
||||
return deleteRequest<CampaignDeleteRes>(`/admin/campaigns/${id}`)
|
||||
}
|
||||
|
||||
export const campaigns = {
|
||||
retrieve: retrieveCampaign,
|
||||
list: listCampaigns,
|
||||
create: createCampaign,
|
||||
update: updateCampaign,
|
||||
delete: deleteCampaign,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiKeys } from "./api-keys"
|
||||
import { auth } from "./auth"
|
||||
import { campaigns } from "./campaigns"
|
||||
import { categories } from "./categories"
|
||||
import { collections } from "./collections"
|
||||
import { currencies } from "./currencies"
|
||||
@@ -19,6 +20,7 @@ import { workflowExecutions } from "./workflow-executions"
|
||||
export const client = {
|
||||
auth: auth,
|
||||
apiKeys: apiKeys,
|
||||
campaigns: campaigns,
|
||||
categories: categories,
|
||||
customers: customers,
|
||||
currencies: currencies,
|
||||
|
||||
@@ -1,23 +1,113 @@
|
||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||
|
||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
import {
|
||||
BatchAddPromotionRulesReq,
|
||||
BatchRemovePromotionRulesReq,
|
||||
BatchUpdatePromotionRulesReq,
|
||||
CreatePromotionReq,
|
||||
UpdatePromotionReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
PromotionDeleteRes,
|
||||
PromotionListRes,
|
||||
PromotionRes,
|
||||
PromotionRuleAttributesListRes,
|
||||
PromotionRuleOperatorsListRes,
|
||||
PromotionRuleValuesListRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
const retrievePromotion = async (
|
||||
id: string,
|
||||
query?: AdminGetPromotionsParams
|
||||
) => {
|
||||
async function retrievePromotion(id: string, query?: AdminGetPromotionsParams) {
|
||||
return getRequest<PromotionRes, AdminGetPromotionsParams>(
|
||||
`/admin/promotions/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
const listPromotions = async (query?: AdminGetPromotionsParams) => {
|
||||
async function listPromotions(query?: AdminGetPromotionsParams) {
|
||||
return getRequest<PromotionListRes>(`/admin/promotions`, query)
|
||||
}
|
||||
|
||||
async function deletePromotion(id: string) {
|
||||
return deleteRequest<PromotionDeleteRes>(`/admin/promotions/${id}`)
|
||||
}
|
||||
|
||||
async function updatePromotion(id: string, payload: UpdatePromotionReq) {
|
||||
return postRequest<PromotionRes>(`/admin/promotions/${id}`, payload)
|
||||
}
|
||||
|
||||
async function createPromotion(payload: CreatePromotionReq) {
|
||||
return postRequest<PromotionRes>(`/admin/promotions`, payload)
|
||||
}
|
||||
|
||||
async function addPromotionRules(
|
||||
id: string,
|
||||
ruleType: string,
|
||||
payload: BatchAddPromotionRulesReq
|
||||
) {
|
||||
return postRequest<PromotionRes>(
|
||||
`/admin/promotions/${id}/${ruleType}/batch/add`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function updatePromotionRules(
|
||||
id: string,
|
||||
ruleType: string,
|
||||
payload: BatchUpdatePromotionRulesReq
|
||||
) {
|
||||
return postRequest<PromotionRes>(
|
||||
`/admin/promotions/${id}/${ruleType}/batch/update`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function removePromotionRules(
|
||||
id: string,
|
||||
ruleType: string,
|
||||
payload: BatchRemovePromotionRulesReq
|
||||
) {
|
||||
return postRequest<PromotionRes>(
|
||||
`/admin/promotions/${id}/${ruleType}/batch/remove`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function listPromotionRules(id: string, ruleType: string) {
|
||||
return getRequest<PromotionRuleAttributesListRes>(
|
||||
`/admin/promotions/${id}/${ruleType}`
|
||||
)
|
||||
}
|
||||
|
||||
async function listPromotionRuleAttributes(ruleType: string) {
|
||||
return getRequest<PromotionRuleAttributesListRes>(
|
||||
`/admin/promotions/rule-attribute-options/${ruleType}`
|
||||
)
|
||||
}
|
||||
|
||||
async function listPromotionRuleValues(ruleType: string, ruleValue: string) {
|
||||
return getRequest<PromotionRuleValuesListRes>(
|
||||
`/admin/promotions/rule-value-options/${ruleType}/${ruleValue}`
|
||||
)
|
||||
}
|
||||
|
||||
async function listPromotionRuleOperators() {
|
||||
return getRequest<PromotionRuleOperatorsListRes>(
|
||||
`/admin/promotions/rule-operator-options`
|
||||
)
|
||||
}
|
||||
|
||||
export const promotions = {
|
||||
retrieve: retrievePromotion,
|
||||
list: listPromotions,
|
||||
delete: deletePromotion,
|
||||
update: updatePromotion,
|
||||
create: createPromotion,
|
||||
addRules: addPromotionRules,
|
||||
removeRules: removePromotionRules,
|
||||
updateRules: updatePromotionRules,
|
||||
listRules: listPromotionRules,
|
||||
listRuleAttributes: listPromotionRuleAttributes,
|
||||
listRuleOperators: listPromotionRuleOperators,
|
||||
listRuleValues: listPromotionRuleValues,
|
||||
}
|
||||
@@ -150,6 +150,34 @@ 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,
|
||||
},
|
||||
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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
|
||||
import {
|
||||
CreateApiKeyDTO,
|
||||
CreateCampaignDTO,
|
||||
CreateCustomerDTO,
|
||||
CreateInviteDTO,
|
||||
CreateProductCollectionDTO,
|
||||
CreatePromotionDTO,
|
||||
CreatePromotionRuleDTO,
|
||||
CreateRegionDTO,
|
||||
CreateSalesChannelDTO,
|
||||
CreateStockLocationInput,
|
||||
UpdateApiKeyDTO,
|
||||
UpdateCampaignDTO,
|
||||
UpdateCustomerDTO,
|
||||
UpdateProductCollectionDTO,
|
||||
UpdatePromotionDTO,
|
||||
UpdatePromotionRuleDTO,
|
||||
UpdateRegionDTO,
|
||||
UpdateSalesChannelDTO,
|
||||
UpdateStockLocationInput,
|
||||
@@ -57,3 +63,14 @@ export type UpdateStockLocationReq = UpdateStockLocationInput
|
||||
// Product Collections
|
||||
export type CreateProductCollectionReq = CreateProductCollectionDTO
|
||||
export type UpdateProductCollectionReq = UpdateProductCollectionDTO
|
||||
|
||||
// Promotion
|
||||
export type CreatePromotionReq = CreatePromotionDTO
|
||||
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
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import {
|
||||
ApiKeyDTO,
|
||||
CampaignDTO,
|
||||
CurrencyDTO,
|
||||
CustomerDTO,
|
||||
InviteDTO,
|
||||
@@ -45,6 +46,10 @@ export type CustomerListRes = { customers: CustomerDTO[] } & ListRes
|
||||
// Promotions
|
||||
export type PromotionRes = { promotion: PromotionDTO }
|
||||
export type PromotionListRes = { promotions: PromotionDTO[] } & ListRes
|
||||
export type PromotionRuleAttributesListRes = { attributes: Record<any, any>[] }
|
||||
export type PromotionRuleOperatorsListRes = { operators: Record<any, any>[] }
|
||||
export type PromotionRuleValuesListRes = { values: Record<any, any>[] }
|
||||
export type PromotionRulesListRes = { rules: Record<any, any>[] }
|
||||
export type PromotionDeleteRes = DeleteRes
|
||||
|
||||
// Users
|
||||
@@ -65,6 +70,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
|
||||
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
||||
import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui"
|
||||
import { Fragment, useState } from "react"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import {
|
||||
usePromotionAddRules,
|
||||
usePromotionRemoveRules,
|
||||
usePromotionRuleValues,
|
||||
usePromotionUpdateRules,
|
||||
useUpdatePromotion,
|
||||
} from "../../../../../hooks/api/promotions"
|
||||
import { RuleTypeValues } from "../../edit-rules"
|
||||
import { getDisguisedRules } from "./utils"
|
||||
|
||||
type EditPromotionFormProps = {
|
||||
promotion: PromotionDTO
|
||||
rules: PromotionRuleDTO[]
|
||||
ruleType: RuleTypeValues
|
||||
attributes: any[]
|
||||
operators: any[]
|
||||
}
|
||||
|
||||
const EditRules = zod.object({
|
||||
rules: zod.array(
|
||||
zod.object({
|
||||
id: zod.string().optional(),
|
||||
attribute: zod.string().min(1, { message: "Required field" }),
|
||||
operator: zod.string().min(1, { message: "Required field" }),
|
||||
values: zod.union([
|
||||
zod.number().min(1, { message: "Required field" }),
|
||||
zod.string().min(1, { message: "Required field" }),
|
||||
zod.array(zod.string()).min(1, { message: "Required field" }),
|
||||
]),
|
||||
required: zod.boolean().optional(),
|
||||
field_type: zod.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const fetchOptionsForRule = (ruleType: string, attribute: string) => {
|
||||
const { values } = usePromotionRuleValues(ruleType, attribute)
|
||||
|
||||
return values || []
|
||||
}
|
||||
|
||||
export const EditRulesForm = ({
|
||||
promotion,
|
||||
rules,
|
||||
ruleType,
|
||||
attributes,
|
||||
operators,
|
||||
}: EditPromotionFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const requiredAttributes = attributes?.filter((ra) => ra.required) || []
|
||||
const requiredAttributeValues = requiredAttributes?.map((ra) => ra.value)
|
||||
const disguisedRules =
|
||||
getDisguisedRules(promotion, requiredAttributes, ruleType) || []
|
||||
const [rulesToRemove, setRulesToRemove] = useState([])
|
||||
|
||||
const form = useForm<zod.infer<typeof EditRules>>({
|
||||
defaultValues: {
|
||||
rules: [...disguisedRules, ...rules].map((rule) => ({
|
||||
id: rule.id,
|
||||
required: requiredAttributeValues.includes(rule.attribute),
|
||||
field_type: rule.field_type,
|
||||
attribute: rule.attribute!,
|
||||
operator: rule.operator!,
|
||||
values: rule?.values?.map((v: { value: string }) => v.value!),
|
||||
})),
|
||||
},
|
||||
resolver: zodResolver(EditRules),
|
||||
})
|
||||
|
||||
const { fields, append, remove, update } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "rules",
|
||||
keyName: "rules_id",
|
||||
})
|
||||
|
||||
const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id)
|
||||
const { mutateAsync: addPromotionRules } = usePromotionAddRules(
|
||||
promotion.id,
|
||||
ruleType
|
||||
)
|
||||
|
||||
const { mutateAsync: removePromotionRules } = usePromotionRemoveRules(
|
||||
promotion.id,
|
||||
ruleType
|
||||
)
|
||||
|
||||
const { mutateAsync: updatePromotionRules } = usePromotionUpdateRules(
|
||||
promotion.id,
|
||||
ruleType
|
||||
)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const applicationMethodData: Record<any, any> = {}
|
||||
const { rules: allRules = [] } = data
|
||||
|
||||
const disguisedRulesData = allRules.filter((rule) =>
|
||||
disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
|
||||
)
|
||||
|
||||
// For all the rules that were disguised, convert them to actual values in the
|
||||
// database, they are currently all under application_method. If more of these are coming
|
||||
// up, abstract this away.
|
||||
for (const rule of disguisedRulesData) {
|
||||
applicationMethodData[rule.id!] = parseInt(rule.values as string)
|
||||
}
|
||||
|
||||
// This variable will contain the rules that are actual rule objects, without the disguised
|
||||
// objects
|
||||
const rulesData = allRules.filter(
|
||||
(rule) =>
|
||||
!disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!)
|
||||
)
|
||||
|
||||
const rulesToCreate = rulesData.filter((rule) => !("id" in rule))
|
||||
const rulesToUpdate = rulesData.filter(
|
||||
(rule) => typeof rule.id === "string"
|
||||
)
|
||||
|
||||
if (Object.keys(applicationMethodData).length) {
|
||||
await updatePromotion({ application_method: applicationMethodData })
|
||||
}
|
||||
|
||||
rulesToCreate.length &&
|
||||
(await addPromotionRules({
|
||||
rules: rulesToCreate.map((rule) => {
|
||||
return {
|
||||
attribute: rule.attribute,
|
||||
operator: rule.operator,
|
||||
values: rule.values,
|
||||
} as any
|
||||
}),
|
||||
}))
|
||||
|
||||
rulesToRemove.length &&
|
||||
(await removePromotionRules({
|
||||
rule_ids: rulesToRemove.map((r) => r.id!),
|
||||
}))
|
||||
|
||||
rulesToUpdate.length &&
|
||||
(await updatePromotionRules({
|
||||
rules: rulesToUpdate.map((rule) => {
|
||||
return {
|
||||
id: rule.id!,
|
||||
attribute: rule.attribute,
|
||||
operator: rule.operator,
|
||||
values: rule.values,
|
||||
} as any
|
||||
}),
|
||||
}))
|
||||
|
||||
handleSuccess()
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<div className="flex flex-col">
|
||||
<Heading level="h2" className="mb-2">
|
||||
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle txt-small mb-10">
|
||||
{t(`promotions.fields.conditions.${ruleType}.description`)}
|
||||
</Text>
|
||||
|
||||
{fields.map((fieldRule, index) => {
|
||||
const identifier = fieldRule.id
|
||||
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 (
|
||||
<Fragment key={index}>
|
||||
<div className="flex flex-row gap-2 bg-ui-bg-subtle py-2 px-2 rounded-xl border border-ui-border-base">
|
||||
<div className="grow">
|
||||
<Form.Field
|
||||
key={`${identifier}.${attributeFields.name}`}
|
||||
{...attributeFields}
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
const existingAttributes =
|
||||
fields?.map((field) => field.attribute) || []
|
||||
const attributeOptions =
|
||||
attributes?.filter((attr) => {
|
||||
if (attr.value === fieldRule.attribute) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !existingAttributes.includes(attr.value)
|
||||
}) || []
|
||||
|
||||
return (
|
||||
<Form.Item className="mb-2">
|
||||
{fieldRule.required && (
|
||||
<p className="text text-ui-fg-muted txt-small">
|
||||
Required
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Form.Control>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
update(index, { ...fieldRule, values: [] })
|
||||
onChange(e)
|
||||
}}
|
||||
disabled={fieldRule.required}
|
||||
>
|
||||
<Select.Trigger
|
||||
ref={attributeRef}
|
||||
className="bg-ui-bg-base"
|
||||
>
|
||||
<Select.Value placeholder="Select Attribute" />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{attributeOptions?.map((c, i) => (
|
||||
<Select.Item
|
||||
key={`${identifier}-attribute-option-${i}`}
|
||||
value={c.value}
|
||||
>
|
||||
<span className="text-ui-fg-subtle">
|
||||
{c.label}
|
||||
</span>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Form.Field
|
||||
key={`${identifier}.${operatorFields.name}`}
|
||||
{...operatorFields}
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Control>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
disabled={fieldRule.required}
|
||||
>
|
||||
<Select.Trigger
|
||||
ref={operatorRef}
|
||||
className="bg-ui-bg-base"
|
||||
>
|
||||
<Select.Value placeholder="Select Operator" />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{operators?.map((c, i) => (
|
||||
<Select.Item
|
||||
key={`${identifier}-operator-option-${i}`}
|
||||
value={c.value}
|
||||
>
|
||||
<span className="text-ui-fg-subtle">
|
||||
{c.label}
|
||||
</span>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
key={`${identifier}.${valuesFields.name}-${fieldRule.attribute}`}
|
||||
{...valuesFields}
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
if (fieldRule.field_type === "number") {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={onChange}
|
||||
className="bg-ui-bg-base"
|
||||
ref={valuesRef}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
} else if (fieldRule.field_type === "text") {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
onChange={onChange}
|
||||
className="bg-ui-bg-base"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
} else {
|
||||
const attribute = attributes?.find(
|
||||
(attr) => attr.value === fieldRule.attribute
|
||||
)
|
||||
const options = attribute
|
||||
? fetchOptionsForRule(ruleType, attribute.id)
|
||||
: []
|
||||
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
placeholder="Select Values"
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
className="bg-ui-bg-base"
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-none self-center px-1">
|
||||
<XMarkMini
|
||||
className={`text-ui-fg-muted cursor-pointer ${
|
||||
fieldRule.required ? "invisible" : "visible"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!fieldRule.required) {
|
||||
if (fieldRule.id) {
|
||||
setRulesToRemove([...rulesToRemove, fieldRule])
|
||||
}
|
||||
|
||||
remove(index)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{index < fields.length - 1 && (
|
||||
<div className="relative px-6 py-3">
|
||||
<div className="absolute top-0 bottom-0 left-[40px] z-[-1] border-ui-border-strong w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
||||
|
||||
<Badge size="2xsmall" className=" text-xs">
|
||||
AND
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="mt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="inline-block"
|
||||
onClick={() => {
|
||||
append({
|
||||
attribute: "",
|
||||
operator: "",
|
||||
values: [],
|
||||
required: false,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Add condition
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="transparent"
|
||||
className="inline-block text-ui-fg-muted hover:text-ui-fg-subtle ml-2"
|
||||
onClick={() => {
|
||||
const indicesToRemove = fields
|
||||
.map((field, index) => (field.required ? null : index))
|
||||
.filter((f) => f !== null)
|
||||
|
||||
setRulesToRemove(fields.filter((f) => !f.required))
|
||||
remove(indicesToRemove)
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
|
||||
<Button size="small" type="submit">
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-rules-form"
|
||||
+52
@@ -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 },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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 {
|
||||
usePromotion,
|
||||
usePromotionRuleAttributes,
|
||||
usePromotionRuleOperators,
|
||||
} from "../../../hooks/api/promotions"
|
||||
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 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 = params.ruleType as RuleTypeValues
|
||||
const id = params.id as string
|
||||
const rules: PromotionRuleDTO[] = []
|
||||
const { promotion, isLoading, isError, error } = usePromotion(id)
|
||||
const {
|
||||
attributes,
|
||||
isError: isAttributesError,
|
||||
error: attributesError,
|
||||
} = usePromotionRuleAttributes(ruleType!)
|
||||
const {
|
||||
operators,
|
||||
isError: isOperatorsError,
|
||||
error: operatorsError,
|
||||
} = usePromotionRuleOperators()
|
||||
|
||||
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 || isAttributesError || isOperatorsError) {
|
||||
throw error || attributesError || operatorsError
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t(`promotions.edit.${ruleType}.title`)}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
|
||||
{!isLoading && promotion && attributes && operators && (
|
||||
<EditRulesForm
|
||||
promotion={promotion}
|
||||
rules={rules}
|
||||
ruleType={ruleType}
|
||||
attributes={attributes}
|
||||
operators={operators}
|
||||
/>
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { EditRules as Component } from "./edit-rules"
|
||||
+151
@@ -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 { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||
|
||||
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<zod.infer<typeof EditPromotionSchema>>({
|
||||
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, isPending } = useUpdatePromotion(promotion.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{ campaign_id: data.campaign_id },
|
||||
{ onSuccess: () => handleSuccess() }
|
||||
)
|
||||
})
|
||||
|
||||
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.Field
|
||||
control={form.control}
|
||||
name="existing"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>Method</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col gap-y-3"
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"true"}
|
||||
label={t("promotions.form.campaign.existing.title")}
|
||||
description={t(
|
||||
"promotions.form.campaign.existing.description"
|
||||
)}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"false"}
|
||||
label={t("promotions.form.campaign.new.title")}
|
||||
description={t(
|
||||
"promotions.form.campaign.new.description"
|
||||
)}
|
||||
disabled
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="campaign_id"
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.form.campaign.existing.title")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Select onValueChange={onChange} {...field}>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{campaigns.map((c) => (
|
||||
<Select.Item key={c.id} value={c.id}>
|
||||
{c.name?.toUpperCase()}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<CampaignDetails campaign={selectedCampaign} />
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
+116
@@ -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 (
|
||||
<Fragment>
|
||||
<div>
|
||||
<Heading level="h2" className="mb-4">
|
||||
{t("campaigns.details")}
|
||||
</Heading>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus font-">
|
||||
{t("campaigns.fields.identifier")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">
|
||||
{campaign.campaign_identifier || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">{t("fields.description")}</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">{campaign.description || "-"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">
|
||||
{t("campaigns.fields.start_date")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">
|
||||
{campaign.starts_at?.toString() || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">
|
||||
{t("campaigns.fields.end_date")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">
|
||||
{campaign.ends_at?.toString() || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Heading level="h2" className="mb-4">
|
||||
{t("campaigns.budget.details")}
|
||||
</Heading>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus font-">
|
||||
{t("campaigns.budget.fields.type")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">{campaign.budget?.type || "-"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">
|
||||
{t("campaigns.budget.fields.currency")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">{campaign.currency || "-"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">
|
||||
{t("campaigns.budget.fields.limit")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">{campaign.budget?.limit || "-"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center py-1">
|
||||
<Text className="txt-small-plus">
|
||||
{t("campaigns.budget.fields.used")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="txt-small">{campaign.budget?.used || "-"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./add-campaign-promotion-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { PromotionAddCampaign as Component } from "./promotion-add-campaign"
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
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 { AddCampaignPromotionForm } from "./components/add-campaign-promotion-form"
|
||||
|
||||
export const PromotionAddCampaign = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
const { promotion, isPending, isError, error } = usePromotion(id!)
|
||||
const {
|
||||
campaigns,
|
||||
isPending: areCampaignsLoading,
|
||||
isError: isCampaignError,
|
||||
error: campaignError,
|
||||
} = useCampaigns()
|
||||
|
||||
if (isError || isCampaignError) {
|
||||
throw error || campaignError
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("promotions.addToCampaign.title")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
|
||||
{!isPending && !areCampaignsLoading && promotion && campaigns && (
|
||||
<AddCampaignPromotionForm promotion={promotion} campaigns={campaigns} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
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 { useParams } from "react-router-dom"
|
||||
|
||||
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 (
|
||||
<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")}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export const CampaignSection = ({ campaign }: { campaign: CampaignDTO }) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("promotions.fields.campaign")}</Heading>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "add-to-campaign",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{campaign ? (
|
||||
<CampaignDetailSection campaign={campaign} />
|
||||
) : (
|
||||
<NoRecords
|
||||
className="h-[180px] p-6 text-center"
|
||||
title="Not part of a campaign"
|
||||
message="Add this promotion to an existing campaign"
|
||||
action={{
|
||||
to: `/promotions/${id}/add-to-campaign`,
|
||||
label: "Add to Campaign",
|
||||
}}
|
||||
buttonVariant="transparentIconLeft"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./campaign-section.tsx"
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./promotion-conditions-section"
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { AdminGetPromotionRulesRes, PromotionRuleTypes } from "@medusajs/types"
|
||||
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 = {
|
||||
rule: AdminGetPromotionRulesRes
|
||||
}
|
||||
|
||||
function RuleBlock({ rule }: RuleProps) {
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-borders-base flex justify-around rounded-md p-2 align-center">
|
||||
<div className="flex text-ui-fg-subtle txt-compact-xsmall whitespace-nowrap items-center">
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
key="rule-attribute"
|
||||
className="mx-1 txt-compact-xsmall-plus tag-neutral-text truncate inline-block"
|
||||
>
|
||||
{rule.attribute_label}
|
||||
</Badge>
|
||||
|
||||
<span className="mx-1 txt-compact-2xsmall inline-block">
|
||||
{rule.operator_label}
|
||||
</span>
|
||||
|
||||
<BadgeListSummary
|
||||
inline
|
||||
className="!txt-compact-small-plus"
|
||||
list={rule.values.map((v) => v.label)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type PromotionConditionsSectionProps = {
|
||||
rules: AdminGetPromotionRulesRes
|
||||
ruleType: PromotionRuleTypes
|
||||
}
|
||||
|
||||
export const PromotionConditionsSection = ({
|
||||
rules,
|
||||
ruleType,
|
||||
}: PromotionConditionsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<Heading>
|
||||
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `${ruleType}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-2 px-6 pb-4 pt-2">
|
||||
{!rules.length && (
|
||||
<NoRecords
|
||||
className="h-[180px]"
|
||||
title="No records yet."
|
||||
message="Please check back later or add a target condition today"
|
||||
action={{ to: "/promotions", label: "Add condition" }}
|
||||
buttonVariant="transparentIconLeft"
|
||||
/>
|
||||
)}
|
||||
|
||||
{rules.map((rule) => (
|
||||
<RuleBlock key={`${rule.id}-${rule.attribute}`} rule={rule} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./promotion-general-section.tsx"
|
||||
+160
@@ -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 { useDeletePromotion } from "../../../../../hooks/api/promotions"
|
||||
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 } = useDeletePromotion(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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<Heading>{promotion.code}</Heading>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={color}>{text}</StatusBadge>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/promotions/${promotion.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("promotions.fields.method")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{promotion.is_automatic ? "Promotion code" : "Automatic"}
|
||||
</Text>
|
||||
</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("fields.code")}
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-pretty"
|
||||
>
|
||||
{promotion.code}
|
||||
</Text>
|
||||
|
||||
<Copy content={promotion.code!} variant="mini" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("promotions.fields.type")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact" className="text-pretty capitalize">
|
||||
{promotion.type}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("promotions.fields.value")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact" className="text-pretty">
|
||||
{promotion.application_method?.value}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("promotions.fields.allocation")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact" className="text-pretty capitalize">
|
||||
{promotion.application_method?.allocation!}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { promotionLoader as loader } from "./loader"
|
||||
export { PromotionDetail as Component } from "./promotion-detail.tsx"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AdminPromotionRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
import { promotionsQueryKeys } from "../../../hooks/api/promotions"
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
|
||||
const promotionDetailQuery = (id: string) => ({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
queryFn: () => client.promotions.retrieve(id),
|
||||
})
|
||||
|
||||
export const promotionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = promotionDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminPromotionRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { usePromotion, usePromotionRules } from "../../../hooks/api/promotions"
|
||||
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<typeof promotionLoader>
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { promotion, isLoading } = usePromotion(id!, { initialData })
|
||||
const { rules } = usePromotionRules(id!, "rules")
|
||||
const { rules: targetRules } = usePromotionRules(id!, "target-rules")
|
||||
const { rules: buyRules } = usePromotionRules(id!, "buy-rules")
|
||||
|
||||
if (isLoading || !promotion) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-2">
|
||||
<PromotionGeneralSection promotion={promotion} />
|
||||
|
||||
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
|
||||
|
||||
<PromotionConditionsSection
|
||||
rules={targetRules || []}
|
||||
ruleType={"target-rules"}
|
||||
/>
|
||||
|
||||
{promotion.type === "buyget" && (
|
||||
<PromotionConditionsSection
|
||||
rules={buyRules || []}
|
||||
ruleType={"buy-rules"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full flex-col gap-y-2 xl:hidden">
|
||||
<CampaignSection campaign={promotion.campaign!} />
|
||||
</div>
|
||||
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<JsonViewSection data={promotion as any} />
|
||||
</div>
|
||||
|
||||
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
||||
<CampaignSection campaign={promotion.campaign!} />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
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 { useUpdatePromotion } from "../../../../../hooks/api/promotions"
|
||||
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<zod.infer<typeof EditPromotionSchema>>({
|
||||
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, isPending } = useUpdatePromotion(promotion.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
console.log("data ------ ", data)
|
||||
await mutateAsync(
|
||||
{
|
||||
is_automatic: data.is_automatic === "true",
|
||||
code: data.code,
|
||||
application_method: {
|
||||
value: data.value,
|
||||
type: data.value_type as any,
|
||||
allocation: data.allocation as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
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.Field
|
||||
control={form.control}
|
||||
name="is_automatic"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>Method</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"true"}
|
||||
label={t("promotions.form.method.automatic.title")}
|
||||
description={t(
|
||||
"promotions.form.method.automatic.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.form.code.title")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="promotions.form.code.description"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value_type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.fields.value_type")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"percentage"}
|
||||
label={t(
|
||||
"promotions.form.value_type.percentage.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.percentage.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{isFixedValueType
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
{isFixedValueType ? (
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onValueChange={onChange}
|
||||
code={"USD"}
|
||||
symbol={getCurrencySymbol("USD")}
|
||||
{...field}
|
||||
value={Number(field.value)}
|
||||
/>
|
||||
) : (
|
||||
<PercentageInput
|
||||
key="amount"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.value === "" ? null : e.target.value
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="allocation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.fields.allocation")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"across"}
|
||||
label={t("promotions.form.allocation.across.title")}
|
||||
description={t(
|
||||
"promotions.form.allocation.across.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</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 size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-promotion-details-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { PromotionEditDetails as Component } from "./promotion-edit-details"
|
||||
+30
@@ -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 { usePromotion } from "../../../hooks/api/promotions"
|
||||
import { EditPromotionDetailsForm } from "./components/edit-promotion-form"
|
||||
|
||||
export const PromotionEditDetails = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { promotion, isLoading, isError, error } = usePromotion(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("promotions.edit.title")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
|
||||
{!isLoading && promotion && (
|
||||
<EditPromotionDetailsForm promotion={promotion} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
+42
-4
@@ -1,15 +1,18 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PromotionDTO } from "@medusajs/types"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
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 { keepPreviousData } from "@tanstack/react-query"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { usePromotions } from "../../../../../hooks/api/promotions"
|
||||
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"
|
||||
@@ -79,6 +82,36 @@ export const PromotionListTable = () => {
|
||||
|
||||
const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const navigate = useNavigate()
|
||||
const { mutateAsync } = useDeletePromotion(promotion.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("/promotions", { replace: true })
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Promotion with code ${promotion.code} could not be deleted`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
@@ -90,6 +123,11 @@ const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
||||
label: t("actions.edit"),
|
||||
to: `/promotions/${promotion.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { QueryClient } from "@tanstack/react-query"
|
||||
import { adminPromotionKeys } from "../../../lib/api-v2"
|
||||
import { promotionsQueryKeys } from "../../../hooks/api/promotions"
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
const params = {
|
||||
@@ -13,7 +13,7 @@ const params = {
|
||||
}
|
||||
|
||||
const promotionsListQuery = () => ({
|
||||
queryKey: adminPromotionKeys.list(params),
|
||||
queryKey: promotionsQueryKeys.list(params),
|
||||
queryFn: async () =>
|
||||
medusa.admin.custom.get<AdminGetPromotionsParams, AdminPromotionsListRes>(
|
||||
"/promotions",
|
||||
|
||||
@@ -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<string, any>`.
|
||||
* @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
|
||||
* <RequestQuery, ResponseData>(
|
||||
@@ -33,7 +33,7 @@ import { UseQueryOptionsWrapper } from "../../../types"
|
||||
* title: "My post"
|
||||
* }
|
||||
* )
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isLoading && <span>Loading...</span>}
|
||||
@@ -50,9 +50,9 @@ import { UseQueryOptionsWrapper } from "../../../types"
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
*
|
||||
*
|
||||
* export default Custom
|
||||
*
|
||||
*
|
||||
* @customNamespace Hooks.Admin.Custom
|
||||
* @category Mutations
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./types"
|
||||
export * from "./validators"
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CampaignDTO, PaginatedResponse } from "@medusajs/types"
|
||||
|
||||
export type AdminCampaignsListRes = PaginatedResponse<{
|
||||
campaigns: CampaignDTO[]
|
||||
}>
|
||||
|
||||
export type AdminCampaignRes = {
|
||||
campaign: CampaignDTO
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./campaigns"
|
||||
export * from "./promotions"
|
||||
@@ -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<string, string>(
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -21,25 +21,35 @@ 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",
|
||||
"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,
|
||||
@@ -57,3 +67,9 @@ export const listRuleValueTransformQueryConfig = {
|
||||
allowed: [],
|
||||
isList: true,
|
||||
}
|
||||
|
||||
// TODO: replace this with the old one
|
||||
export const retrieveNewTransformQueryConfig = {
|
||||
defaults,
|
||||
isList: false,
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
+6
-55
@@ -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)
|
||||
|
||||
@@ -3,3 +3,7 @@ import { PaginatedResponse, PromotionDTO } from "@medusajs/types"
|
||||
export type AdminPromotionsListRes = PaginatedResponse<{
|
||||
promotions: PromotionDTO[]
|
||||
}>
|
||||
|
||||
export type AdminPromotionRes = {
|
||||
promotion: PromotionDTO
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}[]
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./common"
|
||||
export * from "./http"
|
||||
export * from "./mutations"
|
||||
export * from "./service"
|
||||
export * from "./workflows"
|
||||
Reference in new issue
Block a user