diff --git a/src/components/Datagrid/MoneyCell.tsx b/src/components/Datagrid/MoneyCell.tsx index 0272d2a4f..2cee560ad 100644 --- a/src/components/Datagrid/MoneyCell.tsx +++ b/src/components/Datagrid/MoneyCell.tsx @@ -6,7 +6,6 @@ import { } from "@glideapps/glide-data-grid"; import React from "react"; -import { Locale } from "../Locale"; import { usePriceField } from "../PriceField/usePriceField"; interface MoneyCellProps { @@ -46,33 +45,13 @@ const MoneyCellEdit: ReturnType> = ({ ); }; -const getFractionDigits = (locale: Locale, currency: string) => { - try { - const numberFormat = new Intl.NumberFormat(locale, { - style: "currency", - currency, - }); - - return numberFormat.resolvedOptions().maximumFractionDigits; - } catch (e) { - return 2; - } -}; - -export const moneyCellRenderer = ( - locale: Locale, -): CustomCellRenderer => ({ +export const moneyCellRenderer = (): CustomCellRenderer => ({ isMatch: (c): c is MoneyCell => (c.data as any).kind === "money-cell", draw: (args, cell) => { const { ctx, theme, rect } = args; const { currency, value } = cell.data; const hasValue = value === 0 ? true : !!value; - const currencyFractionDigits = getFractionDigits(locale, currency); - const formatted = - value?.toLocaleString(locale, { - maximumFractionDigits: currencyFractionDigits, - minimumFractionDigits: currencyFractionDigits, - }) ?? "-"; + const formatted = value?.toString() ?? "-"; ctx.fillStyle = theme.textDark; ctx.textAlign = "right"; diff --git a/src/components/Datagrid/useCells.ts b/src/components/Datagrid/useCells.ts index c395ec840..f6e4deade 100644 --- a/src/components/Datagrid/useCells.ts +++ b/src/components/Datagrid/useCells.ts @@ -10,7 +10,7 @@ function useCells() { const { locale } = useLocale(); const value = useMemo( () => [ - moneyCellRenderer(locale), + moneyCellRenderer(), numberCellRenderer(locale), dropdownCellRenderer, ], diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index 77f49e9e2..f876df089 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -23,6 +23,7 @@ import { ProductFragment } from "@saleor/graphql"; import useForm from "@saleor/hooks/useForm"; import useFormset from "@saleor/hooks/useFormset"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; +import useLocale from "@saleor/hooks/useLocale"; import { getAttributeInputFromProduct, getProductUpdatePageFormData, @@ -46,6 +47,7 @@ import { UseProductUpdateFormOpts, UseProductUpdateFormOutput, } from "./types"; +import { prepareVariantChangeData } from "./utils"; function useProductUpdateForm( product: ProductFragment, @@ -71,6 +73,7 @@ function useProductUpdateForm( data: formData, setIsSubmitDisabled, } = form; + const { locale } = useLocale(); const datagrid = useDatagridChangeState(); const variants = useRef({ @@ -78,10 +81,14 @@ function useProductUpdateForm( removed: [], updates: [], }); - const handleVariantChange = React.useCallback((data: DatagridChangeOpts) => { - variants.current = data; - triggerChange(); - }, []); + + const handleVariantChange = React.useCallback( + (data: DatagridChangeOpts) => { + variants.current = prepareVariantChangeData(data, locale, product); + triggerChange(); + }, + [locale, product, triggerChange], + ); const attributes = useFormset(getAttributeInputFromProduct(product)); const { diff --git a/src/products/components/ProductUpdatePage/utils.test.ts b/src/products/components/ProductUpdatePage/utils.test.ts new file mode 100644 index 000000000..9a1bc5609 --- /dev/null +++ b/src/products/components/ProductUpdatePage/utils.test.ts @@ -0,0 +1,67 @@ +import { Locale } from "@saleor/components/Locale"; + +import { parseCurrency } from "./utils"; + +describe("parseCurrency", () => { + it("rounds down to 3 decimals in 3 digit currency - EN locale (dot)", () => { + // Arrange + const value = "5.6777"; + const currency = "BHD"; + const locale = Locale.EN; + + // Act + const parsed = parseCurrency(value, locale, currency); + + // Assert + expect(parsed).toBe(5.677); + }); + + it("rounds down to 0 decimals in 0 digit currency - PL locale (comma)", () => { + // Arrange + const value = "12,90679"; + const currency = "JPY"; + const locale = Locale.PL; + + // Act + const parsed = parseCurrency(value, locale, currency); + + // Assert + expect(parsed).toBe(12); + }); + it("rounds down to 2 decimals in 2 digit currency - EN locale (dot)", () => { + // Arrange + const value = "244.98721"; + const currency = "PLN"; + const locale = Locale.EN; + + // Act + const parsed = parseCurrency(value, locale, currency); + + // Assert + expect(parsed).toBe(244.98); + }); + it("rounds down to 3 decimals in 3 digit currency - PL locale (comma)", () => { + // Arrange + const value = "0,27386"; + const currency = "TND"; + const locale = Locale.PL; + + // Act + const parsed = parseCurrency(value, locale, currency); + + // Assert + expect(parsed).toBe(0.273); + }); + it("rounds down correctly (floating point difficult case)", () => { + // Arrange + const value = "2.07"; + const currency = "EUR"; + const locale = Locale.PL; + + // Act + const parsed = parseCurrency(value, locale, currency); + + // Assert + expect(parsed).toBe(2.07); + }); +}); diff --git a/src/products/components/ProductUpdatePage/utils.ts b/src/products/components/ProductUpdatePage/utils.ts new file mode 100644 index 000000000..918d68e9a --- /dev/null +++ b/src/products/components/ProductUpdatePage/utils.ts @@ -0,0 +1,87 @@ +import { + DatagridChange, + DatagridChangeOpts, +} from "@saleor/components/Datagrid/useDatagridChange"; +import { Locale } from "@saleor/components/Locale"; +import { ProductFragment } from "@saleor/graphql"; + +const getFractionDigits = (locale: Locale, currency: string) => { + try { + const numberFormat = new Intl.NumberFormat(locale, { + style: "currency", + currency, + }); + + return numberFormat.resolvedOptions().maximumFractionDigits; + } catch (e) { + return 2; + } +}; + +export const parseCurrency = ( + value: string, + locale: Locale, + currency: string, +): number => { + // Thousand seperators are not allowedd + const number = value.replace(/,/, "."); + const fractionDigits = getFractionDigits(locale, currency); + const trimmedNumber = number.slice( + 0, + number.lastIndexOf(".") + 1 + fractionDigits, + ); + + return parseFloat(trimmedNumber); +}; + +export const prepareVariantChangeData = ( + data: DatagridChangeOpts, + locale: Locale, + product: ProductFragment, +): DatagridChangeOpts => { + data.updates = data.updates.map(update => { + if (isColumnChannelPrice(update.column)) { + return updateVaraintWithPriceFormat(update, locale, product); + } + return update; + }); + + return data; +}; + +function updateVaraintWithPriceFormat( + dataChange: DatagridChange, + locale: Locale, + product: ProductFragment, +) { + const channelId = dataChange.column.split(":")[1]; + const currencyCode = getChannelCurrencyCodeById( + channelId, + product.channelListings, + ); + + dataChange.data.value = parseCurrency( + `${dataChange.data.value}`, + locale, + currencyCode, + ); + + return dataChange; +} + +function isColumnChannelPrice(name: string) { + return name.includes("channel:"); +} + +function getChannelCurrencyCodeById( + channelId: string, + channelList: ProductFragment["channelListings"], +): string { + const channel = channelList.find(({ channel }) => channel.id === channelId); + + if (channel) { + return channel.channel.currencyCode; + } + + return ""; +}