feat: ability to switch currency format (#12542)

Fixes #11927

I have added 'format' in the zod schema of currency, and for using it, I
am separately passing 'format' to 'currencyDisplay.'
The feature is working correctly.

---------

Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Mohit Agrawal
2025-06-24 13:58:50 +05:30
committed by GitHub
parent 6651abae18
commit d0126e22ee
13 changed files with 122 additions and 33 deletions

View File

@ -1,9 +1,9 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros'; import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros';
import { isDefined } from 'twenty-shared/utils';
export const useExportProcessRecordsForCSV = (objectNameSingular: string) => { export const useExportProcessRecordsForCSV = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({

View File

@ -2,7 +2,12 @@ import { useCurrencyFieldDisplay } from '@/object-record/record-field/meta-types
import { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay'; import { CurrencyDisplay } from '@/ui/field/display/components/CurrencyDisplay';
export const CurrencyFieldDisplay = () => { export const CurrencyFieldDisplay = () => {
const { fieldValue } = useCurrencyFieldDisplay(); const { fieldValue, fieldDefinition } = useCurrencyFieldDisplay();
return <CurrencyDisplay currencyValue={fieldValue} />; return (
<CurrencyDisplay
currencyValue={fieldValue}
fieldDefinition={fieldDefinition}
/>
);
}; };

View File

@ -2,12 +2,21 @@ import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '../../contexts/FieldContext'; import { FieldContext } from '../../contexts/FieldContext';
import { FieldCurrencyValue } from '../../types/FieldMetadata'; import { FieldCurrencyValue } from '../../types/FieldMetadata';
export const useCurrencyFieldDisplay = () => { export const useCurrencyFieldDisplay = () => {
const { recordId, fieldDefinition } = useContext(FieldContext); const { recordId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.CURRENCY,
isFieldCurrency,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName; const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldCurrencyValue | undefined>( const fieldValue = useRecordFieldValue<FieldCurrencyValue | undefined>(

View File

@ -84,7 +84,9 @@ export type FieldLinksMetadata = BaseFieldMetadata & {
export type FieldCurrencyMetadata = BaseFieldMetadata & { export type FieldCurrencyMetadata = BaseFieldMetadata & {
placeHolder: string; placeHolder: string;
isPositive?: boolean; isPositive?: boolean;
settings?: null; settings?: {
format: FieldCurrencyFormat | null;
};
}; };
export type FieldFullNameMetadata = BaseFieldMetadata & { export type FieldFullNameMetadata = BaseFieldMetadata & {
@ -211,6 +213,9 @@ export type FieldLinksValue = {
primaryLinkUrl: string | null; primaryLinkUrl: string | null;
secondaryLinks?: { label: string | null; url: string | null }[] | null; secondaryLinks?: { label: string | null; url: string | null }[] | null;
}; };
export const fieldMetadataCurrencyFormat = ['short', 'full'] as const;
export type FieldCurrencyFormat = (typeof fieldMetadataCurrencyFormat)[number];
export type FieldCurrencyValue = { export type FieldCurrencyValue = {
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
amountMicros: number | null; amountMicros: number | null;

View File

@ -0,0 +1,6 @@
import { fieldMetadataCurrencyFormat } from '@/object-record/record-field/types/FieldMetadata';
import { z } from 'zod';
export const currencyFieldSettingsSchema = z.object({
format: z.enum(fieldMetadataCurrencyFormat),
});

View File

@ -1,9 +1,9 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros'; import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros';
import { isDefined } from 'twenty-shared/utils';
export const useExportProcessRecordsForCSV = (objectNameSingular: string) => { export const useExportProcessRecordsForCSV = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({

View File

@ -13,22 +13,22 @@ import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Section } from '@react-email/components'; import { Section } from '@react-email/components';
import { useState } from 'react'; import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import { H2Title, IconSearch } from 'twenty-ui/display';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SettingsDataModelFieldTypeFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'; import { SettingsDataModelFieldTypeFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { t } from '@lingui/core/macro';
import { H2Title, IconSearch } from 'twenty-ui/display';
import { UndecoratedLink } from 'twenty-ui/navigation';
type SettingsObjectNewFieldSelectorProps = { type SettingsObjectNewFieldSelectorProps = {
className?: string; className?: string;
excludedFieldTypes?: FieldType[]; excludedFieldTypes?: FieldType[];
fieldMetadataItem?: Pick< fieldMetadataItem?: Pick<
FieldMetadataItem, FieldMetadataItem,
'defaultValue' | 'options' | 'type' 'defaultValue' | 'options' | 'type' | 'settings'
>; >;
objectNamePlural: string; objectNamePlural: string;

View File

@ -2,17 +2,20 @@ import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldCurrencyFormat } from '@/object-record/record-field/types/FieldMetadata';
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema'; import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
import { currencyFieldSettingsSchema } from '@/object-record/record-field/validation-schemas/currencyFieldSettingsSchema';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
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 { IconCurrencyDollar } from 'twenty-ui/display'; import { IconCheckbox, IconCurrencyDollar } from 'twenty-ui/display';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
export const settingsDataModelFieldCurrencyFormSchema = z.object({ export const settingsDataModelFieldCurrencyFormSchema = z.object({
defaultValue: currencyFieldDefaultValueSchema, defaultValue: currencyFieldDefaultValueSchema,
settings: currencyFieldSettingsSchema,
}); });
export type SettingsDataModelFieldCurrencyFormValues = z.infer< export type SettingsDataModelFieldCurrencyFormValues = z.infer<
@ -21,7 +24,10 @@ export type SettingsDataModelFieldCurrencyFormValues = z.infer<
type SettingsDataModelFieldCurrencyFormProps = { type SettingsDataModelFieldCurrencyFormProps = {
disabled?: boolean; disabled?: boolean;
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue'>; fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
>;
}; };
export const SettingsDataModelFieldCurrencyForm = ({ export const SettingsDataModelFieldCurrencyForm = ({
@ -29,12 +35,16 @@ export const SettingsDataModelFieldCurrencyForm = ({
fieldMetadataItem, fieldMetadataItem,
}: SettingsDataModelFieldCurrencyFormProps) => { }: SettingsDataModelFieldCurrencyFormProps) => {
const { t } = useLingui(); const { t } = useLingui();
const {
initialAmountMicrosValue,
initialCurrencyCodeValue,
initialSettingsValue,
} = useCurrencySettingsFormInitialValues({
fieldMetadataItem,
});
const { control } = const { control } =
useFormContext<SettingsDataModelFieldCurrencyFormValues>(); useFormContext<SettingsDataModelFieldCurrencyFormValues>();
const { initialAmountMicrosValue, initialCurrencyCodeValue } =
useCurrencySettingsFormInitialValues({ fieldMetadataItem });
return ( return (
<> <>
<Controller <Controller
@ -69,6 +79,32 @@ export const SettingsDataModelFieldCurrencyForm = ({
</SettingsOptionCardContentSelect> </SettingsOptionCardContentSelect>
)} )}
/> />
<Controller
name="settings.format"
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>
)}
/>
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
@ -17,7 +17,7 @@ type SettingsDataModelFieldCurrencySettingsFormCardProps = {
disabled?: boolean; disabled?: boolean;
fieldMetadataItem: Pick< fieldMetadataItem: Pick<
FieldMetadataItem, FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue' 'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
>; >;
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>; } & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
@ -31,9 +31,10 @@ export const SettingsDataModelFieldCurrencySettingsFormCard = ({
fieldMetadataItem, fieldMetadataItem,
objectMetadataItem, objectMetadataItem,
}: SettingsDataModelFieldCurrencySettingsFormCardProps) => { }: SettingsDataModelFieldCurrencySettingsFormCardProps) => {
const { initialDefaultValue } = useCurrencySettingsFormInitialValues({ const { initialDefaultValue, initialSettingsValue } =
fieldMetadataItem, useCurrencySettingsFormInitialValues({
}); fieldMetadataItem,
});
const { watch: watchFormValue } = const { watch: watchFormValue } =
useFormContext<SettingsDataModelFieldCurrencyFormValues>(); useFormContext<SettingsDataModelFieldCurrencyFormValues>();
@ -45,6 +46,7 @@ export const SettingsDataModelFieldCurrencySettingsFormCard = ({
fieldMetadataItem={{ fieldMetadataItem={{
...fieldMetadataItem, ...fieldMetadataItem,
defaultValue: watchFormValue('defaultValue', initialDefaultValue), defaultValue: watchFormValue('defaultValue', initialDefaultValue),
settings: watchFormValue('settings', initialSettingsValue),
}} }}
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
/> />

View File

@ -5,31 +5,42 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { SettingsDataModelFieldCurrencyFormValues } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm'; import { SettingsDataModelFieldCurrencyFormValues } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
type UseCurrencySettingsFormInitialValuesArgs = {
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'settings'>;
};
export const useCurrencySettingsFormInitialValues = ({ export const useCurrencySettingsFormInitialValues = ({
fieldMetadataItem, fieldMetadataItem,
}: { }: UseCurrencySettingsFormInitialValuesArgs) => {
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
}) => {
const initialAmountMicrosValue = const initialAmountMicrosValue =
(fieldMetadataItem?.defaultValue?.amountMicros as number | null) ?? null; (fieldMetadataItem?.defaultValue?.amountMicros as number | null) ?? null;
const initialCurrencyCodeValue = const initialCurrencyCodeValue =
fieldMetadataItem?.defaultValue?.currencyCode ?? fieldMetadataItem?.defaultValue?.currencyCode ??
applySimpleQuotesToString(CurrencyCode.USD); applySimpleQuotesToString(CurrencyCode.USD);
const initialDefaultValue = { const initialFormValues: SettingsDataModelFieldCurrencyFormValues = {
amountMicros: initialAmountMicrosValue, settings: {
currencyCode: initialCurrencyCodeValue, format: fieldMetadataItem?.settings?.format ?? 'short',
},
defaultValue: {
amountMicros: initialAmountMicrosValue,
currencyCode: initialCurrencyCodeValue,
},
}; };
const { resetField } = const { resetField } =
useFormContext<SettingsDataModelFieldCurrencyFormValues>(); useFormContext<SettingsDataModelFieldCurrencyFormValues>();
const resetDefaultValueField = () => const resetDefaultValueField = () => {
resetField('defaultValue', { defaultValue: initialDefaultValue }); resetField('defaultValue', {
defaultValue: initialFormValues.defaultValue,
});
resetField('settings', { defaultValue: initialFormValues.settings });
};
return { return {
initialAmountMicrosValue, initialAmountMicrosValue,
initialCurrencyCodeValue, initialCurrencyCodeValue,
initialDefaultValue, initialSettingsValue: initialFormValues.settings,
initialDefaultValue: initialFormValues.defaultValue,
resetDefaultValueField, resetDefaultValueField,
}; };
}; };

View File

@ -11,7 +11,7 @@ export const getCurrencyFieldPreviewValue = ({
}: { }: {
fieldMetadataItem: Pick< fieldMetadataItem: Pick<
FieldMetadataItem, FieldMetadataItem,
'defaultValue' | 'options' | 'type' 'defaultValue' | 'options' | 'type' | 'settings'
>; >;
}): FieldCurrencyValue | null => { }): FieldCurrencyValue | null => {
if (fieldMetadataItem.type !== FieldMetadataType.CURRENCY) return null; if (fieldMetadataItem.type !== FieldMetadataType.CURRENCY) return null;

View File

@ -1,17 +1,26 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldCurrencyMetadata,
FieldCurrencyValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { formatAmount } from '~/utils/format/formatAmount'; import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type CurrencyDisplayProps = { type CurrencyDisplayProps = {
currencyValue: FieldCurrencyValue | null | undefined; currencyValue: FieldCurrencyValue | null | undefined;
fieldDefinition: FieldDefinition<FieldCurrencyMetadata>;
}; };
export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => { export const CurrencyDisplay = ({
currencyValue,
fieldDefinition,
}: CurrencyDisplayProps) => {
const theme = useTheme(); const theme = useTheme();
const shouldDisplayCurrency = isDefined(currencyValue?.currencyCode); const shouldDisplayCurrency = isDefined(currencyValue?.currencyCode);
@ -24,6 +33,8 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
? null ? null
: currencyValue?.amountMicros / 1000000; : currencyValue?.amountMicros / 1000000;
const format = fieldDefinition.metadata.settings?.format;
if (!shouldDisplayCurrency) { if (!shouldDisplayCurrency) {
return <EllipsisDisplay>{0}</EllipsisDisplay>; return <EllipsisDisplay>{0}</EllipsisDisplay>;
} }
@ -39,7 +50,11 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
/>{' '} />{' '}
</> </>
)} )}
{amountToDisplay !== null ? formatAmount(amountToDisplay) : ''} {amountToDisplay !== null
? !isDefined(format) || format === 'short'
? formatAmount(amountToDisplay)
: formatNumber(amountToDisplay)
: null}
</EllipsisDisplay> </EllipsisDisplay>
); );
}; };

View File

@ -1,5 +1,5 @@
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68'; const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68';