Add "number of decimal" on currency field. (#16439)
Closes [571](https://github.com/twentyhq/core-team-issues/issues/571). Replicates the behavior of number field and allows specifying the number of decimals for currency field. <img width="931" height="858" alt="image" src="https://github.com/user-attachments/assets/f5100d58-b1b0-4a88-a090-e98b2feeebd0" /> Currencies around the world have a maximum of three decimal places - BHD, KWD etc. However, I have added a maximum of five decimal places in case someone wants to use the currency field type for displaying things like `per-second-billing` or `exchange rates`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds configurable decimals (0–5) for currency fields, updating settings UI, types/schema, display/aggregate formatting, and input precision. > > - **Currency field settings**: > - Add `decimals` (0–5) to `FieldCurrencyMetadata.settings` and validate via `currencyFieldSettingsSchema`. > - Update `SettingsDataModelFieldCurrencyForm` to manage `format` (short/full) and `decimals` with counter when `full`; wire defaults via `useCurrencySettingsFormInitialValues`. > - **Display & aggregation**: > - `CurrencyDisplay` and `transformAggregateRawValueIntoAggregateDisplayValue` honor `decimals` when `format` is `full`; keep short format using `formatToShortNumber`. > - **Input**: > - Increase currency `IMaskInput` precision with `scale=5`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 959a8fc1ea726d206548ea32cada28d77c1c6eb9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+70
-25
@@ -1,15 +1,26 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldCurrencyFormat } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import {
|
||||
fieldMetadataCurrencyFormat,
|
||||
type FieldCurrencyFormat,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema';
|
||||
import { currencyFieldSettingsSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldSettingsSchema';
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
|
||||
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconCheckbox, IconCurrencyDollar } from 'twenty-ui/display';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconCurrencyDollar,
|
||||
IconDecimal,
|
||||
} from 'twenty-ui/display';
|
||||
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/formatNumber';
|
||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||
|
||||
export const settingsDataModelFieldCurrencyFormSchema = z.object({
|
||||
@@ -75,31 +86,65 @@ export const SettingsDataModelFieldCurrencyForm = ({
|
||||
</SettingsOptionCardContentSelect>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<Controller
|
||||
name="settings.format"
|
||||
name="settings"
|
||||
control={control}
|
||||
defaultValue={initialSettingsValue.format}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconCheckbox}
|
||||
title={t`Format`}
|
||||
description={t`Choose between Short and Full`}
|
||||
>
|
||||
<Select<FieldCurrencyFormat>
|
||||
dropdownWidth={140}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-format-select"
|
||||
options={[
|
||||
{ label: 'Short', value: 'short' },
|
||||
{ label: 'Full', value: 'full' },
|
||||
]}
|
||||
selectSizeVariant="small"
|
||||
withSearchInput={false}
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
)}
|
||||
defaultValue={initialSettingsValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const format = value?.format ?? fieldMetadataCurrencyFormat[0];
|
||||
const decimals = value?.decimals ?? DEFAULT_DECIMAL_VALUE;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconCheckbox}
|
||||
title={t`Format`}
|
||||
description={t`Choose between Short and Full`}
|
||||
>
|
||||
<Select<FieldCurrencyFormat>
|
||||
dropdownWidth={140}
|
||||
value={format}
|
||||
onChange={(newFormat) =>
|
||||
onChange({
|
||||
format: newFormat,
|
||||
decimals:
|
||||
newFormat === fieldMetadataCurrencyFormat[0]
|
||||
? DEFAULT_DECIMAL_VALUE
|
||||
: decimals,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-format-select"
|
||||
options={[
|
||||
{ label: 'Short', value: fieldMetadataCurrencyFormat[0] },
|
||||
{ label: 'Full', value: fieldMetadataCurrencyFormat[1] },
|
||||
]}
|
||||
selectSizeVariant="small"
|
||||
withSearchInput={false}
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
<Separator />
|
||||
{format === 'full' && (
|
||||
<SettingsOptionCardContentCounter
|
||||
Icon={IconDecimal}
|
||||
title={t`Number of decimals`}
|
||||
description={plural(decimals, {
|
||||
one: `E.g. ${(1000).toFixed(decimals)} for ${decimals} decimal`,
|
||||
other: `E.g. ${(1000).toFixed(decimals)} for ${decimals} decimals`,
|
||||
})}
|
||||
value={decimals}
|
||||
onChange={(newDecimals: number) =>
|
||||
onChange({ format, decimals: newDecimals })
|
||||
}
|
||||
disabled={disabled}
|
||||
minValue={0}
|
||||
maxValue={5}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada
|
||||
import { type SettingsDataModelFieldCurrencyFormValues } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { CurrencyCode } from 'twenty-shared/constants';
|
||||
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/formatNumber';
|
||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
@@ -28,6 +29,7 @@ export const useCurrencySettingsFormInitialValues = ({
|
||||
const initialFormValues: SettingsDataModelFieldCurrencyFormValues = {
|
||||
settings: {
|
||||
format: fieldMetadataItem?.settings?.format ?? 'short',
|
||||
decimals: fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
|
||||
},
|
||||
defaultValue: {
|
||||
amountMicros: initialAmountMicrosValue,
|
||||
|
||||
Reference in New Issue
Block a user