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 fad3dacbd..a9e8e5b67 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 fff48ece1..a001b51a5 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 bf92d9cde..121a6b216 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 cefdf18e9..a1efde2c8 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 65b8e0b17..82cba4e10 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 b9c4c84c2..c8ef2611f 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 93b6f5405..3d6a03d99 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 4f99035fa..4b370a3b2 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 05cc0bdd3..dd7ec9f97 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 a6abbd80e..a524679fd 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 8e37947a6..9e5cbecc3 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 b8d71ec8d..2d8539889 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 1a0cceee7..b872acab5 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 53d32b269..3e3dd0dfd 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 4db2793e0..000000000 --- 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 c596ecfbd..000000000 --- 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 e1041843d..76a0079eb 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 a3e8fd73f..000000000 --- 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 e999f6394..000000000 --- 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 f965c403f..000000000 --- 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 969b63943..000000000 --- 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 b5858858b..000000000 --- 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 cfc4bce55..000000000 --- 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 7df977a2a..000000000 --- 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 6e2f5a516..000000000 --- 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 79e8ff6c8..000000000 --- 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 ddf4d40e5..000000000 --- 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 41813d0dd..1c2cb0943 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 4f63b2d1b..000000000 --- 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 55cd0b401..000000000 --- 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 359b1c755..455cb9655 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 a48dec8f8..7d2a8e762 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 2dbe174a6..be8281102 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 2ef89de54..000000000 --- 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 4c1a927f7..c58b5a9ef 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 5ade36582..58e0349e2 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 f4cd5914b..000000000 --- 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 eb07c0ddc..000000000 --- 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 195d21cbf..000000000 --- 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 84683c615..000000000 --- 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 357fae472..000000000 --- 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 bcd10fcd7..521473627 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 90713931b..000000000 --- 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 000000000..c6771ec7e --- /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 000000000..cbf0e2f41 --- /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 e65b8058c..000000000 --- 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 d95b3c96d..000000000 --- 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 c383d22df..636519921 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 3275f0923..bb971eb92 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 d1b09c737..e63787053 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 89b990973..000000000 --- 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 f3de9a204..000000000 --- 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 000000000..acec84ba7 --- /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 000000000..09e6b04d6 --- /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 000000000..b0d34ff84 --- /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 40ed872f9..874598a78 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 3f82be635..802e03bb6 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 b5e1501a1..62c552c8e 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 19539090e..000000000 --- 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 0af9d4931..f114b3f8d 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 ad119af39..382ce817e 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 22d25858f..72b17fb37 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 489364a1e..000000000 --- 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 49256f8bb..000000000 --- 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 000000000..b5994a93e --- /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 000000000..a4081e2a1 --- /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 c6a53c39e..5553632b1 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 20b8d009c..dd20d678d 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 ba99ce464..67ba4f8c6 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 14d756c8c..84d7aab6b 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 f0e35e6af..7c381ac86 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 3d0080526..d32185298 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 574383743..50c1faf79 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 f76074e28..d8deb5119 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 c27cc5822..838c1d143 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 61a829cc9..ad2c13a5b 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 e8d124ce8..d0893b392 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 a4edf8c39..4f0ce1e76 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 feaa73ed9..396fdf4af 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 c71702c94..c13ec68d5 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 a1d34e9b5..f8f32cead 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 956f3a5ca..32992648d 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 000000000..6b8929c32 --- /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 3c70a01ad..70f9f5ce2 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 444c566f2..ebd05e796 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 3e99775eb..f663359ad 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 3ef788f6c..000000000 --- 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 a7fe89f4d..000000000 --- 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 000000000..31fb89655 --- /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 000000000..de0e24135 --- /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 000000000..427a5e86a --- /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 348c0cc54..000000000 --- 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 3f9e35a95..5fe2cf5b2 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 9e153f991..000000000 --- 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 9bb619202..000000000 --- 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 4b0bc978e..d680582e9 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 297be5db3..83bd27859 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 000000000..ec9ec8b3a --- /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 dd6e94295..000000000 --- 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 11f1cec67..000000000 --- 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 31a72e8de..000000000 --- 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 6c2139148..7b827c2ba 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 da024846a..f2cd62ff4 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 c058a5349..16d3122c3 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 000000000..7a799bf9d --- /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 f43ce56e0..000000000 --- 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 affe038e9..70090d490 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 86a09e9f8..7327e2e16 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 000000000..5db9a5e65 --- /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 000000000..5de407423 --- /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 000000000..82399a003 --- /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 f52d8e0fb..c427bd055 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 92186f3df..a89390196 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 000000000..3100edffa --- /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 000000000..15636bc74 --- /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 847b5c0e6..000000000 --- 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 58a9e3fc2..837af4dd2 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 480e74bab..57e18be23 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 9120bb026..df7c90426 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 6d58d3b7d..000000000 --- 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 9d1ac683d..000000000 --- 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 5e133b868..e2926f2c8 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 779294692..000000000 --- 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 938f848a8..59b560fcd 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 ebd22838c..000000000 --- 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 3a88f95ce..19f0c38cc 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 4531e47e6..ec39dd385 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 ade2c951e..269c1715c 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 686a5cc5c..91d33f9ad 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 b6837892a..291a15d85 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 5a21fe3de..e190ca5f2 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 8880f3a0b..5bda2bea6 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 a682f1e2b..000000000 --- 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 e2e11bd00..073cb86fd 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 b89bb5e3b..000000000 --- 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 000000000..13b7d5a7a --- /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 fc1c16a14..31d1191b0 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 e3db24455..6757c1bc0 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 9afa22d27..1ac509d20 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 3b0f98e7f..ac88dfb71 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 147edd7f8..52f4a1a6d 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 23d64cae4..7dbe87226 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 036549e61..0f6246887 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 76035fb78..ec337f898 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 019a8c150..68ca007a0 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 3f9194726..688cd9f12 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 675dd938d..75395c27a 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 0cfdaebf7..008c8b2f4 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 52bb21602..ba79640f1 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 80db04662..2d8b31493 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 48cb0dd91..000000000 --- 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 000000000..4fd45e2fa --- /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 4dd64c020..9239f9f16 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 c220aefe4..e1ed69d59 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 294c138a7..b010e65db 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 3997bae73..3072733c8 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 f0e2198a7..518ed1590 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 b5aa326bc..70d3cd816 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 06b503c48..085eb0368 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 7e9a043c0..59c506a63 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 da26b69bd..6857f8342 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 a657c9953..c54ff5dd1 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 e8be9da07..90a17d2dc 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 123ce27f0..9c2633037 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 acf40cb23..cb8d4f0b7 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 000000000..43c2107f0 --- /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 4b66f1ce6..81eb67203 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 951e63042..abbc0a05c 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; }