feat(dashboard,medusa): Promotion Campaign fixes (#7337)

* chore(medusa): strict zod versions in workspace

* feat(dashboard): add campaign create to promotion UI

* wip

* fix(medusa): Missing middlewares export (#7289)

* fix(docblock-generator): fix how type names created from Zod objects are inferred (#7292)

* feat(api-ref): show schema of a tag (#7297)

* feat: Add support for sendgrid and logger notification providers (#7290)

* feat: Add support for sendgrid and logger notification providers

* fix: changes based on PR review

* chore: add action to automatically label docs (#7284)

* chore: add action to automatically label docs

* removes the paths param

* docs: preparations for preview (#7267)

* configured base paths + added development banner

* fix typelist site url

* added navbar and sidebar badges

* configure algolia filters

* remove AI assistant

* remove unused imports

* change navbar text and badge

* lint fixes

* fix build error

* add to api reference rewrites

* fix build error

* fix build errors in user-guide

* fix feedback component

* add parent title to pagination

* added breadcrumbs component

* remove user-guide links

* resolve todos

* fix details about authentication

* change documentation title

* lint content

* chore: fix bug with form reset

* chore: address reviews

* chore: fix specs

* chore: loads of FE fixes + BE adds

* chore: add more polishes + reorg files

* chore: fixes to promotions modal

* chore: cleanup

* chore: cleanup

* chore: fix build

* chore: fkix cart spec

* chore: fix module tests

* chore: fix moar tests

* wip

* chore: templates + fixes + migrate currency

* chore: fix build, add validation for max_quantity

* chore: allow removing campaigns

* chore: fix specs

* chore: scope campaigns based on currency

* remove console logs

* chore: add translations + update keys

* chore: move over filesfrom v2 to routes

* chore(dashboard): Delete old translation files (#7423)

* feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support for UI extensions (#7383)

* intial work

* update lock

* add routes and fix HMR of configs

* cleanup

* rm imports

* rm debug from plugin

* address feedback

* address feedback

* temp skip specs

---------

Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>
Co-authored-by: Shahed Nasser <shahednasser@gmail.com>
Co-authored-by: Stevche Radevski <sradevski@live.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2024-05-23 15:28:00 +02:00
committed by GitHub
parent 4a10821bfe
commit d1d23f1e8d
72 changed files with 5380 additions and 3473 deletions
@@ -1,4 +1,4 @@
import { CampaignBudgetType } from "@medusajs/utils"
import { CampaignBudgetType, isPresent } from "@medusajs/utils"
import { z } from "zod"
import { createFindParams, createSelectParams } from "../../utils/validators"
@@ -10,44 +10,70 @@ export type AdminGetCampaignsParamsType = z.infer<
export const AdminGetCampaignsParams = createFindParams({
offset: 0,
limit: 50,
}).merge(
z.object({
q: z.string().optional(),
campaign_identifier: z.string().optional(),
currency: z.string().optional(),
$and: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
$or: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
})
.merge(
z.object({
q: z.string().optional(),
campaign_identifier: z.string().optional(),
budget: z
.object({
currency_code: z.string().optional(),
})
.optional(),
$and: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
$or: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
})
)
.strict()
export const CreateCampaignBudget = z
.object({
type: z.nativeEnum(CampaignBudgetType),
limit: z.number().optional().nullable(),
currency_code: z.string().optional().nullable(),
})
)
.strict()
.refine(
(data) =>
data.type !== CampaignBudgetType.SPEND || isPresent(data.currency_code),
{
path: ["currency_code"],
message: `currency_code is required when budget type is ${CampaignBudgetType.SPEND}`,
}
)
.refine(
(data) =>
data.type !== CampaignBudgetType.USAGE || !isPresent(data.currency_code),
{
path: ["currency_code"],
message: `currency_code should not be present when budget type is ${CampaignBudgetType.USAGE}`,
}
)
const CreateCampaignBudget = z.object({
type: z.nativeEnum(CampaignBudgetType),
limit: z.number(),
})
const UpdateCampaignBudget = z.object({
type: z.nativeEnum(CampaignBudgetType).optional(),
limit: z.number().optional(),
})
export const UpdateCampaignBudget = z
.object({
limit: z.number().optional().nullable(),
})
.strict()
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
export const AdminCreateCampaign = z.object({
name: z.string(),
campaign_identifier: z.string(),
description: z.string().optional(),
currency: z.string().optional(),
budget: CreateCampaignBudget.optional(),
starts_at: z.coerce.date().optional(),
ends_at: z.coerce.date().optional(),
promotions: z.array(z.object({ id: z.string() })).optional(),
})
export const AdminCreateCampaign = z
.object({
name: z.string(),
campaign_identifier: z.string(),
description: z.string().optional(),
budget: CreateCampaignBudget.optional(),
starts_at: z.coerce.date().optional(),
ends_at: z.coerce.date().optional(),
promotions: z.array(z.object({ id: z.string() })).optional(),
})
.strict()
export type AdminUpdateCampaignType = z.infer<typeof AdminUpdateCampaign>
export const AdminUpdateCampaign = z.object({
name: z.string().optional(),
campaign_identifier: z.string().optional(),
description: z.string().optional(),
currency: z.string().optional(),
budget: UpdateCampaignBudget.optional(),
starts_at: z.coerce.date().optional(),
ends_at: z.coerce.date().optional(),
@@ -57,6 +57,7 @@ export const GET = async (
attribute: disguisedRule.id,
attribute_label: disguisedRule.label,
field_type: disguisedRule.field_type,
hydrate: disguisedRule.hydrate || false,
operator: RuleOperator.EQ,
operator_label: operatorsMap[RuleOperator.EQ].label,
values,
@@ -67,9 +68,11 @@ export const GET = async (
continue
}
for (const promotionRule of promotionRules) {
for (const promotionRule of [...promotionRules, ...transformedRules]) {
const currentRuleAttribute = ruleAttributes.find(
(attr) => attr.value === promotionRule.attribute
(attr) =>
attr.value === promotionRule.attribute ||
attr.value === promotionRule.attribute
)
if (!currentRuleAttribute) {
@@ -77,6 +80,11 @@ export const GET = async (
}
const queryConfig = ruleQueryConfigurations[currentRuleAttribute.id]
if (!queryConfig) {
continue
}
const rows = await remoteQuery(
remoteQueryObjectFromString({
entryPoint: queryConfig.entryPoint,
@@ -101,15 +109,17 @@ export const GET = async (
label: valueLabelMap.get(value.value) || value.value,
}))
transformedRules.push({
...promotionRule,
attribute_label: currentRuleAttribute.label,
field_type: currentRuleAttribute.field_type,
operator_label:
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
disguised: false,
required: currentRuleAttribute.required || false,
})
if (!currentRuleAttribute.hydrate) {
transformedRules.push({
...promotionRule,
attribute_label: currentRuleAttribute.label,
field_type: currentRuleAttribute.field_type,
operator_label:
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
disguised: false,
required: currentRuleAttribute.required || false,
})
}
}
if (requiredRules.length && !transformedRules.length) {
@@ -124,6 +134,7 @@ export const GET = async (
values: [],
disguised: true,
required: true,
hydrate: false,
})
continue
@@ -19,6 +19,13 @@ export const GET = async (
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const filterableFields = req.filterableFields
if (filterableFields.value) {
filterableFields[queryConfig.valueAttr] = filterableFields.value
delete filterableFields.value
}
validateRuleType(ruleType)
validateRuleAttribute(ruleType, ruleAttributeId)
@@ -27,7 +34,7 @@ export const GET = async (
remoteQueryObjectFromString({
entryPoint: queryConfig.entryPoint,
variables: {
filters: req.filterableFields,
filters: filterableFields,
...req.remoteQueryConfig.pagination,
},
fields: [queryConfig.labelAttr, queryConfig.valueAttr],
@@ -1,6 +1,7 @@
export enum DisguisedRule {
APPLY_TO_QUANTITY = "apply_to_quantity",
BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity",
CURRENCY_CODE = "currency_code",
}
export const disguisedRulesMap = {
@@ -10,38 +11,48 @@ export const disguisedRulesMap = {
[DisguisedRule.BUY_RULES_MIN_QUANTITY]: {
relation: "application_method",
},
[DisguisedRule.CURRENCY_CODE]: {
relation: "application_method",
},
}
const ruleAttributes = [
{
id: "currency",
value: "currency_code",
label: "Currency code",
id: DisguisedRule.CURRENCY_CODE,
value: DisguisedRule.CURRENCY_CODE,
label: "Currency Code",
field_type: "select",
required: true,
disguised: true,
hydrate: true,
},
{
id: "customer_group",
value: "customer_group.id",
value: "customer.groups.id",
label: "Customer Group",
required: false,
field_type: "multiselect",
},
{
id: "region",
value: "region.id",
label: "Region",
required: false,
field_type: "multiselect",
},
{
id: "country",
value: "shipping_address.country_code",
label: "Country",
required: false,
field_type: "multiselect",
},
{
id: "sales_channel",
value: "sales_channel.id",
value: "sales_channel_id",
label: "Sales Channel",
required: false,
field_type: "multiselect",
},
]
@@ -51,30 +62,35 @@ const commonAttributes = [
value: "items.product.id",
label: "Product",
required: false,
field_type: "multiselect",
},
{
id: "product_category",
value: "items.product.categories.id",
label: "Product Category",
required: false,
field_type: "multiselect",
},
{
id: "product_collection",
value: "items.product.collection_id",
label: "Product Collection",
required: false,
field_type: "multiselect",
},
{
id: "product_type",
value: "items.product.type_id",
label: "Product Type",
required: false,
field_type: "multiselect",
},
{
id: "product_tag",
value: "items.product.tags.id",
label: "Product Tag",
required: false,
field_type: "multiselect",
},
]
@@ -4,9 +4,9 @@ export const ruleQueryConfigurations = {
labelAttr: "name",
valueAttr: "id",
},
currency: {
currency_code: {
entryPoint: "currency",
labelAttr: "code",
labelAttr: "name",
valueAttr: "code",
},
customer_group: {
@@ -2,7 +2,6 @@ import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
ApplicationMethodType,
CampaignBudgetType,
PromotionRuleOperator,
PromotionType,
} from "@medusajs/utils"
@@ -12,6 +11,7 @@ import {
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
import { AdminCreateCampaign } from "../campaigns/validators"
export type AdminGetPromotionParamsType = z.infer<
typeof AdminGetPromotionParams
@@ -30,6 +30,11 @@ export const AdminGetPromotionsParams = createFindParams({
q: z.string().optional(),
code: z.union([z.string(), z.array(z.string())]).optional(),
campaign_id: z.union([z.string(), z.array(z.string())]).optional(),
application_method: z
.object({
currency_code: z.union([z.string(), z.array(z.string())]).optional(),
})
.optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
@@ -58,6 +63,7 @@ export const AdminGetPromotionsRuleValueParams = createFindParams({
}).merge(
z.object({
q: z.string().optional(),
value: z.union([z.string(), z.array(z.string())]).optional(),
})
)
@@ -69,7 +75,7 @@ export const AdminCreatePromotionRule = z
operator: z.nativeEnum(PromotionRuleOperator),
description: z.string().optional(),
attribute: z.string(),
values: z.array(z.string()),
values: z.union([z.string(), z.array(z.string())]),
})
.strict()
@@ -82,7 +88,7 @@ export const AdminUpdatePromotionRule = z
operator: z.nativeEnum(PromotionRuleOperator).optional(),
description: z.string().optional(),
attribute: z.string().optional(),
values: z.array(z.string()).optional(),
values: z.union([z.string(), z.array(z.string())]),
})
.strict()
@@ -93,12 +99,12 @@ export const AdminCreateApplicationMethod = z
.object({
description: z.string().optional(),
value: z.number(),
max_quantity: z.number().optional(),
currency_code: z.string(),
max_quantity: z.number().optional().nullable(),
type: z.nativeEnum(ApplicationMethodType),
target_type: z.nativeEnum(ApplicationMethodTargetType),
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
target_rules: z.array(AdminCreatePromotionRule).optional(),
buy_rules: z.array(AdminCreatePromotionRule).optional(),
apply_to_quantity: z.number().optional(),
buy_rules_min_quantity: z.number().optional(),
@@ -111,8 +117,9 @@ export type AdminUpdateApplicationMethodType = z.infer<
export const AdminUpdateApplicationMethod = z
.object({
description: z.string().optional(),
value: z.string().optional(),
max_quantity: z.number().optional(),
value: z.number().optional(),
max_quantity: z.number().optional().nullable(),
currency_code: z.string().optional(),
type: z.nativeEnum(ApplicationMethodType).optional(),
target_type: z.nativeEnum(ApplicationMethodTargetType).optional(),
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
@@ -140,23 +147,6 @@ const promoRefinement = (promo) => {
return true
}
// Ideally we don't allow for creation of campaigns through promotions, it should be the other way around.
const CreateCampaignBudget = z.object({
type: z.nativeEnum(CampaignBudgetType),
limit: z.number(),
})
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
export const AdminCreateCampaign = z.object({
name: z.string(),
campaign_identifier: z.string(),
description: z.string().optional(),
currency: z.string().optional(),
budget: CreateCampaignBudget.optional(),
starts_at: z.coerce.date().optional(),
ends_at: z.coerce.date().optional(),
})
export type AdminCreatePromotionType = z.infer<typeof AdminCreatePromotion>
export const AdminCreatePromotion = z
.object({