From f60817a1e1de69232af98eb06ad5dd8cdd2f2323 Mon Sep 17 00:00:00 2001 From: Guillim Date: Tue, 7 Oct 2025 17:25:11 +0200 Subject: [PATCH] Morph many to one picker (#14155) This PR follows the multiSelect PR merged previously. It will enable morph relation Many to One to be handled from the table, using a singleSelect picker Main point : I decided to change the singleSelect API to take an array of **objectMetadataName** instead of only one to deal with both our usecases. --------- Co-authored-by: Charles Bochet --- .../utils/generateUpdateOneRecordMutation.ts | 0 .../utils/mapFieldMetadataToGraphQLQuery.ts | 19 +- .../utils/mapObjectMetadataToGraphQLQuery.ts | 37 ++- .../useAttachMorphRelatedRecordFromRecord.ts | 187 ----------- .../hooks/useAttachRelatedRecordFromRecord.ts | 118 ------- .../useDetachMorphRelatedRecordFromRecord.ts | 170 ---------- .../hooks/useDetachRelatedRecordFromRecord.ts | 89 ----- ...eRecordOneToManyFieldAttachTargetRecord.ts | 111 +++++++ ...eRecordOneToManyFieldDetachTargetRecord.ts | 92 ++++++ ...useUpdateMultipleRecordsFromManyObjects.ts | 284 ---------------- .../hooks/useUpdateOneRecordMutation.ts | 2 +- .../hooks/useUpdateOneRecordV2.ts | 222 +++++++++++++ .../utils/getTargetFieldMetadataName.ts | 25 -- ...ordDetailRelationSectionDropdownToMany.tsx | 4 +- ...cordDetailRelationSectionDropdownToOne.tsx | 2 +- .../hooks/useRecordFieldGqlFields.ts | 18 +- .../ui/components/FieldDisplay.tsx | 8 +- .../record-field/ui/components/FieldInput.tsx | 20 +- .../ui/components/FormFieldInput.tsx | 4 +- .../FormRelationToOneFieldInput.tsx | 2 +- .../components/FormSingleRecordPicker.tsx | 26 +- .../FormSingleRecordPicker.stories.tsx | 6 +- .../ui/hooks/useOpenFieldInputEditMode.ts | 35 +- .../record-field/ui/hooks/usePersistField.ts | 136 ++++++-- .../useMorphRelationFromManyFieldDisplay.ts | 1 - .../useMorphRelationToOneFieldDisplay.ts | 4 - .../MorphRelationManyToOneFieldInput.tsx | 69 ++++ .../MorphRelationOneToManyFieldInput.tsx | 8 +- ...ut.tsx => RelationManyToOneFieldInput.tsx} | 8 +- ...ut.tsx => RelationOneToManyFieldInput.tsx} | 6 +- ...> RelationManyToOneFieldInput.stories.tsx} | 16 +- ...> RelationOneToManyFieldInput.stories.tsx} | 12 +- ...seOpenMorphRelationManyToOneFieldInput.tsx | 91 ++++++ ...eOpenMorphRelationOneToManyFieldInput.tsx} | 9 +- ...UpdateMorphRelationManyToOneFieldInput.tsx | 66 ---- ...UpdateMorphRelationOneToManyFieldInput.tsx | 87 +++++ .../useUpdateRelationFromManyFieldInput.tsx | 61 ---- .../useUpdateRelationOneToManyFieldInput.tsx | 123 +++++++ ...eObject.ts => isFieldRelationManyToOne.ts} | 2 +- ...ue.ts => isFieldRelationManyToOneValue.ts} | 2 +- ...Objects.ts => isFieldRelationOneToMany.ts} | 2 +- ...ue.ts => isFieldRelationOneToManyValue.ts} | 2 +- .../components/SingleRecordPicker.tsx | 4 +- .../SingleRecordPickerMenuItemsWithSearch.tsx | 33 +- .../SingleRecordPicker.stories.tsx | 2 +- ...yToOneValueWithObjectNameFamilySelector.ts | 12 +- ...ToManyValueWithObjectNameFamilySelector.ts | 12 +- .../RecordTableCellFieldContextGeneric.tsx | 8 +- .../utils/computeOptimisticRecordFromInput.ts | 25 +- .../utils/sanitizeRecordInput.ts | 21 +- .../WorkflowEditActionDeleteRecord.tsx | 2 +- .../WorkflowEditActionUpdateRecord.tsx | 2 +- .../WorkflowStepFilterValueInput.tsx | 2 +- .../WorkflowEditActionFormFiller.tsx | 2 +- .../engine/dataloaders/dataloader.service.ts | 63 +++- .../field-metadata-morph-relation.service.ts | 6 +- .../compute-morph-relation-field-name.util.ts | 26 -- ...ield-input-to-flat-field-metadatas.util.ts | 6 +- ...rom-morph-field-metadata-name.util.spec.ts | 81 +++++ ...ame-from-morph-field-metadata-name.util.ts | 30 ++ ...morph-field-on-object-names-update.util.ts | 40 +-- .../utils/search-and-replace-last.util.ts | 19 ++ .../survey-result-data-seeds.constant.ts | 2 +- ...relation-creation.integration-spec.ts.snap | 28 +- packages/twenty-shared/eslint.config.mjs | 4 + packages/twenty-shared/project.json | 7 +- .../CompositeFieldTypeSubFieldsNames.ts | 56 ++-- .../src/constants/CurrencyCode.ts | 309 +++++++++--------- .../src/constants/CurrencyCodeLabels.ts | 9 +- .../src/constants/QueryDefaultLimitRecords.ts | 2 +- packages/twenty-shared/src/index.ts | 1 - .../src/testing/types/TestingGenerics.type.ts | 4 +- .../types/CompositeFieldSubFieldNameType.ts | 7 +- .../src/types/FieldRatingValue.ts | 4 +- packages/twenty-shared/src/types/Nullable.ts | 2 +- .../src/types/PartialFieldMetadataItem.ts | 16 +- .../src/types/PartialFieldMetadataOption.ts | 11 +- .../src/types/RelationCreationPayload.ts | 4 +- .../twenty-shared/src/types/RelationType.ts | 2 +- .../src/types/ViewFilterOperandDeprecated.ts | 33 +- .../src/utils/__tests__/applyDiff.test.ts | 206 ++++++++---- .../src/utils/__tests__/findOrThrow.test.ts | 22 +- .../utils/__tests__/isNonEmptyArray.test.ts | 2 +- .../src/utils/__tests__/parseJson.test.ts | 2 +- .../safeParseRelativeDateFilterValue.test.ts | 2 +- packages/twenty-shared/src/utils/applyDiff.ts | 107 +++--- .../src/utils/array/filterOutByProperty.ts | 11 +- .../twenty-shared/src/utils/array/findById.ts | 6 +- .../src/utils/array/findByProperty.ts | 11 +- .../src/utils/array/findOrThrow.ts | 4 +- .../src/utils/array/sumByProperty.ts | 12 +- .../compute-morph-relation-field-name.ts | 34 ++ .../checkIfShouldComputeEmptinessFilter.ts | 8 +- .../computeGqlOperationFilterForEmails.ts | 5 +- .../computeGqlOperationFilterForLinks.ts | 12 +- ...computeEmptyGqlOperationFilterForEmails.ts | 7 +- .../computeEmptyGqlOperationFilterForLinks.ts | 2 +- .../filter/computeRecordGqlOperationFilter.ts | 4 +- .../twenty-shared/src/utils/filter/index.ts | 2 - .../src/utils/filter/isEmptinessOperand.ts | 7 +- .../turnAnyFieldFilterIntoRecordGqlFilter.ts | 13 +- ...RecordFilterGroupIntoGqlOperationFilter.ts | 24 +- .../turnRecordFilterIntoGqlOperationFilter.ts | 21 +- .../arrayOfStringsOrVariablesSchema.test.ts | 2 +- ...rateILikeFiltersForCompositeFields.test.ts | 2 +- ...nAnyFieldFilterIntoRecordGqlFilter.test.ts | 12 +- ...iew-filter-operand-to-core-operand.util.ts | 9 +- .../utils/convertViewFilterValueToString.ts | 6 +- ...reateAnyFieldRecordFilterBaseProperties.ts | 5 +- .../filter/utils/fieldRatingConvertors.ts | 4 +- .../filterSelectOptionsOfFieldMetadataItem.ts | 2 +- .../utils/getEmptyRecordGqlOperationFilter.ts | 4 +- .../utils/getFilterTypeFromFieldType.ts | 8 +- .../filter/utils/isExpectedSubFieldName.ts | 8 +- .../utils/resolveDateViewFilterValue.ts | 5 +- packages/twenty-shared/src/utils/index.ts | 1 + packages/twenty-shared/src/utils/parseJson.ts | 10 +- .../utils/safeParseRelativeDateFilterValue.ts | 41 ++- .../twenty-shared/src/utils/strings/index.ts | 1 - .../src/utils/tiptap/tiptap-marks.ts | 6 +- .../src/utils/typeguard/throwIfNotDefined.ts | 8 +- .../src/utils/url/absoluteUrlSchema.ts | 6 +- .../__tests__/assertIsDefinedOrThrow.test.ts | 6 +- .../validation/assertIsDefinedOrThrow.ts | 4 +- .../src/workflow/constants/TriggerStepId.ts | 2 +- .../workflow/schemas/base-trigger-schema.ts | 12 +- .../schemas/base-workflow-action-schema.ts | 16 +- .../base-workflow-action-settings-schema.ts | 4 +- .../schemas/create-record-action-schema.ts | 10 +- .../create-record-action-settings-schema.ts | 5 +- .../schemas/delete-record-action-schema.ts | 10 +- .../schemas/filter-action-settings-schema.ts | 4 +- .../workflow/schemas/manual-trigger-schema.ts | 4 +- .../workflow/schemas/object-record-schema.ts | 6 +- .../schemas/update-record-action-schema.ts | 10 +- .../workflow-run-state-step-info-schema.ts | 18 +- .../types/WorkflowRunStateStepInfos.ts | 14 +- .../utils/canObjectBeManagedByWorkflow.ts | 7 +- 138 files changed, 2116 insertions(+), 1763 deletions(-) rename packages/twenty-front/src/modules/{object-record/multiple-objects => object-metadata}/utils/generateUpdateOneRecordMutation.ts (100%) delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useUpdateMultipleRecordsFromManyObjects.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts delete mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/utils/getTargetFieldMetadataName.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx rename packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/{RelationToOneFieldInput.tsx => RelationManyToOneFieldInput.tsx} (97%) rename packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/{RelationFromManyFieldInput.tsx => RelationOneToManyFieldInput.tsx} (96%) rename packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/{RelationToOneFieldInput.stories.tsx => RelationManyToOneFieldInput.stories.tsx} (91%) rename packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/{RelationFromManyFieldInput.stories.tsx => RelationOneToManyFieldInput.stories.tsx} (90%) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput.tsx rename packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/{useOpenMorphRelationFromManyFieldInput.tsx => useOpenMorphRelationOneToManyFieldInput.tsx} (95%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput.tsx rename packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/{isFieldRelationToOneObject.ts => isFieldRelationManyToOne.ts} (92%) rename packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/{isFieldRelationToOneValue.ts => isFieldRelationManyToOneValue.ts} (87%) rename packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/{isFieldRelationFromManyObjects.ts => isFieldRelationOneToMany.ts} (91%) rename packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/{isFieldRelationFromManyValue.ts => isFieldRelationOneToManyValue.ts} (87%) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/get-morph-name-from-morph-field-metadata-name.util.spec.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util.ts create mode 100644 packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.ts diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-metadata/utils/generateUpdateOneRecordMutation.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateUpdateOneRecordMutation.ts rename to packages/twenty-front/src/modules/object-metadata/utils/generateUpdateOneRecordMutation.ts 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 80a83bb336e..811469a4f6d 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -6,7 +6,7 @@ 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 { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { type FieldMetadataItem } from '../types/FieldMetadataItem'; type MapFieldMetadataToGraphQLQueryArgs = { @@ -49,7 +49,14 @@ export const mapFieldMetadataToGraphQLQuery = ({ ) { let gqlMorphField = ''; for (const morphRelation of fieldMetadata.morphRelations ?? []) { - const relationFieldName = morphRelation.sourceFieldMetadata.name; + const relationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationType: fieldMetadata.settings?.relationType, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.id === morphRelation.targetObjectMetadata.id, @@ -80,6 +87,10 @@ export const mapFieldMetadataToGraphQLQuery = ({ } if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { + if (gqlField !== relationFieldName) { + continue; + } + gqlMorphField += `${relationFieldName} { edges { @@ -102,6 +113,10 @@ export const mapFieldMetadataToGraphQLQuery = ({ continue; } + if (gqlField !== relationFieldName) { + continue; + } + gqlMorphField += `${relationFieldName} ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, 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 cd9ac793a78..fecc4810505 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[]; @@ -71,7 +71,14 @@ export const mapObjectMetadataToGraphQLQuery = ({ } return fieldMetadata.morphRelations.map((morphRelation) => ({ - gqlField: morphRelation.sourceFieldMetadata.name, + gqlField: computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }), fieldMetadata: fieldMetadata, })); }); @@ -99,19 +106,30 @@ export const mapObjectMetadataToGraphQLQuery = ({ } return fieldMetadata.morphRelations.map((morphRelation) => ({ - gqlField: morphRelation.sourceFieldMetadata.name, + gqlField: computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }), fieldMetadata, })); }); - const gqlFieldWithFieldMetadataThatCouldBeQueried = [ + const gqlFieldsWithFieldMetadata = [ ...activeReadableFields, ...manyToOneRelationGqlFieldWithFieldMetadata, - ].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) => - gqlFieldWithFieldMetadataA.gqlField.localeCompare( - gqlFieldWithFieldMetadataB.gqlField, - ), - ); + ]; + + const gqlFieldWithFieldMetadataThatCouldBeQueried = + gqlFieldsWithFieldMetadata.sort( + (gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) => + gqlFieldWithFieldMetadataA.gqlField.localeCompare( + gqlFieldWithFieldMetadataB.gqlField, + ), + ); const gqlFieldWithFieldMetadataThatSouldBeQueried = gqlFieldWithFieldMetadataThatCouldBeQueried.filter( @@ -128,7 +146,6 @@ export const mapObjectMetadataToGraphQLQuery = ({ __ref }`; } - const mappedFields = gqlFieldWithFieldMetadataThatSouldBeQueried .map((gqlFieldWithFieldMetadata) => { const currentRecordGqlFields = diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts deleted file mode 100644 index ec18f902649..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; -import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; -import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects'; -import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -export const useAttachMorphRelatedRecordFromRecord = () => { - const apolloCoreClient = useApolloCoreClient(); - const { fieldDefinition } = useContext(FieldContext); - const { objectMetadataItems } = useObjectMetadataItems(); - const { updateMultipleRecordsFromManyObjects } = - useUpdateMultipleRecordsFromManyObjects(); - - if (!isFieldMorphRelation(fieldDefinition)) { - throw new Error('Field is not a morph relation'); - } - - const objectNameSingular = - fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata - .nameSingular; - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ); - - if (!isDefined(objectMetadataItem)) { - throw new Error('Could not find object metadata item'); - } - - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - - const updateOneRecordAndAttachMorphRelations = useRecoilCallback( - ({ set, snapshot }) => - async ({ - recordId, - relatedRecordId, - objectNameSingulars, - }: { - recordId: string; - relatedRecordId: string; - objectNameSingulars: string[]; - }) => { - const relatedObjectMetadataItems = objectMetadataItems.filter( - (objectMetadataItem) => - objectNameSingulars.includes(objectMetadataItem.nameSingular), - ); - - const parentRecord = snapshot - .getLoadable(recordStoreFamilyState(recordId)) - .getValue(); - - if (isDefined(parentRecord)) { - relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => { - const currentMorphFieldValue = - parentRecord[fieldDefinition.metadata.fieldName]; - - const objectRecordFromCache = getRecordFromCache({ - objectMetadataItem: relatedObjectMetadataItem, - recordId: relatedRecordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - - if (!isDefined(objectRecordFromCache)) { - return; - } - - set(recordStoreFamilyState(recordId), { - ...parentRecord, - [fieldDefinition.metadata.fieldName]: [ - ...currentMorphFieldValue, - objectRecordFromCache, - ], - }); - }); - } - - const relatedObjectMetadataItemsWithCachedRecord = - relatedObjectMetadataItems - .map((objectMetadataItem) => { - const cachedRelatedRecord = getRecordFromCache({ - objectMetadataItem, - recordId: relatedRecordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - return { - cachedRelatedRecord, - objectMetadataItem, - }; - }) - .find((item) => isDefined(item.cachedRelatedRecord)); - - if (!relatedObjectMetadataItemsWithCachedRecord) { - throw new Error('Could not find cached related record'); - } - - const { - objectMetadataItem: relatedObjectMetadataItem, - cachedRelatedRecord, - } = relatedObjectMetadataItemsWithCachedRecord; - - if ( - !isDefined(cachedRelatedRecord) || - !isDefined(cachedRelatedRecord.id) - ) { - throw new Error('Could not find cached related record'); - } - - const fieldOnRelatedObject = getTargetFieldMetadataName({ - fieldDefinition, - objectNameSingular: relatedObjectMetadataItem.nameSingular, - }); - if (!isDefined(fieldOnRelatedObject)) { - throw new Error('Could not find field on related object'); - } - - const previousRecordId = - cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`]; - - if (isDefined(previousRecordId)) { - const previousRecord = getRecordFromCache({ - objectMetadataItem, - recordId: previousRecordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - - const previousRecordWithRelation = { - ...cachedRelatedRecord, - [fieldOnRelatedObject]: previousRecord, - }; - - const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({ - objectMetadataItem: relatedObjectMetadataItem, - record: previousRecordWithRelation, - }); - - updateRecordFromCache({ - objectMetadataItems, - objectMetadataItem: relatedObjectMetadataItem, - cache: apolloCoreClient.cache, - record: { - ...cachedRelatedRecord, - [fieldOnRelatedObject]: previousRecord, - }, - recordGqlFields: gqlFields, - objectPermissionsByObjectMetadataId, - }); - } - - const updatedManyRecordsArgs = [ - { - idToUpdate: relatedRecordId, - objectNameSingulars, - relatedRecordId: recordId, - // recordGqlFields: gqlFields, - }, - ]; - - await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs); - }, - [ - fieldDefinition, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - updateMultipleRecordsFromManyObjects, - apolloCoreClient.cache, - objectMetadataItem, - ], - ); - - return { updateOneRecordAndAttachMorphRelations }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts deleted file mode 100644 index 043ee620f65..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; -import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; -import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { CustomError, isDefined } from 'twenty-shared/utils'; - -type useAttachRelatedRecordFromRecordProps = { - recordObjectNameSingular: string; - fieldNameOnRecordObject: string; -}; - -export const useAttachRelatedRecordFromRecord = ({ - recordObjectNameSingular, - fieldNameOnRecordObject, -}: useAttachRelatedRecordFromRecordProps) => { - const apolloCoreClient = useApolloCoreClient(); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: recordObjectNameSingular, - }); - - const fieldOnObject = objectMetadataItem.readableFields.find((field) => { - return field.name === fieldNameOnRecordObject; - }); - - const relatedRecordObjectNameSingular = - fieldOnObject?.relation?.targetObjectMetadata.nameSingular; - - if (!relatedRecordObjectNameSingular) { - throw new CustomError( - `Could not find record related to ${recordObjectNameSingular}`, - 'RELATED_RECORD_NOT_FOUND', - ); - } - const { objectMetadataItem: relatedObjectMetadataItem } = - useObjectMetadataItem({ - objectNameSingular: relatedRecordObjectNameSingular, - }); - - const fieldOnRelatedObject = - fieldOnObject?.relation?.targetFieldMetadata.name; - - if (!fieldOnRelatedObject) { - throw new CustomError( - `Missing target field for ${fieldNameOnRecordObject}`, - 'MISSING_TARGET_FIELD', - ); - } - - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: relatedRecordObjectNameSingular, - }); - - const getRecordFromCache = useGetRecordFromCache({ - objectNameSingular: recordObjectNameSingular, - }); - - const getRelatedRecordFromCache = useGetRecordFromCache({ - objectNameSingular: relatedRecordObjectNameSingular, - }); - - const { objectMetadataItems } = useObjectMetadataItems(); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - const updateOneRecordAndAttachRelations = async ({ - recordId, - relatedRecordId, - }: { - recordId: string; - relatedRecordId: string; - }) => { - const cachedRelatedRecord = - getRelatedRecordFromCache(relatedRecordId); - - if (!cachedRelatedRecord) { - throw new Error('Could not find cached related record'); - } - - const previousRecordId = cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`]; - - if (isDefined(previousRecordId)) { - const previousRecord = getRecordFromCache(previousRecordId); - - const previousRecordWithRelation = { - ...cachedRelatedRecord, - [fieldOnRelatedObject]: previousRecord, - }; - const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({ - objectMetadataItem: relatedObjectMetadataItem, - record: previousRecordWithRelation, - }); - updateRecordFromCache({ - objectMetadataItems, - objectMetadataItem: relatedObjectMetadataItem, - cache: apolloCoreClient.cache, - record: { - ...cachedRelatedRecord, - [fieldOnRelatedObject]: previousRecord, - }, - recordGqlFields: gqlFields, - objectPermissionsByObjectMetadataId, - }); - } - - await updateOneRecord({ - idToUpdate: relatedRecordId, - updateOneRecordInput: { - [`${fieldOnRelatedObject}Id`]: recordId, - }, - }); - }; - - return { updateOneRecordAndAttachRelations }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts deleted file mode 100644 index fca0b20b187..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { type Reference } from '@apollo/client'; - -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; -import { getRefName } from '@/object-record/cache/utils/getRefName'; -import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -export const useDetachMorphRelatedRecordFromRecord = () => { - const apolloCoreClient = useApolloCoreClient(); - const { fieldDefinition } = useContext(FieldContext); - const { objectMetadataItems } = useObjectMetadataItems(); - const { updateMultipleRecordsFromManyObjects } = - useUpdateMultipleRecordsFromManyObjects(); - - if (!isFieldMorphRelation(fieldDefinition)) { - throw new Error('Field is not a morph relation'); - } - - const objectNameSingular = - fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata - .nameSingular; - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ); - - if (!isDefined(objectMetadataItem)) { - throw new Error('Could not find object metadata item'); - } - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - - const updateOneRecordAndDetachMorphRelations = useRecoilCallback( - ({ set, snapshot }) => - async ({ - recordId, - relatedRecordId, - objectNameSingulars, - }: { - recordId: string; - relatedRecordId: string; - objectNameSingulars: string[]; - }) => { - const relatedObjectMetadataItems = objectMetadataItems.filter( - (objectMetadataItem) => - objectNameSingulars.includes(objectMetadataItem.nameSingular), - ); - - const parentRecord = snapshot - .getLoadable(recordStoreFamilyState(recordId)) - .getValue(); - - if (isDefined(parentRecord)) { - relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => { - const currentMorphFieldValue = parentRecord[ - fieldDefinition.metadata.fieldName - ] as ObjectRecord[]; - - const objectRecordFromCache = getRecordFromCache({ - objectMetadataItem: relatedObjectMetadataItem, - recordId: relatedRecordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - - if (!isDefined(objectRecordFromCache)) { - return; - } - - if (Array.isArray(currentMorphFieldValue)) { - set(recordStoreFamilyState(recordId), { - ...parentRecord, - [fieldDefinition.metadata.fieldName]: - currentMorphFieldValue.filter( - (record) => record.id !== relatedRecordId, - ), - }); - } - }); - } - - const relatedObjectMetadataItemsWithCachedRecord = - relatedObjectMetadataItems - .map((objectMetadataItem) => { - const cachedRelatedRecord = getRecordFromCache({ - objectMetadataItem, - recordId: relatedRecordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - return { - cachedRelatedRecord, - objectMetadataItem, - }; - }) - .find((item) => isDefined(item.cachedRelatedRecord)); - - if (!relatedObjectMetadataItemsWithCachedRecord) { - throw new Error('Could not find cached related record'); - } - - const { objectMetadataItem: relatedObjectMetadataItem } = - relatedObjectMetadataItemsWithCachedRecord; - - modifyRecordFromCache({ - objectMetadataItem, - cache: apolloCoreClient.cache, - fieldModifiers: { - [fieldDefinition.metadata.fieldName]: ( - fieldNameOnRecordObjectConnection, - { readField }, - ) => { - const edges = readField<{ node: Reference }[]>( - 'edges', - fieldNameOnRecordObjectConnection, - ); - - if (!edges) return fieldNameOnRecordObjectConnection; - - return { - ...fieldNameOnRecordObjectConnection, - edges: edges.filter( - (edge) => - !( - edge.node.__ref === - getRefName( - relatedObjectMetadataItem.nameSingular, - relatedRecordId, - ) - ), - ), - }; - }, - }, - recordId, - }); - - const updatedManyRecordsArgs = [ - { - idToUpdate: relatedRecordId, - objectNameSingulars, - relatedRecordId: null, - }, - ]; - - await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs); - }, - [ - fieldDefinition, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - updateMultipleRecordsFromManyObjects, - apolloCoreClient.cache, - objectMetadataItem, - ], - ); - - return { updateOneRecordAndDetachMorphRelations }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts deleted file mode 100644 index 64eef3d5ce0..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useDetachRelatedRecordFromRecord.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { type Reference } from '@apollo/client'; - -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { getRefName } from '@/object-record/cache/utils/getRefName'; -import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; - -type useDetachRelatedRecordFromRecordProps = { - recordObjectNameSingular: string; - fieldNameOnRecordObject: string; -}; - -export const useDetachRelatedRecordFromRecord = ({ - recordObjectNameSingular, - fieldNameOnRecordObject, -}: useDetachRelatedRecordFromRecordProps) => { - const apolloCoreClient = useApolloCoreClient(); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: recordObjectNameSingular, - }); - - const fieldOnObject = objectMetadataItem.readableFields.find((field) => { - return field.name === fieldNameOnRecordObject; - }); - - const relatedRecordObjectNameSingular = - fieldOnObject?.relation?.targetObjectMetadata.nameSingular; - - const fieldOnRelatedObject = - fieldOnObject?.relation?.targetFieldMetadata.name; - - if (!relatedRecordObjectNameSingular) { - throw new Error( - `Could not find record related to ${recordObjectNameSingular}`, - ); - } - - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: relatedRecordObjectNameSingular, - }); - - const updateOneRecordAndDetachRelations = async ({ - recordId, - relatedRecordId, - }: { - recordId: string; - relatedRecordId: string; - }) => { - modifyRecordFromCache({ - objectMetadataItem, - cache: apolloCoreClient.cache, - fieldModifiers: { - [fieldNameOnRecordObject]: ( - fieldNameOnRecordObjectConnection, - { readField }, - ) => { - const edges = readField<{ node: Reference }[]>( - 'edges', - fieldNameOnRecordObjectConnection, - ); - - if (!edges) return fieldNameOnRecordObjectConnection; - - return { - ...fieldNameOnRecordObjectConnection, - edges: edges.filter( - (edge) => - !( - edge.node.__ref === - getRefName(relatedRecordObjectNameSingular, relatedRecordId) - ), - ), - }; - }, - }, - recordId, - }); - await updateOneRecord({ - idToUpdate: relatedRecordId, - updateOneRecordInput: { - [`${fieldOnRelatedObject}Id`]: null, - }, - }); - }; - - return { updateOneRecordAndDetachRelations }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord.ts new file mode 100644 index 00000000000..b6b3a897335 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord.ts @@ -0,0 +1,111 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; + +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; +import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2'; +import { CustomError, isDefined } from 'twenty-shared/utils'; + +export const useRecordOneToManyFieldAttachTargetRecord = () => { + const apolloCoreClient = useApolloCoreClient(); + + const { objectMetadataItems } = useObjectMetadataItems(); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { updateOneRecord } = useUpdateOneRecordV2(); + + const recordOneToManyFieldAttachTargetRecord = async ({ + sourceObjectNameSingular, + targetObjectNameSingular, + targetGQLFieldName, + sourceRecordId, + targetRecordId, + }: { + sourceObjectNameSingular: string; + targetObjectNameSingular: string; + targetGQLFieldName: string; + sourceRecordId: string; + targetRecordId: string; + }) => { + const targetObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === targetObjectNameSingular, + ); + + const sourceObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === sourceObjectNameSingular, + ); + + if (!targetObjectMetadataItem) { + throw new CustomError( + `Could not find related object metadata for ${targetObjectNameSingular}`, + 'RELATED_OBJECT_METADATA_NOT_FOUND', + ); + } + + if (!sourceObjectMetadataItem) { + throw new CustomError( + `Could not find source object metadata for ${sourceObjectNameSingular}`, + 'SOURCE_OBJECT_METADATA_NOT_FOUND', + ); + } + + const cachedTargetRecord = getRecordFromCache({ + objectMetadataItem: targetObjectMetadataItem, + recordId: targetRecordId, + cache: apolloCoreClient.cache, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }); + + if (!cachedTargetRecord) { + throw new Error('Could not find cached related record'); + } + + const previousRecordId = cachedTargetRecord?.[`${targetGQLFieldName}Id`]; + + if (isDefined(previousRecordId)) { + const previousRecord = getRecordFromCache({ + objectMetadataItem: sourceObjectMetadataItem, + recordId: previousRecordId, + cache: apolloCoreClient.cache, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }); + + const previousRecordWithRelation = { + ...cachedTargetRecord, + [targetGQLFieldName]: previousRecord, + }; + + const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({ + objectMetadataItem: targetObjectMetadataItem, + record: previousRecordWithRelation, + }); + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem: targetObjectMetadataItem, + cache: apolloCoreClient.cache, + record: { + ...cachedTargetRecord, + [targetGQLFieldName]: previousRecord, + }, + recordGqlFields: gqlFields, + objectPermissionsByObjectMetadataId, + }); + } + + await updateOneRecord({ + objectNameSingular: targetObjectNameSingular, + idToUpdate: targetRecordId, + updateOneRecordInput: { + [`${targetGQLFieldName}Id`]: sourceRecordId, + }, + }); + }; + + return { recordOneToManyFieldAttachTargetRecord }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord.ts new file mode 100644 index 00000000000..1d47ec46481 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord.ts @@ -0,0 +1,92 @@ +import { type Reference } from '@apollo/client'; + +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getRefName } from '@/object-record/cache/utils/getRefName'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; +import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2'; +import { CustomError } from 'twenty-shared/utils'; + +export const useRecordOneToManyFieldDetachTargetRecord = () => { + const apolloCoreClient = useApolloCoreClient(); + const { objectMetadataItems } = useObjectMetadataItems(); + const { updateOneRecord } = useUpdateOneRecordV2(); + + const recordOneToManyFieldDetachTargetRecord = async ({ + sourceObjectNameSingular, + targetObjectNameSingular, + targetGQLFieldName, + sourceRecordId, + targetRecordId, + }: { + sourceObjectNameSingular: string; + targetObjectNameSingular: string; + targetGQLFieldName: string; + sourceRecordId: string; + targetRecordId: string; + }) => { + const targetObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === targetObjectNameSingular, + ); + + const sourceObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === sourceObjectNameSingular, + ); + + if (!targetObjectMetadataItem) { + throw new CustomError( + `Could not find related object metadata for ${targetObjectNameSingular}`, + 'RELATED_OBJECT_METADATA_NOT_FOUND', + ); + } + + if (!sourceObjectMetadataItem) { + throw new CustomError( + `Could not find source object metadata for ${sourceObjectNameSingular}`, + 'SOURCE_OBJECT_METADATA_NOT_FOUND', + ); + } + + modifyRecordFromCache({ + objectMetadataItem: sourceObjectMetadataItem, + cache: apolloCoreClient.cache, + fieldModifiers: { + [targetGQLFieldName]: ( + fieldNameOnRecordObjectConnection, + { readField }, + ) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + fieldNameOnRecordObjectConnection, + ); + + if (!edges) return fieldNameOnRecordObjectConnection; + + return { + ...fieldNameOnRecordObjectConnection, + edges: edges.filter( + (edge) => + !( + edge.node.__ref === + getRefName(targetObjectNameSingular, targetRecordId) + ), + ), + }; + }, + }, + recordId: sourceRecordId, + }); + + await updateOneRecord({ + objectNameSingular: targetObjectNameSingular, + idToUpdate: targetRecordId, + updateOneRecordInput: { + [`${targetGQLFieldName}Id`]: null, + }, + }); + }; + + return { recordOneToManyFieldDetachTargetRecord }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateMultipleRecordsFromManyObjects.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateMultipleRecordsFromManyObjects.ts deleted file mode 100644 index 0ba80bc0184..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateMultipleRecordsFromManyObjects.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; -import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; -import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; -import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; -import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; -import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation'; -import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; -import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; -import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; -import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; -import { isNull } from '@sniptt/guards'; -import { useContext } from 'react'; -import { useRecoilValue } from 'recoil'; -import { CustomError, isDefined } from 'twenty-shared/utils'; -import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; - -type UpdateManyRecordArgs = { - idToUpdate: string; - relatedRecordId: string | null; - objectNameSingulars: string[]; - recordGqlFields?: Record; -}; - -export const useUpdateMultipleRecordsFromManyObjects = () => { - const { fieldDefinition } = useContext(FieldContext); - const apolloCoreClient = useApolloCoreClient(); - - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - - const { objectMetadataItems } = useObjectMetadataItems(); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - - const updateMultipleRecordsFromManyObjects = async ( - updatedManyRecordsArgs: UpdateManyRecordArgs[], - ) => { - for (const { - idToUpdate, - objectNameSingulars, - relatedRecordId, - recordGqlFields, - } of updatedManyRecordsArgs) { - const objectMetadataItemArray = objectMetadataItems.filter( - (objectMetadataItem) => - objectNameSingulars.includes(objectMetadataItem.nameSingular), - ); - - if (objectMetadataItemArray.length === 0) { - throw new CustomError( - `Object metadata item not found ${objectNameSingulars.join(', ')}`, - 'OBJECT_METADATA_ITEM_NOT_FOUND', - ); - } - - const objectMetadataItemWithCachedRecord = objectMetadataItemArray - .map((objectMetadataItem) => { - const cachedRecord = getRecordFromCache({ - objectMetadataItem, - recordId: idToUpdate, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - return { - cachedRecord, - objectMetadataItem, - }; - }) - .find((item) => isDefined(item.cachedRecord)); - - if (!isDefined(objectMetadataItemWithCachedRecord)) { - throw new CustomError( - `Record not found ${idToUpdate}`, - 'RECORD_NOT_FOUND_IN_OBJECT_METADATA_ITEM', - ); - } - - const { objectMetadataItem, cachedRecord } = - objectMetadataItemWithCachedRecord; - - if ( - !isFieldRelation(fieldDefinition) && - !isFieldMorphRelation(fieldDefinition) - ) { - throw new CustomError( - `Should never happen`, - 'TARGET_FIELD_NAME_NOT_FOUND', - ); - } - - const targetFieldName = getTargetFieldMetadataName({ - fieldDefinition, - objectNameSingular: objectMetadataItem.nameSingular, - }); - - if (!isDefined(targetFieldName)) { - throw new CustomError( - `Cannot find Target field name for the (morph) relation field ${fieldDefinition.metadata.fieldName} on ${objectMetadataItem.nameSingular}`, - 'TARGET_FIELD_NAME_NOT_FOUND', - ); - } - - const updateOneRecordInput = { - [`${targetFieldName}Id`]: relatedRecordId, - }; - - const optimisticRecordInput = computeOptimisticRecordFromInput({ - objectMetadataItem, - currentWorkspaceMember: currentWorkspaceMember, - recordInput: updateOneRecordInput, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }); - - const computedRecordGqlFields = - recordGqlFields ?? - generateDepthOneRecordGqlFields({ objectMetadataItem }); - - const cachedRecordWithConnection = getRecordNodeFromRecord({ - record: cachedRecord, - objectMetadataItem, - objectMetadataItems, - recordGqlFields: computedRecordGqlFields, - computeReferences: false, - }); - - const computedOptimisticRecord = { - ...cachedRecord, - ...optimisticRecordInput, - id: idToUpdate, - __typename: getObjectTypename(objectMetadataItem.nameSingular), - }; - - const optimisticRecordWithConnection = - getRecordNodeFromRecord({ - record: computedOptimisticRecord, - objectMetadataItem, - objectMetadataItems, - recordGqlFields: computedRecordGqlFields, - computeReferences: false, - }); - - const shouldHandleOptimisticCache = - !isNull(cachedRecord) && - isDefined(optimisticRecordWithConnection) && - isDefined(cachedRecordWithConnection); - - if (shouldHandleOptimisticCache) { - const recordGqlFields = computeDepthOneRecordGqlFieldsFromRecord({ - objectMetadataItem, - record: optimisticRecordInput, - }); - - updateRecordFromCache({ - objectMetadataItems, - objectMetadataItem, - cache: apolloCoreClient.cache, - record: computedOptimisticRecord, - recordGqlFields, - objectPermissionsByObjectMetadataId, - }); - - triggerUpdateRecordOptimisticEffect({ - cache: apolloCoreClient.cache, - objectMetadataItem, - currentRecord: cachedRecordWithConnection, - updatedRecord: optimisticRecordWithConnection, - objectMetadataItems, - }); - } - - const mutationResponseField = getUpdateOneRecordMutationResponseField( - objectMetadataItem.nameSingular, - ); - - const sanitizedInput = { - ...sanitizeRecordInput({ - objectMetadataItem, - recordInput: updateOneRecordInput, - }), - }; - - const updateOneRecordMutation = generateUpdateOneRecordMutation({ - objectMetadataItem, - objectMetadataItems, - recordGqlFields: computedRecordGqlFields, - computeReferences: false, - objectPermissionsByObjectMetadataId, - }); - - const updatedRecord = await apolloCoreClient - .mutate({ - mutation: updateOneRecordMutation, - variables: { - idToUpdate, - input: sanitizedInput, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; - if (!isDefined(record)) return; - - triggerUpdateRecordOptimisticEffect({ - cache, - objectMetadataItem, - currentRecord: computedOptimisticRecord, - updatedRecord: record, - objectMetadataItems, - }); - }, - }) - .catch((error: Error) => { - if (!shouldHandleOptimisticCache) { - throw error; - } - const cachedRecordKeys = new Set(Object.keys(cachedRecord)); - const recordKeysAddedByOptimisticCache = Object.keys( - optimisticRecordInput, - ).filter((diffKey) => !cachedRecordKeys.has(diffKey)); - - const recordGqlFields = { - ...computeDepthOneRecordGqlFieldsFromRecord({ - objectMetadataItem, - record: cachedRecord, - }), - ...buildRecordFromKeysWithSameValue( - recordKeysAddedByOptimisticCache, - true, - ), - }; - - updateRecordFromCache({ - objectMetadataItems, - objectMetadataItem, - cache: apolloCoreClient.cache, - record: { - ...cachedRecord, - ...buildRecordFromKeysWithSameValue( - recordKeysAddedByOptimisticCache, - null, - ), - }, - recordGqlFields, - objectPermissionsByObjectMetadataId, - }); - - triggerUpdateRecordOptimisticEffect({ - cache: apolloCoreClient.cache, - objectMetadataItem, - currentRecord: optimisticRecordWithConnection, - updatedRecord: cachedRecordWithConnection, - objectMetadataItems, - }); - - throw error; - }); - - const refetchAggregateQueries = async () => { - const queryName = getAggregateQueryName(objectMetadataItem.namePlural); - - await apolloCoreClient.refetchQueries({ - include: [queryName], - }); - }; - await refetchAggregateQueries(); - - return updatedRecord?.data?.[mutationResponseField] ?? null; - } - }; - - return { - updateMultipleRecordsFromManyObjects, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts index bdfb65c9f91..df290afdebb 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts @@ -2,10 +2,10 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useUpdateOneRecordMutation = ({ diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts new file mode 100644 index 00000000000..44daa934754 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts @@ -0,0 +1,222 @@ +import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation'; +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; +import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; +import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; +import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; +import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; +import { isNull } from '@sniptt/guards'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; + +type UpdateOneRecordArgs = { + idToUpdate: string; + updateOneRecordInput: Partial>; + optimisticRecord?: Partial; + objectNameSingular: string; + recordGqlFields?: Record; +}; +export const useUpdateOneRecordV2 = () => { + const apolloCoreClient = useApolloCoreClient(); + + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const { objectMetadataItems } = useObjectMetadataItems(); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const updateOneRecord = async < + UpdatedObjectRecord extends ObjectRecord = ObjectRecord, + >({ + objectNameSingular, + recordGqlFields, + idToUpdate, + updateOneRecordInput, + optimisticRecord, + }: UpdateOneRecordArgs) => { + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === objectNameSingular, + ); + + if (!objectMetadataItem) { + throw new Error( + `Object metadata item not found for ${objectNameSingular}`, + ); + } + + const computedRecordGqlFields = + recordGqlFields ?? + generateDepthOneRecordGqlFields({ objectMetadataItem }); + + const optimisticRecordInput = + optimisticRecord ?? + computeOptimisticRecordFromInput({ + objectMetadataItem, + currentWorkspaceMember: currentWorkspaceMember, + recordInput: updateOneRecordInput, + cache: apolloCoreClient.cache, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }); + const cachedRecord = getRecordFromCache({ + cache: apolloCoreClient.cache, + objectMetadataItem, + objectMetadataItems, + recordId: idToUpdate, + recordGqlFields: computedRecordGqlFields, + objectPermissionsByObjectMetadataId, + }); + const cachedRecordWithConnection = getRecordNodeFromRecord({ + record: cachedRecord, + objectMetadataItem, + objectMetadataItems, + recordGqlFields: computedRecordGqlFields, + computeReferences: false, + }); + + const computedOptimisticRecord = { + ...cachedRecord, + ...optimisticRecordInput, + id: idToUpdate, + __typename: getObjectTypename(objectMetadataItem.nameSingular), + }; + + const optimisticRecordWithConnection = + getRecordNodeFromRecord({ + record: computedOptimisticRecord, + objectMetadataItem, + objectMetadataItems, + recordGqlFields: computedRecordGqlFields, + computeReferences: false, + }); + + const shouldHandleOptimisticCache = + !isNull(cachedRecord) && + isDefined(optimisticRecordWithConnection) && + isDefined(cachedRecordWithConnection); + + if (shouldHandleOptimisticCache) { + const recordGqlFields = computeDepthOneRecordGqlFieldsFromRecord({ + objectMetadataItem, + record: optimisticRecordInput, + }); + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloCoreClient.cache, + record: computedOptimisticRecord, + recordGqlFields, + objectPermissionsByObjectMetadataId, + }); + + triggerUpdateRecordOptimisticEffect({ + cache: apolloCoreClient.cache, + objectMetadataItem, + currentRecord: cachedRecordWithConnection, + updatedRecord: optimisticRecordWithConnection, + objectMetadataItems, + }); + } + + const mutationResponseField = + getUpdateOneRecordMutationResponseField(objectNameSingular); + + const sanitizedInput = { + ...sanitizeRecordInput({ + objectMetadataItem, + recordInput: updateOneRecordInput, + }), + }; + + const updateOneRecordMutation = generateUpdateOneRecordMutation({ + objectMetadataItem, + objectMetadataItems, + recordGqlFields, + computeReferences: false, + objectPermissionsByObjectMetadataId, + }); + + const updatedRecord = await apolloCoreClient + .mutate({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate, + input: sanitizedInput, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + if (!isDefined(record)) return; + + triggerUpdateRecordOptimisticEffect({ + cache, + objectMetadataItem, + currentRecord: computedOptimisticRecord, + updatedRecord: record, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (!shouldHandleOptimisticCache) { + throw error; + } + const cachedRecordKeys = new Set(Object.keys(cachedRecord)); + const recordKeysAddedByOptimisticCache = Object.keys( + optimisticRecordInput, + ).filter((diffKey) => !cachedRecordKeys.has(diffKey)); + + const recordGqlFields = { + ...computeDepthOneRecordGqlFieldsFromRecord({ + objectMetadataItem, + record: cachedRecord, + }), + ...buildRecordFromKeysWithSameValue( + recordKeysAddedByOptimisticCache, + true, + ), + }; + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloCoreClient.cache, + record: { + ...cachedRecord, + ...buildRecordFromKeysWithSameValue( + recordKeysAddedByOptimisticCache, + null, + ), + }, + recordGqlFields, + objectPermissionsByObjectMetadataId, + }); + + triggerUpdateRecordOptimisticEffect({ + cache: apolloCoreClient.cache, + objectMetadataItem, + currentRecord: optimisticRecordWithConnection, + updatedRecord: cachedRecordWithConnection, + objectMetadataItems, + }); + + throw error; + }); + + return updatedRecord?.data?.[mutationResponseField] ?? null; + }; + + return { + updateOneRecord, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getTargetFieldMetadataName.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getTargetFieldMetadataName.ts deleted file mode 100644 index cf93fca43a4..00000000000 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getTargetFieldMetadataName.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; -import { - type FieldMorphRelationMetadata, - type FieldRelationMetadata, -} from '@/object-record/record-field/ui/types/FieldMetadata'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; - -export const getTargetFieldMetadataName = ({ - fieldDefinition, - objectNameSingular, -}: { - fieldDefinition: - | FieldDefinition - | FieldDefinition; - objectNameSingular: string; -}) => { - if (isFieldRelation(fieldDefinition)) { - return fieldDefinition.metadata.targetFieldMetadataName; - } else { - return fieldDefinition.metadata.morphRelations.find( - (morphRelation) => - morphRelation.targetObjectMetadata.nameSingular === objectNameSingular, - )?.targetFieldMetadata.name; - } -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx index 01cac9cfcb3..d2d972b0cef 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx @@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; -import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; @@ -22,6 +21,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; +import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput'; export const RecordDetailRelationSectionDropdownToMany = () => { const { recordId, fieldDefinition } = useContext(FieldContext); @@ -83,7 +83,7 @@ export const RecordDetailRelationSectionDropdownToMany = () => { setMultipleRecordPickerSearchFilter(''); }, [setMultipleRecordPickerSearchFilter]); - const { updateRelation } = useUpdateRelationFromManyFieldInput(); + const { updateRelation } = useUpdateRelationOneToManyFieldInput(); const { createNewRecordAndOpenRightDrawer } = useAddNewRecordAndOpenRightDrawer({ diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx index 38c81d7bf2e..2a03d3f9181 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx @@ -134,7 +134,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => { componentInstanceId={dropdownId} EmptyIcon={IconForbid} onRecordSelected={handleRelationPickerEntitySelected} - objectNameSingular={relationObjectMetadataNameSingular} + objectNameSingulars={[relationObjectMetadataNameSingular]} recordPickerInstanceId={dropdownId} onCancel={() => closeDropdown(dropdownId)} onCreate={shouldAllowCreateNew ? handleCreateNew : undefined} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldGqlFields.ts index 27d09373f77..1c60845d3f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldGqlFields.ts @@ -9,7 +9,7 @@ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-fie import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; export const useRecordFieldGqlFields = ({ objectMetadataItem, @@ -73,10 +73,18 @@ export const useRecordFieldGqlFields = ({ ); } - return fieldMetadataItem.morphRelations.map((morphRelation) => [ - morphRelation.sourceFieldMetadata.name, - true, - ]); + return fieldMetadataItem.morphRelations.map((morphRelation) => { + const computedFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadataItem.name, + relationType: fieldMetadataItem.settings?.relationType, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); + + return [computedFieldName, true]; + }); }, ), ); 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 8089dc462fc..9a80fc4f20c 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 @@ -19,8 +19,6 @@ import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFi import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks'; import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones'; import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating'; -import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects'; -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'; @@ -28,6 +26,8 @@ import { MorphRelationManyToOneFieldDisplay } from '@/object-record/record-field 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 { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany'; import { isDefined } from 'twenty-shared/utils'; import { FieldContext } from '../contexts/FieldContext'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; @@ -74,9 +74,9 @@ export const FieldDisplay = () => { return isChipDisplay ? ( - ) : isFieldRelationToOneObject(fieldDefinition) ? ( + ) : isFieldRelationManyToOne(fieldDefinition) ? ( - ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( + ) : isFieldRelationOneToMany(fieldDefinition) ? ( ) : isFieldMorphRelationManyToOne(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldInput.tsx index f50306f62cb..5caf0845436 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FieldInput.tsx @@ -8,13 +8,14 @@ import { LinksFieldInput } from '@/object-record/record-field/ui/meta-types/inpu import { MultiSelectFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput'; import { PhonesFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput'; import { RawJsonFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RawJsonFieldInput'; -import { RelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/SelectFieldInput'; import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones'; -import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects'; import { ArrayFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput'; +import { MorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput'; import { MorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput'; +import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput'; +import { RelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput'; import { RichTextFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RichTextFieldInput'; import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress'; import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray'; @@ -25,12 +26,14 @@ import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/is import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails'; import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName'; import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks'; +import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany'; import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect'; import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber'; import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating'; import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson'; -import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany'; import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2'; import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect'; import { FieldContext } from '../contexts/FieldContext'; @@ -39,7 +42,6 @@ import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyField import { DateTimeFieldInput } from '../meta-types/input/components/DateTimeFieldInput'; import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput'; import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput'; -import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput'; import { TextFieldInput } from '../meta-types/input/components/TextFieldInput'; import { isFieldText } from '../types/guards/isFieldText'; @@ -48,10 +50,12 @@ export const FieldInput = () => { return ( <> - {isFieldRelationToOneObject(fieldDefinition) ? ( - - ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - + {isFieldRelationManyToOne(fieldDefinition) ? ( + + ) : isFieldRelationOneToMany(fieldDefinition) ? ( + + ) : isFieldMorphRelationManyToOne(fieldDefinition) ? ( + ) : isFieldMorphRelationOneToMany(fieldDefinition) ? ( ) : isFieldPhones(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx index afd748679e6..9a5ce97d37d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx @@ -42,7 +42,7 @@ import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber'; import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones'; import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson'; -import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2'; import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFieldText'; @@ -210,7 +210,7 @@ export const FormFieldInput = ({ readonly={readonly} placeholder={placeholder} /> - ) : isFieldRelationToOneObject(field) ? ( + ) : isFieldRelationManyToOne(field) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx index da0b546368c..a33ea86f4df 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx @@ -16,10 +16,9 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { useCallback, useId } from 'react'; -import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { CustomError, isDefined, isValidUuid } from 'twenty-shared/utils'; import { IconChevronDown, IconForbid } from 'twenty-ui/display'; const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)<{ @@ -63,7 +62,7 @@ export type FormSingleRecordPickerProps = { label?: string; defaultValue?: RecordId | Variable; onChange: (value: RecordId | Variable | null) => void; - objectNameSingular: string; + objectNameSingulars: string[]; disabled?: boolean; testId?: string; VariablePicker?: VariablePickerComponent; @@ -72,14 +71,12 @@ export type FormSingleRecordPickerProps = { export const FormSingleRecordPicker = ({ label, defaultValue, - objectNameSingular, + objectNameSingulars, onChange, disabled, testId, VariablePicker, }: FormSingleRecordPickerProps) => { - const { t } = useLingui(); - const theme = useTheme(); const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString( defaultValue, @@ -93,12 +90,19 @@ export const FormSingleRecordPicker = ({ value: defaultValue || '', }; + if (objectNameSingulars.length === 0) { + throw new CustomError( + 'Object is missing while building form single record picker', + 'FORM_SINGLE_RECORD_PICKER_OBJECT_NAME_SINGULAR_IS_MISSING', + ); + } + const { record: selectedRecord } = useFindOneRecord({ objectRecordId: isDefined(defaultValue) && !isStandaloneVariableString(defaultValue) ? defaultValue : '', - objectNameSingular, + objectNameSingular: objectNameSingulars[0], withSoftDeleted: true, skip: !isDefined(defaultValue) || !isValidUuid(defaultValue), }); @@ -171,7 +175,7 @@ export const FormSingleRecordPicker = ({ @@ -193,7 +197,7 @@ export const FormSingleRecordPicker = ({ @@ -210,10 +214,10 @@ export const FormSingleRecordPicker = ({ focusId={dropdownId} componentInstanceId={dropdownId} EmptyIcon={IconForbid} - emptyLabel={t`No ${objectNameSingular}`} + emptyLabel={'No ' + objectNameSingulars.join(' or ')} onCancel={() => closeDropdown(dropdownId)} onRecordSelected={handleRecordSelected} - objectNameSingular={objectNameSingular} + objectNameSingulars={objectNameSingulars} recordPickerInstanceId={dropdownId} dropdownWidth={GenericDropdownContentWidth.ExtraLarge} /> diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx index 276eeafd75b..99f345befd2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx @@ -36,7 +36,7 @@ export const Default: Story = { args: { label: 'Company', defaultValue: '123e4567-e89b-12d3-a456-426614174000', - objectNameSingular: 'company', + objectNameSingulars: ['company'], onChange: fn(), }, play: async ({ canvasElement }) => { @@ -56,7 +56,7 @@ export const WithVariables: Story = { args: { label: 'Company', defaultValue: `{{${MOCKED_STEP_ID}.company.id}}`, - objectNameSingular: 'company', + objectNameSingulars: ['company'], onChange: fn(), VariablePicker: () =>
VariablePicker
, }, @@ -80,7 +80,7 @@ export const Disabled: Story = { args: { label: 'Company', defaultValue: '123e4567-e89b-12d3-a456-426614174000', - objectNameSingular: 'company', + objectNameSingulars: ['company'], onChange: fn(), disabled: true, VariablePicker: () =>
VariablePicker
, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useOpenFieldInputEditMode.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useOpenFieldInputEditMode.ts index a859c4e118c..ad639085401 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useOpenFieldInputEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useOpenFieldInputEditMode.ts @@ -5,7 +5,8 @@ import { type Task } from '@/activities/types/Task'; import { type TaskTarget } from '@/activities/types/TaskTarget'; import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { useOpenMorphRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput'; +import { useOpenMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput'; +import { useOpenMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput'; import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationFromManyFieldInput'; import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationToOneFieldInput'; import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; @@ -15,9 +16,10 @@ import { type FieldRelationValue, } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany'; -import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects'; -import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -32,12 +34,15 @@ export const useOpenFieldInputEditMode = () => { const { openRelationFromManyFieldInput } = useOpenRelationFromManyFieldInput(); - const { openMorphRelationFromManyFieldInput } = - useOpenMorphRelationFromManyFieldInput(); + const { openMorphRelationOneToManyFieldInput } = + useOpenMorphRelationOneToManyFieldInput(); const { openActivityTargetCellEditMode } = useOpenActivityTargetCellEditMode(); + const { openMorphRelationManyToOneFieldInput } = + useOpenMorphRelationManyToOneFieldInput(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const openFieldInput = useRecoilCallback( @@ -52,7 +57,7 @@ export const useOpenFieldInputEditMode = () => { prefix?: string; }) => { if ( - isFieldRelationFromManyObjects(fieldDefinition) && + isFieldRelationOneToMany(fieldDefinition) && ['taskTarget', 'noteTarget'].includes( fieldDefinition.metadata.relationObjectMetadataNameSingular, ) @@ -91,7 +96,7 @@ export const useOpenFieldInputEditMode = () => { return; } - if (isFieldRelationToOneObject(fieldDefinition)) { + if (isFieldRelationManyToOne(fieldDefinition)) { openRelationToOneFieldInput({ fieldName: fieldDefinition.metadata.fieldName, recordId, @@ -106,7 +111,7 @@ export const useOpenFieldInputEditMode = () => { throw new Error('Field is not a morph relation one to many'); } - openMorphRelationFromManyFieldInput({ + openMorphRelationOneToManyFieldInput({ recordId, prefix, fieldDefinition, @@ -114,7 +119,7 @@ export const useOpenFieldInputEditMode = () => { return; } - if (isFieldRelationFromManyObjects(fieldDefinition)) { + if (isFieldRelationOneToMany(fieldDefinition)) { if ( isDefined( fieldDefinition.metadata.relationObjectMetadataNameSingular, @@ -131,6 +136,15 @@ export const useOpenFieldInputEditMode = () => { } } + if (isFieldMorphRelationManyToOne(fieldDefinition)) { + openMorphRelationManyToOneFieldInput({ + recordId, + prefix, + fieldDefinition, + }); + return; + } + pushFocusItemToFocusStack({ focusId: getRecordFieldInputInstanceId({ recordId, @@ -152,7 +166,8 @@ export const useOpenFieldInputEditMode = () => { }, [ openActivityTargetCellEditMode, - openMorphRelationFromManyFieldInput, + openMorphRelationManyToOneFieldInput, + openMorphRelationOneToManyFieldInput, openRelationFromManyFieldInput, openRelationToOneFieldInput, pushFocusItemToFocusStack, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts index dfcce9029a2..bd70155a493 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts @@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil'; import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; import { type FieldMetadata, + type FieldMorphRelationMetadata, type FieldRelationMetadata, } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress'; @@ -21,21 +22,28 @@ import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFi import { isFieldPhonesValue } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue'; import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson'; import { isFieldRawJsonValue } from '@/object-record/record-field/ui/types/guards/isFieldRawJsonValue'; -import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; -import { isFieldRelationToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneValue'; import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect'; import { isFieldSelectValue } from '@/object-record/record-field/ui/types/guards/isFieldSelectValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray'; import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isFieldRelationManyToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue'; import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText'; import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2'; import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValue'; import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isFieldBoolean } from '../types/guards/isFieldBoolean'; import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue'; @@ -55,13 +63,16 @@ export const usePersistField = ({ }: { objectMetadataItemId: string; }) => { + const apolloCoreClient = useApolloCoreClient(); const { objectMetadataItem } = useObjectMetadataItemById({ objectId: objectMetadataItemId, }); + const { objectMetadataItems } = useObjectMetadataItems(); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular: objectMetadataItem?.nameSingular ?? '', }); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const persistField = useRecoilCallback( ({ set, snapshot }) => @@ -74,10 +85,15 @@ export const usePersistField = ({ fieldDefinition: FieldDefinition; valueToPersist: unknown; }) => { - const fieldIsRelationToOneObject = - isFieldRelationToOneObject( + const fieldIsRelationManyToOne = + isFieldRelationManyToOne( fieldDefinition as FieldDefinition, - ) && isFieldRelationToOneValue(valueToPersist); + ) && isFieldRelationManyToOneValue(valueToPersist); + + const fieldIsMorphRelationManyToOne = + isFieldMorphRelationManyToOne( + fieldDefinition as FieldDefinition, + ) && isFieldRelationManyToOneValue(valueToPersist); const fieldIsText = isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist); @@ -150,7 +166,8 @@ export const usePersistField = ({ } const isValuePersistable = - fieldIsRelationToOneObject || + fieldIsMorphRelationManyToOne || + fieldIsRelationManyToOne || fieldIsText || fieldIsBoolean || fieldIsEmails || @@ -177,23 +194,14 @@ export const usePersistField = ({ .getLoadable(recordStoreFamilySelector({ recordId, fieldName })) .getValue(); - if ( - fieldIsRelationToOneObject && - valueToPersist?.id === currentValue?.id - ) { - return; - } + if (fieldIsRelationManyToOne) { + if ( + fieldIsRelationManyToOne && + valueToPersist?.id === currentValue?.id + ) { + return; + } - if (isDeeplyEqual(valueToPersist, currentValue)) { - return; - } - - set( - recordStoreFamilySelector({ recordId, fieldName }), - valueToPersist, - ); - - if (fieldIsRelationToOneObject) { updateOneRecord?.({ idToUpdate: recordId, updateOneRecordInput: { @@ -201,6 +209,79 @@ export const usePersistField = ({ valueToPersist?.id ?? null, }, }); + + set( + recordStoreFamilySelector({ recordId, fieldName }), + valueToPersist, + ); + return; + } + + if (fieldIsMorphRelationManyToOne) { + if (isFieldMorphRelation(fieldDefinition)) { + // TODO: this is hack, we should refactor the SingleRecordPicker to pass the updated object + const objectNameSingulars = + fieldDefinition.metadata.morphRelations.map( + (morphRelation) => + morphRelation.targetObjectMetadata.nameSingular, + ); + + if (!isDefined(valueToPersist?.id)) { + // Handle detach + return; + } + + for (const objectNameSingular of objectNameSingulars) { + const candidateObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === objectNameSingular, + ); + + if (!isDefined(candidateObjectMetadataItem)) { + throw new Error( + `Object metadata item not found for ${objectNameSingular}`, + ); + } + + const cachedRecord = getRecordFromCache({ + cache: apolloCoreClient.cache, + objectMetadataItem: candidateObjectMetadataItem, + objectMetadataItems, + recordId: valueToPersist.id, + objectPermissionsByObjectMetadataId, + }); + + if (!isDefined(cachedRecord)) { + continue; + } + + const computedFieldName = computeMorphRelationFieldName({ + fieldName, + relationType: fieldDefinition.metadata.relationType, + targetObjectMetadataNameSingular: objectNameSingular, + targetObjectMetadataNamePlural: + candidateObjectMetadataItem.namePlural, + }); + + updateOneRecord?.({ + idToUpdate: recordId, + updateOneRecordInput: { + [`${computedFieldName}Id`]: valueToPersist.id, + }, + }); + set( + recordStoreFamilySelector({ + recordId, + fieldName: computedFieldName, + }), + valueToPersist, + ); + return; + } + } + } + + if (isDeeplyEqual(valueToPersist, currentValue)) { return; } @@ -210,6 +291,10 @@ export const usePersistField = ({ [fieldName]: valueToPersist, }, }); + set( + recordStoreFamilySelector({ recordId, fieldName }), + valueToPersist, + ); } else { throw new Error( `Invalid value to persist: ${JSON.stringify( @@ -220,7 +305,12 @@ export const usePersistField = ({ ); } }, - [updateOneRecord], + [ + apolloCoreClient.cache, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + updateOneRecord, + ], ); return persistField; 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 index 354b8951a2e..9e29f809bca 100644 --- 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 @@ -36,7 +36,6 @@ export const useMorphRelationFromManyFieldDisplay = () => { recordStoreMorphOneToManyValueWithObjectNameFamilySelector({ recordId, morphRelations: fieldDefinition.metadata.morphRelations, - fieldName: fieldDefinition.metadata.fieldName, }), ); 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 index 8b2e270b149..e80115c24cc 100644 --- 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 @@ -17,7 +17,6 @@ import { isDefined } from 'twenty-shared/utils'; export const useMorphRelationToOneFieldDisplay = () => { const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); - const { chipGeneratorPerObjectPerField } = useContext( PreComputedChipGeneratorsContext, ); @@ -34,13 +33,10 @@ export const useMorphRelationToOneFieldDisplay = () => { const button = fieldDefinition.editButtonIcon; - const fieldName = fieldDefinition.metadata.fieldName; - const morphFieldValueWithObjectName = useRecoilValue( recordStoreMorphManyToOneValueWithObjectNameFamilySelector({ recordId, morphRelations: fieldDefinition.metadata.morphRelations, - fieldName, }), ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx new file mode 100644 index 00000000000..02a57154123 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx @@ -0,0 +1,69 @@ +import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; + +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; +import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState'; +import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; +import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useContext } from 'react'; +import { IconForbid } from 'twenty-ui/display'; + +export const MorphRelationManyToOneFieldInput = () => { + const { fieldDefinition } = useContext(FieldContext); + + const { onCancel, onSubmit } = useContext(FieldInputEventContext); + const { onCloseTableCell } = useRecordTableBodyContextOrThrow(); + + const instanceId = useAvailableComponentInstanceIdOrThrow( + RecordFieldComponentInstanceContext, + ); + + const handleRecordSelected = ( + selectedRecord: SingleRecordPickerRecord | null | undefined, + ) => { + onSubmit?.({ newValue: selectedRecord?.record ?? null }); + onCloseTableCell(); + }; + + const layoutDirection = useRecoilComponentValue( + recordFieldInputLayoutDirectionComponentState, + ); + + const isLoading = useRecoilComponentValue( + recordFieldInputLayoutDirectionLoadingComponentState, + ); + + if (isLoading) { + return <>; + } + + if (!isFieldMorphRelationManyToOne(fieldDefinition)) { + return null; + } + const objectNameSingulars = fieldDefinition.metadata.morphRelations.map( + (morphRelation) => morphRelation.targetObjectMetadata.nameSingular, + ); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput.tsx index be999eaeb39..a0e84a13b00 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput.tsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; -import { useUpdateMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput'; +import { useUpdateMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; @@ -15,8 +15,8 @@ export const MorphRelationOneToManyFieldInput = () => { const { onSubmit } = useContext(FieldInputEventContext); - const { updateMorphRelationManyToOne } = - useUpdateMorphRelationManyToOneFieldInput(); + const { updateMorphRelationOneToMany } = + useUpdateMorphRelationOneToManyFieldInput(); const handleSubmit = () => { onSubmit?.({ skipPersist: true }); @@ -32,7 +32,7 @@ export const MorphRelationOneToManyFieldInput = () => { componentInstanceId={instanceId} onSubmit={handleSubmit} onChange={(morphItem) => { - updateMorphRelationManyToOne(morphItem); + updateMorphRelationOneToMany(morphItem); }} onClickOutside={handleSubmit} layoutDirection={ diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationToOneFieldInput.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx index 47334a17088..9d4075d0155 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx @@ -18,7 +18,7 @@ import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { IconForbid } from 'twenty-ui/display'; -export const RelationToOneFieldInput = () => { +export const RelationManyToOneFieldInput = () => { const { t } = useLingui(); const { fieldDefinition, recordId } = useRelationField(); @@ -86,9 +86,9 @@ export const RelationToOneFieldInput = () => { onCancel={onCancel} onCreate={handleCreateNew} onRecordSelected={handleRecordSelected} - objectNameSingular={ - fieldDefinition.metadata.relationObjectMetadataNameSingular - } + objectNameSingulars={[ + fieldDefinition.metadata.relationObjectMetadataNameSingular, + ]} recordPickerInstanceId={instanceId} layoutDirection={ layoutDirection === 'downward' diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx similarity index 96% rename from packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx index a0de03bef24..225ab7a2d6c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx @@ -10,7 +10,6 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; import { useRelationField } from '@/object-record/record-field/ui/meta-types/hooks/useRelationField'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; -import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; @@ -23,8 +22,9 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component- import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput'; -export const RelationFromManyFieldInput = () => { +export const RelationOneToManyFieldInput = () => { const { fieldDefinition, recordId } = useContext(FieldContext); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, @@ -32,7 +32,7 @@ export const RelationFromManyFieldInput = () => { const { onSubmit } = useContext(FieldInputEventContext); - const { updateRelation } = useUpdateRelationFromManyFieldInput(); + const { updateRelation } = useUpdateRelationOneToManyFieldInput(); const fieldName = fieldDefinition.metadata.fieldName; const objectMetadataNameSingular = fieldDefinition.metadata.objectMetadataNameSingular; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx similarity index 91% rename from packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx index 97e54b82323..993dd1b55b5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx @@ -22,8 +22,8 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { FieldMetadataType } from 'twenty-shared/types'; import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; -import { RelationToOneFieldInput } from '../RelationToOneFieldInput'; +import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks'; @@ -56,14 +56,14 @@ const { handleCancelMocked, } = getFieldInputEventContextProviderWithJestMocks(); -type RelationToOneFieldInputWithContextProps = { +type RelationManyToOneFieldInputWithContextProps = { value: number; recordId: string; }; -const RelationToOneFieldInputWithContext = ({ +const RelationManyToOneFieldInputWithContext = ({ recordId, -}: RelationToOneFieldInputWithContextProps) => { +}: RelationManyToOneFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); useEffect(() => { @@ -108,7 +108,7 @@ const RelationToOneFieldInputWithContext = ({ - + @@ -126,8 +126,8 @@ const clearMocksDecorator: Decorator = (Story, context) => { }; const meta: Meta = { - title: 'UI/Data/Field/Input/RelationToOneFieldInput', - component: RelationToOneFieldInputWithContext, + title: 'UI/Data/Field/Input/RelationManyToOneFieldInput', + component: RelationManyToOneFieldInputWithContext, args: { useEditButton: true, onSubmit: handleSubmitMocked, @@ -151,7 +151,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationOneToManyFieldInput.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationOneToManyFieldInput.stories.tsx index 81c22340347..bc973a4925f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationOneToManyFieldInput.stories.tsx @@ -4,7 +4,6 @@ import { useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { RelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; @@ -17,6 +16,7 @@ import { import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/ui/hooks/useOpenFieldInputEditMode'; +import { RelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; @@ -37,7 +37,7 @@ const RelationWorkspaceSetterEffect = () => { return <>; }; -const RelationManyFieldInputWithContext = () => { +const RelationOneToManyFieldInputWithContext = () => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const fieldDefinition = useMemo( @@ -104,7 +104,7 @@ const RelationManyFieldInputWithContext = () => { }} > - +
@@ -112,8 +112,8 @@ const RelationManyFieldInputWithContext = () => { ); }; const meta: Meta = { - title: 'UI/Data/Field/Input/RelationManyFieldInput', - component: RelationManyFieldInputWithContext, + title: 'UI/Data/Field/Input/RelationOneToManyFieldInput', + component: RelationOneToManyFieldInputWithContext, args: {}, decorators: [ObjectMetadataItemsDecorator, SnackBarDecorator], parameters: { @@ -124,6 +124,6 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput.tsx new file mode 100644 index 00000000000..4aab804c352 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput.tsx @@ -0,0 +1,91 @@ +import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; +import { + type FieldMorphRelationMetadata, + type FieldRelationToOneValue, + type FieldRelationValue, +} from '@/object-record/record-field/ui/types/FieldMetadata'; +import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; +import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; +import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { useRecoilCallback } from 'recoil'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; + +export const useOpenMorphRelationManyToOneFieldInput = () => { + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { openSingleRecordPicker } = useSingleRecordPickerOpen(); + + const openMorphRelationManyToOneFieldInput = useRecoilCallback( + ({ set, snapshot }) => + ({ + fieldDefinition, + recordId, + prefix, + }: { + fieldDefinition: FieldDefinition; + recordId: string; + prefix?: string; + }) => { + const potentielFieldNames = fieldDefinition.metadata.morphRelations.map( + (morphRelation) => { + return computeMorphRelationFieldName({ + fieldName: fieldDefinition.metadata.fieldName, + relationType: fieldDefinition.metadata.relationType, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); + }, + ); + + const fieldValue = potentielFieldNames + .map((fieldName) => { + const fieldValue = snapshot + .getLoadable>( + recordStoreFamilySelector({ + recordId, + fieldName, + }), + ) + .getValue(); + + return fieldValue; + }) + .find((fieldValue) => isDefined(fieldValue)); + + const recordPickerInstanceId = getRecordFieldInputInstanceId({ + recordId, + fieldName: fieldDefinition.metadata.fieldName, + prefix, + }); + + if (isDefined(fieldValue)) { + set( + singleRecordPickerSelectedIdComponentState.atomFamily({ + instanceId: recordPickerInstanceId, + }), + fieldValue.id, + ); + } + + openSingleRecordPicker(recordPickerInstanceId); + + pushFocusItemToFocusStack({ + focusId: recordPickerInstanceId, + component: { + type: FocusComponentType.OPENED_FIELD_INPUT, + instanceId: recordPickerInstanceId, + }, + globalHotkeysConfig: { + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + }); + }, + [openSingleRecordPicker, pushFocusItemToFocusStack], + ); + + return { openMorphRelationManyToOneFieldInput }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx similarity index 95% rename from packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx index b4420c46562..cb8bef82491 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx @@ -16,13 +16,13 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -export const useOpenMorphRelationFromManyFieldInput = () => { +export const useOpenMorphRelationOneToManyFieldInput = () => { const { performSearch } = useMultipleRecordPickerPerformSearch(); const { openMultipleRecordPicker } = useMultipleRecordPickerOpen(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); - const openMorphRelationFromManyFieldInput = useRecoilCallback( + const openMorphRelationOneToManyFieldInput = useRecoilCallback( ({ set, snapshot }) => ({ recordId, @@ -48,7 +48,6 @@ export const useOpenMorphRelationFromManyFieldInput = () => { recordStoreMorphOneToManyValueWithObjectNameFamilySelector({ recordId, morphRelations: fieldDefinition.metadata.morphRelations, - fieldName: fieldDefinition.metadata.fieldName, }), ) .getValue(); @@ -136,5 +135,7 @@ export const useOpenMorphRelationFromManyFieldInput = () => { [openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack], ); - return { openMorphRelationFromManyFieldInput }; + return { + openMorphRelationOneToManyFieldInput, + }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput.tsx deleted file mode 100644 index ef5c97719d7..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; - -import { useAttachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useAttachMorphRelatedRecordFromRecord'; -import { useDetachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useDetachMorphRelatedRecordFromRecord'; -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 { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export const useUpdateMorphRelationManyToOneFieldInput = () => { - const { recordId, fieldDefinition } = useContext(FieldContext); - - assertFieldMetadata( - FieldMetadataType.MORPH_RELATION, - isFieldMorphRelation, - fieldDefinition, - ); - - if (!fieldDefinition.metadata.objectMetadataNameSingular) { - throw new Error('ObjectMetadataNameSingular is required'); - } - - const { updateOneRecordAndDetachMorphRelations } = - useDetachMorphRelatedRecordFromRecord(); - - const { updateOneRecordAndAttachMorphRelations } = - useAttachMorphRelatedRecordFromRecord(); - - const updateMorphRelationManyToOne = useRecoilCallback( - () => - async ( - morphItem: Pick< - RecordPickerPickableMorphItem, - 'recordId' | 'isSelected' - >, - ) => { - const recordObjectNameSingulars = - fieldDefinition.metadata.morphRelations.map( - (morphRelation) => morphRelation.targetObjectMetadata.nameSingular, - ); - if (morphItem.isSelected) { - await updateOneRecordAndAttachMorphRelations({ - recordId, - relatedRecordId: morphItem.recordId, - objectNameSingulars: recordObjectNameSingulars, - }); - } else { - await updateOneRecordAndDetachMorphRelations({ - recordId, - relatedRecordId: morphItem.recordId, - objectNameSingulars: recordObjectNameSingulars, - }); - } - }, - [ - fieldDefinition.metadata.morphRelations, - recordId, - updateOneRecordAndAttachMorphRelations, - updateOneRecordAndDetachMorphRelations, - ], - ); - - return { updateMorphRelationManyToOne }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput.tsx new file mode 100644 index 00000000000..3b87315f3e2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput.tsx @@ -0,0 +1,87 @@ +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; + +import { useRecordOneToManyFieldAttachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord'; +import { useRecordOneToManyFieldDetachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord'; +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 { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; +import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const useUpdateMorphRelationOneToManyFieldInput = () => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + + if (!fieldDefinition.metadata.objectMetadataNameSingular) { + throw new Error('ObjectMetadataNameSingular is required'); + } + + const { recordOneToManyFieldDetachTargetRecord } = + useRecordOneToManyFieldDetachTargetRecord(); + + const { recordOneToManyFieldAttachTargetRecord } = + useRecordOneToManyFieldAttachTargetRecord(); + + const updateMorphRelationOneToMany = useRecoilCallback( + () => + async ( + morphItem: Pick< + RecordPickerPickableMorphItem, + 'recordId' | 'isSelected' | 'objectMetadataId' + >, + ) => { + if (!fieldDefinition.metadata.objectMetadataNameSingular) { + throw new Error('ObjectMetadataNameSingular is required'); + } + + const targetMorphRelation = + fieldDefinition.metadata.morphRelations.find( + (morphRelation) => + morphRelation.targetObjectMetadata.id === + morphItem.objectMetadataId, + ); + + if (!isDefined(targetMorphRelation)) { + throw new Error('TargetMorphRelation is required'); + } + + if (morphItem.isSelected) { + await recordOneToManyFieldAttachTargetRecord({ + sourceObjectNameSingular: + fieldDefinition.metadata.objectMetadataNameSingular, + targetObjectNameSingular: + targetMorphRelation.targetObjectMetadata.nameSingular, + targetGQLFieldName: targetMorphRelation.targetFieldMetadata.name, + sourceRecordId: recordId, + targetRecordId: morphItem.recordId, + }); + } else { + await recordOneToManyFieldDetachTargetRecord({ + sourceObjectNameSingular: + fieldDefinition.metadata.objectMetadataNameSingular, + targetObjectNameSingular: + targetMorphRelation.targetObjectMetadata.nameSingular, + targetGQLFieldName: targetMorphRelation.targetFieldMetadata.name, + sourceRecordId: recordId, + targetRecordId: morphItem.recordId, + }); + } + }, + [ + fieldDefinition.metadata.morphRelations, + fieldDefinition.metadata.objectMetadataNameSingular, + recordId, + recordOneToManyFieldAttachTargetRecord, + recordOneToManyFieldDetachTargetRecord, + ], + ); + + return { updateMorphRelationOneToMany }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx deleted file mode 100644 index de3155a5daf..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; - -import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord'; -import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; -import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export const useUpdateRelationFromManyFieldInput = () => { - const { recordId, fieldDefinition } = useContext(FieldContext); - - assertFieldMetadata( - FieldMetadataType.RELATION, - isFieldRelation, - fieldDefinition, - ); - - if (!fieldDefinition.metadata.objectMetadataNameSingular) { - throw new Error('ObjectMetadataNameSingular is required'); - } - - const { updateOneRecordAndDetachRelations } = - useDetachRelatedRecordFromRecord({ - recordObjectNameSingular: - fieldDefinition.metadata.objectMetadataNameSingular, - fieldNameOnRecordObject: fieldDefinition.metadata.fieldName, - }); - - const { updateOneRecordAndAttachRelations } = - useAttachRelatedRecordFromRecord({ - recordObjectNameSingular: - fieldDefinition.metadata.objectMetadataNameSingular, - fieldNameOnRecordObject: fieldDefinition.metadata.fieldName, - }); - - const updateRelation = useRecoilCallback( - () => async (morphItem: RecordPickerPickableMorphItem) => { - if (morphItem.isSelected) { - await updateOneRecordAndAttachRelations({ - recordId, - relatedRecordId: morphItem.recordId, - }); - } else { - await updateOneRecordAndDetachRelations({ - recordId, - relatedRecordId: morphItem.recordId, - }); - } - }, - [ - recordId, - updateOneRecordAndAttachRelations, - updateOneRecordAndDetachRelations, - ], - ); - - return { updateRelation }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput.tsx new file mode 100644 index 00000000000..1c1256ea3d4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput.tsx @@ -0,0 +1,123 @@ +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useRecordOneToManyFieldAttachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord'; +import { useRecordOneToManyFieldDetachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const useUpdateRelationOneToManyFieldInput = () => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.RELATION, + isFieldRelation, + fieldDefinition, + ); + + const { objectMetadataItem: targetObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + }); + + if (!isDefined(fieldDefinition.metadata.objectMetadataNameSingular)) { + throw new Error('ObjectMetadataNameSingular is required'); + } + + const { objectMetadataItem: sourceObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: fieldDefinition.metadata.objectMetadataNameSingular, + }); + + if (!fieldDefinition.metadata.objectMetadataNameSingular) { + throw new Error('ObjectMetadataNameSingular is required'); + } + + if (!isDefined(targetObjectMetadataItem)) { + throw new Error('TargetObjectMetadataItem is required'); + } + + const { recordOneToManyFieldDetachTargetRecord } = + useRecordOneToManyFieldDetachTargetRecord(); + + const { recordOneToManyFieldAttachTargetRecord } = + useRecordOneToManyFieldAttachTargetRecord(); + + const updateRelation = useRecoilCallback( + () => async (morphItem: RecordPickerPickableMorphItem) => { + if ( + !fieldDefinition.metadata?.relationObjectMetadataNameSingular || + !fieldDefinition.metadata?.targetFieldMetadataName + ) { + throw new Error('RelationObjectMetadataNameSingular is required'); + } + + if (!fieldDefinition.metadata?.objectMetadataNameSingular) { + throw new Error('RelationFieldMetadata is required'); + } + + const targetFieldMetadata = targetObjectMetadataItem.fields.find( + (field) => + field.name === fieldDefinition.metadata.targetFieldMetadataName, + ); + + if (!isDefined(targetFieldMetadata)) { + throw new Error('TargetFieldMetadata is required'); + } + + let targetGQLFieldName: string; + if (targetFieldMetadata.type === FieldMetadataType.MORPH_RELATION) { + targetGQLFieldName = computeMorphRelationFieldName({ + fieldName: fieldDefinition.metadata.targetFieldMetadataName, + relationType: targetFieldMetadata.settings?.relationType, + targetObjectMetadataNameSingular: + sourceObjectMetadataItem.nameSingular, + targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural, + }); + } else { + targetGQLFieldName = fieldDefinition.metadata.targetFieldMetadataName; + } + + if (morphItem.isSelected) { + await recordOneToManyFieldAttachTargetRecord({ + sourceObjectNameSingular: + fieldDefinition.metadata.objectMetadataNameSingular, + targetObjectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + targetGQLFieldName, + sourceRecordId: recordId, + targetRecordId: morphItem.recordId, + }); + } else { + await recordOneToManyFieldDetachTargetRecord({ + sourceObjectNameSingular: + fieldDefinition.metadata.objectMetadataNameSingular, + targetObjectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + targetGQLFieldName, + sourceRecordId: recordId, + targetRecordId: morphItem.recordId, + }); + } + }, + [ + fieldDefinition.metadata.objectMetadataNameSingular, + fieldDefinition.metadata.relationObjectMetadataNameSingular, + fieldDefinition.metadata.targetFieldMetadataName, + recordId, + recordOneToManyFieldAttachTargetRecord, + recordOneToManyFieldDetachTargetRecord, + sourceObjectMetadataItem.namePlural, + sourceObjectMetadataItem.nameSingular, + targetObjectMetadataItem.fields, + ], + ); + + return { updateRelation }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneObject.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOne.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneObject.ts rename to packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOne.ts index e2731354d95..82149c6f325 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneObject.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOne.ts @@ -7,7 +7,7 @@ import { type FieldRelationMetadata, } from '../FieldMetadata'; -export const isFieldRelationToOneObject = ( +export const isFieldRelationManyToOne = ( field: Pick, 'type' | 'metadata'>, ): field is FieldDefinition => isFieldRelation(field) && diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue.ts similarity index 87% rename from packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneValue.ts rename to packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue.ts index b67b1ac4438..7f2d9839ebf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationToOneValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue.ts @@ -3,7 +3,7 @@ import { isNull, isObject, isUndefined } from '@sniptt/guards'; import { type FieldRelationToOneValue } from '@/object-record/record-field/ui/types/FieldMetadata'; // TODO: add zod -export const isFieldRelationToOneValue = ( +export const isFieldRelationManyToOneValue = ( fieldValue: unknown, ): fieldValue is FieldRelationToOneValue => !isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue)); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToMany.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects.ts rename to packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToMany.ts index 084622ec214..a3b42bf3fd0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToMany.ts @@ -7,7 +7,7 @@ import { type FieldRelationMetadata, } from '../FieldMetadata'; -export const isFieldRelationFromManyObjects = ( +export const isFieldRelationOneToMany = ( field: Pick, 'type' | 'metadata'>, ): field is FieldDefinition => isFieldRelation(field) && diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToManyValue.ts similarity index 87% rename from packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyValue.ts rename to packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToManyValue.ts index bad389d782e..e97ed4eeec9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationFromManyValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRelationOneToManyValue.ts @@ -3,7 +3,7 @@ import { isNull, isObject, isUndefined } from '@sniptt/guards'; import { type FieldRelationFromManyValue } from '@/object-record/record-field/ui/types/FieldMetadata'; // TODO: add zod -export const isFieldRelationFromManyValue = ( +export const isFieldRelationOneToManyValue = ( fieldValue: unknown, ): fieldValue is FieldRelationFromManyValue => !isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue)); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx index 2af038de254..165fefb6f95 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx @@ -25,7 +25,7 @@ export const SingleRecordPicker = ({ onCancel, onCreate, onRecordSelected, - objectNameSingular, + objectNameSingulars, componentInstanceId, layoutDirection, dropdownWidth, @@ -84,7 +84,7 @@ export const SingleRecordPicker = ({ onCancel: handleCancel, onCreate, onRecordSelected: handleRecordSelected, - objectNameSingular, + objectNameSingulars, layoutDirection, }} /> diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx index 1e1a54495d5..8f1e44357d5 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx @@ -1,5 +1,5 @@ -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { SingleRecordPickerLoadingEffect } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect'; import { SingleRecordPickerMenuItems, @@ -22,7 +22,7 @@ import { IconPlus } from 'twenty-ui/display'; export type SingleRecordPickerMenuItemsWithSearchProps = { excludedRecordIds?: string[]; onCreate?: ((searchInput?: string) => void) | (() => void); - objectNameSingular: string; + objectNameSingulars: string[]; recordPickerInstanceId?: string; layoutDirection?: RecordPickerLayoutDirection; focusId: string; @@ -42,7 +42,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ onCancel, onCreate, onRecordSelected, - objectNameSingular, + objectNameSingulars, layoutDirection = 'search-bar-on-top', focusId, }: SingleRecordPickerMenuItemsWithSearchProps) => { @@ -58,19 +58,24 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ ); const { records } = useSingleRecordPickerRecords({ - objectNameSingulars: [objectNameSingular], + objectNameSingulars, excludedRecordIds, }); - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const objectPermissions = useObjectPermissionsForObject( - objectMetadataItem.id, + const { objectMetadataItems: allObjectMetadataItems } = + useObjectMetadataItems(); + const objectMetadataItems = allObjectMetadataItems.filter( + (objectMetadataItem) => + objectNameSingulars.includes(objectMetadataItem.nameSingular), ); - const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const hasUpdatePermissions = objectMetadataItems.every( + (objectMetadataItem) => + objectPermissionsByObjectMetadataId[objectMetadataItem.id] + ?.canUpdateObjectRecords, + ); const handleCreateNew = () => { onCreate?.(recordPickerSearchFilter); @@ -81,7 +86,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ {layoutDirection === 'search-bar-on-bottom' && ( <> - {isDefined(onCreate) && hasObjectUpdatePermissions && ( + {isDefined(onCreate) && hasUpdatePermissions && ( <> - {isDefined(onCreate) && hasObjectUpdatePermissions && ( + {isDefined(onCreate) && hasUpdatePermissions && ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx index 31c93e3bee0..732b66d5c7d 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx @@ -34,7 +34,7 @@ const meta: Meta = { I18nFrontDecorator, ], args: { - objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + objectNameSingulars: [CoreObjectNameSingular.WorkspaceMember], componentInstanceId: 'single-record-picker', }, argTypes: { 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 index af89493aaa4..7977ec7ca9e 100644 --- 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 @@ -3,24 +3,30 @@ 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 { isDefined } from 'twenty-shared/utils'; +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 fieldName = computeMorphRelationFieldName({ + fieldName: morphRelation.sourceFieldMetadata.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); return { objectNameSingular: morphRelation.targetObjectMetadata.nameSingular, 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 index 5d746e095f0..ade8333d96b 100644 --- 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 @@ -3,23 +3,31 @@ 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 { type 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 fieldName = computeMorphRelationFieldName({ + fieldName: morphRelation.sourceFieldMetadata.name, + relationType: morphRelation.type as RelationType, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); return { objectNameSingular: morphRelation.targetObjectMetadata.nameSingular, 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 d9900b9dd3c..fef06771c2a 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 @@ -3,8 +3,8 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifie import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects'; -import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; @@ -39,8 +39,8 @@ export const RecordTableCellFieldContextGeneric = ({ // todo @guillim : adjust this to handle morph relations permissions display if ( - isFieldRelationToOneObject(fieldDefinition) || - isFieldRelationFromManyObjects(fieldDefinition) + isFieldRelationManyToOne(fieldDefinition) || + isFieldRelationOneToMany(fieldDefinition) ) { const relationObjectMetadataId = fieldDefinition.metadata.relationObjectMetadataId; diff --git a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts index 994311c1694..51345ef4ac6 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts @@ -9,12 +9,13 @@ import { import { GRAPHQL_TYPENAME_KEY } from '@/object-record/constants/GraphqlTypenameKey'; import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; type ComputeOptimisticCacheRecordInputArgs = { @@ -39,16 +40,34 @@ export const computeOptimisticRecordFromInput = ({ (field) => field.name === recordKey, ); - const potentialJoinColumnNameFieldMetadataItem = + const potentialRelationJoinColumnNameFieldMetadataItem = objectMetadataItem.fields.find( (field) => field.type === FieldMetadataType.RELATION && field.settings?.joinColumnName === recordKey, ); + const potentialMorphRelationJoinColumnNameFieldMetadataItem = + objectMetadataItem.fields.find((field) => { + if (!isFieldMorphRelation(field)) return false; + + return field.morphRelations?.some((morphRelation) => { + const computedFieldName = computeMorphRelationFieldName({ + fieldName: field.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); + return computedFieldName === recordKey.replace('Id', ''); + }); + }); + const isUnknownField = !isDefined(correspondingFieldMetadataItem) && - !isDefined(potentialJoinColumnNameFieldMetadataItem); + !isDefined(potentialRelationJoinColumnNameFieldMetadataItem) && + !isDefined(potentialMorphRelationJoinColumnNameFieldMetadataItem); const isTypenameField = recordKey === GRAPHQL_TYPENAME_KEY; return isUnknownField && !isTypenameField; diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index 210fb30a26e..a1de2e936ff 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -1,7 +1,8 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; export const sanitizeRecordInput = ({ @@ -27,10 +28,26 @@ export const sanitizeRecordInput = ({ field.type === FieldMetadataType.RELATION && field.settings?.joinColumnName === fieldName, ); + const potentialMorphRelationJoinColumnNameFieldMetadataItem = + objectMetadataItem.fields.find((field) => { + if (!isFieldMorphRelation(field)) return false; + return field.morphRelations?.some((morphRelation) => { + const computedFieldName = computeMorphRelationFieldName({ + fieldName: field.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); + return computedFieldName === fieldName.replace('Id', ''); + }); + }); if ( !isDefined(fieldMetadataItem) && - !isDefined(potentialJoinColumnNameFieldMetadataItem) + !isDefined(potentialJoinColumnNameFieldMetadataItem) && + !isDefined(potentialMorphRelationJoinColumnNameFieldMetadataItem) ) { return undefined; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx index f461943dee5..52a9667d54f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx @@ -172,7 +172,7 @@ export const WorkflowEditActionDeleteRecord = ({ onChange={(objectRecordId) => handleFieldChange('objectRecordId', objectRecordId) } - objectNameSingular={objectNameSingular} + objectNameSingulars={[objectNameSingular]} defaultValue={formData.objectRecordId} testId="workflow-edit-action-record-delete-object-record-id" disabled={isFormDisabled} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx index f84100212a1..4d1184f8b37 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx @@ -211,7 +211,7 @@ export const WorkflowEditActionUpdateRecord = ({ onChange={(objectRecordId) => handleFieldChange('objectRecordId', objectRecordId) } - objectNameSingular={objectNameSingular} + objectNameSingulars={[objectNameSingular]} defaultValue={formData.objectRecordId} disabled={isFormDisabled} VariablePicker={WorkflowVariablePicker} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx index e22f44cb776..06d852d0ccf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx @@ -116,7 +116,7 @@ export const WorkflowStepFilterValueInput = ({ defaultValue={stepFilter.value} onChange={handleValueChange} VariablePicker={WorkflowVariablePicker} - objectNameSingular={objectMetadataItem.nameSingular} + objectNameSingulars={[objectMetadataItem.nameSingular]} disabled={readonly} /> ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx index 9faa2a3712c..590061e221c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx @@ -137,7 +137,7 @@ export const WorkflowEditActionFormFiller = ({ }, }); }} - objectNameSingular={objectNameSingular} + objectNameSingulars={[objectNameSingular]} disabled={actionOptions.readonly} /> ); diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index ea92b89ebd7..1d81a40b3ce 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -23,6 +23,7 @@ import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-mod import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util'; import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -154,18 +155,30 @@ export class DataloaderService { FieldMetadataType.MORPH_RELATION, ) ) { - const allMorphFlatFieldMetadatas = - findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ + const morphNameFromMorphFieldMetadataName = + getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata: targetFlatFieldMetadata, + nameSingular: sourceFlatObjectMetadata.nameSingular, + namePlural: sourceFlatObjectMetadata.namePlural, + }); + + const allMorphFlatFieldMetadatas = [ + targetFlatFieldMetadata, + ...findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ flatFieldMetadata: targetFlatFieldMetadata, flatFieldMetadataMaps, flatObjectMetadata: targetFlatObjectMetadata, - }).sort((a, b) => (a.id > b.id ? 1 : -1)); + }), + ].sort((a, b) => (a.id > b.id ? 1 : -1)); relationDtos.push( fromMorphOrRelationFlatFieldMetadataToRelationDto({ sourceFlatFieldMetadata, sourceFlatObjectMetadata, - targetFlatFieldMetadata: allMorphFlatFieldMetadatas[0], + targetFlatFieldMetadata: { + ...allMorphFlatFieldMetadatas[0], + name: morphNameFromMorphFieldMetadataName, + }, targetFlatObjectMetadata, }), ); @@ -256,8 +269,18 @@ export class DataloaderService { flatEntityMaps: flatObjectMetadataMaps, }); + const morphNameFromMorphFieldMetadataName = + getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata: sourceFlatFieldMetadata, + nameSingular: targetFlatObjectMetadata.nameSingular, + namePlural: targetFlatObjectMetadata.namePlural, + }); + return fromMorphOrRelationFlatFieldMetadataToRelationDto({ - sourceFlatFieldMetadata, + sourceFlatFieldMetadata: { + ...sourceFlatFieldMetadata, + name: morphNameFromMorphFieldMetadataName, + }, targetFlatFieldMetadata, targetFlatObjectMetadata, sourceFlatObjectMetadata, @@ -372,7 +395,35 @@ export class DataloaderService { overriddenFieldMetadataEntities, ); - return filteredFieldMetadataEntities.map( + const filteredFieldMetadataEntitiesWithMorphRenamed = + filteredFieldMetadataEntities.map((flatFieldMetadata) => { + if ( + isFlatFieldMetadataOfType( + flatFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + const relationTargetObjectMetadata = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: + flatFieldMetadata.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); + + return { + ...flatFieldMetadata, + name: getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata: flatFieldMetadata, + nameSingular: relationTargetObjectMetadata.nameSingular, + namePlural: relationTargetObjectMetadata.namePlural, + }), + }; + } + + return flatFieldMetadata; + }); + + return filteredFieldMetadataEntitiesWithMorphRenamed.map( fromFlatFieldMetadataToFieldMetadataDto, ); }, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts index d38a8701445..45701c2d02f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import omit from 'lodash.omit'; import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { type Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -16,7 +16,6 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -75,7 +74,8 @@ export class FieldMetadataMorphRelationService { const currentMorphRelationFieldName = computeMorphRelationFieldName({ fieldName: fieldMetadataForCreate.name, relationType: relationCreationPayload.type, - targetObjectMetadata, + targetObjectMetadataNameSingular: targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: targetObjectMetadata.namePlural, }); const relationFieldMetadataForCreate = this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation( diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts deleted file mode 100644 index 160cb0edcf0..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { capitalize } from 'twenty-shared/utils'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; - -type ComputeMorphRelationFieldNameArgs = { - fieldName: string; - relationType: RelationType; - targetObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'namePlural' - >; -}; - -export const computeMorphRelationFieldName = ({ - fieldName, - relationType, - targetObjectMetadata, -}: ComputeMorphRelationFieldNameArgs): string => { - if (relationType === RelationType.MANY_TO_ONE) { - return `${fieldName}${capitalize(targetObjectMetadata.nameSingular)}`; - } - - return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts index 94688357368..c7dc619d07d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -1,13 +1,12 @@ import { t } from '@lingui/core/macro'; import { type FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; @@ -71,7 +70,8 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ const currentMorphRelationFieldName = computeMorphRelationFieldName({ fieldName: createFieldInput.name, relationType: relationCreationPayload.type, - targetObjectMetadata: targetFlatObjectMetadata, + targetObjectMetadataNameSingular: targetFlatObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural, }); const sourceFlatObjectMetadataJoinColumnName = computeMorphOrRelationFieldJoinColumnName({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/get-morph-name-from-morph-field-metadata-name.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/get-morph-name-from-morph-field-metadata-name.util.spec.ts new file mode 100644 index 00000000000..e9b632f1205 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/__tests__/get-morph-name-from-morph-field-metadata-name.util.spec.ts @@ -0,0 +1,81 @@ +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util'; + +describe('getMorphNameFromMorphFieldMetadataName', () => { + const createMorphRelationFlatFieldMetadataMock = ( + name: string, + relationType: RelationType, + ) => + ({ + name, + type: FieldMetadataType.MORPH_RELATION, + label: name, + settings: { relationType }, + }) as unknown as FlatFieldMetadata; + + it('should extract base morph field name for MANY_TO_ONE (removes singular suffix)', () => { + const morphRelationFlatFieldMetadata = + createMorphRelationFlatFieldMetadataMock( + 'contactDeal', + RelationType.MANY_TO_ONE, + ); + + const result = getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata, + nameSingular: 'Deal', + namePlural: 'Deals', + }); + + expect(result).toBe('contact'); + }); + + it('should also work extract base morph field name for MANY_TO_ONE if casing is not correct', () => { + const morphRelationFlatFieldMetadata = + createMorphRelationFlatFieldMetadataMock( + 'contactDeal', + RelationType.MANY_TO_ONE, + ); + + const result = getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata, + nameSingular: 'deal', + namePlural: 'deals', + }); + + expect(result).toBe('contact'); + }); + + it('should extract base morph field name for ONE_TO_MANY (removes plural suffix)', () => { + const morphRelationFlatFieldMetadata = + createMorphRelationFlatFieldMetadataMock( + 'contactDeals', + RelationType.ONE_TO_MANY, + ); + + const result = getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata, + nameSingular: 'Deal', + namePlural: 'Deals', + }); + + expect(result).toBe('contact'); + }); + + it('should return the original name when suffix does not match', () => { + const morphRelationFlatFieldMetadata = + createMorphRelationFlatFieldMetadataMock( + 'randomName', + RelationType.MANY_TO_ONE, + ); + + const result = getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata, + nameSingular: 'account', + namePlural: 'accounts', + }); + + expect(result).toBe('randomName'); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util.ts new file mode 100644 index 00000000000..8b2a8dce7c2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util.ts @@ -0,0 +1,30 @@ +import { type FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { searchAndReplaceLast } from 'src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util'; + +export const getMorphNameFromMorphFieldMetadataName = ({ + morphRelationFlatFieldMetadata, + nameSingular, + namePlural, +}: { + morphRelationFlatFieldMetadata: Pick< + FlatFieldMetadata, + 'name' | 'settings' + >; + nameSingular: string; + namePlural: string; +}): string => { + const isManyToOneRelationType = + morphRelationFlatFieldMetadata.settings.relationType === + RelationType.MANY_TO_ONE; + + return searchAndReplaceLast({ + source: morphRelationFlatFieldMetadata.name, + replace: '', + search: isManyToOneRelationType + ? capitalize(nameSingular) + : capitalize(namePlural), + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts index 3a44029ab6a..8f8b3b52dcd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts @@ -3,34 +3,15 @@ import { type FieldMetadataType, type FromTo, } from 'twenty-shared/types'; +import { computeMorphRelationFieldName } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util'; - -const searchAndReplaceLast = ({ - replace, - search, - source, -}: { - source: string; - search: string; - replace: string; -}) => { - const lastIndex = source.lastIndexOf(search); - - if (lastIndex === -1) return source; - - return ( - source.slice(0, lastIndex) + - replace + - source.slice(lastIndex + search.length) - ); -}; +import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util'; type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo< FlatObjectMetadata, @@ -59,17 +40,18 @@ export const renameRelatedMorphFieldOnObjectNamesUpdate = ({ const isManyToOneRelationType = morphRelationFlatFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE; - const initialMorphRelationFieldName = searchAndReplaceLast({ - source: morphRelationFlatFieldMetadata.name, - replace: '', - search: isManyToOneRelationType - ? fromFlatObjectMetadata.nameSingular - : fromFlatObjectMetadata.namePlural, - }); + const initialMorphRelationFieldName = + getMorphNameFromMorphFieldMetadataName({ + morphRelationFlatFieldMetadata, + nameSingular: fromFlatObjectMetadata.nameSingular, + namePlural: fromFlatObjectMetadata.namePlural, + }); + const newMorphFieldName = computeMorphRelationFieldName({ fieldName: initialMorphRelationFieldName, relationType: morphRelationFlatFieldMetadata.settings.relationType, - targetObjectMetadata: toFlatObjectMetadata, + targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: toFlatObjectMetadata.namePlural, }); const newJoinColumnName = isManyToOneRelationType ? computeMorphOrRelationFieldJoinColumnName({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util.ts new file mode 100644 index 00000000000..7b6b123073d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util.ts @@ -0,0 +1,19 @@ +export const searchAndReplaceLast = ({ + replace, + search, + source, +}: { + source: string; + search: string; + replace: string; +}) => { + const lastIndex = source.lastIndexOf(search); + + if (lastIndex === -1) return source; + + return ( + source.slice(0, lastIndex) + + replace + + source.slice(lastIndex + search.length) + ); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/survey-result-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/survey-result-data-seeds.constant.ts index ec7ea6c1223..f084f3b2753 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/survey-result-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/survey-result-data-seeds.constant.ts @@ -21,7 +21,7 @@ export const SURVEY_RESULT_DATA_SEED_COLUMNS: (keyof SurveyResultDataSeed)[] = [ ]; export const SURVEY_RESULT_DATA_SEED_IDS = { - ID_1: '20202020-0f2a-49d8-8aa2-ec8786153a0b', + ID_1: '20202020-3d5f-4e2a-9c1b-7f8e2d3c4b5a', ID_2: '20202020-4e83-41ec-93e2-fd70ff09f68c', ID_3: '20202020-e716-4dd5-ac61-3315bc559e2d', }; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap index d2e7d522210..64d2b58dffb 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap @@ -73,12 +73,11 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE [ { "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE", - "name": "UserInputError", + "message": "Invalid relation type (wrong) for field owner on firstTargetRelation", }, ] `; @@ -87,12 +86,11 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE [ { "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE", - "name": "UserInputError", + "message": "Invalid relation type (undefined) for field owner on firstTargetRelation", }, ] `; @@ -170,12 +168,11 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY [ { "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE", - "name": "UserInputError", + "message": "Invalid relation type (wrong) for field owner on firstTargetRelation", }, ] `; @@ -184,12 +181,11 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY [ { "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE", - "name": "UserInputError", + "message": "Invalid relation type (undefined) for field owner on firstTargetRelation", }, ] `; diff --git a/packages/twenty-shared/eslint.config.mjs b/packages/twenty-shared/eslint.config.mjs index 1ac9dfca4fd..5bc29c579e6 100644 --- a/packages/twenty-shared/eslint.config.mjs +++ b/packages/twenty-shared/eslint.config.mjs @@ -26,6 +26,9 @@ export default [ { ignores: [ '**/node_modules/**', + 'packages/twenty-shared/**/dist/**', + 'packages/twenty-shared/vite.config.ts', + 'packages/twenty-shared/scripts/**', ], }, @@ -50,6 +53,7 @@ export default [ 'no-duplicate-imports': 'error', 'no-undef': 'off', 'no-unused-vars': 'off', + 'prettier/prettier': 'error', // Nx rules '@nx/enforce-module-boundaries': [ diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json index 931647642bf..cd4aad853c7 100644 --- a/packages/twenty-shared/project.json +++ b/packages/twenty-shared/project.json @@ -51,12 +51,7 @@ "typecheck": {}, "test": {}, "lint": { - "options": { - "lintFilePatterns": [ - "{projectRoot}/src/**/*.{ts,tsx,json}", - "{projectRoot}/package.json" - ] - }, + "options": {}, "configurations": { "fix": {} } diff --git a/packages/twenty-shared/src/constants/CompositeFieldTypeSubFieldsNames.ts b/packages/twenty-shared/src/constants/CompositeFieldTypeSubFieldsNames.ts index 07919c62977..7ba8907b018 100644 --- a/packages/twenty-shared/src/constants/CompositeFieldTypeSubFieldsNames.ts +++ b/packages/twenty-shared/src/constants/CompositeFieldTypeSubFieldsNames.ts @@ -2,48 +2,46 @@ import { FieldMetadataType } from '@/types'; export const COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES = { [FieldMetadataType.CURRENCY]: { - 'amountMicros': 'amountMicros', - 'currencyCode': 'currencyCode', + amountMicros: 'amountMicros', + currencyCode: 'currencyCode', }, [FieldMetadataType.EMAILS]: { - 'primaryEmail': 'primaryEmail', - 'additionalEmails': 'additionalEmails', + primaryEmail: 'primaryEmail', + additionalEmails: 'additionalEmails', }, [FieldMetadataType.LINKS]: { - 'primaryLinkUrl': 'primaryLinkUrl', - 'primaryLinkLabel': 'primaryLinkLabel', - 'secondaryLinks': 'secondaryLinks', + primaryLinkUrl: 'primaryLinkUrl', + primaryLinkLabel: 'primaryLinkLabel', + secondaryLinks: 'secondaryLinks', }, [FieldMetadataType.PHONES]: { - 'primaryPhoneCallingCode': 'primaryPhoneCallingCode', - 'primaryPhoneCountryCode': 'primaryPhoneCountryCode', - 'primaryPhoneNumber': 'primaryPhoneNumber', - 'additionalPhones': 'additionalPhones', + primaryPhoneCallingCode: 'primaryPhoneCallingCode', + primaryPhoneCountryCode: 'primaryPhoneCountryCode', + primaryPhoneNumber: 'primaryPhoneNumber', + additionalPhones: 'additionalPhones', }, [FieldMetadataType.FULL_NAME]: { - 'firstName': 'firstName', - 'lastName': 'lastName', + firstName: 'firstName', + lastName: 'lastName', }, [FieldMetadataType.ADDRESS]: { - 'addressStreet1': 'addressStreet1', - 'addressStreet2': 'addressStreet2', - 'addressCity': 'addressCity', - 'addressState': 'addressState', - 'addressCountry': 'addressCountry', - 'addressPostcode': 'addressPostcode', - 'addressLat': 'addressLat', - 'addressLng': 'addressLng', + addressStreet1: 'addressStreet1', + addressStreet2: 'addressStreet2', + addressCity: 'addressCity', + addressState: 'addressState', + addressCountry: 'addressCountry', + addressPostcode: 'addressPostcode', + addressLat: 'addressLat', + addressLng: 'addressLng', }, [FieldMetadataType.ACTOR]: { - 'source': 'source', - 'name': 'name', - 'workspaceMemberId': 'workspaceMemberId', - 'context': 'context', + source: 'source', + name: 'name', + workspaceMemberId: 'workspaceMemberId', + context: 'context', }, [FieldMetadataType.RICH_TEXT_V2]: { - 'blocknote': 'blocknote', - 'markdown': 'markdown', + blocknote: 'blocknote', + markdown: 'markdown', }, } as const; - - diff --git a/packages/twenty-shared/src/constants/CurrencyCode.ts b/packages/twenty-shared/src/constants/CurrencyCode.ts index cfb98224303..48e49d4102b 100644 --- a/packages/twenty-shared/src/constants/CurrencyCode.ts +++ b/packages/twenty-shared/src/constants/CurrencyCode.ts @@ -1,156 +1,155 @@ export enum CurrencyCode { - AED = 'AED', - AFN = 'AFN', - ALL = 'ALL', - AMD = 'AMD', - ANG = 'ANG', - AOA = 'AOA', - ARS = 'ARS', - AUD = 'AUD', - AWG = 'AWG', - AZN = 'AZN', - BAM = 'BAM', - BBD = 'BBD', - BDT = 'BDT', - BGN = 'BGN', - BHD = 'BHD', - BIF = 'BIF', - BMD = 'BMD', - BND = 'BND', - BOB = 'BOB', - BRL = 'BRL', - BSD = 'BSD', - BTN = 'BTN', - BWP = 'BWP', - BYN = 'BYN', - BZD = 'BZD', - CAD = 'CAD', - CDF = 'CDF', - CHF = 'CHF', - CLP = 'CLP', - CNY = 'CNY', - COP = 'COP', - CRC = 'CRC', - CUP = 'CUP', - CVE = 'CVE', - CZK = 'CZK', - DJF = 'DJF', - DKK = 'DKK', - DOP = 'DOP', - DZD = 'DZD', - EGP = 'EGP', - ERN = 'ERN', - ETB = 'ETB', - EUR = 'EUR', - FJD = 'FJD', - FKP = 'FKP', - GBP = 'GBP', - GEL = 'GEL', - GHS = 'GHS', - GIP = 'GIP', - GMD = 'GMD', - GNF = 'GNF', - GTQ = 'GTQ', - GYD = 'GYD', - HKD = 'HKD', - HNL = 'HNL', - HTG = 'HTG', - HUF = 'HUF', - IDR = 'IDR', - ILS = 'ILS', - INR = 'INR', - IQD = 'IQD', - IRR = 'IRR', - ISK = 'ISK', - JMD = 'JMD', - JOD = 'JOD', - JPY = 'JPY', - KES = 'KES', - KGS = 'KGS', - KHR = 'KHR', - KMF = 'KMF', - KPW = 'KPW', - KRW = 'KRW', - KWD = 'KWD', - KYD = 'KYD', - KZT = 'KZT', - LAK = 'LAK', - LBP = 'LBP', - LKR = 'LKR', - LRD = 'LRD', - LSL = 'LSL', - LYD = 'LYD', - MAD = 'MAD', - MDL = 'MDL', - MGA = 'MGA', - MKD = 'MKD', - MMK = 'MMK', - MNT = 'MNT', - MOP = 'MOP', - MRU = 'MRU', - MUR = 'MUR', - MVR = 'MVR', - MWK = 'MWK', - MXN = 'MXN', - MYR = 'MYR', - MZN = 'MZN', - NAD = 'NAD', - NGN = 'NGN', - NIO = 'NIO', - NOK = 'NOK', - NPR = 'NPR', - NZD = 'NZD', - OMR = 'OMR', - PAB = 'PAB', - PEN = 'PEN', - PGK = 'PGK', - PHP = 'PHP', - PKR = 'PKR', - PLN = 'PLN', - PYG = 'PYG', - QAR = 'QAR', - RON = 'RON', - RSD = 'RSD', - RUB = 'RUB', - RWF = 'RWF', - SAR = 'SAR', - SBD = 'SBD', - SCR = 'SCR', - SDG = 'SDG', - SEK = 'SEK', - SGD = 'SGD', - SHP = 'SHP', - SLE = 'SLE', - SOS = 'SOS', - SRD = 'SRD', - SSP = 'SSP', - STN = 'STN', - SVC = 'SVC', - SYP = 'SYP', - SZL = 'SZL', - THB = 'THB', - TJS = 'TJS', - TMT = 'TMT', - TND = 'TND', - TOP = 'TOP', - TRY = 'TRY', - TTD = 'TTD', - TWD = 'TWD', - TZS = 'TZS', - UAH = 'UAH', - UGX = 'UGX', - USD = 'USD', - UYU = 'UYU', - UZS = 'UZS', - VES = 'VES', - VND = 'VND', - VUV = 'VUV', - WST = 'WST', - XCD = 'XCD', - XOF = 'XOF', - YER = 'YER', - ZAR = 'ZAR', - ZMW = 'ZMW', - ZWG = 'ZWG', - } - \ No newline at end of file + AED = 'AED', + AFN = 'AFN', + ALL = 'ALL', + AMD = 'AMD', + ANG = 'ANG', + AOA = 'AOA', + ARS = 'ARS', + AUD = 'AUD', + AWG = 'AWG', + AZN = 'AZN', + BAM = 'BAM', + BBD = 'BBD', + BDT = 'BDT', + BGN = 'BGN', + BHD = 'BHD', + BIF = 'BIF', + BMD = 'BMD', + BND = 'BND', + BOB = 'BOB', + BRL = 'BRL', + BSD = 'BSD', + BTN = 'BTN', + BWP = 'BWP', + BYN = 'BYN', + BZD = 'BZD', + CAD = 'CAD', + CDF = 'CDF', + CHF = 'CHF', + CLP = 'CLP', + CNY = 'CNY', + COP = 'COP', + CRC = 'CRC', + CUP = 'CUP', + CVE = 'CVE', + CZK = 'CZK', + DJF = 'DJF', + DKK = 'DKK', + DOP = 'DOP', + DZD = 'DZD', + EGP = 'EGP', + ERN = 'ERN', + ETB = 'ETB', + EUR = 'EUR', + FJD = 'FJD', + FKP = 'FKP', + GBP = 'GBP', + GEL = 'GEL', + GHS = 'GHS', + GIP = 'GIP', + GMD = 'GMD', + GNF = 'GNF', + GTQ = 'GTQ', + GYD = 'GYD', + HKD = 'HKD', + HNL = 'HNL', + HTG = 'HTG', + HUF = 'HUF', + IDR = 'IDR', + ILS = 'ILS', + INR = 'INR', + IQD = 'IQD', + IRR = 'IRR', + ISK = 'ISK', + JMD = 'JMD', + JOD = 'JOD', + JPY = 'JPY', + KES = 'KES', + KGS = 'KGS', + KHR = 'KHR', + KMF = 'KMF', + KPW = 'KPW', + KRW = 'KRW', + KWD = 'KWD', + KYD = 'KYD', + KZT = 'KZT', + LAK = 'LAK', + LBP = 'LBP', + LKR = 'LKR', + LRD = 'LRD', + LSL = 'LSL', + LYD = 'LYD', + MAD = 'MAD', + MDL = 'MDL', + MGA = 'MGA', + MKD = 'MKD', + MMK = 'MMK', + MNT = 'MNT', + MOP = 'MOP', + MRU = 'MRU', + MUR = 'MUR', + MVR = 'MVR', + MWK = 'MWK', + MXN = 'MXN', + MYR = 'MYR', + MZN = 'MZN', + NAD = 'NAD', + NGN = 'NGN', + NIO = 'NIO', + NOK = 'NOK', + NPR = 'NPR', + NZD = 'NZD', + OMR = 'OMR', + PAB = 'PAB', + PEN = 'PEN', + PGK = 'PGK', + PHP = 'PHP', + PKR = 'PKR', + PLN = 'PLN', + PYG = 'PYG', + QAR = 'QAR', + RON = 'RON', + RSD = 'RSD', + RUB = 'RUB', + RWF = 'RWF', + SAR = 'SAR', + SBD = 'SBD', + SCR = 'SCR', + SDG = 'SDG', + SEK = 'SEK', + SGD = 'SGD', + SHP = 'SHP', + SLE = 'SLE', + SOS = 'SOS', + SRD = 'SRD', + SSP = 'SSP', + STN = 'STN', + SVC = 'SVC', + SYP = 'SYP', + SZL = 'SZL', + THB = 'THB', + TJS = 'TJS', + TMT = 'TMT', + TND = 'TND', + TOP = 'TOP', + TRY = 'TRY', + TTD = 'TTD', + TWD = 'TWD', + TZS = 'TZS', + UAH = 'UAH', + UGX = 'UGX', + USD = 'USD', + UYU = 'UYU', + UZS = 'UZS', + VES = 'VES', + VND = 'VND', + VUV = 'VUV', + WST = 'WST', + XCD = 'XCD', + XOF = 'XOF', + YER = 'YER', + ZAR = 'ZAR', + ZMW = 'ZMW', + ZWG = 'ZWG', +} diff --git a/packages/twenty-shared/src/constants/CurrencyCodeLabels.ts b/packages/twenty-shared/src/constants/CurrencyCodeLabels.ts index 4ddd49a1762..e4558defbc9 100644 --- a/packages/twenty-shared/src/constants/CurrencyCodeLabels.ts +++ b/packages/twenty-shared/src/constants/CurrencyCodeLabels.ts @@ -1,9 +1,6 @@ -import { type CurrencyCode } from "@/constants/CurrencyCode"; +import { type CurrencyCode } from '@/constants/CurrencyCode'; -export const CURRENCY_CODE_LABELS: Record< - CurrencyCode, - { label: string } -> = { +export const CURRENCY_CODE_LABELS: Record = { AED: { label: 'UAE dirham' }, AFN: { label: 'Afghan afghani' }, ALL: { label: 'Albanian lek' }, @@ -157,4 +154,4 @@ export const CURRENCY_CODE_LABELS: Record< ZAR: { label: 'South African rand' }, ZMW: { label: 'Zambian kwacha' }, ZWG: { label: 'Zimbabwe Gold' }, -}; \ No newline at end of file +}; diff --git a/packages/twenty-shared/src/constants/QueryDefaultLimitRecords.ts b/packages/twenty-shared/src/constants/QueryDefaultLimitRecords.ts index daa25163781..ce16fbfb297 100644 --- a/packages/twenty-shared/src/constants/QueryDefaultLimitRecords.ts +++ b/packages/twenty-shared/src/constants/QueryDefaultLimitRecords.ts @@ -1 +1 @@ -export const QUERY_DEFAULT_LIMIT_RECORDS = 60; \ No newline at end of file +export const QUERY_DEFAULT_LIMIT_RECORDS = 60; diff --git a/packages/twenty-shared/src/index.ts b/packages/twenty-shared/src/index.ts index ef44d4c3415..4b2fef6e10a 100644 --- a/packages/twenty-shared/src/index.ts +++ b/packages/twenty-shared/src/index.ts @@ -8,4 +8,3 @@ */ export default {}; - diff --git a/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts index d0164ad8a9b..6903862fc28 100644 --- a/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts +++ b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts @@ -51,7 +51,9 @@ type BasicTests = [ Expect>, Expect>, - Expect>, + Expect< + HasAllProperties<{ a?: string; b: number }, { a?: string; b: number }> + >, Expect>, diff --git a/packages/twenty-shared/src/types/CompositeFieldSubFieldNameType.ts b/packages/twenty-shared/src/types/CompositeFieldSubFieldNameType.ts index 5c14bb4800b..b44dc18088d 100644 --- a/packages/twenty-shared/src/types/CompositeFieldSubFieldNameType.ts +++ b/packages/twenty-shared/src/types/CompositeFieldSubFieldNameType.ts @@ -1,6 +1,7 @@ import { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from '@/constants/CompositeFieldTypeSubFieldsNames'; -const _allSubFieldValues = Object.values(COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES) - .flatMap(obj => Object.values(obj)); +const _allSubFieldValues = Object.values( + COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES, +).flatMap((obj) => Object.values(obj)); -export type CompositeFieldSubFieldName = typeof _allSubFieldValues[number]; \ No newline at end of file +export type CompositeFieldSubFieldName = (typeof _allSubFieldValues)[number]; diff --git a/packages/twenty-shared/src/types/FieldRatingValue.ts b/packages/twenty-shared/src/types/FieldRatingValue.ts index 504bac09e6f..6e32f10367f 100644 --- a/packages/twenty-shared/src/types/FieldRatingValue.ts +++ b/packages/twenty-shared/src/types/FieldRatingValue.ts @@ -1,3 +1,3 @@ -import { type RATING_VALUES } from "@/constants/RatingValues"; +import { type RATING_VALUES } from '@/constants/RatingValues'; -export type FieldRatingValue = (typeof RATING_VALUES)[number] | null; \ No newline at end of file +export type FieldRatingValue = (typeof RATING_VALUES)[number] | null; diff --git a/packages/twenty-shared/src/types/Nullable.ts b/packages/twenty-shared/src/types/Nullable.ts index 7b5ecba9366..164a7056d97 100644 --- a/packages/twenty-shared/src/types/Nullable.ts +++ b/packages/twenty-shared/src/types/Nullable.ts @@ -1 +1 @@ -export type Nullable = T | null | undefined; \ No newline at end of file +export type Nullable = T | null | undefined; diff --git a/packages/twenty-shared/src/types/PartialFieldMetadataItem.ts b/packages/twenty-shared/src/types/PartialFieldMetadataItem.ts index 4708a530b19..b32d966ad6c 100644 --- a/packages/twenty-shared/src/types/PartialFieldMetadataItem.ts +++ b/packages/twenty-shared/src/types/PartialFieldMetadataItem.ts @@ -1,10 +1,10 @@ -import { type FieldMetadataType } from "@/types/FieldMetadataType"; -import { type PartialFieldMetadataItemOption } from "@/types/PartialFieldMetadataOption"; +import { type FieldMetadataType } from '@/types/FieldMetadataType'; +import { type PartialFieldMetadataItemOption } from '@/types/PartialFieldMetadataOption'; export type PartialFieldMetadataItem = { - id: string; - name: string; - type: FieldMetadataType; - label: string; - options?: PartialFieldMetadataItemOption[] | null; - } \ No newline at end of file + id: string; + name: string; + type: FieldMetadataType; + label: string; + options?: PartialFieldMetadataItemOption[] | null; +}; diff --git a/packages/twenty-shared/src/types/PartialFieldMetadataOption.ts b/packages/twenty-shared/src/types/PartialFieldMetadataOption.ts index f20481dcccd..601f5704ebd 100644 --- a/packages/twenty-shared/src/types/PartialFieldMetadataOption.ts +++ b/packages/twenty-shared/src/types/PartialFieldMetadataOption.ts @@ -1,7 +1,6 @@ - export type PartialFieldMetadataItemOption = { - id: string; - label: string; - position: number; - value: string; - }; \ No newline at end of file + id: string; + label: string; + position: number; + value: string; +}; diff --git a/packages/twenty-shared/src/types/RelationCreationPayload.ts b/packages/twenty-shared/src/types/RelationCreationPayload.ts index 3006656dc29..fbd271dd79f 100644 --- a/packages/twenty-shared/src/types/RelationCreationPayload.ts +++ b/packages/twenty-shared/src/types/RelationCreationPayload.ts @@ -1,8 +1,8 @@ -import { type RelationType } from "@/types/RelationType"; +import { type RelationType } from '@/types/RelationType'; export type RelationCreationPayload = { type: RelationType; targetObjectMetadataId: string; targetFieldLabel: string; targetFieldIcon: string; -}; \ No newline at end of file +}; diff --git a/packages/twenty-shared/src/types/RelationType.ts b/packages/twenty-shared/src/types/RelationType.ts index d433ce26a92..05a4adb8c60 100644 --- a/packages/twenty-shared/src/types/RelationType.ts +++ b/packages/twenty-shared/src/types/RelationType.ts @@ -1,4 +1,4 @@ export enum RelationType { MANY_TO_ONE = 'MANY_TO_ONE', - ONE_TO_MANY = 'ONE_TO_MANY' + ONE_TO_MANY = 'ONE_TO_MANY', } diff --git a/packages/twenty-shared/src/types/ViewFilterOperandDeprecated.ts b/packages/twenty-shared/src/types/ViewFilterOperandDeprecated.ts index a7585309316..813f41acace 100644 --- a/packages/twenty-shared/src/types/ViewFilterOperandDeprecated.ts +++ b/packages/twenty-shared/src/types/ViewFilterOperandDeprecated.ts @@ -1,19 +1,18 @@ export enum ViewFilterOperandDeprecated { - Is = 'is', - IsNotNull = 'isNotNull', - IsNot = 'isNot', - LessThanOrEqual = 'lessThan', - GreaterThanOrEqual = 'greaterThan', - IsBefore = 'isBefore', - IsAfter = 'isAfter', - Contains = 'contains', - DoesNotContain = 'doesNotContain', - IsEmpty = 'isEmpty', - IsNotEmpty = 'isNotEmpty', - IsRelative = 'isRelative', - IsInPast = 'isInPast', - IsInFuture = 'isInFuture', - IsToday = 'isToday', - VectorSearch = 'vectorSearch', + Is = 'is', + IsNotNull = 'isNotNull', + IsNot = 'isNot', + LessThanOrEqual = 'lessThan', + GreaterThanOrEqual = 'greaterThan', + IsBefore = 'isBefore', + IsAfter = 'isAfter', + Contains = 'contains', + DoesNotContain = 'doesNotContain', + IsEmpty = 'isEmpty', + IsNotEmpty = 'isNotEmpty', + IsRelative = 'isRelative', + IsInPast = 'isInPast', + IsInFuture = 'isInFuture', + IsToday = 'isToday', + VectorSearch = 'vectorSearch', } - \ No newline at end of file diff --git a/packages/twenty-shared/src/utils/__tests__/applyDiff.test.ts b/packages/twenty-shared/src/utils/__tests__/applyDiff.test.ts index 87909362159..642ea060cbc 100644 --- a/packages/twenty-shared/src/utils/__tests__/applyDiff.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/applyDiff.test.ts @@ -6,19 +6,19 @@ describe('applyDiff', () => { describe('input validation', () => { it('should throw error for non-array diffs', () => { const obj = { test: 'value' }; - + expect(() => applyDiff(obj, null as any)).toThrow( - 'Diffs must be an array' + 'Diffs must be an array', ); expect(() => applyDiff(obj, 'invalid' as any)).toThrow( - 'Diffs must be an array' + 'Diffs must be an array', ); }); it('should handle empty diffs array', () => { const obj = { test: 'value' }; const result = applyDiff(obj, []); - + expect(result).toEqual({ test: 'value' }); expect(result).not.toBe(obj); // Should return a copy }); @@ -30,7 +30,7 @@ describe('applyDiff', () => { { type: 'CREATE', path: [], value: 'test' } as any, { type: 'CREATE', path: ['test'], value: 'updated' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ test: 'updated' }); }); @@ -42,7 +42,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'CREATE', path: ['newProp'], value: 'newValue' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ existing: 'value', @@ -55,7 +55,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'CREATE', path: ['level1', 'newProp'], value: 'newValue' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ level1: { @@ -70,7 +70,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'CREATE', path: [1], value: 'newElement' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['existing', 'newElement']); }); @@ -80,7 +80,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ level1: { @@ -98,7 +98,7 @@ describe('applyDiff', () => { { type: 'CREATE', path: ['level1', 'level2'], value: {} }, { type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ level1: { @@ -114,9 +114,14 @@ describe('applyDiff', () => { it('should change existing properties', () => { const obj = { prop: 'oldValue' }; const diffs: Difference[] = [ - { type: 'CHANGE', path: ['prop'], oldValue: 'oldValue', value: 'newValue' }, + { + type: 'CHANGE', + path: ['prop'], + oldValue: 'oldValue', + value: 'newValue', + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ prop: 'newValue' }); }); @@ -124,9 +129,14 @@ describe('applyDiff', () => { it('should change nested properties', () => { const obj = { level1: { prop: 'oldValue' } }; const diffs: Difference[] = [ - { type: 'CHANGE', path: ['level1', 'prop'], oldValue: 'oldValue', value: 'newValue' }, + { + type: 'CHANGE', + path: ['level1', 'prop'], + oldValue: 'oldValue', + value: 'newValue', + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ level1: { prop: 'newValue' }, @@ -138,7 +148,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'CHANGE', path: [0], oldValue: 'old', value: 'new' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['new', 'values']); }); @@ -151,9 +161,14 @@ describe('applyDiff', () => { ], }; const diffs: Difference[] = [ - { type: 'CHANGE', path: ['users', 0, 'name'], oldValue: 'John', value: 'Johnny' }, + { + type: 'CHANGE', + path: ['users', 0, 'name'], + oldValue: 'John', + value: 'Johnny', + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ users: [ @@ -170,7 +185,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ keep: 'value' }); }); @@ -185,7 +200,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'REMOVE', path: ['level1', 'remove'], oldValue: 'toDelete' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ level1: { keep: 'value' }, @@ -197,7 +212,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'REMOVE', path: [1], oldValue: 'remove' }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['keep1', 'keep2']); }); @@ -208,7 +223,7 @@ describe('applyDiff', () => { { type: 'REMOVE', path: [1], oldValue: 'b' }, // Remove 'b' { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['a', 'c', 'e']); }); @@ -220,7 +235,7 @@ describe('applyDiff', () => { { type: 'REMOVE', path: [2], oldValue: 'c' }, // Remove 'c' { type: 'REMOVE', path: [4], oldValue: 'e' }, // Remove 'e' ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['b', 'd', 'f']); }); @@ -235,7 +250,7 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'REMOVE', path: ['items', 0, 'tags', 1], oldValue: 'tag2' }, // Remove 'tag2' ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ items: [ @@ -250,9 +265,9 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'REMOVE', path: ['invalid'], oldValue: 'invalid' }, ]; - + expect(() => applyDiff(obj, diffs)).toThrow( - 'Expected numeric index for array removal, got string' + 'Expected numeric index for array removal, got string', ); }); }); @@ -267,15 +282,20 @@ describe('applyDiff', () => { array: ['a', 'b', 'c'], }, }; - + const diffs: Difference[] = [ { type: 'CREATE', path: ['newProp'], value: 'newValue' }, - { type: 'CHANGE', path: ['change'], oldValue: 'oldValue', value: 'newValue' }, + { + type: 'CHANGE', + path: ['change'], + oldValue: 'oldValue', + value: 'newValue', + }, { type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' }, { type: 'REMOVE', path: ['nested', 'array', 1], oldValue: 'b' }, // Remove 'b' { type: 'CREATE', path: ['nested', 'newArray'], value: [1, 2, 3] }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ keep: 'value', @@ -296,7 +316,7 @@ describe('applyDiff', () => { { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual(['A', 'c']); }); @@ -308,20 +328,25 @@ describe('applyDiff', () => { const diffs: Difference[] = [ { type: 'INVALID' as any, path: ['test'], value: 'newValue' }, ]; - + expect(() => applyDiff(obj, diffs)).toThrow( - 'Unsupported diff type: INVALID' + 'Unsupported diff type: INVALID', ); }); it('should throw error with path information for invalid operations', () => { const obj = { test: 'value' }; const diffs: Difference[] = [ - { type: 'CHANGE', path: ['nonExistent', 'deep', 'path'], oldValue: 'value', value: 'value' }, + { + type: 'CHANGE', + path: ['nonExistent', 'deep', 'path'], + oldValue: 'value', + value: 'value', + }, ]; - + expect(() => applyDiff(obj, diffs)).toThrow( - 'Failed to apply diff at path nonExistent.deep.path' + 'Failed to apply diff at path nonExistent.deep.path', ); }); }); @@ -335,7 +360,7 @@ describe('applyDiff', () => { ]; expect(() => applyDiff(obj, diffs)).toThrow( - "Refusing to set forbidden property key '__proto__' on object (prototype pollution protection)" + "Refusing to set forbidden property key '__proto__' on object (prototype pollution protection)", ); }); @@ -344,20 +369,20 @@ describe('applyDiff', () => { const unicodeBypassAttempts = [ // __proto__ with Unicode escapes - '__\u0070roto__', // \u0070 = 'p' - '__\u{70}roto__', // ES6 syntax - '__pr\u006fto__', // \u006f = 'o' + '__\u0070roto__', // \u0070 = 'p' + '__\u{70}roto__', // ES6 syntax + '__pr\u006fto__', // \u006f = 'o' '__proto\u005f\u005f', // \u005f = '_' - // constructor with Unicode escapes + // constructor with Unicode escapes 'construc\u0074or', // \u0074 = 't' 'constr\u0075ctor', // \u0075 = 'u' '\u0063onstructor', // \u0063 = 'c' // prototype with Unicode escapes - 'proto\u0074ype', // \u0074 = 't' - 'prototy\u0070e', // \u0070 = 'p' - '\u0070rototype', // \u0070 = 'p' + 'proto\u0074ype', // \u0074 = 't' + 'prototy\u0070e', // \u0070 = 'p' + '\u0070rototype', // \u0070 = 'p' ]; unicodeBypassAttempts.forEach((maliciousKey) => { @@ -366,7 +391,9 @@ describe('applyDiff', () => { ]; expect(() => applyDiff(obj, diffs)).toThrow( - new RegExp(`Refusing to set forbidden property key.*prototype pollution protection`) + new RegExp( + `Refusing to set forbidden property key.*prototype pollution protection`, + ), ); }); }); @@ -375,11 +402,16 @@ describe('applyDiff', () => { const obj = { safe: 'value' }; const diffs: Difference[] = [ - { type: 'CHANGE', path: ['constructor'], oldValue: 'old', value: 'malicious' }, + { + type: 'CHANGE', + path: ['constructor'], + oldValue: 'old', + value: 'malicious', + }, ]; expect(() => applyDiff(obj, diffs)).toThrow( - "Refusing to set forbidden property key 'constructor' on object (prototype pollution protection)" + "Refusing to set forbidden property key 'constructor' on object (prototype pollution protection)", ); }); @@ -414,36 +446,51 @@ describe('applyDiff', () => { it('should not modify the original object', () => { const obj = { prop: 'value', nested: { deep: 'value' } }; const originalObj = JSON.parse(JSON.stringify(obj)); - + const diffs: Difference[] = [ - { type: 'CHANGE', path: ['prop'], oldValue: 'value', value: 'newValue' }, - { type: 'CHANGE', path: ['nested', 'deep'], oldValue: 'value', value: 'newDeepValue' }, + { + type: 'CHANGE', + path: ['prop'], + oldValue: 'value', + value: 'newValue', + }, + { + type: 'CHANGE', + path: ['nested', 'deep'], + oldValue: 'value', + value: 'newDeepValue', + }, ]; - + applyDiff(obj, diffs); - + expect(obj).toEqual(originalObj); }); it('should not modify the original array', () => { const obj = ['a', 'b', 'c']; const originalObj = [...obj]; - + const diffs: Difference[] = [ { type: 'REMOVE', path: [1], oldValue: 'b' }, ]; - + applyDiff(obj, diffs); - + expect(obj).toEqual(originalObj); }); it('should handle frozen objects', () => { const obj = Object.freeze({ prop: 'value' }); const diffs: Difference[] = [ - { type: 'CHANGE', path: ['prop'], oldValue: 'value', value: 'newValue' }, + { + type: 'CHANGE', + path: ['prop'], + oldValue: 'value', + value: 'newValue', + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ prop: 'newValue' }); expect(obj.prop).toBe('value'); // Original unchanged @@ -470,22 +517,36 @@ describe('applyDiff', () => { }, ], }; - + const diffs: Difference[] = [ // Update trigger settings - { type: 'CHANGE', path: ['trigger', 'settings', 'table'], oldValue: 'users', value: 'contacts' }, + { + type: 'CHANGE', + path: ['trigger', 'settings', 'table'], + oldValue: 'users', + value: 'contacts', + }, // Remove first step { type: 'REMOVE', path: ['steps', 0], oldValue: '1' }, // Update remaining step - { type: 'CHANGE', path: ['steps', 1, 'settings', 'to'], oldValue: 'test@example.com', value: 'new@example.com' }, + { + type: 'CHANGE', + path: ['steps', 1, 'settings', 'to'], + oldValue: 'test@example.com', + value: 'new@example.com', + }, // Add new step - { type: 'CREATE', path: ['steps', 2], value: { - id: '3', - type: 'WEBHOOK', - settings: { url: 'https://api.example.com/webhook' }, - }}, + { + type: 'CREATE', + path: ['steps', 2], + value: { + id: '3', + type: 'WEBHOOK', + settings: { url: 'https://api.example.com/webhook' }, + }, + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ trigger: { @@ -517,18 +578,27 @@ describe('applyDiff', () => { { id: 5, name: 'Item 5' }, ], }; - + const diffs: Difference[] = [ // Remove items at indices 1 and 3 (Item 2 and Item 4) { type: 'REMOVE', path: ['items', 1], oldValue: 'Item 2' }, { type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' }, { type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' }, // Update remaining item - { type: 'CHANGE', path: ['items', 0, 'name'], oldValue: 'Item 1', value: 'Updated Item 1' }, + { + type: 'CHANGE', + path: ['items', 0, 'name'], + oldValue: 'Item 1', + value: 'Updated Item 1', + }, // Add new item - { type: 'CREATE', path: ['items', 5], value: { id: 6, name: 'New Item' } }, + { + type: 'CREATE', + path: ['items', 5], + value: { id: 6, name: 'New Item' }, + }, ]; - + const result = applyDiff(obj, diffs); expect(result).toEqual({ items: [ diff --git a/packages/twenty-shared/src/utils/__tests__/findOrThrow.test.ts b/packages/twenty-shared/src/utils/__tests__/findOrThrow.test.ts index 0558fa482f5..e941509cf6a 100644 --- a/packages/twenty-shared/src/utils/__tests__/findOrThrow.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/findOrThrow.test.ts @@ -3,36 +3,40 @@ import { findOrThrow } from '@/utils'; describe('findOrThrow', () => { it('returns the element when predicate matches', () => { const arr = [1, 2, 3, 4]; - const result = findOrThrow(arr, x => x === 3); + const result = findOrThrow(arr, (x) => x === 3); expect(result).toBe(3); }); it('throws default error when no element matches', () => { const arr = [1, 2, 3]; - expect(() => findOrThrow(arr, x => x === 5)).toThrow(new Error('Element not found')); + expect(() => findOrThrow(arr, (x) => x === 5)).toThrow( + new Error('Element not found'), + ); }); it('throws custom error when provided', () => { const arr = ['a', 'b', 'c']; const customError = new Error('Custom error!'); - expect(() => findOrThrow(arr, x => x === 'z', customError)).toThrow(customError); + expect(() => findOrThrow(arr, (x) => x === 'z', customError)).toThrow( + customError, + ); }); it('returns the first matching element when multiple match', () => { const arr = [2, 4, 6, 8]; - const result = findOrThrow(arr, x => x % 2 === 0); + const result = findOrThrow(arr, (x) => x % 2 === 0); expect(result).toBe(2); }); it('works with object arrays', () => { - const arr = [{id: 1}, {id: 2}, {id: 3}]; - const result = findOrThrow(arr, obj => obj.id === 2); - expect(result).toEqual({id: 2}); + const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const result = findOrThrow(arr, (obj) => obj.id === 2); + expect(result).toEqual({ id: 2 }); }); it('works with primitive arrays', () => { const arr = ['apple', 'banana', 'cherry']; - const result = findOrThrow(arr, fruit => fruit.startsWith('b')); + const result = findOrThrow(arr, (fruit) => fruit.startsWith('b')); expect(result).toBe('banana'); }); -}); \ No newline at end of file +}); diff --git a/packages/twenty-shared/src/utils/__tests__/isNonEmptyArray.test.ts b/packages/twenty-shared/src/utils/__tests__/isNonEmptyArray.test.ts index 844ac18cb67..f51b8cd7084 100644 --- a/packages/twenty-shared/src/utils/__tests__/isNonEmptyArray.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/isNonEmptyArray.test.ts @@ -1,4 +1,4 @@ -import { isNonEmptyArray } from "@/utils/array/isNonEmptyArray"; +import { isNonEmptyArray } from '@/utils/array/isNonEmptyArray'; describe('isNonEmptyArray', () => { it('should return true for a non empty array', () => { diff --git a/packages/twenty-shared/src/utils/__tests__/parseJson.test.ts b/packages/twenty-shared/src/utils/__tests__/parseJson.test.ts index ae90469a83f..e5c10dc1733 100644 --- a/packages/twenty-shared/src/utils/__tests__/parseJson.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/parseJson.test.ts @@ -1,4 +1,4 @@ -import { parseJson } from "@/utils/parseJson"; +import { parseJson } from '@/utils/parseJson'; describe('parseJson', () => { it('if value is null', () => { diff --git a/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts b/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts index 39444f4a9e3..b8d67be1bd9 100644 --- a/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts @@ -419,4 +419,4 @@ describe('safeParseRelativeDateFilterValue', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/twenty-shared/src/utils/applyDiff.ts b/packages/twenty-shared/src/utils/applyDiff.ts index 61803138bfa..02f689ba5a0 100644 --- a/packages/twenty-shared/src/utils/applyDiff.ts +++ b/packages/twenty-shared/src/utils/applyDiff.ts @@ -34,7 +34,9 @@ export const applyDiff = (obj: T, diffs: Difference[]): T => { try { applyDiffToPath(mutableObj as MutableData, diff, arrayDeletionQueue); } catch (error) { - throw new Error(`Failed to apply diff at path ${diff.path.join('.')}: ${error}`); + throw new Error( + `Failed to apply diff at path ${diff.path.join('.')}: ${error}`, + ); } } @@ -46,82 +48,103 @@ export const applyDiff = (obj: T, diffs: Difference[]): T => { const applyDiffToPath = ( obj: MutableData, diff: Difference, - arrayDeletionQueue: ArrayDeletionFn[] + arrayDeletionQueue: ArrayDeletionFn[], ) => { const { path, type } = diff; const value = 'value' in diff ? diff.value : undefined; const pathLength = path.length; const lastPathElement = path[pathLength - 1]; - + const parentContainer = navigateToParent(obj, path); - + switch (type) { case 'CREATE': case 'CHANGE': setValueAtPath(parentContainer, lastPathElement, value); break; - + case 'REMOVE': - handleRemoval(obj, path, parentContainer, lastPathElement, arrayDeletionQueue); + handleRemoval( + obj, + path, + parentContainer, + lastPathElement, + arrayDeletionQueue, + ); break; - + default: throw new Error(`Unsupported diff type: ${type}`); } }; -const navigateToParent = (obj: MutableData, path: (string | number)[]): MutableData => { +const navigateToParent = ( + obj: MutableData, + path: (string | number)[], +): MutableData => { let current = obj; - + for (let i = 0; i < path.length - 1; i++) { const pathElement = path[i]; - + if (current === null || current === undefined) { - throw new Error(`Cannot traverse path: found null/undefined at element ${i}`); + throw new Error( + `Cannot traverse path: found null/undefined at element ${i}`, + ); } - + if (isNumber(pathElement) && !Array.isArray(current)) { - throw new Error(`Expected array at path element ${i}, got ${typeof current}`); + throw new Error( + `Expected array at path element ${i}, got ${typeof current}`, + ); } - + if (isString(pathElement) && Array.isArray(current)) { throw new Error(`Expected object at path element ${i}, got array`); } - + if (Array.isArray(current)) { current = current[pathElement as number] as MutableData; } else { current = (current as ObjectType)[pathElement as string] as MutableData; } } - + return current; }; const setValueAtPath = ( container: MutableData, pathElement: string | number, - value: unknown + value: unknown, ): void => { if (Array.isArray(container)) { if (!isNumber(pathElement)) { - throw new Error(`Expected numeric index for array, got ${typeof pathElement}`); - } - - try { - container[pathElement] = value; - } catch (error) { - throw new Error(`Cannot set array element at index ${pathElement}: ${error}. Array may be non-extensible.`); - } - } else if (isObject(container)) { - if (FORBIDDEN_OBJECT_KEYS.includes(pathElement as string)) { - throw new Error(`Refusing to set forbidden property key '${pathElement}' on object (prototype pollution protection)`); + throw new Error( + `Expected numeric index for array, got ${typeof pathElement}`, + ); } try { container[pathElement] = value; } catch (error) { - throw new Error(`Cannot set property '${String(pathElement)}': ${error}. Object may be non-extensible.`); + throw new Error( + `Cannot set array element at index ${pathElement}: ${error}. Array may be non-extensible.`, + ); + } + } else if (isObject(container)) { + if (FORBIDDEN_OBJECT_KEYS.includes(pathElement as string)) { + throw new Error( + `Refusing to set forbidden property key '${pathElement}' on object (prototype pollution protection)`, + ); + } + + try { + container[pathElement] = value; + } catch (error) { + throw new Error( + `Cannot set property '${String(pathElement)}': ${error}. Object may be non-extensible.`, + ); } } else { throw new Error(`Expected object or array, got ${typeof container}`); @@ -133,10 +156,16 @@ const handleRemoval = ( fullPath: (string | number)[], parentContainer: MutableData, lastPathElement: string | number, - arrayDeletionQueue: ArrayDeletionFn[] + arrayDeletionQueue: ArrayDeletionFn[], ): void => { if (Array.isArray(parentContainer)) { - handleArrayRemoval(rootObj, fullPath, parentContainer, lastPathElement, arrayDeletionQueue); + handleArrayRemoval( + rootObj, + fullPath, + parentContainer, + lastPathElement, + arrayDeletionQueue, + ); } else { handleObjectRemoval(parentContainer as ObjectType, lastPathElement); } @@ -148,14 +177,16 @@ const handleArrayRemoval = ( fullPath: (string | number)[], parentArray: ArrayType, index: string | number, - arrayDeletionQueue: ArrayDeletionFn[] + arrayDeletionQueue: ArrayDeletionFn[], ): void => { if (typeof index !== 'number') { - throw new Error(`Expected numeric index for array removal, got ${typeof index}`); + throw new Error( + `Expected numeric index for array removal, got ${typeof index}`, + ); } - + parentArray[index] = REMOVE_SYMBOL; - + arrayDeletionQueue.push(() => { if (fullPath.length === 1) { if (Array.isArray(rootObj)) { @@ -169,7 +200,7 @@ const handleArrayRemoval = ( const handleObjectRemoval = ( parentObject: ObjectType, - key: string | number + key: string | number, ): void => { if (FORBIDDEN_OBJECT_KEYS.includes(key as string)) { return; @@ -180,13 +211,13 @@ const handleObjectRemoval = ( const filterRemoveSymbols = (array: ArrayType): void => { const indicesToRemove: number[] = []; - + for (let i = 0; i < array.length; i++) { if (array[i] === REMOVE_SYMBOL) { indicesToRemove.push(i); } } - + for (let i = indicesToRemove.length - 1; i >= 0; i--) { array.splice(indicesToRemove[i], 1); } diff --git a/packages/twenty-shared/src/utils/array/filterOutByProperty.ts b/packages/twenty-shared/src/utils/array/filterOutByProperty.ts index 387c6d969ac..e000e518ccc 100644 --- a/packages/twenty-shared/src/utils/array/filterOutByProperty.ts +++ b/packages/twenty-shared/src/utils/array/filterOutByProperty.ts @@ -1,5 +1,8 @@ -export const filterOutByProperty = (property: K, valueToExclude: T[K] | null | undefined) => { +export const filterOutByProperty = ( + property: K, + valueToExclude: T[K] | null | undefined, +) => { return (itemToFilter: T) => { - return itemToFilter[property] !== valueToExclude - } -} \ No newline at end of file + return itemToFilter[property] !== valueToExclude; + }; +}; diff --git a/packages/twenty-shared/src/utils/array/findById.ts b/packages/twenty-shared/src/utils/array/findById.ts index 63a51abda39..14d8d546129 100644 --- a/packages/twenty-shared/src/utils/array/findById.ts +++ b/packages/twenty-shared/src/utils/array/findById.ts @@ -1,5 +1,5 @@ export const findById = (idToMatch: string) => { return (itemToFind: T) => { - return itemToFind.id === idToMatch - } -} \ No newline at end of file + return itemToFind.id === idToMatch; + }; +}; diff --git a/packages/twenty-shared/src/utils/array/findByProperty.ts b/packages/twenty-shared/src/utils/array/findByProperty.ts index 6788b704ce1..4cdd4c9af74 100644 --- a/packages/twenty-shared/src/utils/array/findByProperty.ts +++ b/packages/twenty-shared/src/utils/array/findByProperty.ts @@ -1,5 +1,8 @@ -export const findByProperty = (property: K, valueToMatch: T[K] | null | undefined) => { +export const findByProperty = ( + property: K, + valueToMatch: T[K] | null | undefined, +) => { return (itemToFind: T) => { - return itemToFind[property] === valueToMatch - } -} \ No newline at end of file + return itemToFind[property] === valueToMatch; + }; +}; diff --git a/packages/twenty-shared/src/utils/array/findOrThrow.ts b/packages/twenty-shared/src/utils/array/findOrThrow.ts index 58941864f9c..7d2e7b04bd6 100644 --- a/packages/twenty-shared/src/utils/array/findOrThrow.ts +++ b/packages/twenty-shared/src/utils/array/findOrThrow.ts @@ -3,11 +3,11 @@ import { assertIsDefinedOrThrow } from '@/utils'; export const findOrThrow = ( array: T[], predicate: (value: T) => boolean, - error: Error = new Error('Element not found'), + error: Error = new Error('Element not found'), ): T => { const result = array.find(predicate); - assertIsDefinedOrThrow(result, error) + assertIsDefinedOrThrow(result, error); return result; }; diff --git a/packages/twenty-shared/src/utils/array/sumByProperty.ts b/packages/twenty-shared/src/utils/array/sumByProperty.ts index 1bcadd53d07..4140bafab86 100644 --- a/packages/twenty-shared/src/utils/array/sumByProperty.ts +++ b/packages/twenty-shared/src/utils/array/sumByProperty.ts @@ -1,17 +1,17 @@ -import { isNumberOrNaN } from "@sniptt/guards"; +import { isNumberOrNaN } from '@sniptt/guards'; export const sumByProperty = (property: K) => { return (accumulator: number, nextItem: T) => { - if(typeof accumulator !== "number") { + if (typeof accumulator !== 'number') { accumulator = 0; } - if(!isNumberOrNaN(nextItem[property])) { + if (!isNumberOrNaN(nextItem[property])) { return accumulator; } accumulator += nextItem[property]; - return accumulator - } -} \ No newline at end of file + return accumulator; + }; +}; diff --git a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.ts b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.ts new file mode 100644 index 00000000000..59ddf3acb5d --- /dev/null +++ b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.ts @@ -0,0 +1,34 @@ +import { CustomError } from '@/utils/errors'; +import { capitalize } from '@/utils/strings'; + +enum RelationType { + MANY_TO_ONE = 'MANY_TO_ONE', + ONE_TO_MANY = 'ONE_TO_MANY', +} + +type ComputeMorphRelationFieldNameArgs = { + fieldName: string; + relationType: RelationType; + targetObjectMetadataNameSingular: string; + targetObjectMetadataNamePlural: string; +}; + +export const computeMorphRelationFieldName = ({ + fieldName, + relationType, + targetObjectMetadataNameSingular: nameSingular, + targetObjectMetadataNamePlural: namePlural, +}: ComputeMorphRelationFieldNameArgs): string => { + if (relationType === RelationType.MANY_TO_ONE) { + return `${fieldName}${capitalize(nameSingular)}`; + } + + if (relationType === RelationType.ONE_TO_MANY) { + return `${fieldName}${capitalize(namePlural)}`; + } + + throw new CustomError( + `Invalid relation type (${relationType}) for field ${fieldName} on ${nameSingular}`, + 'INVALID_RELATION_TYPE_FOR_COMPUTE_MORPH_RELATION_FIELD_NAME', + ); +}; diff --git a/packages/twenty-shared/src/utils/filter/checkIfShouldComputeEmptinessFilter.ts b/packages/twenty-shared/src/utils/filter/checkIfShouldComputeEmptinessFilter.ts index c20a20ec586..6a84b854372 100644 --- a/packages/twenty-shared/src/utils/filter/checkIfShouldComputeEmptinessFilter.ts +++ b/packages/twenty-shared/src/utils/filter/checkIfShouldComputeEmptinessFilter.ts @@ -1,4 +1,8 @@ -import { type FieldMetadataType, type FilterableAndTSVectorFieldType, type ViewFilterOperand } from '@/types'; +import { + type FieldMetadataType, + type FilterableAndTSVectorFieldType, + type ViewFilterOperand, +} from '@/types'; import { isEmptinessOperand } from '@/utils/filter/isEmptinessOperand'; import { getFilterTypeFromFieldType } from '@/utils/filter/utils/getFilterTypeFromFieldType'; @@ -7,7 +11,7 @@ export const checkIfShouldComputeEmptinessFilter = ({ correspondingFieldMetadataItem, }: { recordFilterOperand: ViewFilterOperand; - correspondingFieldMetadataItem: {type: FieldMetadataType}; + correspondingFieldMetadataItem: { type: FieldMetadataType }; }) => { const isAnEmptinessOperand = isEmptinessOperand(recordFilterOperand); diff --git a/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts b/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts index 20620deb1a3..82baec7cb4b 100644 --- a/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts +++ b/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts @@ -16,7 +16,10 @@ export const computeGqlOperationFilterForEmails = ({ subFieldName, }: { recordFilter: RecordFilter; - correspondingFieldMetadataItem: Pick; + correspondingFieldMetadataItem: Pick< + PartialFieldMetadataItem, + 'name' | 'type' + >; subFieldName: CompositeFieldSubFieldName | null | undefined; }): RecordGqlOperationFilter => { const isSubFieldFilter = isNonEmptyString(subFieldName); diff --git a/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts b/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts index 6ed34afb17d..d082907b759 100644 --- a/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts +++ b/packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts @@ -1,4 +1,9 @@ -import { ViewFilterOperand as RecordFilterOperand, type CompositeFieldSubFieldName, type LinksFilter, type PartialFieldMetadataItem } from '@/types'; +import { + ViewFilterOperand as RecordFilterOperand, + type CompositeFieldSubFieldName, + type LinksFilter, + type PartialFieldMetadataItem, +} from '@/types'; import { CustomError } from '@/utils/errors'; import { type RecordFilter } from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter'; import { isNonEmptyString } from '@sniptt/guards'; @@ -9,7 +14,10 @@ export const computeGqlOperationFilterForLinks = ({ subFieldName, }: { recordFilter: RecordFilter; - correspondingFieldMetadataItem: Pick + correspondingFieldMetadataItem: Pick< + PartialFieldMetadataItem, + 'name' | 'type' + >; subFieldName: CompositeFieldSubFieldName | null | undefined; }) => { const isSubFieldFilter = isNonEmptyString(subFieldName); diff --git a/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForEmails.ts b/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForEmails.ts index c676370bda5..ffcc4730c22 100644 --- a/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForEmails.ts +++ b/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForEmails.ts @@ -13,7 +13,10 @@ export const computeEmptyGqlOperationFilterForEmails = ({ correspondingFieldMetadataItem, }: { recordFilter: RecordFilter; - correspondingFieldMetadataItem: Pick; + correspondingFieldMetadataItem: Pick< + PartialFieldMetadataItem, + 'name' | 'type' + >; }): RecordGqlOperationFilter => { const subFieldName = recordFilter.subFieldName; const isSubFieldFilter = isNonEmptyString(subFieldName); @@ -53,7 +56,7 @@ export const computeEmptyGqlOperationFilterForEmails = ({ }; } default: { - throw new CustomError( + throw new CustomError( `Unknown subfield name ${subFieldName}`, 'UNKNOWN_SUBFIELD_NAME', ); diff --git a/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForLinks.ts b/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForLinks.ts index ef78023fcf0..f5e5a8c4f47 100644 --- a/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForLinks.ts +++ b/packages/twenty-shared/src/utils/filter/computeEmptyGqlOperationFilterForLinks.ts @@ -69,7 +69,7 @@ export const computeEmptyGqlOperationFilterForLinks = ({ }; } default: { - throw new CustomError( + throw new CustomError( `Unknown subfield name ${subFieldName}`, 'UNKNOWN_SUBFIELD_NAME', ); diff --git a/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts index 6fad5201c8e..30b4ac280ee 100644 --- a/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts @@ -1,4 +1,3 @@ - import { type PartialFieldMetadataItem, type RecordFilterValueDependencies, @@ -46,8 +45,7 @@ export const computeRecordGqlOperationFilter = ({ fields, recordFilterGroups, currentRecordFilterGroupId: outermostFilterGroupId, - } - ); + }); const recordGqlOperationFilters = [ ...regularRecordGqlOperationFilter, diff --git a/packages/twenty-shared/src/utils/filter/index.ts b/packages/twenty-shared/src/utils/filter/index.ts index 2f92c94ab73..4f151ec38d5 100644 --- a/packages/twenty-shared/src/utils/filter/index.ts +++ b/packages/twenty-shared/src/utils/filter/index.ts @@ -19,5 +19,3 @@ export * from './utils/resolveDateViewFilterValue'; export * from './utils/validation-schemas/arrayOfStringsOrVariablesSchema'; export * from './utils/validation-schemas/arrayOfUuidsOrVariablesSchema'; export * from './utils/validation-schemas/jsonRelationFilterValueSchema'; - - diff --git a/packages/twenty-shared/src/utils/filter/isEmptinessOperand.ts b/packages/twenty-shared/src/utils/filter/isEmptinessOperand.ts index 3f0f508a3f0..04e5661ac4e 100644 --- a/packages/twenty-shared/src/utils/filter/isEmptinessOperand.ts +++ b/packages/twenty-shared/src/utils/filter/isEmptinessOperand.ts @@ -1,7 +1,8 @@ import { ViewFilterOperand as RecordFilterOperand } from '@/types'; export const isEmptinessOperand = (operand: RecordFilterOperand): boolean => { - return [RecordFilterOperand.IS_EMPTY, RecordFilterOperand.IS_NOT_EMPTY].includes( - operand, - ); + return [ + RecordFilterOperand.IS_EMPTY, + RecordFilterOperand.IS_NOT_EMPTY, + ].includes(operand); }; diff --git a/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts b/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts index f1e49a60fb6..2d89553267f 100644 --- a/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts @@ -1,7 +1,15 @@ import { CURRENCY_CODE_LABELS } from '@/constants'; import { type CurrencyCode } from '@/constants/CurrencyCode'; -import { FieldMetadataType, ViewFilterOperand, type PartialFieldMetadataItem, type RecordGqlOperationFilter } from '@/types'; -import { filterSelectOptionsOfFieldMetadataItem, type RecordFilter } from '@/utils'; +import { + FieldMetadataType, + ViewFilterOperand, + type PartialFieldMetadataItem, + type RecordGqlOperationFilter, +} from '@/types'; +import { + filterSelectOptionsOfFieldMetadataItem, + type RecordFilter, +} from '@/utils'; import { isNonEmptyArray } from '@/utils/array/isNonEmptyArray'; import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; import { createAnyFieldRecordFilterBaseProperties } from '@/utils/filter/utils/createAnyFieldRecordFilterBaseProperties'; @@ -24,7 +32,6 @@ export const turnAnyFieldFilterIntoRecordGqlFilter = ({ filterValue: string; fields: PartialFieldMetadataItem[]; }) => { - const anyFieldRecordFilters: RecordFilter[] = []; const isFilterValueANumber = z.coerce.number().safeParse(filterValue).success; diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts index 7e4f4e20b04..9f88d1fd91e 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts @@ -1,4 +1,12 @@ -import { type CompositeFieldSubFieldName, type FilterableAndTSVectorFieldType, type PartialFieldMetadataItem, RecordFilterGroupLogicalOperator, type RecordFilterValueDependencies, type RecordGqlOperationFilter, type ViewFilterOperand } from '@/types'; +import { + type CompositeFieldSubFieldName, + type FilterableAndTSVectorFieldType, + type PartialFieldMetadataItem, + RecordFilterGroupLogicalOperator, + type RecordFilterValueDependencies, + type RecordGqlOperationFilter, + type ViewFilterOperand, +} from '@/types'; import { isDefined } from '@/utils'; import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; @@ -17,7 +25,7 @@ export type RecordFilterGroup = { id: string; parentRecordFilterGroupId?: string | null; logicalOperator: RecordFilterGroupLogicalOperator; -} +}; export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ filterValueDependencies, @@ -26,11 +34,11 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ recordFilterGroups, currentRecordFilterGroupId, }: { - filterValueDependencies: RecordFilterValueDependencies, - filters: RecordFilter[], - fields: PartialFieldMetadataItem[], - recordFilterGroups: RecordFilterGroup[], - currentRecordFilterGroupId?: string, + filterValueDependencies: RecordFilterValueDependencies; + filters: RecordFilter[]; + fields: PartialFieldMetadataItem[]; + recordFilterGroups: RecordFilterGroup[]; + currentRecordFilterGroupId?: string; }): RecordGqlOperationFilter | undefined => { const currentRecordFilterGroup = recordFilterGroups.find( (recordFilterGroup) => recordFilterGroup.id === currentRecordFilterGroupId, @@ -45,7 +53,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ ); const groupRecordGqlOperationFilters = recordFiltersInGroup - .map((recordFilter) => + .map((recordFilter) => turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: recordFilter, diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index 7d4e831c228..517cd8db6e0 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -29,7 +29,8 @@ import { convertGreaterThanOrEqualRatingToArrayOfRatingValues, convertLessThanOrEqualRatingToArrayOfRatingValues, convertRatingToRatingValue, - generateILikeFiltersForCompositeFields, getEmptyRecordGqlOperationFilter, + generateILikeFiltersForCompositeFields, + getEmptyRecordGqlOperationFilter, isExpectedSubFieldName, } from '@/utils/filter'; @@ -37,7 +38,14 @@ import { resolveDateViewFilterValue } from '@/utils/filter/utils/resolveDateView import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; -import { checkIfShouldComputeEmptinessFilter, checkIfShouldSkipFiltering, CustomError, getFilterTypeFromFieldType, isDefined, type RecordFilter } from '@/utils'; +import { + checkIfShouldComputeEmptinessFilter, + checkIfShouldSkipFiltering, + CustomError, + getFilterTypeFromFieldType, + isDefined, + type RecordFilter, +} from '@/utils'; import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema'; import { arrayOfUuidOrVariableSchema } from '@/utils/filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema'; import { jsonRelationFilterValueSchema } from '@/utils/filter/utils/validation-schemas/jsonRelationFilterValueSchema'; @@ -47,7 +55,7 @@ type FieldShared = { name: string; type: FieldMetadataType; label: string; -} +}; type TurnRecordFilterIntoRecordGqlOperationFilterParams = { filterValueDependencies?: RecordFilterValueDependencies; @@ -56,7 +64,6 @@ type TurnRecordFilterIntoRecordGqlOperationFilterParams = { recordIdsForUuid?: string[]; }; - export const turnRecordFilterIntoRecordGqlOperationFilter = ({ recordFilter, fieldMetadataItems, @@ -337,7 +344,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ ] : selectedRecordIds; - if (!isDefined (recordIds) || recordIds.length === 0) return; + if (!isDefined(recordIds) || recordIds.length === 0) return; switch (recordFilter.operand) { case RecordFilterOperand.IS: @@ -347,7 +354,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } as RelationFilter, }; case RecordFilterOperand.IS_NOT: { - if (!isDefined (recordIds) || recordIds.length === 0) return; + if (!isDefined(recordIds) || recordIds.length === 0) return; return { or: [ { @@ -1201,7 +1208,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ case 'UUID': { const recordIds = recordIdsForUuid; - if (!isDefined (recordIds) || recordIds.length === 0) return; + if (!isDefined(recordIds) || recordIds.length === 0) return; switch (recordFilter.operand) { case RecordFilterOperand.IS: diff --git a/packages/twenty-shared/src/utils/filter/utils/__tests__/arrayOfStringsOrVariablesSchema.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/arrayOfStringsOrVariablesSchema.test.ts index a7af5ac248e..bb646b3649c 100644 --- a/packages/twenty-shared/src/utils/filter/utils/__tests__/arrayOfStringsOrVariablesSchema.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/arrayOfStringsOrVariablesSchema.test.ts @@ -1,4 +1,4 @@ -import { arrayOfStringsOrVariablesSchema } from "@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema"; +import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema'; describe('arrayOfStringsOrVariablesSchema', () => { describe('Empty value handling', () => { diff --git a/packages/twenty-shared/src/utils/filter/utils/__tests__/generateILikeFiltersForCompositeFields.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/generateILikeFiltersForCompositeFields.test.ts index e4ae7a34418..bc5ae9c2c5a 100644 --- a/packages/twenty-shared/src/utils/filter/utils/__tests__/generateILikeFiltersForCompositeFields.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/generateILikeFiltersForCompositeFields.test.ts @@ -1,4 +1,4 @@ -import { generateILikeFiltersForCompositeFields } from "@/utils/filter/utils/generateILikeFiltersForCompositeFields"; +import { generateILikeFiltersForCompositeFields } from '@/utils/filter/utils/generateILikeFiltersForCompositeFields'; describe('generateILikeFiltersForCompositeFields', () => { it('should format composite filters for simple filter string', () => { diff --git a/packages/twenty-shared/src/utils/filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts index 38d79b63542..f5b22144608 100644 --- a/packages/twenty-shared/src/utils/filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts @@ -1,7 +1,6 @@ -import { FieldMetadataType, type PartialFieldMetadataItem } from "@/types"; -import { turnAnyFieldFilterIntoRecordGqlFilter } from "@/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter"; -import { filterSelectOptionsOfFieldMetadataItem } from "@/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem"; - +import { FieldMetadataType, type PartialFieldMetadataItem } from '@/types'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter'; +import { filterSelectOptionsOfFieldMetadataItem } from '@/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem'; const baseFieldMetadataItem: PartialFieldMetadataItem = { id: 'base-field-metadata-item-id', @@ -303,7 +302,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => { const result = turnAnyFieldFilterIntoRecordGqlFilter({ filterValue, fields: [arrayFieldMetadataItem], - }); expect(result.recordGqlOperationFilter.or).toContainEqual({ @@ -351,7 +349,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => { const result = turnAnyFieldFilterIntoRecordGqlFilter({ filterValue, fields: [phonesFieldMetadataItem], - }); expect(result.recordGqlOperationFilter.or).toContainEqual({ @@ -388,7 +385,7 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => { const result = turnAnyFieldFilterIntoRecordGqlFilter({ filterValue, - fields: [numberFieldMetadataItem], + fields: [numberFieldMetadataItem], }); expect(result.recordGqlOperationFilter.or).toContainEqual({ @@ -417,7 +414,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => { const result = turnAnyFieldFilterIntoRecordGqlFilter({ filterValue, fields: [currencyFieldMetadataItem], - }); expect(result.recordGqlOperationFilter.or).toContainEqual({ diff --git a/packages/twenty-shared/src/utils/filter/utils/convert-view-filter-operand-to-core-operand.util.ts b/packages/twenty-shared/src/utils/filter/utils/convert-view-filter-operand-to-core-operand.util.ts index 9b8746a4082..7fb6b2d9277 100644 --- a/packages/twenty-shared/src/utils/filter/utils/convert-view-filter-operand-to-core-operand.util.ts +++ b/packages/twenty-shared/src/utils/filter/utils/convert-view-filter-operand-to-core-operand.util.ts @@ -4,12 +4,15 @@ const operandMapping: Record = { [ViewFilterOperandDeprecated.Is]: ViewFilterOperand.IS, [ViewFilterOperandDeprecated.IsNotNull]: ViewFilterOperand.IS_NOT_NULL, [ViewFilterOperandDeprecated.IsNot]: ViewFilterOperand.IS_NOT, - [ViewFilterOperandDeprecated.LessThanOrEqual]: ViewFilterOperand.LESS_THAN_OR_EQUAL, - [ViewFilterOperandDeprecated.GreaterThanOrEqual]: ViewFilterOperand.GREATER_THAN_OR_EQUAL, + [ViewFilterOperandDeprecated.LessThanOrEqual]: + ViewFilterOperand.LESS_THAN_OR_EQUAL, + [ViewFilterOperandDeprecated.GreaterThanOrEqual]: + ViewFilterOperand.GREATER_THAN_OR_EQUAL, [ViewFilterOperandDeprecated.IsBefore]: ViewFilterOperand.IS_BEFORE, [ViewFilterOperandDeprecated.IsAfter]: ViewFilterOperand.IS_AFTER, [ViewFilterOperandDeprecated.Contains]: ViewFilterOperand.CONTAINS, - [ViewFilterOperandDeprecated.DoesNotContain]: ViewFilterOperand.DOES_NOT_CONTAIN, + [ViewFilterOperandDeprecated.DoesNotContain]: + ViewFilterOperand.DOES_NOT_CONTAIN, [ViewFilterOperandDeprecated.IsEmpty]: ViewFilterOperand.IS_EMPTY, [ViewFilterOperandDeprecated.IsNotEmpty]: ViewFilterOperand.IS_NOT_EMPTY, [ViewFilterOperandDeprecated.IsRelative]: ViewFilterOperand.IS_RELATIVE, diff --git a/packages/twenty-shared/src/utils/filter/utils/convertViewFilterValueToString.ts b/packages/twenty-shared/src/utils/filter/utils/convertViewFilterValueToString.ts index 7719942f3f6..197274114c1 100644 --- a/packages/twenty-shared/src/utils/filter/utils/convertViewFilterValueToString.ts +++ b/packages/twenty-shared/src/utils/filter/utils/convertViewFilterValueToString.ts @@ -1,5 +1,3 @@ export const convertViewFilterValueToString = (value: any) => { - return typeof value === 'string' - ? value - : JSON.stringify(value ?? ''); -}; \ No newline at end of file + return typeof value === 'string' ? value : JSON.stringify(value ?? ''); +}; diff --git a/packages/twenty-shared/src/utils/filter/utils/createAnyFieldRecordFilterBaseProperties.ts b/packages/twenty-shared/src/utils/filter/utils/createAnyFieldRecordFilterBaseProperties.ts index 41ed70a347d..96f857a728d 100644 --- a/packages/twenty-shared/src/utils/filter/utils/createAnyFieldRecordFilterBaseProperties.ts +++ b/packages/twenty-shared/src/utils/filter/utils/createAnyFieldRecordFilterBaseProperties.ts @@ -8,10 +8,7 @@ export const createAnyFieldRecordFilterBaseProperties = ({ }: { filterValue: string; fieldMetadataItem: PartialFieldMetadataItem; -}): Pick< - RecordFilter, - 'id' | 'value' | 'fieldMetadataId' -> => { +}): Pick => { return { id: v4(), value: filterValue, diff --git a/packages/twenty-shared/src/utils/filter/utils/fieldRatingConvertors.ts b/packages/twenty-shared/src/utils/filter/utils/fieldRatingConvertors.ts index 7900f81714e..3e3978799ac 100644 --- a/packages/twenty-shared/src/utils/filter/utils/fieldRatingConvertors.ts +++ b/packages/twenty-shared/src/utils/filter/utils/fieldRatingConvertors.ts @@ -1,5 +1,5 @@ -import { RATING_VALUES } from "@/constants"; -import { type FieldRatingValue } from "@/types"; +import { RATING_VALUES } from '@/constants'; +import { type FieldRatingValue } from '@/types'; export const convertGreaterThanOrEqualRatingToArrayOfRatingValues = ( greaterThanValue: number, diff --git a/packages/twenty-shared/src/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem.ts b/packages/twenty-shared/src/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem.ts index a7d9f6fc7a8..339d7a5818b 100644 --- a/packages/twenty-shared/src/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem.ts +++ b/packages/twenty-shared/src/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem.ts @@ -1,4 +1,4 @@ -import { type PartialFieldMetadataItem } from "@/types"; +import { type PartialFieldMetadataItem } from '@/types'; export const filterSelectOptionsOfFieldMetadataItem = ({ fieldMetadataItem, diff --git a/packages/twenty-shared/src/utils/filter/utils/getEmptyRecordGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/utils/getEmptyRecordGqlOperationFilter.ts index 6f624b6625f..1288873907c 100644 --- a/packages/twenty-shared/src/utils/filter/utils/getEmptyRecordGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/getEmptyRecordGqlOperationFilter.ts @@ -385,7 +385,7 @@ export const getEmptyRecordGqlOperationFilter = ({ }); break; default: - throw new CustomError( + throw new CustomError( `Unsupported empty filter type ${filterType}`, 'UNSUPPORTED_EMPTY_FILTER_TYPE', ); @@ -399,7 +399,7 @@ export const getEmptyRecordGqlOperationFilter = ({ not: emptyRecordFilter, }; default: - throw new CustomError( + throw new CustomError( `Unknown operand ${operand} for ${filterType} filter`, 'UNKNOWN_OPERAND_FOR_FILTER', ); diff --git a/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts b/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts index e1d9642c005..bd50c50fc54 100644 --- a/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts +++ b/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts @@ -1,5 +1,7 @@ -import { FieldMetadataType, type FilterableAndTSVectorFieldType } from "@/types"; - +import { + FieldMetadataType, + type FilterableAndTSVectorFieldType, +} from '@/types'; export const getFilterTypeFromFieldType = ( fieldType: FieldMetadataType, @@ -46,4 +48,4 @@ export const getFilterTypeFromFieldType = ( default: return 'TEXT'; } -}; \ No newline at end of file +}; diff --git a/packages/twenty-shared/src/utils/filter/utils/isExpectedSubFieldName.ts b/packages/twenty-shared/src/utils/filter/utils/isExpectedSubFieldName.ts index 19c741bce47..3ee8fcab2d4 100644 --- a/packages/twenty-shared/src/utils/filter/utils/isExpectedSubFieldName.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isExpectedSubFieldName.ts @@ -11,14 +11,12 @@ export const isExpectedSubFieldName = < subFieldName: PossibleSubFieldName, subFieldNameToCheck: string | null | undefined, ): subFieldNameToCheck is PossibleSubFieldName => { - const allowedSubFields = Object.values(COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES[ - fieldMetadataType - ]) as readonly string[]; + const allowedSubFields = Object.values( + COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES[fieldMetadataType], + ) as readonly string[]; return ( allowedSubFields.includes(subFieldName as string) && subFieldName === subFieldNameToCheck ); }; - - diff --git a/packages/twenty-shared/src/utils/filter/utils/resolveDateViewFilterValue.ts b/packages/twenty-shared/src/utils/filter/utils/resolveDateViewFilterValue.ts index 481cf9b616d..58c686948ef 100644 --- a/packages/twenty-shared/src/utils/filter/utils/resolveDateViewFilterValue.ts +++ b/packages/twenty-shared/src/utils/filter/utils/resolveDateViewFilterValue.ts @@ -169,7 +169,10 @@ export type ResolvedDateViewFilterValue = ? ReturnType : Date | null; -type PartialViewFilter = {value: string, operand: O} // TODO, was done to avoid ViewFilter export +type PartialViewFilter = { + value: string; + operand: O; +}; // TODO, was done to avoid ViewFilter export export const resolveDateViewFilterValue = ( viewFilter: PartialViewFilter, diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 33b5860f68a..0e9a90d5fa2 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -19,6 +19,7 @@ export { computeDiffBetweenObjects } from './compute-diff-between-objects'; export { deepMerge } from './deepMerge'; export { CustomError } from './errors/CustomError'; export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields'; +export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name'; export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind'; export { checkIfShouldComputeEmptinessFilter } from './filter/checkIfShouldComputeEmptinessFilter'; export { checkIfShouldSkipFiltering } from './filter/checkIfShouldSkipFiltering'; diff --git a/packages/twenty-shared/src/utils/parseJson.ts b/packages/twenty-shared/src/utils/parseJson.ts index 9afc7d7e472..b5fed6d3411 100644 --- a/packages/twenty-shared/src/utils/parseJson.ts +++ b/packages/twenty-shared/src/utils/parseJson.ts @@ -1,6 +1,8 @@ -import { isDefined } from "@/utils/validation"; +import { isDefined } from '@/utils/validation'; -export const parseJson = (rawJson: string | boolean | null | number | undefined): T | null => { +export const parseJson = ( + rawJson: string | boolean | null | number | undefined, +): T | null => { try { if (!isDefined(rawJson)) { return null; @@ -11,8 +13,8 @@ export const parseJson = (rawJson: string | boolean | null | number | undefin } // This is a hack to handle the case where the value is a scalar value which is part of JSON spec but not implemented before ES2019 - return JSON.parse("[" + rawJson + "]")[0]; + return JSON.parse('[' + rawJson + ']')[0]; } catch { - return null; + return null; } }; diff --git a/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts b/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts index 6c1a8994ec7..f14b9f75cc1 100644 --- a/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts +++ b/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts @@ -5,18 +5,33 @@ import { } from '@/types/RelativeDateValue'; import { z } from 'zod'; -const RelativeDateValueSchema = z.object({ - direction: z.enum(['NEXT', 'THIS', 'PAST'] as const) as z.ZodType, - unit: z.enum(['DAY', 'WEEK', 'MONTH', 'YEAR'] as const) as z.ZodType, - amount: z.number().positive().optional(), -}).refine((data) => { - if (data.direction === 'NEXT' || data.direction === 'PAST') { - return data.amount !== undefined && data.amount > 0; - } - return true; -}, { - error: 'Amount is required for NEXT and PAST directions and must be positive' -}); +const RelativeDateValueSchema = z + .object({ + direction: z.enum([ + 'NEXT', + 'THIS', + 'PAST', + ] as const) as z.ZodType, + unit: z.enum([ + 'DAY', + 'WEEK', + 'MONTH', + 'YEAR', + ] as const) as z.ZodType, + amount: z.number().positive().optional(), + }) + .refine( + (data) => { + if (data.direction === 'NEXT' || data.direction === 'PAST') { + return data.amount !== undefined && data.amount > 0; + } + return true; + }, + { + error: + 'Amount is required for NEXT and PAST directions and must be positive', + }, + ); export const safeParseRelativeDateFilterValue = ( value: string, @@ -34,4 +49,4 @@ export const safeParseRelativeDateFilterValue = ( } catch { return undefined; } -} +}; diff --git a/packages/twenty-shared/src/utils/strings/index.ts b/packages/twenty-shared/src/utils/strings/index.ts index 399e9198a2c..89859f65b12 100644 --- a/packages/twenty-shared/src/utils/strings/index.ts +++ b/packages/twenty-shared/src/utils/strings/index.ts @@ -1,2 +1 @@ export * from './capitalize'; - diff --git a/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts b/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts index 83cc301846e..a236f745111 100644 --- a/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts +++ b/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts @@ -16,8 +16,10 @@ export const TIPTAP_NODE_TYPES = { IMAGE: 'image', } as const; -export type TipTapMarkType = typeof TIPTAP_MARK_TYPES[keyof typeof TIPTAP_MARK_TYPES]; -export type TipTapNodeType = typeof TIPTAP_NODE_TYPES[keyof typeof TIPTAP_NODE_TYPES]; +export type TipTapMarkType = + (typeof TIPTAP_MARK_TYPES)[keyof typeof TIPTAP_MARK_TYPES]; +export type TipTapNodeType = + (typeof TIPTAP_NODE_TYPES)[keyof typeof TIPTAP_NODE_TYPES]; // Order for mark rendering (inner to outer) export const TIPTAP_MARKS_RENDER_ORDER: readonly TipTapMarkType[] = [ diff --git a/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts b/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts index a4bf0a98e47..94fcef47c8a 100644 --- a/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts +++ b/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts @@ -1,8 +1,10 @@ export const throwIfNotDefined = ( value: T, - variableName: string + variableName: string, ): asserts value is NonNullable => { if (value === null || value === undefined) { - throw new Error(`Value must be defined for variable ${variableName}, this should not happen`); + throw new Error( + `Value must be defined for variable ${variableName}, this should not happen`, + ); } -} +}; diff --git a/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts b/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts index 54f6259f47f..2e55781855d 100644 --- a/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts +++ b/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts @@ -16,7 +16,7 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => { // if the hostname is a number, it's not a valid url // if we let URL() parse it, it will throw cast an IP address and we lose the information ctx.addIssue({ - code: "custom", + code: 'custom', message: 'domain is not a valid url', }); @@ -28,14 +28,14 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => { return absoluteUrl; } ctx.addIssue({ - code: "custom", + code: 'custom', message: 'domain is not a valid url', }); return z.NEVER; } catch { ctx.addIssue({ - code: "custom", + code: 'custom', message: 'domain is not a valid url', }); diff --git a/packages/twenty-shared/src/utils/validation/__tests__/assertIsDefinedOrThrow.test.ts b/packages/twenty-shared/src/utils/validation/__tests__/assertIsDefinedOrThrow.test.ts index ac1b9a51d90..625bca036f1 100644 --- a/packages/twenty-shared/src/utils/validation/__tests__/assertIsDefinedOrThrow.test.ts +++ b/packages/twenty-shared/src/utils/validation/__tests__/assertIsDefinedOrThrow.test.ts @@ -16,7 +16,9 @@ describe('assertIsDefinedOrThrow', () => { }); it('throws the default error when value is undefined', () => { - expect(() => assertIsDefinedOrThrow(undefined)).toThrow('Value not defined'); + expect(() => assertIsDefinedOrThrow(undefined)).toThrow( + 'Value not defined', + ); }); it('throws the default error when value is null', () => { @@ -38,4 +40,4 @@ describe('assertIsDefinedOrThrow', () => { // After the assertion, TypeScript should treat `maybe` as `string`. expectString(maybe); }); -}); \ No newline at end of file +}); diff --git a/packages/twenty-shared/src/utils/validation/assertIsDefinedOrThrow.ts b/packages/twenty-shared/src/utils/validation/assertIsDefinedOrThrow.ts index 0cc086caff0..2a366fccc00 100644 --- a/packages/twenty-shared/src/utils/validation/assertIsDefinedOrThrow.ts +++ b/packages/twenty-shared/src/utils/validation/assertIsDefinedOrThrow.ts @@ -4,9 +4,7 @@ import { isDefined } from '@/utils'; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function assertIsDefinedOrThrow( value: T | undefined | null, - exceptionToThrow: Error = new Error( - 'Value not defined', - ), + exceptionToThrow: Error = new Error('Value not defined'), ): asserts value is T { if (!isDefined(value)) throw exceptionToThrow; } diff --git a/packages/twenty-shared/src/workflow/constants/TriggerStepId.ts b/packages/twenty-shared/src/workflow/constants/TriggerStepId.ts index 9da1339acde..2325548eb18 100644 --- a/packages/twenty-shared/src/workflow/constants/TriggerStepId.ts +++ b/packages/twenty-shared/src/workflow/constants/TriggerStepId.ts @@ -1 +1 @@ -export const TRIGGER_STEP_ID = 'trigger'; \ No newline at end of file +export const TRIGGER_STEP_ID = 'trigger'; diff --git a/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts index 43f5aa4c3b8..4073e4e8bbf 100644 --- a/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts @@ -4,7 +4,9 @@ export const baseTriggerSchema = z.object({ name: z .string() .optional() - .describe('Human-readable name for the trigger. Optional but recommended for clarity.'), + .describe( + 'Human-readable name for the trigger. Optional but recommended for clarity.', + ), type: z .enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK']) .describe( @@ -14,10 +16,14 @@ export const baseTriggerSchema = z.object({ .object({ x: z.number(), y: z.number() }) .optional() .nullable() - .describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'), + .describe( + 'Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.', + ), nextStepIds: z .array(z.string()) .optional() .nullable() - .describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'), + .describe( + 'Array of step IDs that the trigger connects to. These are the first steps in the workflow.', + ), }); diff --git a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts index adf0dd16799..1edc32c1be7 100644 --- a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts @@ -3,18 +3,26 @@ import { z } from 'zod'; export const baseWorkflowActionSchema = z.object({ id: z .string() - .describe('Unique identifier for the workflow step. Must be unique within the workflow.'), + .describe( + 'Unique identifier for the workflow step. Must be unique within the workflow.', + ), name: z .string() - .describe('Human-readable name for the workflow step. Should clearly describe what the step does.'), + .describe( + 'Human-readable name for the workflow step. Should clearly describe what the step does.', + ), valid: z .boolean() - .describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'), + .describe( + 'Whether the step configuration is valid. Set to true when all required fields are properly configured.', + ), nextStepIds: z .array(z.string()) .optional() .nullable() - .describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'), + .describe( + 'Array of step IDs that this step connects to. Leave empty or null for the final step.', + ), position: z .object({ x: z.number(), y: z.number() }) .optional() diff --git a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts index 02ddba3a53e..8be2e85b536 100644 --- a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts @@ -16,7 +16,9 @@ export const baseWorkflowActionSettingsSchema = z.object({ value: z.boolean().describe('Whether to retry the action if it fails.'), }), continueOnFailure: z.object({ - value: z.boolean().describe('Whether to continue to the next step if this action fails.'), + value: z + .boolean() + .describe('Whether to continue to the next step if this action fails.'), }), }), }); diff --git a/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts index f6ed390f269..491481178df 100644 --- a/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts @@ -2,7 +2,9 @@ import { z } from 'zod'; import { baseWorkflowActionSchema } from './base-workflow-action-schema'; import { workflowCreateRecordActionSettingsSchema } from './create-record-action-settings-schema'; -export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('CREATE_RECORD'), - settings: workflowCreateRecordActionSettingsSchema, -}); +export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('CREATE_RECORD'), + settings: workflowCreateRecordActionSettingsSchema, + }, +); diff --git a/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts index 8208dcfa212..074991fd5ca 100644 --- a/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts @@ -10,9 +10,6 @@ export const workflowCreateRecordActionSettingsSchema = .describe( 'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").', ), - objectRecord: objectRecordSchema - .describe( - 'The record data to create.', - ) + objectRecord: objectRecordSchema.describe('The record data to create.'), }), }); diff --git a/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts index 2a87aae5639..5c02781e314 100644 --- a/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts @@ -2,7 +2,9 @@ import { z } from 'zod'; import { baseWorkflowActionSchema } from './base-workflow-action-schema'; import { workflowDeleteRecordActionSettingsSchema } from './delete-record-action-settings-schema'; -export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('DELETE_RECORD'), - settings: workflowDeleteRecordActionSettingsSchema, -}); +export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('DELETE_RECORD'), + settings: workflowDeleteRecordActionSettingsSchema, + }, +); diff --git a/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts index 4c7da8b800d..25d04ee3fcd 100644 --- a/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts @@ -20,7 +20,9 @@ export const workflowFilterActionSettingsSchema = id: z.string(), type: z.string(), stepOutputKey: z.string(), - operand: z.enum(ViewFilterOperand).or(z.enum(ViewFilterOperandDeprecated)), + operand: z + .enum(ViewFilterOperand) + .or(z.enum(ViewFilterOperandDeprecated)), value: z.string(), stepFilterGroupId: z.string(), positionInStepFilterGroup: z.number().optional(), diff --git a/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts index 8349d6e5418..e2bbc81787b 100644 --- a/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { baseTriggerSchema } from './base-trigger-schema'; - export const workflowManualTriggerSchema = baseTriggerSchema +export const workflowManualTriggerSchema = baseTriggerSchema .extend({ type: z.literal('MANUAL'), settings: z.object({ @@ -34,4 +34,4 @@ import { baseTriggerSchema } from './base-trigger-schema'; }) .describe( 'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.', - ); \ No newline at end of file + ); diff --git a/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts b/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts index f5e91668702..604ce2dec16 100644 --- a/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts @@ -4,7 +4,7 @@ export const objectRecordSchema = z .record(z.string(), z.any()) .describe( 'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' + - '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + - '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + - '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', + '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + + '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + + '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', ); diff --git a/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts index 228f000d943..8582f564b87 100644 --- a/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts @@ -2,7 +2,9 @@ import { z } from 'zod'; import { baseWorkflowActionSchema } from './base-workflow-action-schema'; import { workflowUpdateRecordActionSettingsSchema } from './update-record-action-settings-schema'; -export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('UPDATE_RECORD'), - settings: workflowUpdateRecordActionSettingsSchema, -}); +export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('UPDATE_RECORD'), + settings: workflowUpdateRecordActionSettingsSchema, + }, +); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts index 913ffa5acfb..beaa6622e36 100644 --- a/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts @@ -6,12 +6,14 @@ export const workflowRunStateStepInfoSchema = z.object({ error: z.string().optional(), status: workflowRunStepStatusSchema, get history() { - return z.array( - workflowRunStateStepInfoSchema.pick({ - result: true, - status: true, - error: true, - }) - ).optional(); - } + return z + .array( + workflowRunStateStepInfoSchema.pick({ + result: true, + status: true, + error: true, + }), + ) + .optional(); + }, }); diff --git a/packages/twenty-shared/src/workflow/types/WorkflowRunStateStepInfos.ts b/packages/twenty-shared/src/workflow/types/WorkflowRunStateStepInfos.ts index e0e07178a09..444cac78a3a 100644 --- a/packages/twenty-shared/src/workflow/types/WorkflowRunStateStepInfos.ts +++ b/packages/twenty-shared/src/workflow/types/WorkflowRunStateStepInfos.ts @@ -1,6 +1,6 @@ -import { type workflowRunStateStepInfoSchema } from "@/workflow/schemas/workflow-run-state-step-info-schema"; -import { type workflowRunStateStepInfosSchema } from "@/workflow/schemas/workflow-run-state-step-infos-schema"; -import type z from "zod"; +import { type workflowRunStateStepInfoSchema } from '@/workflow/schemas/workflow-run-state-step-info-schema'; +import { type workflowRunStateStepInfosSchema } from '@/workflow/schemas/workflow-run-state-step-infos-schema'; +import type z from 'zod'; export enum StepStatus { NOT_STARTED = 'NOT_STARTED', @@ -11,6 +11,10 @@ export enum StepStatus { PENDING = 'PENDING', } -export type WorkflowRunStepInfo = z.infer +export type WorkflowRunStepInfo = z.infer< + typeof workflowRunStateStepInfoSchema +>; -export type WorkflowRunStepInfos = z.infer; +export type WorkflowRunStepInfos = z.infer< + typeof workflowRunStateStepInfosSchema +>; diff --git a/packages/twenty-shared/src/workflow/utils/canObjectBeManagedByWorkflow.ts b/packages/twenty-shared/src/workflow/utils/canObjectBeManagedByWorkflow.ts index 89c48fa663e..1506683a314 100644 --- a/packages/twenty-shared/src/workflow/utils/canObjectBeManagedByWorkflow.ts +++ b/packages/twenty-shared/src/workflow/utils/canObjectBeManagedByWorkflow.ts @@ -12,7 +12,8 @@ export const canObjectBeManagedByWorkflow = ({ 'dashboard', ]; - return !excludedNonSystemObjectMetadataItemNames.includes( - nameSingular, - ) && !isSystem; + return ( + !excludedNonSystemObjectMetadataItemNames.includes(nameSingular) && + !isSystem + ); };