Use defaultValue in currency input (#4911)

- Fix default value sent to backend, using single quotes by default
- Use default value in field definition and column definition so that
field inputs can access it
- Used currency default value in CurrencyFieldInput

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-04-11 16:49:00 +02:00
committed by GitHub
parent e48960afbe
commit c69a3f01da
17 changed files with 188 additions and 103 deletions

View File

@ -1,6 +1,7 @@
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { FieldMetadataOption } from '@/object-metadata/types/FieldMetadataOption.ts'; import { FieldMetadataOption } from '@/object-metadata/types/FieldMetadataOption.ts';
import { getDefaultValueForBackend } from '@/object-metadata/utils/getDefaultValueForBackend';
import { Field } from '~/generated/graphql'; import { Field } from '~/generated/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -24,11 +25,11 @@ export const useFieldMetadataItem = () => {
}, },
) => { ) => {
const formattedInput = formatFieldMetadataItemInput(input); const formattedInput = formatFieldMetadataItemInput(input);
const defaultValue = input.defaultValue
? typeof input.defaultValue == 'string' const defaultValue = getDefaultValueForBackend(
? `'${input.defaultValue}'` input.defaultValue ?? formattedInput.defaultValue,
: input.defaultValue input.type,
: formattedInput.defaultValue ?? undefined; );
return createOneFieldMetadataItem({ return createOneFieldMetadataItem({
...formattedInput, ...formattedInput,

View File

@ -43,5 +43,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
options: field.options, options: field.options,
}, },
iconName: field.icon ?? 'Icon123', iconName: field.icon ?? 'Icon123',
defaultValue: field.defaultValue,
}; };
}; };

View File

@ -0,0 +1,19 @@
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getDefaultValueForBackend = (
defaultValue: any,
fieldMetadataType: FieldMetadataType,
) => {
if (fieldMetadataType === FieldMetadataType.Currency) {
const currencyDefaultValue = defaultValue as FieldCurrencyValue;
return {
amountMicros: currencyDefaultValue.amountMicros,
currencyCode: `'${currencyDefaultValue.currencyCode}'` as any,
} satisfies FieldCurrencyValue;
} else if (typeof defaultValue === 'string') {
return `'${defaultValue}'`;
}
return defaultValue;
};

View File

@ -264,6 +264,7 @@ export const RecordBoardCard = () => {
iconName: fieldDefinition.iconName, iconName: fieldDefinition.iconName,
type: fieldDefinition.type, type: fieldDefinition.type,
metadata: fieldDefinition.metadata, metadata: fieldDefinition.metadata,
defaultValue: fieldDefinition.defaultValue,
}, },
useUpdateRecord: useUpdateOneRecordHook, useUpdateRecord: useUpdateOneRecordHook,
hotkeyScope: InlineCellHotkeyScope.InlineCell, hotkeyScope: InlineCellHotkeyScope.InlineCell,

View File

@ -64,6 +64,8 @@ export const useCurrencyField = () => {
const draftValue = useRecoilValue(getDraftValueSelector()); const draftValue = useRecoilValue(getDraftValueSelector());
const defaultValue = fieldDefinition.defaultValue;
return { return {
fieldDefinition, fieldDefinition,
fieldValue, fieldValue,
@ -72,5 +74,6 @@ export const useCurrencyField = () => {
setFieldValue, setFieldValue,
hotkeyScope, hotkeyScope,
persistCurrencyField, persistCurrencyField,
defaultValue,
}; };
}; };

View File

