From 3b458d52071db0644c726c43f7dcb39a0790f23d Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 29 Jan 2024 16:12:52 +0100 Subject: [PATCH] Activity injection into Apollo cache (#3665) - Created addRecordInCache to inject a record in Apollo cache and inject single read query on this record - Created createOneRecordInCache and createManyRecordsInCache that uses this addRecordInCache - Created useOpenCreateActivityDrawerV2 hook to create an activity in cache and inject it into all other relevant requests in the app before opening activity drawer - Refactored DEFAULT_SEARCH_REQUEST_LIMIT constant and hardcoded arbitrary request limits - Added Apollo dev logs to see errors in the console when manipulating cache --- packages/twenty-front/src/index.tsx | 2 + .../hooks/useActivityTargetObjectRecords.ts | 58 +++---- .../activities/hooks/useActivityTargets.ts | 27 ++- .../useHandleCheckableActivityTargetChange.ts | 76 -------- .../useModifyActivityOnActivityTargetCache.ts | 46 +++++ ...useModifyActivityTargetsOnActivityCache.ts | 51 ++++++ ...enCreateActivityDrawerForSelectedRowIds.ts | 34 +++- .../hooks/useOpenCreateActivityDrawerV2.ts | 163 ++++++++++++++++++ .../hooks/useWriteActivityTargetsInCache.ts | 84 +++++++++ ...InjectIntoActivityTargetInlineCellCache.ts | 58 +++++++ .../tasks/components/PageAddTaskButton.tsx | 1 + .../timeline/components/Timeline.tsx | 37 ++-- .../useInjectIntoTimelineActivitiesQuery.ts | 89 ++++++++++ .../timeline/hooks/useTimelineActivities.ts | 73 ++++++++ .../makeTimelineActivitiesQueryVariables.ts | 18 ++ .../types/ActivityTargetableEntity.ts | 3 + ...ityTargetsToCreateFromTargetableObjects.ts | 38 ++++ .../apollo/types/CachedObjectRecord.ts | 4 +- .../debug/components/ApolloDevLogEffect.tsx | 18 ++ .../hooks/useObjectMetadataItem.ts | 4 +- .../cache/hooks/useAddRecordInCache.ts | 72 ++++++++ .../hooks/useGenerateCachedObjectRecord.ts | 0 .../hooks/useGetRecordFromCache.ts | 0 .../hooks/useModifyRecordFromCache.ts | 0 .../utils/getCacheReferenceFromRecord.ts | 31 ++++ .../utils/getCachedRecordEdgesFromRecords.ts | 43 +++++ .../cache/utils/getCachedRecordFromRecord.ts | 16 ++ .../cache/utils/getConnectionTypename.ts | 9 + .../cache/utils/getEdgeTypename.ts | 9 + .../cache/utils/getEmptyPageInfo.ts | 8 + .../cache/utils/getNodeTypename.ts | 9 + .../utils/getRecordConnectionFromEdges.ts | 19 ++ .../utils/getRecordConnectionFromRecords.ts | 24 +++ .../cache/utils/getRecordEdgeFromRecord.ts | 21 +++ .../utils/getRecordsFromRecordConnection.ts | 10 ++ .../constants/DefaultSearchRequestLimit.ts | 1 + .../hooks/__mocks__/useFindManyRecords.ts | 2 +- .../useModifyRecordFromCache.test.tsx | 2 +- .../hooks/useCreateManyRecords.ts | 2 +- .../hooks/useCreateManyRecordsInCache.ts | 48 ++++++ .../object-record/hooks/useCreateOneRecord.ts | 2 +- .../hooks/useCreateOneRecordInCache.ts | 39 +++++ .../object-record/hooks/useFindManyRecords.ts | 3 +- .../hooks/useGenerateFindManyRecordsQuery.ts | 2 +- ...turnObjectDropdownFilterIntoQueryFilter.ts | 4 +- .../components/RecordShowContainer.tsx | 25 +-- .../hooks/useLimitPerMetadataItem.ts | 2 +- .../hooks/useMultiObjectSearch.ts | 2 +- ...archMatchesSearchFilterAndToSelectQuery.ts | 4 +- .../select/hooks/useRecordsForSelect.ts | 13 +- .../utils/generateEmptyFieldValue.ts | 19 +- ...Variables.ts => makeAndFilterVariables.ts} | 2 +- ...rVariables.ts => makeOrFilterVariables.ts} | 2 +- .../object-record/utils/mapToObjectId.ts | 5 + .../__mocks__/useFilteredSearchEntityQuery.ts | 2 +- .../hooks/useFilteredSearchEntityQuery.ts | 13 +- .../pages/object-record/RecordShowPage.tsx | 1 + 57 files changed, 1160 insertions(+), 190 deletions(-) delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts create mode 100644 packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts create mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts create mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts create mode 100644 packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts create mode 100644 packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts rename packages/twenty-front/src/modules/object-record/{ => cache}/hooks/useGenerateCachedObjectRecord.ts (100%) rename packages/twenty-front/src/modules/object-record/{ => cache}/hooks/useGetRecordFromCache.ts (100%) rename packages/twenty-front/src/modules/object-record/{ => cache}/hooks/useModifyRecordFromCache.ts (100%) create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getEmptyPageInfo.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts create mode 100644 packages/twenty-front/src/modules/object-record/constants/DefaultSearchRequestLimit.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts rename packages/twenty-front/src/modules/object-record/utils/{andFilterVariables.ts => makeAndFilterVariables.ts} (91%) rename packages/twenty-front/src/modules/object-record/utils/{orFilterVariables.ts => makeOrFilterVariables.ts} (91%) create mode 100644 packages/twenty-front/src/modules/object-record/utils/mapToObjectId.ts diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index 3fe6f23ad..0b944a3ea 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -6,6 +6,7 @@ import { RecoilRoot } from 'recoil'; import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; +import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; @@ -35,6 +36,7 @@ root.render( + diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts index b3ef7be3d..4b7477a93 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; @@ -15,41 +14,40 @@ export const useActivityTargetObjectRecords = ({ }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const { records: activityTargets } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - filter: { - activityId: { - eq: activityId, + const { records: activityTargets, loading: loadingActivityTargets } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + filter: { + activityId: { + eq: activityId, + }, }, - }, - }); + }); - const activityTargetObjectRecords = useMemo(() => { - return activityTargets - .map>((activityTarget) => { - const correspondingObjectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - isDefined(activityTarget[objectMetadataItem.nameSingular]) && - !objectMetadataItem.isSystem, - ); + const activityTargetObjectRecords = activityTargets + .map>((activityTarget) => { + const correspondingObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + isDefined(activityTarget[objectMetadataItem.nameSingular]) && + !objectMetadataItem.isSystem, + ); - if (!correspondingObjectMetadataItem) { - return null; - } + if (!correspondingObjectMetadataItem) { + return null; + } - return { - activityTargetRecord: activityTarget, - targetObjectRecord: - activityTarget[correspondingObjectMetadataItem.nameSingular], - targetObjectMetadataItem: correspondingObjectMetadataItem, - targetObjectNameSingular: - correspondingObjectMetadataItem.nameSingular, - }; - }) - .filter(isDefined); - }, [activityTargets, objectMetadataItems]); + return { + activityTargetRecord: activityTarget, + targetObjectRecord: + activityTarget[correspondingObjectMetadataItem.nameSingular], + targetObjectMetadataItem: correspondingObjectMetadataItem, + targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular, + }; + }) + .filter(isDefined); return { activityTargetObjectRecords, + loadingActivityTargets, }; }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts index 11757793c..68e88f4ec 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; @@ -13,17 +15,26 @@ export const useActivityTargets = ({ nameSingular: targetableObject.targetObjectNameSingular, }); - const { records: activityTargets } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - filter: { - [targetObjectFieldName]: { - eq: targetableObject.id, + const [initialized, setInitialized] = useState(false); + + const { records: activityTargets, loading: loadingActivityTargets } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + filter: { + [targetObjectFieldName]: { + eq: targetableObject.id, + }, }, - }, - skip: !targetableObject.id, - }); + onCompleted: () => { + if (!initialized) { + setInitialized(true); + } + }, + }); return { activityTargets: activityTargets as ActivityTarget[], + loadingActivityTargets, + initialized, }; }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts b/packages/twenty-front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts deleted file mode 100644 index b7694f841..000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; -import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; - -export const useHandleCheckableActivityTargetChange = ({ - activityId, - currentActivityTargets, -}: { - activityId: string; - currentActivityTargets: any[]; -}) => { - const { createOneRecord: createOneActivityTarget } = - useCreateOneRecord({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - return async ( - entityValues: Record, - entitiesToSelect: any, - selectedEntities: any, - ) => { - if (!activityId) { - return; - } - const currentActivityTargetRecordIds = currentActivityTargets.map( - ({ companyId, personId }) => companyId ?? personId ?? '', - ); - - const idsToAdd = Object.entries(entityValues) - .filter( - ([recordId, value]) => - value && !currentActivityTargetRecordIds.includes(recordId), - ) - .map(([id, _]) => id); - - const idsToDelete = Object.entries(entityValues) - .filter(([_, value]) => !value) - .map(([id, _]) => id); - - if (idsToAdd.length) { - idsToAdd.forEach((id) => { - const entityFromToSelect = entitiesToSelect.filter( - (entity: any) => entity.id === id, - ).length - ? entitiesToSelect.filter((entity: any) => entity.id === id)[0] - : null; - - const entityFromSelected = selectedEntities.filter( - (entity: any) => entity.id === id, - ).length - ? selectedEntities.filter((entity: any) => entity.id === id)[0] - : null; - - const entity = entityFromToSelect ?? entityFromSelected; - createOneActivityTarget?.({ - activityId: activityId, - companyId: entity.record.__typename === 'Company' ? entity.id : null, - personId: entity.record.__typename === 'Person' ? entity.id : null, - }); - }); - } - - if (idsToDelete.length) { - idsToDelete.forEach((id) => { - const currentActivityTargetId = currentActivityTargets.filter( - ({ companyId, personId }) => companyId === id || personId === id, - )[0].id; - deleteOneActivityTarget?.(currentActivityTargetId); - }); - } - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts b/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts new file mode 100644 index 000000000..84683c615 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useModifyActivityOnActivityTargetCache.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..357fae472 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useModifyActivityTargetsOnActivityCache.ts @@ -0,0 +1,51 @@ +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/useOpenCreateActivityDrawerForSelectedRowIds.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts index 02b7861b7..c141649cf 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts @@ -1,8 +1,10 @@ import { useRecoilCallback } from 'recoil'; 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'; @@ -27,21 +29,35 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = ( getSelectedRowIdsSelector(), ); - let activityTargetableEntityArray: ActivityTargetableObject[] = - selectedRowIds.map((id: string) => ({ - type: 'Custom', - targetObjectNameSingular: objectNameSingular, - id, - })); + 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 (relatedEntities) { - activityTargetableEntityArray = - activityTargetableEntityArray.concat(relatedEntities); + activityTargetableObjectArray = + activityTargetableObjectArray.concat(relatedEntities); } openCreateActivityDrawer({ type, - targetableObjects: activityTargetableEntityArray, + targetableObjects: activityTargetableObjectArray, }); }, [openCreateActivityDrawer, getSelectedRowIdsSelector], diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts new file mode 100644 index 000000000..cb8ebfda7 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts @@ -0,0 +1,163 @@ +import { useCallback } from 'react'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { v4 } from 'uuid'; + +import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; +import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache'; +import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache'; +import { useWriteActivityTargetsInCache } from '@/activities/hooks/useWriteActivityTargetsInCache'; +import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; +import { useInjectIntoTimelineActivitiesQuery } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery'; +import { Activity, ActivityType } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache'; +import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { mapToRecordId } from '@/object-record/utils/mapToObjectId'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState'; +import { viewableActivityIdState } from '../states/viewableActivityIdState'; +import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; + +export const useOpenCreateActivityDrawerV2 = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const { openRightDrawer } = useRightDrawer(); + + const { createManyRecordsInCache: createManyActivityTargetsInCache } = + useCreateManyRecordsInCache({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { createOneRecordInCache: createOneActivityInCache } = + useCreateOneRecordInCache({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const { record: workspaceMemberRecord } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + objectRecordId: currentWorkspaceMember?.id, + }); + + const setHotkeyScope = useSetHotkeyScope(); + + const [, setActivityTargetableEntityArray] = useRecoilState( + activityTargetableEntityArrayState, + ); + const [, setViewableActivityId] = useRecoilState(viewableActivityIdState); + + const { activityTargets } = useActivityTargets({ + targetableObject, + }); + + const { injectIntoTimelineActivitiesNextQuery } = + useInjectIntoTimelineActivitiesQuery(); + + const { injectIntoActivityTargetInlineCellCache } = + useInjectIntoActivityTargetInlineCellCache(); + + const { injectIntoUseActivityTargets } = useWriteActivityTargetsInCache(); + + const { modifyActivityTargetsOnActivityCache } = + useModifyActivityTargetsOnActivityCache(); + + const { modifyActivityOnActivityTargetsCache } = + useModifyActivityOnActivityTargetsCache(); + + return useCallback( + async ({ + type, + targetableObjects, + assigneeId, + }: { + type: ActivityType; + targetableObjects: ActivityTargetableObject[]; + assigneeId?: string; + }) => { + const activityId = v4(); + + const createdActivityInCache = await createOneActivityInCache({ + id: activityId, + author: workspaceMemberRecord, + authorId: workspaceMemberRecord?.id, + assignee: !assigneeId ? workspaceMemberRecord : undefined, + assigneeId: + assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id) + ? workspaceMemberRecord?.id + : undefined, + type: type, + }); + + if (!createdActivityInCache) { + return; + } + + const activityTargetsToCreate = + getActivityTargetsToCreateFromTargetableObjects({ + activityId, + targetableObjects, + }); + + const createdActivityTargetsInCache = + await createManyActivityTargetsInCache(activityTargetsToCreate); + + injectIntoUseActivityTargets({ + targetableObject, + activityTargetsToInject: createdActivityTargetsInCache, + }); + + injectIntoTimelineActivitiesNextQuery({ + activityTargets, + activityToInject: createdActivityInCache, + }); + + injectIntoActivityTargetInlineCellCache({ + activityId, + activityTargetsToInject: createdActivityTargetsInCache, + }); + + modifyActivityTargetsOnActivityCache({ + activityId, + activityTargets: createdActivityTargetsInCache, + }); + + modifyActivityOnActivityTargetsCache({ + activityTargetIds: createdActivityTargetsInCache.map(mapToRecordId), + activity: createdActivityInCache, + }); + + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + setViewableActivityId(activityId); + setActivityTargetableEntityArray(targetableObjects ?? []); + openRightDrawer(RightDrawerPages.CreateActivity); + }, + [ + openRightDrawer, + setActivityTargetableEntityArray, + createManyActivityTargetsInCache, + setHotkeyScope, + setViewableActivityId, + createOneActivityInCache, + workspaceMemberRecord, + activityTargets, + targetableObject, + injectIntoTimelineActivitiesNextQuery, + injectIntoActivityTargetInlineCellCache, + injectIntoUseActivityTargets, + modifyActivityTargetsOnActivityCache, + modifyActivityOnActivityTargetsCache, + ], + ); +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts new file mode 100644 index 000000000..7e4830a93 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts @@ -0,0 +1,84 @@ +import { useApolloClient } from '@apollo/client'; + +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; + +export const useWriteActivityTargetsInCache = () => { + const apolloClient = useApolloClient(); + + const { + objectMetadataItem: objectMetadataItemActivityTarget, + findManyRecordsQuery: findManyActivityTargetsQuery, + } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const injectIntoUseActivityTargets = ({ + targetableObject, + activityTargetsToInject, + }: { + targetableObject: Pick< + ActivityTargetableObject, + 'id' | 'targetObjectNameSingular' + >; + activityTargetsToInject: ActivityTarget[]; + }) => { + const targetObjectFieldName = getActivityTargetObjectFieldIdName({ + nameSingular: targetableObject.targetObjectNameSingular, + }); + + const existingActivityTargetsForTargetableObjectQueryResult = + apolloClient.readQuery({ + query: findManyActivityTargetsQuery, + variables: { + filter: { + [targetObjectFieldName]: { + eq: targetableObject.id, + }, + }, + }, + }); + + const existingActivityTargetsForTargetableObject = + getRecordsFromRecordConnection({ + recordConnection: existingActivityTargetsForTargetableObjectQueryResult[ + objectMetadataItemActivityTarget.namePlural + ] as ObjectRecordConnection, + }); + + const newActivityTargetsForTargetableObject = [ + ...existingActivityTargetsForTargetableObject, + ...activityTargetsToInject, + ]; + + const newActivityTargetsConnection = getRecordConnectionFromRecords({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + records: newActivityTargetsForTargetableObject, + }); + + apolloClient.writeQuery({ + query: findManyActivityTargetsQuery, + variables: { + filter: { + [targetObjectFieldName]: { + eq: targetableObject.id, + }, + }, + }, + data: { + [objectMetadataItemActivityTarget.namePlural]: + newActivityTargetsConnection, + }, + }); + }; + + return { + injectIntoUseActivityTargets, + }; +}; 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 new file mode 100644 index 000000000..ec0bdeb13 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts @@ -0,0 +1,58 @@ +import { useApolloClient } from '@apollo/client'; + +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getRecordConnectionFromEdges } from '@/object-record/cache/utils/getRecordConnectionFromEdges'; +import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord'; + +export const useInjectIntoActivityTargetInlineCellCache = () => { + const apolloClient = useApolloClient(); + + const { + objectMetadataItem: objectMetadataItemActivityTarget, + findManyRecordsQuery: findManyActivityTargetsQuery, + } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const injectIntoActivityTargetInlineCellCache = ({ + activityId, + activityTargetsToInject, + }: { + activityId: string; + activityTargetsToInject: ActivityTarget[]; + }) => { + const newActivityTargetEdgesForCache = activityTargetsToInject.map( + (activityTargetToInject) => + getRecordEdgeFromRecord({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + record: activityTargetToInject, + }), + ); + + const newActivityTargetConnectionForCache = getRecordConnectionFromEdges({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + edges: newActivityTargetEdgesForCache, + }); + + apolloClient.writeQuery({ + query: findManyActivityTargetsQuery, + variables: { + filter: { + activityId: { + eq: activityId, + }, + }, + }, + data: { + [objectMetadataItemActivityTarget.namePlural]: + newActivityTargetConnectionForCache, + }, + }); + }; + + return { + injectIntoActivityTargetInlineCellCache, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx index 5d3b6af61..510e8a642 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx @@ -14,6 +14,7 @@ export const PageAddTaskButton = ({ const { selectedFilter } = useFilterDropdown({ filterDropdownId: filterDropdownId, }); + const openCreateActivity = useOpenCreateActivityDrawer(); const handleClick = () => { diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx index cd5d749d4..538c2c0fd 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx @@ -1,14 +1,9 @@ -import React from 'react'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton'; -import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { Activity } from '@/activities/types/Activity'; +import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { TimelineItemsContainer } from './TimelineItemsContainer'; @@ -55,26 +50,22 @@ export const Timeline = ({ }: { targetableObject: ActivityTargetableObject; }) => { - const { activityTargets } = useActivityTargets({ targetableObject }); - - const { records: activities } = useFindManyRecords({ - skip: !activityTargets?.length, - objectNameSingular: CoreObjectNameSingular.Activity, - filter: { - id: { - in: activityTargets - ?.map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString), - }, - }, - orderBy: { - createdAt: 'AscNullsFirst', - }, + const { activities, initialized } = useTimelineActivities({ + targetableObject, }); const openCreateActivity = useOpenCreateActivityDrawer(); - if (!activities.length) { + const showEmptyState = initialized && activities.length === 0; + + const showLoadingState = !initialized; + + if (showLoadingState) { + // TODO: Display a beautiful loading page + return <>; + } + + if (showEmptyState) { return ( No activity yet @@ -99,7 +90,7 @@ export const Timeline = ({ return ( - + ); }; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts new file mode 100644 index 000000000..3f15c968c --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts @@ -0,0 +1,89 @@ +import { useApolloClient } from '@apollo/client'; +import { isNonEmptyString } from '@sniptt/guards'; + +import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; + +export const useInjectIntoTimelineActivitiesQuery = () => { + const apolloClient = useApolloClient(); + + const { + objectMetadataItem: objectMetadataItemActivity, + findManyRecordsQuery: findManyActivitiesQuery, + } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const injectIntoTimelineActivitiesQuery = ({ + activityTargets, + activityToInject, + }: { + activityTargets: ActivityTarget[]; + activityToInject: Activity; + }) => { + const activityIds = activityTargets + ?.map((activityTarget) => activityTarget.activityId) + .filter(isNonEmptyString); + + const timelineActivitiesQueryVariables = + makeTimelineActivitiesQueryVariables({ + activityIds, + }); + + const exitistingActivitiesQueryResult = apolloClient.readQuery({ + query: findManyActivitiesQuery, + variables: timelineActivitiesQueryVariables, + }); + + const extistingActivities = exitistingActivitiesQueryResult + ? getRecordsFromRecordConnection({ + recordConnection: exitistingActivitiesQueryResult[ + objectMetadataItemActivity.namePlural + ] as ObjectRecordConnection, + }) + : []; + + const newActivity = { + ...activityToInject, + __typename: 'Activity', + }; + + const newActivitiesSortedAsActivitiesQuery = [ + newActivity, + ...extistingActivities, + ]; + + const newActivityIdsSortedAsActivityTargetsQuery = [ + ...extistingActivities, + newActivity, + ].map((activity) => activity.id); + + const newTimelineActivitiesQueryVariables = + makeTimelineActivitiesQueryVariables({ + activityIds: newActivityIdsSortedAsActivityTargetsQuery, + }); + + const newActivityConnectionForCache = getRecordConnectionFromRecords({ + objectNameSingular: CoreObjectNameSingular.Activity, + records: newActivitiesSortedAsActivitiesQuery, + }); + + apolloClient.writeQuery({ + query: findManyActivitiesQuery, + variables: newTimelineActivitiesQueryVariables, + data: { + [objectMetadataItemActivity.namePlural]: newActivityConnectionForCache, + }, + }); + }; + + return { + injectIntoTimelineActivitiesNextQuery: injectIntoTimelineActivitiesQuery, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts new file mode 100644 index 000000000..675c1edcc --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts @@ -0,0 +1,73 @@ +import { useEffect, useState } from 'react'; +import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; + +import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; +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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; + +export const useTimelineActivities = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const { + activityTargets, + loadingActivityTargets, + initialized: initializedActivityTargets, + } = useActivityTargets({ + targetableObject, + }); + + const [initialized, setInitialized] = useState(false); + + const [activities, setActivities] = useState([]); + + const activityIds = activityTargets + ?.map((activityTarget) => activityTarget.activityId) + .filter(isNonEmptyString); + + const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables( + { + activityIds, + }, + ); + + const { records: activitiesFromRequest, loading: loadingActivities } = + useFindManyRecords({ + skip: loadingActivityTargets || !isNonEmptyArray(activityTargets), + objectNameSingular: CoreObjectNameSingular.Activity, + filter: timelineActivitiesQueryVariables.filter, + orderBy: timelineActivitiesQueryVariables.orderBy, + onCompleted: () => { + if (!initialized) { + setInitialized(true); + } + }, + }); + + useEffect(() => { + if (!loadingActivities) { + setActivities(activitiesFromRequest); + } + }, [activitiesFromRequest, loadingActivities]); + + const noActivityTargets = + initializedActivityTargets && !isNonEmptyArray(activityTargets); + + useEffect(() => { + if (noActivityTargets) { + setInitialized(true); + } + }, [noActivityTargets]); + + const loading = loadingActivities || loadingActivityTargets; + + return { + activities, + loading, + initialized, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts new file mode 100644 index 000000000..4cd8092d4 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts @@ -0,0 +1,18 @@ +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const makeTimelineActivitiesQueryVariables = ({ + activityIds, +}: { + activityIds: string[]; +}): ObjectRecordQueryVariables => { + return { + filter: { + id: { + in: activityIds, + }, + }, + orderBy: { + createdAt: 'AscNullsFirst', + }, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts index 22d25858f..c68fca98d 100644 --- a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts +++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts @@ -1,5 +1,8 @@ +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + export type ActivityTargetableObject = { id: string; targetObjectNameSingular: string; + targetObjectRecord: ObjectRecord; relatedTargetableObjects?: ActivityTargetableObject[]; }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts new file mode 100644 index 000000000..34d87be32 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts @@ -0,0 +1,38 @@ +import { v4 } from 'uuid'; + +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; + +export const getActivityTargetsToCreateFromTargetableObjects = ({ + targetableObjects, + activityId, +}: { + targetableObjects: ActivityTargetableObject[]; + activityId: string; +}): Partial[] => { + const activityTargetableObjects = targetableObjects + ? flattenTargetableObjectsAndTheirRelatedTargetableObjects( + targetableObjects, + ) + : []; + + const activityTargetsToCreate = activityTargetableObjects.map( + (targetableObject) => { + const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ + nameSingular: targetableObject.targetObjectNameSingular, + }); + + return { + [targetableObject.targetObjectNameSingular]: + targetableObject.targetObjectRecord, + [targetableObjectFieldIdName]: targetableObject.id, + activityId, + id: v4(), + }; + }, + ); + + return activityTargetsToCreate; +}; diff --git a/packages/twenty-front/src/modules/apollo/types/CachedObjectRecord.ts b/packages/twenty-front/src/modules/apollo/types/CachedObjectRecord.ts index a2037ddaa..a457a06e0 100644 --- a/packages/twenty-front/src/modules/apollo/types/CachedObjectRecord.ts +++ b/packages/twenty-front/src/modules/apollo/types/CachedObjectRecord.ts @@ -1,3 +1,5 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -export type CachedObjectRecord = ObjectRecord & { __typename: string }; +export type CachedObjectRecord = T & { + __typename: string; +}; diff --git a/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx new file mode 100644 index 000000000..93a6af6f0 --- /dev/null +++ b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev'; +import { useRecoilValue } from 'recoil'; + +import { isDebugModeState } from '@/client-config/states/isDebugModeState'; + +export const ApolloDevLogEffect = () => { + const isDebugMode = useRecoilValue(isDebugModeState); + + useEffect(() => { + if (isDebugMode) { + loadDevMessages(); + loadErrorMessages(); + } + }, [isDebugMode]); + + return null; +}; 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 3bedb55d3..9853480d9 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -10,6 +10,8 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; 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'; @@ -17,8 +19,6 @@ import { useGenerateExecuteQuickActionOnOneRecordMutation } from '@/object-recor import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery'; import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery'; import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation'; -import { useGetRecordFromCache } from '@/object-record/hooks/useGetRecordFromCache'; -import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache'; import { generateDeleteOneRecordMutation } from '@/object-record/utils/generateDeleteOneRecordMutation'; import { isDefined } from '~/utils/isDefined'; 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 new file mode 100644 index 000000000..7e1293a15 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts @@ -0,0 +1,72 @@ +import { useApolloClient } from '@apollo/client'; +import gql from 'graphql-tag'; +import { useRecoilCallback } from 'recoil'; + +import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery'; +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 mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); + const apolloClient = useApolloClient(); + + const generateFindOneRecordQuery = useGenerateFindOneRecordQuery(); + + const findOneRecordQuery = generateFindOneRecordQuery({ + objectMetadataItem, + }); + + return useRecoilCallback( + ({ set }) => + (record: ObjectRecord) => { + apolloClient.writeFragment({ + id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`, + fragment: gql` + fragment Create${capitalize( + objectMetadataItem.nameSingular, + )}InCache on ${capitalize(objectMetadataItem.nameSingular)} { + __typename + id + ${objectMetadataItem.fields + .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .join('\n')} + } + `, + data: { + __typename: `${capitalize(objectMetadataItem.nameSingular)}`, + ...record, + }, + }); + + // TODO: Turn into injectIntoFindOneRecordQueryCache + apolloClient.writeQuery({ + query: findOneRecordQuery, + variables: { + objectRecordId: record.id, + }, + data: { + [objectMetadataItem.nameSingular]: { + __typename: `${capitalize(objectMetadataItem.nameSingular)}`, + ...record, + }, + }, + }); + + // TODO: remove this once we get rid of entityFieldsFamilyState + set(recordStoreFamilyState(record.id), record); + }, + [ + objectMetadataItem, + apolloClient, + mapFieldMetadataToGraphQLQuery, + findOneRecordQuery, + ], + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCachedObjectRecord.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateCachedObjectRecord.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/hooks/useGenerateCachedObjectRecord.ts rename to packages/twenty-front/src/modules/object-record/cache/hooks/useGenerateCachedObjectRecord.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGetRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/hooks/useGetRecordFromCache.ts rename to packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/hooks/useModifyRecordFromCache.ts rename to packages/twenty-front/src/modules/object-record/cache/hooks/useModifyRecordFromCache.ts 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 new file mode 100644 index 000000000..dd6e94295 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getCacheReferenceFromRecord.ts @@ -0,0 +1,31 @@ +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 new file mode 100644 index 000000000..11f1cec67 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordEdgesFromRecords.ts @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..31a72e8de --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getCachedRecordFromRecord.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 000000000..6c2139148 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getConnectionTypename.ts @@ -0,0 +1,9 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getConnectionTypename = ({ + objectNameSingular, +}: { + objectNameSingular: string; +}) => { + return `${capitalize(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 new file mode 100644 index 000000000..da024846a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getEdgeTypename.ts @@ -0,0 +1,9 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getEdgeTypename = ({ + objectNameSingular, +}: { + objectNameSingular: string; +}) => { + return `${capitalize(objectNameSingular)}Edge`; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getEmptyPageInfo.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getEmptyPageInfo.ts new file mode 100644 index 000000000..95ad87886 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getEmptyPageInfo.ts @@ -0,0 +1,8 @@ +export const getEmptyPageInfo = () => { + return { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }; +}; 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 new file mode 100644 index 000000000..c058a5349 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getNodeTypename.ts @@ -0,0 +1,9 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getNodeTypename = ({ + objectNameSingular, +}: { + 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 new file mode 100644 index 000000000..f43ce56e0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromEdges.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..18fa0c6de --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordConnectionFromRecords.ts @@ -0,0 +1,24 @@ +import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename'; +import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; +import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; + +export const getRecordConnectionFromRecords = ({ + objectNameSingular, + records, +}: { + objectNameSingular: string; + records: T[]; +}) => { + return { + __typename: getConnectionTypename({ objectNameSingular }), + edges: records.map((record) => { + return getRecordEdgeFromRecord({ + objectNameSingular, + record, + }); + }), + pageInfo: getEmptyPageInfo(), + } 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 new file mode 100644 index 000000000..8921edae2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts @@ -0,0 +1,21 @@ +import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; +import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; + +export const getRecordEdgeFromRecord = ({ + objectNameSingular, + record, +}: { + objectNameSingular: string; + record: T; +}) => { + return { + __typename: getEdgeTypename({ objectNameSingular }), + node: { + __typename: getNodeTypename({ objectNameSingular }), + ...record, + }, + cursor: '', + } as ObjectRecordEdge; +}; 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 new file mode 100644 index 000000000..f52d8e0fb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordsFromRecordConnection.ts @@ -0,0 +1,10 @@ +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; + +export const getRecordsFromRecordConnection = ({ + recordConnection, +}: { + recordConnection: ObjectRecordConnection; +}): T[] => { + return recordConnection.edges.map((edge) => edge.node); +}; diff --git a/packages/twenty-front/src/modules/object-record/constants/DefaultSearchRequestLimit.ts b/packages/twenty-front/src/modules/object-record/constants/DefaultSearchRequestLimit.ts new file mode 100644 index 000000000..98c110837 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/constants/DefaultSearchRequestLimit.ts @@ -0,0 +1 @@ +export const DEFAULT_SEARCH_REQUEST_LIMIT = 60; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts index 3b6e569c2..62e1654bf 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts @@ -5,7 +5,7 @@ export const query = gql` $filter: PersonFilterInput $orderBy: PersonOrderByInput $lastCursor: String - $limit: Float = 30 + $limit: Float ) { people( filter: $filter 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 index ee1556536..9d1ac683d 100644 --- 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 @@ -5,7 +5,7 @@ import { act, renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; -import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache'; +import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; const Wrapper = ({ children }: { children: ReactNode }) => ( 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 504f44008..13e3b2c2e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -3,7 +3,7 @@ import { useApolloClient } from '@apollo/client'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord'; +import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts new file mode 100644 index 000000000..cc3d3fd93 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts @@ -0,0 +1,48 @@ +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 { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +export const useCreateManyRecordsInCache = ({ + objectNameSingular, +}: ObjectMetadataItemIdentifier) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ + objectMetadataItem, + }); + + const addRecordInCache = useAddRecordInCache({ + objectMetadataItem, + }); + + const createManyRecordsInCache = async (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 = generateCachedObjectRecord({ + ...record, + }); + + if (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 76410cde1..00d72c531 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord'; +import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts new file mode 100644 index 000000000..c1d10022e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts @@ -0,0 +1,39 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache'; +import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +type useCreateOneRecordInCacheProps = { + objectNameSingular: string; +}; + +export const useCreateOneRecordInCache = ({ + objectNameSingular, +}: useCreateOneRecordInCacheProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ + objectMetadataItem, + }); + + const addRecordInCache = useAddRecordInCache({ + objectMetadataItem, + }); + + const createOneRecordInCache = async (input: ObjectRecord) => { + const generatedCachedObjectRecord = generateCachedObjectRecord({ + createdAt: new Date().toISOString(), + ...input, + }); + + addRecordInCache(generatedCachedObjectRecord); + + return generatedCachedObjectRecord as T; + }; + + return { + createOneRecordInCache, + }; +}; 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 3f78365e8..90936b8b6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -13,7 +13,6 @@ import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnec import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; -import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { logError } from '~/utils/logError'; import { capitalize } from '~/utils/string/capitalize'; @@ -28,7 +27,7 @@ export const useFindManyRecords = ({ objectNameSingular, filter, orderBy, - limit = DEFAULT_SEARCH_REQUEST_LIMIT, + limit, onCompleted, skip, useRecordsWithoutConnection = false, 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 10f0aa06c..9c910df82 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts @@ -20,7 +20,7 @@ export const useGenerateFindManyRecordsQuery = () => { objectMetadataItem.nameSingular, )}FilterInput, $orderBy: ${capitalize( objectMetadataItem.nameSingular, - )}OrderByInput, $lastCursor: String, $limit: Float = 60) { + )}OrderByInput, $lastCursor: String, $limit: Float) { ${ objectMetadataItem.namePlural }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index d7084f363..09c716ff5 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -10,7 +10,7 @@ import { URLFilter, UUIDFilter, } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; -import { andFilterVariables } from '@/object-record/utils/andFilterVariables'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { Field } from '~/generated/graphql'; @@ -260,5 +260,5 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } } - return andFilterVariables(objectRecordFilters); + return makeAndFilterVariables(objectRecordFilters); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 72495e697..81162d68b 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -238,16 +238,21 @@ export const RecordShowContainer = ({ )} - + {record ? ( + + ) : ( + <> + )} ); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts index 1f1107ea4..95be7ef2b 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useLimitPerMetadataItem.ts @@ -1,5 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery'; +import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts index 01c740433..b91c00084 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts @@ -5,7 +5,7 @@ import { useMultiObjectSearchSelectedItemsQuery } from '@/object-record/relation import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; -export const DEFAULT_SEARCH_REQUEST_LIMIT = 5; +export const MULTI_OBJECT_SEARCH_REQUEST_LIMIT = 5; export type ObjectRecordForSelect = { objectMetadataItem: ObjectMetadataItem; 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 79ac8483f..518f78477 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 @@ -13,7 +13,7 @@ import { import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem'; import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem'; -import { andFilterVariables } from '@/object-record/utils/andFilterVariables'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; @@ -69,7 +69,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ return [ `filter${capitalize(nameSingular)}`, - andFilterVariables(searchFilters), + makeAndFilterVariables(searchFilters), ]; }) .filter(isDefined), diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index 7750faaf6..ed72f8409 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -2,13 +2,12 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { OrderBy } from '@/object-metadata/types/OrderBy'; +import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields'; -import { andFilterVariables } from '@/object-record/utils/andFilterVariables'; -import { orFilterVariables } from '@/object-record/utils/orFilterVariables'; - -export const DEFAULT_SEARCH_REQUEST_LIMIT = 60; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; +import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; export const useRecordsForSelect = ({ searchFilterText, @@ -53,7 +52,7 @@ export const useRecordsForSelect = ({ return undefined; } - return orFilterVariables( + return makeOrFilterVariables( fieldNames.map((fieldName) => { const [parentFieldName, subFieldName] = fieldName.split('.'); @@ -81,7 +80,7 @@ export const useRecordsForSelect = ({ loading: filteredSelectedRecordsLoading, records: filteredSelectedRecordsData, } = useFindManyRecords({ - filter: andFilterVariables([...searchFilters, selectedIdsFilter]), + filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), orderBy: orderByField, objectNameSingular, skip: !selectedIds.length, @@ -93,7 +92,7 @@ export const useRecordsForSelect = ({ : undefined; const { loading: recordsToSelectLoading, records: recordsToSelectData } = useFindManyRecords({ - filter: andFilterVariables([...searchFilters, notFilter]), + filter: makeAndFilterVariables([...searchFilters, notFilter]), limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, orderBy: orderByField, objectNameSingular, 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 c69c49b71..40ff109cb 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -1,5 +1,8 @@ +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, @@ -39,7 +42,21 @@ export const generateEmptyFieldValue = ( return true; } case FieldMetadataType.Relation: { - return null; + if ( + !isNonEmptyString( + fieldMetadataItem.fromRelationMetadata?.toObjectMetadata + ?.nameSingular, + ) + ) { + return null; + } + + return { + __typename: `${capitalize( + fieldMetadataItem.fromRelationMetadata.toObjectMetadata.nameSingular, + )}Connection`, + edges: [], + }; } case FieldMetadataType.Currency: { return { diff --git a/packages/twenty-front/src/modules/object-record/utils/andFilterVariables.ts b/packages/twenty-front/src/modules/object-record/utils/makeAndFilterVariables.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/utils/andFilterVariables.ts rename to packages/twenty-front/src/modules/object-record/utils/makeAndFilterVariables.ts index 6e985f224..2f5e36d93 100644 --- a/packages/twenty-front/src/modules/object-record/utils/andFilterVariables.ts +++ b/packages/twenty-front/src/modules/object-record/utils/makeAndFilterVariables.ts @@ -1,7 +1,7 @@ import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; import { isDefined } from '~/utils/isDefined'; -export const andFilterVariables = ( +export const makeAndFilterVariables = ( filters: (ObjectRecordQueryFilter | undefined)[], ): ObjectRecordQueryFilter | undefined => { const definedFilters = filters.filter(isDefined); diff --git a/packages/twenty-front/src/modules/object-record/utils/orFilterVariables.ts b/packages/twenty-front/src/modules/object-record/utils/makeOrFilterVariables.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/utils/orFilterVariables.ts rename to packages/twenty-front/src/modules/object-record/utils/makeOrFilterVariables.ts index e8c4435fd..01cceb25c 100644 --- a/packages/twenty-front/src/modules/object-record/utils/orFilterVariables.ts +++ b/packages/twenty-front/src/modules/object-record/utils/makeOrFilterVariables.ts @@ -1,7 +1,7 @@ import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; import { isDefined } from '~/utils/isDefined'; -export const orFilterVariables = ( +export const makeOrFilterVariables = ( filters: (ObjectRecordQueryFilter | undefined)[], ): ObjectRecordQueryFilter | undefined => { const definedFilters = filters.filter(isDefined); diff --git a/packages/twenty-front/src/modules/object-record/utils/mapToObjectId.ts b/packages/twenty-front/src/modules/object-record/utils/mapToObjectId.ts new file mode 100644 index 000000000..c062c1c3c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/mapToObjectId.ts @@ -0,0 +1,5 @@ +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +export const mapToRecordId = (objectRecord: ObjectRecord) => { + return objectRecord.id; +}; diff --git a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts index bd044c28e..39201e66a 100644 --- a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts @@ -5,7 +5,7 @@ export const query = gql` $filter: PersonFilterInput $orderBy: PersonOrderByInput $lastCursor: String - $limit: Float = 30 + $limit: Float = 60 ) { people( filter: $filter diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts index 46319d744..939160961 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts @@ -2,18 +2,17 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { OrderBy } from '@/object-metadata/types/OrderBy'; +import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { andFilterVariables } from '@/object-record/utils/andFilterVariables'; -import { orFilterVariables } from '@/object-record/utils/orFilterVariables'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; +import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { assertNotNull } from '~/utils/assert'; type SearchFilter = { fieldNames: string[]; filter: string | number }; -export const DEFAULT_SEARCH_REQUEST_LIMIT = 60; - // TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search // Filtered entities to select are @@ -56,7 +55,7 @@ export const useFilteredSearchEntityQuery = ({ return undefined; } - return orFilterVariables( + return makeOrFilterVariables( fieldNames.map((fieldName) => { const [parentFieldName, subFieldName] = fieldName.split('.'); @@ -85,7 +84,7 @@ export const useFilteredSearchEntityQuery = ({ records: filteredSelectedRecords, } = useFindManyRecords({ objectNameSingular, - filter: andFilterVariables([...searchFilters, selectedIdsFilter]), + filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), orderBy: { [orderByField]: sortOrder }, skip: !selectedIds.length, }); @@ -97,7 +96,7 @@ export const useFilteredSearchEntityQuery = ({ const { loading: recordsToSelectLoading, records: recordsToSelect } = useFindManyRecords({ objectNameSingular, - filter: andFilterVariables([...searchFilters, notFilter]), + filter: makeAndFilterVariables([...searchFilters, notFilter]), limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, orderBy: { [orderByField]: sortOrder }, }); diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 53b5b32c8..98641f3ed 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -92,6 +92,7 @@ export const RecordShowPage = () => { entity={{ id: record.id, targetObjectNameSingular: objectMetadataItem?.nameSingular, + targetObjectRecord: record, }} />