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:
@ -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,
|
||||
|
||||
@ -43,5 +43,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
||||
options: field.options,
|
||||
},
|
||||
iconName: field.icon ?? 'Icon123',
|
||||
defaultValue: field.defaultValue,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -264,6 +264,7 @@ export const RecordBoardCard = () => {
|
||||
iconName: fieldDefinition.iconName,
|
||||
type: fieldDefinition.type,
|
||||
metadata: fieldDefinition.metadata,
|
||||
defaultValue: fieldDefinition.defaultValue,
|
||||
},
|
||||
useUpdateRecord: useUpdateOneRecordHook,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 = ({
|
||||
<FieldInputOverlay>
|
||||
<CurrencyInput
|
||||
value={draftValue?.amount?.toString() ?? ''}
|
||||
currencyCode={draftValue?.currencyCode ?? CurrencyCode.USD}
|
||||
currencyCode={currencyCode}
|
||||
autoFocus
|
||||
placeholder="Currency"
|
||||
onClickOutside={handleClickOutside}
|
||||
|
||||
@ -23,4 +23,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
||||
type: FieldMetadataType;
|
||||
metadata: T;
|
||||
infoTooltipContent?: string;
|
||||
defaultValue: any;
|
||||
};
|
||||
|
||||
@ -155,6 +155,8 @@ export const RecordShowContainer = ({
|
||||
labelIdentifierFieldMetadataItem?.name || '',
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
},
|
||||
defaultValue:
|
||||
labelIdentifierFieldMetadataItem?.defaultValue,
|
||||
},
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
|
||||
@ -15,7 +15,10 @@ import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldPreviewProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> & {
|
||||
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',
|
||||
}}
|
||||
|
||||
@ -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<FieldMetadata>[]
|
||||
).filter(filterAvailableTableColumns);
|
||||
|
||||
@ -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<Currency | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLInputElement>(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 (
|
||||
<StyledContainer ref={wrapperRef}>
|
||||
<CurrencyPickerDropdownButton
|
||||
valueCode={internalCurrency?.value ?? ''}
|
||||
valueCode={currency?.value ?? ''}
|
||||
onChange={handleCurrencyChange}
|
||||
currencies={currencies}
|
||||
/>
|
||||
|
||||
@ -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<Currency>();
|
||||
|
||||
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 (
|
||||
<Dropdown
|
||||
@ -90,7 +83,7 @@ export const CurrencyPickerDropdownButton = ({
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||
<StyledIconContainer>
|
||||
{selectedCurrency ? selectedCurrency.value : CurrencyCode.USD}
|
||||
{currencyCode}
|
||||
<IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledIconContainer>
|
||||
</StyledDropdownButtonContainer>
|
||||
@ -98,7 +91,7 @@ export const CurrencyPickerDropdownButton = ({
|
||||
dropdownComponents={
|
||||
<CurrencyPickerDropdownSelect
|
||||
currencies={currencies}
|
||||
selectedCurrency={selectedCurrency}
|
||||
selectedCurrency={currency}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ export const mapViewFieldsToColumnDefinitions = ({
|
||||
viewFieldId: viewField.id,
|
||||
isSortable: correspondingColumnDefinition.isSortable,
|
||||
isFilterable: correspondingColumnDefinition.isFilterable,
|
||||
defaultValue: correspondingColumnDefinition.defaultValue,
|
||||
} as ColumnDefinition<FieldMetadata>;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@ -280,7 +280,6 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
};
|
||||
|
||||
const excludedFieldTypes: SettingsSupportedFieldType[] = [
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.Email,
|
||||
FieldMetadataType.FullName,
|
||||
FieldMetadataType.Link,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user