@ -1,4 +1,5 @@
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
@ -21,14 +22,27 @@ export const CurrencyFieldInput = ({
onTab, onTab,
onShiftTab, onShiftTab,
}: CurrencyFieldInputProps) => { }: CurrencyFieldInputProps) => {
const { hotkeyScope, draftValue, persistCurrencyField, setDraftValue } = const {
useCurrencyField(); hotkeyScope,
draftValue,
persistCurrencyField,
setDraftValue,
defaultValue,
} = useCurrencyField();
const currencyCode =
draftValue?.currencyCode ??
((defaultValue as FieldCurrencyValue).currencyCode.replace(
/'/g,
'',
) as CurrencyCode) ??
CurrencyCode.USD;
const handleEnter = (newValue: string) => { const handleEnter = (newValue: string) => {
onEnter?.(() => { onEnter?.(() => {
persistCurrencyField({ persistCurrencyField({
amountText: newValue, amountText: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}); });
}); });
}; };
@ -37,7 +51,7 @@ export const CurrencyFieldInput = ({
onEscape?.(() => { onEscape?.(() => {
persistCurrencyField({ persistCurrencyField({
amountText: newValue, amountText: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}); });
}); });
}; };
@ -49,7 +63,7 @@ export const CurrencyFieldInput = ({
onClickOutside?.(() => { onClickOutside?.(() => {
persistCurrencyField({ persistCurrencyField({
amountText: newValue, amountText: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}); });
}); });
}; };
@ -58,7 +72,7 @@ export const CurrencyFieldInput = ({
onTab?.(() => { onTab?.(() => {
persistCurrencyField({ persistCurrencyField({
amountText: newValue, amountText: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}); });
}); });
}; };
@ -67,7 +81,7 @@ export const CurrencyFieldInput = ({
onShiftTab?.(() => onShiftTab?.(() =>
persistCurrencyField({ persistCurrencyField({
amountText: newValue, amountText: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}), }),
); );
}; };
@ -75,7 +89,7 @@ export const CurrencyFieldInput = ({
const handleChange = (newValue: string) => { const handleChange = (newValue: string) => {
setDraftValue({ setDraftValue({
amount: newValue, amount: newValue,
currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, currencyCode,
}); });
}; };
@ -90,7 +104,7 @@ export const CurrencyFieldInput = ({
<FieldInputOverlay> <FieldInputOverlay>
<CurrencyInput <CurrencyInput
value={draftValue?.amount?.toString() ?? ''} value={draftValue?.amount?.toString() ?? ''}
currencyCode={draftValue?.currencyCode ?? CurrencyCode.USD} currencyCode={currencyCode}
autoFocus autoFocus
placeholder="Currency" placeholder="Currency"
onClickOutside={handleClickOutside} onClickOutside={handleClickOutside}

View File

@ -23,4 +23,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
type: FieldMetadataType; type: FieldMetadataType;
metadata: T; metadata: T;
infoTooltipContent?: string; infoTooltipContent?: string;
defaultValue: any;
}; };

View File

@ -155,6 +155,8 @@ export const RecordShowContainer = ({
labelIdentifierFieldMetadataItem?.name || '', labelIdentifierFieldMetadataItem?.name || '',
objectMetadataNameSingular: objectNameSingular, objectMetadataNameSingular: objectNameSingular,
}, },
defaultValue:
labelIdentifierFieldMetadataItem?.defaultValue,
}, },
useUpdateRecord: useUpdateOneObjectRecordMutation, useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell, hotkeyScope: InlineCellHotkeyScope.InlineCell,

View File

@ -15,7 +15,10 @@ import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export type SettingsDataModelFieldPreviewProps = { export type SettingsDataModelFieldPreviewProps = {
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> & { fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue'
> & {
id?: string; id?: string;
name?: string; name?: string;
}; };
@ -106,6 +109,7 @@ export const SettingsDataModelFieldPreview = ({
relationObjectMetadataItem?.nameSingular, relationObjectMetadataItem?.nameSingular,
options: selectOptions, options: selectOptions,
}, },
defaultValue: fieldMetadataItem.defaultValue,
}, },
hotkeyScope: 'field-preview', hotkeyScope: 'field-preview',
}} }}

View File

