From 2835935f1150dd71d6c66190f4bafc9b5e7b21ff Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 21 Jan 2026 11:49:20 +0100 Subject: [PATCH] Handled SSE create event (#17293) This PR handles SSE create event. It also fixes a regression on board, which was making it flash with skeletons on any update / create. This PR was a good test case to experience how each main components of the app : table, board, calendar, reacts to a create event. It is not straightforward for now, because each component handles records with its own state management to turn those records into a coherent display of rows or cards. The requests for each component are also different : fetch more, group by, multiple requests in parallel, so the cleanest way to handle optimistic effect requires to create one small optimistic engine per component tuned for its internal data logic. For now we've decided to implement what's doable in a reasonable amount of time and that includes not handling table with groups for now. Creating a clean optimistic logic for each component will be done later. # QA ## Importing 100 records via the API (could be a script, a workflow, an AI calling tools, a CSV import, etc.) https://github.com/user-attachments/assets/d38a3770-8b0a-4f83-8275-5e7d1be0a5c6 ## Creating from different components and seeing the SSE event being processed by other components https://github.com/user-attachments/assets/106b2f49-19cb-4190-92b7-0653c8373366 --- ...eNewIndexRecordNoSelectionRecordAction.tsx | 2 +- .../object-record/hooks/useCreateOneRecord.ts | 29 +++- .../RecordBoardDataChangedEffect.tsx | 145 +++++++++++++++--- ...uldInitializeRecordBoardForUpdateInputs.ts | 2 +- .../RecordBoardColumnCardsContainer.tsx | 24 +-- ...ldFetchMoreInColumnComponentFamilyState.ts | 2 +- ...upFromGroupValueComponentFamilySelector.ts | 44 ++++++ ...bleHeaderLabelIdentifierCellPlusButton.tsx | 4 +- .../types/ObjectRecordOperation.ts | 6 +- ...ggerOptimisticEffectFromSseCreateEvents.ts | 76 +++++++++ ...useTriggerOptimisticEffectFromSseEvents.ts | 16 +- ...ventToObjectRecordOperationBrowserEvent.ts | 12 +- 12 files changed, 311 insertions(+), 51 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector.ts create mode 100644 packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseCreateEvents.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/CreateNewIndexRecordNoSelectionRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/CreateNewIndexRecordNoSelectionRecordAction.tsx index 85f39bf554..2008016df3 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/CreateNewIndexRecordNoSelectionRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/CreateNewIndexRecordNoSelectionRecordAction.tsx @@ -11,7 +11,7 @@ export const CreateNewIndexRecordNoSelectionRecordAction = () => { return ( createNewIndexRecord()} + onClick={() => createNewIndexRecord({ position: 'first' })} closeSidePanelOnCommandMenuListActionExecution={false} /> ); 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 fbbc356a11..cac40d617d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -18,13 +18,18 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { + type ObjectRecord as ObjectRecordShared, + type RecordGqlOperationGqlRecordFields, +} from 'twenty-shared/types'; + +import { type BaseObjectRecord } from '@/object-record/types/BaseObjectRecord'; import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput'; import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { useRecoilValue } from 'recoil'; -import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types'; import { CustomError, isDefined } from 'twenty-shared/utils'; type useCreateOneRecordProps = { @@ -135,7 +140,7 @@ export const useCreateOneRecord = < getCreateOneRecordMutationResponseField(objectNameSingular); const createdObject = await apolloCoreClient - .mutate({ + .mutate({ mutation: createOneRecordMutation, variables: { input: sanitizedInput, @@ -186,17 +191,31 @@ export const useCreateOneRecord = < await refetchAggregateQueries(); + const positionToUse = + recordInput.position === 'first' + ? 'first' + : recordInput.position === 'last' + ? 'last' + : null; + + const createdRecord = createdObject.data?.[ + mutationResponseField + ] as ObjectRecordShared & BaseObjectRecord; + dispatchObjectRecordOperationBrowserEvent({ objectMetadataItem, - operation: { type: 'create-one' }, + operation: { + type: 'create-one', + createdRecord: { ...createdRecord, position: positionToUse }, + }, }); - if (!isDefined(createdObject.data?.[mutationResponseField])) { + if (!isDefined(createdRecord)) { throw new CustomError('Failed to create record'); } return getRecordFromRecordNode({ - recordNode: createdObject.data?.[mutationResponseField], + recordNode: createdRecord, }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx index 95f133260b..62ddd85583 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx @@ -1,8 +1,16 @@ import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent'; import { useGetShouldInitializeRecordBoardForUpdateInputs } from '@/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs'; import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery'; +import { recordGroupFromGroupValueComponentFamilySelector } from '@/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; export const RecordBoardDataChangedEffect = () => { const { objectMetadataItem } = useRecordIndexContextOrThrow(); @@ -11,31 +19,126 @@ export const RecordBoardDataChangedEffect = () => { const { getShouldInitializeRecordBoardForUpdateInputs } = useGetShouldInitializeRecordBoardForUpdateInputs(); - const handleObjectRecordOperation = ( - objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail, - ) => { - const objectRecordOperation = objectRecordOperationEventDetail.operation; + const recordGroupFromGroupValueCallbackState = + useRecoilComponentCallbackState( + recordGroupFromGroupValueComponentFamilySelector, + ); + const recordIndexGroupFieldMetadataItemCallbackState = + useRecoilComponentCallbackState( + recordIndexGroupFieldMetadataItemComponentState, + ); + const recordIndexRecordIdsByGroupCallbackState = + useRecoilComponentCallbackState( + recordIndexRecordIdsByGroupComponentFamilyState, + ); - const isUpdateOperation = - objectRecordOperation.type === 'update-one' || - objectRecordOperation.type === 'update-many'; + const handleObjectRecordOperation = useRecoilCallback( + ({ snapshot }) => + ( + objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail, + ) => { + const objectRecordOperation = + objectRecordOperationEventDetail.operation; - if (isUpdateOperation) { - const updateInputs = - objectRecordOperation.type === 'update-one' - ? [objectRecordOperation.result.updateInput] - : objectRecordOperation.result.updateInputs; + switch (objectRecordOperation.type) { + case 'update-one': + case 'update-many': + { + const updateInputs = + objectRecordOperation.type === 'update-one' + ? [objectRecordOperation.result.updateInput] + : objectRecordOperation.result.updateInputs; - const shouldInitializeForUpdateOperation = - getShouldInitializeRecordBoardForUpdateInputs(updateInputs); + const shouldInitializeForUpdateOperation = + getShouldInitializeRecordBoardForUpdateInputs(updateInputs); - if (shouldInitializeForUpdateOperation) { - triggerRecordBoardInitialQuery(); - } - } else { - triggerRecordBoardInitialQuery(); - } - }; + if (shouldInitializeForUpdateOperation) { + triggerRecordBoardInitialQuery(); + } + } + break; + case 'create-one': { + if (objectRecordOperation.createdRecord.position === 'first') { + triggerRecordBoardInitialQuery(); + } else { + const createdRecordPosition = + objectRecordOperation.createdRecord.position; + + if (!isDefined(createdRecordPosition)) { + return; + } + + const recordIndexGroupFieldMetadataItem = getSnapshotValue( + snapshot, + recordIndexGroupFieldMetadataItemCallbackState, + ); + + if (!isDefined(recordIndexGroupFieldMetadataItem)) { + return; + } + + const recordGroupValue = + objectRecordOperation.createdRecord[ + recordIndexGroupFieldMetadataItem.name + ]; + + const recordGroupDefinitionFromGroupValue = getSnapshotValue( + snapshot, + recordGroupFromGroupValueCallbackState({ recordGroupValue }), + ); + + if (!isDefined(recordGroupDefinitionFromGroupValue)) { + return; + } + + const recordIdsForGroup = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackState( + recordGroupDefinitionFromGroupValue.id, + ), + ); + + const recordIdsWithoutCreatedRecord = recordIdsForGroup.filter( + (recordId) => + recordId !== objectRecordOperation.createdRecord.id, + ); + + const groupIsEmpty = recordIdsWithoutCreatedRecord.length === 0; + + if (groupIsEmpty) { + triggerRecordBoardInitialQuery(); + return; + } + + const firstRecordIdInGroup = recordIdsWithoutCreatedRecord[0]; + const firstExistingRecordInGroup = getSnapshotValue( + snapshot, + recordStoreFamilyState(firstRecordIdInGroup), + ); + + if (!isDefined(firstExistingRecordInGroup)) { + return; + } + + if (createdRecordPosition < firstExistingRecordInGroup.position) { + triggerRecordBoardInitialQuery(); + } + } + break; + } + default: { + triggerRecordBoardInitialQuery(); + } + } + }, + [ + triggerRecordBoardInitialQuery, + getShouldInitializeRecordBoardForUpdateInputs, + recordIndexGroupFieldMetadataItemCallbackState, + recordGroupFromGroupValueCallbackState, + recordIndexRecordIdsByGroupCallbackState, + ], + ); useListenToObjectRecordOperationBrowserEvent({ onObjectRecordOperationBrowserEvent: handleObjectRecordOperation, diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs.ts index 00f80566c8..90c790193e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs.ts @@ -84,7 +84,7 @@ export const useGetShouldInitializeRecordBoardForUpdateInputs = () => { } if (updatedFieldNames.has('position')) { - return true; + return false; } return ( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 2d986eae4e..0e000e1d78 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -9,10 +9,8 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo import { RecordBoardColumnLoadingSkeletonCards } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards'; import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; const StyledColumnCardsContainer = styled.div` display: flex; @@ -40,10 +38,6 @@ export const RecordBoardColumnCardsContainer = ({ recordBoardColumnId, ); - const recordIndexRecordGroupsAreInInitialLoading = useRecoilComponentValue( - recordIndexRecordGroupsAreInInitialLoadingComponentState, - ); - const recordBoardShouldFetchMoreInColumn = useRecoilComponentFamilyValue( recordBoardShouldFetchMoreInColumnComponentFamilyState, recordBoardColumnId, @@ -55,17 +49,13 @@ export const RecordBoardColumnCardsContainer = ({ // eslint-disable-next-line react/jsx-props-no-spreading {...droppableProvided?.droppableProps} > - {recordIndexRecordGroupsAreInInitialLoading ? ( - - ) : ( - recordIds.map((recordId, index) => ( - - )) - )} + {recordIds.map((recordId, index) => ( + + ))} {recordBoardShouldFetchMoreInColumn ? ( ) : null} diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts index f79c886be3..996618ef30 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts @@ -4,6 +4,6 @@ import { createComponentFamilyState } from '@/ui/utilities/state/component-state export const recordBoardShouldFetchMoreInColumnComponentFamilyState = createComponentFamilyState({ key: 'recordBoardShouldFetchMoreInColumnComponentFamilyState', - defaultValue: false, + defaultValue: true, componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector.ts new file mode 100644 index 0000000000..f2c75e65e1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector.ts @@ -0,0 +1,44 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector'; + +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { type Nullable } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const recordGroupFromGroupValueComponentFamilySelector = + createComponentFamilySelector< + RecordGroupDefinition | undefined, + { recordGroupValue: Nullable } + >({ + key: 'recordGroupFromGroupValueComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId, familyKey }) => + ({ get }): RecordGroupDefinition | undefined => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + const recordGroupId = recordGroupIds.find((recordGroupId) => { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + return recordGroupDefinition?.value === familyKey.recordGroupValue; + }); + + if (!isDefined(recordGroupId)) { + return undefined; + } + + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + return recordGroupDefinition; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx index 991a661e52..5ae9473c98 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx @@ -23,7 +23,9 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => { }); const handlePlusButtonClick = () => { - createNewIndexRecord(); + createNewIndexRecord({ + position: 'first', + }); }; const isReadOnly = isObjectMetadataReadOnly({ diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts index 232e157fcb..1677073374 100644 --- a/packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts +++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts @@ -1,4 +1,5 @@ import { type ObjectRecordOperationUpdateInput } from '@/object-record/types/ObjectRecordOperationUpdateInput'; +import { type ObjectRecord } from 'twenty-shared/types'; export type ObjectRecordOperation = | { @@ -13,9 +14,12 @@ export type ObjectRecordOperation = updateInputs: ObjectRecordOperationUpdateInput[]; }; } + | { + type: 'create-one'; + createdRecord: ObjectRecord; + } | { type: - | 'create-one' | 'create-many' | 'destroy-one' | 'destroy-many' diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseCreateEvents.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseCreateEvents.ts new file mode 100644 index 0000000000..2b5f6e260d --- /dev/null +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseCreateEvents.ts @@ -0,0 +1,76 @@ +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; +import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useRefetchAggregateQueriesForObjectMetadataItem } from '@/object-record/hooks/useRefetchAggregateQueriesForObjectMetadataItem'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { useCallback } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; +import { + DatabaseEventAction, + type ObjectRecordEvent, +} from '~/generated/graphql'; + +export const useTriggerOptimisticEffectFromSseCreateEvents = () => { + const apolloCoreClient = useApolloCoreClient(); + const { objectMetadataItems } = useObjectMetadataItems(); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { refetchAggregateQueriesForObjectMetadataItem } = + useRefetchAggregateQueriesForObjectMetadataItem(); + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const debouncedRefetchAggregateQueriesForObjectMetadataItem = + useDebouncedCallback(refetchAggregateQueriesForObjectMetadataItem, 100); + + const triggerOptimisticEffectFromSseCreateEvents = useCallback( + ({ + objectRecordEvents, + objectMetadataItem, + }: { + objectRecordEvents: ObjectRecordEvent[]; + objectMetadataItem: ObjectMetadataItem; + }) => { + const createEvents = objectRecordEvents.filter((objectRecordEvent) => { + return objectRecordEvent.action === DatabaseEventAction.CREATED; + }); + + const cache = apolloCoreClient.cache; + + const recordsToCreate = createEvents.map((createEvent) => { + return { + ...createEvent.properties.after, + __typename: getObjectTypename(objectMetadataItem.nameSingular), + } as RecordGqlNode; + }); + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: recordsToCreate, + objectMetadataItems, + shouldMatchRootQueryFilter: true, + checkForRecordInCache: true, + objectPermissionsByObjectMetadataId, + upsertRecordsInStore, + }); + + debouncedRefetchAggregateQueriesForObjectMetadataItem({ + objectMetadataItem, + }); + }, + [ + apolloCoreClient.cache, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + upsertRecordsInStore, + debouncedRefetchAggregateQueriesForObjectMetadataItem, + ], + ); + + return { + triggerOptimisticEffectFromSseCreateEvents, + }; +}; diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents.ts index 6a62b899d0..e3ed19641b 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents.ts @@ -1,4 +1,5 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useTriggerOptimisticEffectFromSseCreateEvents } from '@/sse-db-event/hooks/useTriggerOptimisticEffectFromSseCreateEvents'; import { useTriggerOptimisticEffectFromSseUpdateEvents } from '@/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents'; import { groupObjectRecordSseEventsByEventType } from '@/sse-db-event/utils/groupObjectRecordSseEventsByEventType'; import { groupObjectRecordSseEventsByObjectMetadataItemNameSingular } from '@/sse-db-event/utils/groupObjectRecordSseEventsByObjectMetadataItemNameSingular'; @@ -15,6 +16,9 @@ export const useTriggerOptimisticEffectFromSseEvents = () => { const { triggerOptimisticEffectFromSseUpdateEvents } = useTriggerOptimisticEffectFromSseUpdateEvents(); + const { triggerOptimisticEffectFromSseCreateEvents } = + useTriggerOptimisticEffectFromSseCreateEvents(); + const triggerOptimisticEffectFromSseEvents = useCallback( ({ objectRecordEvents }: { objectRecordEvents: ObjectRecordEvent[] }) => { const objectRecordEventsByObjectMetadataItemNameSingular = @@ -58,11 +62,21 @@ export const useTriggerOptimisticEffectFromSseEvents = () => { objectMetadataItem, }); break; + case DatabaseEventAction.CREATED: + triggerOptimisticEffectFromSseCreateEvents({ + objectRecordEvents: objectRecordEventsForThisEventType, + objectMetadataItem, + }); + break; } } } }, - [objectMetadataItems, triggerOptimisticEffectFromSseUpdateEvents], + [ + objectMetadataItems, + triggerOptimisticEffectFromSseUpdateEvents, + triggerOptimisticEffectFromSseCreateEvents, + ], ); return { triggerOptimisticEffectFromSseEvents }; diff --git a/packages/twenty-front/src/modules/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent.ts b/packages/twenty-front/src/modules/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent.ts index 2c5cd0befa..3d62be835f 100644 --- a/packages/twenty-front/src/modules/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent.ts +++ b/packages/twenty-front/src/modules/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent.ts @@ -90,7 +90,11 @@ export const turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents = ({ if (hasSingleEvent) { objectRecordOperationBrowserEvents.push({ objectMetadataItem, - operation: { type: 'create-one' }, + operation: { + type: 'create-one', + createdRecord: + objectRecordEventsForThisEventType[0].properties.after, + }, }); } else { objectRecordOperationBrowserEvents.push({ @@ -103,7 +107,11 @@ export const turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents = ({ if (hasSingleEvent) { objectRecordOperationBrowserEvents.push({ objectMetadataItem, - operation: { type: 'create-one' }, + operation: { + type: 'create-one', + createdRecord: + objectRecordEventsForThisEventType[0].properties.after, + }, }); } else { objectRecordOperationBrowserEvents.push({