fix(dashboard): support more decimals for tx rates (#13407)

* Percentage formatter set maximum digits to 4

* Modify Tax Region Forms to allow 4 digits tax rates

* Add changeset

* Revert placeholder value to use only 2 decimals

* fix(dashboard): support more decimals for tx rates

* changeset

* bypass scale

* refactor

* fix removal of deprecated input

* cleanup deprecated percentage input

* small mistake in onchange

* add deprecated percentage input back

* import

* remove file extension

* frane comment

---------

Co-authored-by: AmbroziuBaban <ambroziubaban@gmail.com>
This commit is contained in:
William Bouchard
2025-09-05 07:02:44 -04:00
committed by GitHub
parent 0000ed6998
commit 9b3831d258
10 changed files with 102 additions and 41 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
fix(dashboard): support more decimals for tx rates

View File

@@ -1,10 +1,22 @@
import { Input, Text, clx } from "@medusajs/ui"
import { clx, Input, Text } from "@medusajs/ui"
import { getNumberOfDecimalPlaces } from "../../../lib/number-helper"
import { ComponentProps, ElementRef, forwardRef } from "react"
import Primitive from "react-currency-input-field"
/**
* @deprecated Use `PercentageInput` instead
*/
const MIN_DECIMAL_SCALE = 2
function resolveDecimalScale(
value: string | readonly string[] | number | undefined | null
): number | undefined {
if (value == null || Array.isArray(value)) {
return MIN_DECIMAL_SCALE
}
return Math.max(
getNumberOfDecimalPlaces(parseFloat(value.toString())),
MIN_DECIMAL_SCALE
)
}
export const DeprecatedPercentageInput = forwardRef<
ElementRef<typeof Input>,
Omit<ComponentProps<typeof Input>, "type">
@@ -38,38 +50,56 @@ DeprecatedPercentageInput.displayName = "PercentageInput"
export const PercentageInput = forwardRef<
ElementRef<"input">,
ComponentProps<typeof Primitive>
>(({ min = 0, decimalScale = 2, className, ...props }, ref) => {
return (
<div className="relative">
<Primitive
ref={ref as any} // dependency is typed incorrectly
min={min}
autoComplete="off"
decimalScale={decimalScale}
decimalsLimit={decimalScale}
{...props}
className={clx(
"caret-ui-fg-base bg-ui-bg-field shadow-buttons-neutral transition-fg txt-compact-small flex w-full select-none appearance-none items-center justify-between rounded-md px-2 py-1.5 pr-10 text-right outline-none",
"placeholder:text-ui-fg-muted text-ui-fg-base",
"hover:bg-ui-bg-field-hover",
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
"invalid::border-ui-border-error invalid:shadow-borders-error",
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
className
)}
/>
<div className="absolute inset-y-0 right-0 z-10 flex w-8 items-center justify-center border-l">
<Text
className="text-ui-fg-muted"
size="small"
leading="compact"
weight="plus"
>
%
</Text>
>(
(
{
min = 0,
max = 100,
decimalScale,
decimalsLimit,
value,
className,
...props
},
ref
) => {
const resolvedDecimalScale = decimalScale ?? resolveDecimalScale(value)
const resolvedDecimalsLimit = decimalsLimit ?? resolvedDecimalScale
return (
<div className="relative">
<Primitive
ref={ref as any} // dependency is typed incorrectly
min={min}
max={max}
autoComplete="off"
decimalScale={resolvedDecimalScale}
decimalsLimit={resolvedDecimalsLimit}
value={value}
{...props}
className={clx(
"caret-ui-fg-base bg-ui-bg-field shadow-buttons-neutral transition-fg txt-compact-small flex w-full select-none appearance-none items-center justify-between rounded-md px-2 py-1.5 pl-10 text-left outline-none",
"placeholder:text-ui-fg-muted text-ui-fg-base",
"hover:bg-ui-bg-field-hover",
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
"invalid::border-ui-border-error invalid:shadow-borders-error",
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
className
)}
/>
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
<Text
className="text-ui-fg-muted"
size="small"
leading="compact"
weight="plus"
>
%
</Text>
</div>
</div>
</div>
)
})
)
}
)
PercentageInput.displayName = "PercentageInput"

View File

@@ -0,0 +1,19 @@
/**
* Gets the number of decimal places in a number
* @param num - The number for which we are getting the number of decimal places
* @returns The number of decimal places
*
* @example
* getDecimalPlaces(123.456) // 3
* getDecimalPlaces(42) // 0
* getDecimalPlaces(10.0000) // 0
*/
export function getNumberOfDecimalPlaces(num: number): number {
// Convert to string and check if it contains a decimal point
const str = num.toString()
if (str.indexOf(".") === -1) {
return 0
}
// Return the length of the part after the decimal point
return str.split(".")[1].length
}

View File

@@ -1,6 +1,7 @@
const formatter = new Intl.NumberFormat([], {
style: "percent",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
})
/**

View File

@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod"
import { InformationCircleSolid } from "@medusajs/icons"
import { Button, Heading, Input, Text, Tooltip, toast } from "@medusajs/ui"
import { Button, Heading, Input, Text, toast, Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import { CountrySelect } from "../../../../../components/inputs/country-select"
@@ -221,6 +221,7 @@ export const TaxRegionCreateForm = ({ parentId }: TaxRegionCreateFormProps) => {
<PercentageInput
{...field}
value={value?.value}
decimalsLimit={4}
onValueChange={(value, _name, values) =>
onChange({
value: value,

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { InformationCircleSolid } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Button, Heading, Input, Text, Tooltip, toast } from "@medusajs/ui"
import { Button, Heading, Input, Text, toast, Tooltip } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
@@ -189,6 +189,7 @@ export const TaxRegionProvinceCreateForm = ({
<PercentageInput
{...field}
value={value?.value}
decimalsLimit={4}
onValueChange={(value, _name, values) =>
onChange({
value: value,

View File

@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
Button,
clx,
Divider,
Heading,
Hint,
@@ -8,7 +9,6 @@ import {
Label,
Select,
Text,
clx,
toast,
} from "@medusajs/ui"
import { useFieldArray, useForm, useWatch } from "react-hook-form"
@@ -414,6 +414,7 @@ export const TaxRegionCreateTaxOverrideForm = ({
<PercentageInput
{...field}
placeholder="0.00"
decimalsLimit={4}
value={value?.value}
onValueChange={(value, _name, values) =>
onChange({

View File

@@ -3,6 +3,7 @@ import { MagnifyingGlass } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import {
Button,
clx,
Divider,
Heading,
Hint,
@@ -10,7 +11,6 @@ import {
Label,
Select,
Text,
clx,
toast,
} from "@medusajs/ui"
import { useFieldArray, useForm, useWatch } from "react-hook-form"
@@ -414,6 +414,7 @@ export const TaxRegionTaxOverrideEditForm = ({
<PercentageInput
{...field}
value={value?.value}
decimalsLimit={4}
onValueChange={(value, _name, values) =>
onChange({
value: value,

View File

@@ -119,6 +119,7 @@ export const TaxRegionTaxRateCreateForm = ({
<PercentageInput
{...field}
value={value?.value}
decimalsLimit={4}
onValueChange={(value, _name, values) =>
onChange({
value: value,

View File

@@ -117,6 +117,7 @@ export const TaxRegionTaxRateEditForm = ({
<PercentageInput
{...field}
value={value?.value}
decimalsLimit={4}
onValueChange={(value, _name, values) =>
onChange({
value: value,