Allow input and display of floats for Number fields (#7340)
### Description - We added a decimal field for a Number Field type in the settings - We updated the Number Field type create a form with decimals input - We are not implementing the dropdown present on the Figma because it seems not related ### Demo <https://www.loom.com/share/18a8d4b712a14f6d8b66806764f8467f?sid=3fc79b46-ae32-46e3-8635-d0eee02e53b2> Fixes #6987 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
committed by
GitHub
parent
e3ed574420
commit
97eff774bd
@ -51,6 +51,7 @@ export const queries = {
|
||||
${baseFields}
|
||||
defaultValue
|
||||
options
|
||||
settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@ -17,7 +17,7 @@ export type FieldMetadataItemOption = {
|
||||
|
||||
export type FieldMetadataItem = Omit<
|
||||
Field,
|
||||
'__typename' | 'defaultValue' | 'options' | 'settings' | 'relationDefinition'
|
||||
'__typename' | 'defaultValue' | 'options' | 'relationDefinition'
|
||||
> & {
|
||||
__typename?: string;
|
||||
defaultValue?: any;
|
||||
|
||||
@ -54,5 +54,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
||||
metadata: fieldDefintionMetadata,
|
||||
type: field.type,
|
||||
}),
|
||||
settings: field.settings,
|
||||
};
|
||||
};
|
||||
|
||||
@ -338,6 +338,7 @@ export const RecordBoardCard = ({
|
||||
metadata: fieldDefinition.metadata,
|
||||
type: fieldDefinition.type,
|
||||
}),
|
||||
settings: fieldDefinition.settings,
|
||||
},
|
||||
useUpdateRecord: useUpdateOneRecordHook,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
|
||||
@ -2,7 +2,11 @@ import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/h
|
||||
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
|
||||
|
||||
export const NumberFieldDisplay = () => {
|
||||
const { fieldValue } = useNumberFieldDisplay();
|
||||
|
||||
return <NumberDisplay value={fieldValue} />;
|
||||
const { fieldValue, fieldDefinition } = useNumberFieldDisplay();
|
||||
return (
|
||||
<NumberDisplay
|
||||
value={fieldValue}
|
||||
decimals={fieldDefinition.settings?.decimals}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,10 +5,11 @@ import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecor
|
||||
import { FieldNumberValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
canBeCastAsNumberOrNull,
|
||||
castAsNumberOrNull,
|
||||
} from '~/utils/cast-as-number-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
@ -32,11 +33,11 @@ export const useNumberField = () => {
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistNumberField = (newValue: string) => {
|
||||
if (!canBeCastAsIntegerOrNull(newValue)) {
|
||||
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const castedValue = castAsIntegerOrNull(newValue);
|
||||
const castedValue = castAsNumberOrNull(newValue);
|
||||
|
||||
persistField(castedValue);
|
||||
};
|
||||
|
||||
@ -16,4 +16,7 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
||||
infoTooltipContent?: string;
|
||||
defaultValue?: any;
|
||||
editButtonIcon?: IconComponent;
|
||||
settings?: {
|
||||
decimals?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const numberFieldDefaultValueSchema = z.object({
|
||||
decimals: z.number().nullable(),
|
||||
});
|
||||
@ -11,6 +11,8 @@ import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/
|
||||
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
||||
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
|
||||
import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
|
||||
import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard';
|
||||
import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
|
||||
import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard';
|
||||
import {
|
||||
@ -52,6 +54,10 @@ const multiSelectFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.MultiSelect) })
|
||||
.merge(settingsDataModelFieldMultiSelectFormSchema);
|
||||
|
||||
const numberFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Number) })
|
||||
.merge(settingsDataModelFieldNumberFormSchema);
|
||||
|
||||
const otherFieldsFormSchema = z.object({
|
||||
type: z.enum(
|
||||
Object.keys(
|
||||
@ -63,6 +69,7 @@ const otherFieldsFormSchema = z.object({
|
||||
FieldMetadataType.MultiSelect,
|
||||
FieldMetadataType.Date,
|
||||
FieldMetadataType.DateTime,
|
||||
FieldMetadataType.Number,
|
||||
]),
|
||||
) as [FieldMetadataType, ...FieldMetadataType[]],
|
||||
),
|
||||
@ -78,13 +85,17 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
relationFieldFormSchema,
|
||||
selectFieldFormSchema,
|
||||
multiSelectFieldFormSchema,
|
||||
numberFieldFormSchema,
|
||||
otherFieldsFormSchema,
|
||||
],
|
||||
);
|
||||
|
||||
type SettingsDataModelFieldSettingsFormCardProps = {
|
||||
isCreatingField?: boolean;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom'
|
||||
> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
@ -163,6 +174,16 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.Number) {
|
||||
return (
|
||||
<SettingsDataModelFieldNumberSettingsFormCard
|
||||
disabled={fieldMetadataItem.isCustom === false}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Select ||
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
|
||||
@ -0,0 +1,168 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { IconInfoCircle, IconMinus, IconPlus } from 'twenty-ui';
|
||||
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
|
||||
|
||||
type SettingsDataModelFieldNumberDecimalsInputProps = {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledCounterContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledExampleText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledCounterControlsIcons = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledCounterInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const StyledTextInput = styled(TextInput)`
|
||||
width: ${({ theme }) => theme.spacing(16)};
|
||||
input {
|
||||
width: ${({ theme }) => theme.spacing(16)};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
text-align: center;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
}
|
||||
input ~ div {
|
||||
padding-right: ${({ theme }) => theme.spacing(0)};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledControlButton = styled(Button)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
svg {
|
||||
height: ${({ theme }) => theme.spacing(4)};
|
||||
width: ${({ theme }) => theme.spacing(4)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInfoButton = styled(Button)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
svg {
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
height: ${({ theme }) => theme.spacing(4)};
|
||||
width: ${({ theme }) => theme.spacing(4)};
|
||||
}
|
||||
`;
|
||||
|
||||
const MIN_VALUE = 0;
|
||||
const MAX_VALUE = 100;
|
||||
export const SettingsDataModelFieldNumberDecimalsInput = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: SettingsDataModelFieldNumberDecimalsInputProps) => {
|
||||
const exampleValue = (1000).toFixed(value);
|
||||
|
||||
const handleIncrementCounter = () => {
|
||||
if (value < MAX_VALUE) {
|
||||
const newValue = value + 1;
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrementCounter = () => {
|
||||
if (value > MIN_VALUE) {
|
||||
const newValue = value - 1;
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextInputChange = (value: string) => {
|
||||
const castedNumber = castAsNumberOrNull(value);
|
||||
if (castedNumber === null) {
|
||||
onChange(MIN_VALUE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (castedNumber < MIN_VALUE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (castedNumber > MAX_VALUE) {
|
||||
onChange(MAX_VALUE);
|
||||
return;
|
||||
}
|
||||
onChange(castedNumber);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StyledTitle>Number of decimals</StyledTitle>
|
||||
<StyledCounterContainer>
|
||||
<StyledCounterInnerContainer>
|
||||
<StyledExampleText>Example: {exampleValue}</StyledExampleText>
|
||||
<StyledCounterControlsIcons>
|
||||
<StyledInfoButton variant="tertiary" Icon={IconInfoCircle} />
|
||||
<StyledControlButton
|
||||
variant="secondary"
|
||||
onClick={handleDecrementCounter}
|
||||
Icon={IconMinus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledTextInput
|
||||
name="decimals"
|
||||
fullWidth
|
||||
value={value.toString()}
|
||||
onChange={(value) => handleTextInputChange(value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledControlButton
|
||||
variant="secondary"
|
||||
onClick={handleIncrementCounter}
|
||||
Icon={IconPlus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledCounterControlsIcons>
|
||||
</StyledCounterInnerContainer>
|
||||
</StyledCounterContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { numberFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema';
|
||||
import { SettingsDataModelFieldNumberDecimalsInput } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number';
|
||||
|
||||
export const settingsDataModelFieldNumberFormSchema = z.object({
|
||||
settings: numberFieldDefaultValueSchema,
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldNumberFormValues = z.infer<
|
||||
typeof settingsDataModelFieldNumberFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldNumberFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldNumberForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldNumberFormProps) => {
|
||||
const { control } = useFormContext<SettingsDataModelFieldNumberFormValues>();
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<Controller
|
||||
name="settings"
|
||||
defaultValue={{
|
||||
decimals:
|
||||
fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
|
||||
}}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const count = value?.decimals ?? 0;
|
||||
|
||||
return (
|
||||
<SettingsDataModelFieldNumberDecimalsInput
|
||||
value={count}
|
||||
onChange={(value) => onChange({ decimals: value })}
|
||||
disabled={disabled}
|
||||
></SettingsDataModelFieldNumberDecimalsInput>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { SettingsDataModelFieldNumberForm } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
|
||||
type SettingsDataModelFieldNumberSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldNumberSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldNumberSettingsFormCardProps) => {
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldNumberForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -4,8 +4,11 @@ import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type NumberDisplayProps = {
|
||||
value: string | number | null | undefined;
|
||||
decimals?: number;
|
||||
};
|
||||
|
||||
export const NumberDisplay = ({ value }: NumberDisplayProps) => (
|
||||
<EllipsisDisplay>{value && formatNumber(Number(value))}</EllipsisDisplay>
|
||||
export const NumberDisplay = ({ value, decimals }: NumberDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value && formatNumber(Number(value), decimals)}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
|
||||
@ -50,6 +50,7 @@ export const mapViewFieldsToColumnDefinitions = ({
|
||||
isSortable: correspondingColumnDefinition.isSortable,
|
||||
isFilterable: correspondingColumnDefinition.isFilterable,
|
||||
defaultValue: correspondingColumnDefinition.defaultValue,
|
||||
settings: correspondingColumnDefinition.settings,
|
||||
} as ColumnDefinition<FieldMetadata>;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
Reference in New Issue
Block a user