From bc787f72ba2f84f8acda83f9a5a055e01ba9f2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Tue, 28 Nov 2023 23:44:21 +0100 Subject: [PATCH] feat: add Select field preview and form (#2655) Closes #2432 --- .../components/SettingsObjectFieldPreview.tsx | 24 +++---- .../SettingsObjectFieldSelectForm.tsx | 58 +++++++++++++++++ .../SettingsObjectFieldTypeSelectSection.tsx | 17 ++++- .../constants/settingsFieldMetadataTypes.ts | 1 - .../data-model/hooks/useFieldMetadataForm.ts | 59 ++++++++++++++--- .../data-model/hooks/useFieldPreview.ts | 64 ++++++++++++++----- .../data-model/hooks/useFieldPreviewValue.ts | 25 ++++++++ ...iew.ts => useRelationFieldPreviewValue.ts} | 19 +++--- .../object/field/components/FieldDisplay.tsx | 21 +++--- .../display/components/SelectFieldDisplay.tsx | 9 +++ .../field/meta-types/hooks/useSelectField.ts | 47 ++++++++++++++ .../ui/object/field/types/FieldMetadata.ts | 21 ++++-- .../field/types/guards/assertFieldMetadata.ts | 3 + .../field/types/guards/isFieldEnumValue.ts | 14 ---- .../field/types/guards/isFieldSelect.ts | 6 ++ .../field/types/guards/isFieldSelectValue.ts | 14 ++++ .../data-model/SettingsObjectFieldEdit.tsx | 1 + .../SettingsObjectNewFieldStep2.tsx | 1 + 18 files changed, 317 insertions(+), 87 deletions(-) create mode 100644 front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx create mode 100644 front/src/modules/settings/data-model/hooks/useFieldPreviewValue.ts rename front/src/modules/settings/data-model/hooks/{useRelationFieldPreview.ts => useRelationFieldPreviewValue.ts} (57%) create mode 100644 front/src/modules/ui/object/field/meta-types/display/components/SelectFieldDisplay.tsx create mode 100644 front/src/modules/ui/object/field/meta-types/hooks/useSelectField.ts delete mode 100644 front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldSelect.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx index 0f3b5ed77..c57378a2e 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx @@ -10,15 +10,16 @@ import { Field } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect'; -import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes'; import { useFieldPreview } from '../hooks/useFieldPreview'; -import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview'; + +import { SettingsObjectFieldSelectFormValues } from './SettingsObjectFieldSelectForm'; export type SettingsObjectFieldPreviewProps = { className?: string; fieldMetadata: Pick & { id?: string }; objectMetadataId: string; relationObjectMetadataId?: string; + selectOptions?: SettingsObjectFieldSelectFormValues; shrink?: boolean; }; @@ -73,6 +74,7 @@ export const SettingsObjectFieldPreview = ({ fieldMetadata, objectMetadataId, relationObjectMetadataId, + selectOptions, shrink, }: SettingsObjectFieldPreviewProps) => { const theme = useTheme(); @@ -81,27 +83,17 @@ export const SettingsObjectFieldPreview = ({ entityId, FieldIcon, fieldName, - hasValue, ObjectIcon, objectMetadataItem, + relationObjectMetadataItem, value, } = useFieldPreview({ fieldMetadata, objectMetadataId, + relationObjectMetadataId, + selectOptions, }); - const { defaultValue: relationDefaultValue, relationObjectMetadataItem } = - useRelationFieldPreview({ - relationObjectMetadataId, - skipDefaultValue: - fieldMetadata.type !== FieldMetadataType.Relation || hasValue, - }); - - const defaultValue = - fieldMetadata.type === FieldMetadataType.Relation - ? relationDefaultValue - : settingsFieldMetadataTypes[fieldMetadata.type].defaultValue; - return ( @@ -123,7 +115,7 @@ export const SettingsObjectFieldPreview = ({ diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx new file mode 100644 index 000000000..19e426264 --- /dev/null +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; + +import { TextInput } from '@/ui/input/components/TextInput'; +import { ThemeColor } from '@/ui/theme/constants/colors'; + +export type SettingsObjectFieldSelectFormValues = { + color: ThemeColor; + text: string; +}[]; + +type SettingsObjectFieldSelectFormProps = { + onChange: (values: SettingsObjectFieldSelectFormValues) => void; + values?: SettingsObjectFieldSelectFormValues; +}; + +const StyledLabel = styled.span` + color: ${({ theme }) => theme.font.color.light}; + display: block; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + text-transform: uppercase; +`; + +const StyledInputsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +const StyledOptionInput = styled(TextInput)` + & input { + height: ${({ theme }) => theme.spacing(2)}; + } +`; + +export const SettingsObjectFieldSelectForm = ({ + onChange, + values = [], +}: SettingsObjectFieldSelectFormProps) => { + return ( +
+ Options + + {values.map((value, index) => ( + { + const nextValues = [...values]; + nextValues.splice(index, 1, { ...values[index], text }); + onChange(nextValues); + }} + /> + ))} + +
+ ); +}; diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index 6060fb65f..0d99accf4 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -16,11 +16,16 @@ import { SettingsObjectFieldRelationForm, SettingsObjectFieldRelationFormValues, } from './SettingsObjectFieldRelationForm'; +import { + SettingsObjectFieldSelectForm, + SettingsObjectFieldSelectFormValues, +} from './SettingsObjectFieldSelectForm'; import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard'; export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{ type: FieldMetadataType; relation: SettingsObjectFieldRelationFormValues; + select: SettingsObjectFieldSelectFormValues; }>; type SettingsObjectFieldTypeSelectSectionProps = { @@ -54,6 +59,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ values, }: SettingsObjectFieldTypeSelectSectionProps) => { const relationFormConfig = values?.relation; + const selectFormConfig = values?.select; const fieldTypeOptions = Object.entries(settingsFieldMetadataTypes) .filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType)) @@ -80,6 +86,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ FieldMetadataType.Boolean, FieldMetadataType.Currency, FieldMetadataType.DateTime, + FieldMetadataType.Enum, FieldMetadataType.Link, FieldMetadataType.Number, FieldMetadataType.Relation, @@ -98,6 +105,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ relationObjectMetadataId={ relationFormConfig?.objectMetadataId } + selectOptions={selectFormConfig} /> {values.type === FieldMetadataType.Relation && !!relationFormConfig?.type && @@ -127,7 +135,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ } form={ - values.type === FieldMetadataType.Relation && ( + values.type === FieldMetadataType.Relation ? ( - ) + ) : values.type === FieldMetadataType.Enum ? ( + onChange({ select: nextValues })} + /> + ) : undefined } /> )} diff --git a/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts b/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts index 5bee58163..8ba5a59f3 100644 --- a/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts +++ b/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts @@ -61,7 +61,6 @@ export const settingsFieldMetadataTypes: Record< [FieldMetadataType.Enum]: { label: 'Select', Icon: IconTag, - defaultValue: { color: 'green', text: 'Option 1' }, }, [FieldMetadataType.Currency]: { label: 'Currency', diff --git a/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts index 1fdf060ed..c64e61a89 100644 --- a/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts +++ b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts @@ -2,6 +2,7 @@ import { useState } from 'react'; import { DeepPartial } from 'react-hook-form'; import { z } from 'zod'; +import { mainColors, ThemeColor } from '@/ui/theme/constants/colors'; import { FieldMetadataType, RelationMetadataType, @@ -16,6 +17,7 @@ type FormValues = { label: string; type: FieldMetadataType; relation: SettingsObjectFieldTypeSelectSectionFormValues['relation']; + select: SettingsObjectFieldTypeSelectSectionFormValues['select']; }; const defaultValues: FormValues = { @@ -25,6 +27,7 @@ const defaultValues: FormValues = { relation: { type: RelationMetadataType.OneToMany, }, + select: [{ color: 'green', text: 'Option 1' }], }; const fieldSchema = z.object({ @@ -48,7 +51,27 @@ const relationSchema = fieldSchema.merge( }), ); -const { Relation: _, ...otherFieldTypes } = FieldMetadataType; +const selectSchema = fieldSchema.merge( + z.object({ + type: z.literal(FieldMetadataType.Enum), + select: z + .array( + z.object({ + color: z.enum( + Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]], + ), + text: z.string().min(1), + }), + ) + .nonempty(), + }), +); + +const { + Enum: _Enum, + Relation: _Relation, + ...otherFieldTypes +} = FieldMetadataType; const otherFieldTypesSchema = fieldSchema.merge( z.object({ @@ -63,9 +86,13 @@ const otherFieldTypesSchema = fieldSchema.merge( const schema = z.discriminatedUnion('type', [ relationSchema, + selectSchema, otherFieldTypesSchema, ]); +type PartialFormValues = Partial & + DeepPartial>; + export const useFieldMetadataForm = () => { const [isInitialized, setIsInitialized] = useState(false); const [initialFormValues, setInitialFormValues] = @@ -73,14 +100,15 @@ export const useFieldMetadataForm = () => { const [formValues, setFormValues] = useState(defaultValues); const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false); const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false); + const [hasSelectFormChanged, setHasSelectFormChanged] = useState(false); const [validationResult, setValidationResult] = useState( schema.safeParse(formValues), ); const mergePartialValues = ( previousValues: FormValues, - nextValues: DeepPartial, - ) => ({ + nextValues: PartialFormValues, + ): FormValues => ({ ...previousValues, ...nextValues, relation: { @@ -93,7 +121,7 @@ export const useFieldMetadataForm = () => { }, }); - const initForm = (lazyInitialFormValues: DeepPartial) => { + const initForm = (lazyInitialFormValues: PartialFormValues) => { if (isInitialized) return; const mergedFormValues = mergePartialValues( @@ -107,16 +135,22 @@ export const useFieldMetadataForm = () => { setIsInitialized(true); }; - const handleFormChange = (values: DeepPartial) => { + const handleFormChange = (values: PartialFormValues) => { const nextFormValues = mergePartialValues(formValues, values); setFormValues(nextFormValues); setValidationResult(schema.safeParse(nextFormValues)); - const { relation: initialRelationFormValues, ...initialFieldFormValues } = - initialFormValues; - const { relation: nextRelationFormValues, ...nextFieldFormValues } = - nextFormValues; + const { + relation: initialRelationFormValues, + select: initialSelectFormValues, + ...initialFieldFormValues + } = initialFormValues; + const { + relation: nextRelationFormValues, + select: nextSelectFormValues, + ...nextFieldFormValues + } = nextFormValues; setHasFieldFormChanged( !isDeeplyEqual(initialFieldFormValues, nextFieldFormValues), @@ -125,13 +159,18 @@ export const useFieldMetadataForm = () => { nextFieldFormValues.type === FieldMetadataType.Relation && !isDeeplyEqual(initialRelationFormValues, nextRelationFormValues), ); + setHasSelectFormChanged( + nextFieldFormValues.type === FieldMetadataType.Enum && + !isDeeplyEqual(initialSelectFormValues, nextSelectFormValues), + ); }; return { formValues, handleFormChange, hasFieldFormChanged, - hasFormChanged: hasFieldFormChanged || hasRelationFormChanged, + hasFormChanged: + hasFieldFormChanged || hasRelationFormChanged || hasSelectFormChanged, hasRelationFormChanged, initForm, isInitialized, diff --git a/front/src/modules/settings/data-model/hooks/useFieldPreview.ts b/front/src/modules/settings/data-model/hooks/useFieldPreview.ts index e80270bc9..c8ad82f75 100644 --- a/front/src/modules/settings/data-model/hooks/useFieldPreview.ts +++ b/front/src/modules/settings/data-model/hooks/useFieldPreview.ts @@ -1,43 +1,73 @@ import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; -import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; -import { Field } from '~/generated-metadata/graphql'; -import { assertNotNull } from '~/utils/assert'; +import { Field, FieldMetadataType } from '~/generated-metadata/graphql'; + +import { SettingsObjectFieldSelectFormValues } from '../components/SettingsObjectFieldSelectForm'; +import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes'; + +import { useFieldPreviewValue } from './useFieldPreviewValue'; +import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue'; export const useFieldPreview = ({ fieldMetadata, objectMetadataId, + relationObjectMetadataId, + selectOptions, }: { - fieldMetadata: Partial>; + fieldMetadata: Pick & { id?: string }; objectMetadataId: string; + relationObjectMetadataId?: string; + selectOptions?: SettingsObjectFieldSelectFormValues; }) => { const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); const objectMetadataItem = findObjectMetadataItemById(objectMetadataId); - const { objects } = useFindManyObjectRecords({ - objectNamePlural: objectMetadataItem?.namePlural, - skip: !objectMetadataItem || !fieldMetadata.id, - }); - const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? ''); const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? ''); - const [firstRecord] = objects; const fieldName = fieldMetadata.id ? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name : undefined; - const value = - fieldMetadata.type !== 'RELATION' && fieldName - ? firstRecord?.[fieldName] - : undefined; + + const { value: firstRecordFieldValue } = useFieldPreviewValue({ + fieldName: fieldName || '', + objectNamePlural: objectMetadataItem?.namePlural || '', + skip: + !fieldName || + !objectMetadataItem || + fieldMetadata.type === FieldMetadataType.Relation, + }); + + const { relationObjectMetadataItem, value: relationValue } = + useRelationFieldPreviewValue({ + relationObjectMetadataId, + skip: fieldMetadata.type !== FieldMetadataType.Relation, + }); + + const defaultValue = + fieldMetadata.type === FieldMetadataType.Enum + ? selectOptions?.[0] + : settingsFieldMetadataTypes[fieldMetadata.type].defaultValue; + + const isValidSelectValue = + fieldMetadata.type === FieldMetadataType.Enum && + !!firstRecordFieldValue && + selectOptions?.some( + (selectOption) => selectOption.text === firstRecordFieldValue, + ); return { - entityId: firstRecord?.id || `${objectMetadataId}-no-records`, + entityId: `${objectMetadataId}-field-form`, FieldIcon, fieldName: fieldName || `${fieldMetadata.type}-new-field`, - hasValue: assertNotNull(value), ObjectIcon, objectMetadataItem, - value, + relationObjectMetadataItem, + value: + (fieldMetadata.type === FieldMetadataType.Relation + ? relationValue + : fieldMetadata.type !== FieldMetadataType.Enum || isValidSelectValue + ? firstRecordFieldValue + : undefined) || defaultValue, }; }; diff --git a/front/src/modules/settings/data-model/hooks/useFieldPreviewValue.ts b/front/src/modules/settings/data-model/hooks/useFieldPreviewValue.ts new file mode 100644 index 000000000..7d1d5c212 --- /dev/null +++ b/front/src/modules/settings/data-model/hooks/useFieldPreviewValue.ts @@ -0,0 +1,25 @@ +import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; +import { assertNotNull } from '~/utils/assert'; + +export const useFieldPreviewValue = ({ + fieldName, + objectNamePlural, + skip, +}: { + fieldName: string; + objectNamePlural: string; + skip?: boolean; +}) => { + const { objects } = useFindManyObjectRecords({ + objectNamePlural, + skip, + }); + + const firstRecordWithValue = objects.find( + (record) => assertNotNull(record[fieldName]) && record[fieldName] !== '', + ); + + return { + value: firstRecordWithValue?.[fieldName], + }; +}; diff --git a/front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts b/front/src/modules/settings/data-model/hooks/useRelationFieldPreviewValue.ts similarity index 57% rename from front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts rename to front/src/modules/settings/data-model/hooks/useRelationFieldPreviewValue.ts index a18c11adf..cfa810245 100644 --- a/front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts +++ b/front/src/modules/settings/data-model/hooks/useRelationFieldPreviewValue.ts @@ -1,13 +1,12 @@ import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; -import { capitalize } from '~/utils/string/capitalize'; -export const useRelationFieldPreview = ({ +export const useRelationFieldPreviewValue = ({ relationObjectMetadataId, - skipDefaultValue, + skip, }: { relationObjectMetadataId?: string; - skipDefaultValue: boolean; + skip?: boolean; }) => { const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); @@ -17,18 +16,16 @@ export const useRelationFieldPreview = ({ const { objects: relationObjects } = useFindManyObjectRecords({ objectNamePlural: relationObjectMetadataItem?.namePlural, - skip: skipDefaultValue || !relationObjectMetadataItem, + skip: skip || !relationObjectMetadataItem, }); - const mockValueName = capitalize( - relationObjectMetadataItem?.nameSingular ?? '', - ); + const label = relationObjectMetadataItem?.labelSingular ?? ''; return { relationObjectMetadataItem, - defaultValue: relationObjects?.[0] ?? { - company: { name: mockValueName }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented - name: mockValueName, + value: relationObjects?.[0] ?? { + company: { name: label }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented + name: label, }, }; }; diff --git a/front/src/modules/ui/object/field/components/FieldDisplay.tsx b/front/src/modules/ui/object/field/components/FieldDisplay.tsx index 760456a40..f8a3d96c9 100644 --- a/front/src/modules/ui/object/field/components/FieldDisplay.tsx +++ b/front/src/modules/ui/object/field/components/FieldDisplay.tsx @@ -1,28 +1,29 @@ import { useContext } from 'react'; -import { ChipFieldDisplay } from '@/ui/object/field/meta-types/display/components/ChipFieldDisplay'; -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'; +import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay'; import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay'; import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay'; import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay'; +import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay'; +import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay'; import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay'; import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; +import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay'; +import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay'; import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay'; +import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay'; import { isFieldCurrency } from '../types/guards/isFieldCurrency'; import { isFieldDateTime } from '../types/guards/isFieldDateTime'; import { isFieldEmail } from '../types/guards/isFieldEmail'; +import { isFieldFullName } from '../types/guards/isFieldFullName'; +import { isFieldLink } from '../types/guards/isFieldLink'; import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldRelation } from '../types/guards/isFieldRelation'; +import { isFieldSelect } from '../types/guards/isFieldSelect'; import { isFieldText } from '../types/guards/isFieldText'; +import { isFieldUuid } from '../types/guards/isFieldUuid'; export const FieldDisplay = () => { const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext); @@ -55,6 +56,8 @@ export const FieldDisplay = () => { ) : isFieldPhone(fieldDefinition) ? ( + ) : isFieldSelect(fieldDefinition) ? ( + ) : ( <> )} diff --git a/front/src/modules/ui/object/field/meta-types/display/components/SelectFieldDisplay.tsx b/front/src/modules/ui/object/field/meta-types/display/components/SelectFieldDisplay.tsx new file mode 100644 index 000000000..6e81e9f1b --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/display/components/SelectFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { Tag } from '@/ui/display/tag/components/Tag'; + +import { useSelectField } from '../../hooks/useSelectField'; + +export const SelectFieldDisplay = () => { + const { fieldValue } = useSelectField(); + + return ; +}; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useSelectField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useSelectField.ts new file mode 100644 index 000000000..a06a33244 --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/hooks/useSelectField.ts @@ -0,0 +1,47 @@ +import { useContext } from 'react'; +import { useRecoilState } from 'recoil'; + +import { ThemeColor } from '@/ui/theme/constants/colors'; + +import { FieldContext } from '../../contexts/FieldContext'; +import { useFieldInitialValue } from '../../hooks/useFieldInitialValue'; +import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector'; +import { FieldSelectValue } from '../../types/FieldMetadata'; +import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; +import { isFieldSelect } from '../../types/guards/isFieldSelect'; +import { isFieldSelectValue } from '../../types/guards/isFieldSelectValue'; + +export const useSelectField = () => { + const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); + + assertFieldMetadata('ENUM', isFieldSelect, fieldDefinition); + + const { fieldName } = fieldDefinition.metadata; + + const [fieldValue, setFieldValue] = useRecoilState( + entityFieldsFamilySelector({ + entityId: entityId, + fieldName: fieldName, + }), + ); + const fieldSelectValue = isFieldSelectValue(fieldValue) + ? fieldValue + : { color: 'green' as ThemeColor, text: '' }; + + const fieldInitialValue = useFieldInitialValue(); + + const initialValue = { + color: 'green' as ThemeColor, + text: fieldInitialValue?.isEmpty + ? '' + : fieldInitialValue?.value ?? fieldSelectValue?.text ?? '', + }; + + return { + fieldDefinition, + fieldValue: fieldSelectValue, + 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 2ebbacb93..71919a0a8 100644 --- a/front/src/modules/ui/object/field/types/FieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/FieldMetadata.ts @@ -1,4 +1,5 @@ import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; +import { ThemeColor } from '@/ui/theme/constants/colors'; export type FieldUuidMetadata = { objectMetadataNameSingular?: string; @@ -80,19 +81,24 @@ export type FieldRelationMetadata = { relationObjectMetadataNamePlural: string; }; +export type FieldSelectMetadata = { + fieldName: string; +}; + export type FieldMetadata = | FieldBooleanMetadata - | FieldNumberMetadata - | FieldDateTimeMetadata - | FieldTextMetadata - | FieldUuidMetadata | FieldCurrencyMetadata - | FieldLinkMetadata - | FieldPhoneMetadata + | FieldDateTimeMetadata | FieldEmailMetadata + | FieldFullNameMetadata + | FieldLinkMetadata + | FieldNumberMetadata + | FieldPhoneMetadata | FieldProbabilityMetadata | FieldRelationMetadata - | FieldFullNameMetadata; + | FieldSelectMetadata + | FieldTextMetadata + | FieldUuidMetadata; export type FieldTextValue = string; export type FieldUUidValue = string; @@ -109,5 +115,6 @@ export type FieldCurrencyValue = { }; export type FieldFullNameValue = { firstName: string; lastName: string }; export type FieldProbabilityValue = number; +export type FieldSelectValue = { color: ThemeColor; text: string }; export type FieldRelationValue = EntityForSelect | null; 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 8fec0dd26..878ae281b 100644 --- a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts @@ -11,6 +11,7 @@ import { FieldPhoneMetadata, FieldProbabilityMetadata, FieldRelationMetadata, + FieldSelectMetadata, FieldTextMetadata, FieldUuidMetadata, } from '../FieldMetadata'; @@ -28,6 +29,8 @@ type AssertFieldMetadataFunction = < ? FieldDateTimeMetadata : E extends 'EMAIL' ? FieldEmailMetadata + : E extends 'ENUM' + ? FieldSelectMetadata : E extends 'LINK' ? FieldLinkMetadata : E extends 'NUMBER' diff --git a/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts deleted file mode 100644 index 834457aac..000000000 --- a/front/src/modules/ui/object/field/types/guards/isFieldEnumValue.ts +++ /dev/null @@ -1,14 +0,0 @@ -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; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldSelect.ts b/front/src/modules/ui/object/field/types/guards/isFieldSelect.ts new file mode 100644 index 000000000..f4ab515f0 --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldSelect.ts @@ -0,0 +1,6 @@ +import { FieldDefinition } from '../FieldDefinition'; +import { FieldMetadata, FieldSelectMetadata } from '../FieldMetadata'; + +export const isFieldSelect = ( + field: FieldDefinition, +): field is FieldDefinition => field.type === 'ENUM'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts new file mode 100644 index 000000000..8f333b093 --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldSelectValue.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +import { mainColors, ThemeColor } from '@/ui/theme/constants/colors'; + +const selectColors = Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]]; +const selectValueSchema = z.object({ + color: z.enum(selectColors), + text: z.string(), +}); + +export const isFieldSelectValue = ( + fieldValue: unknown, +): fieldValue is z.infer => + selectValueSchema.safeParse(fieldValue).success; diff --git a/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 25700de2c..fece251da 100644 --- a/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -172,6 +172,7 @@ export const SettingsObjectFieldEdit = () => { values={{ type: formValues.type, relation: formValues.relation, + select: formValues.select, }} />
diff --git a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index 20d6f626e..91bb47fdc 100644 --- a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -223,6 +223,7 @@ export const SettingsObjectNewFieldStep2 = () => { values={{ type: formValues.type, relation: formValues.relation, + select: formValues.select, }} />