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:
Abdullah.
2025-12-26 22:53:43 +05:00
committed by GitHub
parent 28dc3e470e
commit c020f71ba1
8 changed files with 90 additions and 27 deletions
@@ -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}
/>
)}
</>
);
}}
/>
</>
);
@@ -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,