diff --git a/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts index e1fdffcd2d0..41c039a235e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts @@ -100,7 +100,7 @@ export const useBatchCreateManyRecords = < await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { type: 'create-many' }); + registerObjectOperation(objectMetadataItem, { type: 'create-many' }); return allCreatedRecords; }; 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 a2d1b4e18e9..0513afd3067 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -243,7 +243,7 @@ export const useCreateManyRecords = < await refetchAggregateQueries(); } - registerObjectOperation(objectNameSingular, { type: 'create-many' }); + registerObjectOperation(objectMetadataItem, { type: 'create-many' }); return createdObjects.data?.[mutationResponseField] ?? []; }; 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 20ceacc4927..039faa09836 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -187,7 +187,7 @@ export const useCreateOneRecord = < await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { type: 'create-one' }); + registerObjectOperation(objectMetadataItem, { type: 'create-one' }); if (!isDefined(createdObject.data?.[mutationResponseField])) { throw new CustomError('Failed to create record'); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 2e3229ff2fd..df4ee128708 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -224,7 +224,7 @@ export const useDeleteManyRecords = ({ } await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'delete-many', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 82f2746958e..b5955389a70 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -165,7 +165,7 @@ export const useDeleteOneRecord = ({ await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'delete-one', }); @@ -179,7 +179,6 @@ export const useDeleteOneRecord = ({ deleteOneRecordMutation, refetchAggregateQueries, registerObjectOperation, - objectNameSingular, mutationResponseField, objectPermissionsByObjectMetadataId, upsertRecordsInStore, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 528b7448dd5..5cc85c08f6c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -145,7 +145,7 @@ export const useDestroyManyRecords = ({ await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'destroy-many', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts index 3126752d6f6..42cd7f4ab76 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts @@ -89,7 +89,7 @@ export const useDestroyOneRecord = ({ throw error; }); - registerObjectOperation(objectMetadataItem.nameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'destroy-one', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDeleteManyRecords.ts index 14ed0996095..db772e0403c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDeleteManyRecords.ts @@ -225,7 +225,7 @@ export const useIncrementalDeleteManyRecords = ({ await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'delete-many', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDestroyManyRecords.ts index fd564fd6103..1b56469d1da 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalDestroyManyRecords.ts @@ -155,7 +155,7 @@ export const useIncrementalDestroyManyRecords = ({ await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'destroy-many', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts index 6c022e00b30..0bf9bd6c50a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts @@ -103,7 +103,7 @@ export const useMergeManyRecords = < if (!preview) { await refetchAggregateQueries(); - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'merge-records', }); } @@ -115,14 +115,13 @@ export const useMergeManyRecords = < } }, [ - objectMetadataItem.namePlural, + objectMetadataItem, apolloCoreClient, mergeManyRecordsMutation, findOneRecordQuery, findDuplicateRecordsQuery, - registerObjectOperation, - objectNameSingular, refetchAggregateQueries, + registerObjectOperation, ], ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRegisterObjectOperation.ts b/packages/twenty-front/src/modules/object-record/hooks/useRegisterObjectOperation.ts index 446ec1e5cca..c59acf65d99 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRegisterObjectOperation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRegisterObjectOperation.ts @@ -1,30 +1,27 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { + objectOperationsState, type ObjectOperationData, - objectOperationsByObjectNameSingularFamilyState, -} from '@/object-record/states/objectOperationsByObjectNameSingularFamilyState'; +} from '@/object-record/states/objectOperationsComponentState'; import { useRecoilCallback } from 'recoil'; import { v4 } from 'uuid'; export const useRegisterObjectOperation = () => { const registerObjectOperation = useRecoilCallback( ({ set }) => - (objectNameSingular: string, data: ObjectOperationData) => { - set( - objectOperationsByObjectNameSingularFamilyState({ - objectNameSingular, - }), - (currentValue) => { - const newValue = currentValue.concat(); + (objectMetadataItem: ObjectMetadataItem, data: ObjectOperationData) => { + set(objectOperationsState, (currentValue) => { + const newValue = currentValue.concat(); - newValue.push({ - id: v4(), - timestamp: +new Date(), - data, - }); + newValue.push({ + id: v4(), + timestamp: +new Date(), + objectMetadataItemId: objectMetadataItem.id, + data, + }); - return newValue; - }, - ); + return newValue; + }); }, [], ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts index c3cf1c660f3..5138d651e23 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts @@ -200,7 +200,7 @@ export const useRestoreManyRecords = ({ restoredRecords.push(...restoredRecordsForThisBatch); - registerObjectOperation(objectMetadataItem.nameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'restore-many', }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index a41d97f7add..47b934522d4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -230,9 +230,9 @@ export const useUpdateOneRecord = < const udpatedRecord = updatedRecord?.data?.[mutationResponseField] ?? null; - registerObjectOperation(objectNameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'update-one', - result: { updatedRecord, updateInput: updateOneRecordInput }, + result: { updateInput: updateOneRecordInput }, }); return udpatedRecord; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts index c49981b105e..e13ffbbaea0 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts @@ -238,10 +238,11 @@ export const useUpdateOneRecordV2 = () => { throw error; }); - registerObjectOperation(objectMetadataItem.nameSingular, { + registerObjectOperation(objectMetadataItem, { type: 'update-one', - result: { updatedRecord, updateInput: updateOneRecordInput }, + result: { updateInput: updateOneRecordInput }, }); + return updatedRecord?.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts index dd0a184f572..96b0d2c55fc 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts @@ -2,6 +2,7 @@ import { type DropResult } from '@hello-pangea/dnd'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { useTriggerTableWithoutGroupDragAndDropOptimisticUpdate } from '@/object-record/record-drag/hooks/useTriggerTableWithoutGroupDragAndDropOptimisticUpdate'; import { originalDragSelectionComponentState } from '@/object-record/record-drag/states/originalDragSelectionComponentState'; import { getDragOperationType } from '@/object-record/record-drag/utils/getDragOperationType'; import { processMultiDrag } from '@/object-record/record-drag/utils/processMultiDrag'; @@ -12,8 +13,6 @@ import { currentRecordSortsComponentState } from '@/object-record/record-sort/st import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; -import { useResetVirtualizationBecauseDataChanged } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged'; -import { useTriggerFetchPages } from '@/object-record/record-table/virtualization/hooks/useTriggerFetchPages'; import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; @@ -46,10 +45,8 @@ export const useProcessTableWithoutGroupRecordDrop = () => { const { openModal } = useModal(); - const { resetVirtualization } = - useResetVirtualizationBecauseDataChanged(objectNameSingular); - - const { triggerFetchPagesWithoutDebounce } = useTriggerFetchPages(); + const { triggerTableWithoutGroupDragAndDropOptimisticUpdate } = + useTriggerTableWithoutGroupDragAndDropOptimisticUpdate(); const processTableWithoutGroupRecordDrop = useRecoilCallback( ({ snapshot }) => @@ -115,6 +112,10 @@ export const useProcessTableWithoutGroupRecordDrop = () => { return; } + triggerTableWithoutGroupDragAndDropOptimisticUpdate([ + singleDragResult, + ]); + updateOneRow({ idToUpdate: singleDragResult.id, updateOneRecordInput: { @@ -145,6 +146,10 @@ export const useProcessTableWithoutGroupRecordDrop = () => { isDroppedAfterList, }); + triggerTableWithoutGroupDragAndDropOptimisticUpdate( + multiDragResult.recordUpdates, + ); + for (const update of multiDragResult.recordUpdates) { updateOneRow({ idToUpdate: update.id, @@ -154,10 +159,6 @@ export const useProcessTableWithoutGroupRecordDrop = () => { }); } } - - await resetVirtualization(); - - await triggerFetchPagesWithoutDebounce(); }, [ selectedRowIdsSelector, @@ -166,8 +167,7 @@ export const useProcessTableWithoutGroupRecordDrop = () => { currentRecordSorts, originalDragSelectionCallbackState, allRecordIdsWithoutGroupCallbackSelector, - resetVirtualization, - triggerFetchPagesWithoutDebounce, + triggerTableWithoutGroupDragAndDropOptimisticUpdate, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useTriggerTableWithoutGroupDragAndDropOptimisticUpdate.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useTriggerTableWithoutGroupDragAndDropOptimisticUpdate.ts new file mode 100644 index 00000000000..8671cf2bd58 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useTriggerTableWithoutGroupDragAndDropOptimisticUpdate.ts @@ -0,0 +1,125 @@ +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows'; +import { lastScrollPositionComponentState } from '@/object-record/record-table/virtualization/states/lastScrollPositionComponentState'; +import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState'; +import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState'; +import { getVirtualizationOverscanWindow } from '@/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow'; +import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord'; +import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; +import { findById, isDefined } from 'twenty-shared/utils'; +import { sortByProperty } from '~/utils/array/sortByProperty'; + +// TODO: does not work when scrolling while dragging and does not work if not paired with a network refetch right after +// But it's sufficient right now for the main use case +export const useTriggerTableWithoutGroupDragAndDropOptimisticUpdate = () => { + const recordIdByRealIndexCallbackFamilyState = + useRecoilComponentCallbackState(recordIdByRealIndexComponentFamilyState); + + const lastScrollPositionCallbackState = useRecoilComponentCallbackState( + lastScrollPositionComponentState, + ); + const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); + const totalNumberOfRecordsToVirtualizeCallbackState = + useRecoilComponentCallbackState( + totalNumberOfRecordsToVirtualizeComponentState, + ); + + const { loadRecordsToVirtualRows } = useLoadRecordsToVirtualRows(); + + const triggerTableWithoutGroupDragAndDropOptimisticUpdate = useRecoilCallback( + ({ snapshot }) => + (updatedRecords: RecordWithPosition[]) => { + const tableScrollWrapperHeight = + scrollWrapperHTMLElement?.clientHeight ?? 0; + + const lastScrollPosition = getSnapshotValue( + snapshot, + lastScrollPositionCallbackState, + ); + + const totalNumberOfRecordsToVirtualize = + getSnapshotValue( + snapshot, + totalNumberOfRecordsToVirtualizeCallbackState, + ) ?? 0; + + const { + firstRealIndexInOverscanWindow, + lastRealIndexInOverscanWindow, + } = getVirtualizationOverscanWindow( + lastScrollPosition, + tableScrollWrapperHeight, + totalNumberOfRecordsToVirtualize, + ); + + const recordsInOverscanWindowToReorder: RecordWithPosition[] = []; + + for ( + let realIndex = firstRealIndexInOverscanWindow; + realIndex <= lastRealIndexInOverscanWindow; + realIndex++ + ) { + const recordIdAtRealIndex = getSnapshotValue( + snapshot, + recordIdByRealIndexCallbackFamilyState({ realIndex }), + ); + + if (!isDefined(recordIdAtRealIndex)) { + continue; + } + + const correspondingRecordInStore = getSnapshotValue( + snapshot, + recordStoreFamilyState(recordIdAtRealIndex), + ); + + if ( + isDefined(correspondingRecordInStore) && + isDefined(correspondingRecordInStore.position) + ) { + const correspondingDraggedRecord = updatedRecords.find( + findById(correspondingRecordInStore.id), + ); + + const hasRecordBeenDragged = isDefined(correspondingDraggedRecord); + + const positionToUse = hasRecordBeenDragged + ? correspondingDraggedRecord.position + : correspondingRecordInStore.position; + + recordsInOverscanWindowToReorder.push({ + id: correspondingRecordInStore.id, + position: positionToUse, + }); + } + } + + const shouldReorderRecordsInOverscanWindow = + recordsInOverscanWindowToReorder.length > 0; + + if (!shouldReorderRecordsInOverscanWindow) { + return; + } + + const reorderedRecordsWithPosition = + recordsInOverscanWindowToReorder.toSorted(sortByProperty('position')); + + loadRecordsToVirtualRows({ + records: reorderedRecordsWithPosition as any[], + startingRealIndex: firstRealIndexInOverscanWindow, + }); + }, + [ + lastScrollPositionCallbackState, + loadRecordsToVirtualRows, + recordIdByRealIndexCallbackFamilyState, + scrollWrapperHTMLElement?.clientHeight, + totalNumberOfRecordsToVirtualizeCallbackState, + ], + ); + + return { triggerTableWithoutGroupDragAndDropOptimisticUpdate }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx index dde7f2eb30e..50cbd943fc3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx @@ -32,6 +32,7 @@ export const RecordTableDraggableTr = ({ return ( { + const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { objectNameSingular } = useRecordTableContextOrThrow(); + const { getObjectMetadataItemById } = useGetObjectMetadataItemById(); + const { resetVirtualizationBecauseDataChanged } = useResetVirtualizationBecauseDataChanged(objectNameSingular); @@ -20,12 +31,22 @@ export const RecordTableVirtualizedDataChangedEffect = () => { lastObjectOperationThatResettedVirtualizationComponentState, ); - const objectOperationsForObjectNameSingular = useRecoilValue( - objectOperationsByObjectNameSingularFamilyState({ objectNameSingular }), + const { activeFieldMetadataItems } = useActiveFieldMetadataItems({ + objectMetadataItem, + }); + + const currentRecordSorts = useRecoilComponentValue( + currentRecordSortsComponentState, ); + const currentRecordFilters = useRecoilComponentValue( + currentRecordFiltersComponentState, + ); + + const objectOperations = useRecoilValue(objectOperationsState); + useEffect(() => { - const lastObjectOperation = objectOperationsForObjectNameSingular.at(-1); + const lastObjectOperation = objectOperations.at(-1); if ( !isDefined(lastObjectOperationThatResettedVirtualization) && @@ -41,14 +62,64 @@ export const RecordTableVirtualizedDataChangedEffect = () => { setLastObjectOperationThatResettedVirtualization(lastObjectOperation); if (isDefined(lastObjectOperation)) { - resetVirtualizationBecauseDataChanged(lastObjectOperation); + if ( + lastObjectOperation.objectMetadataItemId !== objectMetadataItem.id + ) { + return; + } + + if (lastObjectOperation.data.type === 'update-one') { + const updatedFieldNames = + Object.keys(lastObjectOperation?.data.result.updateInput ?? {}) ?? + []; + + const updatedFieldMetadataItems = activeFieldMetadataItems.filter( + (fieldMetadataItemToFilter) => + updatedFieldNames.includes(fieldMetadataItemToFilter.name) || + (fieldMetadataItemToFilter.type === FieldMetadataType.RELATION && + updatedFieldNames.includes( + `${fieldMetadataItemToFilter.name}Id`, + )), + ); + + const updatedFieldMetadataItemIds = + updatedFieldMetadataItems.map(mapById); + + const thereIsAnUpdateOnAFilteredField = currentRecordFilters.some( + (recordFilter) => + updatedFieldMetadataItemIds.includes( + recordFilter.fieldMetadataId, + ), + ); + + const thereIsAnUpdateOnASortedField = currentRecordSorts.some( + (recordSort) => + updatedFieldMetadataItemIds.includes(recordSort.fieldMetadataId), + ); + + if (updatedFieldNames.includes('position')) { + resetVirtualizationBecauseDataChanged(); + } else if ( + thereIsAnUpdateOnAFilteredField || + thereIsAnUpdateOnASortedField + ) { + resetVirtualizationBecauseDataChanged(); + } + } else { + resetVirtualizationBecauseDataChanged(); + } } } }, [ lastObjectOperationThatResettedVirtualization, - objectOperationsForObjectNameSingular, + objectOperations, setLastObjectOperationThatResettedVirtualization, resetVirtualizationBecauseDataChanged, + activeFieldMetadataItems, + currentRecordFilters, + currentRecordSorts, + getObjectMetadataItemById, + objectMetadataItem.id, ]); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfOverscanPages.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfOverscanPages.ts new file mode 100644 index 00000000000..43bf957c9d2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfOverscanPages.ts @@ -0,0 +1 @@ +export const TABLE_VIRTUALIZATION_NUMBER_OF_OVERSCAN_PAGES = 3; diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfRecordsPerPage.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfRecordsPerPage.ts new file mode 100644 index 00000000000..2c61c0b2dd6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfRecordsPerPage.ts @@ -0,0 +1 @@ +export const TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE = 10; diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts index a53bdad24a5..162067833e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts @@ -3,20 +3,20 @@ import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRec import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; -import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { useTriggerFetchPages } from '@/object-record/record-table/virtualization/hooks/useTriggerFetchPages'; import { dataLoadingStatusByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/dataLoadingStatusByRealIndexComponentFamilyState'; import { dataPagesLoadedComponentState } from '@/object-record/record-table/virtualization/states/dataPagesLoadedComponentState'; +import { lastScrollPositionComponentState } from '@/object-record/record-table/virtualization/states/lastScrollPositionComponentState'; import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState'; -import { tableHasAnyFilterOrSortComponentSelector } from '@/object-record/record-table/virtualization/states/tableHasAnyFilterOrSortComponentSelector'; import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState'; -import { type ObjectOperation } from '@/object-record/states/objectOperationsByObjectNameSingularFamilyState'; +import { getVirtualizationOverscanWindow } from '@/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow'; +import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useCallback } from 'react'; import { useRecoilCallback } from 'recoil'; +import { sleep } from '~/utils/sleep'; export const useResetVirtualizationBecauseDataChanged = ( objectNameSingular: string, @@ -26,6 +26,7 @@ export const useResetVirtualizationBecauseDataChanged = ( }); const params = useFindManyRecordIndexTableParams(objectNameSingular); + const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); // TODO: we could optimize this by using an aggregate or using only id: true in recordGqlFields const recordGqlFields = useRecordsFieldVisibleGqlFields({ @@ -48,6 +49,10 @@ export const useResetVirtualizationBecauseDataChanged = ( dataPagesLoadedComponentState, ); + const lastScrollPositionCallbackState = useRecoilComponentCallbackState( + lastScrollPositionComponentState, + ); + const recordIdByRealIndexCallbackState = useRecoilComponentFamilyCallbackState( recordIdByRealIndexComponentFamilyState, @@ -58,10 +63,6 @@ export const useResetVirtualizationBecauseDataChanged = ( dataLoadingStatusByRealIndexComponentFamilyState, ); - const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackState( - recordIndexAllRecordIdsComponentSelector, - ); - const { triggerFetchPagesWithoutDebounce } = useTriggerFetchPages(); const resetVirtualization = useRecoilCallback( @@ -69,68 +70,71 @@ export const useResetVirtualizationBecauseDataChanged = ( async () => { const { totalCount } = await findManyRecordsLazy(); - const currentRecordIds = getSnapshotValue( + const tableScrollWrapperHeight = + scrollWrapperHTMLElement?.clientHeight ?? 0; + + const lastScrollPosition = getSnapshotValue( snapshot, - recordIndexAllRecordIdsSelector, + lastScrollPositionCallbackState, ); - for (const [index] of currentRecordIds.entries()) { - set( - dataLoadingStatusByRealIndexCallbackState({ - realIndex: index, - }), - null, - ); + const totalNumberOfRecordsToVirtualize = + getSnapshotValue( + snapshot, + totalNumberOfRecordsToVirtualizeCallbackState, + ) ?? 0; - set( - recordIdByRealIndexCallbackState({ - realIndex: index, - }), - null, - ); + const { + firstRealIndexInOverscanWindow, + lastRealIndexInOverscanWindow, + } = getVirtualizationOverscanWindow( + lastScrollPosition, + tableScrollWrapperHeight, + totalNumberOfRecordsToVirtualize, + ); + + for (let i = 0; i < totalNumberOfRecordsToVirtualize; i++) { + const indexIsInOverscanWindow = + i >= firstRealIndexInOverscanWindow && + i <= lastRealIndexInOverscanWindow; + + if (!indexIsInOverscanWindow) { + set( + dataLoadingStatusByRealIndexCallbackState({ + realIndex: i, + }), + null, + ); + + set( + recordIdByRealIndexCallbackState({ + realIndex: i, + }), + null, + ); + } } - - set( - recordIndexAllRecordIdsSelector, - currentRecordIds.slice(0, totalCount), - ); set(dataPagesLoadedCallbackState, []); set(totalNumberOfRecordsToVirtualizeCallbackState, totalCount); }, [ - dataPagesLoadedCallbackState, - recordIdByRealIndexCallbackState, - dataLoadingStatusByRealIndexCallbackState, - recordIndexAllRecordIdsSelector, findManyRecordsLazy, + scrollWrapperHTMLElement?.clientHeight, + lastScrollPositionCallbackState, totalNumberOfRecordsToVirtualizeCallbackState, + dataPagesLoadedCallbackState, + dataLoadingStatusByRealIndexCallbackState, + recordIdByRealIndexCallbackState, ], ); - const tableHasAnyFilterOrSort = useRecoilComponentValue( - tableHasAnyFilterOrSortComponentSelector, - ); + const resetVirtualizationBecauseDataChanged = useCallback(async () => { + await resetVirtualization(); - const resetVirtualizationBecauseDataChanged = useCallback( - async (objectOperation: ObjectOperation) => { - if (!objectOperation.data.type.startsWith('update')) { - await resetVirtualization(); + await sleep(50); - await triggerFetchPagesWithoutDebounce(); - } else { - if (tableHasAnyFilterOrSort) { - await resetVirtualization(); - - await triggerFetchPagesWithoutDebounce(); - } - } - }, - [ - resetVirtualization, - triggerFetchPagesWithoutDebounce, - tableHasAnyFilterOrSort, - ], - ); + await triggerFetchPagesWithoutDebounce(); + }, [resetVirtualization, triggerFetchPagesWithoutDebounce]); return { resetVirtualizationBecauseDataChanged, diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerFetchPages.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerFetchPages.ts index fd1f0d3da30..a3bfa9533e9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerFetchPages.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerFetchPages.ts @@ -1,13 +1,14 @@ import { useLazyFindManyRecordsWithOffset } from '@/object-record/hooks/useLazyFindManyRecordsWithOffset'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE } from '@/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfRecordsPerPage'; import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows'; import { dataPagesLoadedComponentState } from '@/object-record/record-table/virtualization/states/dataPagesLoadedComponentState'; import { lastScrollPositionComponentState } from '@/object-record/record-table/virtualization/states/lastScrollPositionComponentState'; import { lowDetailsActivatedComponentState } from '@/object-record/record-table/virtualization/states/lowDetailsActivatedComponentState'; import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState'; +import { getVirtualizationOverscanWindow } from '@/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow'; import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; @@ -23,9 +24,6 @@ const TIME_BETWEEN_UI_BATCH_UPDATE = 25; const PAGING_FOR_UI_UPDATE = 10; const TIME_BETWEEN_TWO_REQUETS = 25; -const DATA_PAGE_SIZE = 10; -const DATA_PAGE_OVERSCAN = 3; - export const useTriggerFetchPages = () => { const { objectNameSingular } = useRecordTableContextOrThrow(); const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); @@ -75,40 +73,18 @@ export const useTriggerFetchPages = () => { lastScrollPositionCallbackState, ); - const numberOfRowsDisplayedInTable = Math.min( - Math.floor(tableScrollWrapperHeight / (RECORD_TABLE_ROW_HEIGHT + 1)), - 30, - ); - - const halfNumberOfRowsVisible = Math.floor( - numberOfRowsDisplayedInTable / 2, - ); - - const realIndexAtTheMiddleOfTheTable = - Math.floor(lastScrollPosition / (RECORD_TABLE_ROW_HEIGHT + 1)) + - halfNumberOfRowsVisible; - - const pageForRealIndex = Math.ceil( - realIndexAtTheMiddleOfTheTable / DATA_PAGE_SIZE, - ); - - const totalNumberOfRealIndices = + const totalNumberOfRecordsToVirtualize = getSnapshotValue( snapshot, totalNumberOfRecordsToVirtualizeCallbackState, ) ?? 0; - const maxPage = Math.ceil(totalNumberOfRealIndices / DATA_PAGE_SIZE); - - const overscanPageAtTop = Math.max( - 0, - pageForRealIndex - DATA_PAGE_OVERSCAN, - ); - - const overscanPageAtBottom = Math.min( - maxPage, - pageForRealIndex + DATA_PAGE_OVERSCAN, - ); + const { overscanPageAtBottom, overscanPageAtTop } = + getVirtualizationOverscanWindow( + lastScrollPosition, + tableScrollWrapperHeight, + totalNumberOfRecordsToVirtualize, + ); const pagesAlreadyLoaded = getSnapshotValue( snapshot, @@ -150,10 +126,13 @@ export const useTriggerFetchPages = () => { if (pagesAreContiguous) { const startingRealIndexToFetch = - (pagesToFetch.at(0) ?? 0) * DATA_PAGE_SIZE; + (pagesToFetch.at(0) ?? 0) * + TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE; const endingRealIndexToFetch = - (pagesToFetch.at(-1) ?? 0) * DATA_PAGE_SIZE + DATA_PAGE_SIZE; + (pagesToFetch.at(-1) ?? 0) * + TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE + + TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE; const numberOfRecordsToFetch = endingRealIndexToFetch - startingRealIndexToFetch; diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/lastObjectOperationThatResettedVirtualizationComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/lastObjectOperationThatResettedVirtualizationComponentState.ts index 2f7fd890342..e2ca873dbf7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/lastObjectOperationThatResettedVirtualizationComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/lastObjectOperationThatResettedVirtualizationComponentState.ts @@ -1,5 +1,5 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { type ObjectOperation } from '@/object-record/states/objectOperationsByObjectNameSingularFamilyState'; +import { type ObjectOperation } from '@/object-record/states/objectOperationsComponentState'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { type Nullable } from 'twenty-shared/types'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/realIndexByRecordIdComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/realIndexByRecordIdComponentSelector.ts new file mode 100644 index 00000000000..f11308bf7f2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/states/realIndexByRecordIdComponentSelector.ts @@ -0,0 +1,46 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState'; +import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; +import { isDefined } from 'twenty-shared/utils'; + +export const realIndexByRecordIdComponentSelector = createComponentSelector<{ + realIndexByRecordIdMap: Map; +}>({ + key: 'realIndexByRecordIdComponentSelector', + componentInstanceContext: RecordTableComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const totalNumberOfRecordsToVirtualize = get( + totalNumberOfRecordsToVirtualizeComponentState.atomFamily({ + instanceId, + }), + ); + + const realIndexByRecordIdMap = new Map(); + + if (!isDefined(totalNumberOfRecordsToVirtualize)) { + return { realIndexByRecordIdMap }; + } + + for ( + let realIndex = 0; + realIndex < totalNumberOfRecordsToVirtualize; + realIndex++ + ) { + const recordId = get( + recordIdByRealIndexComponentFamilyState.atomFamily({ + instanceId, + familyKey: { realIndex }, + }), + ); + + if (isDefined(recordId)) { + realIndexByRecordIdMap.set(recordId, realIndex); + } + } + + return { realIndexByRecordIdMap }; + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow.ts new file mode 100644 index 00000000000..342e26afdd8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/utils/getVirtualizationOverscanWindow.ts @@ -0,0 +1,53 @@ +import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight'; +import { TABLE_VIRTUALIZATION_NUMBER_OF_OVERSCAN_PAGES } from '@/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfOverscanPages'; +import { TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE } from '@/object-record/record-table/virtualization/constants/TableVirtualizationNumberOfRecordsPerPage'; + +export const getVirtualizationOverscanWindow = ( + scrollPosition: number, + scrollWrapperHeight: number, + totalNumberOfRecordsToVirtualize: number, +) => { + const numberOfRowsDisplayedInTable = Math.min( + Math.floor(scrollWrapperHeight / (RECORD_TABLE_ROW_HEIGHT + 1)), + 30, + ); + + const halfNumberOfRowsVisible = Math.floor(numberOfRowsDisplayedInTable / 2); + + const realIndexAtTheMiddleOfTheTable = + Math.floor(scrollPosition / (RECORD_TABLE_ROW_HEIGHT + 1)) + + halfNumberOfRowsVisible; + + const pageForRealIndex = Math.ceil( + realIndexAtTheMiddleOfTheTable / + TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE, + ); + + const maxPage = Math.ceil( + totalNumberOfRecordsToVirtualize / + TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE, + ); + + const overscanPageAtTop = Math.max( + 0, + pageForRealIndex - TABLE_VIRTUALIZATION_NUMBER_OF_OVERSCAN_PAGES, + ); + + const overscanPageAtBottom = Math.min( + maxPage, + pageForRealIndex + TABLE_VIRTUALIZATION_NUMBER_OF_OVERSCAN_PAGES, + ); + + const firstRealIndexInOverscanWindow = + overscanPageAtTop * TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE; + + const lastRealIndexInOverscanWindow = + overscanPageAtBottom * TABLE_VIRTUALIZATION_NUMBER_OF_RECORDS_PER_PAGE; + + return { + overscanPageAtTop, + overscanPageAtBottom, + firstRealIndexInOverscanWindow, + lastRealIndexInOverscanWindow, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/states/objectOperationsByObjectNameSingularFamilyState.ts b/packages/twenty-front/src/modules/object-record/states/objectOperationsComponentState.ts similarity index 60% rename from packages/twenty-front/src/modules/object-record/states/objectOperationsByObjectNameSingularFamilyState.ts rename to packages/twenty-front/src/modules/object-record/states/objectOperationsComponentState.ts index 7dff3d217cc..553ab357cfb 100644 --- a/packages/twenty-front/src/modules/object-record/states/objectOperationsByObjectNameSingularFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/states/objectOperationsComponentState.ts @@ -1,18 +1,16 @@ -import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; +import { createState } from 'twenty-ui/utilities'; export type ObjectOperationData = | { type: 'update-one'; result: { updateInput: any; - updatedRecord: any; }; } | { type: 'update-many'; result: { updateInputs: any[]; - updatedRecords: any[]; }; } | { @@ -30,12 +28,12 @@ export type ObjectOperationData = export type ObjectOperation = { id: string; + objectMetadataItemId: string; data: ObjectOperationData; timestamp: number; }; -export const objectOperationsByObjectNameSingularFamilyState = - createFamilyState({ - key: 'objectOperationsByObjectNameSingularFamilyState', - defaultValue: [], - }); +export const objectOperationsState = createState({ + key: 'objectOperationsState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts index 5b2c820f78b..375f03937de 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts @@ -1,4 +1,6 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation'; import { useStopWorkflowRunMutation } from '~/generated-metadata/graphql'; @@ -9,17 +11,20 @@ export const useStopWorkflowRun = () => { }); const { registerObjectOperation } = useRegisterObjectOperation(); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.WorkflowRun, + }); + const stopWorkflowRun = async (workflowRunId: string) => { - const { data } = await mutate({ + await mutate({ variables: { workflowRunId, }, }); - registerObjectOperation('workflowRun', { + registerObjectOperation(objectMetadataItem, { type: 'update-one', result: { - updatedRecord: data?.stopWorkflowRun, updateInput: { id: workflowRunId }, }, }); diff --git a/packages/twenty-shared/src/utils/array/mapById.ts b/packages/twenty-shared/src/utils/array/mapById.ts new file mode 100644 index 00000000000..3326e0f1655 --- /dev/null +++ b/packages/twenty-shared/src/utils/array/mapById.ts @@ -0,0 +1,3 @@ +export const mapById = (itemToMap: T) => { + return itemToMap.id; +}; diff --git a/packages/twenty-shared/src/utils/array/mapByProperty.ts b/packages/twenty-shared/src/utils/array/mapByProperty.ts new file mode 100644 index 00000000000..5965d8640ee --- /dev/null +++ b/packages/twenty-shared/src/utils/array/mapByProperty.ts @@ -0,0 +1,5 @@ +export const mapByProperty = + (propertyName: keyof T) => + (itemToMap: T) => { + return itemToMap[propertyName]; + }; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 2dfaf333a15..483d9c5bd9d 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -14,6 +14,8 @@ export { findByProperty } from './array/findByProperty'; export { findOrThrow } from './array/findOrThrow'; export { getContiguousIncrementalValues } from './array/getContiguousIncrementalValues'; export { isNonEmptyArray } from './array/isNonEmptyArray'; +export { mapById } from './array/mapById'; +export { mapByProperty } from './array/mapByProperty'; export { sumByProperty } from './array/sumByProperty'; export { upsertIntoArrayOfObjectsComparingId } from './array/upsertIntoArrayOfObjectComparingId'; export { assertUnreachable } from './assertUnreachable';