diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts index 23a380707c..fc188df7a8 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts @@ -229,3 +229,5 @@ idealCustomerProfile ); }); }); + +// todo @guillim add a test for the morph relation diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 433d05bcdc..4060cd8c55 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -1,5 +1,4 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { isUndefined } from '@sniptt/guards'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -7,7 +6,11 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField'; import { type ObjectPermissions } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { + computeMorphRelationFieldJoinColumnName, + computeMorphRelationFieldName, + isDefined, +} from 'twenty-shared/utils'; import { type FieldMetadataItem } from '../types/FieldMetadataItem'; type MapFieldMetadataToGraphQLQueryArgs = { @@ -15,7 +18,7 @@ type MapFieldMetadataToGraphQLQueryArgs = { gqlField: string; fieldMetadata: Pick< FieldMetadataItem, - 'name' | 'type' | 'relation' | 'settings' + 'name' | 'type' | 'relation' | 'morphRelations' | 'settings' >; relationRecordGqlFields?: RecordGqlFields; computeReferences?: boolean; @@ -41,6 +44,90 @@ export const mapFieldMetadataToGraphQLQuery = ({ return gqlField; } + if ( + fieldType === FieldMetadataType.MORPH_RELATION && + (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY || + fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) + ) { + let gqlMorphField = ''; + for (const morphRelation of fieldMetadata.morphRelations ?? []) { + const relationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings?.relationType, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }); + const relationMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === morphRelation.targetObjectMetadata.id, + ); + + if (!isDefined(relationMetadataItem)) { + continue; + } + + if ( + isDefined(objectPermissionsByObjectMetadataId) && + isDefined(relationMetadataItem.id) + ) { + if (!isDefined(morphRelation.targetObjectMetadata.id)) { + throw new Error( + `Target object metadata id not found with field metadata ${fieldMetadata.name}`, + ); + } + + const objectPermission = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + morphRelation.targetObjectMetadata.id, + ); + + if (!objectPermission.canReadObjectRecords) { + continue; + } + } + + if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { + gqlMorphField += `${relationFieldName} +{ + edges { + node ${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems, + objectMetadataItem: relationMetadataItem, + recordGqlFields: relationRecordGqlFields, + computeReferences, + isRootLevel: false, + objectPermissionsByObjectMetadataId, + })} + } +}`; + } + + if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) { + const joinColumnName = computeMorphRelationFieldJoinColumnName({ + name: fieldMetadata.name, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + }); + if (gqlField === joinColumnName) { + gqlMorphField += `${gqlField} + `; + continue; + } + + gqlMorphField += `${relationFieldName} +${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems, + objectMetadataItem: relationMetadataItem, + recordGqlFields: relationRecordGqlFields, + computeReferences, + isRootLevel: false, + objectPermissionsByObjectMetadataId, +})}`; + } + } + return `${gqlMorphField}`; + } + if ( fieldType === FieldMetadataType.RELATION && fieldMetadata.relation?.type === RelationType.MANY_TO_ONE @@ -51,7 +138,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ fieldMetadata.relation?.targetObjectMetadata.id, ); - if (isUndefined(relationMetadataItem)) { + if (!isDefined(relationMetadataItem)) { return ''; } @@ -100,7 +187,7 @@ ${mapObjectMetadataToGraphQLQuery({ fieldMetadata.relation?.targetObjectMetadata.id, ); - if (isUndefined(relationMetadataItem)) { + if (!isDefined(relationMetadataItem)) { return ''; } diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index 9f91c38238..32faf894d0 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -5,7 +5,7 @@ import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueri import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode'; import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; type MapObjectMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; @@ -46,22 +46,75 @@ export const mapObjectMetadataToGraphQLQuery = ({ const manyToOneRelationFields = objectMetadataItem?.readableFields .filter((field) => field.isActive) - .filter((field) => field.type === FieldMetadataType.RELATION) + .filter( + (field) => + field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.MORPH_RELATION, + ) .filter((field) => isDefined(field.settings?.joinColumnName)); const manyToOneRelationGqlFieldWithFieldMetadata = - manyToOneRelationFields.map((field) => ({ - gqlField: field.settings?.joinColumnName, - fieldMetadata: field, + manyToOneRelationFields.flatMap((field) => { + const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION; + if (!isMorphRelation) { + return { + gqlField: field.settings?.joinColumnName, + fieldMetadata: field, + }; + } + + if (!isDefined(field.morphRelations)) { + throw new Error( + `Field ${field.name} is missing, please refresh the page. If the problem persists, please contact support.`, + ); + } + + return field.morphRelations.map((morphRelation) => ({ + gqlField: computeMorphRelationFieldName({ + fieldName: field.name, + relationDirection: morphRelation.type, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }), + fieldMetadata: field, + })); + }); + + const readableFields = objectMetadataItem.readableFields.filter( + (fieldMetadata) => fieldMetadata.isActive, + ); + + const activeReadableFields = readableFields.flatMap((fieldMetadata) => { + const isMorphRelation = + fieldMetadata.type === FieldMetadataType.MORPH_RELATION; + if (!isMorphRelation) { + return [ + { + gqlField: fieldMetadata.name, + fieldMetadata, + }, + ]; + } + + if (!isDefined(fieldMetadata.morphRelations)) { + throw new Error( + `Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`, + ); + } + + return fieldMetadata.morphRelations.map((morphRelation) => ({ + gqlField: computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: morphRelation.type, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }), + fieldMetadata, })); + }); const gqlFieldWithFieldMetadataThatCouldBeQueried = [ - ...objectMetadataItem.readableFields - .filter((fieldMetadata) => fieldMetadata.isActive) - .map((fieldMetadata) => ({ - gqlField: fieldMetadata.name, - fieldMetadata, - })), + ...activeReadableFields, ...manyToOneRelationGqlFieldWithFieldMetadata, ].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) => gqlFieldWithFieldMetadataA.gqlField.localeCompare( diff --git a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts index cf3c22215f..822905b186 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts @@ -1,8 +1,8 @@ import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { isDefined } from 'twenty-shared/utils'; import { type FieldMetadataItem } from '../types/FieldMetadataItem'; @@ -18,16 +18,21 @@ export const shouldFieldBeQueried = ({ recordGqlFields?: RecordGqlOperationGqlRecordFields; }): any => { const isJoinColumn: boolean = - isFieldRelation(fieldMetadata) && + (isFieldRelation(fieldMetadata) || isFieldMorphRelation(fieldMetadata)) && fieldMetadata.settings.joinColumnName === gqlField; if ( isUndefinedOrNull(recordGqlFields) && - (fieldMetadata.type !== FieldMetadataType.RELATION || isJoinColumn) + !isFieldRelation(fieldMetadata) && + !isFieldMorphRelation(fieldMetadata) ) { return true; } + if (isUndefinedOrNull(recordGqlFields) && isJoinColumn) { + return true; + } + if ( isDefined(recordGqlFields) && isDefined(recordGqlFields[gqlField]) && diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields.ts index 1cdc641a71..6f9c4683f3 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields.ts @@ -9,7 +9,11 @@ export const generateDepthOneWithoutRelationsRecordGqlFields = ({ objectMetadataItem, }: GenerateDepthOneWithoutRelationsRecordGqlFields) => { return objectMetadataItem.fields - .filter((field) => field.type !== FieldMetadataType.RELATION) + .filter( + (field) => + field.type !== FieldMetadataType.RELATION && + field.type !== FieldMetadataType.MORPH_RELATION, + ) .reduce>((acc, field) => { return { ...acc, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldDisplay.tsx index f65e8e7cb5..8089dc462f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldDisplay.tsx @@ -23,6 +23,11 @@ import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/ import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText'; import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2'; + +import { MorphRelationManyToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay'; +import { MorphRelationOneToManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay'; +import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; +import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany'; import { isDefined } from 'twenty-shared/utils'; import { FieldContext } from '../contexts/FieldContext'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; @@ -73,6 +78,10 @@ export const FieldDisplay = () => { ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( + ) : isFieldMorphRelationManyToOne(fieldDefinition) ? ( + + ) : isFieldMorphRelationOneToMany(fieldDefinition) ? ( + ) : isFieldText(fieldDefinition) ? ( ) : isFieldUuid(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay.tsx new file mode 100644 index 0000000000..6f1806433f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay.tsx @@ -0,0 +1,33 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RecordChip } from '@/object-record/components/RecordChip'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useMorphRelationToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useMorphRelationToOneFieldDisplay'; +import { useContext } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const MorphRelationManyToOneFieldDisplay = () => { + const { morphFieldValuesWithObjectName } = + useMorphRelationToOneFieldDisplay(); + + const { disableChipClick, triggerEvent } = useContext(FieldContext); + + if (!isDefined(morphFieldValuesWithObjectName?.value)) { + return null; + } + + const isWorkspaceMemberFieldMetadataRelation = + morphFieldValuesWithObjectName.objectNameSingular === + CoreObjectNameSingular.WorkspaceMember; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx new file mode 100644 index 0000000000..0ad4ae0214 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx @@ -0,0 +1,45 @@ +import { RecordChip } from '@/object-record/components/RecordChip'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus'; +import { useMorphRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay'; + +import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; +import { Fragment, useContext } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const MorphRelationOneToManyFieldDisplay = () => { + const { morphValuesWithObjectNameSingular } = + useMorphRelationFromManyFieldDisplay(); + const { isFocused } = useFieldFocus(); + const { disableChipClick, triggerEvent } = useContext(FieldContext); + + if (!morphValuesWithObjectNameSingular) { + return null; + } + + return ( + + {morphValuesWithObjectNameSingular + .filter(isDefined) + .map((morphValueWithObjectNameSingular) => { + return ( + + {morphValueWithObjectNameSingular.value.map((record) => { + return ( + + ); + })} + + ); + })} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay.ts new file mode 100644 index 0000000000..354b8951a2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay.ts @@ -0,0 +1,61 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useContext } from 'react'; + +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; +import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; + +import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { FieldContext } from '../../contexts/FieldContext'; +import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; + +export const useMorphRelationFromManyFieldDisplay = () => { + const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); + + const { chipGeneratorPerObjectPerField } = useContext( + PreComputedChipGeneratorsContext, + ); + + if (!isDefined(chipGeneratorPerObjectPerField)) { + throw new Error('Chip generator per object per field is not defined'); + } + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + + const button = fieldDefinition.editButtonIcon; + + const morphValuesWithObjectNameSingular = useRecoilValue( + recordStoreMorphOneToManyValueWithObjectNameFamilySelector({ + recordId, + morphRelations: fieldDefinition.metadata.morphRelations, + fieldName: fieldDefinition.metadata.fieldName, + }), + ); + + const maxWidthForField = + isDefined(button) && isDefined(maxWidth) + ? maxWidth - FIELD_EDIT_BUTTON_WIDTH + : maxWidth; + + if ( + !isDefined(fieldDefinition.metadata.objectMetadataNameSingular) || + !isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular) + ) { + throw new Error('Object metadata name singular is not a non-empty string'); + } + + return { + fieldDefinition, + morphValuesWithObjectNameSingular, + maxWidth: maxWidthForField, + recordId, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationToOneFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationToOneFieldDisplay.ts new file mode 100644 index 0000000000..8b2e270b14 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useMorphRelationToOneFieldDisplay.ts @@ -0,0 +1,78 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useContext } from 'react'; + +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; + +import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; + +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; + +import { recordStoreMorphManyToOneValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useMorphRelationToOneFieldDisplay = () => { + const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); + + const { chipGeneratorPerObjectPerField } = useContext( + PreComputedChipGeneratorsContext, + ); + + if (!isDefined(chipGeneratorPerObjectPerField)) { + throw new Error('Chip generator per object per field is not defined'); + } + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + + const button = fieldDefinition.editButtonIcon; + + const fieldName = fieldDefinition.metadata.fieldName; + + const morphFieldValueWithObjectName = useRecoilValue( + recordStoreMorphManyToOneValueWithObjectNameFamilySelector({ + recordId, + morphRelations: fieldDefinition.metadata.morphRelations, + fieldName, + }), + ); + + if (!isDefined(morphFieldValueWithObjectName)) { + return { + fieldDefinition, + fieldValues: [], + maxWidth: maxWidth, + recordId, + generateRecordChipData: () => ({ + recordId: '', + objectNameSingular: '', + }), + }; + } + + const maxWidthForField = + isDefined(button) && isDefined(maxWidth) + ? maxWidth - FIELD_EDIT_BUTTON_WIDTH + : maxWidth; + + if ( + !isDefined(fieldDefinition.metadata.objectMetadataNameSingular) || + !isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular) + ) { + throw new Error('Object metadata name singular is not a non-empty string'); + } + + return { + fieldDefinition, + morphFieldValuesWithObjectName: morphFieldValueWithObjectName, + maxWidth: maxWidthForField, + recordId, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts index 015da36f37..c8ea860abb 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts @@ -48,7 +48,7 @@ export const useRelationToOneFieldDisplay = () => { !isDefined(fieldDefinition.metadata.objectMetadataNameSingular) || !isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular) ) { - throw new Error('Object metadata name singular is not a non-empty string'); + throw new Error('Object metadata name singular is empty'); } const fieldChipGenerator = diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne.ts index 2940263dbd..3460fa1348 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne.ts @@ -10,4 +10,4 @@ export const isFieldMorphRelationManyToOne = ( field: Pick, 'type' | 'metadata'>, ): field is FieldDefinition => isFieldMorphRelation(field) && - field.metadata.morphRelations?.[0].type === RelationType.MANY_TO_ONE; + field.metadata.relationType === RelationType.MANY_TO_ONE; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts index a93caa0d5e..5c217ffa75 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts @@ -5,6 +5,8 @@ import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/g import { generateDepthOneWithoutRelationsRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; export const useRecordTableRecordGqlFields = ({ objectMetadataItem, @@ -30,11 +32,41 @@ export const useRecordTableRecordGqlFields = ({ objectMetadataItem, }); + const gqlFieldsList = Object.fromEntries( + visibleTableColumns.flatMap((column) => { + const isMorphRelation = column.type === FieldMetadataType.MORPH_RELATION; + + if (!isMorphRelation) { + return [[column.metadata.fieldName, true]]; + } + + const fieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === column.fieldMetadataId, + ); + if ( + !isDefined(fieldMetadataItem) || + !isDefined(fieldMetadataItem.morphRelations) + ) { + throw new Error( + `Field ${column.metadata.fieldName} is missing, please refresh the page. If the problem persists, please contact support.`, + ); + } + + return fieldMetadataItem.morphRelations.map((morphRelation) => [ + computeMorphRelationFieldName({ + fieldName: fieldMetadataItem.name, + relationDirection: morphRelation.type, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }), + true, + ]); + }), + ); + const recordGqlFields: Record = { ...allDepthOneWithoutRelationsRecordGqlFields, - ...Object.fromEntries( - visibleTableColumns.map((column) => [column.metadata.fieldName, true]), - ), + ...gqlFieldsList, noteTargets: generateDepthOneRecordGqlFields({ objectMetadataItem: noteTargetObjectMetadataItem, }), diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts new file mode 100644 index 0000000000..a65303f5f5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts @@ -0,0 +1,46 @@ +import { selectorFamily } from 'recoil'; + +import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { RelationType } from 'twenty-shared/types'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; + +export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector = + selectorFamily({ + key: 'recordStoreMorphManyToOneValueWithObjectNameFamilySelector', + get: + ({ + fieldName, + recordId, + morphRelations, + }: { + fieldName: string; + recordId: string; + morphRelations: FieldMetadataItemRelation[]; + }) => + ({ get }) => { + const morphValuesWithObjectName = morphRelations.map( + (morphRelation) => { + const computedFieldName = computeMorphRelationFieldName({ + fieldName, + relationDirection: RelationType.MANY_TO_ONE, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }); + return { + objectNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + value: get(recordStoreFamilyState(recordId))?.[ + computedFieldName + ] as ObjectRecord, + }; + }, + ); + + const morphValueFiltered = morphValuesWithObjectName.filter( + (morphValue) => isDefined(morphValue.value), + ); + return morphValueFiltered.length > 0 ? morphValueFiltered[0] : null; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts new file mode 100644 index 0000000000..101b1c453e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts @@ -0,0 +1,43 @@ +import { selectorFamily } from 'recoil'; + +import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { RelationType } from 'twenty-shared/types'; +import { computeMorphRelationFieldName } from 'twenty-shared/utils'; + +export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector = + selectorFamily({ + key: 'recordStoreMorphOneToManyValueWithObjectNameFamilySelector', + get: + ({ + fieldName, + recordId, + morphRelations, + }: { + fieldName: string; + recordId: string; + morphRelations: FieldMetadataItemRelation[]; + }) => + ({ get }) => { + const morphValuesWithObjectName = morphRelations.map( + (morphRelation) => { + const computedFieldName = computeMorphRelationFieldName({ + fieldName, + relationDirection: RelationType.ONE_TO_MANY, + nameSingular: morphRelation.targetObjectMetadata.nameSingular, + namePlural: morphRelation.targetObjectMetadata.namePlural, + }); + return { + objectNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + value: (get(recordStoreFamilyState(recordId))?.[ + computedFieldName + ] || []) as ObjectRecord[], + }; + }, + ); + + return morphValuesWithObjectName; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx index 6add7edf27..76830780f2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx @@ -29,7 +29,7 @@ export const RecordTableCellFieldContextGeneric = ({ const updateRecord = useContext(RecordUpdateContext); let hasObjectReadPermissions = objectPermissions.canReadObjectRecords; - + // todo @guillim : adjust this to handle morph relations permissions display if ( isFieldRelationToOneObject(columnDefinition) || isFieldRelationFromManyObjects(columnDefinition) diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 94dee8f154..7eec016225 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -4,7 +4,10 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; export type GenerateEmptyFieldValueArgs = { - fieldMetadataItem: Pick; + fieldMetadataItem: Pick< + FieldMetadataItem, + 'type' | 'settings' | 'defaultValue' + >; shouldComputeFunctionDefaultValue?: boolean; }; // TODO strictly type each fieldValue following their FieldMetadataType @@ -62,11 +65,13 @@ export const generateEmptyFieldValue = ({ return null; } case FieldMetadataType.BOOLEAN: { - return true; + return fieldMetadataItem?.defaultValue ?? true; } case FieldMetadataType.RELATION: case FieldMetadataType.MORPH_RELATION: { - if (fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE) { + if ( + fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE + ) { return null; } diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx index a58796a77d..77f544d33f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx @@ -1,7 +1,6 @@ import { Separator } from '@/settings/components/Separator'; import { Controller, useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { addressSchema as addressFieldDefaultValueSchema, addressSettingsSchema, @@ -30,10 +29,7 @@ import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFro type SettingsDataModelFieldAddressFormProps = { disabled?: boolean; defaultCountry?: string; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; + existingFieldMetadataId: string; }; export const settingsDataModelFieldAddressFormSchema = z.object({ @@ -47,7 +43,7 @@ export type SettingsDataModelFieldTextFormValues = z.infer< export const SettingsDataModelFieldAddressForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldAddressFormProps) => { const { t } = useLingui(); const { control } = useFormContext(); @@ -66,33 +62,23 @@ export const SettingsDataModelFieldAddressForm = ({ Flag({ width: props.size, height: props.size }), })), ]; - const { initialDisplaySubFields, resetDefaultValueField } = - useAddressSettingsFormInitialValues({ fieldMetadataItem }); + const { + initialDisplaySubFields, + initialDefaultValue, + resetDefaultValueField, + } = useAddressSettingsFormInitialValues({ existingFieldMetadataId }); const { closeDropdown } = useCloseDropdown(); const reset = () => { resetDefaultValueField(); closeDropdown('addressSubFieldsId'); }; - const defaultDefaultValue = { - addressStreet1: "''", - addressStreet2: null, - addressCity: null, - addressState: null, - addressPostcode: null, - addressCountry: null, - addressLat: null, - addressLng: null, - }; return ( <> { const defaultCountry = value?.addressCountry || ''; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard.tsx index c13a014231..37f6577bc3 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard.tsx @@ -1,62 +1,46 @@ -import styled from '@emotion/styled'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; - import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldAddressForm, type SettingsDataModelFieldTextFormValues, } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm'; -import { useAddressSettingsFormInitialValues } from '@/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; import { useFormContext } from 'react-hook-form'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldAddressSettingsFormCardProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldAddressSettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldAddressSettingsFormCardProps) => { - const { initialDisplaySubFields } = useAddressSettingsFormInitialValues({ - fieldMetadataItem, - }); - const { watch: watchFormValue } = - useFormContext(); + const { watch } = useFormContext< + SettingsDataModelFieldTextFormValues & SettingsDataModelFieldEditFormValues + >(); return ( } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/__tests__/useAddressSettingsFormInitialValues.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/__tests__/useAddressSettingsFormInitialValues.test.tsx index 6a5a8c2ecc..600478be8a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/__tests__/useAddressSettingsFormInitialValues.test.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/__tests__/useAddressSettingsFormInitialValues.test.tsx @@ -1,6 +1,9 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { renderHook } from '@testing-library/react'; import { useFormContext } from 'react-hook-form'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; import { useAddressSettingsFormInitialValues } from '../useAddressSettingsFormInitialValues'; jest.mock('react-hook-form', () => ({ @@ -12,6 +15,11 @@ const mockUseFormContext = useFormContext as jest.MockedFunction< typeof useFormContext >; +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], + objectMetadataItems: generatedMockObjectMetadataItems, +}); + describe('useAddressSettingsFormInitialValues', () => { beforeEach(() => { jest.clearAllMocks(); @@ -21,8 +29,14 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should return all address subfields when no fieldMetadataItem is provided', () => { - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({}), + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: Wrapper, + }, ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -36,12 +50,14 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should return all address subfields when fieldMetadataItem has no settings', () => { - const fieldMetadataItem: Pick = { - settings: undefined, - }; - - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({ fieldMetadataItem }), + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: Wrapper, + }, ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -55,14 +71,20 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should return all address subfields when settings.subFields is null', () => { - const fieldMetadataItem: Pick = { - settings: { - subFields: null, - }, - }; + // const fieldMetadataItem: Pick = { + // settings: { + // subFields: null, + // }, + // }; - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({ fieldMetadataItem }), + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: Wrapper, + }, ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -76,14 +98,20 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should return all address subfields when settings.subFields is empty array', () => { - const fieldMetadataItem: Pick = { - settings: { - subFields: [], - }, - }; + // const fieldMetadataItem: Pick = { + // settings: { + // subFields: [], + // }, + // }; - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({ fieldMetadataItem }), + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: Wrapper, + }, ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -97,14 +125,25 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should return configured subFields when they exist', () => { - const fieldMetadataItem: Pick = { - settings: { - subFields: ['addressStreet1', 'addressCity', 'addressCountry'], - }, - }; + const newGeneratedMockObjectMetadataItems = addNewAddressToMetadataItems( + generatedMockObjectMetadataItems, + 'new-field', + ['addressStreet1', 'addressCity', 'addressCountry'], + ); - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({ fieldMetadataItem }), + const WrapperSpecific = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], + objectMetadataItems: newGeneratedMockObjectMetadataItems, + }); + + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: WrapperSpecific, + }, ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -115,8 +154,14 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should call resetField with all address subFields when resetDefaultValueField is called', () => { - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({}), + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: Wrapper, + }, ); result.current.resetDefaultValueField(); @@ -134,14 +179,22 @@ describe('useAddressSettingsFormInitialValues', () => { }); it('should handle partial subFields configuration', () => { - const fieldMetadataItem: Pick = { - settings: { - subFields: ['addressStreet1', 'addressCity'], + const WrapperSpecific = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], + objectMetadataItems: addNewAddressToMetadataItems( + generatedMockObjectMetadataItems, + 'new-field', + ['addressStreet1', 'addressCity'], + ), + }); + const { result } = renderHook( + () => + useAddressSettingsFormInitialValues({ + existingFieldMetadataId: 'new-field', + }), + { + wrapper: WrapperSpecific, }, - }; - - const { result } = renderHook(() => - useAddressSettingsFormInitialValues({ fieldMetadataItem }), ); expect(result.current.initialDisplaySubFields).toEqual([ @@ -150,3 +203,28 @@ describe('useAddressSettingsFormInitialValues', () => { ]); }); }); + +const addNewAddressToMetadataItems = ( + generatedMockObjectMetadataItems: ObjectMetadataItem[], + fieldNameId: string, + subFields: string[], +) => { + return generatedMockObjectMetadataItems + .filter((item) => item.nameSingular === 'company') + .map((item) => { + const fields = item.fields; + const addressField = fields.find( + (field) => field.type === FieldMetadataType.ADDRESS, + ); + if (!addressField) { + throw new Error('Address field not found'); + } + const newField = { + ...addressField, + id: fieldNameId, + type: FieldMetadataType.ADDRESS, + settings: { subFields }, + }; + return { ...item, fields: [...fields, newField] }; + }); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues.ts index 69cab6f8bd..d3b25983b1 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues.ts @@ -1,13 +1,19 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { type SettingsDataModelFieldTextFormValues } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm'; import { DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES } from '@/settings/data-model/fields/forms/address/constants/DefaultSelectionAddressWithMessages'; import { useFormContext } from 'react-hook-form'; +type UseAddressSettingsFormInitialValuesProps = { + existingFieldMetadataId: string; +}; + export const useAddressSettingsFormInitialValues = ({ - fieldMetadataItem, -}: { - fieldMetadataItem?: Pick; -}) => { + existingFieldMetadataId, +}: UseAddressSettingsFormInitialValuesProps) => { + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + const allAddressSubFields = DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES.map( (selectionAddres) => selectionAddres.value, ); @@ -17,6 +23,19 @@ export const useAddressSettingsFormInitialValues = ({ ? fieldMetadataItem.settings.subFields : allAddressSubFields; + const defaultDefaultValue = { + addressStreet1: "''", + addressStreet2: null, + addressCity: null, + addressState: null, + addressPostcode: null, + addressCountry: null, + addressLat: null, + addressLng: null, + }; + const initialDefaultValue = + fieldMetadataItem?.defaultValue ?? defaultDefaultValue; + const { resetField } = useFormContext(); const resetDefaultValueField = () => { @@ -26,6 +45,7 @@ export const useAddressSettingsFormInitialValues = ({ }; return { + initialDefaultValue, initialDisplaySubFields, resetDefaultValueField, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm.tsx index 9a84ef5a5c..fd5c0a7b45 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm.tsx @@ -1,7 +1,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions'; import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; @@ -18,17 +17,17 @@ export type SettingsDataModelFieldBooleanFormValues = z.infer< >; type SettingsDataModelFieldBooleanFormProps = { - fieldMetadataItem: Pick; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldBooleanForm = ({ - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldBooleanFormProps) => { const { t } = useLingui(); const { control } = useFormContext(); const { initialDefaultValue } = useBooleanSettingsFormInitialValues({ - fieldMetadataItem, + existingFieldMetadataId, }); return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard.tsx index 8baba9b328..1b1f5a44be 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard.tsx @@ -1,55 +1,44 @@ import { useFormContext } from 'react-hook-form'; -import styled from '@emotion/styled'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldBooleanForm, type SettingsDataModelFieldBooleanFormValues, } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm'; -import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldBooleanSettingsFormCardProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' - >; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldBooleanSettingsFormCard = ({ - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldBooleanSettingsFormCardProps) => { - const { initialDefaultValue } = useBooleanSettingsFormInitialValues({ - fieldMetadataItem, - }); - - const { watch: watchFormValue } = - useFormContext(); + const { watch } = useFormContext< + SettingsDataModelFieldBooleanFormValues & + SettingsDataModelFieldEditFormValues + >(); return ( } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues.ts index 0bf4181473..6c6451915f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues.ts @@ -1,13 +1,19 @@ import { useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { type SettingsDataModelFieldBooleanFormValues } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm'; +type UseBooleanSettingsFormInitialValuesProps = { + existingFieldMetadataId: string; +}; + export const useBooleanSettingsFormInitialValues = ({ - fieldMetadataItem, -}: { - fieldMetadataItem?: Pick; -}) => { + existingFieldMetadataId, +}: UseBooleanSettingsFormInitialValuesProps) => { + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + const initialDefaultValue = (fieldMetadataItem?.defaultValue as SettingsDataModelFieldBooleanFormValues['defaultValue']) ?? true; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx index 9526f3de47..d3d7c6e5d6 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx @@ -1,12 +1,12 @@ import { Controller, useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; - -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { Separator } from '@/settings/components/Separator'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { canBeUnique } from '@/settings/data-model/fields/forms/utils/canBeUnique.util'; import { t } from '@lingui/core/macro'; +import { type FieldMetadataType } from 'twenty-shared/types'; import { IconKey } from 'twenty-ui/display'; import { Toggle } from 'twenty-ui/input'; @@ -15,31 +15,42 @@ type SettingsDataModelFieldIsUniqueFormValues = { }; type SettingsDataModelFieldIsUniqueFormProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'isCustom' | 'settings' | 'isUnique' - > & - Partial<{ id: string }>; - objectMetadataItem: Pick; + objectNameSingular: string; + fieldType: FieldMetadataType; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldIsUniqueForm = ({ - fieldMetadataItem, - objectMetadataItem, + fieldType, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldIsUniqueFormProps) => { const { control } = useFormContext(); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + const hasStandardUniqueIndex = objectMetadataItem.indexMetadatas.some( (index) => index.isUnique && !index.isCustom && index.indexFieldMetadatas?.some( - (field) => field.fieldMetadataId === fieldMetadataItem.id, + (field) => field.fieldMetadataId === existingFieldMetadataId, ), ); - if (!canBeUnique(fieldMetadataItem)) { + if ( + !canBeUnique({ + type: fieldType, + isCustom: fieldMetadataItem?.isCustom ?? true, + }) + ) { return null; } 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 e2621c07aa..f4a213e90f 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 @@ -1,8 +1,6 @@ -import styled from '@emotion/styled'; import omit from 'lodash.omit'; import { z } from 'zod'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; import { settingsDataModelFieldAddressFormSchema } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm'; @@ -30,11 +28,10 @@ import { settingsDataModelFieldSelectFormSchema, } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm'; import { SettingsDataModelFieldSelectSettingsFormCard } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { useFormContext } from 'react-hook-form'; import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; const isUniqueFieldFormSchema = z.object({ isUnique: z.boolean().nullable().default(false), @@ -136,16 +133,10 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion( ); type SettingsDataModelFieldSettingsFormCardProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'isCustom' | 'settings' - > & - Partial>; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + fieldType: FieldMetadataType; + objectNameSingular: string; +}; const previewableTypes = [ FieldMetadataType.ARRAY, @@ -170,105 +161,110 @@ const previewableTypes = [ ]; export const SettingsDataModelFieldSettingsFormCard = ({ - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + fieldType, + objectNameSingular, }: SettingsDataModelFieldSettingsFormCardProps) => { - if (!previewableTypes.includes(fieldMetadataItem.type)) { + const { watch } = useFormContext(); + + if (!previewableTypes.includes(fieldType)) { return null; } - if (fieldMetadataItem.type === FieldMetadataType.BOOLEAN) { + if (fieldType === FieldMetadataType.BOOLEAN) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.CURRENCY) { + if (fieldType === FieldMetadataType.CURRENCY) { return ( ); } if ( - fieldMetadataItem.type === FieldMetadataType.DATE || - fieldMetadataItem.type === FieldMetadataType.DATE_TIME + fieldType === FieldMetadataType.DATE || + fieldType === FieldMetadataType.DATE_TIME ) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.RELATION) { + if (fieldType === FieldMetadataType.RELATION) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) { + if (fieldType === FieldMetadataType.MORPH_RELATION) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.NUMBER) { + if (fieldType === FieldMetadataType.NUMBER) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.TEXT) { + if (fieldType === FieldMetadataType.TEXT) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.ADDRESS) { + if (fieldType === FieldMetadataType.ADDRESS) { return ( ); } - if (fieldMetadataItem.type === FieldMetadataType.PHONES) { + if (fieldType === FieldMetadataType.PHONES) { return ( ); } if ( - fieldMetadataItem.type === FieldMetadataType.SELECT || - fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT + fieldType === FieldMetadataType.SELECT || + fieldType === FieldMetadataType.MULTI_SELECT ) { return ( ); } @@ -276,15 +272,22 @@ export const SettingsDataModelFieldSettingsFormCard = ({ return ( } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx index 443025d007..355910c48f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx @@ -64,7 +64,6 @@ const StyledSearchInput = styled(SettingsTextInput)` export const SettingsObjectNewFieldSelector = ({ excludedFieldTypes = [], - fieldMetadataItem, objectNamePlural, }: SettingsObjectNewFieldSelectorProps) => { const theme = useTheme(); @@ -80,13 +79,15 @@ export const SettingsObjectNewFieldSelector = ({ ); const { resetDefaultValueField: resetBooleanDefaultValueField } = - useBooleanSettingsFormInitialValues({ fieldMetadataItem }); + useBooleanSettingsFormInitialValues({ existingFieldMetadataId: 'new' }); const { resetDefaultValueField: resetCurrencyDefaultValueField } = - useCurrencySettingsFormInitialValues({ fieldMetadataItem }); + useCurrencySettingsFormInitialValues({ existingFieldMetadataId: 'new' }); const { resetDefaultValueField: resetSelectDefaultValueField } = - useSelectSettingsFormInitialValues({ fieldMetadataItem }); + useSelectSettingsFormInitialValues({ + fieldMetadataId: 'new', + }); const resetDefaultValueField = (nextValue: SettingsFieldType) => { switch (nextValue) { diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx index a8cbfae685..cbac1d9b4c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx @@ -37,8 +37,9 @@ const meta: Meta = { I18nFrontDecorator, ], args: { - fieldMetadataItem, - objectMetadataItem: mockedCompanyObjectMetadataItem, + existingFieldMetadataId: fieldMetadataItem.id, + fieldType: FieldMetadataType.TEXT, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, parameters: { container: { width: 512 }, @@ -53,18 +54,16 @@ export const Default: Story = {}; export const WithRelationForm: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'people', - ), + existingFieldMetadataId: 'new-field', + fieldType: FieldMetadataType.RELATION, + objectNameSingular: 'company', }, }; export const WithSelectForm: Story = { args: { - fieldMetadataItem: { - label: 'Industry', - icon: 'IconBuildingFactory2', - type: FieldMetadataType.SELECT, - }, + existingFieldMetadataId: 'new-field', + fieldType: FieldMetadataType.SELECT, + objectNameSingular: 'company', }, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx index c33d868e49..7e05ada426 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx @@ -1,6 +1,6 @@ import { Controller, useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { TEXT_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/components/text/constants/TextDataModelSelectOptions'; import { Select } from '@/ui/input/components/Select'; @@ -10,16 +10,7 @@ import { z } from 'zod'; type SettingsDataModelFieldTextFormProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - | 'icon' - | 'label' - | 'type' - | 'defaultValue' - | 'settings' - | 'isUnique' - | 'isCustom' - >; + existingFieldMetadataId: string; }; const textFieldDefaultValueSchema = z.object({ @@ -36,16 +27,20 @@ export type SettingsDataModelFieldTextFormValues = z.infer< export const SettingsDataModelFieldTextForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldTextFormProps) => { const { t } = useLingui(); + const { fieldMetadataItem: existingFieldMetadataItem } = + useFieldMetadataItemById(existingFieldMetadataId); + const { control } = useFormContext(); return ( { diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx index bc2faf3c6a..71702701dd 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx @@ -1,56 +1,47 @@ -import styled from '@emotion/styled'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm'; import { SettingsDataModelFieldTextForm } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; import { useFormContext } from 'react-hook-form'; +import { FieldMetadataType } from 'twenty-shared/types'; type SettingsDataModelFieldTextSettingsFormCardProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'isCustom' | 'settings' - > & - Partial<{ id: string }>; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldTextSettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldTextSettingsFormCardProps) => { const { watch } = useFormContext(); return ( } form={ <> } diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx index 199d2992d8..ab0bfff816 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx @@ -1,7 +1,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type FieldCurrencyFormat } from '@/object-record/record-field/ui/types/FieldMetadata'; import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema'; import { currencyFieldSettingsSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldSettingsSchema'; @@ -24,15 +23,12 @@ export type SettingsDataModelFieldCurrencyFormValues = z.infer< type SettingsDataModelFieldCurrencyFormProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldCurrencyForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldCurrencyFormProps) => { const { t } = useLingui(); const { @@ -40,7 +36,7 @@ export const SettingsDataModelFieldCurrencyForm = ({ initialCurrencyCodeValue, initialSettingsValue, } = useCurrencySettingsFormInitialValues({ - fieldMetadataItem, + existingFieldMetadataId, }); const { control } = useFormContext(); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard.tsx index cd886f67ef..aaf1eae051 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard.tsx @@ -1,60 +1,48 @@ -import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldCurrencyForm, type SettingsDataModelFieldCurrencyFormValues, } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm'; -import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldCurrencySettingsFormCardProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldCurrencySettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldCurrencySettingsFormCardProps) => { - const { initialDefaultValue, initialSettingsValue } = - useCurrencySettingsFormInitialValues({ - fieldMetadataItem, - }); - - const { watch: watchFormValue } = - useFormContext(); + const { watch } = useFormContext< + SettingsDataModelFieldCurrencyFormValues & + SettingsDataModelFieldEditFormValues + >(); return ( } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues.ts index 7335ad9bf9..bec48daf89 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues.ts @@ -1,21 +1,30 @@ import { useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { CurrencyCode } from '@/object-record/record-field/ui/types/CurrencyCode'; import { type SettingsDataModelFieldCurrencyFormValues } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm'; +import { isNonEmptyString } from '@sniptt/guards'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; type UseCurrencySettingsFormInitialValuesArgs = { - fieldMetadataItem?: Pick; + existingFieldMetadataId: string; }; export const useCurrencySettingsFormInitialValues = ({ - fieldMetadataItem, + existingFieldMetadataId, }: UseCurrencySettingsFormInitialValuesArgs) => { + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + const initialAmountMicrosValue = (fieldMetadataItem?.defaultValue?.amountMicros as number | null) ?? null; - const initialCurrencyCodeValue = - fieldMetadataItem?.defaultValue?.currencyCode ?? - applySimpleQuotesToString(CurrencyCode.USD); + const initialCurrencyCodeValue = isNonEmptyString( + stripSimpleQuotesFromString(fieldMetadataItem?.defaultValue?.currencyCode), + ) + ? fieldMetadataItem?.defaultValue?.currencyCode + : applySimpleQuotesToString(CurrencyCode.USD); + const initialFormValues: SettingsDataModelFieldCurrencyFormValues = { settings: { format: fieldMetadataItem?.settings?.format ?? 'short', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx index 8799573af9..bc1b91594f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx @@ -2,7 +2,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { validateCustomDateFormat } from '@/localization/utils/validateCustomDateFormat'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldDateDisplayFormat } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isDateFieldCustomDisplayFormat } from '@/object-record/record-field/ui/types/guards/isDateFIeldCustomDisplayFormat'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; @@ -45,12 +44,12 @@ export type SettingsDataModelFieldDateFormValues = z.infer< type SettingsDataModelFieldDateFormProps = { disabled?: boolean; - fieldMetadataItem: Pick; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldDateForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldDateFormProps) => { const { t } = useLingui(); @@ -59,7 +58,7 @@ export const SettingsDataModelFieldDateForm = ({ const { initialDisplayFormat, initialCustomUnicodeDateFormat } = useDateSettingsFormInitialValues({ - fieldMetadataItem, + fieldMetadataId: existingFieldMetadataId, }); const displayFormatFromForm = watch('settings.displayFormat'); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx index c02d4109b6..b42f81edce 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx @@ -1,75 +1,60 @@ -import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm'; import { SettingsDataModelFieldDateForm, type SettingsDataModelFieldDateFormValues, } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm'; -import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { type FieldMetadataType } from 'twenty-shared/types'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldDateSettingsFormCardProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'isCustom' | 'settings' | 'isUnique' - > & - Partial<{ id: string }>; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; - flex: 1 1 100%; -`; + fieldType: FieldMetadataType.DATE_TIME | FieldMetadataType.DATE; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldDateSettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + fieldType, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldDateSettingsFormCardProps) => { - const { initialDisplayFormat, initialCustomUnicodeDateFormat } = - useDateSettingsFormInitialValues({ - fieldMetadataItem, - }); - - const { watch: watchFormValue } = - useFormContext(); + const { watch } = useFormContext< + SettingsDataModelFieldDateFormValues & SettingsDataModelFieldEditFormValues + >(); return ( } form={ <> } diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts index fb17d50726..ec507ce8f8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts @@ -1,14 +1,18 @@ import { useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { FieldDateDisplayFormat } from '@/object-record/record-field/ui/types/FieldMetadata'; import { type SettingsDataModelFieldDateFormValues } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm'; +type UseDateSettingsFormInitialValuesProps = { + fieldMetadataId: string; +}; + export const useDateSettingsFormInitialValues = ({ - fieldMetadataItem, -}: { - fieldMetadataItem?: Pick; -}) => { + fieldMetadataId, +}: UseDateSettingsFormInitialValuesProps) => { + const { fieldMetadataItem } = useFieldMetadataItemById(fieldMetadataId); + const initialDisplayFormat = (fieldMetadataItem?.settings?.displayFormat as FieldDateDisplayFormat) ?? FieldDateDisplayFormat.USER_SETTINGS; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx index a1bbc4bf33..5de6d86d34 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx @@ -2,16 +2,14 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { StyledContainer } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsMorphRelationMultiSelect } from '@/settings/components/SettingsMorphRelationMultiSelect'; import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { useMorphRelationSettingsFormDefaultValuesOnDestination } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormDefaultValuesOnDestination'; import { useMorphRelationSettingsFormInitialTargetMetadatas } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormInitialTargetMetadatas'; import { fieldMetadataItemDisableFieldEdition } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemDisableFieldEdition'; -import { fieldMetadataItemHasMorphRelations } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemHasMorphRelations'; -import { fieldMetadataItemInitialRelationType } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { RELATION_TYPE_OPTIONS, StyledInputsContainer, @@ -23,7 +21,8 @@ import { Select } from '@/ui/input/components/Select'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useLingui } from '@lingui/react/macro'; -import { type RelationType } from '~/generated-metadata/graphql'; +import { isDefined } from 'twenty-shared/utils'; +import { RelationType } from '~/generated-metadata/graphql'; export const settingsDataModelFieldMorphRelationFormSchema = z.object({ morphRelationObjectMetadataIds: z.array(z.string().uuid()).min(2), @@ -39,33 +38,36 @@ export type SettingsDataModelFieldMorphRelationFormValues = z.infer< >; type SettingsDataModelFieldMorphRelationFormProps = { - fieldMetadataItem: Pick; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldMorphRelationForm = ({ - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldMorphRelationFormProps) => { const { t } = useLingui(); const { control } = useFormContext(); - const morphRelationPreviouslyCreated = - fieldMetadataItemHasMorphRelations(fieldMetadataItem); + const { fieldMetadataItem: existingFieldMetadataItem } = + useFieldMetadataItemById(existingFieldMetadataId); + + const disableRelationEdition = isDefined(existingFieldMetadataItem); + const disableFieldEdition = isDefined(existingFieldMetadataItem) + ? fieldMetadataItemDisableFieldEdition(existingFieldMetadataItem) + : false; - const disableRelationEdition = !!morphRelationPreviouslyCreated; - const disableFieldEdition = - fieldMetadataItemDisableFieldEdition(fieldMetadataItem); const initialRelationObjectMetadataItems = useMorphRelationSettingsFormInitialTargetMetadatas({ - fieldMetadataItem, + fieldMetadataItem: existingFieldMetadataItem, }); const initialRelationType = - fieldMetadataItemInitialRelationType(fieldMetadataItem); + existingFieldMetadataItem?.settings?.relationType ?? + RelationType.ONE_TO_MANY; const { label: defaultLabelOnDestination, icon: defaultIconOnDestination } = useMorphRelationSettingsFormDefaultValuesOnDestination({ - fieldMetadataItem, + fieldMetadataItem: existingFieldMetadataItem, objectMetadataItem: initialRelationObjectMetadataItems[0], relationType: initialRelationType, }); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard.tsx index 30f5d06bb3..3f82825bd2 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard.tsx @@ -1,132 +1,82 @@ -import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; -import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { SettingsDataModelFieldMorphRelationForm, type SettingsDataModelFieldMorphRelationFormValues, } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm'; -import { useMorphRelationSettingsFormInitialTargetMetadatas } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormInitialTargetMetadatas'; -import { fieldMetadataItemInitialRelationType } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType'; import { SettingsDataModelFieldRelationPreviewContent } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewContent'; import { SettingsDataModelRelationPreviewImage } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewImageCard'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; -import { SettingsDataModelMorphRelationFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelMorphRelationFieldPreviewCard'; +import { SettingsDataModelRelationFieldPreviewSubWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { isDefined } from 'twenty-shared/utils'; -import { - FieldMetadataType, - RelationType, - type Relation, -} from '~/generated-metadata/graphql'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldMorphRelationFormCardProps = { - fieldMetadataItem: Pick & - Partial>; -} & Pick; - -export const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; - -const StyledMorphRelationFieldPreviewCard = styled( - SettingsDataModelMorphRelationFieldPreviewCard, -)` - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldMorphRelationFormCard = ({ - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldMorphRelationFormCardProps) => { - const { watch: watchFormValue } = - useFormContext(); - const { findObjectMetadataItemById } = useFilteredObjectMetadataItems(); + const { watch } = useFormContext< + SettingsDataModelFieldMorphRelationFormValues & + SettingsDataModelFieldEditFormValues + >(); const isMobile = useIsMobile(); - const initialRelationType = - fieldMetadataItemInitialRelationType(fieldMetadataItem); - const initialRelationObjectMetadataItems = - useMorphRelationSettingsFormInitialTargetMetadatas({ - fieldMetadataItem, - }); - const initialRelationFieldMetadataItem = { - icon: initialRelationObjectMetadataItems[0].icon, - label: [RelationType.MANY_TO_ONE].includes(initialRelationType) - ? initialRelationObjectMetadataItems[0].namePlural - : initialRelationObjectMetadataItems[0].nameSingular, - }; - const initialRelationObjectMetadataItem = - initialRelationObjectMetadataItems[0]; + const { objectMetadataItems } = useObjectMetadataItems(); - const relationObjectMetadataId = watchFormValue( - 'morphRelationObjectMetadataIds.0', - initialRelationObjectMetadataItem?.id, - ); - const relationObjectMetadataItem = findObjectMetadataItemById( - relationObjectMetadataId, + const relationObjectMetadataIds: string[] = watch( + 'morphRelationObjectMetadataIds', + [], ); - if (!relationObjectMetadataItem) return null; + const relationObjectMetadataItems = relationObjectMetadataIds + .map((relationObjectMetadataId) => + objectMetadataItems.find((item) => item.id === relationObjectMetadataId), + ) + .filter(isDefined); - const relationType: RelationType = watchFormValue( + const fallbackRelationObjectMetadataItem = objectMetadataItems[0]; + + const relationType: RelationType = watch( 'relationType', - initialRelationType, + RelationType.ONE_TO_MANY, ); const relationTypeConfig = RELATION_TYPES[relationType]; - if (!isDefined(relationTypeConfig)) return null; - const oppositeRelationType = relationType === RelationType.MANY_TO_ONE ? RelationType.ONE_TO_MANY : RelationType.MANY_TO_ONE; - const morphRelationsForStyledFieldPreviewCard = watchFormValue( - 'morphRelationObjectMetadataIds', - )?.map((morphRelationObjectMetadataId) => { - const relationObjectMetadataItem = findObjectMetadataItemById( - morphRelationObjectMetadataId, - ); - if (!relationObjectMetadataItem) - throw new Error('Relation object metadata item not found'); - return { - targetObjectMetadata: { - id: relationObjectMetadataItem.id, - nameSingular: relationObjectMetadataItem.labelSingular, - labelSingular: relationObjectMetadataItem.labelSingular, - labelPlural: relationObjectMetadataItem.labelPlural, - icon: relationObjectMetadataItem.icon, - isCustom: relationObjectMetadataItem.isCustom, - isRemote: relationObjectMetadataItem.isRemote, - }, - }; - }); - return ( - - {morphRelationsForStyledFieldPreviewCard && ( - - )} + 0 + ? relationObjectMetadataItems.map((item) => item.nameSingular) + : [fallbackRelationObjectMetadataItem.nameSingular] + } + fieldPreviewTargetObjectNameSingular={objectNameSingular} + pluralizeLabel={watch('relationType') !== RelationType.MANY_TO_ONE} + /> } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType.ts deleted file mode 100644 index f112a4ea41..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { RelationType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -export const fieldMetadataItemInitialRelationType = ( - fieldMetadataItem: Pick, -) => { - const morphRelations = fieldMetadataItem?.morphRelations; - return isDefined(morphRelations) && morphRelations.length > 0 - ? morphRelations[0].type - : RelationType.ONE_TO_MANY; -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx index 32d7eb179a..30e5b23f91 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx @@ -1,7 +1,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { numberFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/numberFieldDefaultValueSchema'; import { Separator } from '@/settings/components/Separator'; import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter'; @@ -22,19 +22,20 @@ export type SettingsDataModelFieldNumberFormValues = z.infer< type SettingsDataModelFieldNumberFormProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; + existingFieldMetadataId: string; }; export const SettingsDataModelFieldNumberForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldNumberFormProps) => { const { t } = useLingui(); const { control } = useFormContext(); + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + return ( & - Partial<{ id: string }>; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldNumberSettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldNumberSettingsFormCardProps) => { const { watch } = useFormContext(); return ( } form={ <> } diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx index cf1b1af08b..7a73528f66 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx @@ -1,6 +1,6 @@ import { Controller, useFormContext } from 'react-hook-form'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue'; @@ -20,17 +20,14 @@ import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFro type SettingsDataModelFieldPhonesFormProps = { disabled?: boolean; defaultCountryCode?: string; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' - >; + existingFieldMetadataId: string; }; export const settingsDataModelFieldPhonesFormSchema = z.object({ defaultValue: phonesFieldDefaultValueSchema, }); -export type SettingsDataModelFieldTextFormValues = z.infer< +export type SettingsDataModelFieldPhonesFormValues = z.infer< typeof settingsDataModelFieldPhonesFormSchema >; @@ -38,10 +35,14 @@ export type CountryCodeOrEmpty = CountryCode | ''; export const SettingsDataModelFieldPhonesForm = ({ disabled, - fieldMetadataItem, + existingFieldMetadataId, }: SettingsDataModelFieldPhonesFormProps) => { const { t } = useLingui(); - const { control } = useFormContext(); + const { control } = useFormContext(); + + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); const countries = [ { label: t`No country`, value: '', Icon: IconCircleOff }, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard.tsx index 6f4853152a..20e4dc695d 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard.tsx @@ -1,51 +1,53 @@ -import styled from '@emotion/styled'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; - import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm'; -import { SettingsDataModelFieldPhonesForm } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm'; import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; + SettingsDataModelFieldPhonesForm, + type SettingsDataModelFieldPhonesFormValues, +} from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { useFormContext } from 'react-hook-form'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldPhonesSettingsFormCardProps = { disabled?: boolean; - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'label' | 'type' | 'isCustom' | 'settings' - > & - Partial<{ id: string }>; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldPhonesSettingsFormCard = ({ disabled, - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldPhonesSettingsFormCardProps) => { + const { watch } = useFormContext< + SettingsDataModelFieldPhonesFormValues & + SettingsDataModelFieldEditFormValues + >(); + return ( } form={ <> } diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx index 49e6665fa5..4b6493527d 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx @@ -3,7 +3,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema'; @@ -46,8 +45,8 @@ export type SettingsDataModelFieldRelationFormValues = z.infer< >; type SettingsDataModelFieldRelationFormProps = { - fieldMetadataItem: Pick; - objectMetadataItem: ObjectMetadataItem; + existingFieldMetadataId: string; + objectMetadataItem?: ObjectMetadataItem; }; export const StyledContainer = styled.div` @@ -83,7 +82,7 @@ export const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map( ); export const SettingsDataModelFieldRelationForm = ({ - fieldMetadataItem, + existingFieldMetadataId, objectMetadataItem, }: SettingsDataModelFieldRelationFormProps) => { const { t } = useLingui(); @@ -100,7 +99,7 @@ export const SettingsDataModelFieldRelationForm = ({ initialRelationObjectMetadataItem, initialRelationType, } = useRelationSettingsFormInitialValues({ - fieldMetadataItem, + existingFieldMetadataId, objectMetadataItem, }); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx index ad3866538f..6ab65abecf 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx @@ -1,8 +1,6 @@ -import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; -import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { @@ -11,58 +9,35 @@ import { } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm'; import { SettingsDataModelFieldRelationPreviewContent } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewContent'; import { SettingsDataModelRelationPreviewImage } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewImageCard'; -import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelRelationFieldPreviewSubWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { - FieldMetadataType, - type Relation, - RelationType, -} from '~/generated-metadata/graphql'; -type SettingsDataModelFieldRelationSettingsFormCardProps = { - fieldMetadataItem: Pick & - Partial>; - relationFieldMetadataItem?: FieldMetadataItem; -} & Pick; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - flex: 1 1 100%; -`; +type SettingsDataModelFieldRelationSettingsFormCardProps = { + existingFieldMetadataId: string; + objectNameSingular: string; +}; export const SettingsDataModelFieldRelationSettingsFormCard = ({ - fieldMetadataItem, - objectMetadataItem, + existingFieldMetadataId, + objectNameSingular, }: SettingsDataModelFieldRelationSettingsFormCardProps) => { - const { watch: watchFormValue } = - useFormContext(); - const { findObjectMetadataItemById } = useFilteredObjectMetadataItems(); + const { watch } = useFormContext< + SettingsDataModelFieldRelationFormValues & + SettingsDataModelFieldEditFormValues + >(); const isMobile = useIsMobile(); - const { - initialRelationObjectMetadataItem, - initialRelationType, - initialRelationFieldMetadataItem, - } = useRelationSettingsFormInitialValues({ - fieldMetadataItem, - objectMetadataItem, - }); - const relationObjectMetadataId = watchFormValue( - 'relation.objectMetadataId', - initialRelationObjectMetadataItem?.id, - ); - const relationObjectMetadataItem = findObjectMetadataItemById( - relationObjectMetadataId, - ); + const { objectMetadataItems } = useObjectMetadataItems(); - if (!relationObjectMetadataItem) return null; - - const relationType: RelationType = watchFormValue( - 'relation.type', - initialRelationType, + const relationObjectMetadataId = watch('relation.objectMetadataId'); + const relationObjectMetadataItem = objectMetadataItems.find( + (item) => item.id === relationObjectMetadataId, ); + const relationTargetField = watch('relation.field'); + + const relationType: RelationType = watch('relation.type'); const relationTypeConfig = RELATION_TYPES[relationType]; const oppositeRelationType = @@ -74,56 +49,50 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ - - } form={ } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts index 95d3f4ffcc..d6e239b646 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts @@ -1,22 +1,29 @@ import { useMemo } from 'react'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; -import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; -import { type SettingsDataModelFieldPreviewCardProps } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; import { isDefined } from 'twenty-shared/utils'; import { RelationType } from '~/generated-metadata/graphql'; +type UseRelationSettingsFormInitialValuesProps = { + existingFieldMetadataId: string; + objectMetadataItem?: Pick< + ObjectMetadataItem, + 'id' | 'icon' | 'labelSingular' | 'labelPlural' + >; +}; + export const useRelationSettingsFormInitialValues = ({ - fieldMetadataItem, + existingFieldMetadataId, objectMetadataItem, -}: { - fieldMetadataItem?: Pick; - objectMetadataItem?: SettingsDataModelFieldPreviewCardProps['objectMetadataItem']; -}) => { +}: UseRelationSettingsFormInitialValuesProps) => { const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); const getRelationMetadata = useGetRelationMetadata(); const { @@ -52,14 +59,10 @@ export const useRelationSettingsFormInitialValues = ({ const initialRelationType = relationTypeFromFieldMetadata ?? RelationType.ONE_TO_MANY; - const targetIsMorphRelation = - isDefined(relationFieldMetadataItem) && - isFieldMorphRelation(relationFieldMetadataItem); - return { disableFieldEdition: - relationFieldMetadataItem && - (!relationFieldMetadataItem.isCustom || targetIsMorphRelation), + isDefined(relationFieldMetadataItem) && + relationFieldMetadataItem?.isCustom === true, disableRelationEdition: !!relationFieldMetadataItem, initialRelationFieldMetadataItem: relationFieldMetadataItem ?? { icon: initialRelationObjectMetadataItem.icon ?? 'IconUsers', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx index 7d1f2e7f84..afeafb3503 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx @@ -3,10 +3,7 @@ import { type DropResult } from '@hello-pangea/dnd'; import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; -import { - type FieldMetadataItem, - type FieldMetadataItemOption, -} from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema'; import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/multiSelectFieldDefaultValueSchema'; import { selectFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/selectFieldDefaultValueSchema'; @@ -46,10 +43,8 @@ export type SettingsDataModelFieldSelectFormValues = z.infer< >; type SettingsDataModelFieldSelectFormProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'defaultValue' | 'options' | 'type' - >; + fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT; + existingFieldMetadataId: string; }; const StyledContainer = styled(CardContent)` @@ -112,10 +107,13 @@ const StyledButton = styled(LightButton)` `; export const SettingsDataModelFieldSelectForm = ({ - fieldMetadataItem, + existingFieldMetadataId, + fieldType, }: SettingsDataModelFieldSelectFormProps) => { const { initialDefaultValue, initialOptions } = - useSelectSettingsFormInitialValues({ fieldMetadataItem }); + useSelectSettingsFormInitialValues({ + fieldMetadataId: existingFieldMetadataId, + }); const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState); const { @@ -144,7 +142,7 @@ export const SettingsDataModelFieldSelectForm = ({ optionValue: FieldMetadataItemOption['value'], ) => isSelectOptionDefaultValue(optionValue, { - type: fieldMetadataItem.type, + type: fieldType, defaultValue: watchFormValue('defaultValue'), }); @@ -153,7 +151,7 @@ export const SettingsDataModelFieldSelectForm = ({ ) => { if (isOptionDefaultValue(optionValue)) return; - if (fieldMetadataItem.type === FieldMetadataType.SELECT) { + if (fieldType === FieldMetadataType.SELECT) { setFormValue('defaultValue', applySimpleQuotesToString(optionValue), { shouldDirty: true, }); @@ -163,7 +161,7 @@ export const SettingsDataModelFieldSelectForm = ({ const previousDefaultValue = getValues('defaultValue'); if ( - fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT && + fieldType === FieldMetadataType.MULTI_SELECT && (Array.isArray(previousDefaultValue) || previousDefaultValue === null) ) { setFormValue( @@ -182,7 +180,7 @@ export const SettingsDataModelFieldSelectForm = ({ ) => { if (!isOptionDefaultValue(optionValue)) return; - if (fieldMetadataItem.type === FieldMetadataType.SELECT) { + if (fieldType === FieldMetadataType.SELECT) { setFormValue('defaultValue', null, { shouldDirty: true }); return; } @@ -190,7 +188,7 @@ export const SettingsDataModelFieldSelectForm = ({ const previousDefaultValue = getValues('defaultValue'); if ( - fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT && + fieldType === FieldMetadataType.MULTI_SELECT && (Array.isArray(previousDefaultValue) || previousDefaultValue === null) ) { const nextDefaultValue = previousDefaultValue?.filter( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard.tsx index 25391091f4..a2b7cc76d2 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard.tsx @@ -1,19 +1,15 @@ -import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; import { type z } from 'zod'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { type settingsDataModelFieldMultiSelectFormSchema, SettingsDataModelFieldSelectForm, type settingsDataModelFieldSelectFormSchema, } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm'; -import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; -import { - SettingsDataModelFieldPreviewCard, - type SettingsDataModelFieldPreviewCardProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; +import { type FieldMetadataType } from 'twenty-shared/types'; +import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; type SettingsDataModelFieldSelectOrMultiSelectFormValues = z.infer< | typeof settingsDataModelFieldSelectFormSchema @@ -21,53 +17,40 @@ type SettingsDataModelFieldSelectOrMultiSelectFormValues = z.infer< >; type SettingsDataModelFieldSelectSettingsFormCardProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - | 'icon' - | 'label' - | 'type' - | 'defaultValue' - | 'options' - | 'isUnique' - | 'isCustom' - >; -} & Pick; - -const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; - flex: 1 1 100%; -`; + objectNameSingular: string; + fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT; + existingFieldMetadataId: string; +}; export const SettingsDataModelFieldSelectSettingsFormCard = ({ - fieldMetadataItem, - objectMetadataItem, + objectNameSingular, + fieldType, + existingFieldMetadataId, }: SettingsDataModelFieldSelectSettingsFormCardProps) => { - const { initialOptions, initialDefaultValue } = - useSelectSettingsFormInitialValues({ - fieldMetadataItem, - }); - - const { watch: watchFormValue } = - useFormContext(); + const { watch: watchFormValue } = useFormContext< + SettingsDataModelFieldSelectOrMultiSelectFormValues & + SettingsDataModelFieldEditFormValues + >(); return ( } form={ - <> - - + } /> ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts index 2261b9c7f3..3069ac5bff 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts @@ -2,10 +2,8 @@ import { useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; import { v4 } from 'uuid'; -import { - type FieldMetadataItem, - type FieldMetadataItemOption, -} from '@/object-metadata/types/FieldMetadataItem'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; import { type SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm'; import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils'; @@ -17,11 +15,15 @@ const DEFAULT_OPTION: FieldMetadataItemOption = { value: computeOptionValueFromLabel('Option 1'), }; +type UseSelectSettingsFormInitialValuesProps = { + fieldMetadataId: string; +}; + export const useSelectSettingsFormInitialValues = ({ - fieldMetadataItem, -}: { - fieldMetadataItem?: Pick; -}) => { + fieldMetadataId, +}: UseSelectSettingsFormInitialValuesProps) => { + const { fieldMetadataItem } = useFieldMetadataItemById(fieldMetadataId); + const initialDefaultValue = (fieldMetadataItem?.defaultValue as SettingsDataModelFieldSelectFormValues['defaultValue']) ?? null; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index d6c53ced4a..38e0ca2da7 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -1,38 +1,24 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { BooleanFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/BooleanFieldInput'; import { RatingFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RatingFieldInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect'; -import { SettingsDataModelSetPreviewRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect'; import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue'; -import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord'; -import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -export type SettingsDataModelFieldPreviewProps = { +type SettingsDataModelFieldPreviewProps = { fieldMetadataItem: Pick< FieldMetadataItem, - | 'icon' - | 'label' - | 'type' - | 'defaultValue' - | 'options' - | 'settings' - | 'relation' - > & { - id?: string; - name?: string; - }; - objectMetadataItem: ObjectMetadataItem; - relationObjectMetadataItem?: ObjectMetadataItem; + 'name' | 'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings' + >; + objectNameSingular: string; shrink?: boolean; withFieldLabel?: boolean; }; @@ -64,54 +50,35 @@ const StyledFieldLabel = styled.div` export const SettingsDataModelFieldPreview = ({ fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, + objectNameSingular, shrink, withFieldLabel = true, }: SettingsDataModelFieldPreviewProps) => { const theme = useTheme(); + const { labelIdentifierFieldMetadataItem } = + useLabelIdentifierFieldMetadataItem({ + objectNameSingular, + }); const { getIcon } = useIcons(); const FieldIcon = getIcon(fieldMetadataItem.icon); - // id and name are undefined in create mode (field does not exist yet) - // and defined in edit mode. const isLabelIdentifier = - !!fieldMetadataItem.id && - !!fieldMetadataItem.name && - isLabelIdentifierField({ - fieldMetadataItem: { - id: fieldMetadataItem.id, - name: fieldMetadataItem.name, - }, - objectMetadataItem, - }); + labelIdentifierFieldMetadataItem?.name === fieldMetadataItem.name; - const previewRecord = usePreviewRecord({ - objectMetadataItem, - skip: !isLabelIdentifier, - }); + const fieldName = fieldMetadataItem.name; + const recordId = `${objectNameSingular}-${fieldName}-preview`; const fieldPreviewValue = useFieldPreviewValue({ fieldMetadataItem, - relationObjectMetadataItem, skip: isLabelIdentifier, }); - const fieldName = - fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`; - const recordId = - previewRecord?.id ?? - `${objectMetadataItem.nameSingular}-${fieldName}-${fieldMetadataItem.relation?.type}-${relationObjectMetadataItem?.nameSingular}-preview`; - const metadata = { fieldName, - objectMetadataNameSingular: objectMetadataItem.nameSingular, - relationObjectMetadataNameSingular: - relationObjectMetadataItem?.nameSingular || '', + objectMetadataNameSingular: objectNameSingular, options: fieldMetadataItem.options ?? [], settings: fieldMetadataItem.settings, - relationType: fieldMetadataItem.relation?.type, }; return ( @@ -121,15 +88,11 @@ export const SettingsDataModelFieldPreview = ({ instanceId: 'record-field-component-instance-id', }} > - {isDefined(previewRecord) ? ( - - ) : ( - - )} + {!!withFieldLabel && ( @@ -147,7 +110,7 @@ export const SettingsDataModelFieldPreview = ({ fieldDefinition: { type: fieldMetadataItem.type, iconName: 'FieldIcon', - fieldMetadataId: fieldMetadataItem.id || '', + fieldMetadataId: '', label: fieldMetadataItem.label, metadata, defaultValue: fieldMetadataItem.defaultValue, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx deleted file mode 100644 index d3c29196ae..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import styled from '@emotion/styled'; - -import { - SettingsDataModelFieldPreview, - type SettingsDataModelFieldPreviewProps, -} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview'; -import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; -import { Card, CardContent } from 'twenty-ui/layout'; - -export type SettingsDataModelFieldPreviewCardProps = - SettingsDataModelFieldPreviewProps & { - className?: string; - pluralizeLabel?: boolean; - }; - -const StyledCard = styled(Card)` - border-radius: ${({ theme }) => theme.border.radius.md}; - color: ${({ theme }) => theme.font.color.primary}; -`; - -const StyledCardContent = styled(CardContent)` - padding: ${({ theme }) => theme.spacing(2)}; -`; - -export const SettingsDataModelFieldPreviewCard = ({ - className, - fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, - shrink, - withFieldLabel = true, - pluralizeLabel = false, -}: SettingsDataModelFieldPreviewCardProps) => { - return ( - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx new file mode 100644 index 0000000000..fd80d6513f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx @@ -0,0 +1,68 @@ +import styled from '@emotion/styled'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { SettingsDataModelFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview'; +import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; +import { Card, CardContent } from 'twenty-ui/layout'; +import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils'; + +type SettingsDataModelFieldPreviewWidgetProps = { + className?: string; + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings' + >; + objectNameSingular: string; + shrink?: boolean; + withFieldLabel?: boolean; + pluralizeLabel?: boolean; + fullWidth?: boolean; +}; + +const StyledCard = styled(Card)` + border-radius: ${({ theme }) => theme.border.radius.md}; + color: ${({ theme }) => theme.font.color.primary}; +`; + +const StyledCardContent = styled(CardContent)` + padding: ${({ theme }) => theme.spacing(2)}; +`; + +export const SettingsDataModelFieldPreviewWidget = ({ + className, + fieldMetadataItem, + objectNameSingular, + shrink, + withFieldLabel = true, + pluralizeLabel = false, +}: SettingsDataModelFieldPreviewWidgetProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + return ( + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelMorphRelationFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelMorphRelationFieldPreviewCard.tsx deleted file mode 100644 index 045ca5db38..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelMorphRelationFieldPreviewCard.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import styled from '@emotion/styled'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { SettingsDataModelFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview'; -import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; -import { Card, CardContent } from 'twenty-ui/layout'; - -export type SettingsDataModelMorphRelationFieldPreviewCardProps = { - fieldMetadataItem: Pick< - FieldMetadataItem, - | 'icon' - | 'label' - | 'type' - | 'defaultValue' - | 'options' - | 'settings' - | 'relation' - > & { - id?: string; - name?: string; - morphRelations: { - targetObjectMetadata: Pick< - ObjectMetadataItem, - | 'id' - | 'nameSingular' - | 'labelSingular' - | 'labelPlural' - | 'icon' - | 'isCustom' - | 'isRemote' - >; - }[]; - }; - objectMetadataItem: ObjectMetadataItem; - relationObjectMetadataItem?: ObjectMetadataItem; - shrink?: boolean; - withFieldLabel?: boolean; - className?: string; - pluralizeLabel?: boolean; -}; - -const StyledCard = styled(Card)` - border-radius: ${({ theme }) => theme.border.radius.md}; - color: ${({ theme }) => theme.font.color.primary}; -`; - -const StyledCardContent = styled(CardContent)` - padding: ${({ theme }) => theme.spacing(2)}; -`; - -export const SettingsDataModelMorphRelationFieldPreviewCard = ({ - className, - fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, - shrink, - withFieldLabel = true, - pluralizeLabel = false, -}: SettingsDataModelMorphRelationFieldPreviewCardProps) => { - const objectMetadataItems = fieldMetadataItem.morphRelations?.map( - (morphRelation) => morphRelation.targetObjectMetadata, - ); - - if (!objectMetadataItems || objectMetadataItems.length === 0) { - return null; - } - - return ( - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx new file mode 100644 index 0000000000..7a1926474e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx @@ -0,0 +1,165 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; +import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect'; +import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue'; +import { computeMorphRelationFieldName } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; +import { v4 } from 'uuid'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +type SettingsDataModelRelationFieldPreviewProps = { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'settings' + >; + relationTargetObjectNameSingular: string; + shrink?: boolean; + withFieldLabel?: boolean; +}; + +const StyledFieldPreview = styled.div<{ shrink?: boolean }>` + align-items: center; + background-color: ${({ theme }) => theme.background.primary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + height: fit-content; + line-height: 24px; + overflow: hidden; + padding: 0 + ${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))}; + white-space: nowrap; + margin-top: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; + padding-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledFieldLabel = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +export const SettingsDataModelRelationFieldPreview = ({ + fieldMetadataItem, + relationTargetObjectNameSingular, + shrink, + withFieldLabel = true, +}: SettingsDataModelRelationFieldPreviewProps) => { + const { objectMetadataItem: relationTargetObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: relationTargetObjectNameSingular, + }); + + const theme = useTheme(); + + const { getIcon } = useIcons(); + const FieldIcon = getIcon(fieldMetadataItem.icon); + + const fieldPreviewValue = useFieldPreviewValue({ + fieldMetadataItem, + relationObjectNameSingular: relationTargetObjectNameSingular, + }); + + const fieldName = v4(); + + const recordId = `${relationTargetObjectNameSingular}-${fieldName}-preview`; + + const metadata = { + fieldName, + objectMetadataNameSingular: 'company', + relationObjectMetadataNameSingular: relationTargetObjectNameSingular, + options: [], + settings: fieldMetadataItem.settings, + relationType: fieldMetadataItem.settings?.relationType, + morphRelations: + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION + ? [ + { + type: fieldMetadataItem.settings?.relationType, + sourceFieldMetadata: { + id: v4(), + name: fieldName, + }, + targetFieldMetadata: { + id: v4(), + name: 'does-not-matter', + }, + sourceObjectMetadata: { + id: v4(), + namePlural: 'does-not-matter', + nameSingular: 'does-not-matter', + }, + targetObjectMetadata: { + id: v4(), + namePlural: relationTargetObjectMetadataItem.namePlural, + nameSingular: relationTargetObjectMetadataItem.nameSingular, + }, + } satisfies FieldMetadataItemRelation, + ] + : [], + }; + + return ( + <> + + + + {!!withFieldLabel && ( + + + {fieldMetadataItem.label}: + + )} + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget.tsx new file mode 100644 index 0000000000..0c3dde5460 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget.tsx @@ -0,0 +1,67 @@ +import styled from '@emotion/styled'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { SettingsDataModelRelationFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview'; +import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; +import { isDefined } from 'twenty-shared/utils'; +import { Card, CardContent } from 'twenty-ui/layout'; + +export type SettingsDataModelRelationFieldPreviewSubWidgetProps = { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'settings' + >; + objectNameSingulars: string[]; + fieldPreviewTargetObjectNameSingular: string; + shrink?: boolean; + withFieldLabel?: boolean; + className?: string; + pluralizeLabel?: boolean; +}; + +const StyledCard = styled(Card)` + border-radius: ${({ theme }) => theme.border.radius.md}; + color: ${({ theme }) => theme.font.color.primary}; +`; + +const StyledCardContent = styled(CardContent)` + padding: ${({ theme }) => theme.spacing(2)}; +`; + +export const SettingsDataModelRelationFieldPreviewSubWidget = ({ + className, + fieldMetadataItem, + objectNameSingulars, + fieldPreviewTargetObjectNameSingular, + shrink, + withFieldLabel = true, + pluralizeLabel = false, +}: SettingsDataModelRelationFieldPreviewSubWidgetProps) => { + const { objectMetadataItems } = useObjectMetadataItems(); + + const targetObjectMetadataItems = objectNameSingulars + .map((nameSingular) => + objectMetadataItems.find((item) => item.nameSingular === nameSingular), + ) + .filter(isDefined); + + return ( + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect.tsx index 4dee6e9944..aa9ccec5ae 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect.tsx @@ -1,44 +1,28 @@ -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { settingsPreviewRecordIdState } from '@/settings/data-model/fields/preview/states/settingsPreviewRecordIdState'; import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; +import { useSetRecoilState } from 'recoil'; type SettingsDataModelSetFieldValueEffectProps = { recordId: string; - fieldName: string; + gqlFieldName: string; value: unknown; }; export const SettingsDataModelSetFieldValueEffect = ({ recordId, - fieldName, + gqlFieldName, value, }: SettingsDataModelSetFieldValueEffectProps) => { - const settingsPreviewRecordId = useRecoilValue(settingsPreviewRecordIdState); - - const upsertedPreviewRecord = useRecoilValue( - recordStoreFamilyState(settingsPreviewRecordId ?? ''), - ); - const setFieldValue = useSetRecoilState( recordStoreFamilySelector({ recordId, - fieldName, + fieldName: gqlFieldName, }), ); useEffect(() => { - if ( - isDefined(upsertedPreviewRecord) && - !!upsertedPreviewRecord[fieldName] - ) { - setFieldValue(upsertedPreviewRecord[fieldName]); - } else { - setFieldValue(value); - } - }, [value, setFieldValue, recordId, fieldName, upsertedPreviewRecord]); + setFieldValue(value); + }, [value, setFieldValue, recordId, gqlFieldName]); return null; }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect.tsx deleted file mode 100644 index aafb25f954..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { settingsPreviewRecordIdState } from '@/settings/data-model/fields/preview/states/settingsPreviewRecordIdState'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; - -type SettingsDataModelSetPreviewRecordEffectProps = { - record: ObjectRecord; -}; - -export const SettingsDataModelSetPreviewRecordEffect = ({ - record, -}: SettingsDataModelSetPreviewRecordEffectProps) => { - const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); - - const setSettingsPreviewRecordId = useSetRecoilState( - settingsPreviewRecordIdState, - ); - - useEffect(() => { - upsertRecordsInStore([record]); - setSettingsPreviewRecordId(record.id); - }, [record, upsertRecordsInStore, setSettingsPreviewRecordId]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx similarity index 79% rename from packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx rename to packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx index ec13459f5f..fbf50133fe 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx @@ -9,7 +9,7 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow'; import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; -import { SettingsDataModelFieldPreviewCard } from '../SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelFieldPreviewWidget } from '../SettingsDataModelFieldPreviewWidget'; const mockedCompanyObjectMetadataItem = getMockObjectMetadataItemOrThrow('company'); @@ -20,10 +20,10 @@ const mockedOpportunityObjectMetadataItem = const mockedPersonObjectMetadataItem = getMockObjectMetadataItemOrThrow('person'); -const meta: Meta = { +const meta: Meta = { title: - 'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewCard', - component: SettingsDataModelFieldPreviewCard, + 'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewWidget', + component: SettingsDataModelFieldPreviewWidget, decorators: [ MemoryRouterDecorator, ComponentDecorator, @@ -31,7 +31,7 @@ const meta: Meta = { SnackBarDecorator, ], args: { - objectMetadataItem: mockedPersonObjectMetadataItem, + objectNameSingular: mockedPersonObjectMetadataItem.nameSingular, }, parameters: { container: { width: 480 }, @@ -40,7 +40,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const LabelIdentifier: Story = { args: { @@ -65,7 +65,7 @@ export const Boolean: Story = { ({ name, type }) => name === 'idealCustomerProfile' && type === FieldMetadataType.BOOLEAN, ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, }; @@ -76,7 +76,7 @@ export const Currency: Story = { name === 'annualRecurringRevenue' && type === FieldMetadataType.CURRENCY, ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, }; @@ -85,7 +85,7 @@ export const Date: Story = { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( ({ type }) => type === FieldMetadataType.DATE_TIME, ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, }; @@ -95,7 +95,7 @@ export const Links: Story = { ({ name, type }) => name === 'linkedinLink' && type === FieldMetadataType.LINKS, ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, }; @@ -104,7 +104,7 @@ export const Number: Story = { fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type === FieldMetadataType.NUMBER, ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular, }, }; @@ -123,7 +123,7 @@ export const Relation: Story = { fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ name }) => name === 'company', ), - relationObjectMetadataItem: mockedCompanyObjectMetadataItem, + objectNameSingular: mockedPersonObjectMetadataItem.nameSingular, }, }; @@ -135,13 +135,13 @@ const selectFieldMetadataItem = getMockFieldMetadataItemOrThrow({ export const Select: Story = { args: { fieldMetadataItem: selectFieldMetadataItem, - objectMetadataItem: mockedOpportunityObjectMetadataItem, + objectNameSingular: mockedOpportunityObjectMetadataItem.nameSingular, }, }; export const MultiSelect: Story = { args: { - objectMetadataItem: mockedOpportunityObjectMetadataItem, + objectNameSingular: mockedOpportunityObjectMetadataItem.nameSingular, fieldMetadataItem: { ...structuredClone(selectFieldMetadataItem), type: FieldMetadataType.MULTI_SELECT, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx index f50045244c..806ff4db77 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx @@ -86,7 +86,7 @@ describe('useFieldPreviewValue', () => { () => useFieldPreviewValue({ fieldMetadataItem, - relationObjectMetadataItem, + relationObjectNameSingular: relationObjectMetadataItem?.nameSingular, }), { wrapper: Wrapper }, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts index 16f794cd1b..c1021bc78a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts @@ -1,6 +1,5 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRelationFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue'; import { getAddressFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue'; import { getCurrencyFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue'; @@ -8,33 +7,31 @@ import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils import { getMultiSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue'; import { getPhonesFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue'; import { getSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue'; +import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; type UseFieldPreviewParams = { fieldMetadataItem: Pick< FieldMetadataItem, - 'type' | 'options' | 'defaultValue' | 'relation' + 'type' | 'options' | 'defaultValue' | 'settings' >; - relationObjectMetadataItem?: ObjectMetadataItem; + relationObjectNameSingular?: string; skip?: boolean; }; export const useFieldPreviewValue = ({ fieldMetadataItem, - relationObjectMetadataItem, + relationObjectNameSingular, skip, }: UseFieldPreviewParams) => { const relationFieldPreviewValue = useRelationFieldPreviewValue({ - relationObjectMetadataItem: relationObjectMetadataItem ?? { - fields: [], - labelSingular: '', - labelIdentifierFieldMetadataId: '20202020-1000-4629-87e5-9a1fae1cc2fd', - nameSingular: CoreObjectNameSingular.Company, - }, + relationObjectNameSingular: + relationObjectNameSingular ?? CoreObjectNameSingular.Company, skip: skip || - fieldMetadataItem.type !== FieldMetadataType.RELATION || - !relationObjectMetadataItem, + (fieldMetadataItem.type !== FieldMetadataType.RELATION && + fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION) || + !isDefined(relationObjectNameSingular), }); if (skip === true) return null; @@ -42,8 +39,10 @@ export const useFieldPreviewValue = ({ switch (fieldMetadataItem.type) { case FieldMetadataType.CURRENCY: return getCurrencyFieldPreviewValue({ fieldMetadataItem }); + case FieldMetadataType.MORPH_RELATION: case FieldMetadataType.RELATION: - return fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE + return fieldMetadataItem.settings?.relationType === + RelationType.MANY_TO_ONE ? relationFieldPreviewValue : [relationFieldPreviewValue]; case FieldMetadataType.SELECT: @@ -56,7 +55,9 @@ export const useFieldPreviewValue = ({ return getPhonesFieldPreviewValue({ fieldMetadataItem }); default: return getFieldPreviewValue({ - fieldMetadataItem, + fieldType: fieldMetadataItem.type, + fieldSettings: fieldMetadataItem.settings, + defaultValue: fieldMetadataItem.defaultValue, }); } }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts index 8d420123e0..ada010016f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts @@ -1,41 +1,36 @@ +import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; import { isDefined } from 'twenty-shared/utils'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { pascalCase } from '~/utils/string/pascalCase'; type UsePreviewRecordParams = { - objectMetadataItem: Pick< - ObjectMetadataItem, - | 'fields' - | 'labelIdentifierFieldMetadataId' - | 'labelSingular' - | 'nameSingular' - >; + objectNameSingular: string; skip?: boolean; }; export const usePreviewRecord = ({ - objectMetadataItem, + objectNameSingular, skip: skipFromProps, }: UsePreviewRecordParams): ObjectRecord | null => { - const labelIdentifierFieldMetadataItem = - getLabelIdentifierFieldMetadataItem(objectMetadataItem); + const { labelIdentifierFieldMetadataItem } = + useLabelIdentifierFieldMetadataItem({ + objectNameSingular, + }); + const skip = skipFromProps || !labelIdentifierFieldMetadataItem; let recordGqlFields: Record | undefined = undefined; - if (objectMetadataItem.nameSingular === CoreObjectNameSingular.NoteTarget) + if (objectNameSingular === CoreObjectNameSingular.NoteTarget) recordGqlFields = { id: true, note: true }; - if (objectMetadataItem.nameSingular === CoreObjectNameSingular.TaskTarget) + if (objectNameSingular === CoreObjectNameSingular.TaskTarget) recordGqlFields = { id: true, task: true }; const { records } = useFindManyRecords({ - objectNameSingular: objectMetadataItem.nameSingular, + objectNameSingular, recordGqlFields, limit: 1, skip, @@ -55,15 +50,14 @@ export const usePreviewRecord = ({ return firstRecord; } - const fieldPreviewValue = - labelIdentifierFieldMetadataItem.type === FieldMetadataType.TEXT - ? objectMetadataItem.labelSingular - : getFieldPreviewValue({ - fieldMetadataItem: labelIdentifierFieldMetadataItem, - }); + const fieldPreviewValue = getFieldPreviewValue({ + fieldType: labelIdentifierFieldMetadataItem.type, + fieldSettings: labelIdentifierFieldMetadataItem.settings, + defaultValue: labelIdentifierFieldMetadataItem.defaultValue, + }); const placeholderRecord = { - __typename: pascalCase(objectMetadataItem.nameSingular), + __typename: pascalCase(objectNameSingular), id: '', [labelIdentifierFieldMetadataItem.name]: fieldPreviewValue, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts index 32be713447..de419e9b3f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts @@ -1,22 +1,15 @@ -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord'; type UseRelationFieldPreviewParams = { - relationObjectMetadataItem: Pick< - ObjectMetadataItem, - | 'fields' - | 'labelIdentifierFieldMetadataId' - | 'labelSingular' - | 'nameSingular' - >; + relationObjectNameSingular: string; skip?: boolean; }; export const useRelationFieldPreviewValue = ({ - relationObjectMetadataItem, + relationObjectNameSingular, skip, }: UseRelationFieldPreviewParams) => usePreviewRecord({ - objectMetadataItem: relationObjectMetadataItem, + objectNameSingular: relationObjectNameSingular, skip, }); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/states/settingsPreviewRecordIdState.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/states/settingsPreviewRecordIdState.ts deleted file mode 100644 index 1aaf9b3da1..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/states/settingsPreviewRecordIdState.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -export const settingsPreviewRecordIdState = createState({ - key: 'settingsPreviewRecordIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts index 79ce978a87..581b4725c8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts @@ -25,11 +25,13 @@ describe('getFieldPreviewValue', () => { // When const result = getFieldPreviewValue({ - fieldMetadataItem, + fieldType: fieldMetadataItem.type, + fieldSettings: fieldMetadataItem.settings, + defaultValue: fieldMetadataItem.defaultValue, }); // Then - expect(result).toBe(true); + expect(result).toBe(false); }); it('returns a placeholder defaultValue if the field metadata does not have a defaultValue', () => { @@ -45,7 +47,9 @@ describe('getFieldPreviewValue', () => { // When const result = getFieldPreviewValue({ - fieldMetadataItem, + fieldType: fieldMetadataItem.type, + fieldSettings: fieldMetadataItem.settings, + defaultValue: fieldMetadataItem.defaultValue, }); // Then @@ -68,7 +72,9 @@ describe('getFieldPreviewValue', () => { // When const result = getFieldPreviewValue({ - fieldMetadataItem, + fieldType: fieldMetadataItem.type, + fieldSettings: fieldMetadataItem.settings, + defaultValue: fieldMetadataItem.defaultValue, }); // Then diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts index f72a5fdd58..3a7fb418da 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts @@ -1,34 +1,41 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty'; import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue'; import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; +import { type FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { stripSimpleQuotesFromStringRecursive } from '~/utils/string/stripSimpleQuotesFromString'; type getFieldPreviewValueArgs = { - fieldMetadataItem: Pick; + fieldType: FieldMetadataType; + fieldSettings: FieldMetadata['settings']; + defaultValue: unknown; }; export const getFieldPreviewValue = ({ - fieldMetadataItem, + fieldType, + fieldSettings, + defaultValue, }: getFieldPreviewValueArgs) => { - if (!isFieldTypeSupportedInSettings(fieldMetadataItem.type)) return null; + if (!isFieldTypeSupportedInSettings(fieldType)) return null; if ( !isFieldValueEmpty({ - fieldDefinition: { type: fieldMetadataItem.type }, - fieldValue: stripSimpleQuotesFromStringRecursive( - fieldMetadataItem.defaultValue, - ), + fieldDefinition: { type: fieldType }, + fieldValue: stripSimpleQuotesFromStringRecursive(defaultValue), }) ) { return generateEmptyFieldValue({ - fieldMetadataItem, + fieldMetadataItem: { + type: fieldType, + settings: fieldSettings, + defaultValue, + }, shouldComputeFunctionDefaultValue: true, }); } - const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type); + const fieldTypeConfig = getSettingsFieldTypeConfig(fieldType); if ( isDefined(fieldTypeConfig) && diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx index e15cd09b56..f80d84b49e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx @@ -138,6 +138,7 @@ export const SettingsDataModelObjectPreview = ({ selected++; return selected <= 3 ? ( theme.background.transparent.lighter}; `; @@ -50,8 +46,8 @@ export const SettingsDataModelObjectSettingsFormCard = ({ Preview {labelIdentifierFieldMetadataItem ? ( - diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/getFieldPreviewValueFromRecord.ts b/packages/twenty-front/src/modules/settings/data-model/utils/getFieldPreviewValueFromRecord.ts index 6abe17b94c..d9511c3096 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/getFieldPreviewValueFromRecord.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/getFieldPreviewValueFromRecord.ts @@ -11,6 +11,7 @@ export const getFieldPreviewValueFromRecord = ({ }) => { const recordFieldValue = record[fieldMetadataItem.name]; + // todo @guillim : adjust this to handle morph relations // Relation fields (to many) if ( fieldMetadataItem.type === FieldMetadataType.RELATION && diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 56a85e4ad3..ff908d719e 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -37,7 +37,7 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; //TODO: fix this type -type SettingsDataModelFieldEditFormValues = z.infer< +export type SettingsDataModelFieldEditFormValues = z.infer< ReturnType > & any; @@ -91,6 +91,7 @@ export const SettingsObjectFieldEdit = () => { }, [navigateApp, objectMetadataItem, fieldMetadataItem]); const { isDirty, isValid, isSubmitting } = formConfig.formState; + const canSave = isDirty && isValid && !isSubmitting; if (!isDefined(objectMetadataItem) || !isDefined(fieldMetadataItem)) { @@ -236,8 +237,9 @@ export const SettingsObjectFieldEdit = () => { /> )} diff --git a/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx b/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx index b5593c678c..e28e9c35f6 100644 --- a/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx @@ -10,7 +10,6 @@ import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fie import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; -import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -48,7 +47,7 @@ export const SettingsObjectNewFieldConfigure = () => { const { objectNamePlural = '' } = useParams(); const [searchParams] = useSearchParams(); const fieldType = - (searchParams.get('fieldType') as SettingsFieldType) || + (searchParams.get('fieldType') as FieldMetadataType) || FieldMetadataType.TEXT; const { enqueueErrorSnackBar } = useSnackBar(); @@ -240,13 +239,9 @@ export const SettingsObjectNewFieldConfigure = () => { description={t`Customize field settings`} />
diff --git a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx index 0b6e7f7976..5f249ffc24 100644 --- a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx @@ -2,19 +2,25 @@ import { type ReactNode, useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; export const JestObjectMetadataItemSetter = ({ children, + objectMetadataItems, }: { children: ReactNode; + objectMetadataItems?: ObjectMetadataItem[]; }) => { const setObjectMetadataItems = useSetRecoilState(objectMetadataItemsState); const [isLoaded, setIsLoaded] = useState(false); + useEffect(() => { - setObjectMetadataItems(generatedMockObjectMetadataItems); + setObjectMetadataItems( + objectMetadataItems ?? generatedMockObjectMetadataItems, + ); setIsLoaded(true); - }, [setObjectMetadataItems]); + }, [objectMetadataItems, setObjectMetadataItems]); return isLoaded ? <>{children} : null; }; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx index aacb1852b1..2311ed513a 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx @@ -4,6 +4,7 @@ import { RecoilRoot, type MutableSnapshot } from 'recoil'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; @@ -15,12 +16,14 @@ export const getJestMetadataAndApolloMocksWrapper = ({ apolloMocks, cache, onInitializeRecoilSnapshot, + objectMetadataItems, }: { cache?: InMemoryCache; apolloMocks?: | readonly MockedResponse, Record>[] | undefined; onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; + objectMetadataItems?: ObjectMetadataItem[]; }) => { return ({ children }: { children: ReactNode }) => ( @@ -34,7 +37,9 @@ export const getJestMetadataAndApolloMocksWrapper = ({ - + diff --git a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts new file mode 100644 index 0000000000..6c73895785 --- /dev/null +++ b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts @@ -0,0 +1,13 @@ +import { capitalize } from '@/utils/strings'; + +type ComputeMorphRelationFieldJoinColumnNameArgs = { + name: string; + targetObjectMetadataNameSingular: string; +}; + +export const computeMorphRelationFieldJoinColumnName = ({ + name, + targetObjectMetadataNameSingular, +}: ComputeMorphRelationFieldJoinColumnNameArgs) => { + return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`; +}; diff --git a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts new file mode 100644 index 0000000000..04e8fdc956 --- /dev/null +++ b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts @@ -0,0 +1,34 @@ + +import { capitalize } from '@/utils/strings'; + +enum RelationType { + MANY_TO_ONE = 'MANY_TO_ONE', + ONE_TO_MANY = 'ONE_TO_MANY' +} + + +type ComputeMorphRelationFieldNameArgs = { + fieldName: string; + relationDirection: RelationType; + nameSingular: string; + namePlural: string; +}; + +export const computeMorphRelationFieldName = ({ + fieldName, + relationDirection, + nameSingular, + namePlural, +}: ComputeMorphRelationFieldNameArgs): string => { + if (relationDirection === RelationType.MANY_TO_ONE) { + return `${fieldName}${capitalize(nameSingular)}`; + } + + if (relationDirection === RelationType.ONE_TO_MANY) { + return `${fieldName}${capitalize(namePlural)}`; + } + + throw new Error( + `Invalid relation direction: ${relationDirection} for field ${fieldName}`, + ); +}; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index a2fa7a1eb5..07d1e1358a 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -12,6 +12,8 @@ export { findByProperty } from './array/findByProperty'; export { assertUnreachable } from './assertUnreachable'; export { deepMerge } from './deepMerge'; export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields'; +export { computeMorphRelationFieldJoinColumnName } from './fieldMetadata/compute-morph-relation-field-join-column-name.util'; +export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name.util'; export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind'; export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util'; export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util';