@ -20,6 +20,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconLink', iconName: 'IconLink',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 1, position: 1,
@ -36,6 +37,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconUsers', iconName: 'IconUsers',
isVisible: true, isVisible: true,
defaultValue: 0,
}, },
{ {
position: 2, position: 2,
@ -52,6 +54,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconBuildingSkyscraper', iconName: 'IconBuildingSkyscraper',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 3, position: 3,
@ -69,6 +72,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconHeart', iconName: 'IconHeart',
isVisible: true, isVisible: true,
defaultValue: [],
}, },
{ {
position: 4, position: 4,
@ -85,6 +89,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconMap', iconName: 'IconMap',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 5, position: 5,
@ -102,6 +107,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconUserCircle', iconName: 'IconUserCircle',
isVisible: true, isVisible: true,
defaultValue: null,
}, },
{ {
position: 6, position: 6,
@ -119,6 +125,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconUsers', iconName: 'IconUsers',
isVisible: true, isVisible: true,
defaultValue: [],
}, },
{ {
position: 7, position: 7,
@ -136,6 +143,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconFileImport', iconName: 'IconFileImport',
isVisible: true, isVisible: true,
defaultValue: [],
}, },
{ {
position: 8, position: 8,
@ -152,6 +160,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconCalendar', iconName: 'IconCalendar',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 9, position: 9,
@ -168,6 +177,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconTarget', iconName: 'IconTarget',
isVisible: true, isVisible: true,
defaultValue: false,
}, },
{ {
position: 10, position: 10,
@ -184,6 +194,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconBrandLinkedin', iconName: 'IconBrandLinkedin',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 11, position: 11,
@ -201,6 +212,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconTargetArrow', iconName: 'IconTargetArrow',
isVisible: true, isVisible: true,
defaultValue: [],
}, },
{ {
position: 12, position: 12,
@ -217,6 +229,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconBrandX', iconName: 'IconBrandX',
isVisible: true, isVisible: true,
defaultValue: '',
}, },
{ {
position: 13, position: 13,
@ -234,6 +247,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconCheckbox', iconName: 'IconCheckbox',
isVisible: true, isVisible: true,
defaultValue: [],
}, },
{ {
position: 14, position: 14,
@ -250,6 +264,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
}, },
iconName: 'IconMoneybag', iconName: 'IconMoneybag',
isVisible: true, isVisible: true,
defaultValue: 0,
}, },
] satisfies ColumnDefinition<FieldMetadata>[] ] satisfies ColumnDefinition<FieldMetadata>[]
).filter(filterAvailableTableColumns); ).filter(filterAvailableTableColumns);

View File

