fix(dashboard): Add FE validation for conditional prices (#10523)
* fix(dashboard): Add FE validation for conditional prices * fix(dashboard): Add FE validation for conditional prices * lint * only set error on one field * fix which field shows error
This commit is contained in:
1 parent
69f4c4f4e0
commit
de81187929
5 files changed
+174
-79
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(dashboard): Add FE validation for conditional prices
|
||||||
@@ -2978,15 +2978,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["stockedQuantity"],
|
||||||
"stockedQuantity"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["errors"],
|
||||||
"errors"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -3792,10 +3788,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["title", "requestSuccess"],
|
||||||
"title",
|
|
||||||
"requestSuccess"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"shippingAddress": {
|
"shippingAddress": {
|
||||||
@@ -3808,10 +3801,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["title", "requestSuccess"],
|
||||||
"title",
|
|
||||||
"requestSuccess"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"billingAddress": {
|
"billingAddress": {
|
||||||
@@ -3824,18 +3814,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["title", "requestSuccess"],
|
||||||
"title",
|
|
||||||
"requestSuccess"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["email", "shippingAddress", "billingAddress"],
|
||||||
"email",
|
|
||||||
"shippingAddress",
|
|
||||||
"billingAddress"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"returns": {
|
"returns": {
|
||||||
@@ -4987,11 +4970,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["shipping_address", "billing_address", "email"],
|
||||||
"shipping_address",
|
|
||||||
"billing_address",
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5560,9 +5539,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["cartItemTotal"],
|
||||||
"cartItemTotal"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"summaries": {
|
"summaries": {
|
||||||
@@ -5578,11 +5555,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["range", "greaterThan", "lessThan"],
|
||||||
"range",
|
|
||||||
"greaterThan",
|
|
||||||
"lessThan"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -5595,10 +5568,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["addPrice", "manageConditionalPrices"],
|
||||||
"addPrice",
|
|
||||||
"manageConditionalPrices"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -5614,11 +5584,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["amount", "gte", "lte"],
|
||||||
"amount",
|
|
||||||
"gte",
|
|
||||||
"lte"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"customRules": {
|
"customRules": {
|
||||||
@@ -5640,13 +5606,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["label", "tooltip", "eq", "gt", "lt"],
|
||||||
"label",
|
|
||||||
"tooltip",
|
|
||||||
"eq",
|
|
||||||
"gt",
|
|
||||||
"lt"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -5660,12 +5620,20 @@
|
|||||||
},
|
},
|
||||||
"minGreaterThanMax": {
|
"minGreaterThanMax": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"duplicateAmount": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"overlappingConditions": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"amountRequired",
|
"amountRequired",
|
||||||
"minOrMaxRequired",
|
"minOrMaxRequired",
|
||||||
"minGreaterThanMax"
|
"minGreaterThanMax",
|
||||||
|
"duplicateAmount",
|
||||||
|
"overlappingConditions"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -8842,16 +8810,6 @@
|
|||||||
"deleteSalesChannelWarning": {
|
"deleteSalesChannelWarning": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"cannotDeleteDefault": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["cannotDeleteDefault"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"toast": {
|
"toast": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -8868,6 +8826,16 @@
|
|||||||
"required": ["create", "update", "delete"],
|
"required": ["create", "update", "delete"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cannotDeleteDefault": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cannotDeleteDefault"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -8916,6 +8884,7 @@
|
|||||||
"productAlreadyAdded",
|
"productAlreadyAdded",
|
||||||
"deleteSalesChannelWarning",
|
"deleteSalesChannelWarning",
|
||||||
"toast",
|
"toast",
|
||||||
|
"tooltip",
|
||||||
"products"
|
"products"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -1493,7 +1493,9 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"amountRequired": "Shipping option price is required",
|
"amountRequired": "Shipping option price is required",
|
||||||
"minOrMaxRequired": "At least one of minimum or maximum cart item total must be provided",
|
"minOrMaxRequired": "At least one of minimum or maximum cart item total must be provided",
|
||||||
"minGreaterThanMax": "Minimum cart item total must be less than or equal to maximum cart item total"
|
"minGreaterThanMax": "Minimum cart item total must be less than or equal to maximum cart item total",
|
||||||
|
"duplicateAmount": "Shipping option price must be unique for each condition",
|
||||||
|
"overlappingConditions": "Conditions must be unique across all price rules"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
|||||||
+26
-9
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
clx,
|
||||||
CurrencyInput,
|
CurrencyInput,
|
||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -110,14 +111,28 @@ export const ConditionalPriceForm = ({
|
|||||||
remove(index)
|
remove(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnSubmit = conditionalPriceForm.handleSubmit((values) => {
|
const handleOnSubmit = conditionalPriceForm.handleSubmit(
|
||||||
setFormValue(name, values.prices, {
|
(values) => {
|
||||||
shouldDirty: true,
|
setFormValue(name, values.prices, {
|
||||||
shouldValidate: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldValidate: true,
|
||||||
})
|
shouldTouch: true,
|
||||||
onCloseConditionalPricesModal()
|
})
|
||||||
})
|
onCloseConditionalPricesModal()
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
const indexesWithErrors = Object.keys(e.prices || {})
|
||||||
|
setValue((prev) => {
|
||||||
|
const values = new Set(prev)
|
||||||
|
|
||||||
|
indexesWithErrors.forEach((index) => {
|
||||||
|
values.add(getRuleValue(Number(index)))
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(values)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Intercept the Cmd + Enter key to only save the inner form.
|
// Intercept the Cmd + Enter key to only save the inner form.
|
||||||
const handleOnKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {
|
const handleOnKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {
|
||||||
@@ -255,7 +270,9 @@ const ConditionalPriceItem = ({
|
|||||||
return (
|
return (
|
||||||
<Accordion.Item
|
<Accordion.Item
|
||||||
value={getRuleValue(index)}
|
value={getRuleValue(index)}
|
||||||
className="bg-ui-bg-component shadow-elevation-card-rest rounded-lg"
|
className={clx(
|
||||||
|
"bg-ui-bg-component shadow-elevation-card-rest rounded-lg"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Accordion.Trigger asChild>
|
<Accordion.Trigger asChild>
|
||||||
<div className="group/trigger flex w-full cursor-pointer items-start justify-between gap-x-2 p-3">
|
<div className="group/trigger flex w-full cursor-pointer items-start justify-between gap-x-2 p-3">
|
||||||
|
|||||||
@@ -73,18 +73,120 @@ export type UpdateConditionalPrice = z.infer<
|
|||||||
typeof UpdateConditionalPriceSchema
|
typeof UpdateConditionalPriceSchema
|
||||||
>
|
>
|
||||||
|
|
||||||
export const CondtionalPriceRuleSchema = z.object({
|
function refineDuplicates(
|
||||||
prices: z.array(ConditionalPriceSchema),
|
data: {
|
||||||
})
|
prices: {
|
||||||
|
amount: string | number
|
||||||
|
gte?: string | number | null | undefined
|
||||||
|
lte?: string | number | null | undefined
|
||||||
|
lt?: number | null | undefined
|
||||||
|
gt?: number | null | undefined
|
||||||
|
eq?: number | null | undefined
|
||||||
|
}[]
|
||||||
|
},
|
||||||
|
ctx: z.RefinementCtx
|
||||||
|
) {
|
||||||
|
const prices = data.prices
|
||||||
|
|
||||||
|
for (let i = 0; i < prices.length; i++) {
|
||||||
|
for (let j = i + 1; j < prices.length; j++) {
|
||||||
|
const price1 = prices[i]
|
||||||
|
const price2 = prices[j]
|
||||||
|
|
||||||
|
if (price1.amount === "" || price2.amount === "") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const price1Amount = castNumber(price1.amount)
|
||||||
|
const price2Amount = castNumber(price2.amount)
|
||||||
|
|
||||||
|
if (price1Amount === price2Amount) {
|
||||||
|
addDuplicateAmountError(ctx, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check conditions separately
|
||||||
|
const conditions = [
|
||||||
|
{ value: price1.gte, type: "gte" },
|
||||||
|
{ value: price1.lte, type: "lte" },
|
||||||
|
{ value: price1.eq, type: "eq" },
|
||||||
|
{ value: price1.lt, type: "lt" },
|
||||||
|
{ value: price1.gt, type: "gt" },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
conditions.forEach((condition1) => {
|
||||||
|
if (!condition1.value && condition1.value !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditions2 = [
|
||||||
|
{ value: price2.gte, type: "gte" },
|
||||||
|
{ value: price2.lte, type: "lte" },
|
||||||
|
{ value: price2.eq, type: "eq" },
|
||||||
|
{ value: price2.lt, type: "lt" },
|
||||||
|
{ value: price2.gt, type: "gt" },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
conditions2.forEach((condition2) => {
|
||||||
|
if (!condition2.value && condition2.value !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition1Value = castNumber(
|
||||||
|
condition1.value as string | number
|
||||||
|
)
|
||||||
|
const condition2Value = castNumber(
|
||||||
|
condition2.value as string | number
|
||||||
|
)
|
||||||
|
|
||||||
|
if (condition1Value === condition2Value) {
|
||||||
|
addOverlappingConditionError(ctx, j, condition2.type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CondtionalPriceRuleSchema = z
|
||||||
|
.object({
|
||||||
|
prices: z.array(ConditionalPriceSchema),
|
||||||
|
})
|
||||||
|
.superRefine(refineDuplicates)
|
||||||
|
|
||||||
export type CondtionalPriceRuleSchemaType = z.infer<
|
export type CondtionalPriceRuleSchemaType = z.infer<
|
||||||
typeof CondtionalPriceRuleSchema
|
typeof CondtionalPriceRuleSchema
|
||||||
>
|
>
|
||||||
|
|
||||||
export const UpdateConditionalPriceRuleSchema = z.object({
|
export const UpdateConditionalPriceRuleSchema = z
|
||||||
prices: z.array(UpdateConditionalPriceSchema),
|
.object({
|
||||||
})
|
prices: z.array(UpdateConditionalPriceSchema),
|
||||||
|
})
|
||||||
|
.superRefine(refineDuplicates)
|
||||||
|
|
||||||
export type UpdateConditionalPriceRuleSchemaType = z.infer<
|
export type UpdateConditionalPriceRuleSchemaType = z.infer<
|
||||||
typeof UpdateConditionalPriceRuleSchema
|
typeof UpdateConditionalPriceRuleSchema
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const addDuplicateAmountError = (ctx: z.RefinementCtx, index: number) => {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t(
|
||||||
|
"stockLocations.shippingOptions.conditionalPrices.errors.duplicateAmount"
|
||||||
|
),
|
||||||
|
path: ["prices", index, "amount"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOverlappingConditionError = (
|
||||||
|
ctx: z.RefinementCtx,
|
||||||
|
index: number,
|
||||||
|
type: "gte" | "lte" | "eq" | "lt" | "gt"
|
||||||
|
) => {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t(
|
||||||
|
"stockLocations.shippingOptions.conditionalPrices.errors.overlappingConditions"
|
||||||
|
),
|
||||||
|
path: ["prices", index, type],
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user