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 { 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,
|
||||||
|
|||||||
@ -43,5 +43,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
|||||||
options: field.options,
|
options: field.options,
|
||||||
},
|
},
|
||||||
iconName: field.icon ?? 'Icon123',
|
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,
|
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,
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -23,4 +23,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
|||||||
type: FieldMetadataType;
|
type: FieldMetadataType;
|
||||||
metadata: T;
|
metadata: T;
|
||||||
infoTooltipContent?: string;
|
infoTooltipContent?: string;
|
||||||
|
defaultValue: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user