feat: add EnumFieldDisplay and Enum field preview (#2487)
Closes #2428 Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -32,8 +32,8 @@ const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
// TODO: remove "relation" type for now, add it back when the backend is ready.
|
||||
const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes;
|
||||
// TODO: remove "enum" and "relation" types for now, add them back when the backend is ready.
|
||||
const { ENUM: _ENUM, RELATION: _RELATION, ...allowedDataTypes } = dataTypes;
|
||||
|
||||
export const SettingsObjectFieldTypeSelectSection = ({
|
||||
disabled,
|
||||
@ -57,12 +57,10 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
dropdownScopeId="object-field-type-select"
|
||||
value={fieldType}
|
||||
onChange={onChange}
|
||||
options={Object.entries(dataTypesWithoutRelation).map(
|
||||
([key, dataType]) => ({
|
||||
value: key as FieldMetadataType,
|
||||
...dataType,
|
||||
}),
|
||||
)}
|
||||
options={Object.entries(allowedDataTypes).map(([key, dataType]) => ({
|
||||
value: key as FieldMetadataType,
|
||||
...dataType,
|
||||
}))}
|
||||
/>
|
||||
{['BOOLEAN', 'DATE', 'MONEY', 'NUMBER', 'TEXT', 'URL'].includes(
|
||||
fieldType,
|
||||
|
||||
@ -73,6 +73,14 @@ export const Number: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Select: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconBuildingFactory2',
|
||||
fieldLabel: 'Industry',
|
||||
fieldType: FieldMetadataType.Enum,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomObject: Story = {
|
||||
args: {
|
||||
isObjectCustom: true,
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
IconNumbers,
|
||||
IconPhone,
|
||||
IconPlug,
|
||||
IconTag,
|
||||
IconTextSize,
|
||||
IconUser,
|
||||
} from '@/ui/display/icon';
|
||||
@ -52,6 +53,11 @@ export const dataTypes: Record<
|
||||
Icon: IconCalendarEvent,
|
||||
defaultValue: defaultDateValue.toISOString(),
|
||||
},
|
||||
[FieldMetadataType.Enum]: {
|
||||
label: 'Select',
|
||||
Icon: IconTag,
|
||||
defaultValue: { color: 'green', text: 'Option 1' },
|
||||
},
|
||||
[FieldMetadataType.Currency]: {
|
||||
label: 'Currency',
|
||||
Icon: IconCoins,
|
||||
@ -66,5 +72,4 @@ export const dataTypes: Record<
|
||||
defaultValue: 50,
|
||||
},
|
||||
[FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser },
|
||||
[FieldMetadataType.Enum]: { label: 'Enum', Icon: IconPlug },
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export type MetadataFieldDataType =
|
||||
| 'BOOLEAN'
|
||||
| 'DATE'
|
||||
| 'ENUM'
|
||||
| 'MONEY'
|
||||
| 'NUMBER'
|
||||
| 'RELATION'
|
||||
|
||||
@ -35,10 +35,17 @@ const StyledTag = styled.h3<{
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(5)};
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export type TagProps = {
|
||||
className?: string;
|
||||
color: ThemeColor;
|
||||
@ -52,6 +59,6 @@ export const Tag = ({ className, color, text, onClick }: TagProps) => (
|
||||
color={castToTagColor(color)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{text}
|
||||
<StyledContent>{text}</StyledContent>
|
||||
</StyledTag>
|
||||
);
|
||||
|
||||
@ -34,6 +34,17 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLongText: Story = {
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
color: 'green',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof Tag> = {
|
||||
args: { text: 'Urgent' },
|
||||
argTypes: {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FullNameFieldDisplay } from '@/ui/object/field/meta-types/display/components/FullNameFieldDisplay';
|
||||
import { LinkFieldDisplay } from '@/ui/object/field/meta-types/display/components/LinkFieldDisplay';
|
||||
import { RelationFieldDisplay } from '@/ui/object/field/meta-types/display/components/RelationFieldDisplay';
|
||||
import { UuidFieldDisplay } from '@/ui/object/field/meta-types/display/components/UuidFieldDisplay';
|
||||
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
|
||||
import { isFieldLink } from '@/ui/object/field/types/guards/isFieldLink';
|
||||
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
@ -12,7 +14,7 @@ import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyF
|
||||
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
|
||||
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
|
||||
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
|
||||
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
|
||||
import { EnumFieldDisplay } from '../meta-types/display/components/EnumFieldDisplay';
|
||||
import { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
|
||||
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
|
||||
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
|
||||
@ -23,7 +25,7 @@ import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||
import { isFieldDate } from '../types/guards/isFieldDate';
|
||||
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldEnum } from '../types/guards/isFieldEnum';
|
||||
import { isFieldMoney } from '../types/guards/isFieldMoney';
|
||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
@ -64,6 +66,8 @@ export const FieldDisplay = () => {
|
||||
<ChipFieldDisplay />
|
||||
) : isFieldDoubleTextChip(fieldDefinition) ? (
|
||||
<DoubleTextChipFieldDisplay />
|
||||
) : isFieldEnum(fieldDefinition) ? (
|
||||
<EnumFieldDisplay />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
|
||||
import { useEnumField } from '../../hooks/useEnumField';
|
||||
|
||||
export const EnumFieldDisplay = () => {
|
||||
const { fieldValue } = useEnumField();
|
||||
|
||||
return <Tag color={fieldValue.color} text={fieldValue.text} />;
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { FieldEnumValue } from '../../../../types/FieldMetadata';
|
||||
import { useEnumField } from '../../../hooks/useEnumField';
|
||||
import { EnumFieldDisplay } from '../EnumFieldDisplay';
|
||||
|
||||
const EnumFieldValueSetterEffect = ({ value }: { value: FieldEnumValue }) => {
|
||||
const { setFieldValue } = useEnumField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/EnumFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'enum',
|
||||
label: 'Enum',
|
||||
type: 'ENUM',
|
||||
metadata: {
|
||||
fieldName: 'Enum',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<EnumFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: EnumFieldDisplay,
|
||||
args: {
|
||||
value: { color: 'purple', text: 'Lorem ipsum' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EnumFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,47 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldEnumValue } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldEnum } from '../../types/guards/isFieldEnum';
|
||||
import { isFieldEnumValue } from '../../types/guards/isFieldEnumValue';
|
||||
|
||||
export const useEnumField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('ENUM', isFieldEnum, fieldDefinition);
|
||||
|
||||
const { fieldName } = fieldDefinition.metadata;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldEnumValue>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
const fieldEnumValue = isFieldEnumValue(fieldValue)
|
||||
? fieldValue
|
||||
: { color: 'green' as ThemeColor, text: '' };
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = {
|
||||
color: 'green' as ThemeColor,
|
||||
text: fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldEnumValue?.text ?? '',
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: fieldEnumValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
export type FieldUuidMetadata = {
|
||||
placeHolder: string;
|
||||
@ -94,6 +95,10 @@ export type FieldBooleanMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldEnumMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldMetadata =
|
||||
| FieldBooleanMetadata
|
||||
| FieldChipMetadata
|
||||
@ -107,6 +112,7 @@ export type FieldMetadata =
|
||||
| FieldNumberMetadata
|
||||
| FieldPhoneMetadata
|
||||
| FieldProbabilityMetadata
|
||||
| FieldEnumMetadata
|
||||
| FieldRelationMetadata
|
||||
| FieldTextMetadata
|
||||
| FieldURLMetadata
|
||||
@ -140,3 +146,5 @@ export type FieldDoubleTextChipValue = {
|
||||
};
|
||||
|
||||
export type FieldRelationValue = EntityForSelect | null;
|
||||
|
||||
export type FieldEnumValue = { color: ThemeColor; text: string };
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
export type FieldType =
|
||||
| 'BOOLEAN'
|
||||
| 'UUID'
|
||||
| 'TEXT'
|
||||
| 'RELATION'
|
||||
| 'CHIP'
|
||||
| 'DATE'
|
||||
| 'DOUBLE_TEXT_CHIP'
|
||||
| 'DOUBLE_TEXT'
|
||||
| 'NUMBER'
|
||||
| 'EMAIL'
|
||||
| 'BOOLEAN'
|
||||
| 'DATE'
|
||||
| 'ENUM'
|
||||
| 'MONEY_AMOUNT_V2'
|
||||
| 'MONEY_AMOUNT'
|
||||
| 'MONEY'
|
||||
| 'NUMBER'
|
||||
| 'PHONE'
|
||||
| 'PROBABILITY'
|
||||
| 'RELATION'
|
||||
| 'TEXT'
|
||||
| 'URL'
|
||||
| 'PHONE'
|
||||
| 'URL'
|
||||
| 'LINK'
|
||||
| 'PROBABILITY'
|
||||
| 'CURRENCY'
|
||||
| 'MONEY_AMOUNT'
|
||||
| 'MONEY'
|
||||
| 'FULL_NAME';
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
FieldDoubleTextChipMetadata,
|
||||
FieldDoubleTextMetadata,
|
||||
FieldEmailMetadata,
|
||||
FieldEnumMetadata,
|
||||
FieldFullnameMetadata,
|
||||
FieldLinkMetadata,
|
||||
FieldMetadata,
|
||||
@ -43,6 +44,8 @@ type AssertFieldMetadataFunction = <
|
||||
? FieldLinkMetadata
|
||||
: E extends 'MONEY_AMOUNT'
|
||||
? FieldMoneyMetadata
|
||||
: E extends 'ENUM'
|
||||
? FieldEnumMetadata
|
||||
: E extends 'NUMBER'
|
||||
? FieldNumberMetadata
|
||||
: E extends 'PHONE'
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldEnumMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldEnum = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldEnumMetadata> => field.type === 'ENUM';
|
||||
@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mainColors, ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
const enumColors = Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]];
|
||||
const enumValueSchema = z.object({
|
||||
color: z.enum(enumColors),
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
export const isFieldEnumValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is z.infer<typeof enumValueSchema> =>
|
||||
enumValueSchema.safeParse(fieldValue).success;
|
||||
Reference in New Issue
Block a user