From c69a3f01daba9c48fd3c2e01a113b6a5d56151b0 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 11 Apr 2024 16:49:00 +0200 Subject: [PATCH] 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 --- .../hooks/useFieldMetadataItem.ts | 11 ++- ...ormatFieldMetadataItemAsFieldDefinition.ts | 1 + .../utils/getDefaultValueForBackend.ts | 19 ++++ .../components/RecordBoardCard.tsx | 1 + .../meta-types/hooks/useCurrencyField.ts | 3 + .../input/components/CurrencyFieldInput.tsx | 32 +++++-- .../record-field/types/FieldDefinition.ts | 1 + .../components/RecordShowContainer.tsx | 2 + .../SettingsDataModelFieldPreview.tsx | 6 +- .../SignInBackgroundMockColumnDefinitions.ts | 15 +++ .../field/input/components/CurrencyInput.tsx | 16 +--- .../CurrencyPickerDropdownButton.tsx | 17 +--- .../utils/mapViewFieldsToColumnDefinitions.ts | 1 + .../SettingsObjectNewFieldStep2.tsx | 1 - ...lidate-default-value-based-on-type.spec.ts | 92 +++++++++---------- .../validate-default-value-for-type.util.ts | 53 ++++++++--- ...-field-metadata-default-value.validator.ts | 20 +++- 17 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/getDefaultValueForBackend.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts index f8a61900f..51c900099 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts @@ -1,6 +1,7 @@ import { v4 } from 'uuid'; import { FieldMetadataOption } from '@/object-metadata/types/FieldMetadataOption.ts'; +import { getDefaultValueForBackend } from '@/object-metadata/utils/getDefaultValueForBackend'; import { Field } from '~/generated/graphql'; import { FieldMetadataItem } from '../types/FieldMetadataItem'; @@ -24,11 +25,11 @@ export const useFieldMetadataItem = () => { }, ) => { const formattedInput = formatFieldMetadataItemInput(input); - const defaultValue = input.defaultValue - ? typeof input.defaultValue == 'string' - ? `'${input.defaultValue}'` - : input.defaultValue - : formattedInput.defaultValue ?? undefined; + + const defaultValue = getDefaultValueForBackend( + input.defaultValue ?? formattedInput.defaultValue, + input.type, + ); return createOneFieldMetadataItem({ ...formattedInput, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts index 7b6df02d1..0b8ec528b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts @@ -43,5 +43,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({ options: field.options, }, iconName: field.icon ?? 'Icon123', + defaultValue: field.defaultValue, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getDefaultValueForBackend.ts b/packages/twenty-front/src/modules/object-metadata/utils/getDefaultValueForBackend.ts new file mode 100644 index 000000000..ccd122331 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getDefaultValueForBackend.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index d7e71f434..0c753862a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -264,6 +264,7 @@ export const RecordBoardCard = () => { iconName: fieldDefinition.iconName, type: fieldDefinition.type, metadata: fieldDefinition.metadata, + defaultValue: fieldDefinition.defaultValue, }, useUpdateRecord: useUpdateOneRecordHook, hotkeyScope: InlineCellHotkeyScope.InlineCell, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts index df1bd072d..c92ba97cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts @@ -64,6 +64,8 @@ export const useCurrencyField = () => { const draftValue = useRecoilValue(getDraftValueSelector()); + const defaultValue = fieldDefinition.defaultValue; + return { fieldDefinition, fieldValue, @@ -72,5 +74,6 @@ export const useCurrencyField = () => { setFieldValue, hotkeyScope, persistCurrencyField, + defaultValue, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index eff716fb1..36efd7472 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -1,4 +1,5 @@ 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 { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; @@ -21,14 +22,27 @@ export const CurrencyFieldInput = ({ onTab, onShiftTab, }: CurrencyFieldInputProps) => { - const { hotkeyScope, draftValue, persistCurrencyField, setDraftValue } = - useCurrencyField(); + const { + hotkeyScope, + draftValue, + persistCurrencyField, + setDraftValue, + defaultValue, + } = useCurrencyField(); + + const currencyCode = + draftValue?.currencyCode ?? + ((defaultValue as FieldCurrencyValue).currencyCode.replace( + /'/g, + '', + ) as CurrencyCode) ?? + CurrencyCode.USD; const handleEnter = (newValue: string) => { onEnter?.(() => { persistCurrencyField({ amountText: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }); }); }; @@ -37,7 +51,7 @@ export const CurrencyFieldInput = ({ onEscape?.(() => { persistCurrencyField({ amountText: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }); }); }; @@ -49,7 +63,7 @@ export const CurrencyFieldInput = ({ onClickOutside?.(() => { persistCurrencyField({ amountText: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }); }); }; @@ -58,7 +72,7 @@ export const CurrencyFieldInput = ({ onTab?.(() => { persistCurrencyField({ amountText: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }); }); }; @@ -67,7 +81,7 @@ export const CurrencyFieldInput = ({ onShiftTab?.(() => persistCurrencyField({ amountText: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }), ); }; @@ -75,7 +89,7 @@ export const CurrencyFieldInput = ({ const handleChange = (newValue: string) => { setDraftValue({ amount: newValue, - currencyCode: draftValue?.currencyCode ?? CurrencyCode.USD, + currencyCode, }); }; @@ -90,7 +104,7 @@ export const CurrencyFieldInput = ({ = { type: FieldMetadataType; metadata: T; infoTooltipContent?: string; + defaultValue: any; }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index ac4991b5b..39c8f6c10 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -155,6 +155,8 @@ export const RecordShowContainer = ({ labelIdentifierFieldMetadataItem?.name || '', objectMetadataNameSingular: objectNameSingular, }, + defaultValue: + labelIdentifierFieldMetadataItem?.defaultValue, }, useUpdateRecord: useUpdateOneObjectRecordMutation, hotkeyScope: InlineCellHotkeyScope.InlineCell, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index 429e9c9b9..2914dee78 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -15,7 +15,10 @@ import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export type SettingsDataModelFieldPreviewProps = { - fieldMetadataItem: Pick & { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'defaultValue' + > & { id?: string; name?: string; }; @@ -106,6 +109,7 @@ export const SettingsDataModelFieldPreview = ({ relationObjectMetadataItem?.nameSingular, options: selectOptions, }, + defaultValue: fieldMetadataItem.defaultValue, }, hotkeyScope: 'field-preview', }} diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts index eb8f914a0..d6072c52b 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts @@ -20,6 +20,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconLink', isVisible: true, + defaultValue: '', }, { position: 1, @@ -36,6 +37,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconUsers', isVisible: true, + defaultValue: 0, }, { position: 2, @@ -52,6 +54,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconBuildingSkyscraper', isVisible: true, + defaultValue: '', }, { position: 3, @@ -69,6 +72,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconHeart', isVisible: true, + defaultValue: [], }, { position: 4, @@ -85,6 +89,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconMap', isVisible: true, + defaultValue: '', }, { position: 5, @@ -102,6 +107,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconUserCircle', isVisible: true, + defaultValue: null, }, { position: 6, @@ -119,6 +125,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconUsers', isVisible: true, + defaultValue: [], }, { position: 7, @@ -136,6 +143,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconFileImport', isVisible: true, + defaultValue: [], }, { position: 8, @@ -152,6 +160,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconCalendar', isVisible: true, + defaultValue: '', }, { position: 9, @@ -168,6 +177,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconTarget', isVisible: true, + defaultValue: false, }, { position: 10, @@ -184,6 +194,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconBrandLinkedin', isVisible: true, + defaultValue: '', }, { position: 11, @@ -201,6 +212,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconTargetArrow', isVisible: true, + defaultValue: [], }, { position: 12, @@ -217,6 +229,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconBrandX', isVisible: true, + defaultValue: '', }, { position: 13, @@ -234,6 +247,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconCheckbox', isVisible: true, + defaultValue: [], }, { position: 14, @@ -250,6 +264,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, iconName: 'IconMoneybag', isVisible: true, + defaultValue: 0, }, ] satisfies ColumnDefinition[] ).filter(filterAvailableTableColumns); diff --git a/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx index 5c5da96c3..925bcc1b9 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx @@ -7,7 +7,6 @@ import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/S import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton'; import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle'; -import { isDefined } from '~/utils/isDefined'; export const StyledInput = styled.input` margin: 0; @@ -77,9 +76,6 @@ export const CurrencyInput = ({ const theme = useTheme(); const [internalText, setInternalText] = useState(value); - const [internalCurrency, setInternalCurrency] = useState( - null, - ); const wrapperRef = useRef(null); @@ -89,7 +85,6 @@ export const CurrencyInput = ({ }; const handleCurrencyChange = (currency: Currency) => { - setInternalCurrency(currency); onSelect?.(currency.value); }; @@ -116,23 +111,18 @@ export const CurrencyInput = ({ [], ); - useEffect(() => { - const currency = currencies.find(({ value }) => value === currencyCode); - if (isDefined(currency)) { - setInternalCurrency(currency); - } - }, [currencies, currencyCode]); + const currency = currencies.find(({ value }) => value === currencyCode); useEffect(() => { setInternalText(value); }, [value]); - const Icon: IconComponent = internalCurrency?.Icon; + const Icon: IconComponent = currency?.Icon; return ( diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx index 8400cec67..3ee47ff2c 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconChevronDown } from 'twenty-ui'; @@ -6,7 +5,6 @@ import { IconChevronDown } from 'twenty-ui'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { isDefined } from '~/utils/isDefined'; import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope'; @@ -64,8 +62,6 @@ export const CurrencyPickerDropdownButton = ({ }) => { const theme = useTheme(); - const [selectedCurrency, setSelectedCurrency] = useState(); - const { isDropdownOpen, closeDropdown } = useDropdown( CurrencyPickerHotkeyScope.CurrencyPicker, ); @@ -75,12 +71,9 @@ export const CurrencyPickerDropdownButton = ({ closeDropdown(); }; - useEffect(() => { - const currency = currencies.find(({ value }) => value === valueCode); - if (isDefined(currency)) { - setSelectedCurrency(currency); - } - }, [valueCode, currencies]); + const currency = currencies.find(({ value }) => value === valueCode); + + const currencyCode = currency?.value ?? CurrencyCode.USD; return ( - {selectedCurrency ? selectedCurrency.value : CurrencyCode.USD} + {currencyCode} @@ -98,7 +91,7 @@ export const CurrencyPickerDropdownButton = ({ dropdownComponents={ } diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts index c27b8045d..139ac0427 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts @@ -49,6 +49,7 @@ export const mapViewFieldsToColumnDefinitions = ({ viewFieldId: viewField.id, isSortable: correspondingColumnDefinition.isSortable, isFilterable: correspondingColumnDefinition.isFilterable, + defaultValue: correspondingColumnDefinition.defaultValue, } as ColumnDefinition; }) .filter(isDefined); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index 9c009f392..99ec05470 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -280,7 +280,6 @@ export const SettingsObjectNewFieldStep2 = () => { }; const excludedFieldTypes: SettingsSupportedFieldType[] = [ - FieldMetadataType.Currency, FieldMetadataType.Email, FieldMetadataType.FullName, FieldMetadataType.Link, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts index 6768baf75..757303d4c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts @@ -3,104 +3,103 @@ import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-m describe('validateDefaultValueForType', () => { it('should return true for null defaultValue', () => { - expect(validateDefaultValueForType(FieldMetadataType.TEXT, null)).toBe( - true, - ); + expect( + validateDefaultValueForType(FieldMetadataType.TEXT, null).isValid, + ).toBe(true); }); // Dynamic default values it('should validate uuid dynamic default value for UUID type', () => { - expect(validateDefaultValueForType(FieldMetadataType.UUID, 'uuid')).toBe( - true, - ); + expect( + validateDefaultValueForType(FieldMetadataType.UUID, 'uuid').isValid, + ).toBe(true); }); it('should validate now dynamic default value for DATE_TIME type', () => { expect( - validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now'), + validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now').isValid, ).toBe(true); }); it('should return false for mismatched dynamic default value', () => { - expect(validateDefaultValueForType(FieldMetadataType.UUID, 'now')).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.UUID, 'now').isValid, + ).toBe(false); }); // Static default values it('should validate string default value for TEXT type', () => { - expect(validateDefaultValueForType(FieldMetadataType.TEXT, "'test'")).toBe( - true, - ); + expect( + validateDefaultValueForType(FieldMetadataType.TEXT, "'test'").isValid, + ).toBe(true); }); it('should return false for invalid string default value for TEXT type', () => { - expect(validateDefaultValueForType(FieldMetadataType.TEXT, 123)).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.TEXT, 123).isValid, + ).toBe(false); }); it('should validate string default value for PHONE type', () => { expect( - validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'"), + validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'") + .isValid, ).toBe(true); }); it('should return false for invalid string default value for PHONE type', () => { - expect(validateDefaultValueForType(FieldMetadataType.PHONE, 123)).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.PHONE, 123).isValid, + ).toBe(false); }); it('should validate string default value for EMAIL type', () => { expect( - validateDefaultValueForType( - FieldMetadataType.EMAIL, - "'test@example.com'", - ), + validateDefaultValueForType(FieldMetadataType.EMAIL, "'test@example.com'") + .isValid, ).toBe(true); }); it('should return false for invalid string default value for EMAIL type', () => { - expect(validateDefaultValueForType(FieldMetadataType.EMAIL, 123)).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.EMAIL, 123).isValid, + ).toBe(false); }); it('should validate number default value for NUMBER type', () => { - expect(validateDefaultValueForType(FieldMetadataType.NUMBER, 100)).toBe( - true, - ); + expect( + validateDefaultValueForType(FieldMetadataType.NUMBER, 100).isValid, + ).toBe(true); }); it('should return false for invalid number default value for NUMBER type', () => { - expect(validateDefaultValueForType(FieldMetadataType.NUMBER, '100')).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.NUMBER, '100').isValid, + ).toBe(false); }); it('should validate number default value for PROBABILITY type', () => { expect( - validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5), + validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5).isValid, ).toBe(true); }); it('should return false for invalid number default value for PROBABILITY type', () => { expect( - validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%'), + validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%').isValid, ).toBe(false); }); it('should validate boolean default value for BOOLEAN type', () => { - expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, true)).toBe( - true, - ); + expect( + validateDefaultValueForType(FieldMetadataType.BOOLEAN, true).isValid, + ).toBe(true); }); it('should return false for invalid boolean default value for BOOLEAN type', () => { - expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true')).toBe( - false, - ); + expect( + validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true').isValid, + ).toBe(false); }); // LINK type @@ -109,7 +108,7 @@ describe('validateDefaultValueForType', () => { validateDefaultValueForType(FieldMetadataType.LINK, { label: "'http://example.com'", url: "'Example'", - }), + }).isValid, ).toBe(true); }); @@ -120,7 +119,7 @@ describe('validateDefaultValueForType', () => { // @ts-expect-error Just for testing purposes { label: 123, url: {} }, FieldMetadataType.LINK, - ), + ).isValid, ).toBe(false); }); @@ -130,7 +129,7 @@ describe('validateDefaultValueForType', () => { validateDefaultValueForType(FieldMetadataType.CURRENCY, { amountMicros: '100', currencyCode: "'USD'", - }), + }).isValid, ).toBe(true); }); @@ -141,14 +140,15 @@ describe('validateDefaultValueForType', () => { // @ts-expect-error Just for testing purposes { amountMicros: 100, currencyCode: "'USD'" }, FieldMetadataType.CURRENCY, - ), + ).isValid, ).toBe(false); }); // Unknown type it('should return false for unknown type', () => { expect( - validateDefaultValueForType('unknown' as FieldMetadataType, "'test'"), + validateDefaultValueForType('unknown' as FieldMetadataType, "'test'") + .isValid, ).toBe(false); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts index 9e5d93a9c..5cd428bb0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts @@ -1,5 +1,5 @@ import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; +import { ValidationError, validateSync } from 'class-validator'; import { FieldMetadataClassValidation, @@ -49,17 +49,32 @@ export const defaultValueValidatorsMap = { [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson], }; +type ValidationResult = { + isValid: boolean; + errors: ValidationError[]; +}; + export const validateDefaultValueForType = ( type: FieldMetadataType, defaultValue: FieldMetadataDefaultValue, -): boolean => { - if (defaultValue === null) return true; +): ValidationResult => { + 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) ? defaultValue : { value: defaultValue }; @@ -69,14 +84,24 @@ export const validateDefaultValueForType = ( FieldMetadataClassValidation >(validator, conputedDefaultValue as FieldMetadataClassValidation); - return ( - validateSync(defaultValueInstance, { - whitelist: true, - forbidNonWhitelisted: true, - forbidUnknownValues: true, - }).length === 0 - ); + const errors = validateSync(defaultValueInstance, { + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + }); + + 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), + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts index 1064f999a..f68aea19e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts @@ -14,13 +14,17 @@ import { FieldMetadataType, } 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 { LoggerService } from 'src/engine/integrations/logger/logger.service'; @Injectable() @ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true }) export class IsFieldMetadataDefaultValue implements ValidatorConstraintInterface { - constructor(private readonly fieldMetadataService: FieldMetadataService) {} + constructor( + private readonly fieldMetadataService: FieldMetadataService, + private readonly loggerService: LoggerService, + ) {} async validate( value: FieldMetadataDefaultValue, @@ -48,7 +52,19 @@ export class IsFieldMetadataDefaultValue 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 {