Refactored object operation dispatch for SSE (#17174)

This PR refactors object record operation dispatch through a browser
event instead of a state that was registering all operations.

This is a cleaner pattern as it is a synchronous event code path instead
of relying on a useEffect to watch state change, which is not ideal.

We introduce this change first to then rely on this new pattern to
dispatch SSE events in a following PR.
This commit is contained in:
Lucas Bordeau
2026-01-15 17:38:21 +01:00
committed by GitHub
parent 3f28fde03a
commit 2cff75d772
31 changed files with 380 additions and 482 deletions
@@ -1,12 +1,12 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { renderHook } from '@testing-library/react';
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
jest.mock('@/object-record/hooks/useRegisterObjectOperation');
jest.mock('@/object-record/utils/dispatchObjectRecordOperationBrowserEvent');
jest.mock('@/object-record/hooks/useUpdateManyRecords', () => ({
useUpdateManyRecords: jest.fn(),
}));
@@ -18,7 +18,9 @@ jest.mock('@/object-record/hooks/useRefetchAggregateQueries', () => ({
jest.mock('@/object-record/hooks/useIncrementalFetchAndMutateRecords');
const mockUseObjectMetadataItem = jest.mocked(useObjectMetadataItem);
const mockUseRegisterObjectOperation = jest.mocked(useRegisterObjectOperation);
const mockDispatchObjectRecordOperationBrowserEvent = jest.mocked(
dispatchObjectRecordOperationBrowserEvent,
);
const mockUseUpdateManyRecords = jest.mocked(useUpdateManyRecords);
const mockUseIncrementalFetchAndMutateRecords = jest.mocked(
useIncrementalFetchAndMutateRecords,
@@ -26,7 +28,6 @@ const mockUseIncrementalFetchAndMutateRecords = jest.mocked(
describe('useIncrementalUpdateManyRecords', () => {
const mockUpdateManyRecords = jest.fn();
const mockRegisterObjectOperation = jest.fn();
const mockIncrementalFetchAndMutate = jest.fn();
const mockUpdateProgress = jest.fn();
@@ -39,9 +40,7 @@ describe('useIncrementalUpdateManyRecords', () => {
} as any,
});
mockUseRegisterObjectOperation.mockReturnValue({
registerObjectOperation: mockRegisterObjectOperation,
});
mockDispatchObjectRecordOperationBrowserEvent.mockImplementation(jest.fn());
mockUseUpdateManyRecords.mockReturnValue({
updateManyRecords: mockUpdateManyRecords,
@@ -85,18 +84,24 @@ describe('useIncrementalUpdateManyRecords', () => {
abortSignal: expect.any(AbortSignal),
});
expect(mockUpdateProgress).toHaveBeenCalledWith(2, 2);
expect(mockRegisterObjectOperation).toHaveBeenCalledWith(
expect.anything(),
{
expect(mockDispatchObjectRecordOperationBrowserEvent).toHaveBeenCalledWith({
objectMetadataItem: expect.anything(),
operation: {
type: 'update-many',
result: {
updateInputs: [
{ id: '1', name: 'New Name' },
{ id: '2', name: 'New Name' },
{
recordId: '1',
updatedFields: [{ name: 'New Name' }],
},
{
recordId: '2',
updatedFields: [{ name: 'New Name' }],
},
],
},
},
);
});
});
it('should pass abortSignal to updateManyRecords', async () => {
@@ -140,14 +145,14 @@ describe('useIncrementalUpdateManyRecords', () => {
result.current.incrementalUpdateManyRecords({ name: 'New Name' }),
).rejects.toThrow('Process failed');
expect(mockRegisterObjectOperation).toHaveBeenCalledWith(
expect.anything(),
{
expect(mockDispatchObjectRecordOperationBrowserEvent).toHaveBeenCalledWith({
objectMetadataItem: expect.anything(),
operation: {
type: 'update-many',
result: {
updateInputs: [],
},
},
);
});
});
});
@@ -6,8 +6,8 @@ import {
type useCreateManyRecordsProps,
} from '@/object-record/hooks/useCreateManyRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
@@ -27,8 +27,6 @@ export const useBatchCreateManyRecords = <
setBatchedRecordsCount?: (count: number) => void;
abortController?: AbortController;
}) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { createManyRecords } = useCreateManyRecords({
objectNameSingular,
recordGqlFields,
@@ -100,7 +98,10 @@ export const useBatchCreateManyRecords = <
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, { type: 'create-many' });
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: { type: 'create-many' },
});
return allCreatedRecords;
};
@@ -12,18 +12,18 @@ import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordF
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { type FieldActorForInputValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { useRecoilValue } from 'recoil';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type PartialObjectRecordWithId = Partial<ObjectRecord> & {
@@ -51,7 +51,6 @@ export const useCreateManyRecords = <
shouldMatchRootQueryFilter,
shouldRefetchAggregateQueries = true,
}: useCreateManyRecordsProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apolloCoreClient = useApolloCoreClient();
@@ -243,7 +242,10 @@ export const useCreateManyRecords = <
await refetchAggregateQueries();
}
registerObjectOperation(objectMetadataItem, { type: 'create-many' });
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: { type: 'create-many' },
});
return createdObjects.data?.[mutationResponseField] ?? [];
};
@@ -13,18 +13,18 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
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 = {
@@ -43,7 +43,6 @@ export const useCreateOneRecord = <
shouldMatchRootQueryFilter,
}: useCreateOneRecordProps) => {
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { registerObjectOperation } = useRegisterObjectOperation();
const apolloCoreClient = useApolloCoreClient();
const [loading, setLoading] = useState(false);
@@ -187,7 +186,10 @@ export const useCreateOneRecord = <
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, { type: 'create-one' });
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: { type: 'create-one' },
});
if (!isDefined(createdObject.data?.[mutationResponseField])) {
throw new CustomError('Failed to create record');
@@ -12,9 +12,9 @@ import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -34,7 +34,6 @@ export type DeleteManyRecordsProps = {
export const useDeleteManyRecords = ({
objectNameSingular,
}: useDeleteManyRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apiConfig = useRecoilValue(apiConfigState);
@@ -224,8 +223,11 @@ export const useDeleteManyRecords = ({
}
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'delete-many',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'delete-many',
},
});
return deletedRecords;
@@ -12,9 +12,9 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@@ -26,7 +26,6 @@ type useDeleteOneRecordProps = {
export const useDeleteOneRecord = ({
objectNameSingular,
}: useDeleteOneRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apolloCoreClient = useApolloCoreClient();
@@ -165,8 +164,11 @@ export const useDeleteOneRecord = ({
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'delete-one',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'delete-one',
},
});
return deletedRecord.data?.[mutationResponseField] ?? null;
@@ -178,7 +180,6 @@ export const useDeleteOneRecord = ({
objectMetadataItems,
deleteOneRecordMutation,
refetchAggregateQueries,
registerObjectOperation,
mutationResponseField,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
@@ -9,9 +9,9 @@ import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMu
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -31,7 +31,6 @@ export type DestroyManyRecordsProps = {
export const useDestroyManyRecords = ({
objectNameSingular,
}: useDestroyManyRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apiConfig = useRecoilValue(apiConfigState);
@@ -145,8 +144,11 @@ export const useDestroyManyRecords = ({
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'destroy-many',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'destroy-many',
},
});
return destroyedRecords;
@@ -8,8 +8,8 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -21,7 +21,6 @@ type useDestroyOneRecordProps = {
export const useDestroyOneRecord = ({
objectNameSingular,
}: useDestroyOneRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apolloCoreClient = useApolloCoreClient();
@@ -89,8 +88,11 @@ export const useDestroyOneRecord = ({
throw error;
});
registerObjectOperation(objectMetadataItem, {
type: 'destroy-one',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'destroy-one',
},
});
return deletedRecord.data?.[mutationResponseField] ?? null;
@@ -101,7 +103,6 @@ export const useDestroyOneRecord = ({
destroyOneRecordMutation,
mutationResponseField,
objectNameSingular,
registerObjectOperation,
objectMetadataItem,
objectMetadataItems,
upsertRecordsInStore,
@@ -13,9 +13,9 @@ import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMor
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
@@ -40,7 +40,6 @@ export const useIncrementalDeleteManyRecords = <T>({
delayInMsBetweenMutations = DEFAULT_DELAY_BETWEEN_MUTATIONS_MS,
skipOptimisticEffect = false,
}: UseIncrementalDeleteManyRecordsParams<T>) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const mutationPageSize = pageSize;
@@ -235,8 +234,11 @@ export const useIncrementalDeleteManyRecords = <T>({
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'delete-many',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'delete-many',
},
});
return totalDeletedCount;
@@ -10,9 +10,9 @@ import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMor
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
@@ -37,7 +37,6 @@ export const useIncrementalDestroyManyRecords = <T>({
delayInMsBetweenMutations = DEFAULT_DELAY_BETWEEN_MUTATIONS_MS,
skipOptimisticEffect = false,
}: UseIncrementalDestroyManyRecordsParams<T>) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const mutationPageSize = pageSize;
@@ -165,8 +164,11 @@ export const useIncrementalDestroyManyRecords = <T>({
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'destroy-many',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'destroy-many',
},
});
return totalDestroyedCount;
@@ -3,9 +3,10 @@ import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryP
import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
@@ -28,8 +29,6 @@ export const useIncrementalUpdateManyRecords = <
pageSize = DEFAULT_QUERY_PAGE_SIZE,
delayInMsBetweenMutations = DEFAULT_DELAY_BETWEEN_MUTATIONS_MS,
}: UseIncrementalUpdateManyRecordsParams<T>) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@@ -83,13 +82,16 @@ export const useIncrementalUpdateManyRecords = <
} finally {
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'update-many',
result: {
updateInputs: allUpdatedRecordIds.map((id) => ({
id,
...fieldsToUpdate,
})),
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'update-many',
result: {
updateInputs: allUpdatedRecordIds.map((recordId) => ({
recordId,
updatedFields: getUpdatedFieldsFromRecordInput(fieldsToUpdate),
})),
},
},
});
}
@@ -0,0 +1,57 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { useEffect } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
export const useListenToObjectRecordOperationBrowserEvent = ({
onObjectRecordOperationBrowserEvent,
objectMetadataItemId,
operationTypes,
}: {
onObjectRecordOperationBrowserEvent: (
detail: ObjectRecordOperationBrowserEventDetail,
) => void;
objectMetadataItemId?: string;
operationTypes?: ObjectRecordOperation['type'][];
}) => {
useEffect(() => {
const handleObjectRecordOperationEvent = (event: Event) => {
const detail = (
event as CustomEvent<ObjectRecordOperationBrowserEventDetail>
).detail;
if (
isDefined(objectMetadataItemId) &&
detail.objectMetadataItem.id !== objectMetadataItemId
) {
return;
}
if (
isNonEmptyArray(operationTypes) &&
!operationTypes.includes(detail.operation.type)
) {
return;
}
onObjectRecordOperationBrowserEvent(detail);
};
window.addEventListener(
OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME,
handleObjectRecordOperationEvent,
);
return () => {
window.removeEventListener(
OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME,
handleObjectRecordOperationEvent,
);
};
}, [
objectMetadataItemId,
onObjectRecordOperationBrowserEvent,
operationTypes,
]);
};
@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { useMergeManyRecordsMutation } from '@/object-record/hooks/useMergeManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
import { getOperationName } from '@apollo/client/utilities';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
export type MergeManySettings = {
conflictPriorityIndex: number;
@@ -28,7 +28,6 @@ export const useMergeManyRecords = <
objectNameSingular,
recordGqlFields,
}: UseMergeManyRecordsProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const apolloCoreClient = useApolloCoreClient();
const [loading, setLoading] = useState(false);
@@ -103,8 +102,11 @@ export const useMergeManyRecords = <
if (!preview) {
await refetchAggregateQueries();
registerObjectOperation(objectMetadataItem, {
type: 'merge-records',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'merge-records',
},
});
}
@@ -121,7 +123,6 @@ export const useMergeManyRecords = <
findOneRecordQuery,
findDuplicateRecordsQuery,
refetchAggregateQueries,
registerObjectOperation,
],
);
@@ -1,32 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
objectOperationsState,
type ObjectOperationData,
} from '@/object-record/states/objectOperationsComponentState';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
export const useRegisterObjectOperation = () => {
const registerObjectOperation = useRecoilCallback(
({ set }) =>
(objectMetadataItem: ObjectMetadataItem, data: ObjectOperationData) => {
set(objectOperationsState, (currentValue) => {
const newValue = currentValue.concat();
newValue.push({
id: v4(),
timestamp: +new Date(),
objectMetadataItemId: objectMetadataItem.id,
data,
});
return newValue;
});
},
[],
);
return {
registerObjectOperation,
};
};
@@ -9,10 +9,10 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -32,7 +32,6 @@ type RestoreManyRecordsProps = {
export const useRestoreManyRecords = ({
objectNameSingular,
}: useRestoreManyRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apiConfig = useRecoilValue(apiConfigState);
@@ -200,8 +199,11 @@ export const useRestoreManyRecords = ({
restoredRecords.push(...restoredRecordsForThisBatch);
registerObjectOperation(objectMetadataItem, {
type: 'restore-many',
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'restore-many',
},
});
if (isDefined(delayInMsBetweenRequests)) {
@@ -13,10 +13,11 @@ import { generateDepthRecordGqlFieldsFromRecord } from '@/object-record/graphql/
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpdateManyRecordsMutation } from '@/object-record/hooks/useUpdateManyRecordsMutation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateManyRecordsMutationResponseField } from '@/object-record/utils/getUpdateManyRecordsMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { useRecoilValue } from 'recoil';
@@ -42,7 +43,6 @@ export const useUpdateManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
recordGqlFields,
}: UseUpdateManyRecordsProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apiConfig = useRecoilValue(apiConfigState);
@@ -265,13 +265,17 @@ export const useUpdateManyRecords = <T extends ObjectRecord = ObjectRecord>({
}
if (!skipRegisterObjectOperation) {
registerObjectOperation(objectMetadataItem, {
type: 'update-many',
result: {
updateInputs: recordIdsToUpdate.map((id) => ({
id,
...updateOneRecordInput,
})),
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'update-many',
result: {
updateInputs: recordIdsToUpdate.map((recordId) => ({
recordId,
updatedFields:
getUpdatedFieldsFromRecordInput(updateOneRecordInput),
})),
},
},
});
}
@@ -12,11 +12,12 @@ import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graph
import { generateDepthRecordGqlFieldsFromRecord } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards';
@@ -39,7 +40,6 @@ export const useUpdateOneRecord = <
objectNameSingular,
recordGqlFields,
}: useUpdateOneRecordProps) => {
const { registerObjectOperation } = useRegisterObjectOperation();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const apolloCoreClient = useApolloCoreClient();
@@ -236,9 +236,18 @@ export const useUpdateOneRecord = <
const udpatedRecord = updatedRecord?.data?.[mutationResponseField] ?? null;
registerObjectOperation(objectMetadataItem, {
type: 'update-one',
result: { updateInput: updateOneRecordInput },
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'update-one',
result: {
updateInput: {
recordId: idToUpdate,
updatedFields:
getUpdatedFieldsFromRecordInput(updateOneRecordInput),
},
},
},
});
return udpatedRecord;
@@ -12,10 +12,12 @@ import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { generateDepthRecordGqlFieldsFromRecord } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards';
@@ -33,7 +35,6 @@ type UpdateOneRecordArgs<UpdatedObjectRecord> = {
export const useUpdateOneRecordV2 = () => {
const apolloCoreClient = useApolloCoreClient();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { registerObjectOperation } = useRegisterObjectOperation();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
@@ -238,9 +239,18 @@ export const useUpdateOneRecordV2 = () => {
throw error;
});
registerObjectOperation(objectMetadataItem, {
type: 'update-one',
result: { updateInput: updateOneRecordInput },
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
operation: {
type: 'update-one',
result: {
updateInput: {
recordId: idToUpdate,
updatedFields:
getUpdatedFieldsFromRecordInput(updateOneRecordInput),
},
},
},
});
return updatedRecord?.data?.[mutationResponseField] ?? null;