Fix table virtualization data load < 120 records (#15203)

This PR fixes two bugs : 
- Data loading not working between 60 and 120 records on an object
- Table not refreshing when creating a record in an empty table.

Fixes https://github.com/twentyhq/twenty/issues/15196
This commit is contained in:
Lucas Bordeau
2025-10-20 16:46:23 +02:00
committed by GitHub
parent 89da5ee1b4
commit baf6352eb6
6 changed files with 53 additions and 77 deletions
@@ -14,6 +14,7 @@ import { recordTableWidthComponentState } from '@/object-record/record-table/sta
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { computeVisibleRecordFieldsWidthOnTable } from '@/object-record/record-table/utils/computeVisibleRecordFieldsWidthOnTable';
import { RecordTableVirtualizedDataChangedEffect } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedDataChangedEffect';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
@@ -94,6 +95,7 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
<RecordTableEmptyState />
<RecordTableColumnWidthEffect />
<RecordTableWidthEffect />
<RecordTableVirtualizedDataChangedEffect />
</StyledEmptyStateContainer>
);
};
@@ -11,17 +11,30 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const StyledDebugRow = styled.div`
position: absolute;
left: 250px;
top: ${({ theme }) => theme.spacing(1.25)};
z-index: 20;
color: ${({ theme }) => theme.font.color.primary};
background-color: ${({ theme }) => theme.adaptiveColors.gray1};
border: 1px solid ${({ theme }) => theme.adaptiveColors.blue4};
padding: ${({ theme }) => theme.spacing(0.5)};
display: flex;
max-height: ${({ theme }) => theme.spacing(4)};
overflow: hidden;
`;
const StyledDebugColumn = styled.div<{ width: number }>`
height: 25px;
max-height: 25px;
min-width: ${({ width }) => width}px;
max-width: ${({ width }) => width}px;
overflow: scroll;
overflow: hidden;
display: flex;
text-wrap-mode: nowrap;
padding-right: 2px;
padding-left: 2px;
padding-right: ${({ theme }) => theme.spacing(0.5)};
padding-left: ${({ theme }) => theme.spacing(0.5)};
`;
type RecordTableRowVirtualizedDebugRowHelperProps = {
@@ -62,21 +75,7 @@ export const RecordTableRowVirtualizedDebugRowHelper = ({
const position = record?.position;
return (
<div
style={{
position: 'absolute',
left: 250,
top: 5,
zIndex: 20,
color: 'darkblue',
backgroundColor: 'white',
border: '1px solid blue',
padding: 2,
display: 'flex',
maxHeight: 16,
overflow: 'clip',
}}
>
<StyledDebugRow>
<StyledDebugColumn width={70}>virtual :{virtualIndex}</StyledDebugColumn>
<StyledDebugColumn width={70}>real :{realIndex}</StyledDebugColumn>
<StyledDebugColumn width={100}>pos :{position}</StyledDebugColumn>
@@ -91,6 +90,6 @@ export const RecordTableRowVirtualizedDebugRowHelper = ({
id:
{recordId}
</StyledDebugColumn>
</div>
</StyledDebugRow>
);
};
@@ -1,44 +0,0 @@
import { NUMBER_OF_VIRTUALIZED_ROWS } from '@/object-record/record-table/virtualization/constants/NumberOfVirtualizedRows';
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useResetNumberOfRecordsToVirtualize = () => {
const totalNumberOfRecordsToVirtualizeCallbackState =
useRecoilComponentCallbackState(
totalNumberOfRecordsToVirtualizeComponentState,
);
const resetNumberOfRecordsToVirtualize = useRecoilCallback(
({ snapshot, set }) =>
({
records,
totalCount,
}: {
records: ObjectRecord[];
totalCount: number;
}) => {
const totalNumberOfRecordsToVirtualize = getSnapshotValue(
snapshot,
totalNumberOfRecordsToVirtualizeCallbackState,
);
if (totalCount > NUMBER_OF_VIRTUALIZED_ROWS) {
if (totalNumberOfRecordsToVirtualize !== totalCount) {
set(totalNumberOfRecordsToVirtualizeCallbackState, totalCount);
}
} else {
if (totalNumberOfRecordsToVirtualize !== records.length) {
set(totalNumberOfRecordsToVirtualizeCallbackState, records.length);
}
}
},
[totalNumberOfRecordsToVirtualizeCallbackState],
);
return {
resetNumberOfRecordsToVirtualize,
};
};
@@ -11,7 +11,7 @@ import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/r
import { updateRecordTableCSSVariable } from '@/object-record/record-table/utils/updateRecordTableCSSVariable';
import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows';
import { useReapplyRowSelection } from '@/object-record/record-table/virtualization/hooks/useReapplyRowSelection';
import { useResetNumberOfRecordsToVirtualize } from '@/object-record/record-table/virtualization/hooks/useResetNumberOfRecordsToVirtualize';
import { useResetTableFocuses } from '@/object-record/record-table/virtualization/hooks/useResetTableFocuses';
import { useResetVirtualizedRowTreadmill } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizedRowTreadmill';
import { dataLoadingStatusByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/dataLoadingStatusByRealIndexComponentFamilyState';
@@ -21,6 +21,7 @@ import { lastRealIndexSetComponentState } from '@/object-record/record-table/vir
import { lastScrollPositionComponentState } from '@/object-record/record-table/virtualization/states/lastScrollPositionComponentState';
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
import { scrollAtRealIndexComponentState } from '@/object-record/record-table/virtualization/states/scrollAtRealIndexComponentState';
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
@@ -88,8 +89,6 @@ export const useTriggerInitialRecordTableDataLoad = () => {
const { scrollTableToPosition } = useScrollTableToPosition();
const { resetVirtualizedRowTreadmill } = useResetVirtualizedRowTreadmill();
const { resetNumberOfRecordsToVirtualize } =
useResetNumberOfRecordsToVirtualize();
const { resetTableFocuses } = useResetTableFocuses(recordTableId);
const { upsertRecordsInStore } = useUpsertRecordsInStore();
@@ -98,6 +97,11 @@ export const useTriggerInitialRecordTableDataLoad = () => {
const { reapplyRowSelection } = useReapplyRowSelection();
const totalNumberOfRecordsToVirtualizeCallbackState =
useRecoilComponentCallbackState(
totalNumberOfRecordsToVirtualizeComponentState,
);
const triggerInitialRecordTableDataLoad = useRecoilCallback(
({ snapshot, set }) =>
async () => {
@@ -153,18 +157,17 @@ export const useTriggerInitialRecordTableDataLoad = () => {
null,
);
}
const { records: findManyRecords, totalCount: findManyTotalCount } =
await findManyRecordsLazy();
records = findManyRecords;
totalCount = findManyTotalCount;
}
if (isDefined(records)) {
resetNumberOfRecordsToVirtualize({
records,
totalCount,
});
set(totalNumberOfRecordsToVirtualizeCallbackState, totalCount);
if (isDefined(records)) {
upsertRecordsInStore(records);
loadRecordsToVirtualRows({
@@ -209,7 +212,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
findManyRecordsLazy,
dataLoadingStatusByRealIndexCallbackState,
recordIdByRealIndexCallbackState,
resetNumberOfRecordsToVirtualize,
totalNumberOfRecordsToVirtualizeCallbackState,
upsertRecordsInStore,
loadRecordsToVirtualRows,
reapplyRowSelection,
@@ -1,8 +1,9 @@
import { type Decorator } from '@storybook/react';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { labelIdentifierFieldMetadataItemSelector } from '@/object-metadata/states/labelIdentifierFieldMetadataItemSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -25,6 +26,8 @@ import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
import { useEffect, useMemo } from 'react';
@@ -44,6 +47,12 @@ const InternalTableStateLoaderEffect = ({
currentRecordFieldsComponentState,
);
const setContextStoreCurrentViewId = useSetRecoilComponentState(
contextStoreCurrentViewIdComponentState,
);
const setCoreViews = useSetRecoilState(coreViewsState);
const setRecordTableData = useSetRecordTableData({
recordTableId,
});
@@ -67,12 +76,18 @@ const InternalTableStateLoaderEffect = ({
.filter(isDefined);
setCurrentRecordFields(recordFields);
setCoreViews([view as unknown as CoreViewWithRelations]);
setContextStoreCurrentViewId(view.id);
}, [
loadRecordIndexStates,
objectMetadataItem,
setRecordTableData,
view,
setCurrentRecordFields,
setContextStoreCurrentViewId,
setCoreViews,
]);
return null;
@@ -22,8 +22,10 @@ import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
import { LIST_PLANS } from '@/billing/graphql/queries/listPlans';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { isDefined } from 'twenty-shared/utils';
import { mockBillingPlans } from '~/testing/mock-data/billing-plans';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
import { getRolesMock } from '~/testing/mock-data/roles';
import { mockedTasks } from '~/testing/mock-data/tasks';
@@ -35,8 +37,6 @@ import {
import { oneSucceededWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields';
import { LIST_PLANS } from '@/billing/graphql/queries/listPlans';
import { mockBillingPlans } from '~/testing/mock-data/billing-plans';
const peopleMock = getPeopleRecordConnectionMock();
const companiesMock = getCompaniesRecordConnectionMock();
@@ -384,6 +384,7 @@ export const graphqlMocks = {
startCursor: null,
endCursor: null,
},
totalCount: mockedData.length,
},
},
});