From 20fdb66bd322971575ca11b43107e3c3bc132843 Mon Sep 17 00:00:00 2001 From: Guillim Date: Thu, 16 Oct 2025 09:56:24 +0200 Subject: [PATCH] Morph-settings-relation-and-morph-merge (#15062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎯 Merge Settings Relation and Morph Relation Forms In the settings, we unify morph and relation into a single form. The form now automatically creates - a `RELATION` field when 1 destination object is selected - or a `MORPH_RELATION` field when 2+ objects are selected. Better UI labels (showing object name for single selection, "X Objects" for multiple), proper field editing controls on destination objects, and capitalized field labels. We still make sure the isMorphRelationEnabled feature flag prevents current users from accessing this feature Fixes https://github.com/twentyhq/core-team-issues/issues/1589 --- .../SettingsMorphRelationMultiSelect.tsx | 8 +- .../SettingsFieldTypeCategoryDescriptions.ts | 2 +- ...SettingsDataModelFieldSettingsFormCard.tsx | 35 +-- .../SettingsObjectNewFieldSelector.tsx | 24 +- ...=> SettingsDataModelFieldRelationForm.tsx} | 119 +++++++--- ...ettingsDataModelFieldRelationFormCard.tsx} | 18 +- ...gsDataModelFieldRelationPreviewContent.tsx | 0 ...DataModelFieldRelationPreviewImageCard.tsx | 0 ...gsFormDefaultValuesTargetFieldMetadata.ts} | 43 +++- ...ttingsFormInitialTargetObjectMetadatas.ts} | 19 +- .../fieldMetadataItemDisableFieldEdition.ts | 23 +- .../SettingsDataModelFieldRelationForm.tsx | 207 ------------------ ...DataModelFieldRelationSettingsFormCard.tsx | 105 --------- .../useRelationSettingsFormInitialValues.ts | 76 ------- .../SettingsDataModelRelationFieldPreview.tsx | 6 + .../SettingsObjectNewFieldConfigure.tsx | 76 ++++--- 16 files changed, 241 insertions(+), 520 deletions(-) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/{SettingsDataModelFieldMorphRelationForm.tsx => SettingsDataModelFieldRelationForm.tsx} (53%) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/{SettingsDataModelFieldMorphRelationFormCard.tsx => SettingsDataModelFieldRelationFormCard.tsx} (87%) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/{relation => morph-relation}/components/SettingsDataModelFieldRelationPreviewContent.tsx (100%) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/{relation => morph-relation}/components/SettingsDataModelFieldRelationPreviewImageCard.tsx (100%) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/hooks/{useMorphRelationSettingsFormDefaultValuesOnDestination.ts => useRelationSettingsFormDefaultValuesTargetFieldMetadata.ts} (57%) rename packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/hooks/{useMorphRelationSettingsFormInitialTargetMetadatas.ts => useRelationSettingsFormInitialTargetObjectMetadatas.ts} (75%) delete mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx delete mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx delete mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts diff --git a/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx b/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx index 60b2216f52..911f9d58e2 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx @@ -165,8 +165,8 @@ export const SettingsMorphRelationMultiSelect = ({ {isDisabled ? ( = { Basic: 'All the basic field types you need to start', Advanced: 'More advanced fields for advanced projects', - Relation: 'Create a relation with another object', + Relation: 'Create a relation with other objects', }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index 5825202cfb..fb56567b91 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -15,15 +15,11 @@ 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 { settingsDataModelFieldMorphRelationFormSchema } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm'; - -import { SettingsDataModelFieldMorphRelationFormCard } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard'; +import { settingsDataModelFieldMorphRelationFormSchema } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationForm'; import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm'; import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard'; import { settingsDataModelFieldPhonesFormSchema } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm'; import { SettingsDataModelFieldPhonesSettingsFormCard } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard'; -import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm'; -import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard'; import { settingsDataModelFieldMultiSelectFormSchema, settingsDataModelFieldSelectFormSchema, @@ -31,7 +27,9 @@ import { import { SettingsDataModelFieldSelectSettingsFormCard } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard'; import { settingsDataModelFieldMaxValuesSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldMaxValuesSchema'; import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; + import { Separator } from '@/settings/components/Separator'; +import { SettingsDataModelFieldRelationFormCard } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard'; import { useFormContext } from 'react-hook-form'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; @@ -58,12 +56,10 @@ const dateTimeFieldFormSchema = z .extend(settingsDataModelFieldDateFormSchema.shape) .extend(isUniqueFieldFormSchema.shape); -const relationFieldFormSchema = z - .object({ type: z.literal(FieldMetadataType.RELATION) }) - .extend(settingsDataModelFieldRelationFormSchema.shape); - const morphRelationFieldFormSchema = z - .object({ type: z.literal(FieldMetadataType.MORPH_RELATION) }) + .object({ + type: z.literal(FieldMetadataType.MORPH_RELATION), + }) .extend(settingsDataModelFieldMorphRelationFormSchema.shape); const selectFieldFormSchema = z @@ -115,7 +111,6 @@ const otherFieldsFormSchema = z omit(SETTINGS_FIELD_TYPE_CONFIGS, [ FieldMetadataType.BOOLEAN, FieldMetadataType.CURRENCY, - FieldMetadataType.RELATION, FieldMetadataType.MORPH_RELATION, FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT, @@ -141,7 +136,6 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion( currencyFieldFormSchema, dateFieldFormSchema, dateTimeFieldFormSchema, - relationFieldFormSchema, morphRelationFieldFormSchema, selectFieldFormSchema, multiSelectFieldFormSchema, @@ -231,19 +225,12 @@ export const SettingsDataModelFieldSettingsFormCard = ({ ); } - if (fieldType === FieldMetadataType.RELATION) { + if ( + fieldType === FieldMetadataType.RELATION || + fieldType === FieldMetadataType.MORPH_RELATION + ) { return ( - - ); - } - - if (fieldType === FieldMetadataType.MORPH_RELATION) { - return ( - {' '} @@ -136,12 +132,18 @@ export const SettingsObjectNewFieldSelector = ({ {fieldTypeConfigs .filter(([, config]) => config.category === category) - .filter(([key]) => { - return ( - key !== FieldMetadataType.MORPH_RELATION || - isMorphRelationEnabled - ); - }) + // by default, we hide the relation type and create only the morph relation type + // on submit the new field, we choose the relation type based on the amount of target object + .filter(([key]) => key !== FieldMetadataType.RELATION) + .map( + ([key, config]) => + [ + key, + key === FieldMetadataType.MORPH_RELATION + ? { ...config, label: 'Relation' } + : config, + ] as [string, SettingsFieldTypeConfig], + ) .map(([key, config]) => ( ` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: ${({ isMobile }) => (isMobile ? '1fr' : '1fr 1fr')}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; +const StyledInputsLabel = 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(1)}; +`; + +const StyledInputsContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map( + ([value, { label, Icon }]) => ({ + label, + value: value as RelationType, + Icon, + }), +); export const settingsDataModelFieldMorphRelationFormSchema = z.object({ - morphRelationObjectMetadataIds: z.array(z.uuid()).min(2), + morphRelationObjectMetadataIds: z.array(z.uuid()).min(1), relationType: z.enum( Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]], ), @@ -37,29 +65,29 @@ export type SettingsDataModelFieldMorphRelationFormValues = z.infer< typeof settingsDataModelFieldMorphRelationFormSchema >; -type SettingsDataModelFieldMorphRelationFormProps = { +type SettingsDataModelFieldRelationFormProps = { existingFieldMetadataId: string; disabled?: boolean; }; -export const SettingsDataModelFieldMorphRelationForm = ({ +export const SettingsDataModelFieldRelationForm = ({ existingFieldMetadataId, disabled = false, -}: SettingsDataModelFieldMorphRelationFormProps) => { +}: SettingsDataModelFieldRelationFormProps) => { const { t } = useLingui(); const { control } = useFormContext(); - + const { getIcon } = useIcons(); const { fieldMetadataItem: existingFieldMetadataItem } = useFieldMetadataItemById(existingFieldMetadataId); const disableRelationEdition = isDefined(existingFieldMetadataItem); - const disableFieldEdition = isDefined(existingFieldMetadataItem) - ? fieldMetadataItemDisableFieldEdition(existingFieldMetadataItem) - : false; + const disableFieldEdition = useFieldMetadataItemDisableFieldEdition( + existingFieldMetadataItem, + ); const initialRelationObjectMetadataItems = - useMorphRelationSettingsFormInitialTargetMetadatas({ + useRelationSettingsFormInitialTargetObjectMetadatas({ fieldMetadataItem: existingFieldMetadataItem, }); @@ -68,7 +96,7 @@ export const SettingsDataModelFieldMorphRelationForm = ({ RelationType.ONE_TO_MANY; const { label: defaultLabelOnDestination, icon: defaultIconOnDestination } = - useMorphRelationSettingsFormDefaultValuesOnDestination({ + useRelationSettingsFormDefaultValuesTargetFieldMetadata({ fieldMetadataItem: existingFieldMetadataItem, objectMetadataItem: initialRelationObjectMetadataItems[0], relationType: initialRelationType, @@ -80,6 +108,12 @@ export const SettingsDataModelFieldMorphRelationForm = ({ ); const isMobile = useIsMobile(); + const isMorphRelationEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_MORPH_RELATION_ENABLED, + ); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + return ( @@ -104,17 +138,40 @@ export const SettingsDataModelFieldMorphRelationForm = ({ name="morphRelationObjectMetadataIds" control={control} defaultValue={initialMorphRelationsObjectMetadataIds} - render={({ field: { onChange, value } }) => ( - - )} + render={({ field: { onChange, value } }) => + isMorphRelationEnabled ? ( + + ) : ( + - )} - /> - ( -