@ -7,7 +7,6 @@ import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/S
import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton'; import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton';
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle'; import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
import { isDefined } from '~/utils/isDefined';
export const StyledInput = styled.input` export const StyledInput = styled.input`
margin: 0; margin: 0;
@ -77,9 +76,6 @@ export const CurrencyInput = ({
const theme = useTheme(); const theme = useTheme();
const [internalText, setInternalText] = useState(value); const [internalText, setInternalText] = useState(value);
const [internalCurrency, setInternalCurrency] = useState<Currency | null>(
null,
);
const wrapperRef = useRef<HTMLInputElement>(null); const wrapperRef = useRef<HTMLInputElement>(null);
@ -89,7 +85,6 @@ export const CurrencyInput = ({
}; };
const handleCurrencyChange = (currency: Currency) => { const handleCurrencyChange = (currency: Currency) => {
setInternalCurrency(currency);
onSelect?.(currency.value); onSelect?.(currency.value);
}; };
@ -116,23 +111,18 @@ export const CurrencyInput = ({
[], [],
); );
useEffect(() => { const currency = currencies.find(({ value }) => value === currencyCode);
const currency = currencies.find(({ value }) => value === currencyCode);
if (isDefined(currency)) {
setInternalCurrency(currency);
}
}, [currencies, currencyCode]);
useEffect(() => { useEffect(() => {
setInternalText(value); setInternalText(value);
}, [value]); }, [value]);
const Icon: IconComponent = internalCurrency?.Icon; const Icon: IconComponent = currency?.Icon;
return ( return (
<StyledContainer ref={wrapperRef}> <StyledContainer ref={wrapperRef}>
<CurrencyPickerDropdownButton <CurrencyPickerDropdownButton
valueCode={internalCurrency?.value ?? ''} valueCode={currency?.value ?? ''}
onChange={handleCurrencyChange} onChange={handleCurrencyChange}
currencies={currencies} currencies={currencies}
/> />

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconChevronDown } from 'twenty-ui'; import { IconChevronDown } from 'twenty-ui';
@ -6,7 +5,6 @@ import { IconChevronDown } from 'twenty-ui';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { isDefined } from '~/utils/isDefined';
import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope'; import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope';
@ -64,8 +62,6 @@ export const CurrencyPickerDropdownButton = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [selectedCurrency, setSelectedCurrency] = useState<Currency>();
const { isDropdownOpen, closeDropdown } = useDropdown( const { isDropdownOpen, closeDropdown } = useDropdown(
CurrencyPickerHotkeyScope.CurrencyPicker, CurrencyPickerHotkeyScope.CurrencyPicker,
); );
@ -75,12 +71,9 @@ export const CurrencyPickerDropdownButton = ({
closeDropdown(); closeDropdown();
}; };
useEffect(() => { const currency = currencies.find(({ value }) => value === valueCode);
const currency = currencies.find(({ value }) => value === valueCode);
if (isDefined(currency)) { const currencyCode = currency?.value ?? CurrencyCode.USD;
setSelectedCurrency(currency);
}
}, [valueCode, currencies]);
return ( return (
<Dropdown <Dropdown
@ -90,7 +83,7 @@ export const CurrencyPickerDropdownButton = ({
clickableComponent={ clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}> <StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
<StyledIconContainer> <StyledIconContainer>
{selectedCurrency ? selectedCurrency.value : CurrencyCode.USD} {currencyCode}
<IconChevronDown size={theme.icon.size.sm} /> <IconChevronDown size={theme.icon.size.sm} />
</StyledIconContainer> </StyledIconContainer>
</StyledDropdownButtonContainer> </StyledDropdownButtonContainer>
@ -98,7 +91,7 @@ export const CurrencyPickerDropdownButton = ({
dropdownComponents={ dropdownComponents={
<CurrencyPickerDropdownSelect <CurrencyPickerDropdownSelect
currencies={currencies} currencies={currencies}
selectedCurrency={selectedCurrency} selectedCurrency={currency}
onChange={handleChange} onChange={handleChange}
/> />
} }

View File

@ -49,6 +49,7 @@ export const mapViewFieldsToColumnDefinitions = ({
viewFieldId: viewField.id, viewFieldId: viewField.id,
isSortable: correspondingColumnDefinition.isSortable, isSortable: correspondingColumnDefinition.isSortable,
isFilterable: correspondingColumnDefinition.isFilterable, isFilterable: correspondingColumnDefinition.isFilterable,
defaultValue: correspondingColumnDefinition.defaultValue,
} as ColumnDefinition<FieldMetadata>; } as ColumnDefinition<FieldMetadata>;
}) })
.filter(isDefined); .filter(isDefined);

View File

@ -280,7 +280,6 @@ export const SettingsObjectNewFieldStep2 = () => {
}; };
const excludedFieldTypes: SettingsSupportedFieldType[] = [ const excludedFieldTypes: SettingsSupportedFieldType[] = [
FieldMetadataType.Currency,
FieldMetadataType.Email, FieldMetadataType.Email,
FieldMetadataType.FullName, FieldMetadataType.FullName,
FieldMetadataType.Link, FieldMetadataType.Link,

View File

@ -3,104 +3,103 @@ import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-m
describe('validateDefaultValueForType', () => { describe('validateDefaultValueForType', () => {
it('should return true for null defaultValue', () => { it('should return true for null defaultValue', () => {
expect(validateDefaultValueForType(FieldMetadataType.TEXT, null)).toBe( expect(
true, validateDefaultValueForType(FieldMetadataType.TEXT, null).isValid,
); ).toBe(true);
}); });
// Dynamic default values // Dynamic default values
it('should validate uuid dynamic default value for UUID type', () => { it('should validate uuid dynamic default value for UUID type', () => {
expect(validateDefaultValueForType(FieldMetadataType.UUID, 'uuid')).toBe( expect(
true, validateDefaultValueForType(FieldMetadataType.UUID, 'uuid').isValid,
); ).toBe(true);
}); });
it('should validate now dynamic default value for DATE_TIME type', () => { it('should validate now dynamic default value for DATE_TIME type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now'), validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now').isValid,
).toBe(true); ).toBe(true);
}); });
it('should return false for mismatched dynamic default value', () => { it('should return false for mismatched dynamic default value', () => {
expect(validateDefaultValueForType(FieldMetadataType.UUID, 'now')).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.UUID, 'now').isValid,
); ).toBe(false);
}); });
// Static default values // Static default values
it('should validate string default value for TEXT type', () => { it('should validate string default value for TEXT type', () => {
expect(validateDefaultValueForType(FieldMetadataType.TEXT, "'test'")).toBe( expect(
true, validateDefaultValueForType(FieldMetadataType.TEXT, "'test'").isValid,
); ).toBe(true);
}); });
it('should return false for invalid string default value for TEXT type', () => { it('should return false for invalid string default value for TEXT type', () => {
expect(validateDefaultValueForType(FieldMetadataType.TEXT, 123)).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.TEXT, 123).isValid,
); ).toBe(false);
}); });
it('should validate string default value for PHONE type', () => { it('should validate string default value for PHONE type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'"), validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'")
.isValid,
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid string default value for PHONE type', () => { it('should return false for invalid string default value for PHONE type', () => {
expect(validateDefaultValueForType(FieldMetadataType.PHONE, 123)).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.PHONE, 123).isValid,
); ).toBe(false);
}); });
it('should validate string default value for EMAIL type', () => { it('should validate string default value for EMAIL type', () => {
expect( expect(
validateDefaultValueForType( validateDefaultValueForType(FieldMetadataType.EMAIL, "'test@example.com'")
FieldMetadataType.EMAIL, .isValid,
"'test@example.com'",
),
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid string default value for EMAIL type', () => { it('should return false for invalid string default value for EMAIL type', () => {
expect(validateDefaultValueForType(FieldMetadataType.EMAIL, 123)).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.EMAIL, 123).isValid,
); ).toBe(false);
}); });
it('should validate number default value for NUMBER type', () => { it('should validate number default value for NUMBER type', () => {
expect(validateDefaultValueForType(FieldMetadataType.NUMBER, 100)).toBe( expect(
true, validateDefaultValueForType(FieldMetadataType.NUMBER, 100).isValid,
); ).toBe(true);
}); });
it('should return false for invalid number default value for NUMBER type', () => { it('should return false for invalid number default value for NUMBER type', () => {
expect(validateDefaultValueForType(FieldMetadataType.NUMBER, '100')).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.NUMBER, '100').isValid,
); ).toBe(false);
}); });
it('should validate number default value for PROBABILITY type', () => { it('should validate number default value for PROBABILITY type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5), validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5).isValid,
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid number default value for PROBABILITY type', () => { it('should return false for invalid number default value for PROBABILITY type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%'), validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%').isValid,
).toBe(false); ).toBe(false);
}); });
it('should validate boolean default value for BOOLEAN type', () => { it('should validate boolean default value for BOOLEAN type', () => {
expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, true)).toBe( expect(
true, validateDefaultValueForType(FieldMetadataType.BOOLEAN, true).isValid,
); ).toBe(true);
}); });
it('should return false for invalid boolean default value for BOOLEAN type', () => { it('should return false for invalid boolean default value for BOOLEAN type', () => {
expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true')).toBe( expect(
false, validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true').isValid,
); ).toBe(false);
}); });
// LINK type // LINK type
@ -109,7 +108,7 @@ describe('validateDefaultValueForType', () => {
validateDefaultValueForType(FieldMetadataType.LINK, { validateDefaultValueForType(FieldMetadataType.LINK, {
label: "'http://example.com'", label: "'http://example.com'",
url: "'Example'", url: "'Example'",
}), }).isValid,
).toBe(true); ).toBe(true);
}); });
@ -120,7 +119,7 @@ describe('validateDefaultValueForType', () => {
// @ts-expect-error Just for testing purposes // @ts-expect-error Just for testing purposes
{ label: 123, url: {} }, { label: 123, url: {} },
FieldMetadataType.LINK, FieldMetadataType.LINK,
), ).isValid,
).toBe(false); ).toBe(false);
}); });
@ -130,7 +129,7 @@ describe('validateDefaultValueForType', () => {
validateDefaultValueForType(FieldMetadataType.CURRENCY, { validateDefaultValueForType(FieldMetadataType.CURRENCY, {
amountMicros: '100', amountMicros: '100',
currencyCode: "'USD'", currencyCode: "'USD'",
}), }).isValid,
).toBe(true); ).toBe(true);
}); });
@ -141,14 +140,15 @@ describe('validateDefaultValueForType', () => {
// @ts-expect-error Just for testing purposes // @ts-expect-error Just for testing purposes
{ amountMicros: 100, currencyCode: "'USD'" }, { amountMicros: 100, currencyCode: "'USD'" },
FieldMetadataType.CURRENCY, FieldMetadataType.CURRENCY,
), ).isValid,
).toBe(false); ).toBe(false);
}); });
// Unknown type // Unknown type
it('should return false for unknown type', () => { it('should return false for unknown type', () => {
expect( expect(
validateDefaultValueForType('unknown' as FieldMetadataType, "'test'"), validateDefaultValueForType('unknown' as FieldMetadataType, "'test'")
.isValid,
).toBe(false); ).toBe(false);
}); });
}); });

