From fea0bbeb2ac0f4c201cc5b25307422b042766c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Fri, 17 Nov 2023 23:15:35 +0100 Subject: [PATCH] feat: add EnumFieldDisplay and Enum field preview (#2487) Closes #2428 Co-authored-by: Charles Bochet --- .../SettingsObjectFieldTypeSelectSection.tsx | 14 ++--- .../SettingsObjectFieldPreview.stories.tsx | 8 +++ .../data-model/constants/dataTypes.ts | 7 ++- .../data-model/types/ObjectFieldDataType.ts | 1 + .../modules/ui/display/tag/components/Tag.tsx | 9 ++- .../components/__stories__/Tag.stories.tsx | 11 ++++ .../object/field/components/FieldDisplay.tsx | 8 ++- .../display/components/EnumFieldDisplay.tsx | 9 +++ .../__stories__/EnumFieldDisplay.stories.tsx | 55 +++++++++++++++++++ .../field/meta-types/hooks/useEnumField.ts | 47 ++++++++++++++++ .../ui/object/field/types/FieldMetadata.ts | 8 +++ .../ui/object/field/types/FieldType.ts | 18 ++++-- .../field/types/guards/assertFieldMetadata.ts | 3 + .../object/field/types/guards/isFieldEnum.ts | 6 ++ .../field/types/guards/isFieldEnumValue.ts | 14 +++++ 15 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 front/src/modules/ui/object/field/meta-types/display/components/EnumFieldDisplay.tsx create mode 100644 front/src/modules/ui/object/field/meta-types/display/components/__stories__/EnumFieldDisplay.stories.tsx create mode 100644 front/src/modules/ui/object/field/meta-types/hooks/useEnumField.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldEnum.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index 2099105e9..801582e49 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -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, diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx index adad8371e..4a75619cd 100644 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx +++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx @@ -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, diff --git a/front/src/modules/settings/data-model/constants/dataTypes.ts b/front/src/modules/settings/data-model/constants/dataTypes.ts index 378e03035..d675c04c9 100644 --- a/front/src/modules/settings/data-model/constants/dataTypes.ts +++ b/front/src/modules/settings/data-model/constants/dataTypes.ts @@ -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 }, }; diff --git a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts index 0c47e0585..bf1b72f3c 100644 --- a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts +++ b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts @@ -1,6 +1,7 @@ export type MetadataFieldDataType = | 'BOOLEAN' | 'DATE' + | 'ENUM' | 'MONEY' | 'NUMBER' | 'RELATION' diff --git a/front/src/modules/ui/display/tag/components/Tag.tsx b/front/src/modules/ui/display/tag/components/Tag.tsx index 3eff13037..b198052e8 100644 --- a/front/src/modules/ui/display/tag/components/Tag.tsx +++ b/front/src/modules/ui/display/tag/components/Tag.tsx @@ -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} + {text} ); diff --git a/front/src/modules/ui/display/tag/components/__stories__/Tag.stories.tsx b/front/src/modules/ui/display/tag/components/__stories__/Tag.stories.tsx index 12995bf1c..e376a056e 100644 --- a/front/src/modules/ui/display/tag/components/__stories__/Tag.stories.tsx +++ b/front/src/modules/ui/display/tag/components/__stories__/Tag.stories.tsx @@ -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 = { args: { text: 'Urgent' }, argTypes: { diff --git a/front/src/modules/ui/object/field/components/FieldDisplay.tsx b/front/src/modules/ui/object/field/components/FieldDisplay.tsx index f0498a7ec..1651fd44a 100644 --- a/front/src/modules/ui/object/field/components/FieldDisplay.tsx +++ b/front/src/modules/ui/object/field/components/FieldDisplay.tsx @@ -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 = () => { ) : isFieldDoubleTextChip(fieldDefinition) ? ( + ) : isFieldEnum(fieldDefinition) ? ( + ) : ( <> )} diff --git a/front/src/modules/ui/object/field/meta-types/display/components/EnumFieldDisplay.tsx b/front/src/modules/ui/object/field/meta-types/display/components/EnumFieldDisplay.tsx new file mode 100644 index 000000000..798d0586d --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/display/components/EnumFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { Tag } from '@/ui/display/tag/components/Tag'; + +import { useEnumField } from '../../hooks/useEnumField'; + +export const EnumFieldDisplay = () => { + const { fieldValue } = useEnumField(); + + return ; +}; diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EnumFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EnumFieldDisplay.stories.tsx new file mode 100644 index 000000000..79fe1496b --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EnumFieldDisplay.stories.tsx @@ -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 }) => ( + + + + + ), + ComponentDecorator, + ], + component: EnumFieldDisplay, + args: { + value: { color: 'purple', text: 'Lorem ipsum' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useEnumField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useEnumField.ts new file mode 100644 index 000000000..9e09be1f6 --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/hooks/useEnumField.ts @@ -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( + 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, + }; +}; diff --git a/front/src/modules/ui/object/field/types/FieldMetadata.ts b/front/src/modules/ui/object/field/types/FieldMetadata.ts index 448fc8327..efcd272cf 100644 --- a/front/src/modules/ui/object/field/types/FieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/FieldMetadata.ts @@ -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 }; diff --git a/front/src/modules/ui/object/field/types/FieldType.ts b/front/src/modules/ui/object/field/types/FieldType.ts index 9eb28cab0..4ddcece85 100644 --- a/front/src/modules/ui/object/field/types/FieldType.ts +++ b/front/src/modules/ui/object/field/types/FieldType.ts @@ -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'; diff --git a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts index 320743295..5f05b75e9 100644 --- a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts @@ -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' diff --git a/front/src/modules/ui/object/field/types/guards/isFieldEnum.ts b/front/src/modules/ui/object/field/types/guards/isFieldEnum.ts new file mode 100644 index 000000000..69f337a68 --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldEnum.ts @@ -0,0 +1,6 @@ +import { FieldDefinition } from '../FieldDefinition'; +import { FieldEnumMetadata, FieldMetadata } from '../FieldMetadata'; + +export const isFieldEnum = ( + field: FieldDefinition, +): field is FieldDefinition => field.type === 'ENUM'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts new file mode 100644 index 000000000..834457aac --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts @@ -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 => + enumValueSchema.safeParse(fieldValue).success;