feat: add short number formatting option to number field (#12613)

resolve #11927
Add a new 'Short Number' option that disables decimals and resets the
value to 0 when selected.


https://github.com/user-attachments/assets/d3524115-e3ec-4a07-9dbf-e19d03cf65dd



https://github.com/user-attachments/assets/2f2b46d1-06d9-4a92-8f37-0291d46accab

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Naifer
2025-06-16 10:48:18 +01:00
committed by GitHub
parent 929586e4a9
commit a44ba2065d
8 changed files with 67 additions and 26 deletions

View File

@ -1,6 +1,7 @@
import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay'; import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay';
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay'; import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number'; import { formatNumber } from '~/utils/format/number';
export const NumberFieldDisplay = () => { export const NumberFieldDisplay = () => {
@ -13,7 +14,9 @@ export const NumberFieldDisplay = () => {
const value = const value =
type === 'percentage' type === 'percentage'
? `${formatNumber(Number(fieldValue) * 100, decimals)}%` ? `${formatNumber(Number(fieldValue) * 100, decimals)}%`
: formatNumber(Number(fieldValue), decimals); : type === 'shortNumber'
? formatAmount(Number(fieldValue))
: formatNumber(Number(fieldValue), decimals);
return <NumberDisplay value={value} decimals={decimals} />; return <NumberDisplay value={value} decimals={decimals} />;
}; };

View File

@ -56,7 +56,12 @@ export type FieldDateMetadata = BaseFieldMetadata & {
settings?: FieldDateMetadataSettings; settings?: FieldDateMetadataSettings;
}; };
export type FieldNumberVariant = 'number' | 'percentage'; export const FIELD_NUMBER_VARIANT = [
'number',
'percentage',
'shortNumber',
] as const;
export type FieldNumberVariant = (typeof FIELD_NUMBER_VARIANT)[number];
export type FieldNumberMetadata = BaseFieldMetadata & { export type FieldNumberMetadata = BaseFieldMetadata & {
placeHolder: string; placeHolder: string;

View File

@ -1,6 +1,7 @@
import { FIELD_NUMBER_VARIANT } from '@/object-record/record-field/types/FieldMetadata';
import { z } from 'zod'; import { z } from 'zod';
export const numberFieldDefaultValueSchema = z.object({ export const numberFieldDefaultValueSchema = z.object({
decimals: z.number().nullable(), decimals: z.number().nullable(),
type: z.enum(['percentage', 'number']).nullable(), type: z.enum(FIELD_NUMBER_VARIANT).nullable(),
}); });

View File

@ -6,11 +6,11 @@ import { numberFieldDefaultValueSchema } from '@/object-record/record-field/vali
import { Separator } from '@/settings/components/Separator'; import { Separator } from '@/settings/components/Separator';
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter'; import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { NUMBER_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { IconDecimal, IconEye } from 'twenty-ui/display'; import { IconDecimal, IconEye } from 'twenty-ui/display';
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number'; import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number';
import { NUMBER_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions';
export const settingsDataModelFieldNumberFormSchema = z.object({ export const settingsDataModelFieldNumberFormSchema = z.object({
settings: numberFieldDefaultValueSchema, settings: numberFieldDefaultValueSchema,
@ -60,7 +60,13 @@ export const SettingsDataModelFieldNumberForm = ({
dropdownId="number-type" dropdownId="number-type"
dropdownWidth={120} dropdownWidth={120}
value={type} value={type}
onChange={(value) => onChange({ type: value, decimals: count })} onChange={(value) =>
onChange({
type: value,
decimals:
value === 'shortNumber' ? DEFAULT_DECIMAL_VALUE : count,
})
}
disabled={disabled} disabled={disabled}
needIconCheck={false} needIconCheck={false}
options={NUMBER_DATA_MODEL_SELECT_OPTIONS.map((option) => ({ options={NUMBER_DATA_MODEL_SELECT_OPTIONS.map((option) => ({
@ -70,16 +76,18 @@ export const SettingsDataModelFieldNumberForm = ({
/> />
</SettingsOptionCardContentSelect> </SettingsOptionCardContentSelect>
<Separator /> <Separator />
<SettingsOptionCardContentCounter {type !== 'shortNumber' && (
Icon={IconDecimal} <SettingsOptionCardContentCounter
title={t`Number of decimals`} Icon={IconDecimal}
description={`E.g. ${(type === 'percentage' ? 99 : 1000).toFixed(count)}${type === 'percentage' ? '%' : ''} for ${count} decimal${count > 1 ? 's' : ''}`} title={t`Number of decimals`}
value={count} description={`E.g. ${(type === 'percentage' ? 99 : 1000).toFixed(count)}${type === 'percentage' ? '%' : ''} for ${count} decimal${count > 1 ? 's' : ''}`}
onChange={(value) => onChange({ type: type, decimals: value })} value={count}
disabled={disabled} onChange={(value) => onChange({ type: type, decimals: value })}
minValue={0} disabled={disabled}
maxValue={100} // needs to be changed minValue={0}
/> maxValue={100} // needs to be changed
/>
)}
</> </>
); );
}} }}

View File

@ -1,15 +1,36 @@
import { FieldNumberVariant } from '@/object-record/record-field/types/FieldMetadata';
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { IconNumber9, IconPercentage } from 'twenty-ui/display'; import { ForwardRefExoticComponent, RefAttributes } from 'react';
import {
IconComponent,
IconComponentProps,
IconLetterK,
IconNumber9,
IconPercentage,
} from 'twenty-ui/display';
type NumberDataModelSelectOptions = {
Icon: ForwardRefExoticComponent<
IconComponentProps & RefAttributes<IconComponent>
>;
label: MessageDescriptor;
value: FieldNumberVariant;
};
export const NUMBER_DATA_MODEL_SELECT_OPTIONS = [ export const NUMBER_DATA_MODEL_SELECT_OPTIONS = [
{ {
Icon: IconNumber9, Icon: IconNumber9,
label: msg`Number`, label: msg`Number`,
value: 'number', value: 'number',
}, },
{
Icon: IconLetterK,
label: msg`Short`,
value: 'shortNumber',
},
{ {
Icon: IconPercentage, Icon: IconPercentage,
label: msg`Percentage`, label: msg`Percentage`,
value: 'percentage', value: 'percentage',
}, },
]; ] as const satisfies Array<NumberDataModelSelectOptions>;

View File

@ -25,6 +25,7 @@ import {
enum ValueType { enum ValueType {
PERCENTAGE = 'percentage', PERCENTAGE = 'percentage',
NUMBER = 'number', NUMBER = 'number',
SHORT_NUMBER = 'shortNumber',
} }
class NumberSettingsValidation { class NumberSettingsValidation {
@ -35,7 +36,7 @@ class NumberSettingsValidation {
@IsOptional() @IsOptional()
@IsEnum(ValueType) @IsEnum(ValueType)
type?: 'percentage' | 'number'; type?: 'percentage' | 'number' | 'shortNumber';
} }
class TextSettingsValidation { class TextSettingsValidation {

View File

@ -4,18 +4,18 @@ export {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconApps,
IconAppWindow, IconAppWindow,
IconApps,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconArrowUp, IconArrowUp,
IconArrowUpRight, IconArrowUpRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconAt, IconAt,
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconBell, IconBell,
@ -47,8 +47,8 @@ export {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconChevronsRight,
IconChevronUp, IconChevronUp,
IconChevronsRight,
IconCircleDot, IconCircleDot,
IconCircleOff, IconCircleOff,
IconCirclePlus, IconCirclePlus,
@ -186,6 +186,7 @@ export {
IconLayoutSidebarRight, IconLayoutSidebarRight,
IconLayoutSidebarRightCollapse, IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand, IconLayoutSidebarRightExpand,
IconLetterK,
IconLibraryPlus, IconLibraryPlus,
IconLifebuoy, IconLifebuoy,
IconLink, IconLink,

View File

@ -65,18 +65,18 @@ export {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconApps,
IconAppWindow, IconAppWindow,
IconApps,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconArrowUp, IconArrowUp,
IconArrowUpRight, IconArrowUpRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconAt, IconAt,
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconBell, IconBell,
@ -108,8 +108,8 @@ export {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconChevronsRight,
IconChevronUp, IconChevronUp,
IconChevronsRight,
IconCircleDot, IconCircleDot,
IconCircleOff, IconCircleOff,
IconCirclePlus, IconCirclePlus,
@ -247,6 +247,7 @@ export {
IconLayoutSidebarRight, IconLayoutSidebarRight,
IconLayoutSidebarRightCollapse, IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand, IconLayoutSidebarRightExpand,
IconLetterK,
IconLibraryPlus, IconLibraryPlus,
IconLifebuoy, IconLifebuoy,
IconLink, IconLink,