View File

@ -1,5 +1,5 @@
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator'; import { ValidationError, validateSync } from 'class-validator';
import { import {
FieldMetadataClassValidation, FieldMetadataClassValidation,
@ -49,17 +49,32 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson], [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
}; };
type ValidationResult = {
isValid: boolean;
errors: ValidationError[];
};
export const validateDefaultValueForType = ( export const validateDefaultValueForType = (
type: FieldMetadataType, type: FieldMetadataType,
defaultValue: FieldMetadataDefaultValue, defaultValue: FieldMetadataDefaultValue,
): boolean => { ): ValidationResult => {
if (defaultValue === null) return true; if (defaultValue === null) {
return {
isValid: true,
errors: [],
};
}
const validators = defaultValueValidatorsMap[type]; const validators = defaultValueValidatorsMap[type] as any[];
if (!validators) return false; if (!validators) {
return {
isValid: false,
errors: [],
};
}
const isValid = validators.some((validator) => { const validationResults = validators.map((validator) => {
const conputedDefaultValue = isCompositeFieldMetadataType(type) const conputedDefaultValue = isCompositeFieldMetadataType(type)
? defaultValue ? defaultValue
: { value: defaultValue }; : { value: defaultValue };
@ -69,14 +84,24 @@ export const validateDefaultValueForType = (
FieldMetadataClassValidation FieldMetadataClassValidation
>(validator, conputedDefaultValue as FieldMetadataClassValidation); >(validator, conputedDefaultValue as FieldMetadataClassValidation);
return ( const errors = validateSync(defaultValueInstance, {
validateSync(defaultValueInstance, { whitelist: true,
whitelist: true, forbidNonWhitelisted: true,
forbidNonWhitelisted: true, forbidUnknownValues: true,
forbidUnknownValues: true, });
}).length === 0
); const isValid = errors.length === 0;
return {
isValid,
errors,
};
}); });
return isValid; const isValid = validationResults.some((result) => result.isValid);
return {
isValid,
errors: validationResults.flatMap((result) => result.errors),
};
}; };

View File

@ -14,13 +14,17 @@ import {
FieldMetadataType, FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
import { LoggerService } from 'src/engine/integrations/logger/logger.service';
@Injectable() @Injectable()
@ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true }) @ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true })
export class IsFieldMetadataDefaultValue export class IsFieldMetadataDefaultValue
implements ValidatorConstraintInterface implements ValidatorConstraintInterface
{ {
constructor(private readonly fieldMetadataService: FieldMetadataService) {} constructor(
private readonly fieldMetadataService: FieldMetadataService,
private readonly loggerService: LoggerService,
) {}
async validate( async validate(
value: FieldMetadataDefaultValue, value: FieldMetadataDefaultValue,
@ -48,7 +52,19 @@ export class IsFieldMetadataDefaultValue
type = fieldMetadata.type; type = fieldMetadata.type;
} }
return validateDefaultValueForType(type, value); const validationResult = validateDefaultValueForType(type, value);
if (!validationResult.isValid) {
this.loggerService.error(
{
message: 'Error during field validation',
errors: validationResult.errors,
},
'Field Metadata Validation',
);
}
return validationResult.isValid;
} }
defaultMessage(): string { defaultMessage(): string {