From 02673a82afb82342f59c5fc240ca69e2558655b8 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 1 Apr 2024 13:12:37 +0200 Subject: [PATCH] Feat/put target object identifier on use activities (#4682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When writing to the normalized cache (record), it's crucial to use _refs for relationships to avoid many problems. Essentially, we only deal with level 0 and generate all fields to be comfortable with their defaults. When writing in queries (which should be very rare, the only cases are prefetch and the case of activities due to the nested query; I've reduced this to a single file for activities usePrepareFindManyActivitiesQuery 🙂), it's important to use queryFields to avoid bugs. I've implemented them on the side of query generation and record generation. When doing an updateOne / createOne, etc., it's necessary to distinguish between optimistic writing (which we actually want to do with _refs) and the server response without refs. This allows for a clean write in the optimistic cache without worrying about nesting (as the first point). To simplify the whole activities part, write to the normalized cache first. Then, base queries on it in an idempotent manner. This way, there's no need to worry about the current page or action. The normalized cache is up-to-date, so I update the queries. Same idea as for optimisticEffects, actually. Finally, I've triggered optimisticEffects rather than the manual update of many queries. --------- Co-authored-by: Lucas Bordeau --- packages/twenty-front/.gitignore | 3 +- packages/twenty-front/jest.config.ts | 6 +- packages/twenty-front/nyc.config.cjs | 4 +- .../src/generated-metadata/graphql.ts | 2 +- .../__stories__/Calendar.stories.tsx | 6 + .../components/ActivityBodyEditor.tsx | 38 +- .../components/ActivityEditorEffect.tsx | 11 +- .../components/ActivityEditorFields.tsx | 22 +- .../activities/components/ActivityTitle.tsx | 20 +- .../hooks/useRightDrawerEmailThread.ts | 1 - .../activities/events/hooks/useEvents.tsx | 2 +- .../activities/files/hooks/useAttachments.tsx | 6 +- .../files/hooks/useUploadAttachmentFile.tsx | 2 +- .../hooks/__tests__/useActivities.test.tsx | 4 - .../hooks/__tests__/useActivityById.test.tsx | 80 -- .../useActivityConnectionUtils.test.tsx | 111 --- .../useActivityTargetObjectRecords.test.tsx | 293 +++---- ...useAttachRelationInBothDirections.test.tsx | 76 -- .../useDeleteActivityFromCache.test.tsx | 53 -- .../useInjectIntoActivitiesQueries.test.tsx | 60 -- ...eInjectIntoActivityTargetsQueries.test.tsx | 64 -- ...ifyActivityOnActivityTargetsCache.test.tsx | 44 - ...ifyActivityTargetsOnActivityCache.test.tsx | 43 - ...teActivityDrawerForSelectedRowIds.test.tsx | 110 --- .../useRemoveFromActivitiesQueries.test.tsx | 63 -- ...eRemoveFromActivityTargetsQueries.test.tsx | 72 -- .../__tests__/useUpsertActivity.test.tsx | 187 ----- .../modules/activities/hooks/useActivities.ts | 44 +- .../activities/hooks/useActivityById.ts | 26 - .../hooks/useActivityConnectionUtils.ts | 112 --- .../hooks/useActivityTargetObjectRecords.ts | 61 +- .../useActivityTargetsForTargetableObject.ts | 6 +- .../useActivityTargetsForTargetableObjects.ts | 17 +- .../useAttachRelationInBothDirections.ts | 91 -- .../hooks/useCreateActivityInCache.ts | 140 +++- .../activities/hooks/useCreateActivityInDB.ts | 30 +- .../hooks/useDeleteActivityFromCache.ts | 39 - .../hooks/useInjectIntoActivitiesQueries.ts | 175 ---- .../useInjectIntoActivityTargetsQueries.ts | 82 -- .../useModifyActivityOnActivityTargetCache.ts | 46 - ...useModifyActivityTargetsOnActivityCache.ts | 51 -- .../hooks/useOpenCreateActivityDrawer.ts | 2 +- ...enCreateActivityDrawerForSelectedRowIds.ts | 64 -- .../usePrepareFindManyActivitiesQuery.ts | 123 +++ ...efreshShowPageFindManyActivitiesQueries.ts | 49 ++ .../hooks/useRemoveFromActivitiesQueries.ts | 117 --- .../useRemoveFromActivityTargetsQueries.ts | 73 -- .../activities/hooks/useUpsertActivity.ts | 125 +-- .../ActivityTargetInlineCellEditMode.tsx | 60 +- .../components/ActivityTargetsInlineCell.tsx | 5 +- ...tIntoActivityTargetInlineCellCache.test.ts | 52 -- ...InjectIntoActivityTargetInlineCellCache.ts | 44 - .../query-keys/CreateOneActivityQueryKey.ts | 34 + .../query-keys/FindManyActivitiesQueryKey.ts | 38 + .../FindManyActivityTargetsQueryKey.ts | 21 + .../components/ActivityActionBar.tsx | 158 +--- .../CurrentUserDueTaskCountEffect.tsx | 3 +- .../activities/tasks/components/TaskRow.tsx | 4 +- ...jectIntoTimelineActivitiesQueries.test.tsx | 54 -- .../useInjectIntoTimelineActivitiesQueries.ts | 32 - .../timeline/hooks/useTimelineActivities.ts | 17 +- .../activities/types/ActivityTargetObject.ts | 1 - .../types/ActivityTargetableEntity.ts | 1 - .../getTargetableEntitiesWithParents.test.ts | 48 -- ...ObjectsAndTheirRelatedTargetableObjects.ts | 23 - .../generateActivityTargetMorphFieldKeys.ts | 31 + ... => getActivityTargetObjectFieldIdName.ts} | 0 .../utils/getActivityTargetObjectFieldName.ts | 7 + .../utils/getActivityTargetsFilter.ts | 2 +- ...ityTargetsToCreateFromTargetableObjects.ts | 51 +- .../triggerAttachRelationOptimisticEffect.ts | 8 +- .../triggerCreateRecordsOptimisticEffect.ts | 10 +- .../triggerDeleteRecordsOptimisticEffect.ts | 4 +- .../triggerDetachRelationOptimisticEffect.ts | 4 +- .../triggerUpdateRecordOptimisticEffect.ts | 10 +- .../triggerUpdateRelationsOptimisticEffect.ts | 2 +- .../object-metadata/graphql/queries.ts | 21 + .../ApolloMetadataClientProvider.tsx | 13 +- .../__tests__/useObjectMetadataItem.test.tsx | 2 - .../hooks/useObjectMetadataItem.ts | 15 +- .../types/FieldMetadataItem.ts | 21 +- .../mapFieldMetadataToGraphQLQuery.test.tsx | 27 +- .../mapObjectMetadataToGraphQLQuery.test.tsx | 64 +- .../__tests__/shouldFieldBeQueried.test.ts | 28 +- .../utils/getFieldRelationDirections.ts | 38 + .../utils/mapFieldMetadataToGraphQLQuery.ts | 27 +- .../utils/mapObjectMetadataToGraphQLQuery.ts | 43 +- .../utils/shouldFieldBeQueried.ts | 14 +- .../cache/hooks/useAddRecordInCache.ts | 64 -- .../useAppendToFindManyRecordsQueryInCache.ts | 50 -- .../hooks/useCreateManyRecordsInCache.ts | 42 + .../cache/hooks/useCreateOneRecordInCache.ts | 62 ++ .../cache/hooks/useDeleteRecordFromCache.ts | 29 + ...eGenerateObjectRecordOptimisticResponse.ts | 79 -- .../cache/hooks/useGetRecordFromCache.ts | 34 +- .../useInjectIntoFindOneRecordQueryCache.ts | 44 - .../cache/hooks/useModifyRecordFromCache.ts | 32 - .../useReadFindManyRecordsQueryInCache.ts | 6 + .../useUpsertFindManyRecordsQueryInCache.ts | 16 +- .../cache/utils/deleteRecordFromCache.ts | 30 + .../utils/getCacheReferenceFromRecord.ts | 31 - .../utils/getCachedRecordEdgesFromRecords.ts | 43 - .../cache/utils/getCachedRecordFromRecord.ts | 16 - .../cache/utils/getConnectionTypename.ts | 9 +- .../cache/utils/getEdgeTypename.ts | 9 +- .../cache/utils/getNodeTypename.ts | 9 +- .../cache/utils/getObjectTypename.ts | 5 + .../utils/getRecordConnectionFromEdges.ts | 19 - .../utils/getRecordConnectionFromRecords.ts | 33 +- .../cache/utils/getRecordEdgeFromRecord.ts | 49 +- .../cache/utils/getRecordFromCache.ts | 55 ++ .../cache/utils/getRecordFromRecordNode.ts | 34 + .../cache/utils/getRecordNodeFromRecord.ts | 143 ++++ .../utils/getRecordsFromRecordConnection.ts | 5 +- .../cache}/utils/isObjectRecordConnection.ts | 0 .../isObjectRecordConnectionWithRefs.ts} | 2 +- .../cache/utils/modifyRecordFromCache.ts | 33 + .../cache/utils/updateRecordFromCache.ts | 59 ++ .../__mocks__/useMapConnectionToRecords.ts | 783 ------------------ .../__tests__/useCreateOneRecord.test.tsx | 1 - .../__tests__/useFindManyRecords.test.tsx | 9 - ...ordsForMultipleMetadataItemsQuery.test.tsx | 2 +- .../useMapConnectionToRecords.test.tsx | 190 ----- .../useModifyRecordFromCache.test.tsx | 52 -- .../hooks/useCreateManyRecords.ts | 115 ++- .../hooks/useCreateManyRecordsInCache.ts | 49 -- .../object-record/hooks/useCreateOneRecord.ts | 95 ++- .../hooks/useCreateOneRecordInCache.ts | 38 - .../hooks/useFindDuplicateRecords.ts | 16 +- .../object-record/hooks/useFindManyRecords.ts | 87 +- .../object-record/hooks/useFindOneRecord.ts | 25 +- .../useGenerateCreateManyRecordMutation.ts | 6 + .../useGenerateCreateOneRecordMutation.ts | 6 + .../hooks/useGenerateFindManyRecordsQuery.ts | 9 +- .../useGenerateUpdateOneRecordMutation.ts | 6 + .../hooks/useMapConnectionToRecords.ts | 113 --- .../object-record/hooks/useUpdateOneRecord.ts | 71 +- .../hooks/useUpsertRecordFieldFromState.ts | 24 - ...FindManyRecordsForMultipleMetadataItems.ts | 44 + ...anyRecordsForMultipleMetadataItemsQuery.ts | 0 .../query-keys/types/QueryKey.ts | 4 +- .../record-field/hooks/usePersistField.ts | 13 + .../__stories__/AddressFieldInput.stories.tsx | 4 +- .../record-field/types/FieldDefinition.ts | 5 + .../hooks/useLoadRecordIndexTable.ts | 3 +- .../options/hooks/useExportTableData.ts | 4 +- .../components/RecordShowContainerEffect.tsx | 14 +- .../RecordDetailRelationSection.tsx | 7 +- .../record-table/components/RecordTable.tsx | 2 +- ...atchesSearchFilterAndSelectedItemsQuery.ts | 2 +- ...archMatchesSearchFilterAndToSelectQuery.ts | 2 +- .../useMultiObjectSearchSelectedItemsQuery.ts | 2 +- .../types/ObjectRecordConnection.ts | 3 +- .../utils/generateEmptyFieldValue.ts | 10 +- .../utils/mapPaginatedRecordsToRecords.ts | 24 - .../object-record/utils/prefillRecord.ts | 35 + .../utils/sanitizeRecordInput.ts | 7 +- .../components/PrefetchRunQueriesEffect.tsx | 44 +- .../hooks/internal/usePrefetchRunQuery.ts | 25 +- .../prefetch/hooks/usePrefetchedData.ts | 1 - .../views/components/ViewBarEffect.tsx | 5 +- .../modules/views/hooks/useGetCurrentView.ts | 6 +- .../onCurrentViewChangeComponentState.ts | 4 +- .../src/modules/views/types/View.ts | 14 +- .../SettingsObjectNewFieldStep2.tsx | 138 +-- .../ObjectMetadataItemsDecorator.tsx | 11 +- .../src/testing/decorators/PageDecorator.tsx | 10 +- .../src/testing/decorators/RootDecorator.tsx | 10 +- ...{mockedClient.ts => mockedApolloClient.ts} | 2 +- .../src/testing/mockedMetadataApolloClient.ts | 6 + packages/twenty-front/src/utils/isDefined.ts | 2 +- .../services/type-mapper.service.ts | 2 +- 172 files changed, 2182 insertions(+), 4915 deletions(-) delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useDeleteActivityFromCache.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivitiesQueries.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivityTargetsQueries.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityOnActivityTargetsCache.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityTargetsOnActivityCache.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivitiesQueries.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivityTargetsQueries.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useActivityById.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useInjectIntoActivityTargetInlineCellCache.test.ts delete mode 100644 packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts create mode 100644 packages/twenty-front/src/modules/activities/query-keys/CreateOneActivityQueryKey.ts create mode 100644 packages/twenty-front/src/modules/activities/query-keys/FindManyActivitiesQueryKey.ts create mode 100644 packages/twenty-front/src/modules/activities/query-keys/FindManyActivityTargetsQueryKey.ts delete mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/__tests__/useInjectIntoTimelineActivitiesQueries.test.tsx delete mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts delete mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts delete mode 100644 packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts rename packages/twenty-front/src/modules/activities/utils/{getTargetObjectFilterFieldName.ts => getActivityTargetObjectFieldIdName.ts} (100%) create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldName.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/getFieldRelationDirections.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts delete mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts rename packages/twenty-front/src/modules/{apollo/optimistic-effect => object-record/cache}/utils/isObjectRecordConnection.ts (100%) rename packages/twenty-front/src/modules/{apollo/optimistic-effect/utils/isCachedObjectRecordConnection.ts => object-record/cache/utils/isObjectRecordConnectionWithRefs.ts} (95%) create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/__mocks__/useMapConnectionToRecords.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useModifyRecordFromCache.test.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useUpsertRecordFieldFromState.ts create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useFindManyRecordsForMultipleMetadataItems.ts rename packages/twenty-front/src/modules/object-record/{ => multiple-objects}/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts (100%) delete mode 100644 packages/twenty-front/src/modules/object-record/utils/mapPaginatedRecordsToRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts rename packages/twenty-front/src/testing/{mockedClient.ts => mockedApolloClient.ts} (74%) create mode 100644 packages/twenty-front/src/testing/mockedMetadataApolloClient.ts diff --git a/packages/twenty-front/.gitignore b/packages/twenty-front/.gitignore index fad3dacbdc..a9e8e5b674 100644 --- a/packages/twenty-front/.gitignore +++ b/packages/twenty-front/.gitignore @@ -40,4 +40,5 @@ dist-ssr *.sln *.sw? -.vite/ \ No newline at end of file +.vite/ +.nyc_output/ \ No newline at end of file diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index fff48ece1d..a001b51a5e 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -18,9 +18,9 @@ export default { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 70, - lines: 70, - functions: 60, + statements: 65, + lines: 65, + functions: 55, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs index bf92d9cde3..121a6b216a 100644 --- a/packages/twenty-front/nyc.config.cjs +++ b/packages/twenty-front/nyc.config.cjs @@ -14,8 +14,8 @@ const modulesCoverage = { }; const pagesCoverage = { - statements: 60, - lines: 60, + statements: 55, + lines: 55, functions: 45, exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'], }; diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index cefdf18e90..a1efde2c89 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1244,4 +1244,4 @@ export const UpdateOneFieldMetadataItemDocument = {"kind":"Document","definition export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; -export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"fromRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"fromRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx index 65b8e0b17b..82cba4e103 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx @@ -13,6 +13,12 @@ const meta: Meta = { container: { width: 728 }, msw: graphqlMocks, }, + args: { + targetableObject: { + id: '1', + targetObjectNameSingular: 'Person', + }, + }, }; export default meta; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index b9c4c84c24..c8ef2611fc 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -1,4 +1,5 @@ import { ClipboardEvent, useCallback, useMemo } from 'react'; +import { useApolloClient } from '@apollo/client'; import { useCreateBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; import { isArray, isNonEmptyString } from '@sniptt/guards'; @@ -16,7 +17,7 @@ import { Activity } from '@/activities/types/Activity'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; @@ -47,7 +48,7 @@ export const ActivityBodyEditor = ({ fillTitleFromBody, }: ActivityBodyEditorProps) => { const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId)); - + const cache = useApolloClient().cache; const activity = activityInStore as Activity | null; const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( @@ -67,9 +68,6 @@ export const ActivityBodyEditor = ({ objectNameSingular: CoreObjectNameSingular.Activity, }); - const modifyActivityFromCache = useModifyRecordFromCache({ - objectMetadataItem: objectMetadataItemActivity, - }); const { goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope, @@ -172,10 +170,15 @@ export const ActivityBodyEditor = ({ }; }); - modifyActivityFromCache(activityId, { - body: () => { - return newStringifiedBody; + modifyRecordFromCache({ + recordId: activityId, + fieldModifiers: { + body: () => { + return newStringifiedBody; + }, }, + cache, + objectMetadataItem: objectMetadataItemActivity, }); const activityTitleHasBeenSet = snapshot @@ -198,16 +201,27 @@ export const ActivityBodyEditor = ({ }; }); - modifyActivityFromCache(activityId, { - title: () => { - return newTitleFromBody; + modifyRecordFromCache({ + recordId: activityId, + fieldModifiers: { + title: () => { + return newTitleFromBody; + }, }, + cache, + objectMetadataItem: objectMetadataItemActivity, }); } handlePersistBody(newStringifiedBody); }, - [activityId, fillTitleFromBody, modifyActivityFromCache, handlePersistBody], + [ + activityId, + cache, + objectMetadataItemActivity, + fillTitleFromBody, + handlePersistBody, + ], ); const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500); diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx index 93b6f54056..3d6a03d994 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx @@ -1,6 +1,5 @@ import { useRecoilCallback } from 'recoil'; -import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState'; @@ -8,6 +7,8 @@ import { canCreateActivityState } from '@/activities/states/canCreateActivitySta import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { Activity } from '@/activities/types/Activity'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; @@ -23,7 +24,9 @@ export const ActivityEditorEffect = ({ ); const { upsertActivity } = useUpsertActivity(); - const { deleteActivityFromCache } = useDeleteActivityFromCache(); + const deleteRecordFromCache = useDeleteRecordFromCache({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); const upsertActivityCallback = useRecoilCallback( ({ snapshot, set }) => @@ -68,7 +71,7 @@ export const ActivityEditorEffect = ({ }, }); } else { - deleteActivityFromCache(activity); + deleteRecordFromCache(activity); } set(isActivityInCreateModeState, false); @@ -87,7 +90,7 @@ export const ActivityEditorEffect = ({ } } }, - [activityId, deleteActivityFromCache, upsertActivity], + [activityId, deleteRecordFromCache, upsertActivity], ); useRegisterClickOutsideListenerCallback({ diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx index 4f99035fab..4b370a3b24 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx @@ -1,10 +1,12 @@ import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { Activity } from '@/activities/types/Activity'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { RecordUpdateHook, @@ -26,9 +28,17 @@ export const ActivityEditorFields = ({ }) => { const { upsertActivity } = useUpsertActivity(); - const [activityFromStore] = useRecoilState( - recordStoreFamilyState(activityId), - ); + const { objectMetadataItem } = useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const getRecordFromCache = useGetRecordFromCache({ + objectMetadataItem, + }); + + const activityFromCache = getRecordFromCache(activityId); + + const activityFromStore = useRecoilValue(recordStoreFamilyState(activityId)); const activity = activityFromStore as Activity; @@ -88,9 +98,9 @@ export const ActivityEditorFields = ({ )} - {ActivityTargetsContextProvider && ( + {ActivityTargetsContextProvider && isDefined(activityFromCache) && ( - + )} diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx index 05cc0bdd3e..dd7ec9f97d 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx @@ -1,4 +1,5 @@ import { useRef } from 'react'; +import { useApolloClient } from '@apollo/client'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilState } from 'recoil'; @@ -13,7 +14,7 @@ import { Activity } from '@/activities/types/Activity'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { Checkbox, @@ -64,6 +65,8 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => { recordStoreFamilyState(activityId), ); + const cache = useApolloClient().cache; + const [activityTitle, setActivityTitle] = useRecoilState( activityTitleFamilyState({ activityId }), ); @@ -112,10 +115,6 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => { objectNameSingular: CoreObjectNameSingular.Activity, }); - const modifyActivityFromCache = useModifyRecordFromCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - const persistTitleDebounced = useDebouncedCallback((newTitle: string) => { upsertActivity({ activity, @@ -142,10 +141,15 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => { setCanCreateActivity(true); } - modifyActivityFromCache(activity.id, { - title: () => { - return newTitle; + modifyRecordFromCache({ + recordId: activity.id, + fieldModifiers: { + title: () => { + return newTitle; + }, }, + cache: cache, + objectMetadataItem: objectMetadataItemActivity, }); }, 500); diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts index a6abbd80e2..a524679fdb 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts @@ -40,7 +40,6 @@ export const useRightDrawerEmailThread = () => { receivedAt: 'AscNullsLast', }, skip: !viewableEmailThreadId, - useRecordsWithoutConnection: true, }); const fetchMoreMessages = useCallback(() => { diff --git a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx index 8e37947a65..9e5cbecc3e 100644 --- a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx +++ b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx @@ -1,6 +1,6 @@ import { Event } from '@/activities/events/types/Event'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx index b8d71ec8d8..2d8539889d 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -1,6 +1,6 @@ import { Attachment } from '@/activities/files/types/Attachment'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -10,7 +10,7 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { nameSingular: targetableObject.targetObjectNameSingular, }); - const { records: attachments } = useFindManyRecords({ + const { records: attachments } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, filter: { [targetableObjectFieldIdName]: { @@ -23,6 +23,6 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { }); return { - attachments: attachments as Attachment[], + attachments, }; }; diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index 1a0cceee7a..b872acab55 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -2,7 +2,7 @@ import { useRecoilValue } from 'recoil'; import { getFileType } from '@/activities/files/utils/getFileType'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { Attachment } from '@/attachments/types/Attachment'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx index 53d32b2694..3e3dd0dfd3 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx @@ -21,7 +21,6 @@ const mockActivityTarget = { const mockActivity = { __typename: 'Activity', - activityTargets: [], updatedAt: '2021-08-03T19:20:06.000Z', createdAt: '2021-08-03T19:20:06.000Z', completedAt: '2021-08-03T19:20:06.000Z', @@ -29,7 +28,6 @@ const mockActivity = { title: 'title', authorId: '1', body: 'body', - comments: [], dueAt: '2021-08-03T19:20:06.000Z', type: 'type', assigneeId: '1', @@ -66,9 +64,7 @@ const mocks: MockedResponse[] = [ __typename updatedAt createdAt - personId activityId - companyId id } cursor diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.tsx deleted file mode 100644 index 4db2793e09..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; -import { renderHook, waitFor } from '@testing-library/react'; -import gql from 'graphql-tag'; -import { RecoilRoot } from 'recoil'; - -import { useActivityById } from '@/activities/hooks/useActivityById'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const mocks: MockedResponse[] = [ - { - request: { - query: gql` - query FindOneActivity($objectRecordId: UUID!) { - activity(filter: { id: { eq: $objectRecordId } }) { - __typename - createdAt - reminderAt - authorId - title - completedAt - updatedAt - body - dueAt - type - id - assigneeId - } - } - `, - variables: { objectRecordId: '1234' }, - }, - result: jest.fn(() => ({ - data: { - activity: mockedActivities[0], - }, - })), - }, -]; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -describe('useActivityById', () => { - it('works as expected', async () => { - const { result } = renderHook( - () => useActivityById({ activityId: '1234' }), - { wrapper: Wrapper }, - ); - - expect(result.current.loading).toBe(true); - - await waitFor(() => !result.current.loading); - - expect(result.current.activity).toEqual({ - __typename: 'Activity', - assigneeId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', - authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', - body: '', - comments: [], - completedAt: null, - createdAt: '2023-04-26T10:12:42.33625+00:00', - activityTargets: [], - dueAt: '2023-04-26T10:12:42.33625+00:00', - id: '3ecaa1be-aac7-463a-a38e-64078dd451d5', - reminderAt: null, - title: 'My very first note', - type: 'Note', - updatedAt: '2023-04-26T10:23:42.33625+00:00', - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx deleted file mode 100644 index c596ecfbd6..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; -import { Comment } from '@/activities/types/Comment'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; - -const mockActivityWithConnectionRelation = { - activityTargets: { - edges: [ - { - __typename: 'ActivityTargetEdge', - node: { - id: '20202020-1029-4661-9e91-83bad932bdff', - }, - }, - ], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - }, - }, - comments: { - edges: [ - { - __typename: 'CommentEdge', - node: { - id: '20202020-1029-4661-9e91-83bad932bdee', - }, - }, - ] as ObjectRecordEdge[], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - }, - }, -}; - -const mockActivityWithArrayRelation = { - activityTargets: [ - { - id: '20202020-1029-4661-9e91-83bad932bdff', - }, - ], - comments: [ - { - id: '20202020-1029-4661-9e91-83bad932bdee', - }, - ], -}; - -describe('useActivityConnectionUtils', () => { - it('Should turn activity with connection relation in activity with array relation', async () => { - const { result } = renderHook(() => useActivityConnectionUtils(), { - wrapper: ({ children }) => ( - { - snapshot.set( - objectMetadataItemsState, - getObjectMetadataItemsMock(), - ); - }} - > - {children} - - ), - }); - - const { makeActivityWithoutConnection } = result.current; - - const { activity: activityWithArrayRelation } = - makeActivityWithoutConnection(mockActivityWithConnectionRelation as any); - - expect(activityWithArrayRelation).toBeDefined(); - - expect(activityWithArrayRelation.activityTargets[0].id).toEqual( - mockActivityWithArrayRelation.activityTargets[0].id, - ); - }); - - it('Should turn activity with connection relation in activity with array relation', async () => { - const { result } = renderHook(() => useActivityConnectionUtils(), { - wrapper: ({ children }) => ( - { - snapshot.set( - objectMetadataItemsState, - getObjectMetadataItemsMock(), - ); - }} - > - {children} - - ), - }); - - const { makeActivityWithConnection } = result.current; - - const { activityWithConnection } = makeActivityWithConnection( - mockActivityWithArrayRelation as any, - ); - - expect(activityWithConnection).toBeDefined(); - - expect(activityWithConnection.activityTargets.edges[0].node.id).toEqual( - mockActivityWithConnectionRelation.activityTargets.edges[0].node.id, - ); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx index e1041843dc..76a0079eb8 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx @@ -1,167 +1,119 @@ import { ReactNode } from 'react'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import gql from 'graphql-tag'; +import { gql, InMemoryCache } from '@apollo/client'; +import { MockedProvider } from '@apollo/client/testing'; +import { act, renderHook } from '@testing-library/react'; import { RecoilRoot, useSetRecoilState } from 'recoil'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { mockedActivities } from '~/testing/mock-data/activities'; -import { mockedCompaniesData } from '~/testing/mock-data/companies'; -import { mockedPeopleData } from '~/testing/mock-data/people'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; -const defaultResponseData = { - pageInfo: { - hasNextPage: false, - startCursor: '', - endCursor: '', - }, - totalCount: 1, -}; - -const mockActivityTarget = { - __typename: 'ActivityTarget', - updatedAt: '2021-08-03T19:20:06.000Z', - createdAt: '2021-08-03T19:20:06.000Z', - personId: '1', - activityId: '234', - companyId: '1', - id: '123', - person: { ...mockedPeopleData[0], __typename: 'Person', updatedAt: '' }, - company: { ...mockedCompaniesData[0], __typename: 'Company', updatedAt: '' }, - activity: mockedActivities[0], -}; - -const mocks: MockedResponse[] = [ - { - request: { - query: gql` - query FindManyActivityTargets( - $filter: ActivityTargetFilterInput - $orderBy: ActivityTargetOrderByInput - $lastCursor: String - $limit: Float - ) { - activityTargets( - filter: $filter - orderBy: $orderBy - first: $limit - after: $lastCursor - ) { - edges { - node { - __typename - updatedAt - createdAt - company { - __typename - xLink { - label - url - } - linkedinLink { - label - url - } - domainName - annualRecurringRevenue { - amountMicros - currencyCode - } - createdAt - address - updatedAt - name - accountOwnerId - employees - id - idealCustomerProfile - } - personId - activityId - companyId - id - activity { - __typename - createdAt - reminderAt - authorId - title - completedAt - updatedAt - body - dueAt - type - id - assigneeId - } - person { - __typename - xLink { - label - url - } - id - createdAt - city - email - jobTitle - name { - firstName - lastName - } - phone - linkedinLink { - label - url - } - updatedAt - avatarUrl - companyId - } - } - cursor - } - pageInfo { - hasNextPage - startCursor - endCursor - } - totalCount - } - } - `, - variables: { - filter: { activityId: { eq: '1234' } }, - limit: undefined, - orderBy: undefined, - }, - }, - result: jest.fn(() => ({ - data: { - activityTargets: { - ...defaultResponseData, - edges: [ - { - node: mockActivityTarget, - cursor: '1', - }, - ], - }, - }, - })), - }, -]; - const mockObjectMetadataItems = getObjectMetadataItemsMock(); +const cache = new InMemoryCache(); + +const activityNode = { + id: '3ecaa1be-aac7-463a-a38e-64078dd451d5', + createdAt: '2023-04-26T10:12:42.33625+00:00', + updatedAt: '2023-04-26T10:23:42.33625+00:00', + reminderAt: null, + title: 'My very first note', + type: 'Note', + body: '', + dueAt: '2023-04-26T10:12:42.33625+00:00', + completedAt: null, + author: null, + assignee: null, + assigneeId: null, + authorId: null, + comments: { + edges: [], + }, + activityTargets: { + edges: [ + { + node: { + id: '89bb825c-171e-4bcc-9cf7-43448d6fb300', + createdAt: '2023-04-26T10:12:42.33625+00:00', + updatedAt: '2023-04-26T10:23:42.33625+00:00', + personId: null, + companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280', + company: { + id: '89bb825c-171e-4bcc-9cf7-43448d6fb280', + name: 'Airbnb', + domainName: 'airbnb.com', + }, + person: null, + activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230', + activity: { + id: '89bb825c-171e-4bcc-9cf7-43448d6fb230', + createdAt: '2023-04-26T10:12:42.33625+00:00', + updatedAt: '2023-04-26T10:23:42.33625+00:00', + }, + __typename: 'ActivityTarget', + }, + __typename: 'ActivityTargetEdge', + }, + ], + __typename: 'ActivityTargetConnection', + }, + __typename: 'Activity' as const, +}; + +cache.writeFragment({ + fragment: gql` + fragment CreateOneActivityInCache on Activity { + id + createdAt + updatedAt + reminderAt + title + body + dueAt + completedAt + author + assignee + assigneeId + authorId + activityTargets { + edges { + node { + id + createdAt + updatedAt + targetObjectNameSingular + personId + companyId + company { + id + name + domainName + } + person + activityId + activity { + id + createdAt + updatedAt + } + __typename + } + } + } + __typename + } + `, + id: activityNode.id, + data: activityNode, +}); + const Wrapper = ({ children }: { children: ReactNode }) => ( - + {children} @@ -170,19 +122,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( ); describe('useActivityTargetObjectRecords', () => { - it('returns default response', () => { - const { result } = renderHook( - () => useActivityTargetObjectRecords({ activityId: '1234' }), - { wrapper: Wrapper }, - ); - - expect(result.current).toEqual({ - activityTargetObjectRecords: [], - loadingActivityTargets: false, - }); - }); - - it('fetches records', async () => { + it('return targetObjects', async () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( @@ -192,11 +132,12 @@ describe('useActivityTargetObjectRecords', () => { objectMetadataItemsState, ); - const { activityTargetObjectRecords, loadingActivityTargets } = - useActivityTargetObjectRecords({ activityId: '1234' }); + const { activityTargetObjectRecords } = useActivityTargetObjectRecords( + getRecordFromRecordNode({ recordNode: activityNode as any }), + ); + return { activityTargetObjectRecords, - loadingActivityTargets, setCurrentWorkspaceMember, setObjectMetadataItems, }; @@ -208,16 +149,18 @@ describe('useActivityTargetObjectRecords', () => { result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]); result.current.setObjectMetadataItems(mockObjectMetadataItems); }); + const activityTargetObjectRecords = + result.current.activityTargetObjectRecords; - expect(result.current.loadingActivityTargets).toBe(true); - - // Wait for activityTargets to complete fetching - await waitFor(() => !result.current.loadingActivityTargets); - - expect(mocks[0].result).toHaveBeenCalled(); - expect(result.current.activityTargetObjectRecords).toHaveLength(1); + expect(activityTargetObjectRecords).toHaveLength(1); + expect(activityTargetObjectRecords[0].activityTarget).toEqual( + activityNode.activityTargets.edges[0].node, + ); + expect(activityTargetObjectRecords[0].targetObject).toEqual( + activityNode.activityTargets.edges[0].node.company, + ); expect( - result.current.activityTargetObjectRecords[0].targetObjectNameSingular, - ).toBe('person'); + activityTargetObjectRecords[0].targetObjectMetadataItem.nameSingular, + ).toEqual('company'); }); }); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx deleted file mode 100644 index a3e8fd73f0..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot, useSetRecoilState } from 'recoil'; - -import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; - -const mocks: MockedResponse[] = []; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -const mockObjectMetadataItems = getObjectMetadataItemsMock(); - -describe('useAttachRelationInBothDirections', () => { - it('works as expected', () => { - const { result } = renderHook( - () => { - const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState, - ); - const setObjectMetadataItems = useSetRecoilState( - objectMetadataItemsState, - ); - - const res = useAttachRelationInBothDirections(); - return { - ...res, - setCurrentWorkspaceMember, - setObjectMetadataItems, - }; - }, - { wrapper: Wrapper }, - ); - - act(() => { - result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]); - result.current.setObjectMetadataItems(mockObjectMetadataItems); - }); - const targetRecords = [ - { id: '5678', person: { id: '1234' } }, - { id: '91011', person: { id: '1234' } }, - ]; - - const forEachSpy = jest.spyOn(targetRecords, 'forEach'); - - act(() => { - result.current.attachRelationInBothDirections({ - sourceRecord: { - id: '1234', - company: { id: '5678' }, - }, - targetRecords, - sourceObjectNameSingular: 'person', - targetObjectNameSingular: 'company', - fieldNameOnSourceRecord: 'company', - fieldNameOnTargetRecord: 'person', - }); - }); - - // expect forEach to have been called on targetRecords - expect(forEachSpy).toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useDeleteActivityFromCache.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useDeleteActivityFromCache.test.tsx deleted file mode 100644 index e999f63944..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useDeleteActivityFromCache.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import pick from 'lodash.pick'; -import { RecoilRoot } from 'recoil'; - -import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; -import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const triggerDeleteRecordsOptimisticEffectMock = jest.fn(); - -// mock the triggerDeleteRecordsOptimisticEffect function -jest.mock( - '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect', - () => ({ - triggerDeleteRecordsOptimisticEffect: jest.fn(), - }), -); - -(triggerDeleteRecordsOptimisticEffect as jest.Mock).mockImplementation( - triggerDeleteRecordsOptimisticEffectMock, -); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -describe('useDeleteActivityFromCache', () => { - it('works as expected', () => { - const { result } = renderHook(() => useDeleteActivityFromCache(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.deleteActivityFromCache( - pick(mockedActivities[0], [ - 'id', - 'title', - 'body', - 'type', - 'completedAt', - 'dueAt', - 'updatedAt', - ]), - ); - - expect(triggerDeleteRecordsOptimisticEffectMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivitiesQueries.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivitiesQueries.test.tsx deleted file mode 100644 index f965c403f3..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivitiesQueries.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const upsertFindManyRecordsQueryInCacheMock = jest.fn(); - -jest.mock( - '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache', - () => ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(), - }), -); - -(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock, -})); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -describe('useInjectIntoActivitiesQueries', () => { - it('works as expected', () => { - const { result } = renderHook(() => useInjectIntoActivitiesQueries(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.injectActivitiesQueries({ - activityToInject: mockedActivities[0], - activityTargetsToInject: [], - targetableObjects: [{ id: '123', targetObjectNameSingular: 'person' }], - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1); - }); - - act(() => { - result.current.injectActivitiesQueries({ - activityToInject: mockedActivities[0], - activityTargetsToInject: [], - targetableObjects: [], - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivityTargetsQueries.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivityTargetsQueries.test.tsx deleted file mode 100644 index 969b63943d..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useInjectIntoActivityTargetsQueries.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const upsertFindManyRecordsQueryInCacheMock = jest.fn(); - -jest.mock( - '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache', - () => ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(), - }), -); - -(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock, -})); - -const mockActivityTarget = { - __typename: 'ActivityTarget', - updatedAt: '2021-08-03T19:20:06.000Z', - createdAt: '2021-08-03T19:20:06.000Z', - personId: '1', - activityId: '234', - companyId: '1', - id: '123', - activity: mockedActivities[0], -}; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -describe('useInjectIntoActivityTargetsQueries', () => { - it('works as expected', () => { - const { result } = renderHook(() => useInjectIntoActivityTargetsQueries(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.injectActivityTargetsQueries({ - activityTargetsToInject: [mockActivityTarget], - targetableObjects: [{ id: '123', targetObjectNameSingular: 'person' }], - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1); - }); - - act(() => { - result.current.injectActivityTargetsQueries({ - activityTargetsToInject: [mockActivityTarget], - targetableObjects: [], - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityOnActivityTargetsCache.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityOnActivityTargetsCache.test.tsx deleted file mode 100644 index b5858858bd..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityOnActivityTargetsCache.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const useModifyRecordFromCacheMock = jest.fn(); - -jest.mock('@/object-record/cache/hooks/useModifyRecordFromCache', () => ({ - useModifyRecordFromCache: jest.fn(), -})); - -(useModifyRecordFromCache as jest.Mock).mockImplementation( - () => useModifyRecordFromCacheMock, -); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -describe('useModifyActivityOnActivityTargetsCache', () => { - it('works as expected', () => { - const { result } = renderHook( - () => useModifyActivityOnActivityTargetsCache(), - { - wrapper: Wrapper, - }, - ); - - act(() => { - result.current.modifyActivityOnActivityTargetsCache({ - activity: mockedActivities[0], - activityTargetIds: ['123', '456'], - }); - }); - - expect(useModifyRecordFromCacheMock).toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityTargetsOnActivityCache.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityTargetsOnActivityCache.test.tsx deleted file mode 100644 index cfc4bce55d..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useModifyActivityTargetsOnActivityCache.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; - -const useModifyRecordFromCacheMock = jest.fn(); - -jest.mock('@/object-record/cache/hooks/useModifyRecordFromCache', () => ({ - useModifyRecordFromCache: jest.fn(), -})); - -(useModifyRecordFromCache as jest.Mock).mockImplementation( - () => useModifyRecordFromCacheMock, -); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -describe('useModifyActivityTargetsOnActivityCache', () => { - it('works as expected', () => { - const { result } = renderHook( - () => useModifyActivityTargetsOnActivityCache(), - { - wrapper: Wrapper, - }, - ); - - act(() => { - result.current.modifyActivityTargetsOnActivityCache({ - activityId: '1234', - activityTargets: [], - }); - }); - - expect(useModifyRecordFromCacheMock).toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx deleted file mode 100644 index 7df977a2ad..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; -import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; - -const useOpenCreateActivityDrawerMock = jest.fn(); -jest.mock('@/activities/hooks/useOpenCreateActivityDrawer', () => ({ - useOpenCreateActivityDrawer: jest.fn(), -})); - -(useOpenCreateActivityDrawer as jest.Mock).mockImplementation( - () => useOpenCreateActivityDrawerMock, -); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -const mockObjectMetadataItems = getObjectMetadataItemsMock(); -const recordTableId = 'recordTableId'; -const tableRowIds = ['123', '456']; -const recordObject = { - id: '789', -}; - -describe('useOpenCreateActivityDrawerForSelectedRowIds', () => { - it('works as expected', async () => { - const { result } = renderHook( - () => { - const openCreateActivityDrawerForSelectedRowIds = - useOpenCreateActivityDrawerForSelectedRowIds(recordTableId); - const viewableActivityId = useRecoilValue(viewableActivityIdState); - const activityIdInDrawer = useRecoilValue(activityIdInDrawerState); - const setObjectMetadataItems = useSetRecoilState( - objectMetadataItemsState, - ); - const scopeId = `${recordTableId}-scope`; - const setTableRowIds = useSetRecoilState( - tableRowIdsComponentState({ scopeId }), - ); - const setIsRowSelectedComponentFamilyState = useSetRecoilState( - isRowSelectedComponentFamilyState({ - scopeId, - familyKey: tableRowIds[0], - }), - ); - const setRecordStoreFamilyState = useSetRecoilState( - recordStoreFamilyState(tableRowIds[0]), - ); - return { - openCreateActivityDrawerForSelectedRowIds, - activityIdInDrawer, - viewableActivityId, - setObjectMetadataItems, - setTableRowIds, - setIsRowSelectedComponentFamilyState, - setRecordStoreFamilyState, - }; - }, - { - wrapper: Wrapper, - }, - ); - - act(() => { - result.current.setTableRowIds(tableRowIds); - result.current.setRecordStoreFamilyState(recordObject); - result.current.setIsRowSelectedComponentFamilyState(true); - result.current.setObjectMetadataItems(mockObjectMetadataItems); - }); - - expect(result.current.activityIdInDrawer).toBeNull(); - expect(result.current.viewableActivityId).toBeNull(); - await act(async () => { - result.current.openCreateActivityDrawerForSelectedRowIds( - 'Note', - 'person', - [{ id: '176', targetObjectNameSingular: 'person' }], - ); - }); - - expect(useOpenCreateActivityDrawerMock).toHaveBeenCalledWith({ - type: 'Note', - targetableObjects: [ - { - type: 'Custom', - targetObjectNameSingular: 'person', - id: '123', - targetObjectRecord: { id: '789' }, - }, - { - id: '176', - targetObjectNameSingular: 'person', - }, - ], - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivitiesQueries.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivitiesQueries.test.tsx deleted file mode 100644 index 6e2f5a5161..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivitiesQueries.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; - -const upsertFindManyRecordsQueryInCacheMock = jest.fn(); -const useReadFindManyRecordsQueryInCacheMock = jest.fn(() => [ - { activityId: '981' }, - { activityId: '345' }, -]); -jest.mock( - '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache', - () => ({ - useReadFindManyRecordsQueryInCache: jest.fn(), - }), -); -jest.mock( - '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache', - () => ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(), - }), -); - -(useReadFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - readFindManyRecordsQueryInCache: useReadFindManyRecordsQueryInCacheMock, -})); - -(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock, -})); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -describe('useRemoveFromActivitiesQueries', () => { - it('works as expected', () => { - const { result } = renderHook(() => useRemoveFromActivitiesQueries(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.removeFromActivitiesQueries({ - activityIdToRemove: '123', - targetableObjects: [], - }); - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledWith({ - objectRecordsToOverwrite: [{ activityId: '981' }, { activityId: '345' }], - queryVariables: { - filter: { id: { in: ['345', '981'] } }, - orderBy: undefined, - }, - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivityTargetsQueries.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivityTargetsQueries.test.tsx deleted file mode 100644 index 79e8ff6c8a..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useRemoveFromActivityTargetsQueries.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const upsertFindManyRecordsQueryInCacheMock = jest.fn(); -const useReadFindManyRecordsQueryInCacheMock = jest.fn(() => [ - { id: '981' }, - { id: '345' }, -]); -jest.mock( - '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache', - () => ({ - useReadFindManyRecordsQueryInCache: jest.fn(), - }), -); -jest.mock( - '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache', - () => ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(), - }), -); - -(useReadFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - readFindManyRecordsQueryInCache: useReadFindManyRecordsQueryInCacheMock, -})); -(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock, -})); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -const mockActivityTarget = { - __typename: 'ActivityTarget', - updatedAt: '2021-08-03T19:20:06.000Z', - createdAt: '2021-08-03T19:20:06.000Z', - personId: '1', - activityId: '234', - companyId: '1', - id: '123', - activity: mockedActivities[0], -}; - -describe('useRemoveFromActivityTargetsQueries', () => { - it('works as expected', () => { - const { result } = renderHook(() => useRemoveFromActivityTargetsQueries(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.removeFromActivityTargetsQueries({ - activityTargetsToRemove: [mockActivityTarget], - targetableObjects: [], - }); - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledWith({ - objectRecordsToOverwrite: [{ id: '981' }, { id: '345' }], - queryVariables: { filter: {} }, - depth: 2, - }); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx deleted file mode 100644 index ddf4d40e5e..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import gql from 'graphql-tag'; -import { RecoilRoot, useSetRecoilState } from 'recoil'; - -import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; -import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; -import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; -import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; -import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; -import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; -import { Activity } from '@/activities/types/Activity'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -const newId = 'new-id'; -const activity = mockedActivities[0]; -const input: Partial = { id: newId }; - -const mockedDate = '2024-03-15T12:00:00.000Z'; -const toISOStringMock = jest.fn(() => mockedDate); -global.Date.prototype.toISOString = toISOStringMock; - -const useCreateActivityInDBMock = jest.fn(); - -jest.mock('@/activities/hooks/useCreateActivityInDB', () => ({ - useCreateActivityInDB: jest.fn(), -})); -(useCreateActivityInDB as jest.Mock).mockImplementation(() => ({ - createActivityInDB: useCreateActivityInDBMock, -})); - -const mocks: MockedResponse[] = [ - { - request: { - query: gql` - mutation UpdateOneActivity( - $idToUpdate: ID! - $input: ActivityUpdateInput! - ) { - updateActivity(id: $idToUpdate, data: $input) { - __typename - createdAt - reminderAt - authorId - title - completedAt - updatedAt - body - dueAt - type - id - assigneeId - } - } - `, - variables: { - idToUpdate: activity.id, - input: { id: 'new-id' }, - }, - }, - result: jest.fn(() => ({ - data: { - updateActivity: { ...activity, ...input }, - }, - })), - }, -]; - -const getWrapper = - (initialIndex: 0 | 1) => - ({ children }: { children: ReactNode }) => ( - - - - {children} - - - - ); - -describe('useUpsertActivity', () => { - it('updates an activity', async () => { - const { result } = renderHook(() => useUpsertActivity(), { - wrapper: getWrapper(0), - }); - - await act(async () => { - await result.current.upsertActivity({ - activity, - input, - }); - }); - - expect(mocks[0].result).toHaveBeenCalled(); - }); - - it('creates an activity on tasks page', async () => { - const { result } = renderHook( - () => { - const res = useUpsertActivity(); - const setIsActivityInCreateMode = useSetRecoilState( - isActivityInCreateModeState, - ); - - return { ...res, setIsActivityInCreateMode }; - }, - { - wrapper: getWrapper(0), - }, - ); - - act(() => { - result.current.setIsActivityInCreateMode(true); - }); - - await act(async () => { - await result.current.upsertActivity({ - activity, - input: {}, - }); - }); - - expect(useCreateActivityInDBMock).toHaveBeenCalledTimes(1); - }); - - it('creates an activity on objects page', async () => { - const { result } = renderHook( - () => { - const res = useUpsertActivity(); - const setIsActivityInCreateMode = useSetRecoilState( - isActivityInCreateModeState, - ); - const setObjectShowPageTargetableObject = useSetRecoilState( - objectShowPageTargetableObjectState, - ); - const setCurrentCompletedTaskQueryVariables = useSetRecoilState( - currentCompletedTaskQueryVariablesState, - ); - const setCurrentIncompleteTaskQueryVariables = useSetRecoilState( - currentIncompleteTaskQueryVariablesState, - ); - - const setCurrentNotesQueryVariables = useSetRecoilState( - currentNotesQueryVariablesState, - ); - - return { - ...res, - setIsActivityInCreateMode, - setObjectShowPageTargetableObject, - setCurrentCompletedTaskQueryVariables, - setCurrentIncompleteTaskQueryVariables, - setCurrentNotesQueryVariables, - }; - }, - { - wrapper: getWrapper(1), - }, - ); - - act(() => { - result.current.setIsActivityInCreateMode(true); - result.current.setObjectShowPageTargetableObject({ - id: '123', - targetObjectNameSingular: 'people', - }); - result.current.setCurrentCompletedTaskQueryVariables({}); - result.current.setCurrentIncompleteTaskQueryVariables({}); - result.current.setCurrentNotesQueryVariables({}); - }); - - await act(async () => { - await result.current.upsertActivity({ - activity, - input: {}, - }); - }); - - expect(useCreateActivityInDBMock).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts index 41813d0dd7..1c2cb0943b 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts @@ -2,13 +2,12 @@ import { useEffect, useState } from 'react'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects'; +import { FIND_MANY_ACTIVITIES_QUERY_KEY } from '@/activities/query-keys/FindManyActivitiesQueryKey'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { OrderByField } from '@/object-metadata/types/OrderByField'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -29,7 +28,7 @@ export const useActivities = ({ }) => { const [initialized, setInitialized] = useState(false); - const { makeActivityWithoutConnection } = useActivityConnectionUtils(); + const { objectMetadataItems } = useObjectMetadataItems(); const { activityTargets, @@ -40,13 +39,17 @@ export const useActivities = ({ skip: skipActivityTargets || skip, }); - const activityIds = activityTargets - ? [ - ...activityTargets - .map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString), - ].sort(sortByAscString) - : []; + const activityIds = [ + ...new Set( + activityTargets + ? [ + ...activityTargets + .map((activityTarget) => activityTarget.activityId) + .filter(isNonEmptyString), + ].sort(sortByAscString) + : [], + ), + ]; const activityTargetsFound = initializedActivityTargets && isNonEmptyArray(activityTargets); @@ -65,24 +68,22 @@ export const useActivities = ({ (!skipActivityTargets && (!initializedActivityTargets || !activityTargetsFound)); - const { records: activitiesWithConnection, loading: loadingActivities } = + const { records: activities, loading: loadingActivities } = useFindManyRecords({ skip: skipActivities, - objectNameSingular: CoreObjectNameSingular.Activity, - depth: 1, + objectNameSingular: FIND_MANY_ACTIVITIES_QUERY_KEY.objectNameSingular, + depth: FIND_MANY_ACTIVITIES_QUERY_KEY.depth, + queryFields: + FIND_MANY_ACTIVITIES_QUERY_KEY.fieldsFactory?.(objectMetadataItems), filter, orderBy: activitiesOrderByVariables, onCompleted: useRecoilCallback( ({ set }) => - (data) => { + (activities) => { if (!initialized) { setInitialized(true); } - const activities = getRecordsFromRecordConnection({ - recordConnection: data, - }); - for (const activity of activities) { set(recordStoreFamilyState(activity.id), activity); } @@ -93,11 +94,6 @@ export const useActivities = ({ const loading = loadingActivities || loadingActivityTargets; - // TODO: fix connection in relation => automatically change to an array - const activities: Activity[] = activitiesWithConnection - ?.map(makeActivityWithoutConnection as any) - .map(({ activity }: any) => activity); - const noActivities = (!activityTargetsFound && !skipActivityTargets && initialized) || (initialized && !loading && !isNonEmptyArray(activities)); diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts deleted file mode 100644 index 4f63b2d1b2..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; - -const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3; - -export const useActivityById = ({ activityId }: { activityId: string }) => { - const { makeActivityWithoutConnection } = useActivityConnectionUtils(); - - // TODO: fix connection in relation => automatically change to an array - const { record: activityWithConnections, loading } = useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activityId, - skip: !activityId, - depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS, - }); - - const { activity } = activityWithConnections - ? makeActivityWithoutConnection(activityWithConnections as any) - : { activity: null }; - - return { - activity, - loading, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts deleted file mode 100644 index 55cd0b4018..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { isNonEmptyArray } from '@apollo/client/utilities'; - -import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { Comment } from '@/activities/types/Comment'; -import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; -import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; -import { isDefined } from '~/utils/isDefined'; - -export const useActivityConnectionUtils = () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const makeActivityWithoutConnection = ( - activityWithConnections: Activity & { - activityTargets: ObjectRecordConnection; - comments: ObjectRecordConnection; - }, - ) => { - if (!isDefined(activityWithConnections)) { - throw new Error('Activity with connections is not defined'); - } - - const hasActivityTargetsConnection = isObjectRecordConnection( - CoreObjectNameSingular.ActivityTarget, - activityWithConnections?.activityTargets, - ); - - const activityTargets: ActivityTarget[] = []; - - if (hasActivityTargetsConnection) { - const newActivityTargets = mapConnectionToRecords({ - objectRecordConnection: activityWithConnections?.activityTargets, - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - depth: 5, - }) as ActivityTarget[]; - - activityTargets.push(...newActivityTargets); - } - - const hasCommentsConnection = isObjectRecordConnection( - CoreObjectNameSingular.Comment, - activityWithConnections?.comments, - ); - - const comments: Comment[] = []; - - if (hasCommentsConnection) { - const newComments = mapConnectionToRecords({ - objectRecordConnection: activityWithConnections?.comments, - objectNameSingular: CoreObjectNameSingular.Comment, - depth: 5, - }) as Comment[]; - - comments.push(...newComments); - } - - const activity: Activity = { - ...activityWithConnections, - activityTargets, - comments, - }; - - return { activity }; - }; - - const makeActivityWithConnection = (activity: Activity) => { - const activityTargetEdges = isNonEmptyArray(activity?.activityTargets) - ? activity.activityTargets.map((activityTarget) => ({ - node: activityTarget, - cursor: '', - })) - : []; - - const commentEdges = isNonEmptyArray(activity?.comments) - ? activity.comments.map((comment) => ({ - node: comment, - cursor: '', - })) - : []; - - const activityTargets = { - __typename: 'ActivityTargetConnection', - edges: activityTargetEdges, - pageInfo: getEmptyPageInfo(), - } as ObjectRecordConnection; - - const comments = { - __typename: 'CommentConnection', - edges: commentEdges, - pageInfo: getEmptyPageInfo(), - } as ObjectRecordConnection; - - const activityWithConnection = { - ...activity, - activityTargets, - comments, - } as Activity & { - activityTargets: ObjectRecordConnection; - comments: ObjectRecordConnection; - }; - - return { activityWithConnection }; - }; - - return { - makeActivityWithoutConnection, - makeActivityWithConnection, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts index 359b1c7550..455cb96559 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts @@ -1,56 +1,73 @@ -import { isNonEmptyString } from '@sniptt/guards'; +import { useApolloClient } from '@apollo/client'; import { useRecoilValue } from 'recoil'; +import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { Nullable } from '~/types/Nullable'; import { isDefined } from '~/utils/isDefined'; -export const useActivityTargetObjectRecords = ({ - activityId, -}: { - activityId: string; -}) => { +export const useActivityTargetObjectRecords = (activity: Activity) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const { records: activityTargets, loading: loadingActivityTargets } = - useFindManyRecords({ + const activityTargets = activity.activityTargets ?? []; + + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, - skip: !isNonEmptyString(activityId), - filter: { - activityId: { - eq: activityId, - }, - }, }); + const getRecordFromCache = useGetRecordFromCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const apolloClient = useApolloClient(); + const activityTargetObjectRecords = activityTargets .map>((activityTarget) => { + const activityTargetFromCache = getRecordFromCache( + activityTarget.id, + apolloClient.cache, + ); + + if (!isDefined(activityTargetFromCache)) { + throw new Error( + `Cannot find activity target ${activityTarget.id} in cache, this shouldn't happen.`, + ); + } + const correspondingObjectMetadataItem = objectMetadataItems.find( (objectMetadataItem) => - isDefined(activityTarget[objectMetadataItem.nameSingular]) && + isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) && !objectMetadataItem.isSystem, ); if (!correspondingObjectMetadataItem) { - return null; + return undefined; + } + + const targetObjectRecord = + activityTargetFromCache[correspondingObjectMetadataItem.nameSingular]; + + if (!targetObjectRecord) { + throw new Error( + `Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`, + ); } return { - activityTarget: activityTarget, - targetObject: - activityTarget[correspondingObjectMetadataItem.nameSingular], + activityTarget: activityTargetFromCache ?? activityTarget, + targetObject: targetObjectRecord ?? undefined, targetObjectMetadataItem: correspondingObjectMetadataItem, - targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular, }; }) .filter(isDefined); return { activityTargetObjectRecords, - loadingActivityTargets, }; }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts index a48dec8f8d..7d2a8e762c 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts @@ -3,7 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -26,7 +26,7 @@ export const useActivityTargetsForTargetableObject = ({ // If we are on a show page and we remove the current show page object corresponding activity target // See also if we need to update useTimelineActivities const { records: activityTargets, loading: loadingActivityTargets } = - useFindManyRecords({ + useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, skip: skipRequest, filter: { @@ -42,7 +42,7 @@ export const useActivityTargetsForTargetableObject = ({ }); return { - activityTargets: activityTargets as ActivityTarget[], + activityTargets, loadingActivityTargets, initialized, }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts index 2dbe174a6a..be82811029 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts @@ -1,9 +1,11 @@ import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY } from '@/activities/query-keys/FindManyActivityTargetsQueryKey'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; export const useActivityTargetsForTargetableObjects = ({ @@ -20,16 +22,23 @@ export const useActivityTargetsForTargetableObjects = ({ targetableObjects: targetableObjects, }); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const [initialized, setInitialized] = useState(false); // TODO: We want to optimistically remove from this request // If we are on a show page and we remove the current show page object corresponding activity target // See also if we need to update useTimelineActivities const { records: activityTargets, loading: loadingActivityTargets } = - useFindManyRecords({ + useFindManyRecords({ skip, - objectNameSingular: CoreObjectNameSingular.ActivityTarget, + objectNameSingular: + FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY.objectNameSingular, filter: activityTargetsFilter, + queryFields: + FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY.fieldsFactory?.( + objectMetadataItems, + ), onCompleted: () => { if (!initialized) { setInitialized(true); @@ -38,7 +47,7 @@ export const useActivityTargetsForTargetableObjects = ({ }); return { - activityTargets: activityTargets as ActivityTarget[], + activityTargets, loadingActivityTargets, initialized, }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts b/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts deleted file mode 100644 index 2ef89de545..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { useApolloClient } from '@apollo/client'; -import { StringKeyOf } from 'type-fest'; - -import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition'; -import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isDefined } from '~/utils/isDefined'; - -export const useAttachRelationInBothDirections = () => { - const { objectMetadataItems } = useObjectMetadataItems(); - - const apolloClient = useApolloClient(); - - const attachRelationInBothDirections = < - Source extends ObjectRecord = ObjectRecord, - Target extends ObjectRecord = ObjectRecord, - >({ - sourceRecord, - targetRecords, - sourceObjectNameSingular, - targetObjectNameSingular, - fieldNameOnSourceRecord, - fieldNameOnTargetRecord, - }: { - sourceRecord: Source; - targetRecords: Target[]; - sourceObjectNameSingular: string; - targetObjectNameSingular: string; - fieldNameOnSourceRecord: StringKeyOf; - fieldNameOnTargetRecord: StringKeyOf; - }) => { - const sourceObjectMetadataItem = getObjectMetadataItemByNameSingular({ - objectMetadataItems, - objectNameSingular: sourceObjectNameSingular, - }); - - const targetObjectMetadataItem = getObjectMetadataItemByNameSingular({ - objectMetadataItems, - objectNameSingular: targetObjectNameSingular, - }); - - const fieldMetadataItemOnSourceRecord = - sourceObjectMetadataItem.fields.find( - (field) => field.name === fieldNameOnSourceRecord, - ); - - if (!isDefined(fieldMetadataItemOnSourceRecord)) { - throw new Error( - `Field ${fieldNameOnSourceRecord} not found on object ${sourceObjectNameSingular}`, - ); - } - - const relationDefinition = getRelationDefinition({ - fieldMetadataItemOnSourceRecord: fieldMetadataItemOnSourceRecord, - objectMetadataItems, - }); - - if (!isDefined(relationDefinition)) { - throw new Error( - `Relation metadata not found for field ${fieldNameOnSourceRecord} on object ${sourceObjectNameSingular}`, - ); - } - - // TODO: could we use triggerUpdateRelationsOptimisticEffect here? - targetRecords.forEach((relationTargetRecord) => { - triggerAttachRelationOptimisticEffect({ - cache: apolloClient.cache, - sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, - sourceRecordId: sourceRecord.id, - fieldNameOnTargetRecord: fieldNameOnTargetRecord, - targetObjectNameSingular: targetObjectMetadataItem.nameSingular, - targetRecordId: relationTargetRecord.id, - }); - - triggerAttachRelationOptimisticEffect({ - cache: apolloClient.cache, - sourceObjectNameSingular: targetObjectMetadataItem.nameSingular, - sourceRecordId: relationTargetRecord.id, - fieldNameOnTargetRecord: fieldNameOnSourceRecord, - targetObjectNameSingular: sourceObjectMetadataItem.nameSingular, - targetRecordId: sourceRecord.id, - }); - }); - }; - - return { - attachRelationInBothDirections, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts index 4c1a927f7a..c58b5a9ef6 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts @@ -1,20 +1,24 @@ +import { Reference, useApolloClient } from '@apollo/client'; import { useRecoilCallback, useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; -import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections'; -import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; import { Activity, ActivityType } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { makeActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache'; -import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache'; +import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache'; +import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateActivityInCache = () => { const { createManyRecordsInCache: createManyActivityTargetsInCache } = @@ -22,11 +26,9 @@ export const useCreateActivityInCache = () => { objectNameSingular: CoreObjectNameSingular.ActivityTarget, }); - const { createOneRecordInCache: createOneActivityInCache } = - useCreateOneRecordInCache({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); + const cache = useApolloClient().cache; + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { record: currentWorkspaceMemberRecord } = useFindOneRecord({ @@ -35,27 +37,37 @@ export const useCreateActivityInCache = () => { depth: 0, }); - const { injectIntoActivityTargetInlineCellCache } = - useInjectIntoActivityTargetInlineCellCache(); + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); - const { attachRelationInBothDirections } = - useAttachRelationInBothDirections(); + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const createOneActivityInCache = useCreateOneRecordInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); const createActivityInCache = useRecoilCallback( ({ snapshot, set }) => ({ type, - targetableObjects, + targetObject, customAssignee, }: { type: ActivityType; - targetableObjects: ActivityTargetableObject[]; + targetObject?: ActivityTargetableObject; customAssignee?: WorkspaceMember; }) => { const activityId = v4(); const createdActivityInCache = createOneActivityInCache({ id: activityId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), author: currentWorkspaceMemberRecord, authorId: currentWorkspaceMemberRecord?.id, assignee: customAssignee ?? currentWorkspaceMemberRecord, @@ -63,42 +75,89 @@ export const useCreateActivityInCache = () => { type, }); - const targetObjectRecords = targetableObjects - .map((targetableObject) => { - const targetObject = snapshot - .getLoadable(recordStoreFamilyState(targetableObject.id)) - .getValue(); + if (isUndefinedOrNull(createdActivityInCache)) { + throw new Error('Failed to create activity in cache'); + } - return targetObject; - }) - .filter(isDefined); + if (isUndefinedOrNull(targetObject)) { + set(recordStoreFamilyState(activityId), { + ...createdActivityInCache, + activityTargets: [], + comments: [], + }); + + return { + createdActivityInCache: { + ...createdActivityInCache, + activityTargets: [], + }, + }; + } + + const targetObjectRecord = snapshot + .getLoadable(recordStoreFamilyState(targetObject.id)) + .getValue(); + + if (isUndefinedOrNull(targetObjectRecord)) { + throw new Error('Failed to find target object record'); + } const activityTargetsToCreate = makeActivityTargetsToCreateFromTargetableObjects({ - activityId, - targetableObjects, - targetObjectRecords, + activity: createdActivityInCache, + targetableObjects: [targetObject], + targetObjectRecords: [targetObjectRecord], }); const createdActivityTargetsInCache = createManyActivityTargetsInCache( activityTargetsToCreate, ); - injectIntoActivityTargetInlineCellCache({ - activityId, - activityTargetsToInject: createdActivityTargetsInCache, + const activityTargetsConnection = getRecordConnectionFromRecords({ + objectMetadataItems: objectMetadataItems, + objectMetadataItem: objectMetadataItemActivityTarget, + records: createdActivityTargetsInCache, + withPageInfo: false, + computeReferences: true, + isRootLevel: false, }); - attachRelationInBothDirections({ - sourceRecord: createdActivityInCache, - fieldNameOnSourceRecord: 'activityTargets', - sourceObjectNameSingular: CoreObjectNameSingular.Activity, - fieldNameOnTargetRecord: 'activity', - targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget, - targetRecords: createdActivityTargetsInCache, + modifyRecordFromCache({ + recordId: createdActivityInCache.id, + cache, + fieldModifiers: { + activityTargets: () => activityTargetsConnection, + }, + objectMetadataItem: objectMetadataItemActivity, }); - // TODO: should refactor when refactoring make activity connection utils + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === targetObject.targetObjectNameSingular, + ); + + if (isDefined(targetObjectMetadataItem)) { + modifyRecordFromCache({ + cache, + objectMetadataItem: targetObjectMetadataItem, + recordId: targetObject.id, + fieldModifiers: { + activityTargets: (activityTargetsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + activityTargetsRef, + ); + + if (!edges) return activityTargetsRef; + + return { + ...activityTargetsRef, + edges: [...edges, ...activityTargetsConnection.edges], + }; + }, + }, + }); + } + set(recordStoreFamilyState(activityId), { ...createdActivityInCache, activityTargets: createdActivityTargetsInCache, @@ -110,15 +169,16 @@ export const useCreateActivityInCache = () => { ...createdActivityInCache, activityTargets: createdActivityTargetsInCache, }, - createdActivityTargetsInCache, }; }, [ - attachRelationInBothDirections, - createManyActivityTargetsInCache, createOneActivityInCache, currentWorkspaceMemberRecord, - injectIntoActivityTargetInlineCellCache, + createManyActivityTargetsInCache, + objectMetadataItems, + objectMetadataItemActivityTarget, + cache, + objectMetadataItemActivity, ], ); diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts index 5ade365820..58e0349e28 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts @@ -1,6 +1,6 @@ import { isNonEmptyArray } from '@sniptt/guards'; -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; +import { CREATE_ONE_ACTIVITY_QUERY_KEY } from '@/activities/query-keys/CreateOneActivityQueryKey'; import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -9,37 +9,27 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; export const useCreateActivityInDB = () => { const { createOneRecord: createOneActivity } = useCreateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Activity, + objectNameSingular: CREATE_ONE_ACTIVITY_QUERY_KEY.objectNameSingular, + queryFields: CREATE_ONE_ACTIVITY_QUERY_KEY.fields, + depth: CREATE_ONE_ACTIVITY_QUERY_KEY.depth, }); const { createManyRecords: createManyActivityTargets } = useCreateManyRecords({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, + skipPostOptmisticEffect: true, }); - const { makeActivityWithConnection } = useActivityConnectionUtils(); - const createActivityInDB = async (activityToCreate: ActivityForEditor) => { - const { activityWithConnection } = makeActivityWithConnection( - activityToCreate as any, // TODO: fix type - ); - - await createOneActivity?.( - { - ...activityWithConnection, - updatedAt: new Date().toISOString(), - }, - { - skipOptimisticEffect: true, - }, - ); + await createOneActivity?.({ + ...activityToCreate, + updatedAt: new Date().toISOString(), + }); const activityTargetsToCreate = activityToCreate.activityTargets ?? []; if (isNonEmptyArray(activityTargetsToCreate)) { - await createManyActivityTargets(activityTargetsToCreate, { - skipOptimisticEffect: true, - }); + await createManyActivityTargets(activityTargetsToCreate); } }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts b/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts deleted file mode 100644 index f4cd5914bd..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useApolloClient } from '@apollo/client'; - -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; -import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; -import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; - -// TODO: this should be useDeleteRecordFromCache -export const useDeleteActivityFromCache = () => { - const { makeActivityWithConnection } = useActivityConnectionUtils(); - - const apolloClient = useApolloClient(); - - const { objectMetadataItem: objectMetadataItemActivity } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const { objectMetadataItems } = useObjectMetadataItems(); - - const deleteActivityFromCache = (activityToDelete: ActivityForEditor) => { - const { activityWithConnection } = makeActivityWithConnection( - activityToDelete as any, // TODO: fix type - ); - - triggerDeleteRecordsOptimisticEffect({ - cache: apolloClient.cache, - objectMetadataItem: objectMetadataItemActivity, - objectMetadataItems, - recordsToDelete: [activityWithConnection], - }); - }; - - return { - deleteActivityFromCache, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts deleted file mode 100644 index eb07c0ddce..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; - -import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { OrderByField } from '@/object-metadata/types/OrderByField'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { sortByAscString } from '~/utils/array/sortByAscString'; - -// TODO: create a generic hook from this -export const useInjectIntoActivitiesQueries = () => { - const { objectMetadataItem: objectMetadataItemActivity } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const { - upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const injectActivitiesQueries = ({ - activityToInject, - activityTargetsToInject, - targetableObjects, - activitiesFilters, - activitiesOrderByVariables, - injectOnlyInIdFilter, - }: { - activityToInject: Activity; - activityTargetsToInject: ActivityTarget[]; - targetableObjects: ActivityTargetableObject[]; - activitiesFilters?: ObjectRecordQueryFilter; - activitiesOrderByVariables?: OrderByField; - injectOnlyInIdFilter?: boolean; - }) => { - const hasActivityTargets = isNonEmptyArray(targetableObjects); - - if (hasActivityTargets) { - const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ - targetableObjects, - }); - - const findManyActivitiyTargetsQueryVariables = { - filter: findManyActivitiyTargetsQueryFilter, - }; - - const existingActivityTargetsWithMaybeDuplicates = - readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivitiyTargetsQueryVariables, - }); - - const existingActivityTargetsWithoutDuplicates: ObjectRecord[] = - existingActivityTargetsWithMaybeDuplicates.filter( - (existingActivityTarget) => - !activityTargetsToInject.some( - (activityTargetToInject) => - activityTargetToInject.id === existingActivityTarget.id, - ), - ); - - const existingActivityIdsFromTargets = - existingActivityTargetsWithoutDuplicates - ?.map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString); - - const currentFindManyActivitiesQueryVariables = { - filter: { - id: { - in: [...existingActivityIdsFromTargets].sort(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const existingActivities = readFindManyActivitiesQueryInCache({ - queryVariables: currentFindManyActivitiesQueryVariables, - }); - - const nextActivityIds = [ - ...existingActivityIdsFromTargets, - activityToInject.id, - ]; - - const nextFindManyActivitiesQueryVariables = { - filter: { - id: { - in: [...nextActivityIds].sort(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const newActivities = [...existingActivities]; - - if (!injectOnlyInIdFilter) { - const newActivity = { - ...activityToInject, - __typename: 'Activity', - }; - - newActivities.unshift(newActivity); - } - - overwriteFindManyActivitiesInCache({ - objectRecordsToOverwrite: newActivities, - queryVariables: nextFindManyActivitiesQueryVariables, - }); - } else { - const currentFindManyActivitiesQueryVariables = { - filter: { - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const existingActivities = readFindManyActivitiesQueryInCache({ - queryVariables: currentFindManyActivitiesQueryVariables, - }); - - const nextFindManyActivitiesQueryVariables = { - filter: { - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const newActivities = [...existingActivities]; - - if (!injectOnlyInIdFilter) { - const newActivity = { - ...activityToInject, - __typename: 'Activity', - }; - - newActivities.unshift(newActivity); - } - - overwriteFindManyActivitiesInCache({ - objectRecordsToOverwrite: newActivities, - queryVariables: nextFindManyActivitiesQueryVariables, - }); - } - }; - - return { - injectActivitiesQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts deleted file mode 100644 index 195d21cbf8..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { isNonEmptyArray } from '@sniptt/guards'; - -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - -// TODO: create a generic hook from this -export const useInjectIntoActivityTargetsQueries = () => { - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const { - upsertFindManyRecordsQueryInCache: - overwriteFindManyActivityTargetsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const injectActivityTargetsQueries = ({ - activityTargetsToInject, - targetableObjects, - }: { - activityTargetsToInject: ActivityTarget[]; - targetableObjects: ActivityTargetableObject[]; - }) => { - const hasActivityTargets = isNonEmptyArray(targetableObjects); - - if (!hasActivityTargets) { - return; - } - - const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ - targetableObjects, - }); - - const findManyActivitiyTargetsQueryVariables = { - filter: findManyActivitiyTargetsQueryFilter, - }; - - const existingActivityTargetsWithMaybeDuplicates = - readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivitiyTargetsQueryVariables, - }); - - const existingActivityTargetsWithoutDuplicates: ObjectRecord[] = - existingActivityTargetsWithMaybeDuplicates.filter( - (existingActivityTarget) => - !activityTargetsToInject.some( - (activityTargetToInject) => - activityTargetToInject.id === existingActivityTarget.id, - ), - ); - - const newActivityTargets = [ - ...existingActivityTargetsWithoutDuplicates, - ...activityTargetsToInject, - ]; - - overwriteFindManyActivityTargetsQueryInCache({ - objectRecordsToOverwrite: newActivityTargets, - queryVariables: findManyActivitiyTargetsQueryVariables, - depth: 2, - }); - }; - - return { - injectActivityTargetsQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts b/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts deleted file mode 100644 index 84683c6157..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useApolloClient } from '@apollo/client'; - -import { Activity } from '@/activities/types/Activity'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; -import { getCacheReferenceFromRecord } from '@/object-record/cache/utils/getCacheReferenceFromRecord'; - -export const useModifyActivityOnActivityTargetsCache = () => { - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const modifyActivityTargetFromCache = useModifyRecordFromCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const apolloClient = useApolloClient(); - - const modifyActivityOnActivityTargetsCache = ({ - activityTargetIds, - activity, - }: { - activityTargetIds: string[]; - activity: Activity; - }) => { - for (const activityTargetId of activityTargetIds) { - modifyActivityTargetFromCache(activityTargetId, { - activity: () => { - const newActivityReference = getCacheReferenceFromRecord({ - apolloClient, - objectNameSingular: CoreObjectNameSingular.Activity, - record: activity, - }); - - return newActivityReference; - }, - }); - } - }; - - return { - modifyActivityOnActivityTargetsCache, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts b/packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts deleted file mode 100644 index 357fae472c..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useApolloClient } from '@apollo/client'; - -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; -import { getCachedRecordEdgesFromRecords } from '@/object-record/cache/utils/getCachedRecordEdgesFromRecords'; - -export const useModifyActivityTargetsOnActivityCache = () => { - const { objectMetadataItem: objectMetadataItemActivity } = - useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const modifyActivityFromCache = useModifyRecordFromCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const apolloClient = useApolloClient(); - - const modifyActivityTargetsOnActivityCache = ({ - activityId, - activityTargets, - }: { - activityId: string; - activityTargets: ActivityTarget[]; - }) => { - modifyActivityFromCache(activityId, { - activityTargets: ( - activityTargetsCachedConnection: CachedObjectRecordConnection, - ) => { - const newActivityTargetsCachedRecordEdges = - getCachedRecordEdgesFromRecords({ - apolloClient, - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - records: activityTargets, - }); - - return { - ...activityTargetsCachedConnection, - edges: newActivityTargetsCachedRecordEdges, - }; - }, - }); - }; - - return { - modifyActivityTargetsOnActivityCache, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index bcd10fcd77..5214736270 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -51,7 +51,7 @@ export const useOpenCreateActivityDrawer = () => { }) => { const { createdActivityInCache } = createActivityInCache({ type, - targetableObjects, + targetObject: targetableObjects[0], customAssignee, }); diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts deleted file mode 100644 index 90713931b6..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { ActivityType } from '@/activities/types/Activity'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; -import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; -import { isDefined } from '~/utils/isDefined'; - -import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; - -export const useOpenCreateActivityDrawerForSelectedRowIds = ( - recordTableId: string, -) => { - const openCreateActivityDrawer = useOpenCreateActivityDrawer(); - - const { selectedRowIdsSelector } = useRecordTableStates(recordTableId); - - return useRecoilCallback( - ({ snapshot }) => - ( - type: ActivityType, - objectNameSingular: string, - relatedEntities?: ActivityTargetableObject[], - ) => { - const selectedRowIds = getSnapshotValue( - snapshot, - selectedRowIdsSelector(), - ); - - let activityTargetableObjectArray: ActivityTargetableObject[] = - selectedRowIds - .map((recordId: string) => { - const targetObjectRecord = getSnapshotValue( - snapshot, - recordStoreFamilyState(recordId), - ); - - if (!targetObjectRecord) { - return null; - } - - return { - type: 'Custom', - targetObjectNameSingular: objectNameSingular, - id: recordId, - targetObjectRecord, - }; - }) - .filter(isDefined); - - if (isDefined(relatedEntities)) { - activityTargetableObjectArray = - activityTargetableObjectArray.concat(relatedEntities); - } - - openCreateActivityDrawer({ - type, - targetableObjects: activityTargetableObjectArray, - }); - }, - [selectedRowIdsSelector, openCreateActivityDrawer], - ); -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts new file mode 100644 index 0000000000..c6771ec7eb --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts @@ -0,0 +1,123 @@ +import { useApolloClient } from '@apollo/client'; + +import { FIND_MANY_ACTIVITIES_QUERY_KEY } from '@/activities/query-keys/FindManyActivitiesQueryKey'; +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortByAscString } from '~/utils/array/sortByAscString'; +import { isDefined } from '~/utils/isDefined'; + +export const usePrepareFindManyActivitiesQuery = () => { + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const getActivityFromCache = useGetRecordFromCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const cache = useApolloClient().cache; + const { objectMetadataItems } = useObjectMetadataItems(); + + const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } = + useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const prepareFindManyActivitiesQuery = ({ + targetableObject, + additionalFilter, + shouldActivityBeExcluded, + }: { + additionalFilter?: Record; + targetableObject: ActivityTargetableObject; + shouldActivityBeExcluded?: (activityTarget: Activity) => boolean; + }) => { + const targetableObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === + targetableObject.targetObjectNameSingular, + ); + + if (!targetableObjectMetadataItem) { + throw new Error( + `Cannot find object metadata item for targetable object ${targetableObject.targetObjectNameSingular}`, + ); + } + + const targetableObjectRecord = getRecordFromCache({ + recordId: targetableObject.id, + objectMetadataItem: targetableObjectMetadataItem, + objectMetadataItems, + cache, + }); + + const activityTargets: ActivityTarget[] = + targetableObjectRecord?.activityTargets ?? []; + + const activityTargetIds = [ + ...new Set( + activityTargets + .map((activityTarget) => activityTarget.id) + .filter(isDefined), + ), + ]; + + const activities: Activity[] = activityTargetIds + .map((activityTargetId) => { + const activityTarget = activityTargets.find( + (activityTarget) => activityTarget.id === activityTargetId, + ); + + if (!activityTarget) { + return undefined; + } + + return getActivityFromCache(activityTarget.activityId); + }) + .filter(isDefined); + + const activityIds = [...new Set(activities.map((activity) => activity.id))]; + + const nextFindManyActivitiesQueryFilter = { + filter: { + id: { + in: [...activityIds].sort(sortByAscString), + }, + ...additionalFilter, + }, + }; + + const filteredActivities = [ + ...activities.filter( + (activity) => !shouldActivityBeExcluded?.(activity) ?? true, + ), + ].sort((a, b) => { + return a.createdAt > b.createdAt ? -1 : 1; + }); + + upsertFindManyActivitiesInCache({ + objectRecordsToOverwrite: filteredActivities, + queryVariables: { + ...nextFindManyActivitiesQueryFilter, + orderBy: { createdAt: 'DescNullsFirst' }, + }, + depth: FIND_MANY_ACTIVITIES_QUERY_KEY.depth, + queryFields: + FIND_MANY_ACTIVITIES_QUERY_KEY.fieldsFactory?.(objectMetadataItems), + computeReferences: true, + }); + }; + + return { + prepareFindManyActivitiesQuery, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts new file mode 100644 index 0000000000..cbf0e2f419 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts @@ -0,0 +1,49 @@ +import { useRecoilValue } from 'recoil'; + +import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; +import { Activity } from '@/activities/types/Activity'; +import { isDefined } from '~/utils/isDefined'; + +// This hook should only be executed if the normalized cache is up-to-date +// It will take a targetableObject and prepare the queries for the activities +// based on the activityTargets of the targetableObject +export const useRefreshShowPageFindManyActivitiesQueries = () => { + const objectShowPageTargetableObject = useRecoilValue( + objectShowPageTargetableObjectState, + ); + + const { prepareFindManyActivitiesQuery } = + usePrepareFindManyActivitiesQuery(); + + const refreshShowPageFindManyActivitiesQueries = () => { + if (isDefined(objectShowPageTargetableObject)) { + prepareFindManyActivitiesQuery({ + targetableObject: objectShowPageTargetableObject, + }); + prepareFindManyActivitiesQuery({ + targetableObject: objectShowPageTargetableObject, + additionalFilter: { + completedAt: { is: 'NULL' }, + type: { eq: 'Task' }, + }, + shouldActivityBeExcluded: (activity: Activity) => { + return activity.type !== 'Task'; + }, + }); + prepareFindManyActivitiesQuery({ + targetableObject: objectShowPageTargetableObject, + additionalFilter: { + type: { eq: 'Note' }, + }, + shouldActivityBeExcluded: (activity: Activity) => { + return activity.type !== 'Note'; + }, + }); + } + }; + + return { + refreshShowPageFindManyActivitiesQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts deleted file mode 100644 index e65b8058c0..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; - -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { OrderByField } from '@/object-metadata/types/OrderByField'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; -import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; -import { sortByAscString } from '~/utils/array/sortByAscString'; - -// TODO: improve, no bug if query to inject doesn't exist -export const useRemoveFromActivitiesQueries = () => { - const { objectMetadataItem: objectMetadataItemActivity } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const { - upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const removeFromActivitiesQueries = ({ - activityIdToRemove, - targetableObjects, - activitiesFilters, - activitiesOrderByVariables, - }: { - activityIdToRemove: string; - targetableObjects: ActivityTargetableObject[]; - activitiesFilters?: ObjectRecordQueryFilter; - activitiesOrderByVariables?: OrderByField; - }) => { - const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ - targetableObjects, - }); - - const findManyActivityTargetsQueryVariables = { - filter: findManyActivitiyTargetsQueryFilter, - } as ObjectRecordQueryVariables; - - const existingActivityTargetsForTargetableObject = - readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivityTargetsQueryVariables, - }); - - const existingActivityIds = existingActivityTargetsForTargetableObject - ?.map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString); - - const currentFindManyActivitiesQueryVariables = { - filter: { - id: { - in: [...existingActivityIds].sort(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const existingActivities = readFindManyActivitiesQueryInCache({ - queryVariables: currentFindManyActivitiesQueryVariables, - }); - - if (!isNonEmptyArray(existingActivities)) { - return; - } - - const activityIdsAfterRemoval = existingActivityIds.filter( - (existingActivityId) => existingActivityId !== activityIdToRemove, - ); - - const nextFindManyActivitiesQueryVariables = { - filter: { - id: { - in: [...activityIdsAfterRemoval].sort(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const newActivities = existingActivities.filter( - (existingActivity) => existingActivity.id !== activityIdToRemove, - ); - - overwriteFindManyActivitiesInCache({ - objectRecordsToOverwrite: newActivities, - queryVariables: nextFindManyActivitiesQueryVariables, - }); - }; - - return { - removeFromActivitiesQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts deleted file mode 100644 index d95b3c96d4..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { isNonEmptyArray } from '@sniptt/guards'; - -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; - -export const useRemoveFromActivityTargetsQueries = () => { - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const { - upsertFindManyRecordsQueryInCache: - overwriteFindManyActivityTargetsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const removeFromActivityTargetsQueries = ({ - activityTargetsToRemove, - targetableObjects, - }: { - activityTargetsToRemove: ActivityTarget[]; - targetableObjects: ActivityTargetableObject[]; - }) => { - const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ - targetableObjects, - }); - - const findManyActivityTargetsQueryVariables = { - filter: findManyActivitiyTargetsQueryFilter, - } as ObjectRecordQueryVariables; - - const existingActivityTargetsForTargetableObject = - readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivityTargetsQueryVariables, - }); - - const newActivityTargetsForTargetableObject = isNonEmptyArray( - activityTargetsToRemove, - ) - ? existingActivityTargetsForTargetableObject.filter( - (existingActivityTarget) => - activityTargetsToRemove.some( - (activityTargetToRemove) => - activityTargetToRemove.id !== existingActivityTarget.id, - ), - ) - : existingActivityTargetsForTargetableObject; - - overwriteFindManyActivityTargetsQueryInCache({ - objectRecordsToOverwrite: newActivityTargetsForTargetableObject, - queryVariables: findManyActivityTargetsQueryVariables, - depth: 2, - }); - }; - - return { - removeFromActivityTargetsQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts index c383d22dfe..6365199216 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -1,24 +1,16 @@ -import { useLocation } from 'react-router-dom'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; -import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; -import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries'; -import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; +import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; -import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; -import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; -import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries'; import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { Activity } from '@/activities/types/Activity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { isDefined } from '~/utils/isDefined'; -// TODO: create a generic way to have records only in cache for create mode and delete them afterwards ? export const useUpsertActivity = () => { const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState( isActivityInCreateModeState, @@ -40,31 +32,8 @@ export const useUpsertActivity = () => { objectShowPageTargetableObjectState, ); - const { injectActivitiesQueries } = useInjectIntoActivitiesQueries(); - const { injectActivityTargetsQueries } = - useInjectIntoActivityTargetsQueries(); - - const { pathname } = useLocation(); - - const weAreOnObjectShowPage = pathname.startsWith('/object'); - const weAreOnTaskPage = pathname.startsWith('/tasks'); - - const { injectIntoTimelineActivitiesQueries } = - useInjectIntoTimelineActivitiesQueries(); - - const { makeActivityWithConnection } = useActivityConnectionUtils(); - - const currentCompletedTaskQueryVariables = useRecoilValue( - currentCompletedTaskQueryVariablesState, - ); - - const currentIncompleteTaskQueryVariables = useRecoilValue( - currentIncompleteTaskQueryVariablesState, - ); - - const currentNotesQueryVariables = useRecoilValue( - currentNotesQueryVariablesState, - ); + const { refreshShowPageFindManyActivitiesQueries } = + useRefreshShowPageFindManyActivitiesQueries(); const upsertActivity = async ({ activity, @@ -74,103 +43,19 @@ export const useUpsertActivity = () => { input: Partial; }) => { setIsUpsertingActivityInDB(true); - if (isActivityInCreateMode) { const activityToCreate: Activity = { ...activity, ...input, }; - const { activityWithConnection } = - makeActivityWithConnection(activityToCreate); - - if (weAreOnTaskPage) { - if (isDefined(activityWithConnection.completedAt)) { - injectActivitiesQueries({ - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - activityTargetsToInject: activityToCreate.activityTargets, - activityToInject: activityWithConnection, - targetableObjects: [], - }); - } else { - injectActivitiesQueries({ - activitiesFilters: currentIncompleteTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy, - activityTargetsToInject: activityToCreate.activityTargets, - activityToInject: activityWithConnection, - targetableObjects: [], - }); - } - - injectActivityTargetsQueries({ - activityTargetsToInject: activityToCreate.activityTargets, - targetableObjects: [], - }); - } - - // Call optimistic effects - if (weAreOnObjectShowPage && isDefined(objectShowPageTargetableObject)) { - injectIntoTimelineActivitiesQueries({ - timelineTargetableObject: objectShowPageTargetableObject, - activityToInject: activityWithConnection, - activityTargetsToInject: activityToCreate.activityTargets, - }); - - const injectOnlyInIdFilterForTaskQueries = - activityWithConnection.type !== 'Task'; - - const injectOnlyInIdFilterForNotesQueries = - activityWithConnection.type !== 'Note'; - - if (isDefined(currentCompletedTaskQueryVariables)) { - injectActivitiesQueries({ - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - activityTargetsToInject: activityToCreate.activityTargets, - activityToInject: activityWithConnection, - targetableObjects: [objectShowPageTargetableObject], - injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries, - }); - } - - if (isDefined(currentIncompleteTaskQueryVariables)) { - injectActivitiesQueries({ - activitiesFilters: - currentIncompleteTaskQueryVariables?.filter ?? {}, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy ?? {}, - activityTargetsToInject: activityToCreate.activityTargets, - activityToInject: activityWithConnection, - targetableObjects: [objectShowPageTargetableObject], - injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries, - }); - } - - if (isDefined(currentNotesQueryVariables)) { - injectActivitiesQueries({ - activitiesFilters: currentNotesQueryVariables?.filter, - activitiesOrderByVariables: currentNotesQueryVariables?.orderBy, - activityTargetsToInject: activityToCreate.activityTargets, - activityToInject: activityWithConnection, - targetableObjects: [objectShowPageTargetableObject], - injectOnlyInIdFilter: injectOnlyInIdFilterForNotesQueries, - }); - } - - injectActivityTargetsQueries({ - activityTargetsToInject: activityToCreate.activityTargets, - targetableObjects: [objectShowPageTargetableObject], - }); + if (isDefined(objectShowPageTargetableObject)) { + refreshShowPageFindManyActivitiesQueries(); } await createActivityInDB(activityToCreate); setActivityIdInDrawer(activityToCreate.id); - setIsActivityInCreateMode(false); } else { await updateOneActivity?.({ diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx index 3275f09233..bb971eb92b 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx @@ -1,23 +1,25 @@ import styled from '@emotion/styled'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { useRecoilState } from 'recoil'; +import { isNonEmptyArray, isNull } from '@sniptt/guards'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { v4 } from 'uuid'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; -import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; +import { getActivityTargetObjectFieldName } from '@/activities/utils/getActivityTargetObjectFieldName'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; +import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect'; import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { prefillRecord } from '@/object-record/utils/prefillRecord'; const StyledSelectContainer = styled.div` left: 0px; @@ -38,7 +40,7 @@ export const ActivityTargetInlineCellEditMode = ({ const selectedTargetObjectIds = activityTargetWithTargetRecords.map( (activityTarget) => ({ - objectNameSingular: activityTarget.targetObjectNameSingular, + objectNameSingular: activityTarget.targetObjectMetadataItem.nameSingular, id: activityTarget.targetObject.id, }), ); @@ -63,12 +65,13 @@ export const ActivityTargetInlineCellEditMode = ({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, }); - const { injectIntoActivityTargetInlineCellCache } = - useInjectIntoActivityTargetInlineCellCache(); + const setActivityFromStore = useSetRecoilState( + recordStoreFamilyState(activity.id), + ); - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem: objectMetadataItemActivityTarget, + const { createManyRecordsInCache: createManyActivityTargetsInCache } = + useCreateManyRecordsInCache({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, }); const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => { @@ -100,17 +103,22 @@ export const ActivityTargetInlineCellEditMode = ({ const activityTargetsToCreate = selectedTargetObjectsToCreate.map( (selectedRecord) => { - const emptyActivityTarget = - generateObjectRecordOptimisticResponse({ + const emptyActivityTarget = prefillRecord({ + objectMetadataItem: objectMetadataItemActivityTarget, + input: { id: v4(), activityId: activity.id, activity, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), + [getActivityTargetObjectFieldName({ + nameSingular: selectedRecord.objectMetadataItem.nameSingular, + })]: selectedRecord.record, [getActivityTargetObjectFieldIdName({ nameSingular: selectedRecord.objectMetadataItem.nameSingular, })]: selectedRecord.recordIdentifier.id, - }); + }, + }); return emptyActivityTarget; }, @@ -128,12 +136,8 @@ export const ActivityTargetInlineCellEditMode = ({ ); } - injectIntoActivityTargetInlineCellCache({ - activityId: activity.id, - activityTargetsToInject: activityTargetsAfterUpdate, - }); - if (isActivityInCreateMode) { + createManyActivityTargetsInCache(activityTargetsToCreate); upsertActivity({ activity, input: { @@ -142,9 +146,7 @@ export const ActivityTargetInlineCellEditMode = ({ }); } else { if (activityTargetsToCreate.length > 0) { - await createManyActivityTargets(activityTargetsToCreate, { - skipOptimisticEffect: true, - }); + await createManyActivityTargets(activityTargetsToCreate); } if (activityTargetsToDelete.length > 0) { @@ -153,12 +155,20 @@ export const ActivityTargetInlineCellEditMode = ({ (activityTargetObjectRecord) => activityTargetObjectRecord.activityTarget.id, ), - { - skipOptimisticEffect: true, - }, ); } } + + setActivityFromStore((currentActivity) => { + if (isNull(currentActivity)) { + return null; + } + + return { + ...currentActivity, + activityTargets: activityTargetsAfterUpdate, + }; + }); }; const handleCancel = () => { diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index d1b09c7372..e63787053b 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -18,9 +18,8 @@ type ActivityTargetsInlineCellProps = { export const ActivityTargetsInlineCell = ({ activity, }: ActivityTargetsInlineCellProps) => { - const { activityTargetObjectRecords } = useActivityTargetObjectRecords({ - activityId: activity?.id ?? '', - }); + const { activityTargetObjectRecords } = + useActivityTargetObjectRecords(activity); const { closeInlineCell } = useInlineCell(); useScopedHotkeys( diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useInjectIntoActivityTargetInlineCellCache.test.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useInjectIntoActivityTargetInlineCellCache.test.ts deleted file mode 100644 index 89b990973b..0000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useInjectIntoActivityTargetInlineCellCache.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { renderHook } from '@testing-library/react'; - -import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; -import { Activity } from '@/activities/types/Activity'; - -jest.mock('@/object-metadata/hooks/useObjectMetadataItemOnly', () => ({ - useObjectMetadataItemOnly: jest.fn(() => ({ - objectMetadataItem: { exampleMetadataItem: 'example' }, - })), -})); - -jest.mock( - '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache', - () => ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(() => ({ - upsertFindManyRecordsQueryInCache: jest.fn(), - })), - }), -); - -describe('useInjectIntoActivityTargetInlineCellCache', () => { - it('should inject into activity target inline cell cache as expected', () => { - const { result } = renderHook(() => - useInjectIntoActivityTargetInlineCellCache(), - ); - - const { injectIntoActivityTargetInlineCellCache } = result.current; - - const mockActivityId = 'mockId'; - const mockActivityTargetsToInject = [ - { - id: '1', - name: 'Example Activity Target', - createdAt: '2022-01-01', - updatedAt: '2022-01-01', - activity: { - id: '1', - createdAt: '2022-01-01', - updatedAt: '2022-01-01', - } as Pick, - }, - ]; - injectIntoActivityTargetInlineCellCache({ - activityId: mockActivityId, - activityTargetsToInject: mockActivityTargetsToInject, - }); - - expect( - result.current.injectIntoActivityTargetInlineCellCache, - ).toBeDefined(); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts deleted file mode 100644 index f3de9a2041..0000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; - -export const useInjectIntoActivityTargetInlineCellCache = () => { - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - upsertFindManyRecordsQueryInCache: - overwriteFindManyActivityTargetsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const injectIntoActivityTargetInlineCellCache = ({ - activityId, - activityTargetsToInject, - }: { - activityId: string; - activityTargetsToInject: ActivityTarget[]; - }) => { - const activityTargetInlineCellQueryVariables = { - filter: { - activityId: { - eq: activityId, - }, - }, - }; - - overwriteFindManyActivityTargetsQueryInCache({ - queryVariables: activityTargetInlineCellQueryVariables, - objectRecordsToOverwrite: activityTargetsToInject, - depth: 2, - }); - }; - - return { - injectIntoActivityTargetInlineCellCache, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/query-keys/CreateOneActivityQueryKey.ts b/packages/twenty-front/src/modules/activities/query-keys/CreateOneActivityQueryKey.ts new file mode 100644 index 0000000000..acec84ba75 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/query-keys/CreateOneActivityQueryKey.ts @@ -0,0 +1,34 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; + +export const CREATE_ONE_ACTIVITY_QUERY_KEY: QueryKey = { + objectNameSingular: CoreObjectNameSingular.Activity, + variables: {}, + fields: { + id: true, + __typename: true, + createdAt: true, + updatedAt: true, + author: { + id: true, + name: true, + __typename: true, + }, + authorId: true, + assigneeId: true, + assignee: { + id: true, + name: true, + __typename: true, + }, + comments: true, + attachments: true, + body: true, + title: true, + completedAt: true, + dueAt: true, + reminderAt: true, + type: true, + }, + depth: 1, +}; diff --git a/packages/twenty-front/src/modules/activities/query-keys/FindManyActivitiesQueryKey.ts b/packages/twenty-front/src/modules/activities/query-keys/FindManyActivitiesQueryKey.ts new file mode 100644 index 0000000000..09e6b04d64 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/query-keys/FindManyActivitiesQueryKey.ts @@ -0,0 +1,38 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; + +export const FIND_MANY_ACTIVITIES_QUERY_KEY: QueryKey = { + objectNameSingular: CoreObjectNameSingular.Activity, + variables: {}, + fieldsFactory: (_objectMetadataItems: ObjectMetadataItem[]) => { + return { + id: true, + __typename: true, + createdAt: true, + updatedAt: true, + author: { + id: true, + name: true, + __typename: true, + }, + authorId: true, + assigneeId: true, + assignee: { + id: true, + name: true, + __typename: true, + }, + comments: true, + attachments: true, + body: true, + title: true, + completedAt: true, + dueAt: true, + reminderAt: true, + type: true, + activityTargets: true, + }; + }, + depth: 2, +}; diff --git a/packages/twenty-front/src/modules/activities/query-keys/FindManyActivityTargetsQueryKey.ts b/packages/twenty-front/src/modules/activities/query-keys/FindManyActivityTargetsQueryKey.ts new file mode 100644 index 0000000000..b0d34ff84a --- /dev/null +++ b/packages/twenty-front/src/modules/activities/query-keys/FindManyActivityTargetsQueryKey.ts @@ -0,0 +1,21 @@ +import { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; + +export const FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY: QueryKey = { + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + variables: {}, + fieldsFactory: (objectMetadataItems: ObjectMetadataItem[]) => { + return { + id: true, + __typename: true, + createdAt: true, + updatedAt: true, + activity: true, + activityId: true, + ...generateActivityTargetMorphFieldKeys(objectMetadataItems), + }; + }, + depth: 1, +}; diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx index 40ed872f97..874598a786 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx @@ -1,25 +1,20 @@ -import { useLocation } from 'react-router-dom'; import styled from '@emotion/styled'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; -import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries'; -import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries'; -import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; +import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; -import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; -import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy'; import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -56,7 +51,12 @@ export const ActivityActionBar = () => { const [temporaryActivityForEditor, setTemporaryActivityForEditor] = useRecoilState(temporaryActivityForEditorState); - const { deleteActivityFromCache } = useDeleteActivityFromCache(); + const deleteActivityFromCache = useDeleteRecordFromCache({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + const deleteActivityTargetFromCache = useDeleteRecordFromCache({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState); const [isUpsertingActivityInDB] = useRecoilState( @@ -67,28 +67,11 @@ export const ActivityActionBar = () => { objectShowPageTargetableObjectState, ); + const { refreshShowPageFindManyActivitiesQueries } = + useRefreshShowPageFindManyActivitiesQueries(); + const openCreateActivity = useOpenCreateActivityDrawer(); - const currentCompletedTaskQueryVariables = useRecoilValue( - currentCompletedTaskQueryVariablesState, - ); - - const currentIncompleteTaskQueryVariables = useRecoilValue( - currentIncompleteTaskQueryVariablesState, - ); - - const currentNotesQueryVariables = useRecoilValue( - currentNotesQueryVariablesState, - ); - - const { pathname } = useLocation(); - const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries(); - const { removeFromActivityTargetsQueries } = - useRemoveFromActivityTargetsQueries(); - - const weAreOnObjectShowPage = pathname.startsWith('/object'); - const weAreOnTaskPage = pathname.startsWith('/tasks'); - const deleteActivity = useRecoilCallback( ({ snapshot }) => async () => { @@ -108,105 +91,46 @@ export const ActivityActionBar = () => { setIsRightDrawerOpen(false); - if (isNonEmptyString(viewableActivityId)) { - if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) { - deleteActivityFromCache(temporaryActivityForEditor); - setTemporaryActivityForEditor(null); - } else if (isNonEmptyString(activityIdInDrawer)) { - const activityTargetIdsToDelete: string[] = - activityTargets.map(mapToRecordId) ?? []; + if (!isNonEmptyString(viewableActivityId)) { + return; + } - if (weAreOnTaskPage) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [], - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - }); + if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) { + deleteActivityFromCache(temporaryActivityForEditor); + setTemporaryActivityForEditor(null); + return; + } - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [], - activitiesFilters: currentIncompleteTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy, - }); - } else if ( - weAreOnObjectShowPage && - isDefined(objectShowPageTargetableObject) - ) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: {}, - activitiesOrderByVariables: - FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, - }); + if (isNonEmptyString(activityIdInDrawer)) { + const activityTargetIdsToDelete: string[] = + activityTargets.map(mapToRecordId) ?? []; - if (isDefined(currentCompletedTaskQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - }); - } + deleteActivityFromCache(activity); + activityTargets.forEach((activityTarget: ActivityTarget) => { + deleteActivityTargetFromCache(activityTarget); + }); - if (isDefined(currentIncompleteTaskQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: - currentIncompleteTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy, - }); - } + refreshShowPageFindManyActivitiesQueries(); - if (isDefined(currentNotesQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: currentNotesQueryVariables?.filter, - activitiesOrderByVariables: - currentNotesQueryVariables?.orderBy, - }); - } - - removeFromActivityTargetsQueries({ - activityTargetsToRemove: activity?.activityTargets ?? [], - targetableObjects: [objectShowPageTargetableObject], - }); - } - - if (isNonEmptyArray(activityTargetIdsToDelete)) { - await deleteManyActivityTargets(activityTargetIdsToDelete); - } - - await deleteOneActivity?.(viewableActivityId); + if (isNonEmptyArray(activityTargetIdsToDelete)) { + await deleteManyActivityTargets(activityTargetIdsToDelete); } + + await deleteOneActivity?.(viewableActivityId); } }, [ activityIdInDrawer, - currentCompletedTaskQueryVariables, - currentIncompleteTaskQueryVariables, - currentNotesQueryVariables, - deleteActivityFromCache, - deleteManyActivityTargets, - deleteOneActivity, - isActivityInCreateMode, - objectShowPageTargetableObject, - removeFromActivitiesQueries, - removeFromActivityTargetsQueries, - setTemporaryActivityForEditor, - temporaryActivityForEditor, - viewableActivityId, - weAreOnObjectShowPage, - weAreOnTaskPage, setIsRightDrawerOpen, + viewableActivityId, + isActivityInCreateMode, + temporaryActivityForEditor, + deleteActivityFromCache, + setTemporaryActivityForEditor, + refreshShowPageFindManyActivitiesQueries, + deleteOneActivity, + deleteActivityTargetFromCache, + deleteManyActivityTargets, ], ); diff --git a/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx index 3f82be635b..802e03bb6b 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx @@ -3,6 +3,7 @@ import { DateTime } from 'luxon'; import { useRecoilState, useRecoilValue } from 'recoil'; import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState'; +import { Activity } from '@/activities/types/Activity'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -15,7 +16,7 @@ export const CurrentUserDueTaskCountEffect = () => { currentUserDueTaskCountState, ); - const { records: tasks } = useFindManyRecords({ + const { records: tasks } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Activity, depth: 0, filter: { diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index b5e1501a19..62c552c8e7 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -78,9 +78,7 @@ export const TaskRow = ({ task }: { task: Activity }) => { const body = getActivitySummary(task.body); const { completeTask } = useCompleteTask(task); - const { activityTargetObjectRecords } = useActivityTargetObjectRecords({ - activityId: task.id, - }); + const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task); return ( ({ - useUpsertFindManyRecordsQueryInCache: jest.fn(), - }), -); - -(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({ - upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock, -})); - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -describe('useInjectIntoTimelineActivitiesQueries', () => { - it('works as expected', () => { - const { result } = renderHook( - () => useInjectIntoTimelineActivitiesQueries(), - { wrapper: Wrapper }, - ); - - act(() => { - result.current.injectIntoTimelineActivitiesQueries({ - activityToInject: mockedActivities[0], - activityTargetsToInject: [], - timelineTargetableObject: { - id: '123', - targetObjectNameSingular: 'person', - }, - }); - }); - - expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts deleted file mode 100644 index 19539090e0..0000000000 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; -import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; - -export const useInjectIntoTimelineActivitiesQueries = () => { - const { injectActivitiesQueries } = useInjectIntoActivitiesQueries(); - - const injectIntoTimelineActivitiesQueries = ({ - activityToInject, - activityTargetsToInject, - timelineTargetableObject, - }: { - activityToInject: Activity; - activityTargetsToInject: ActivityTarget[]; - timelineTargetableObject: ActivityTargetableObject; - }) => { - injectActivitiesQueries({ - activitiesFilters: {}, - activitiesOrderByVariables: { - createdAt: 'DescNullsFirst', - }, - activityTargetsToInject, - activityToInject, - targetableObjects: [timelineTargetableObject], - }); - }; - - return { - injectIntoTimelineActivitiesQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts index 0af9d49312..f114b3f8d9 100644 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts @@ -2,14 +2,12 @@ import { useEffect, useState } from 'react'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useRecoilState } from 'recoil'; -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { sortByAscString } from '~/utils/array/sortByAscString'; @@ -20,8 +18,6 @@ export const useTimelineActivities = ({ }: { targetableObject: ActivityTargetableObject; }) => { - const { makeActivityWithoutConnection } = useActivityConnectionUtils(); - const [, setObjectShowPageTargetableObject] = useRecoilState( objectShowPageTargetableObjectState, ); @@ -60,7 +56,7 @@ export const useTimelineActivities = ({ }, ); - const { records: activitiesWithConnection, loading: loadingActivities } = + const { records: activities, loading: loadingActivities } = useFindManyRecords({ skip: loadingActivityTargets || !isNonEmptyArray(activityTargets), objectNameSingular: CoreObjectNameSingular.Activity, @@ -68,15 +64,11 @@ export const useTimelineActivities = ({ orderBy: timelineActivitiesQueryVariables.orderBy, onCompleted: useRecoilCallback( ({ set }) => - (data) => { + (activities) => { if (!initialized) { setInitialized(true); } - const activities = getRecordsFromRecordConnection({ - recordConnection: data, - }); - for (const activity of activities) { set(recordStoreFamilyState(activity.id), activity); } @@ -97,11 +89,6 @@ export const useTimelineActivities = ({ const loading = loadingActivities || loadingActivityTargets; - const activities = activitiesWithConnection - ?.map(makeActivityWithoutConnection as any) - .map(({ activity }: any) => activity as any) - .filter(isDefined); - return { activities, loading, diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts index ad119af39b..382ce817ea 100644 --- a/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts +++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetObject.ts @@ -6,5 +6,4 @@ export type ActivityTargetWithTargetRecord = { targetObjectMetadataItem: ObjectMetadataItem; activityTarget: ActivityTarget; targetObject: ObjectRecord; - targetObjectNameSingular: string; }; diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts index 22d25858f1..72b17fb37a 100644 --- a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts +++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts @@ -1,5 +1,4 @@ export type ActivityTargetableObject = { id: string; targetObjectNameSingular: string; - relatedTargetableObjects?: ActivityTargetableObject[]; }; diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts deleted file mode 100644 index 489364a1e1..0000000000 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getTargetableEntitiesWithParents.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects'; - -describe('getTargetableEntitiesWithParents', () => { - it('should return the correct value', () => { - const entities: ActivityTargetableObject[] = [ - { - id: '1', - targetObjectNameSingular: 'person', - relatedTargetableObjects: [ - { - id: '2', - targetObjectNameSingular: 'company', - }, - ], - }, - { - id: '4', - targetObjectNameSingular: 'person', - }, - { - id: '3', - targetObjectNameSingular: 'car', - relatedTargetableObjects: [ - { - id: '6', - targetObjectNameSingular: 'person', - }, - { - id: '5', - targetObjectNameSingular: 'company', - }, - ], - }, - ]; - - const res = - flattenTargetableObjectsAndTheirRelatedTargetableObjects(entities); - - expect(res).toHaveLength(6); - expect(res[0].id).toBe('1'); - expect(res[1].id).toBe('2'); - expect(res[2].id).toBe('4'); - expect(res[3].id).toBe('3'); - expect(res[4].id).toBe('6'); - expect(res[5].id).toBe('5'); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts deleted file mode 100644 index 49256f8bbc..0000000000 --- a/packages/twenty-front/src/modules/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isDefined } from '~/utils/isDefined'; - -import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; - -export const flattenTargetableObjectsAndTheirRelatedTargetableObjects = ( - targetableObjectsWithRelatedTargetableObjects: ActivityTargetableObject[], -): ActivityTargetableObject[] => { - const flattenedTargetableObjects: ActivityTargetableObject[] = []; - - for (const targetableObject of targetableObjectsWithRelatedTargetableObjects ?? - []) { - flattenedTargetableObjects.push(targetableObject); - - if (isDefined(targetableObject.relatedTargetableObjects)) { - for (const relatedEntity of targetableObject.relatedTargetableObjects ?? - []) { - flattenedTargetableObjects.push(relatedEntity); - } - } - } - - return flattenedTargetableObjects; -}; diff --git a/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts b/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts new file mode 100644 index 0000000000..b5994a93e7 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/generateActivityTargetMorphFieldKeys.ts @@ -0,0 +1,31 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const generateActivityTargetMorphFieldKeys = ( + objectMetadataItems: ObjectMetadataItem[], +) => { + const targetableObjects = Object.fromEntries( + objectMetadataItems + .filter( + (objectMetadataItem) => + objectMetadataItem.isActive && !objectMetadataItem.isSystem, + ) + .map((objectMetadataItem) => [objectMetadataItem.nameSingular, true]), + ); + + const targetableObjectIds = Object.fromEntries( + objectMetadataItems + .filter( + (objectMetadataItem) => + objectMetadataItem.isActive && !objectMetadataItem.isSystem, + ) + .map((objectMetadataItem) => [ + `${objectMetadataItem.nameSingular}Id`, + true, + ]), + ); + + return { + ...targetableObjects, + ...targetableObjectIds, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/utils/getTargetObjectFilterFieldName.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/utils/getTargetObjectFilterFieldName.ts rename to packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldIdName.ts diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldName.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldName.ts new file mode 100644 index 0000000000..a4081e2a10 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectFieldName.ts @@ -0,0 +1,7 @@ +export const getActivityTargetObjectFieldName = ({ + nameSingular, +}: { + nameSingular: string; +}) => { + return `${nameSingular}`; +}; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts index c6a53c39ee..5553632b1c 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts @@ -1,5 +1,5 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; export const getActivityTargetsFilter = ({ targetableObjects, diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts index 20b8d009c2..dd20d678d8 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts @@ -1,48 +1,41 @@ import { v4 } from 'uuid'; +import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; export const makeActivityTargetsToCreateFromTargetableObjects = ({ targetableObjects, - activityId, + activity, targetObjectRecords, }: { targetableObjects: ActivityTargetableObject[]; - activityId: string; + activity: Activity; targetObjectRecords: ObjectRecord[]; }): Partial[] => { - const activityTargetableObjects = targetableObjects - ? flattenTargetableObjectsAndTheirRelatedTargetableObjects( - targetableObjects, - ) - : []; + const activityTargetsToCreate = targetableObjects.map((targetableObject) => { + const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ + nameSingular: targetableObject.targetObjectNameSingular, + }); - const activityTargetsToCreate = activityTargetableObjects.map( - (targetableObject) => { - const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ - nameSingular: targetableObject.targetObjectNameSingular, - }); + const relatedObjectRecord = targetObjectRecords.find( + (record) => record.id === targetableObject.id, + ); - const relatedObjectRecord = targetObjectRecords.find( - (record) => record.id === targetableObject.id, - ); + const activityTarget = { + [targetableObject.targetObjectNameSingular]: relatedObjectRecord, + [targetableObjectFieldIdName]: targetableObject.id, + activity, + activityId: activity.id, + id: v4(), + updatedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + } as Partial; - const activityTarget = { - [targetableObject.targetObjectNameSingular]: relatedObjectRecord, - [targetableObjectFieldIdName]: targetableObject.id, - activityId, - id: v4(), - updatedAt: new Date().toISOString(), - createdAt: new Date().toISOString(), - } as Partial; - - return activityTarget; - }, - ); + return activityTarget; + }); return activityTargetsToCreate; }; diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts index ba99ce464e..67ba4f8c69 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts @@ -1,7 +1,7 @@ import { ApolloCache, StoreObject } from '@apollo/client'; -import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; @@ -32,8 +32,8 @@ export const triggerAttachRelationOptimisticEffect = ({ id: targetRecordCacheId, fields: { [fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => { - const fieldValueIsCachedObjectRecordConnection = - isCachedObjectRecordConnection( + const fieldValueisObjectRecordConnectionWithRefs = + isObjectRecordConnectionWithRefs( sourceObjectNameSingular, targetRecordFieldValue, ); @@ -47,7 +47,7 @@ export const triggerAttachRelationOptimisticEffect = ({ return targetRecordFieldValue; } - if (fieldValueIsCachedObjectRecordConnection) { + if (fieldValueisObjectRecordConnectionWithRefs) { const nextEdges: CachedObjectRecordEdge[] = [ ...targetRecordFieldValue.edges, { diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index 14d756c8ce..84d7aab6bf 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -1,12 +1,12 @@ import { ApolloCache, StoreObject } from '@apollo/client'; import { isNonEmptyString } from '@sniptt/guards'; -import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; /* TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are. @@ -24,10 +24,6 @@ export const triggerCreateRecordsOptimisticEffect = ({ recordsToCreate: CachedObjectRecord[]; objectMetadataItems: ObjectMetadataItem[]; }) => { - const objectEdgeTypeName = getEdgeTypename({ - objectNameSingular: objectMetadataItem.nameSingular, - }); - recordsToCreate.forEach((record) => triggerUpdateRelationsOptimisticEffect({ cache, @@ -49,7 +45,7 @@ export const triggerCreateRecordsOptimisticEffect = ({ toReference, }, ) => { - const shouldSkip = !isCachedObjectRecordConnection( + const shouldSkip = !isObjectRecordConnectionWithRefs( objectMetadataItem.nameSingular, rootQueryCachedResponse, ); @@ -97,7 +93,7 @@ export const triggerCreateRecordsOptimisticEffect = ({ if (recordToCreateReference && !recordAlreadyInCache) { nextRootQueryCachedRecordEdges.unshift({ - __typename: objectEdgeTypeName, + __typename: getEdgeTypename(objectMetadataItem.nameSingular), node: recordToCreateReference, cursor: '', }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts index f0e35e6af7..7c381ac866 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts @@ -1,11 +1,11 @@ import { ApolloCache, StoreObject } from '@apollo/client'; -import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { isDefined } from '~/utils/isDefined'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; @@ -27,7 +27,7 @@ export const triggerDeleteRecordsOptimisticEffect = ({ { DELETE, readField, storeFieldName }, ) => { const rootQueryCachedResponseIsNotACachedObjectRecordConnection = - !isCachedObjectRecordConnection( + !isObjectRecordConnectionWithRefs( objectMetadataItem.nameSingular, rootQueryCachedResponse, ); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts index 3d00805261..d321852989 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts @@ -1,6 +1,6 @@ import { ApolloCache, StoreObject } from '@apollo/client'; -import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { capitalize } from '~/utils/string/capitalize'; export const triggerDetachRelationOptimisticEffect = ({ @@ -32,7 +32,7 @@ export const triggerDetachRelationOptimisticEffect = ({ targetRecordFieldValue, { isReference, readField }, ) => { - const isRecordConnection = isCachedObjectRecordConnection( + const isRecordConnection = isObjectRecordConnectionWithRefs( sourceObjectNameSingular, targetRecordFieldValue, ); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts index 574383743b..50c1faf790 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts @@ -1,6 +1,5 @@ import { ApolloCache, StoreObject } from '@apollo/client'; -import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges'; import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; @@ -8,6 +7,7 @@ import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; +import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { isDefined } from '~/utils/isDefined'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; @@ -27,10 +27,6 @@ export const triggerUpdateRecordOptimisticEffect = ({ updatedRecord: CachedObjectRecord; objectMetadataItems: ObjectMetadataItem[]; }) => { - const objectEdgeTypeName = getEdgeTypename({ - objectNameSingular: objectMetadataItem.nameSingular, - }); - triggerUpdateRelationsOptimisticEffect({ cache, sourceObjectMetadataItem: objectMetadataItem, @@ -45,7 +41,7 @@ export const triggerUpdateRecordOptimisticEffect = ({ rootQueryCachedResponse, { DELETE, readField, storeFieldName, toReference }, ) => { - const shouldSkip = !isCachedObjectRecordConnection( + const shouldSkip = !isObjectRecordConnectionWithRefs( objectMetadataItem.nameSingular, rootQueryCachedResponse, ); @@ -103,7 +99,7 @@ export const triggerUpdateRecordOptimisticEffect = ({ if (isDefined(updatedRecordNodeReference)) { rootQueryNextEdges.push({ - __typename: objectEdgeTypeName, + __typename: getEdgeTypename(objectMetadataItem.nameSingular), node: updatedRecordNodeReference, cursor: '', }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts index f76074e284..d8deb5119d 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts @@ -1,7 +1,6 @@ import { ApolloCache } from '@apollo/client'; import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition'; -import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection'; import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect'; @@ -9,6 +8,7 @@ import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts index c27cc58221..838c1d143d 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts @@ -65,6 +65,27 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql` } defaultValue options + relationDefinition { + direction + sourceObjectMetadata { + id + nameSingular + namePlural + } + sourceFieldMetadata { + id + name + } + targetObjectMetadata { + id + nameSingular + namePlural + } + targetFieldMetadata { + id + name + } + } } } pageInfo { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx index 61a829cc96..ad2c13a5b7 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx @@ -1,21 +1,16 @@ import { ReactNode } from 'react'; -import { - ApolloClient, - NormalizedCacheObject, - useApolloClient, -} from '@apollo/client'; import { ApolloMetadataClientContext } from '@/object-metadata/context/ApolloClientMetadataContext'; +import { mockedMetadataApolloClient } from '~/testing/mockedMetadataApolloClient'; -export const TestApolloMetadataClientProvider = ({ +export const ApolloMetadataClientMockedProvider = ({ children, }: { children: ReactNode; }) => { - const client = useApolloClient() as ApolloClient; return ( - - {client ? children : ''} + + {mockedMetadataApolloClient ? children : ''} ); }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx index e8d124ce8e..d0893b3923 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx @@ -32,7 +32,6 @@ describe('useObjectMetadataItem', () => { labelIdentifierFieldMetadata, getRecordFromCache, findManyRecordsQuery, - modifyRecordFromCache, findOneRecordQuery, createOneRecordMutation, updateOneRecordMutation, @@ -48,7 +47,6 @@ describe('useObjectMetadataItem', () => { expect(basePathToShowPage).toBe('/object/opportunity/'); expect(objectMetadataItem.id).toBe('20202020-cae9-4ff4-9579-f7d9fe44c937'); expect(typeof getRecordFromCache).toBe('function'); - expect(typeof modifyRecordFromCache).toBe('function'); expect(typeof mapToObjectRecordIdentifier).toBe('function'); expect(typeof getObjectOrderByField).toBe('function'); expect(findManyRecordsQuery).toHaveProperty('kind', 'Document'); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index a4edf8c39c..4f0ce1e765 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -11,7 +11,6 @@ import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShow import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; import { useGenerateDeleteManyRecordMutation } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation'; @@ -40,7 +39,8 @@ export const EMPTY_MUTATION = gql` export const useObjectMetadataItem = ( { objectNameSingular }: ObjectMetadataItemIdentifier, depth?: number, - eagerLoadedRelations?: Record, + queryFields?: Record, + computeReferences = false, ) => { const currentWorkspace = useRecoilValue(currentWorkspaceState); @@ -83,15 +83,11 @@ export const useObjectMetadataItem = ( objectMetadataItem, }); - const modifyRecordFromCache = useModifyRecordFromCache({ - objectMetadataItem, - }); - const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery(); const findManyRecordsQuery = generateFindManyRecordsQuery({ objectMetadataItem, depth, - eagerLoadedRelations, + queryFields, }); const generateFindDuplicateRecordsQuery = @@ -109,14 +105,18 @@ export const useObjectMetadataItem = ( const createOneRecordMutation = useGenerateCreateOneRecordMutation({ objectMetadataItem, + depth, }); const createManyRecordsMutation = useGenerateCreateManyRecordMutation({ objectMetadataItem, + depth, }); const updateOneRecordMutation = useGenerateUpdateOneRecordMutation({ objectMetadataItem, + depth, + computeReferences, }); const deleteOneRecordMutation = generateDeleteOneRecordMutation({ @@ -144,7 +144,6 @@ export const useObjectMetadataItem = ( basePathToShowPage, objectMetadataItem, getRecordFromCache, - modifyRecordFromCache, findManyRecordsQuery, findDuplicateRecordsQuery, findOneRecordQuery, diff --git a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts index feaa73ed9f..396fdf4af8 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts @@ -1,5 +1,10 @@ import { ThemeColor } from '@/ui/theme/constants/MainColorNames'; -import { Field, Relation } from '~/generated-metadata/graphql'; +import { + Field, + Object as MetadataObject, + Relation, + RelationDefinitionType, +} from '~/generated-metadata/graphql'; export type FieldMetadataItemOption = { color: ThemeColor; @@ -16,6 +21,7 @@ export type FieldMetadataItem = Omit< | 'toRelationMetadata' | 'defaultValue' | 'options' + | 'relationDefinition' > & { __typename?: string; fromRelationMetadata?: @@ -36,4 +42,17 @@ export type FieldMetadataItem = Omit< | null; defaultValue?: any; options?: FieldMetadataItemOption[]; + relationDefinition?: { + direction: RelationDefinitionType; + sourceFieldMetadata: Pick; + sourceObjectMetadata: Pick< + MetadataObject, + 'id' | 'nameSingular' | 'namePlural' + >; + targetFieldMetadata: Pick; + targetObjectMetadata: Pick< + MetadataObject, + 'id' | 'nameSingular' | 'namePlural' + >; + } | null; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx index c71702c942..c13ec68d54 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx @@ -40,7 +40,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => { it('should not return relation if depth is < 1', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: mockObjectMetadataItems, - relationFieldDepth: 0, + depth: 0, field: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, @@ -51,7 +51,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => { it('should return relation if it matches depth', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: mockObjectMetadataItems, - relationFieldDepth: 1, + depth: 1, field: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, @@ -88,7 +88,7 @@ idealCustomerProfile it('should return relation with all sub relations if it matches depth', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: mockObjectMetadataItems, - relationFieldDepth: 2, + depth: 2, field: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, @@ -239,11 +239,26 @@ idealCustomerProfile }`); }); - it('should return eagerLoaded relations', async () => { + it('should return GraphQL fields based on queryFields', async () => { const res = mapFieldMetadataToGraphQLQuery({ objectMetadataItems: mockObjectMetadataItems, - relationFieldDepth: 2, - relationFieldEagerLoad: { accountOwner: true, people: true }, + depth: 2, + queryFields: { + accountOwner: true, + people: true, + xLink: true, + linkedinLink: true, + domainName: true, + annualRecurringRevenue: true, + createdAt: true, + address: true, + updatedAt: true, + name: true, + accountOwnerId: true, + employees: true, + id: true, + idealCustomerProfile: true, + }, field: personObjectMetadataItem.fields.find( (field) => field.name === 'company', )!, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx index a1d34e9b5b..f8f32cead2 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx @@ -213,11 +213,25 @@ companyId }`); }); - it('should eager load only specified relations', async () => { + it('should query only specified queryFields', async () => { const res = mapObjectMetadataToGraphQLQuery({ objectMetadataItems: mockObjectMetadataItems, objectMetadataItem: personObjectMetadataItem, - eagerLoadedRelations: { company: true }, + queryFields: { + company: true, + xLink: true, + id: true, + createdAt: true, + city: true, + email: true, + jobTitle: true, + name: true, + phone: true, + linkedinLink: true, + updatedAt: true, + avatarUrl: true, + companyId: true, + }, depth: 1, }); expect(formatGQLString(res)).toEqual(`{ @@ -274,6 +288,52 @@ linkedinLink updatedAt avatarUrl companyId +}`); + }); + + it('should load only specified query fields', async () => { + const res = mapObjectMetadataToGraphQLQuery({ + objectMetadataItems: mockObjectMetadataItems, + objectMetadataItem: personObjectMetadataItem, + queryFields: { company: true, id: true, name: true }, + depth: 1, + }); + expect(formatGQLString(res)).toEqual(`{ +__typename +id +company +{ +__typename +xLink +{ + label + url +} +linkedinLink +{ + label + url +} +domainName +annualRecurringRevenue +{ + amountMicros + currencyCode +} +createdAt +address +updatedAt +name +accountOwnerId +employees +id +idealCustomerProfile +} +name +{ + firstName + lastName +} }`); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts index 956f3a5ca7..32992648d2 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/shouldFieldBeQueried.test.ts @@ -34,10 +34,10 @@ describe('shouldFieldBeQueried', () => { expect(res).toBe(false); }); - it('should not depends on eagerLoadedRelation', () => { + it('should not depends on queryFields', () => { const res = shouldFieldBeQueried({ depth: 0, - eagerLoadedRelations: { + queryFields: { fieldName: true, }, field: { name: 'fieldName', type: FieldMetadataType.Boolean }, @@ -47,14 +47,14 @@ describe('shouldFieldBeQueried', () => { }); describe('if field is relation', () => { - it('should be queried if eagerLoadedRelation and depth are undefined', () => { + it('should be queried if queryFields and depth are undefined', () => { const res = shouldFieldBeQueried({ field: { name: 'fieldName', type: FieldMetadataType.Relation }, }); expect(res).toBe(true); }); - it('should be queried if eagerLoadedRelation is undefined and depth = 1', () => { + it('should be queried if queryFields is undefined and depth = 1', () => { const res = shouldFieldBeQueried({ depth: 1, field: { name: 'fieldName', type: FieldMetadataType.Relation }, @@ -62,7 +62,7 @@ describe('shouldFieldBeQueried', () => { expect(res).toBe(true); }); - it('should be queried if eagerLoadedRelation is undefined and depth > 1', () => { + it('should be queried if queryFields is undefined and depth > 1', () => { const res = shouldFieldBeQueried({ depth: 2, field: { name: 'fieldName', type: FieldMetadataType.Relation }, @@ -70,7 +70,7 @@ describe('shouldFieldBeQueried', () => { expect(res).toBe(true); }); - it('should NOT be queried if eagerLoadedRelation is undefined and depth < 1', () => { + it('should NOT be queried if queryFields is undefined and depth < 1', () => { const res = shouldFieldBeQueried({ depth: 0, field: { name: 'fieldName', type: FieldMetadataType.Relation }, @@ -78,37 +78,37 @@ describe('shouldFieldBeQueried', () => { expect(res).toBe(false); }); - it('should be queried if eagerLoadedRelation is matching and depth > 1', () => { + it('should be queried if queryFields is matching and depth > 1', () => { const res = shouldFieldBeQueried({ depth: 1, - eagerLoadedRelations: { fieldName: true }, + queryFields: { fieldName: true }, field: { name: 'fieldName', type: FieldMetadataType.Relation }, }); expect(res).toBe(true); }); - it('should NOT be queried if eagerLoadedRelation is matching and depth < 1', () => { + it('should NOT be queried if queryFields is matching and depth < 1', () => { const res = shouldFieldBeQueried({ depth: 0, - eagerLoadedRelations: { fieldName: true }, + queryFields: { fieldName: true }, field: { name: 'fieldName', type: FieldMetadataType.Relation }, }); expect(res).toBe(false); }); - it('should NOT be queried if eagerLoadedRelation is not matching (falsy) and depth < 1', () => { + it('should NOT be queried if queryFields is not matching (falsy) and depth < 1', () => { const res = shouldFieldBeQueried({ depth: 1, - eagerLoadedRelations: { fieldName: false }, + queryFields: { fieldName: false }, field: { name: 'fieldName', type: FieldMetadataType.Relation }, }); expect(res).toBe(false); }); - it('should NOT be queried if eagerLoadedRelation is not matching and depth < 1', () => { + it('should NOT be queried if queryFields is not matching and depth < 1', () => { const res = shouldFieldBeQueried({ depth: 0, - eagerLoadedRelations: { anotherFieldName: true }, + queryFields: { anotherFieldName: true }, field: { name: 'fieldName', type: FieldMetadataType.Relation }, }); expect(res).toBe(false); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getFieldRelationDirections.ts b/packages/twenty-front/src/modules/object-metadata/utils/getFieldRelationDirections.ts new file mode 100644 index 0000000000..6b8929c324 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getFieldRelationDirections.ts @@ -0,0 +1,38 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { RelationDirections } from '@/object-record/record-field/types/FieldDefinition'; +import { + FieldMetadataType, + RelationDefinitionType, +} from '~/generated-metadata/graphql'; + +export const getFieldRelationDirections = ( + field: Pick | undefined, +): RelationDirections => { + if (!field || field.type !== FieldMetadataType.Relation) { + throw new Error(`Field is not a relation field.`); + } + + switch (field.relationDefinition?.direction) { + case RelationDefinitionType.ManyToMany: + throw new Error(`Many to many relations are not supported.`); + case RelationDefinitionType.OneToMany: + return { + from: 'FROM_ONE_OBJECT', + to: 'TO_MANY_OBJECTS', + }; + case RelationDefinitionType.ManyToOne: + return { + from: 'FROM_MANY_OBJECTS', + to: 'TO_ONE_OBJECT', + }; + case RelationDefinitionType.OneToOne: + return { + from: 'FROM_ONE_OBJECT', + to: 'TO_ONE_OBJECT', + }; + default: + throw new Error( + `Invalid relation definition type direction : ${field.relationDefinition?.direction}`, + ); + } +}; 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 3c70a01ad2..70f9f5ce2b 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -6,19 +6,22 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataItem } from '../types/FieldMetadataItem'; +// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field export const mapFieldMetadataToGraphQLQuery = ({ objectMetadataItems, field, - relationFieldDepth = 0, - relationFieldEagerLoad, + depth = 0, + queryFields, + computeReferences = false, }: { objectMetadataItems: ObjectMetadataItem[]; field: Pick< FieldMetadataItem, 'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata' >; - relationFieldDepth?: number; - relationFieldEagerLoad?: Record; + depth?: number; + queryFields?: Record; + computeReferences?: boolean; }): any => { const fieldType = field.type; @@ -43,7 +46,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ } else if ( fieldType === 'RELATION' && field.toRelationMetadata?.relationType === 'ONE_TO_MANY' && - relationFieldDepth > 0 + depth > 0 ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => @@ -59,13 +62,15 @@ export const mapFieldMetadataToGraphQLQuery = ({ ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, objectMetadataItem: relationMetadataItem, - eagerLoadedRelations: relationFieldEagerLoad, - depth: relationFieldDepth - 1, + depth: depth - 1, + queryFields, + computeReferences: computeReferences, + isRootLevel: false, })}`; } else if ( fieldType === 'RELATION' && field.fromRelationMetadata?.relationType === 'ONE_TO_MANY' && - relationFieldDepth > 0 + depth > 0 ) { const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => @@ -83,8 +88,10 @@ ${mapObjectMetadataToGraphQLQuery({ node ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, objectMetadataItem: relationMetadataItem, - eagerLoadedRelations: relationFieldEagerLoad, - depth: relationFieldDepth - 1, + depth: depth - 1, + queryFields, + computeReferences, + isRootLevel: false, })} } }`; 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 444c566f2b..ebd05e7965 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -1,5 +1,3 @@ -import { isUndefined } from '@sniptt/guards'; - import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery'; import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried'; @@ -8,28 +6,47 @@ export const mapObjectMetadataToGraphQLQuery = ({ objectMetadataItems, objectMetadataItem, depth = 1, - eagerLoadedRelations, + queryFields, + computeReferences = false, + isRootLevel = true, }: { objectMetadataItems: ObjectMetadataItem[]; objectMetadataItem: Pick; depth?: number; - eagerLoadedRelations?: Record; + queryFields?: Record; + computeReferences?: boolean; + isRootLevel?: boolean; }): any => { + const fieldsThatShouldBeQueried = + objectMetadataItem?.fields + .filter((field) => field.isActive) + .filter((field) => + shouldFieldBeQueried({ + field, + depth, + queryFields, + }), + ) ?? []; + + if (!isRootLevel && computeReferences) { + return `{ + __ref + }`; + } + return `{ __typename -${(objectMetadataItem?.fields ?? []) - .filter((field) => field.isActive) - .filter((field) => - shouldFieldBeQueried({ field, depth, eagerLoadedRelations }), - ) +${fieldsThatShouldBeQueried .map((field) => mapFieldMetadataToGraphQLQuery({ objectMetadataItems, field, - relationFieldDepth: depth, - relationFieldEagerLoad: isUndefined(eagerLoadedRelations) - ? undefined - : eagerLoadedRelations[field.name] ?? undefined, + depth, + queryFields: + typeof queryFields?.[field.name] === 'boolean' + ? undefined + : queryFields?.[field.name], + computeReferences, }), ) .join('\n')} diff --git a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts index 3e99775ebf..f663359ad8 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts @@ -1,17 +1,20 @@ import { isUndefined } from '@sniptt/guards'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; import { FieldMetadataItem } from '../types/FieldMetadataItem'; export const shouldFieldBeQueried = ({ field, depth, - eagerLoadedRelations, + queryFields, }: { field: Pick; depth?: number; - eagerLoadedRelations?: Record; + objectRecord?: ObjectRecord; + queryFields?: Record; }): any => { if (!isUndefined(depth) && depth < 0) { return false; @@ -25,12 +28,7 @@ export const shouldFieldBeQueried = ({ return false; } - if ( - field.type === FieldMetadataType.Relation && - !isUndefined(eagerLoadedRelations) && - (isUndefined(eagerLoadedRelations[field.name]) || - !eagerLoadedRelations[field.name]) - ) { + if (isDefined(queryFields) && !queryFields[field.name]) { return false; } diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts deleted file mode 100644 index 3ef788f6cc..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useApolloClient } from '@apollo/client'; -import gql from 'graphql-tag'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { useInjectIntoFindOneRecordQueryCache } from '@/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useAddRecordInCache = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const apolloClient = useApolloClient(); - - const { injectIntoFindOneRecordQueryCache } = - useInjectIntoFindOneRecordQueryCache({ - objectMetadataItem, - }); - - return useRecoilCallback( - ({ set }) => - (record: ObjectRecord) => { - const fragment = gql` - fragment Create${capitalize( - objectMetadataItem.nameSingular, - )}InCache on ${capitalize( - objectMetadataItem.nameSingular, - )} ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems, - objectMetadataItem, - })} - `; - - const cachedObjectRecord = { - __typename: `${capitalize(objectMetadataItem.nameSingular)}`, - ...record, - }; - - apolloClient.writeFragment({ - id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`, - fragment, - data: cachedObjectRecord, - }); - - // TODO: should we keep this here ? Or should the caller of createOneRecordInCache/createManyRecordsInCache be responsible for this ? - injectIntoFindOneRecordQueryCache(cachedObjectRecord); - - // TODO: remove this once we get rid of entityFieldsFamilyState - set(recordStoreFamilyState(record.id), record); - }, - [ - objectMetadataItem, - objectMetadataItems, - apolloClient, - injectIntoFindOneRecordQueryCache, - ], - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts deleted file mode 100644 index a7fe89f4da..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; - -export const useAppendToFindManyRecordsQueryInCache = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { - const { readFindManyRecordsQueryInCache } = - useReadFindManyRecordsQueryInCache({ - objectMetadataItem, - }); - - const { - upsertFindManyRecordsQueryInCache: overwriteFindManyRecordsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem, - }); - - const appendToFindManyRecordsQueryInCache = < - T extends ObjectRecord = ObjectRecord, - >({ - queryVariables, - objectRecordsToAppend, - }: { - queryVariables: ObjectRecordQueryVariables; - objectRecordsToAppend: T[]; - }) => { - const existingObjectRecords = readFindManyRecordsQueryInCache({ - queryVariables, - }); - - const newObjectRecordList = [ - ...existingObjectRecords, - ...objectRecordsToAppend, - ]; - - overwriteFindManyRecordsQueryInCache({ - objectRecordsToOverwrite: newObjectRecordList, - queryVariables, - }); - }; - - return { - appendToFindManyRecordsQueryInCache, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts new file mode 100644 index 0000000000..31fb896556 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts @@ -0,0 +1,42 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { prefillRecord } from '@/object-record/utils/prefillRecord'; +import { isDefined } from '~/utils/isDefined'; + +export const useCreateManyRecordsInCache = ({ + objectNameSingular, +}: ObjectMetadataItemIdentifier) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const createOneRecordInCache = useCreateOneRecordInCache({ + objectMetadataItem, + }); + + const createManyRecordsInCache = (recordsToCreate: Partial[]) => { + const recordsWithId = recordsToCreate + .map((record) => { + return prefillRecord({ + input: record, + objectMetadataItem, + }); + }) + .filter(isDefined); + + const createdRecordsInCache = [] as T[]; + + for (const record of recordsWithId) { + if (isDefined(record)) { + createOneRecordInCache(record); + createdRecordsInCache.push(record); + } + } + + return createdRecordsInCache; + }; + + return { createManyRecordsInCache }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts new file mode 100644 index 0000000000..de0e241355 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts @@ -0,0 +1,62 @@ +import { useApolloClient } from '@apollo/client'; +import gql from 'graphql-tag'; +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { prefillRecord } from '@/object-record/utils/prefillRecord'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useCreateOneRecordInCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const getRecordFromCache = useGetRecordFromCache({ + objectMetadataItem, + }); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const apolloClient = useApolloClient(); + + return (record: ObjectRecord) => { + const fragment = gql` + fragment Create${capitalize( + objectMetadataItem.nameSingular, + )}InCache on ${capitalize( + objectMetadataItem.nameSingular, + )} ${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems, + objectMetadataItem, + computeReferences: true, + })} + `; + + const prefilledRecord = prefillRecord({ + objectMetadataItem, + input: record, + depth: 1, + }); + + const recordToCreateWithNestedConnections = getRecordNodeFromRecord({ + record: prefilledRecord, + objectMetadataItem, + objectMetadataItems, + }); + + const cachedObjectRecord = { + __typename: `${capitalize(objectMetadataItem.nameSingular)}`, + ...recordToCreateWithNestedConnections, + }; + + apolloClient.writeFragment({ + id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`, + fragment, + data: cachedObjectRecord, + }); + return getRecordFromCache(record.id) as T; + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts new file mode 100644 index 0000000000..427a5e86aa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts @@ -0,0 +1,29 @@ +import { useApolloClient } from '@apollo/client'; + +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +export const useDeleteRecordFromCache = ({ + objectNameSingular, +}: { + objectNameSingular: string; +}) => { + const apolloClient = useApolloClient(); + + const { objectMetadataItem } = useObjectMetadataItemOnly({ + objectNameSingular, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + return (recordToDelete: ObjectRecord) => { + deleteRecordFromCache({ + objectMetadataItem, + objectMetadataItems, + recordToDelete, + cache: apolloClient.cache, + }); + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts deleted file mode 100644 index 348c0cc54c..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { v4 } from 'uuid'; -import { z } from 'zod'; - -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useGenerateObjectRecordOptimisticResponse = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { - const getRelationMetadata = useGetRelationMetadata(); - - const generateObjectRecordOptimisticResponse = < - GeneratedObjectRecord extends ObjectRecord, - >( - input: Record, - ) => { - const recordSchema = z.object( - Object.fromEntries( - objectMetadataItem.fields.map((fieldMetadataItem) => [ - fieldMetadataItem.name, - z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)), - ]), - ), - ); - - const inputWithRelationFields = objectMetadataItem.fields.reduce( - (result, fieldMetadataItem) => { - const relationIdFieldName = `${fieldMetadataItem.name}Id`; - - if (!(relationIdFieldName in input)) return result; - - const relationMetadata = getRelationMetadata({ fieldMetadataItem }); - - if (!relationMetadata) return result; - - const relationRecordTypeName = capitalize( - relationMetadata.relationObjectMetadataItem.nameSingular, - ); - const relationRecordId = result[relationIdFieldName] as string | null; - - const relationRecord = input[fieldMetadataItem.name] as - | ObjectRecord - | undefined; - - return { - ...result, - [fieldMetadataItem.name]: relationRecordId - ? { - __typename: relationRecordTypeName, - id: relationRecordId, - // TODO: there are too many bugs if we don't include the entire relation record - // See if we can find a way to work only with the id and typename - ...relationRecord, - } - : null, - }; - }, - input, - ); - - return { - __typename: capitalize(objectMetadataItem.nameSingular), - ...recordSchema.parse({ - id: v4(), - createdAt: new Date().toISOString(), - ...inputWithRelationFields, - }), - } as GeneratedObjectRecord & { __typename: string }; - }; - - return { - generateObjectRecordOptimisticResponse, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts index 3f9e35a95c..5fe2cf5b29 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts @@ -1,13 +1,11 @@ import { useCallback } from 'react'; -import { gql, useApolloClient } from '@apollo/client'; +import { useApolloClient } from '@apollo/client'; import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { capitalize } from '~/utils/string/capitalize'; export const useGetRecordFromCache = ({ objectMetadataItem, @@ -23,29 +21,11 @@ export const useGetRecordFromCache = ({ recordId: string, cache = apolloClient.cache, ) => { - if (isUndefinedOrNull(objectMetadataItem)) { - return null; - } - - const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); - - const cacheReadFragment = gql` - fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery( - { - objectMetadataItems, - objectMetadataItem, - }, - )} - `; - - const cachedRecordId = cache.identify({ - __typename: capitalize(objectMetadataItem.nameSingular), - id: recordId, - }); - - return cache.readFragment({ - id: cachedRecordId, - fragment: cacheReadFragment, + return getRecordFromCache({ + cache, + recordId, + objectMetadataItems, + objectMetadataItem, }); }, [objectMetadataItem, objectMetadataItems, apolloClient], diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts deleted file mode 100644 index 9e153f9913..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useApolloClient } from '@apollo/client'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useInjectIntoFindOneRecordQueryCache = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { - const apolloClient = useApolloClient(); - - const generateFindOneRecordQuery = useGenerateFindOneRecordQuery(); - - const injectIntoFindOneRecordQueryCache = < - T extends ObjectRecord = ObjectRecord, - >( - record: T, - ) => { - const findOneRecordQueryForCacheInjection = generateFindOneRecordQuery({ - objectMetadataItem, - depth: 1, - }); - - apolloClient.writeQuery({ - query: findOneRecordQueryForCacheInjection, - variables: { - objectRecordId: record.id, - }, - data: { - [objectMetadataItem.nameSingular]: { - __typename: `${capitalize(objectMetadataItem.nameSingular)}`, - ...record, - }, - }, - }); - }; - - return { - injectIntoFindOneRecordQueryCache, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts deleted file mode 100644 index 9bb619202a..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useApolloClient } from '@apollo/client'; -import { Modifiers } from '@apollo/client/cache'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useModifyRecordFromCache = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { - const { cache } = useApolloClient(); - - return ( - recordId: string, - fieldModifiers: Modifiers, - ) => { - if (isUndefinedOrNull(objectMetadataItem)) return; - - const cachedRecordId = cache.identify({ - __typename: capitalize(objectMetadataItem.nameSingular), - id: recordId, - }); - - cache.modify({ - id: cachedRecordId, - fields: fieldModifiers, - }); - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts index 4b0bc978eb..d680582e96 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts @@ -21,11 +21,17 @@ export const useReadFindManyRecordsQueryInCache = ({ T extends ObjectRecord = ObjectRecord, >({ queryVariables, + queryFields, + depth, }: { queryVariables: ObjectRecordQueryVariables; + queryFields?: Record; + depth?: number; }) => { const findManyRecordsQueryForCacheRead = generateFindManyRecordsQuery({ objectMetadataItem, + queryFields, + depth, }); const existingRecordsQueryResult = apolloClient.readQuery< diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts index 297be5db3c..83bd278592 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts @@ -1,5 +1,7 @@ import { useApolloClient } from '@apollo/client'; +import { useRecoilValue } from 'recoil'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection'; import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; @@ -18,6 +20,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({ const apolloClient = useApolloClient(); const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery(); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const upsertFindManyRecordsQueryInCache = < T extends ObjectRecord = ObjectRecord, @@ -25,19 +28,28 @@ export const useUpsertFindManyRecordsQueryInCache = ({ queryVariables, depth = MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, objectRecordsToOverwrite, + queryFields, + computeReferences = false, }: { queryVariables: ObjectRecordQueryVariables; depth?: number; objectRecordsToOverwrite: T[]; + queryFields?: Record; + computeReferences?: boolean; }) => { const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({ objectMetadataItem, - depth, // TODO: fix this + depth, + queryFields, + computeReferences, }); const newObjectRecordConnection = getRecordConnectionFromRecords({ - objectNameSingular: objectMetadataItem.nameSingular, + objectMetadataItems: objectMetadataItems, + objectMetadataItem: objectMetadataItem, records: objectRecordsToOverwrite, + queryFields, + computeReferences, }); apolloClient.writeQuery({ diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts new file mode 100644 index 0000000000..ec9ec8b3a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts @@ -0,0 +1,30 @@ +import { ApolloCache } from '@apollo/client'; + +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +export const deleteRecordFromCache = ({ + objectMetadataItem, + objectMetadataItems, + recordToDelete, + cache, +}: { + objectMetadataItem: ObjectMetadataItem; + objectMetadataItems: ObjectMetadataItem[]; + recordToDelete: ObjectRecord; + cache: ApolloCache; +}) => { + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + objectMetadataItems, + recordsToDelete: [ + { + ...recordToDelete, + __typename: getObjectTypename(objectMetadataItem.nameSingular), + }, + ], + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts deleted file mode 100644 index dd6e94295a..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApolloClient, makeReference, Reference } from '@apollo/client'; - -import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - -export const getCacheReferenceFromRecord = ({ - apolloClient, - objectNameSingular, - record, -}: { - apolloClient: ApolloClient; - objectNameSingular: string; - record: T; -}): Reference => { - const cachedRecord = getCachedRecordFromRecord({ - objectNameSingular, - record, - }); - - const id = apolloClient.cache.identify(cachedRecord); - - if (!id) { - throw new Error( - `Could not identify record "${objectNameSingular}", id : "${record.id}"`, - ); - } - - const recordReference = makeReference(id); - - return recordReference; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts deleted file mode 100644 index 11f1cec675..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ApolloClient, makeReference } from '@apollo/client'; - -import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; -import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord'; -import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - -export const getCachedRecordEdgesFromRecords = ({ - apolloClient, - objectNameSingular, - records, -}: { - apolloClient: ApolloClient; - objectNameSingular: string; - records: T[]; -}): CachedObjectRecordEdge[] => { - const cachedRecordEdges = records.map((record) => { - const cachedRecord = getCachedRecordFromRecord({ - objectNameSingular, - record, - }); - - const id = apolloClient.cache.identify(cachedRecord); - - if (!id) { - throw new Error( - `Could not identify record "${objectNameSingular}", id : "${record.id}"`, - ); - } - - const reference = makeReference(id); - - const cachedObjectRecordEdge: CachedObjectRecordEdge = { - cursor: '', - node: reference, - __typename: getEdgeTypename({ objectNameSingular }), - }; - - return cachedObjectRecordEdge; - }); - - return cachedRecordEdges; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts deleted file mode 100644 index 31a72e8de4..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; -import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - -export const getCachedRecordFromRecord = ({ - objectNameSingular, - record, -}: { - objectNameSingular: string; - record: T; -}): CachedObjectRecord => { - return { - __typename: getNodeTypename({ objectNameSingular }), - ...record, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts index 6c2139148f..7b827c2ba1 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts @@ -1,9 +1,6 @@ +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { capitalize } from '~/utils/string/capitalize'; -export const getConnectionTypename = ({ - objectNameSingular, -}: { - objectNameSingular: string; -}) => { - return `${capitalize(objectNameSingular)}Connection`; +export const getConnectionTypename = (objectNameSingular: string) => { + return `${capitalize(getObjectTypename(objectNameSingular))}Connection`; }; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts index da024846a9..f2cd62ff4c 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts @@ -1,9 +1,6 @@ +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { capitalize } from '~/utils/string/capitalize'; -export const getEdgeTypename = ({ - objectNameSingular, -}: { - objectNameSingular: string; -}) => { - return `${capitalize(objectNameSingular)}Edge`; +export const getEdgeTypename = (objectNameSingular: string) => { + return `${capitalize(getObjectTypename(objectNameSingular))}Edge`; }; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts index c058a53497..16d3122c34 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts @@ -1,9 +1,6 @@ +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { capitalize } from '~/utils/string/capitalize'; -export const getNodeTypename = ({ - objectNameSingular, -}: { - objectNameSingular: string; -}) => { - return capitalize(objectNameSingular); +export const getNodeTypename = (objectNameSingular: string) => { + return capitalize(getObjectTypename(objectNameSingular)); }; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts new file mode 100644 index 0000000000..7a799bf9d2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts @@ -0,0 +1,5 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getObjectTypename = (objectNameSingular: string) => { + return capitalize(objectNameSingular); +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts deleted file mode 100644 index f43ce56e00..0000000000 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename'; -import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; -import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; - -export const getRecordConnectionFromEdges = ({ - objectNameSingular, - edges, -}: { - objectNameSingular: string; - edges: ObjectRecordEdge[]; -}) => { - return { - __typename: getConnectionTypename({ objectNameSingular }), - edges: edges, - pageInfo: getEmptyPageInfo(), - } as ObjectRecordConnection; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts index affe038e99..70090d490c 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts @@ -1,3 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename'; import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord'; @@ -5,21 +6,41 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; export const getRecordConnectionFromRecords = ({ - objectNameSingular, + objectMetadataItems, + objectMetadataItem, records, + queryFields, + withPageInfo = true, + computeReferences = false, + isRootLevel = true, + depth = 1, }: { - objectNameSingular: string; + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: Pick< + ObjectMetadataItem, + 'fields' | 'namePlural' | 'nameSingular' + >; records: T[]; + queryFields?: Record; + withPageInfo?: boolean; + isRootLevel?: boolean; + computeReferences?: boolean; + depth?: number; }) => { return { - __typename: getConnectionTypename({ objectNameSingular }), + __typename: getConnectionTypename(objectMetadataItem.nameSingular), edges: records.map((record) => { return getRecordEdgeFromRecord({ - objectNameSingular, + objectMetadataItems, + objectMetadataItem, + queryFields, record, + isRootLevel, + computeReferences, + depth, }); }), - pageInfo: getEmptyPageInfo(), - totalCount: records.length, + ...(withPageInfo && { pageInfo: getEmptyPageInfo() }), + ...(withPageInfo && { totalCount: records.length }), } as ObjectRecordConnection; }; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts index 86a09e9f8c..7327e2e169 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts @@ -1,37 +1,40 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; -import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; -import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; export const getRecordEdgeFromRecord = ({ - objectNameSingular, + objectMetadataItems, + objectMetadataItem, + queryFields, record, + computeReferences = false, + isRootLevel = false, }: { - objectNameSingular: string; + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: Pick< + ObjectMetadataItem, + 'fields' | 'namePlural' | 'nameSingular' + >; + queryFields?: Record; + computeReferences?: boolean; + isRootLevel?: boolean; + depth?: number; record: T; }) => { - const nestedRecord = Object.fromEntries( - Object.entries(record).map(([key, value]) => { - if (Array.isArray(value)) { - return [ - key, - getRecordConnectionFromRecords({ - // Todo: this is a ugly and broken hack to get the singular, we need to infer this from metadata - objectNameSingular: key.slice(0, -1), - records: value as ObjectRecord[], - }), - ]; - } - return [key, value]; - }), - ) as T; // Todo fix typing once we have investigated apollo edges / nodes removal - return { - __typename: getEdgeTypename({ objectNameSingular }), + __typename: getEdgeTypename(objectMetadataItem.nameSingular), node: { - __typename: getNodeTypename({ objectNameSingular }), - ...nestedRecord, + ...getRecordNodeFromRecord({ + objectMetadataItems, + objectMetadataItem, + queryFields, + record, + computeReferences, + isRootLevel, + depth: 1, + }), }, cursor: '', } as ObjectRecordEdge; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts new file mode 100644 index 0000000000..5db9a5e65f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts @@ -0,0 +1,55 @@ +import { ApolloCache, gql } from '@apollo/client'; + +import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { capitalize } from '~/utils/string/capitalize'; + +export const getRecordFromCache = ({ + objectMetadataItem, + objectMetadataItems, + cache, + recordId, +}: { + cache: ApolloCache; + recordId: string; + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: ObjectMetadataItem; +}) => { + if (isUndefinedOrNull(objectMetadataItem)) { + return null; + } + + const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); + + const cacheReadFragment = gql` + fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery( + { + objectMetadataItems, + objectMetadataItem, + }, + )} + `; + + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem.nameSingular), + id: recordId, + }); + + const record = cache.readFragment({ + id: cachedRecordId, + fragment: cacheReadFragment, + returnPartialData: true, + }); + + if (isUndefinedOrNull(record)) { + return null; + } + + return getRecordFromRecordNode({ + recordNode: record, + }) as CachedObjectRecord; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts new file mode 100644 index 0000000000..5de4074231 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts @@ -0,0 +1,34 @@ +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; + +export const getRecordFromRecordNode = ({ + recordNode, +}: { + recordNode: T; +}): T => { + return { + ...Object.fromEntries( + Object.entries(recordNode).map(([fieldName, value]) => { + if (isUndefinedOrNull(value)) { + return [fieldName, value]; + } + + if (typeof value === 'object' && isDefined(value.edges)) { + return [ + fieldName, + getRecordsFromRecordConnection({ recordConnection: value }), + ]; + } + + if (typeof value === 'object' && !isDefined(value.edges)) { + return [fieldName, getRecordFromRecordNode({ recordNode: value })]; + } + + return [fieldName, value]; + }), + ), + id: recordNode.id, + } as T; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts new file mode 100644 index 0000000000..82399a0030 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts @@ -0,0 +1,143 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; + +export const getRecordNodeFromRecord = ({ + objectMetadataItems, + objectMetadataItem, + queryFields, + record, + computeReferences = true, + isRootLevel = true, + depth = 1, +}: { + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: Pick< + ObjectMetadataItem, + 'fields' | 'namePlural' | 'nameSingular' + >; + queryFields?: Record; + computeReferences?: boolean; + isRootLevel?: boolean; + record: T | null; + depth?: number; +}) => { + if (isNull(record)) { + return null; + } + + const nodeTypeName = getNodeTypename(objectMetadataItem.nameSingular); + + if (!isRootLevel && computeReferences) { + return { + __ref: `${nodeTypeName}:${record.id}`, + } as unknown as CachedObjectRecord; // Todo Fix typing + } + + const nestedRecord = Object.fromEntries( + Object.entries(record) + .map(([fieldName, value]) => { + if (isDefined(queryFields) && !queryFields[fieldName]) { + return undefined; + } + + const field = objectMetadataItem.fields.find( + (field) => field.name === fieldName, + ); + + if (isUndefined(field)) { + return undefined; + } + + if ( + !isUndefined(depth) && + depth < 1 && + field.type === FieldMetadataType.Relation + ) { + return undefined; + } + + if (Array.isArray(value)) { + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.namePlural === fieldName, + ); + + if (!objectMetadataItem) { + return undefined; + } + + return [ + fieldName, + getRecordConnectionFromRecords({ + objectMetadataItems, + objectMetadataItem: objectMetadataItem, + records: value as ObjectRecord[], + queryFields: + queryFields?.[fieldName] === true || + isUndefined(queryFields?.[fieldName]) + ? undefined + : queryFields?.[fieldName], + withPageInfo: false, + isRootLevel: false, + computeReferences, + depth: depth - 1, + }), + ]; + } + + if (field.type === 'RELATION') { + if ( + isUndefined( + field.relationDefinition?.targetObjectMetadata.nameSingular, + ) + ) { + return undefined; + } + + if (isNull(value)) { + return [fieldName, null]; + } + + if (isUndefined(value?.id)) { + return undefined; + } + + const typeName = getObjectTypename( + field.relationDefinition?.targetObjectMetadata.nameSingular, + ); + + if (computeReferences) { + return [ + fieldName, + { + __ref: `${typeName}:${value.id}`, + }, + ]; + } + + return [ + fieldName, + { + __typename: typeName, + ...value, + }, + ]; + } + + return [fieldName, value]; + }) + .filter(isDefined), + ) as T; // Todo fix typing once we have investigated apollo edges / nodes removal + + return { + __typename: getNodeTypename(objectMetadataItem.nameSingular), + ...nestedRecord, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts index f52d8e0fb7..c427bd0559 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts @@ -1,3 +1,4 @@ +import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; @@ -6,5 +7,7 @@ export const getRecordsFromRecordConnection = ({ }: { recordConnection: ObjectRecordConnection; }): T[] => { - return recordConnection.edges.map((edge) => edge.node); + return recordConnection.edges.map((edge) => + getRecordFromRecordNode({ recordNode: edge.node }), + ); }; diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts similarity index 100% rename from packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isObjectRecordConnection.ts rename to packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isCachedObjectRecordConnection.ts b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts similarity index 95% rename from packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isCachedObjectRecordConnection.ts rename to packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts index 92186f3df3..a893901960 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/isCachedObjectRecordConnection.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection'; import { capitalize } from '~/utils/string/capitalize'; -export const isCachedObjectRecordConnection = ( +export const isObjectRecordConnectionWithRefs = ( objectNameSingular: string, storeValue: StoreValue, ): storeValue is CachedObjectRecordConnection => { diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts new file mode 100644 index 0000000000..3100edffa6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts @@ -0,0 +1,33 @@ +import { ApolloCache, Modifiers } from '@apollo/client/cache'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { capitalize } from '~/utils/string/capitalize'; + +export const modifyRecordFromCache = < + CachedObjectRecord extends ObjectRecord = ObjectRecord, +>({ + objectMetadataItem, + cache, + fieldModifiers, + recordId, +}: { + objectMetadataItem: ObjectMetadataItem; + cache: ApolloCache; + fieldModifiers: Modifiers; + recordId: string; +}) => { + if (isUndefinedOrNull(objectMetadataItem)) return; + + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem.nameSingular), + id: recordId, + }); + + cache.modify({ + id: cachedRecordId, + fields: fieldModifiers, + optimistic: true, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts new file mode 100644 index 0000000000..15636bc744 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts @@ -0,0 +1,59 @@ +import { ApolloCache } from '@apollo/client/cache'; +import gql from 'graphql-tag'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { capitalize } from '~/utils/string/capitalize'; + +export const updateRecordFromCache = ({ + objectMetadataItems, + objectMetadataItem, + cache, + record, +}: { + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: ObjectMetadataItem; + cache: ApolloCache; + record: T; +}) => { + if (isUndefinedOrNull(objectMetadataItem)) { + return null; + } + + const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); + + const cacheWriteFragment = gql` + fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery( + { + objectMetadataItems, + objectMetadataItem, + computeReferences: true, + }, + )} + `; + + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem.nameSingular), + id: record.id, + }); + + const recordWithConnection = getRecordNodeFromRecord({ + objectMetadataItems, + objectMetadataItem, + record, + depth: 1, + }); + + if (isUndefinedOrNull(recordWithConnection)) { + return; + } + + cache.writeFragment({ + id: cachedRecordId, + fragment: cacheWriteFragment, + data: recordWithConnection, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useMapConnectionToRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useMapConnectionToRecords.ts deleted file mode 100644 index 847b5c0e6c..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useMapConnectionToRecords.ts +++ /dev/null @@ -1,783 +0,0 @@ -import { Company } from '@/companies/types/Company'; -import { Favorite } from '@/favorites/types/Favorite'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; -import { Person } from '@/people/types/Person'; - -export const emptyConnectionMock: ObjectRecordConnection = { - edges: [], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - totalCount: 0, - __typename: 'ObjectRecordConnection', -}; - -export const companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock: ObjectRecordConnection< - Partial & - Pick & { - people: ObjectRecordConnection< - Pick & { - favorites: ObjectRecordConnection< - Pick - >; - } - >; - } -> = { - pageInfo: { - endCursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==', - hasNextPage: true, - hasPreviousPage: false, - startCursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==', - }, - edges: [ - { - cursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==', - node: { - id: '04b2e9f5-0713-40a5-8216-82802401d33e', - name: 'Qonto', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyIwZDk0MDk5Ny1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==', - node: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - people: { - edges: [ - { - cursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049190df', - name: { - firstName: 'Bertrand', - lastName: 'Voulzy', - }, - favorites: { - edges: [ - { - cursor: - 'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==', - node: { - id: 'c85a867c-5a8f-4861-8ed2-96c390248423', - personId: '240da2ec-2d40-4e49-8df4-9c6a049190df', - companyId: null, - position: 2, - }, - }, - ], - pageInfo: { - endCursor: - 'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==', - }, - totalCount: 1, - }, - }, - }, - { - cursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049190ef', - name: { - firstName: 'Madison', - lastName: 'Perez', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==', - node: { - id: '56955422-5d54-41b7-ba36-f0d20e1417ae', - name: { - firstName: 'Avery', - lastName: 'Carter', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==', - node: { - id: '755035db-623d-41fe-92e7-dd45b7c568e1', - name: { - firstName: 'Ethan', - lastName: 'Mitchell', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==', - node: { - id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35', - name: { - firstName: 'Elizabeth', - lastName: 'Baker', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==', - node: { - id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016', - name: { - firstName: 'Christopher', - lastName: 'Nelson', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==', - }, - totalCount: 6, - }, - }, - }, - { - cursor: 'WyIxMTg5OTVmMy01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==', - node: { - id: '118995f3-5d81-46d6-bf83-f7fd33ea6102', - name: 'Facebook', - people: { - edges: [ - { - cursor: - 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==', - node: { - id: '93c72d2e-f517-42fd-80ae-14173b3b70ae', - name: { - firstName: 'Christopher', - lastName: 'Gonzalez', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==', - node: { - id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e', - name: { - firstName: 'Ashley', - lastName: 'Parker', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==', - }, - totalCount: 2, - }, - }, - }, - { - cursor: 'WyIxZDNhMWM2ZS03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==', - node: { - id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944', - name: 'Netflix', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyI0NjBiNmZiMS1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==', - node: { - id: '460b6fb1-ed89-413a-b31a-962986e67bb4', - name: 'Microsoft', - people: { - edges: [ - { - cursor: - 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==', - node: { - id: '1d151852-490f-4466-8391-733cfd66a0c8', - name: { - firstName: 'Isabella', - lastName: 'Scott', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==', - node: { - id: '98406e26-80f1-4dff-b570-a74942528de3', - name: { - firstName: 'Matthew', - lastName: 'Green', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: - 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==', - node: { - id: '9b324a88-6784-4449-afdf-dc62cb8702f2', - name: { - firstName: 'Nicholas', - lastName: 'Wright', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==', - }, - totalCount: 3, - }, - }, - }, - { - cursor: 'WyI3YTkzZDFlNS0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==', - node: { - id: '7a93d1e5-3f74-492d-a101-2a70f50a1645', - name: 'Libeo', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyI4OWJiODI1Yy0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==', - node: { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', - name: 'Airbnb', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyI5ZDE2MmRlNi1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==', - node: { - id: '9d162de6-cfbf-4156-a790-e39854dcd4eb', - name: 'Claap', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyJhNjc0ZmE2Yy0xNDU1LTRjNTctYWZhZi1kZDVkYzA4NjM2MWQiXQ==', - node: { - id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d', - name: 'Algolia', - people: { - edges: [ - { - cursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049191df', - name: { - firstName: 'Lorie', - lastName: 'Vladim', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==', - }, - totalCount: 1, - }, - }, - }, - { - cursor: 'WyJhN2JjNjhkNS1mNzllLTQwZGQtYmQwNi1jMzZlNmFiYjQ2NzgiXQ==', - node: { - id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678', - name: 'Samsung', - people: { - edges: [ - { - cursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049191de', - name: { - firstName: 'Louis', - lastName: 'Duss', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==', - }, - totalCount: 1, - }, - }, - }, - { - cursor: 'WyJhYWZmY2ZiZC1mODZiLTQxOWYtYjc5NC0wMjMxOWFiZTg2MzciXQ==', - node: { - id: 'aaffcfbd-f86b-419f-b794-02319abe8637', - name: 'Hasura', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyJmMzNkYzI0Mi01NTE4LTQ1NTMtOTQzMy00MmQ4ZWI4MjgzNGIiXQ==', - node: { - id: 'f33dc242-5518-4553-9433-42d8eb82834b', - name: 'Wework', - people: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - { - cursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==', - node: { - id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', - name: 'Linkedin', - people: { - edges: [ - { - cursor: - 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==', - node: { - id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0', - name: { - firstName: 'Sylvie', - lastName: 'Palmer', - }, - favorites: { - edges: [ - { - cursor: - 'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==', - node: { - id: '37b97140-26b9-498c-837b-4f3de499ad83', - personId: '0aa00beb-ac73-4797-824e-87a1f5aea9e0', - companyId: null, - position: 1, - }, - }, - ], - pageInfo: { - endCursor: - 'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==', - }, - totalCount: 1, - }, - }, - }, - { - cursor: - 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==', - node: { - id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', - name: { - firstName: 'Christoph', - lastName: 'Callisto', - }, - favorites: { - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - totalCount: 0, - }, - }, - }, - ], - pageInfo: { - endCursor: - 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: - 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==', - }, - totalCount: 2, - }, - }, - }, - ], - totalCount: 13, -}; - -export const peopleWithTheirUniqueCompanies: ObjectRecordConnection< - Pick & { company: Pick } -> = { - pageInfo: { - endCursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==', - hasNextPage: false, - hasPreviousPage: false, - startCursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==', - }, - totalCount: 15, - edges: [ - { - cursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==', - node: { - id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0', - company: { - id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', - name: 'Linkedin', - }, - }, - }, - { - cursor: 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==', - node: { - id: '1d151852-490f-4466-8391-733cfd66a0c8', - company: { - id: '460b6fb1-ed89-413a-b31a-962986e67bb4', - name: 'Microsoft', - }, - }, - }, - { - cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049190df', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049190ef', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049191de', - company: { - id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678', - name: 'Samsung', - }, - }, - }, - { - cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==', - node: { - id: '240da2ec-2d40-4e49-8df4-9c6a049191df', - company: { - id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d', - name: 'Algolia', - }, - }, - }, - { - cursor: 'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==', - node: { - id: '56955422-5d54-41b7-ba36-f0d20e1417ae', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==', - node: { - id: '755035db-623d-41fe-92e7-dd45b7c568e1', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==', - node: { - id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', - company: { - id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', - name: 'Linkedin', - }, - }, - }, - { - cursor: 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==', - node: { - id: '93c72d2e-f517-42fd-80ae-14173b3b70ae', - company: { - id: '118995f3-5d81-46d6-bf83-f7fd33ea6102', - name: 'Facebook', - }, - }, - }, - { - cursor: 'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==', - node: { - id: '98406e26-80f1-4dff-b570-a74942528de3', - company: { - id: '460b6fb1-ed89-413a-b31a-962986e67bb4', - name: 'Microsoft', - }, - }, - }, - { - cursor: 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==', - node: { - id: '9b324a88-6784-4449-afdf-dc62cb8702f2', - company: { - id: '460b6fb1-ed89-413a-b31a-962986e67bb4', - name: 'Microsoft', - }, - }, - }, - { - cursor: 'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==', - node: { - id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==', - node: { - id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016', - company: { - id: '0d940997-c21e-4ec2-873b-de4264d89025', - name: 'Google', - }, - }, - }, - { - cursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==', - node: { - id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e', - company: { - id: '118995f3-5d81-46d6-bf83-f7fd33ea6102', - name: 'Facebook', - }, - }, - }, - ], -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx index 58a9e3fc22..837af4dd2b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx @@ -53,7 +53,6 @@ describe('useCreateOneRecord', () => { await act(async () => { const res = await result.current.createOneRecord(input); - console.log('res', res); expect(res).toBeDefined(); expect(res).toHaveProperty('id', personId); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx index 480e74bab6..57e18be23c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx @@ -84,14 +84,5 @@ describe('useFindManyRecords', () => { expect(result.current.loading).toBe(true); expect(result.current.error).toBeUndefined(); expect(result.current.records.length).toBe(0); - - // FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory - // await waitFor(() => { - // expect(result.current.loading).toBe(false); - // expect(result.current.records).toBeDefined(); - - // console.log({ res: result.current.records }); - // expect(result.current.records.length > 0).toBe(true); - // }); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx index 9120bb026e..df7c904261 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx @@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; const Wrapper = ({ children }: { children: ReactNode }) => ( {children} diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx deleted file mode 100644 index 6d58d3b7db..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { isNonEmptyArray } from '@sniptt/guards'; -import { renderHook } from '@testing-library/react'; - -import { Company } from '@/companies/types/Company'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock, - emptyConnectionMock, - peopleWithTheirUniqueCompanies, -} from '@/object-record/hooks/__mocks__/useMapConnectionToRecords'; -import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; -import { Person } from '@/people/types/Person'; -import { getJestHookWrapper } from '~/testing/jest/getJestHookWrapper'; -import { isDefined } from '~/utils/isDefined'; - -const Wrapper = getJestHookWrapper({ - apolloMocks: [], - onInitializeRecoilSnapshot: (snapshot) => { - snapshot.set(objectMetadataItemsState, getObjectMetadataItemsMock()); - }, -}); - -describe('useMapConnectionToRecords', () => { - it('Empty edges - should return an empty array if no edge', async () => { - const { result } = renderHook( - () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const records = mapConnectionToRecords({ - objectNameSingular: CoreObjectNameSingular.Company, - objectRecordConnection: emptyConnectionMock, - depth: 5, - }); - - return records; - }, - { - wrapper: Wrapper, - }, - ); - - expect(Array.isArray(result.current)).toBe(true); - }); - - it('No relation fields - should return an array of company records', async () => { - const { result } = renderHook( - () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const records = mapConnectionToRecords({ - objectNameSingular: CoreObjectNameSingular.Company, - objectRecordConnection: - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock, - depth: 5, - }); - - return records; - }, - { - wrapper: Wrapper, - }, - ); - - expect(Array.isArray(result.current)).toBe(true); - }); - - it('n+1 relation fields - should return an array of company records with their people records', async () => { - const { result } = renderHook( - () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const records = mapConnectionToRecords({ - objectNameSingular: CoreObjectNameSingular.Company, - objectRecordConnection: - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock, - depth: 5, - }); - - return records; - }, - { - wrapper: Wrapper, - }, - ); - - const secondCompanyMock = - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock - .edges[1]; - - const secondCompanyPeopleMock = secondCompanyMock.node.people.edges.map( - (edge) => edge.node, - ); - - const companiesResult = result.current; - const secondCompanyResult = result.current[1]; - const secondCompanyPeopleResult = secondCompanyResult.people; - - expect(isNonEmptyArray(companiesResult)).toBe(true); - expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id); - expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true); - expect(secondCompanyPeopleResult[0].id).toEqual( - secondCompanyPeopleMock[0].id, - ); - }); - - it('n+2 relation fields - should return an array of company records with their people records with their favorites records', async () => { - const { result } = renderHook( - () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const records = mapConnectionToRecords({ - objectNameSingular: CoreObjectNameSingular.Company, - objectRecordConnection: - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock, - depth: 5, - }); - - return records; - }, - { - wrapper: Wrapper, - }, - ); - - const secondCompanyMock = - companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock - .edges[1]; - - const secondCompanyPeopleMock = secondCompanyMock.node.people; - - const secondCompanyFirstPersonMock = secondCompanyPeopleMock.edges[0].node; - - const secondCompanyFirstPersonFavoritesMock = - secondCompanyFirstPersonMock.favorites; - - const companiesResult = result.current; - const secondCompanyResult = companiesResult[1]; - const secondCompanyPeopleResult = secondCompanyResult.people; - const secondCompanyFirstPersonResult = secondCompanyPeopleResult[0]; - const secondCompanyFirstPersonFavoritesResult = - secondCompanyFirstPersonResult.favorites; - - expect(isNonEmptyArray(companiesResult)).toBe(true); - expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id); - expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true); - expect(secondCompanyFirstPersonResult.id).toEqual( - secondCompanyFirstPersonMock.id, - ); - expect(isNonEmptyArray(secondCompanyFirstPersonFavoritesResult)).toBe(true); - expect(secondCompanyFirstPersonFavoritesResult[0].id).toEqual( - secondCompanyFirstPersonFavoritesMock.edges[0].node.id, - ); - }); - - it("n+1 relation field TO_ONE_OBJECT - should return an array of people records with their company, mapConnectionToRecords shouldn't try to parse TO_ONE_OBJECT", async () => { - const { result } = renderHook( - () => { - const mapConnectionToRecords = useMapConnectionToRecords(); - - const records = mapConnectionToRecords({ - objectNameSingular: CoreObjectNameSingular.Person, - objectRecordConnection: peopleWithTheirUniqueCompanies, - depth: 5, - }); - - return records as (Person & { company: Company })[]; - }, - { - wrapper: Wrapper, - }, - ); - - const firstPersonMock = peopleWithTheirUniqueCompanies.edges[0].node; - - const firstPersonsCompanyMock = firstPersonMock.company; - - const peopleResult = result.current; - - const firstPersonResult = result.current[0]; - const firstPersonsCompanyresult = firstPersonResult.company; - - expect(isNonEmptyArray(peopleResult)).toBe(true); - expect(firstPersonResult.id).toBe(firstPersonMock.id); - - expect(isDefined(firstPersonsCompanyresult)).toBe(true); - expect(firstPersonsCompanyresult.id).toEqual(firstPersonsCompanyMock.id); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useModifyRecordFromCache.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useModifyRecordFromCache.test.tsx deleted file mode 100644 index 9d1ac683d2..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useModifyRecordFromCache.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ReactNode } from 'react'; -import { useApolloClient } from '@apollo/client'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); - -const recordId = '91408718-a29f-4678-b573-c791e8664c2a'; - -describe('useModifyRecordFromCache', () => { - it('should work as expected', async () => { - const { result } = renderHook( - () => { - const apolloClient = useApolloClient(); - const mockObjectMetadataItems = getObjectMetadataItemsMock(); - - const personMetadataItem = mockObjectMetadataItems.find( - (item) => item.nameSingular === 'person', - )!; - - return { - modifyRecordFromCache: useModifyRecordFromCache({ - objectMetadataItem: personMetadataItem, - }), - cache: apolloClient.cache, - }; - }, - { - wrapper: Wrapper, - }, - ); - - const spy = jest.spyOn(result.current.cache, 'modify'); - - act(() => { - result.current.modifyRecordFromCache(recordId, {}); - }); - - expect(spy).toHaveBeenCalledWith({ - id: `Person:${recordId}`, - fields: {}, - }); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index 5e133b8688..e2926f2c89 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -2,57 +2,87 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; -import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; +import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { + getCreateManyRecordsMutationResponseField, + useGenerateCreateManyRecordMutation, +} from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; +import { isDefined } from '~/utils/isDefined'; -type CreateManyRecordsOptions = { - skipOptimisticEffect?: boolean; +type useCreateManyRecordsProps = { + objectNameSingular: string; + queryFields?: Record; + depth?: number; + skipPostOptmisticEffect?: boolean; }; export const useCreateManyRecords = < CreatedObjectRecord extends ObjectRecord = ObjectRecord, >({ objectNameSingular, -}: ObjectMetadataItemIdentifier) => { + queryFields, + depth = 1, + skipPostOptmisticEffect = false, +}: useCreateManyRecordsProps) => { const apolloClient = useApolloClient(); - const { objectMetadataItem, createManyRecordsMutation } = - useObjectMetadataItem({ - objectNameSingular, - }); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem, - }); + const createManyRecordsMutation = useGenerateCreateManyRecordMutation({ + objectMetadataItem, + queryFields, + depth, + }); + + const createOneRecordInCache = useCreateOneRecordInCache({ + objectMetadataItem, + }); const { objectMetadataItems } = useObjectMetadataItems(); const createManyRecords = async ( - data: Partial[], - options?: CreateManyRecordsOptions, + recordsToCreate: Partial[], ) => { - const sanitizedCreateManyRecordsInput = data.map((input) => { - const idForCreation = input.id ?? v4(); + const sanitizedCreateManyRecordsInput = recordsToCreate.map( + (recordToCreate) => { + const idForCreation = recordToCreate?.id ?? v4(); - const sanitizedRecordInput = sanitizeRecordInput({ - objectMetadataItem, - recordInput: { ...input, id: idForCreation }, - }); - - return sanitizedRecordInput; - }); - - const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map( - (record) => - generateObjectRecordOptimisticResponse(record), + return { + ...sanitizeRecordInput({ + objectMetadataItem, + recordInput: recordToCreate, + }), + id: idForCreation, + }; + }, ); + const recordsCreatedInCache = []; + + for (const recordToCreate of sanitizedCreateManyRecordsInput) { + const recordCreatedInCache = createOneRecordInCache(recordToCreate); + + if (isDefined(recordCreatedInCache)) { + recordsCreatedInCache.push(recordCreatedInCache); + } + } + + if (recordsCreatedInCache.length > 0) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: recordsCreatedInCache, + objectMetadataItems, + }); + } + const mutationResponseField = getCreateManyRecordsMutationResponseField( objectMetadataItem.namePlural, ); @@ -62,25 +92,18 @@ export const useCreateManyRecords = < variables: { data: sanitizedCreateManyRecordsInput, }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: optimisticallyCreatedRecords, - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; + update: (cache, { data }) => { + const records = data?.[mutationResponseField]; - if (!records?.length) return; + if (!records?.length || skipPostOptmisticEffect) return; - triggerCreateRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToCreate: records, - objectMetadataItems, - }); - }, + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: records, + objectMetadataItems, + }); + }, }); return createdObjects.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts deleted file mode 100644 index 779294692b..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { v4 } from 'uuid'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isDefined } from '~/utils/isDefined'; - -export const useCreateManyRecordsInCache = ({ - objectNameSingular, -}: ObjectMetadataItemIdentifier) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem, - }); - - const addRecordInCache = useAddRecordInCache({ - objectMetadataItem, - }); - - const createManyRecordsInCache = (data: Partial[]) => { - const recordsWithId = data.map((record) => ({ - ...record, - id: (record.id as string) ?? v4(), - })); - - const createdRecordsInCache = [] as T[]; - - for (const record of recordsWithId) { - const generatedCachedObjectRecord = - generateObjectRecordOptimisticResponse(record); - - if (isDefined(generatedCachedObjectRecord)) { - addRecordInCache(generatedCachedObjectRecord); - - createdRecordsInCache.push(generatedCachedObjectRecord); - } - } - - return createdRecordsInCache; - }; - - return { createManyRecordsInCache }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index 938f848a89..59b560fcd5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -2,55 +2,73 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; -import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; +import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { + getCreateOneRecordMutationResponseField, + useGenerateCreateOneRecordMutation, +} from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; +import { isDefined } from '~/utils/isDefined'; type useCreateOneRecordProps = { objectNameSingular: string; -}; - -type CreateOneRecordOptions = { - skipOptimisticEffect?: boolean; + queryFields?: Record; + depth?: number; + skipPostOptmisticEffect?: boolean; }; export const useCreateOneRecord = < CreatedObjectRecord extends ObjectRecord = ObjectRecord, >({ objectNameSingular, + queryFields, + depth = 1, + skipPostOptmisticEffect = false, }: useCreateOneRecordProps) => { const apolloClient = useApolloClient(); - const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem( - { objectNameSingular }, - ); + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem, - }); + const createOneRecordMutation = useGenerateCreateOneRecordMutation({ + objectMetadataItem, + queryFields, + depth, + }); + + const createOneRecordInCache = useCreateOneRecordInCache({ + objectMetadataItem, + }); const { objectMetadataItems } = useObjectMetadataItems(); - const createOneRecord = async ( - input: Partial, - options?: CreateOneRecordOptions, - ) => { + const createOneRecord = async (input: Partial) => { const idForCreation = input.id ?? v4(); - const sanitizedCreateOneRecordInput = sanitizeRecordInput({ - objectMetadataItem, - recordInput: { ...input, id: idForCreation }, + const sanitizedInput = { + ...sanitizeRecordInput({ + objectMetadataItem, + recordInput: input, + }), + id: idForCreation, + }; + + const recordCreatedInCache = createOneRecordInCache({ + ...input, + id: idForCreation, }); - const optimisticallyCreatedRecord = - generateObjectRecordOptimisticResponse({ - ...input, - ...sanitizedCreateOneRecordInput, + if (isDefined(recordCreatedInCache)) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: [recordCreatedInCache], + objectMetadataItems, }); + } const mutationResponseField = getCreateOneRecordMutationResponseField(objectNameSingular); @@ -58,27 +76,20 @@ export const useCreateOneRecord = < const createdObject = await apolloClient.mutate({ mutation: createOneRecordMutation, variables: { - input: sanitizedCreateOneRecordInput, + input: sanitizedInput, }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: optimisticallyCreatedRecord, - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const record = data?.[mutationResponseField]; + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; - if (!record) return; + if (!record || skipPostOptmisticEffect) return; - triggerCreateRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToCreate: [record], - objectMetadataItems, - }); - }, + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + }); + }, }); return createdObject.data?.[mutationResponseField] ?? null; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts deleted file mode 100644 index ebd22838c4..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - -type useCreateOneRecordInCacheProps = { - objectNameSingular: string; -}; - -export const useCreateOneRecordInCache = ({ - objectNameSingular, -}: useCreateOneRecordInCacheProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem, - }); - - const addRecordInCache = useAddRecordInCache({ - objectMetadataItem, - }); - - const createOneRecordInCache = (input: ObjectRecord) => { - const generatedCachedObjectRecord = - generateObjectRecordOptimisticResponse(input); - - addRecordInCache(generatedCachedObjectRecord); - - return generatedCachedObjectRecord as T; - }; - - return { - createOneRecordInCache, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts index 3a88f95ce8..19f0c38cc1 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts @@ -3,8 +3,8 @@ import { useQuery } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/hooks/useGenerateFindDuplicateRecordsQuery'; -import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -60,16 +60,14 @@ export const useFindDuplicateRecords = ({ const objectRecordConnection = data?.[queryResponseField]; - const mapConnectionToRecords = useMapConnectionToRecords(); - const records = useMemo( () => - mapConnectionToRecords({ - objectRecordConnection, - objectNameSingular, - depth: 5, - }) as T[], - [mapConnectionToRecords, objectRecordConnection, objectNameSingular], + objectRecordConnection + ? (getRecordsFromRecordConnection({ + recordConnection: objectRecordConnection, + }) as T[]) + : [], + [objectRecordConnection], ); return { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index 4531e47e6e..ec39dd385b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -7,7 +7,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; @@ -22,7 +22,6 @@ import { cursorFamilyState } from '../states/cursorFamilyState'; import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; import { ObjectRecordQueryResult } from '../types/ObjectRecordQueryResult'; -import { mapPaginatedRecordsToRecords } from '../utils/mapPaginatedRecordsToRecords'; export const useFindManyRecords = ({ objectNameSingular, @@ -31,17 +30,20 @@ export const useFindManyRecords = ({ limit, onCompleted, skip, - useRecordsWithoutConnection = false, - depth, + depth = 1, + queryFields, }: ObjectMetadataItemIdentifier & ObjectRecordQueryVariables & { onCompleted?: ( - data: ObjectRecordConnection, - pageInfo: ObjectRecordConnection['pageInfo'], + records: T[], + options?: { + pageInfo?: ObjectRecordConnection['pageInfo']; + totalCount?: number; + }, ) => void; skip?: boolean; - useRecordsWithoutConnection?: boolean; depth?: number; + queryFields?: Record; }) => { const findManyQueryStateIdentifier = objectNameSingular + @@ -66,6 +68,7 @@ export const useFindManyRecords = ({ objectNameSingular, }, depth, + queryFields, ); const { enqueueSnackBar } = useSnackBar(); @@ -81,9 +84,20 @@ export const useFindManyRecords = ({ orderBy, }, onCompleted: (data) => { + if (!isDefined(data)) { + onCompleted?.([]); + } + const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; - onCompleted?.(data[objectMetadataItem.namePlural], pageInfo); + const records = getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, + }); if (isDefined(data?.[objectMetadataItem.namePlural])) { setLastCursor(pageInfo.endCursor ?? ''); @@ -132,24 +146,24 @@ export const useFindManyRecords = ({ const pageInfo = fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; + if (isDefined(data?.[objectMetadataItem.namePlural])) { setLastCursor(pageInfo.endCursor ?? ''); setHasNextPage(pageInfo.hasNextPage ?? false); } - onCompleted?.( - { - __typename: `${capitalize( - objectMetadataItem.nameSingular, - )}Connection`, + const records = getRecordsFromRecordConnection({ + recordConnection: { edges: newEdges, - pageInfo: - fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural].totalCount, + pageInfo, }, + }) as T[]; + + onCompleted?.(records, { pageInfo, - ); + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount, + }); return Object.assign({}, prev, { [objectMetadataItem.namePlural]: { @@ -196,40 +210,23 @@ export const useFindManyRecords = ({ enqueueSnackBar, ]); - // TODO: remove this and use only mapConnectionToRecords when we've finished the refactor + const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0; + const records = useMemo( () => - mapPaginatedRecordsToRecords({ - pagedRecords: data, - objectNamePlural: objectMetadataItem.namePlural, - }) as T[], - [data, objectMetadataItem], - ); + data?.[objectMetadataItem.namePlural] + ? getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) + : ([] as T[]), - const mapConnectionToRecords = useMapConnectionToRecords(); - - const recordsWithoutConnection = useMemo( - () => - useRecordsWithoutConnection - ? (mapConnectionToRecords({ - objectRecordConnection: data?.[objectMetadataItem.namePlural], - objectNameSingular, - depth: 5, - }) as T[]) - : [], - [ - data, - objectNameSingular, - objectMetadataItem.namePlural, - mapConnectionToRecords, - useRecordsWithoutConnection, - ], + [data, objectMetadataItem.namePlural], ); return { objectMetadataItem, - records: useRecordsWithoutConnection ? recordsWithoutConnection : records, - totalCount: data?.[objectMetadataItem.namePlural].totalCount || 0, + records, + totalCount, loading, error, fetchMoreRecords, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts index ade2c951e3..269c1715c0 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts @@ -1,8 +1,11 @@ +import { useMemo } from 'react'; import { useQuery } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isDefined } from '~/utils/isDefined'; // TODO: fix connection in relation => automatically change to an array export const useFindOneRecord = ({ @@ -28,11 +31,29 @@ export const useFindOneRecord = ({ >(findOneRecordQuery, { skip: !objectMetadataItem || !objectRecordId || skip, variables: { objectRecordId }, - onCompleted: (data) => onCompleted?.(data[objectNameSingular]), + onCompleted: (data) => { + const recordWithoutConnection = getRecordFromRecordNode({ + recordNode: { ...data[objectNameSingular] }, + }); + + if (isDefined(recordWithoutConnection)) { + onCompleted?.(recordWithoutConnection); + } + }, }); + const recordWithoutConnection = useMemo( + () => + data?.[objectNameSingular] + ? getRecordFromRecordNode({ + recordNode: data?.[objectNameSingular], + }) + : undefined, + [data, objectNameSingular], + ); + return { - record: data?.[objectNameSingular] || undefined, + record: recordWithoutConnection, loading, error, }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts index 686a5cc5ca..91d33f9ad9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts @@ -14,8 +14,12 @@ export const getCreateManyRecordsMutationResponseField = ( export const useGenerateCreateManyRecordMutation = ({ objectMetadataItem, + queryFields, + depth = 1, }: { objectMetadataItem: ObjectMetadataItem; + queryFields?: Record; + depth?: number; }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -34,6 +38,8 @@ export const useGenerateCreateManyRecordMutation = ({ ${mutationResponseField}(data: $data) ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, objectMetadataItem, + queryFields, + depth, })} }`; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts index b6837892af..291a15d853 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts @@ -14,8 +14,12 @@ export const getCreateOneRecordMutationResponseField = ( export const useGenerateCreateOneRecordMutation = ({ objectMetadataItem, + queryFields, + depth = 1, }: { objectMetadataItem: ObjectMetadataItem; + queryFields?: Record; + depth?: number; }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -34,6 +38,8 @@ export const useGenerateCreateOneRecordMutation = ({ ${mutationResponseField}(data: $input) ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, objectMetadataItem, + queryFields, + depth, })} } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts index 5a21fe3de6..e190ca5f23 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts @@ -12,14 +12,16 @@ export const useGenerateFindManyRecordsQuery = () => { return ({ objectMetadataItem, depth, - eagerLoadedRelations, + queryFields, + computeReferences = false, }: { objectMetadataItem: Pick< ObjectMetadataItem, 'fields' | 'nameSingular' | 'namePlural' >; depth?: number; - eagerLoadedRelations?: Record; + queryFields?: Record; + computeReferences?: boolean; }) => gql` query FindMany${capitalize( objectMetadataItem.namePlural, @@ -36,7 +38,8 @@ export const useGenerateFindManyRecordsQuery = () => { objectMetadataItems, objectMetadataItem, depth, - eagerLoadedRelations, + queryFields, + computeReferences, })} cursor } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts index 8880f3a0b8..5bda2bea6b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts @@ -14,8 +14,12 @@ export const getUpdateOneRecordMutationResponseField = ( export const useGenerateUpdateOneRecordMutation = ({ objectMetadataItem, + depth = 1, + computeReferences = false, }: { objectMetadataItem: ObjectMetadataItem; + depth?: number; + computeReferences?: boolean; }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -35,6 +39,8 @@ export const useGenerateUpdateOneRecordMutation = ({ { objectMetadataItems, objectMetadataItem, + depth, + computeReferences, }, )} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts deleted file mode 100644 index a682f1e2b6..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { useCallback } from 'react'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { produce } from 'immer'; -import { useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; -import { FieldMetadataType } from '~/generated/graphql'; -import { isDefined } from '~/utils/isDefined'; - -export const useMapConnectionToRecords = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const mapConnectionToRecords = useCallback( - ({ - objectRecordConnection, - objectNameSingular, - objectNamePlural, - depth, - }: { - objectRecordConnection: ObjectRecordConnection | undefined | null; - objectNameSingular?: string; - objectNamePlural?: string; - depth: number; - }): ObjectRecord[] => { - if ( - !isDefined(objectRecordConnection) || - !isNonEmptyArray(objectMetadataItems) - ) { - return []; - } - - const currentLevelObjectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular || - objectMetadataItem.namePlural === objectNamePlural, - ); - - if (!currentLevelObjectMetadataItem) { - throw new Error( - `Could not find object metadata item for object name singular "${objectNameSingular}" in mapConnectionToRecords`, - ); - } - - const relationFields = currentLevelObjectMetadataItem.fields.filter( - (field) => field.type === FieldMetadataType.Relation, - ); - - const objectRecords = [ - ...(objectRecordConnection.edges?.map((edge) => edge.node) ?? []), - ]; - - return produce(objectRecords, (objectRecordsDraft) => { - for (const objectRecordDraft of objectRecordsDraft) { - for (const relationField of relationFields) { - const relationType = parseFieldRelationType(relationField); - - if ( - relationType === 'TO_ONE_OBJECT' || - relationType === 'FROM_ONE_OBJECT' - ) { - continue; - } - - const relatedObjectMetadataSingularName = - relationField.toRelationMetadata?.fromObjectMetadata - .nameSingular ?? - relationField.fromRelationMetadata?.toObjectMetadata - .nameSingular ?? - null; - - const relationFieldMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === - relatedObjectMetadataSingularName, - ); - - if ( - !relationFieldMetadataItem || - !isDefined(relatedObjectMetadataSingularName) - ) { - throw new Error( - `Could not find relation object metadata item for object name plural ${relationField.name} in mapConnectionToRecords`, - ); - } - - const relationConnection = objectRecordDraft?.[ - relationField.name - ] as ObjectRecordConnection | undefined | null; - - if (!isDefined(relationConnection)) { - continue; - } - - const relationConnectionMappedToRecords = mapConnectionToRecords({ - objectRecordConnection: relationConnection, - objectNameSingular: relatedObjectMetadataSingularName, - depth: depth - 1, - }); - - (objectRecordDraft as any)[relationField.name] = - relationConnectionMappedToRecords; - } - } - }) as ObjectRecord[]; - }, - [objectMetadataItems], - ); - - return mapConnectionToRecords; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index e2e11bd00c..073cb86fd1 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -3,29 +3,29 @@ import { useApolloClient } from '@apollo/client'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; type useUpdateOneRecordProps = { objectNameSingular: string; + queryFields?: Record; + depth?: number; }; export const useUpdateOneRecord = < UpdatedObjectRecord extends ObjectRecord = ObjectRecord, >({ objectNameSingular, + queryFields, + depth = 1, }: useUpdateOneRecordProps) => { const apolloClient = useApolloClient(); const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } = - useObjectMetadataItem({ objectNameSingular }, 1); - - const { generateObjectRecordOptimisticResponse } = - useGenerateObjectRecordOptimisticResponse({ - objectMetadataItem, - }); + useObjectMetadataItem({ objectNameSingular }, depth, queryFields); const { objectMetadataItems } = useObjectMetadataItems(); @@ -36,17 +36,57 @@ export const useUpdateOneRecord = < idToUpdate: string; updateOneRecordInput: Partial>; }) => { + const sanitizedInput = { + ...sanitizeRecordInput({ + objectMetadataItem, + recordInput: updateOneRecordInput, + }), + }; + const cachedRecord = getRecordFromCache(idToUpdate); - const sanitizedUpdateOneRecordInput = sanitizeRecordInput({ + const cachedRecordWithConnection = getRecordNodeFromRecord({ + record: cachedRecord, objectMetadataItem, - recordInput: updateOneRecordInput, + objectMetadataItems, + depth, + queryFields, + computeReferences: true, }); - const optimisticallyUpdatedRecord = generateObjectRecordOptimisticResponse({ - ...(cachedRecord ?? {}), - ...sanitizedUpdateOneRecordInput, - id: idToUpdate, + const optimisticRecord = { + ...cachedRecord, + ...sanitizedInput, + ...{ id: idToUpdate }, + }; + + const optimisticRecordWithConnection = + getRecordNodeFromRecord({ + record: optimisticRecord, + objectMetadataItem, + objectMetadataItems, + depth, + queryFields, + computeReferences: true, + }); + + if (!optimisticRecordWithConnection || !cachedRecordWithConnection) { + return null; + } + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: optimisticRecord, + }); + + triggerUpdateRecordOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + currentRecord: cachedRecordWithConnection, + updatedRecord: optimisticRecordWithConnection, + objectMetadataItems, }); const mutationResponseField = @@ -56,10 +96,7 @@ export const useUpdateOneRecord = < mutation: updateOneRecordMutation, variables: { idToUpdate, - input: sanitizedUpdateOneRecordInput, - }, - optimisticResponse: { - [mutationResponseField]: optimisticallyUpdatedRecord, + input: sanitizedInput, }, update: (cache, { data }) => { const record = data?.[mutationResponseField]; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpsertRecordFieldFromState.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpsertRecordFieldFromState.ts deleted file mode 100644 index b89bb5e3b2..0000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpsertRecordFieldFromState.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useUpsertRecordFieldFromState = () => - useRecoilCallback( - ({ set }) => - ({ - record, - fieldName, - }: { - record: T; - fieldName: F extends string ? F : never; - }) => - set( - recordStoreFamilySelector({ recordId: record.id, fieldName }), - (previousField) => - isDeeplyEqual(previousField, record[fieldName]) - ? previousField - : record[fieldName], - ), - [], - ); diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useFindManyRecordsForMultipleMetadataItems.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useFindManyRecordsForMultipleMetadataItems.ts new file mode 100644 index 0000000000..13b7d5a7a9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useFindManyRecordsForMultipleMetadataItems.ts @@ -0,0 +1,44 @@ +import { useQuery } from '@apollo/client'; + +import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; + +export const useFindManyRecordsForMultipleMetadataItems = ({ + objectMetadataItems, + skip = false, + depth = 2, +}: { + objectMetadataItems: ObjectMetadataItem[]; + skip: boolean; + depth?: number; +}) => { + const findManyQuery = useGenerateFindManyRecordsForMultipleMetadataItemsQuery( + { + targetObjectMetadataItems: objectMetadataItems, + depth, + }, + ); + + const { data } = useQuery( + findManyQuery ?? EMPTY_QUERY, + { + skip, + }, + ); + + const resultWithoutConnection = Object.fromEntries( + Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [ + namePlural, + getRecordsFromRecordConnection({ + recordConnection: objectRecordConnection, + }), + ]), + ); + + return { + result: resultWithoutConnection, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts rename to packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts diff --git a/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts b/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts index fc1c16a14e..31d1191b0f 100644 --- a/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts +++ b/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts @@ -3,5 +3,7 @@ import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQu export type QueryKey = { objectNameSingular: string; variables: ObjectRecordQueryVariables; - depth: number; + depth?: number; + fields?: Record; // Todo: Fields should be required + fieldsFactory?: (fieldsFactoryParam: any) => Record; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index e3db244553..6757c1bc04 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -109,6 +109,19 @@ export const usePersistField = () => { valueToPersist, ); + if (fieldIsRelation) { + updateRecord?.({ + variables: { + where: { id: entityId }, + updateOneRecordInput: { + [fieldName]: valueToPersist, + [`${fieldName}Id`]: valueToPersist?.id ?? null, + }, + }, + }); + return; + } + updateRecord?.({ variables: { where: { id: entityId }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx index 9afa22d277..1ac509d209 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx @@ -68,7 +68,7 @@ const AddressInputWithContext = ({ onEscape={onEscape} onClickOutside={onClickOutside} value={value} - hotkeyScope="" + hotkeyScope="hotkey-scope" onTab={onTab} onShiftTab={onShiftTab} /> @@ -96,7 +96,7 @@ const clearMocksDecorator: Decorator = (Story, context) => { }; const meta: Meta = { - title: 'UI/Data/Field/Input/AddressInput', + title: 'UI/Data/Field/Input/AddressFieldInput', component: AddressInputWithContext, args: { value: 'text', diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts index 3b0f98e7fa..ac88dfb713 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts @@ -8,6 +8,11 @@ export type FieldDefinitionRelationType = | 'TO_MANY_OBJECTS' | 'TO_ONE_OBJECT'; +export type RelationDirections = { + from: FieldDefinitionRelationType; + to: FieldDefinitionRelationType; +}; + export type FieldDefinition = { fieldMetadataId: string; label: string; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 147edd7f88..52f4a1a6d0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -2,14 +2,13 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; -import { useFindManyRecords } from '../../hooks/useFindManyRecords'; - export const useFindManyParams = ( objectNameSingular: string, recordTableId?: string, diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts index 23d64cae4f..7dbe87226a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts @@ -141,8 +141,8 @@ export const useExportTableData = ({ ...usedFindManyParams, depth: 0, limit: pageSize, - onCompleted: (_data, { hasNextPage }) => { - setHasNextPage(hasNextPage ?? false); + onCompleted: (_data, options) => { + setHasNextPage(options?.pageInfo?.hasNextPage ?? false); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx index 036549e61e..0f62468876 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; -import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { Activity } from '@/activities/types/Activity'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; @@ -15,7 +14,7 @@ export const RecordShowContainer = ({ objectRecordId: string; objectNameSingular: string; }) => { - const { record, loading } = useFindOneRecord({ + const { record: activity, loading } = useFindOneRecord({ objectRecordId, objectNameSingular, depth: 3, @@ -35,14 +34,9 @@ export const RecordShowContainer = ({ } }, [loading, recordLoading, setRecordLoading]); - const { makeActivityWithoutConnection } = useActivityConnectionUtils(); - useEffect(() => { - if (!loading && isDefined(record)) { - const { activity: activityWithoutConnection } = - makeActivityWithoutConnection(record as any); - - setRecordStore(activityWithoutConnection as Activity); + if (!loading && isDefined(activity)) { + setRecordStore(activity); } - }, [loading, record, setRecordStore, makeActivityWithoutConnection]); + }, [loading, setRecordStore, activity]); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 76035fb781..ec337f8986 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -51,16 +51,17 @@ export const RecordDetailRelationSection = () => { ); const fieldValue = useRecoilValue< - ({ id: string } & Record) | null + ({ id: string } & Record) | ObjectRecord[] | null >(recordStoreFamilySelector({ recordId: entityId, fieldName })); + // TODO: use new relation type const isToOneObject = relationType === 'TO_ONE_OBJECT'; const isFromManyObjects = relationType === 'FROM_MANY_OBJECTS'; const relationRecords: ObjectRecord[] = fieldValue && isToOneObject - ? [fieldValue] - : fieldValue?.edges.map(({ node }: { node: ObjectRecord }) => node) ?? []; + ? [fieldValue as ObjectRecord] + : (fieldValue as ObjectRecord[]) ?? []; const relationRecordIds = relationRecords.map(({ id }) => id); const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 019a8c1504..68ca007a00 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -81,7 +81,7 @@ const StyledTable = styled.table<{ z-index: 6; } - thead th:nth-child(n + 3) { + thead th:nth-of-type(n + 3) { top: 0; z-index: 5; position: sticky; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts index 3f91947262..688cd9f120 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil'; import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; import { MultiObjectRecordQueryResult, diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts index 675dd938df..75395c27aa 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil'; import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; import { MultiObjectRecordQueryResult, diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts index 0cfdaebf79..008c8b2f4e 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts @@ -4,7 +4,7 @@ import { isNonEmptyArray } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; import { MultiObjectRecordQueryResult, diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts index 52bb216021..ba79640f1f 100644 --- a/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts +++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordConnection.ts @@ -10,6 +10,7 @@ export type ObjectRecordConnection = { hasPreviousPage?: boolean; startCursor?: Nullable; endCursor?: Nullable; + totalCount?: number; }; - totalCount: number; + totalCount?: number; }; diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 80db046624..2d8b314931 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -2,7 +2,6 @@ import { isNonEmptyString } from '@sniptt/guards'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataType } from '~/generated/graphql'; -import { capitalize } from '~/utils/string/capitalize'; export const generateEmptyFieldValue = ( fieldMetadataItem: FieldMetadataItem, @@ -53,8 +52,6 @@ export const generateEmptyFieldValue = ( return true; } case FieldMetadataType.Relation: { - // TODO: refactor with relationDefiniton once the PR is merged : https://github.com/twentyhq/twenty/pull/4378 - // so we can directly check the relation type from this field point of view. if ( !isNonEmptyString( fieldMetadataItem.fromRelationMetadata?.toObjectMetadata @@ -64,12 +61,7 @@ export const generateEmptyFieldValue = ( return null; } - return { - __typename: `${capitalize( - fieldMetadataItem.fromRelationMetadata.toObjectMetadata.nameSingular, - )}Connection`, - edges: [], - }; + return []; } case FieldMetadataType.Currency: { return { diff --git a/packages/twenty-front/src/modules/object-record/utils/mapPaginatedRecordsToRecords.ts b/packages/twenty-front/src/modules/object-record/utils/mapPaginatedRecordsToRecords.ts deleted file mode 100644 index 48cb0dd913..0000000000 --- a/packages/twenty-front/src/modules/object-record/utils/mapPaginatedRecordsToRecords.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const mapPaginatedRecordsToRecords = < - RecordType extends { id: string } & Record, - RecordTypeQuery extends { - [objectNamePlural: string]: { - edges: RecordEdge[]; - }; - }, - RecordEdge extends { - node: RecordType; - }, ->({ - pagedRecords, - objectNamePlural, -}: { - pagedRecords: RecordTypeQuery | undefined; - objectNamePlural: string; -}) => { - const formattedRecords: RecordType[] = - pagedRecords?.[objectNamePlural]?.edges?.map((recordEdge: RecordEdge) => ({ - ...recordEdge.node, - })) ?? []; - - return formattedRecords; -}; diff --git a/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts new file mode 100644 index 0000000000..4fd45e2fa8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts @@ -0,0 +1,35 @@ +import { isUndefined } from '@sniptt/guards'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue'; +import { isDefined } from '~/utils/isDefined'; + +export const prefillRecord = ({ + objectMetadataItem, + input, + depth = 1, +}: { + objectMetadataItem: ObjectMetadataItem; + input: Record; + depth?: number; +}) => { + return Object.fromEntries( + objectMetadataItem.fields + .filter( + (fieldMetadataItem) => + depth > 0 || fieldMetadataItem.type !== 'RELATION', + ) + .map((fieldMetadataItem) => { + const inputValue = input[fieldMetadataItem.name]; + + return [ + fieldMetadataItem.name, + isUndefined(inputValue) + ? generateEmptyFieldValue(fieldMetadataItem) + : inputValue, + ]; + }) + .filter(isDefined), + ) as T; +}; 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 4dd64c0205..9239f9f169 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -3,6 +3,7 @@ import { isString } from '@sniptt/guards'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeLink } from '@/object-record/utils/sanitizeLinkRecordInput'; import { FieldMetadataType } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; @@ -12,7 +13,7 @@ export const sanitizeRecordInput = ({ recordInput, }: { objectMetadataItem: ObjectMetadataItem; - recordInput: Record; + recordInput: Partial; }) => { const filteredResultRecord = Object.fromEntries( Object.entries(recordInput) @@ -23,6 +24,10 @@ export const sanitizeRecordInput = ({ if (!fieldMetadataItem) return undefined; + if (!fieldMetadataItem.isNullable && fieldValue == null) { + return undefined; + } + if ( fieldMetadataItem.type === FieldMetadataType.Relation && isFieldRelationValue(fieldValue) diff --git a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx index c220aefe4c..e1ed69d597 100644 --- a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx +++ b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx @@ -1,14 +1,12 @@ import { useEffect } from 'react'; -import { useQuery } from '@apollo/client'; import { useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; -import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; -import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { Favorite } from '@/favorites/types/Favorite'; +import { useFindManyRecordsForMultipleMetadataItems } from '@/object-record/multiple-objects/hooks/useFindManyRecordsForMultipleMetadataItems'; import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { View } from '@/views/types/View'; import { isDefined } from '~/utils/isDefined'; export const PrefetchRunQueriesEffect = () => { @@ -17,44 +15,32 @@ export const PrefetchRunQueriesEffect = () => { const { objectMetadataItem: objectMetadataItemView, upsertRecordsInCache: upsertViewsInCache, - } = usePrefetchRunQuery({ + } = usePrefetchRunQuery({ prefetchKey: PrefetchKey.AllViews, - objectNameSingular: CoreObjectNameSingular.View, }); const { objectMetadataItem: objectMetadataItemFavorite, upsertRecordsInCache: upsertFavoritesInCache, - } = usePrefetchRunQuery({ + } = usePrefetchRunQuery({ prefetchKey: PrefetchKey.AllFavorites, - objectNameSingular: CoreObjectNameSingular.Favorite, }); - const prefetchFindManyQuery = - useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ - targetObjectMetadataItems: [ - objectMetadataItemView, - objectMetadataItemFavorite, - ], - depth: 1, - }); - - const { data } = useQuery( - prefetchFindManyQuery ?? EMPTY_QUERY, - { - skip: !currentUser, - }, - ); + const { result } = useFindManyRecordsForMultipleMetadataItems({ + objectMetadataItems: [objectMetadataItemView, objectMetadataItemFavorite], + skip: !currentUser, + depth: 1, + }); useEffect(() => { - if (isDefined(data?.views)) { - upsertViewsInCache(data.views); + if (isDefined(result.views)) { + upsertViewsInCache(result.views as View[]); } - if (isDefined(data?.favorites)) { - upsertFavoritesInCache(data.favorites); + if (isDefined(result.favorites)) { + upsertFavoritesInCache(result.favorites as Favorite[]); } - }, [data, upsertViewsInCache, upsertFavoritesInCache]); + }, [result, upsertViewsInCache, upsertFavoritesInCache]); return <>; }; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts index 294c138a75..b010e65db7 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts @@ -1,29 +1,24 @@ import { useSetRecoilState } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; -import { ALL_VIEWS_QUERY_KEY } from '@/prefetch/query-keys/AllViewsQueryKey'; +import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; export type UsePrefetchRunQuery = { prefetchKey: PrefetchKey; - objectNameSingular: CoreObjectNameSingular; }; export const usePrefetchRunQuery = ({ prefetchKey, - objectNameSingular, }: UsePrefetchRunQuery) => { const setPrefetchDataIsLoadedLoaded = useSetRecoilState( prefetchIsLoadedFamilyState(prefetchKey), ); const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: objectNameSingular, + objectNameSingular: PREFETCH_CONFIG[prefetchKey].objectNameSingular, }); const { upsertFindManyRecordsQueryInCache } = @@ -31,18 +26,12 @@ export const usePrefetchRunQuery = ({ objectMetadataItem: objectMetadataItem, }); - const mapConnectionToRecords = useMapConnectionToRecords(); - - const upsertRecordsInCache = (records: ObjectRecordConnection) => { + const upsertRecordsInCache = (records: T[]) => { upsertFindManyRecordsQueryInCache({ - queryVariables: ALL_VIEWS_QUERY_KEY.variables, - depth: ALL_VIEWS_QUERY_KEY.depth, - objectRecordsToOverwrite: - mapConnectionToRecords({ - objectRecordConnection: records, - objectNameSingular: CoreObjectNameSingular.View, - depth: 2, - }) ?? [], + queryVariables: PREFETCH_CONFIG[prefetchKey].variables, + depth: PREFETCH_CONFIG[prefetchKey].depth, + objectRecordsToOverwrite: records, + computeReferences: false, }); setPrefetchDataIsLoadedLoaded(true); }; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts index 3997bae731..3072733c85 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts @@ -17,7 +17,6 @@ export const usePrefetchedData = ( const { records } = useFindManyRecords({ skip: !isDataPrefetched, ...prefetchQueryKey, - useRecordsWithoutConnection: true, }); return { diff --git a/packages/twenty-front/src/modules/views/components/ViewBarEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarEffect.tsx index f0e2198a7a..518ed1590b 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarEffect.tsx @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil'; import { useViewStates } from '@/views/hooks/internal/useViewStates'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; -import { GraphQLView } from '@/views/types/GraphQLView'; +import { View } from '@/views/types/View'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type ViewBarEffectProps = { @@ -21,8 +21,9 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => { } = useViewStates(viewBarId); const [currentViewSnapshot, setCurrentViewSnapshot] = useState< - GraphQLView | undefined + View | undefined >(undefined); + const onCurrentViewChange = useRecoilValue(onCurrentViewChangeState); const availableFilterDefinitions = useRecoilValue( availableFilterDefinitionsState, diff --git a/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts index b5aa326bcd..70d3cd8160 100644 --- a/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts @@ -6,7 +6,7 @@ import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useViewStates } from '@/views/hooks/internal/useViewStates'; import { ViewScopeInternalContext } from '@/views/scopes/scope-internal-context/ViewScopeInternalContext'; -import { GraphQLView } from '@/views/types/GraphQLView'; +import { View } from '@/views/types/View'; import { combinedViewFilters } from '@/views/utils/combinedViewFilters'; import { combinedViewSorts } from '@/views/utils/combinedViewSorts'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; @@ -18,9 +18,7 @@ export const useGetCurrentView = (viewBarComponentId?: string) => { viewBarComponentId, ); - const { records: views } = usePrefetchedData( - PrefetchKey.AllViews, - ); + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const { currentViewIdState, diff --git a/packages/twenty-front/src/modules/views/states/onCurrentViewChangeComponentState.ts b/packages/twenty-front/src/modules/views/states/onCurrentViewChangeComponentState.ts index 06b503c486..085eb03682 100644 --- a/packages/twenty-front/src/modules/views/states/onCurrentViewChangeComponentState.ts +++ b/packages/twenty-front/src/modules/views/states/onCurrentViewChangeComponentState.ts @@ -1,8 +1,8 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { GraphQLView } from '@/views/types/GraphQLView'; +import { View } from '@/views/types/View'; export const onCurrentViewChangeComponentState = createComponentState< - ((view: GraphQLView | undefined) => void | Promise) | undefined + ((view: View | undefined) => void | Promise) | undefined >({ key: 'onCurrentViewChangeComponentState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index 7e9a043c0f..59c506a63c 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -1,8 +1,20 @@ +import { ViewField } from '@/views/types/ViewField'; +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewKey } from '@/views/types/ViewKey'; +import { ViewSort } from '@/views/types/ViewSort'; import { ViewType } from '@/views/types/ViewType'; export type View = { id: string; name: string; - objectMetadataId: string; type: ViewType; + key: ViewKey | null; + objectMetadataId: string; + isCompact: boolean; + viewFields: ViewField[]; + viewFilters: ViewFilter[]; + viewSorts: ViewSort[]; + kanbanFieldMetadataId: string; + position: number; + icon: string; }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index da26b69bd1..6857f83423 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -1,16 +1,16 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Reference } from '@apollo/client'; +import { Reference, useApolloClient } from '@apollo/client'; import styled from '@emotion/styled'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; @@ -51,6 +51,7 @@ export const SettingsObjectNewFieldStep2 = () => { const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); const { createMetadataField } = useFieldMetadataItem(); + const cache = useApolloClient().cache; const { formValues, @@ -84,38 +85,35 @@ export const SettingsObjectNewFieldStep2 = () => { const [objectViews, setObjectViews] = useState([]); const [relationObjectViews, setRelationObjectViews] = useState([]); - const { modifyRecordFromCache: modifyViewFromCache } = useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.View, - }); + const { objectMetadataItem: viewObjectMetadataItem } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.View, + }); - useFindManyRecords({ + useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.View, filter: { type: { eq: ViewType.Table }, objectMetadataId: { eq: activeObjectMetadataItem?.id }, }, - onCompleted: async (data: ObjectRecordConnection) => { - const views = data.edges; - + onCompleted: async (views) => { if (isUndefinedOrNull(views)) return; - setObjectViews(data.edges.map(({ node }) => node)); + setObjectViews(views); }, }); - useFindManyRecords({ + useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.View, skip: !formValues.relation?.objectMetadataId, filter: { type: { eq: ViewType.Table }, objectMetadataId: { eq: formValues.relation?.objectMetadataId }, }, - onCompleted: async (data: ObjectRecordConnection) => { - const views = data.edges; - + onCompleted: async (views) => { if (isUndefinedOrNull(views)) return; - setRelationObjectViews(data.edges.map(({ node }) => node)); + setRelationObjectViews(views); }, }); @@ -162,47 +160,58 @@ export const SettingsObjectNewFieldStep2 = () => { size: 100, }; - modifyViewFromCache(view.id, { - viewFields: (viewFieldsRef, { readField }) => { - const edges = readField<{ node: Reference }[]>( - 'edges', - viewFieldsRef, - ); + modifyRecordFromCache({ + objectMetadataItem: viewObjectMetadataItem, + cache: cache, + fieldModifiers: { + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); - if (!edges) return viewFieldsRef; + if (!edges) return viewFieldsRef; - return { - ...viewFieldsRef, - edges: [...edges, { node: viewFieldToCreate }], - }; + return { + ...viewFieldsRef, + edges: [...edges, { node: viewFieldToCreate }], + }; + }, }, + recordId: view.id, }); - }); - relationObjectViews.forEach(async (view) => { - const viewFieldToCreate = { - viewId: view.id, - fieldMetadataId: - validatedFormValues.relation.type === 'MANY_TO_ONE' - ? createdRelation.data?.createOneRelation.fromFieldMetadataId - : createdRelation.data?.createOneRelation.toFieldMetadataId, - position: relationObjectMetadataItem?.fields.length, - isVisible: true, - size: 100, - }; - modifyViewFromCache(view.id, { - viewFields: (viewFieldsRef, { readField }) => { - const edges = readField<{ node: Reference }[]>( - 'edges', - viewFieldsRef, - ); - if (!edges) return viewFieldsRef; + relationObjectViews.forEach(async (view) => { + const viewFieldToCreate = { + viewId: view.id, + fieldMetadataId: + validatedFormValues.relation.type === 'MANY_TO_ONE' + ? createdRelation.data?.createOneRelation.fromFieldMetadataId + : createdRelation.data?.createOneRelation.toFieldMetadataId, + position: relationObjectMetadataItem?.fields.length, + isVisible: true, + size: 100, + }; + modifyRecordFromCache({ + objectMetadataItem: viewObjectMetadataItem, + cache: cache, + fieldModifiers: { + viewFields: (viewFieldsRef, { readField }) => { + const edges = readField<{ node: Reference }[]>( + 'edges', + viewFieldsRef, + ); - return { - ...viewFieldsRef, - edges: [...edges, { node: viewFieldToCreate }], - }; - }, + if (!edges) return viewFieldsRef; + + return { + ...viewFieldsRef, + edges: [...edges, { node: viewFieldToCreate }], + }; + }, + }, + recordId: view.id, + }); }); }); } else { @@ -234,20 +243,25 @@ export const SettingsObjectNewFieldStep2 = () => { size: 100, }; - modifyViewFromCache(view.id, { - viewFields: (cachedViewFieldsConnection, { readField }) => { - const edges = readField( - 'edges', - cachedViewFieldsConnection, - ); + modifyRecordFromCache({ + objectMetadataItem: viewObjectMetadataItem, + cache: cache, + fieldModifiers: { + viewFields: (cachedViewFieldsConnection, { readField }) => { + const edges = readField( + 'edges', + cachedViewFieldsConnection, + ); - if (!edges) return cachedViewFieldsConnection; + if (!edges) return cachedViewFieldsConnection; - return { - ...cachedViewFieldsConnection, - edges: [...edges, { node: viewFieldToCreate }], - }; + return { + ...cachedViewFieldsConnection, + edges: [...edges, { node: viewFieldToCreate }], + }; + }, }, + recordId: view.id, }); }); } diff --git a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx index a657c99538..c54ff5dd13 100644 --- a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx @@ -2,9 +2,11 @@ import { useEffect } from 'react'; import { Decorator } from '@storybook/react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { mockedUsersData } from '~/testing/mock-data/users'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; export const ObjectMetadataItemsDecorator: Decorator = (Story) => { @@ -12,11 +14,12 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); + const setCurrentUser = useSetRecoilState(currentUserState); - useEffect( - () => setCurrentWorkspaceMember(mockWorkspaceMembers[0]), - [setCurrentWorkspaceMember], - ); + useEffect(() => { + setCurrentWorkspaceMember(mockWorkspaceMembers[0]); + setCurrentUser(mockedUsersData[0]); + }, [setCurrentUser, setCurrentWorkspaceMember]); return ( <> diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index e8be9da07e..90a17d2dc1 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -4,13 +4,13 @@ import { ApolloProvider } from '@apollo/client'; import { Decorator } from '@storybook/react'; import { RecoilRoot } from 'recoil'; -import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; +import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider'; import { DefaultLayout } from '~/modules/ui/layout/page/DefaultLayout'; import { UserProvider } from '~/modules/users/components/UserProvider'; -import { mockedClient } from '~/testing/mockedClient'; +import { mockedApolloClient } from '~/testing/mockedApolloClient'; import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout'; @@ -37,8 +37,8 @@ export const PageDecorator: Decorator<{ routeParams: RouteParams; }> = (Story, { args }) => ( - - + + - + ); diff --git a/packages/twenty-front/src/testing/decorators/RootDecorator.tsx b/packages/twenty-front/src/testing/decorators/RootDecorator.tsx index 123ce27f04..9c2633037b 100644 --- a/packages/twenty-front/src/testing/decorators/RootDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RootDecorator.tsx @@ -2,18 +2,18 @@ import { ApolloProvider } from '@apollo/client'; import { Decorator } from '@storybook/react'; import { RecoilRoot } from 'recoil'; -import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; +import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider'; import { InitializeHotkeyStorybookHookEffect } from '../InitializeHotkeyStorybookHook'; -import { mockedClient } from '../mockedClient'; +import { mockedApolloClient } from '../mockedApolloClient'; export const RootDecorator: Decorator = (Story) => ( - - + + - + ); diff --git a/packages/twenty-front/src/testing/mockedClient.ts b/packages/twenty-front/src/testing/mockedApolloClient.ts similarity index 74% rename from packages/twenty-front/src/testing/mockedClient.ts rename to packages/twenty-front/src/testing/mockedApolloClient.ts index acf40cb231..cb8d4f0b7d 100644 --- a/packages/twenty-front/src/testing/mockedClient.ts +++ b/packages/twenty-front/src/testing/mockedApolloClient.ts @@ -1,6 +1,6 @@ import { ApolloClient, InMemoryCache } from '@apollo/client'; -export const mockedClient = new ApolloClient({ +export const mockedApolloClient = new ApolloClient({ uri: process.env.REACT_APP_SERVER_BASE_URL + '/graphql', cache: new InMemoryCache(), }); diff --git a/packages/twenty-front/src/testing/mockedMetadataApolloClient.ts b/packages/twenty-front/src/testing/mockedMetadataApolloClient.ts new file mode 100644 index 0000000000..43c2107f01 --- /dev/null +++ b/packages/twenty-front/src/testing/mockedMetadataApolloClient.ts @@ -0,0 +1,6 @@ +import { ApolloClient, InMemoryCache } from '@apollo/client'; + +export const mockedMetadataApolloClient = new ApolloClient({ + uri: process.env.REACT_APP_SERVER_BASE_URL + '/metadata', + cache: new InMemoryCache(), +}); diff --git a/packages/twenty-front/src/utils/isDefined.ts b/packages/twenty-front/src/utils/isDefined.ts index 4b66f1ce6d..81eb67203a 100644 --- a/packages/twenty-front/src/utils/isDefined.ts +++ b/packages/twenty-front/src/utils/isDefined.ts @@ -1,4 +1,4 @@ import { isNull, isUndefined } from '@sniptt/guards'; -export const isDefined = (value: T): value is NonNullable => +export const isDefined = (value: T | null | undefined): value is T => !isUndefined(value) && !isNull(value); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index 951e63042e..abbc0a05ce 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -146,7 +146,7 @@ export class TypeMapperService { ); } - if (!options.nullable && !options.defaultValue) { + if (options.nullable === false && options.defaultValue === null) { graphqlType = new GraphQLNonNull(graphqlType) as unknown as T; }