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