feat: add EnumFieldDisplay and Enum field preview (#2487)

Closes #2428

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thaïs
2023-11-17 23:15:35 +01:00
committed by GitHub
parent e72917c69c
commit fea0bbeb2a
15 changed files with 200 additions and 18 deletions

View File

@ -32,8 +32,8 @@ const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
margin-top: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(4)};
`; `;
// TODO: remove "relation" type for now, add it back when the backend is ready. // TODO: remove "enum" and "relation" types for now, add them back when the backend is ready.
const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes; const { ENUM: _ENUM, RELATION: _RELATION, ...allowedDataTypes } = dataTypes;
export const SettingsObjectFieldTypeSelectSection = ({ export const SettingsObjectFieldTypeSelectSection = ({
disabled, disabled,
@ -57,12 +57,10 @@ export const SettingsObjectFieldTypeSelectSection = ({
dropdownScopeId="object-field-type-select" dropdownScopeId="object-field-type-select"
value={fieldType} value={fieldType}
onChange={onChange} onChange={onChange}
options={Object.entries(dataTypesWithoutRelation).map( options={Object.entries(allowedDataTypes).map(([key, dataType]) => ({
([key, dataType]) => ({ value: key as FieldMetadataType,
value: key as FieldMetadataType, ...dataType,
...dataType, }))}
}),
)}
/> />
{['BOOLEAN', 'DATE', 'MONEY', 'NUMBER', 'TEXT', 'URL'].includes( {['BOOLEAN', 'DATE', 'MONEY', 'NUMBER', 'TEXT', 'URL'].includes(
fieldType, fieldType,

View File

@ -73,6 +73,14 @@ export const Number: Story = {
}, },
}; };
export const Select: Story = {
args: {
fieldIconKey: 'IconBuildingFactory2',
fieldLabel: 'Industry',
fieldType: FieldMetadataType.Enum,
},
};
export const CustomObject: Story = { export const CustomObject: Story = {
args: { args: {
isObjectCustom: true, isObjectCustom: true,

View File

@ -8,6 +8,7 @@ import {
IconNumbers, IconNumbers,
IconPhone, IconPhone,
IconPlug, IconPlug,
IconTag,
IconTextSize, IconTextSize,
IconUser, IconUser,
} from '@/ui/display/icon'; } from '@/ui/display/icon';
@ -52,6 +53,11 @@ export const dataTypes: Record<
Icon: IconCalendarEvent, Icon: IconCalendarEvent,
defaultValue: defaultDateValue.toISOString(), defaultValue: defaultDateValue.toISOString(),
}, },
[FieldMetadataType.Enum]: {
label: 'Select',
Icon: IconTag,
defaultValue: { color: 'green', text: 'Option 1' },
},
[FieldMetadataType.Currency]: { [FieldMetadataType.Currency]: {
label: 'Currency', label: 'Currency',
Icon: IconCoins, Icon: IconCoins,
@ -66,5 +72,4 @@ export const dataTypes: Record<
defaultValue: 50, defaultValue: 50,
}, },
[FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser }, [FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser },
[FieldMetadataType.Enum]: { label: 'Enum', Icon: IconPlug },
}; };

View File

@ -1,6 +1,7 @@
export type MetadataFieldDataType = export type MetadataFieldDataType =
| 'BOOLEAN' | 'BOOLEAN'
| 'DATE' | 'DATE'
| 'ENUM'
| 'MONEY' | 'MONEY'
| 'NUMBER' | 'NUMBER'
| 'RELATION' | 'RELATION'

View File

@ -35,10 +35,17 @@ const StyledTag = styled.h3<{
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(5)}; height: ${({ theme }) => theme.spacing(5)};
margin: 0; margin: 0;
overflow: hidden;
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ 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 = { export type TagProps = {
className?: string; className?: string;
color: ThemeColor; color: ThemeColor;
@ -52,6 +59,6 @@ export const Tag = ({ className, color, text, onClick }: TagProps) => (
color={castToTagColor(color)} color={castToTagColor(color)}
onClick={onClick} onClick={onClick}
> >
{text} <StyledContent>{text}</StyledContent>
</StyledTag> </StyledTag>
); );

View File

@ -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> = { export const Catalog: CatalogStory<Story, typeof Tag> = {
args: { text: 'Urgent' }, args: { text: 'Urgent' },
argTypes: { argTypes: {

View File

@ -1,9 +1,11 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { FullNameFieldDisplay } from '@/ui/object/field/meta-types/display/components/FullNameFieldDisplay'; 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 { RelationFieldDisplay } from '@/ui/object/field/meta-types/display/components/RelationFieldDisplay';
import { UuidFieldDisplay } from '@/ui/object/field/meta-types/display/components/UuidFieldDisplay'; import { UuidFieldDisplay } from '@/ui/object/field/meta-types/display/components/UuidFieldDisplay';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName'; 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 { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { FieldContext } from '../contexts/FieldContext'; 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 { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay'; import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay'; 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 { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay'; import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; 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 { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip'; import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail'; 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 { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldPhone } from '../types/guards/isFieldPhone';
@ -64,6 +66,8 @@ export const FieldDisplay = () => {
<ChipFieldDisplay /> <ChipFieldDisplay />
) : isFieldDoubleTextChip(fieldDefinition) ? ( ) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldDisplay /> <DoubleTextChipFieldDisplay />
) : isFieldEnum(fieldDefinition) ? (
<EnumFieldDisplay />
) : ( ) : (
<></> <></>
)} )}

View File

@ -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} />;
};

View File

@ -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 = {};

View File

@ -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,
};
};

View File

@ -1,5 +1,6 @@
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type FieldUuidMetadata = { export type FieldUuidMetadata = {
placeHolder: string; placeHolder: string;
@ -94,6 +95,10 @@ export type FieldBooleanMetadata = {
fieldName: string; fieldName: string;
}; };
export type FieldEnumMetadata = {
fieldName: string;
};
export type FieldMetadata = export type FieldMetadata =
| FieldBooleanMetadata | FieldBooleanMetadata
| FieldChipMetadata | FieldChipMetadata
@ -107,6 +112,7 @@ export type FieldMetadata =
| FieldNumberMetadata | FieldNumberMetadata
| FieldPhoneMetadata | FieldPhoneMetadata
| FieldProbabilityMetadata | FieldProbabilityMetadata
| FieldEnumMetadata
| FieldRelationMetadata | FieldRelationMetadata
| FieldTextMetadata | FieldTextMetadata
| FieldURLMetadata | FieldURLMetadata
@ -140,3 +146,5 @@ export type FieldDoubleTextChipValue = {
}; };
export type FieldRelationValue = EntityForSelect | null; export type FieldRelationValue = EntityForSelect | null;
export type FieldEnumValue = { color: ThemeColor; text: string };

View File

@ -1,19 +1,25 @@
export type FieldType = export type FieldType =
| 'BOOLEAN'
| 'UUID' | 'UUID'
| 'TEXT' | 'TEXT'
| 'RELATION' | 'RELATION'
| 'CHIP' | 'CHIP'
| 'DATE'
| 'DOUBLE_TEXT_CHIP' | 'DOUBLE_TEXT_CHIP'
| 'DOUBLE_TEXT' | 'DOUBLE_TEXT'
| 'NUMBER'
| 'EMAIL' | 'EMAIL'
| 'BOOLEAN' | 'ENUM'
| 'DATE' | 'MONEY_AMOUNT_V2'
| 'MONEY_AMOUNT'
| 'MONEY'
| 'NUMBER'
| 'PHONE'
| 'PROBABILITY'
| 'RELATION'
| 'TEXT'
| 'URL'
| 'PHONE' | 'PHONE'
| 'URL' | 'URL'
| 'LINK' | 'LINK'
| 'PROBABILITY'
| 'CURRENCY' | 'CURRENCY'
| 'MONEY_AMOUNT'
| 'MONEY'
| 'FULL_NAME'; | 'FULL_NAME';

View File

@ -7,6 +7,7 @@ import {
FieldDoubleTextChipMetadata, FieldDoubleTextChipMetadata,
FieldDoubleTextMetadata, FieldDoubleTextMetadata,
FieldEmailMetadata, FieldEmailMetadata,
FieldEnumMetadata,
FieldFullnameMetadata, FieldFullnameMetadata,
FieldLinkMetadata, FieldLinkMetadata,
FieldMetadata, FieldMetadata,
@ -43,6 +44,8 @@ type AssertFieldMetadataFunction = <
? FieldLinkMetadata ? FieldLinkMetadata
: E extends 'MONEY_AMOUNT' : E extends 'MONEY_AMOUNT'
? FieldMoneyMetadata ? FieldMoneyMetadata
: E extends 'ENUM'
? FieldEnumMetadata
: E extends 'NUMBER' : E extends 'NUMBER'
? FieldNumberMetadata ? FieldNumberMetadata
: E extends 'PHONE' : E extends 'PHONE'

View File

@ -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';

View File

@ -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;