Fix table loading (#12653)

As per title
This commit is contained in:
Charles Bochet
2025-06-17 15:01:28 +02:00
committed by GitHub
parent fb9d5066dc
commit f3a8b849aa
29 changed files with 592 additions and 467 deletions

View File

@ -1,9 +1,8 @@
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { ReactNode, act } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { MockedResponse } from '@apollo/client/testing';
@ -679,16 +678,14 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
);
};
describe('useLazyLoadRecordIndexTable', () => {
describe('useRecordIndexTableQuery', () => {
it('should fetch', async () => {
const { result } = renderHook(
() => {
const { findManyRecords, ...result } =
useLazyLoadRecordIndexTable(objectNameSingular);
const { records } = useRecordIndexTableQuery(objectNameSingular);
return {
findManyRecords,
...result,
records,
};
},
{
@ -696,12 +693,8 @@ describe('useLazyLoadRecordIndexTable', () => {
},
);
expect(result.current.loading).toBe(false);
await act(async () => {
await result.current.findManyRecords();
await waitFor(() => {
expect(result.current.records).toHaveLength(16);
});
expect(result.current.records).toHaveLength(16);
});
});

View File

@ -24,10 +24,10 @@ import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyReco
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
import { capitalize, isDefined } from 'twenty-shared/utils';
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
RecordGqlOperationVariables & {

View File

@ -119,7 +119,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
loading,
error,
fetchMoreRecords,
queryStateIdentifier: queryIdentifier,
queryIdentifier,
hasNextPage,
pageInfo,
};

View File

@ -1,4 +1,4 @@
import { useRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
@ -6,6 +6,7 @@ import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useHandleFindManyRecordsCompleted = <T>({
@ -17,35 +18,36 @@ export const useHandleFindManyRecordsCompleted = <T>({
objectMetadataItem: ObjectMetadataItem;
onCompleted?: OnFindManyRecordsCompleted<T>;
}) => {
const [, setLastCursor] = useRecoilState(cursorFamilyState(queryIdentifier));
const setLastCursor = useSetRecoilState(cursorFamilyState(queryIdentifier));
const [, setHasNextPage] = useRecoilState(
const setHasNextPage = useSetRecoilState(
hasNextPageFamilyState(queryIdentifier),
);
const handleFindManyRecordsCompleted = (
data: RecordGqlOperationFindManyResult,
) => {
if (!isDefined(data)) {
onCompleted?.([]);
}
const handleFindManyRecordsCompleted = useCallback(
(data: RecordGqlOperationFindManyResult) => {
if (!isDefined(data)) {
onCompleted?.([]);
}
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
const records = getRecordsFromRecordConnection({
recordConnection: data?.[objectMetadataItem.namePlural],
}) as T[];
const records = getRecordsFromRecordConnection({
recordConnection: data?.[objectMetadataItem.namePlural],
}) as T[];
onCompleted?.(records, {
pageInfo,
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
});
onCompleted?.(records, {
pageInfo,
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
});
if (isDefined(data?.[objectMetadataItem.namePlural])) {
setLastCursor(pageInfo.endCursor ?? '');
setHasNextPage(pageInfo.hasNextPage ?? false);
}
};
if (isDefined(data?.[objectMetadataItem.namePlural])) {
setLastCursor(pageInfo.endCursor ?? '');
setHasNextPage(pageInfo.hasNextPage ?? false);
}
},
[onCompleted, objectMetadataItem.namePlural, setLastCursor, setHasNextPage],
);
return {
handleFindManyRecordsCompleted,

View File

@ -4,6 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { logError } from '~/utils/logError';
import { useCallback } from 'react';
export const useHandleFindManyRecordsError = ({
handleError,
@ -14,16 +15,19 @@ export const useHandleFindManyRecordsError = ({
}) => {
const { enqueueSnackBar } = useSnackBar();
const handleFindManyRecordsError = (error: ApolloError) => {
logError(
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
error,
);
enqueueSnackBar(`${error.message}`, {
variant: SnackBarVariant.Error,
});
handleError?.(error);
};
const handleFindManyRecordsError = useCallback(
(error: ApolloError) => {
logError(
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
error,
);
enqueueSnackBar(`${error.message}`, {
variant: SnackBarVariant.Error,
});
handleError?.(error);
},
[enqueueSnackBar, handleError, objectMetadataItem.namePlural],
);
return {
handleFindManyRecordsError,

View File

@ -34,7 +34,7 @@ export const useLazyFetchAllRecords = <T>({
const [progress, setProgress] = useState<ExportProgress>({
displayType: 'number',
});
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
const { fetchMoreRecordsLazy, findManyRecordsLazy } = useLazyFindManyRecords({
objectNameSingular,
filter,
orderBy,
@ -47,12 +47,12 @@ export const useLazyFetchAllRecords = <T>({
});
const fetchAllRecords = useCallback(async () => {
if (!isDefined(findManyRecords)) {
if (!isDefined(findManyRecordsLazy)) {
return [];
}
setIsDownloading(true);
const findManyRecordsDataResult = await findManyRecords();
const findManyRecordsDataResult = await findManyRecordsLazy();
const firstQueryResult =
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
@ -84,7 +84,7 @@ export const useLazyFetchAllRecords = <T>({
break;
}
if (!isDefined(fetchMore)) {
if (!isDefined(fetchMoreRecordsLazy)) {
break;
}
@ -92,12 +92,7 @@ export const useLazyFetchAllRecords = <T>({
await sleep(delayMs);
}
const rawResult = await fetchMore({
variables: {
lastCursor: lastCursor,
limit,
},
});
const rawResult = await fetchMoreRecordsLazy();
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
@ -126,8 +121,8 @@ export const useLazyFetchAllRecords = <T>({
return records;
}, [
delayMs,
fetchMore,
findManyRecords,
fetchMoreRecordsLazy,
findManyRecordsLazy,
objectMetadataItem.namePlural,
limit,
maximumRequests,

View File

@ -0,0 +1,194 @@
import {
ApolloError,
ApolloQueryResult,
FetchMoreQueryOptions,
OperationVariables,
WatchQueryFetchPolicy,
} from '@apollo/client';
import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
RecordGqlOperationVariables & {
onCompleted?: OnFindManyRecordsCompleted<T>;
skip?: boolean;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
fetchPolicy?: WatchQueryFetchPolicy;
};
type UseFindManyRecordsStateParams<
T,
TData = RecordGqlOperationFindManyResult,
> = Omit<
UseFindManyRecordsParams<T>,
'skip' | 'recordGqlFields' | 'fetchPolicy' | 'onCompleted'
> & {
data: RecordGqlOperationFindManyResult | undefined;
error: ApolloError | undefined;
fetchMore<
TFetchData = TData,
TFetchVars extends OperationVariables = OperationVariables,
>(
fetchMoreOptions: FetchMoreQueryOptions<TFetchVars, TFetchData> & {
updateQuery?: (
previousQueryResult: TData,
options: {
fetchMoreResult: TFetchData;
variables: TFetchVars;
},
) => TData;
},
): Promise<ApolloQueryResult<TFetchData>>;
objectMetadataItem: ObjectMetadataItem;
};
export const useLazyFetchMoreRecordsWithPagination = <
T extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
filter,
orderBy,
limit,
error,
fetchMore,
objectMetadataItem,
}: UseFindManyRecordsStateParams<T>) => {
const queryIdentifier = getQueryIdentifier({
objectNameSingular,
filter,
limit,
orderBy,
});
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
objectMetadataItem,
});
// TODO: put this into a util inspired from https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts
// This function is equivalent to merge function + read function in field policy
const fetchMoreRecordsLazy = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const hasNextPageLocal = snapshot
.getLoadable(hasNextPageFamilyState(queryIdentifier))
.getValue();
const lastCursorLocal = snapshot
.getLoadable(cursorFamilyState(queryIdentifier))
.getValue();
// Remote objects does not support hasNextPage. We cannot rely on it to fetch more records.
if (
hasNextPageLocal ||
(!isAggregationEnabled(objectMetadataItem) && !error)
) {
try {
const { data: fetchMoreDataResult } = await fetchMore({
variables: {
filter,
orderBy,
lastCursor: isNonEmptyString(lastCursorLocal)
? lastCursorLocal
: undefined,
},
updateQuery: (prev, { fetchMoreResult }) => {
const previousEdges =
prev?.[objectMetadataItem.namePlural]?.edges;
const nextEdges =
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
let newEdges: RecordGqlEdge[] = previousEdges ?? [];
if (isNonEmptyArray(nextEdges)) {
newEdges = filterUniqueRecordEdgesByCursor([
...newEdges,
...(fetchMoreResult?.[objectMetadataItem.namePlural]
?.edges ?? []),
]);
}
const pageInfo =
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
if (isDefined(pageInfo)) {
set(
cursorFamilyState(queryIdentifier),
pageInfo.endCursor ?? '',
);
set(
hasNextPageFamilyState(queryIdentifier),
pageInfo.hasNextPage ?? false,
);
}
return Object.assign({}, prev, {
[objectMetadataItem.namePlural]: {
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Connection`,
edges: newEdges,
pageInfo:
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
totalCount:
fetchMoreResult?.[objectMetadataItem.namePlural]
.totalCount,
},
} as RecordGqlOperationFindManyResult);
},
});
return {
data: fetchMoreDataResult?.[objectMetadataItem.namePlural],
totalCount:
fetchMoreDataResult?.[objectMetadataItem.namePlural]
?.totalCount,
records: getRecordsFromRecordConnection({
recordConnection: {
edges:
fetchMoreDataResult?.[objectMetadataItem.namePlural]?.edges,
pageInfo:
fetchMoreDataResult?.[objectMetadataItem.namePlural]
?.pageInfo,
},
}) as T[],
};
} catch (error) {
handleFindManyRecordsError(error as ApolloError);
return { error: error as ApolloError };
}
}
},
[
queryIdentifier,
objectMetadataItem,
error,
fetchMore,
filter,
orderBy,
handleFindManyRecordsError,
],
);
return {
fetchMoreRecordsLazy,
};
};

View File

@ -2,12 +2,13 @@ import { useLazyQuery } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords';
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
import { useLazyFetchMoreRecordsWithPagination } from '@/object-record/hooks/useLazyFetchMoreRecordsWithPagination';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
@ -16,7 +17,7 @@ import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
type UseLazyFindManyRecordsParams<T> = Omit<
UseFindManyRecordsParams<T>,
'skip'
'skip' | 'onCompleted' | 'onError'
>;
export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
@ -25,9 +26,6 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
orderBy,
limit,
recordGqlFields,
fetchPolicy,
onCompleted,
onError,
}: UseLazyFindManyRecordsParams<T>) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@ -40,7 +38,6 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
objectMetadataItem,
handleError: onError,
});
const queryIdentifier = getQueryIdentifier({
@ -53,7 +50,6 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({
objectMetadataItem,
queryIdentifier,
onCompleted,
});
const objectPermissions = useObjectPermissionsForObject(
@ -62,30 +58,28 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const hasReadPermission = objectPermissions.canReadObjectRecords;
const [findManyRecords, { data, loading, error, fetchMore }] =
const [findManyRecords, { data, error, fetchMore }] =
useLazyQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
variables: {
filter,
limit,
orderBy,
},
fetchPolicy: fetchPolicy,
fetchPolicy: 'network-only',
onCompleted: handleFindManyRecordsCompleted,
onError: handleFindManyRecordsError,
});
const { fetchMoreRecords, totalCount, records, hasNextPage } =
useFetchMoreRecordsWithPagination<T>({
objectNameSingular,
filter,
orderBy,
limit,
onCompleted,
fetchMore,
data,
error,
objectMetadataItem,
});
const { fetchMoreRecordsLazy } = useLazyFetchMoreRecordsWithPagination<T>({
objectNameSingular,
filter,
orderBy,
limit,
fetchMore,
data,
error,
objectMetadataItem,
});
const findManyRecordsLazy = useRecoilCallback(
({ set }) =>
@ -96,9 +90,10 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
return {
data: null,
loading: false,
records: [],
totalCount: 0,
hasNextPage: false,
error: undefined,
called: true,
};
}
@ -115,7 +110,29 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
set(hasNextPageFamilyState(queryIdentifier), hasNextPage);
set(cursorFamilyState(queryIdentifier), lastCursor);
return result;
const records = getRecordsFromRecordConnection({
recordConnection: {
edges: result?.data?.[objectMetadataItem.namePlural]?.edges ?? [],
pageInfo: result?.data?.[objectMetadataItem.namePlural]
?.pageInfo ?? {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
},
});
const totalCount =
result?.data?.[objectMetadataItem.namePlural]?.totalCount ?? 0;
return {
data: result?.data,
records,
totalCount,
hasNextPage,
error: result?.error,
};
},
[
hasReadPermission,
@ -126,16 +143,8 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
);
return {
objectMetadataItem,
records,
totalCount,
loading: hasReadPermission ? loading : false,
error: hasReadPermission ? error : undefined,
fetchMore,
fetchMoreRecords,
queryStateIdentifier: queryIdentifier,
findManyRecords: findManyRecordsLazy,
hasNextPage,
hasReadPermission,
findManyRecordsLazy,
fetchMoreRecordsLazy,
queryIdentifier,
};
};

View File

@ -6,8 +6,6 @@ import { useRecoilValue } from 'recoil';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { GRAY_SCALE } from 'twenty-ui/theme';
@ -33,23 +31,11 @@ export const RecordBoardColumnFetchMoreLoader = () => {
columnDefinition.id,
);
const isLoadMoreLocked = useRecoilComponentValueV2(
isRecordIndexLoadMoreLockedComponentState,
);
const { ref, inView } = useInView();
useEffect(() => {
if (isLoadMoreLocked) {
return;
}
setShouldFetchMore(inView);
}, [setShouldFetchMore, inView, isLoadMoreLocked]);
if (isLoadMoreLocked) {
return null;
}
}, [setShouldFetchMore, inView]);
return (
<div ref={ref}>

View File

@ -1,52 +0,0 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => {
const showAuthModal = useShowAuthModal();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { setRecordTableData, setIsRecordTableInitialLoading } =
useRecordTable();
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const {
findManyRecords,
records,
loading,
totalCount,
fetchMoreRecords,
queryStateIdentifier,
hasNextPage,
} = useLazyFindManyRecords({
...params,
recordGqlFields,
onCompleted: () => {
setIsRecordTableInitialLoading(false);
},
onError: () => {
setIsRecordTableInitialLoading(false);
},
});
return {
findManyRecords,
records: !showAuthModal ? records : SIGN_IN_BACKGROUND_MOCK_COMPANIES,
totalCount: totalCount,
loading,
fetchMoreRecords,
queryStateIdentifier,
setRecordTableData,
hasNextPage,
};
};

View File

@ -84,19 +84,14 @@ export const useLoadRecordIndexBoardColumn = ({
},
]);
const {
records,
loading,
fetchMoreRecords,
queryStateIdentifier,
hasNextPage,
} = useFindManyRecords({
objectNameSingular,
filter: combinedFilters,
orderBy,
recordGqlFields,
limit: 10,
});
const { records, loading, fetchMoreRecords, queryIdentifier, hasNextPage } =
useFindManyRecords({
objectNameSingular,
filter: combinedFilters,
orderBy,
recordGqlFields,
limit: 10,
});
useEffect(() => {
setRecordIdsForColumn(columnId, records);
@ -110,7 +105,7 @@ export const useLoadRecordIndexBoardColumn = ({
records,
loading,
fetchMoreRecords,
queryStateIdentifier,
queryIdentifier,
hasNextPage,
};
};

View File

@ -0,0 +1,24 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
export const useRecordIndexTableFetchMore = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const { fetchMoreRecordsLazy, queryIdentifier } = useLazyFindManyRecords({
...params,
recordGqlFields,
});
return {
fetchMoreRecordsLazy,
queryIdentifier,
};
};

View File

@ -0,0 +1,35 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
export const useRecordIndexTableQuery = (objectNameSingular: string) => {
const { recordTableId } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const showAuthModal = useShowAuthModal();
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const { records, hasNextPage, queryIdentifier, loading } = useFindManyRecords(
{
...params,
recordGqlFields,
skip: showAuthModal,
},
);
return {
records: showAuthModal ? SIGN_IN_BACKGROUND_MOCK_COMPANIES : records,
loading: showAuthModal ? false : loading,
hasNextPage,
queryIdentifier,
};
};

View File

@ -1,9 +0,0 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
export const isRecordIndexLoadMoreLockedComponentState =
createComponentStateV2<boolean>({
key: 'isRecordIndexLoadMoreLockedComponentState',
componentInstanceContext: ViewComponentInstanceContext,
defaultValue: false,
});

View File

@ -6,7 +6,6 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin
type RecordTableComponentInstanceEffectProps = {
onColumnsChange: (columns: ColumnDefinition<FieldMetadata>[]) => void;
onEntityCountChange?: (count: number) => void | Promise<void>;
};
export const RecordTableComponentInstanceEffect = ({

View File

@ -11,24 +11,20 @@ import { recordTableHoverPositionComponentState } from '@/object-record/record-t
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentFamilyCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type useSetRecordTableDataProps = {
recordTableId?: string;
onEntityCountChange: (
entityCount?: number,
currentRecordGroupId?: string,
) => void;
};
export const useSetRecordTableData = ({
recordTableId,
onEntityCountChange,
}: useSetRecordTableDataProps) => {
const recordIndexRecordIdsByGroupFamilyState =
useRecoilComponentCallbackStateV2(
useRecoilComponentFamilyCallbackStateV2(
recordIndexRecordIdsByGroupComponentFamilyState,
recordTableId,
);
@ -38,7 +34,7 @@ export const useSetRecordTableData = ({
recordTableId,
);
const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2(
const isRowSelectedFamilyState = useRecoilComponentFamilyCallbackStateV2(
isRowSelectedComponentFamilyState,
recordTableId,
);
@ -61,11 +57,9 @@ export const useSetRecordTableData = ({
<T extends ObjectRecord>({
records,
currentRecordGroupId,
totalCount,
}: {
records: T[];
currentRecordGroupId?: string;
totalCount?: number;
}) => {
for (const record of records) {
// TODO: refactor with scoped state later
@ -115,8 +109,6 @@ export const useSetRecordTableData = ({
} else {
set(recordIndexAllRecordIdsSelector, recordIds);
}
onEntityCountChange(totalCount, currentRecordGroupId);
}
},
[
@ -125,7 +117,6 @@ export const useSetRecordTableData = ({
hasUserSelectedAllRowsState,
setIsFocusActiveForCurrentPosition,
setRecordTableHoverPosition,
onEntityCountChange,
isRowSelectedFamilyState,
],
);

View File

@ -11,18 +11,15 @@ import { availableTableColumnsComponentState } from '@/object-record/record-tabl
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useLeaveTableFocus } from './internal/useLeaveTableFocus';
import { useResetTableRowSelection } from './internal/useResetTableRowSelection';
import { useSelectAllRows } from './internal/useSelectAllRows';
import { useSetRecordTableData } from './internal/useSetRecordTableData';
import { useSetRecordTableFocusPosition } from './internal/useSetRecordTableFocusPosition';
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
type useRecordTableProps = {
@ -56,11 +53,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
[availableTableColumnsState],
);
const setOnEntityCountChange = useSetRecoilComponentStateV2(
onEntityCountChangeComponentState,
recordTableId,
);
const setOnColumnsChange = useSetRecoilComponentStateV2(
onColumnsChangeComponentState,
recordTableId,
@ -76,11 +68,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
recordTableId,
);
const setRecordTableLastRowVisible = useSetRecoilComponentStateV2(
tableLastRowVisibleComponentState,
recordTableId,
);
const onColumnsChangeState = useRecoilComponentCallbackStateV2(
onColumnsChangeComponentState,
recordTableId,
@ -99,29 +86,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
[onColumnsChangeState],
);
const onEntityCountChangeState = useRecoilComponentCallbackStateV2(
onEntityCountChangeComponentState,
recordTableId,
);
const onEntityCountChange = useRecoilCallback(
({ snapshot }) =>
(count?: number, currentRecordGroupId?: string) => {
const onEntityCountChange = getSnapshotValue(
snapshot,
onEntityCountChangeState,
);
onEntityCountChange?.(count, currentRecordGroupId);
},
[onEntityCountChangeState],
);
const setRecordTableData = useSetRecordTableData({
recordTableId,
onEntityCountChange,
});
const leaveTableFocus = useLeaveTableFocus(recordTableId);
const setRowSelected = useSetRowSelectedState(recordTableId);
@ -139,8 +103,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
return {
onColumnsChange,
setAvailableTableColumns,
setOnEntityCountChange,
setRecordTableData,
leaveTableFocus,
setRowSelected,
resetTableRowSelection,
@ -148,7 +110,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
selectAllRows,
setOnColumnsChange,
setIsRecordTableInitialLoading,
setRecordTableLastRowVisible,
setFocusPosition,
setHasUserSelectedAllRows,
setOnToggleColumnSort,

View File

@ -1,13 +1,15 @@
import styled from '@emotion/styled';
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback } from 'recoil';
import { useRecoilState } from 'recoil';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useRecordIndexTableFetchMore } from '@/object-record/record-index/hooks/useRecordIndexTableFetchMore';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { GRAY_SCALE } from 'twenty-ui/theme';
import { useDebouncedCallback } from 'use-debounce';
const StyledText = styled.div`
align-items: center;
@ -20,21 +22,13 @@ const StyledText = styled.div`
`;
export const RecordTableBodyFetchMoreLoader = () => {
const { setRecordTableLastRowVisible } = useRecordTable();
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
const isRecordTableLoadMoreLocked = useRecoilComponentValueV2(
isRecordIndexLoadMoreLockedComponentState,
);
const { fetchMoreRecordsLazy } =
useRecordIndexTableFetchMore(objectNameSingular);
const onLastRowVisible = useRecoilCallback(
() => async (inView: boolean) => {
if (isRecordTableLoadMoreLocked) {
return;
}
setRecordTableLastRowVisible(inView);
},
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
const [isFetchingMoreRecords, setIsFetchingMoreRecords] = useRecoilState(
isFetchingMoreRecordsFamilyState(recordTableId),
);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
@ -43,11 +37,22 @@ export const RecordTableBodyFetchMoreLoader = () => {
hasRecordTableFetchedAllRecordsComponentStateV2,
);
const showLoadingMoreRow =
!hasRecordTableFetchedAllRecordsComponents && !isRecordTableLoadMoreLocked;
const showLoadingMoreRow = !hasRecordTableFetchedAllRecordsComponents;
const debouncedFetchMoreRecordsLazy = useDebouncedCallback(
fetchMoreRecordsLazy,
100,
);
const { ref: tbodyRef } = useInView({
onChange: onLastRowVisible,
onChange: async (inView) => {
if (isFetchingMoreRecords || !inView) {
return;
}
setIsFetchingMoreRecords(true);
await debouncedFetchMoreRecordsLazy();
setIsFetchingMoreRecords(false);
},
delay: 1000,
rootMargin: '1000px',
root: scrollWrapperHTMLElement,

View File

@ -1,67 +1,73 @@
import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isNonEmptyString } from '@sniptt/guards';
export const RecordTableNoRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();
const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
const { setIsRecordTableInitialLoading } = useRecordTable();
const { records, loading, hasNextPage } =
useRecordIndexTableQuery(objectNameSingular);
const setRecordTableData = useSetRecordTableData({
recordTableId,
});
const showAuthModal = useShowAuthModal();
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
const {
findManyRecords,
fetchMoreRecords,
records,
totalCount,
setRecordTableData,
loading,
queryStateIdentifier,
hasNextPage,
} = useLazyLoadRecordIndexTable(objectNameSingular);
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
);
const tableLastRowVisible = useRecoilComponentValueV2(
tableLastRowVisibleComponentState,
);
const [encounteredUnrecoverableError, setEncounteredUnrecoverableError] =
useRecoilComponentStateV2(tableEncounteredUnrecoverableErrorComponentState);
const setHasRecordTableFetchedAllRecordsComponents =
useSetRecoilComponentStateV2(
hasRecordTableFetchedAllRecordsComponentStateV2,
);
const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState(
lastShowPageRecordIdState,
const setIsRecordTableInitialLoading = useSetRecoilComponentStateV2(
isRecordTableInitialLoadingComponentState,
);
const isFetchingMoreRecords = useRecoilValue(
isFetchingMoreRecordsFamilyState(recordTableId),
);
const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState);
const { scrollToPosition } = useScrollToPosition();
useEffect(() => {
if (isNonEmptyString(lastShowPageRecordId) && !hasInitializedScroll) {
if (!loading && !isFetchingMoreRecords) {
setRecordTableData({
records,
});
setHasRecordTableFetchedAllRecordsComponents(!hasNextPage);
setIsRecordTableInitialLoading(false);
}
}, [
hasNextPage,
isFetchingMoreRecords,
loading,
records,
setHasRecordTableFetchedAllRecordsComponents,
setIsRecordTableInitialLoading,
setRecordTableData,
showAuthModal,
]);
useEffect(() => {
if (hasInitializedScroll) {
return;
}
if (isNonEmptyString(lastShowPageRecordId)) {
const isRecordAlreadyFetched = records.some(
(record) => record.id === lastShowPageRecordId,
);
@ -78,76 +84,7 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
setHasInitializedScroll(true);
}
}
}, [
loading,
lastShowPageRecordId,
records,
scrollToPosition,
hasInitializedScroll,
setLastShowPageRecordId,
]);
useEffect(() => {
if (!loading) {
setRecordTableData({
records,
totalCount,
});
}
}, [records, totalCount, setRecordTableData, loading]);
const fetchMoreDebouncedIfRequested = useDebouncedCallback(async () => {
// We are debouncing here to give the user some room to scroll if they want to within this throttle window
return await fetchMoreRecords();
}, 100);
useEffect(() => {
const allRecordsHaveBeenFetched = !hasNextPage;
setHasRecordTableFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
}, [hasNextPage, setHasRecordTableFetchedAllRecordsComponents]);
useEffect(() => {
(async () => {
if (
!isFetchingMoreObjects &&
tableLastRowVisible &&
hasNextPage &&
!encounteredUnrecoverableError
) {
const result = await fetchMoreDebouncedIfRequested();
const isForbidden =
result?.error?.graphQLErrors.some(
(e) => e.extensions?.code === 'FORBIDDEN',
) ?? false;
if (isForbidden) {
setEncounteredUnrecoverableError(true);
}
}
})();
}, [
hasNextPage,
records,
lastShowPageRecordId,
scrollToPosition,
fetchMoreDebouncedIfRequested,
isFetchingMoreObjects,
tableLastRowVisible,
encounteredUnrecoverableError,
setEncounteredUnrecoverableError,
]);
useEffect(() => {
setIsRecordTableInitialLoading(false);
if (showAuthModal) {
return;
}
findManyRecords();
}, [findManyRecords, setIsRecordTableInitialLoading, showAuthModal]);
}, [hasInitializedScroll, lastShowPageRecordId, records, scrollToPosition]);
return <></>;
};

View File

@ -1,37 +1,35 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isNonEmptyString } from '@sniptt/guards';
import { OnboardingStatus } from '~/generated-metadata/graphql';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();
const { recordTableId } = useRecordTableContextOrThrow();
const [hasInitialized, setHasInitialized] = useState(false);
const setRecordTableData = useSetRecordTableData({
recordTableId,
});
const onboardingStatus = useOnboardingStatus();
const setIsRecordTableInitialLoading = useSetRecoilComponentStateV2(
isRecordTableInitialLoadingComponentState,
);
const recordGroupId = useCurrentRecordGroupId();
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
const {
findManyRecords,
records,
totalCount,
setRecordTableData,
loading,
hasNextPage,
} = useLazyLoadRecordIndexTable(objectNameSingular);
const { records, loading, hasNextPage } =
useRecordIndexTableQuery(objectNameSingular);
const setHasRecordFetchedAllRecordsComponents =
useSetRecoilComponentFamilyStateV2(
@ -44,7 +42,26 @@ export const RecordTableRecordGroupBodyEffect = () => {
const { scrollToPosition } = useScrollToPosition();
useEffect(() => {
if (isNonEmptyString(lastShowPageRecordId) && !hasInitializedScroll) {
if (!loading) {
setRecordTableData({
records,
currentRecordGroupId: recordGroupId,
});
setIsRecordTableInitialLoading(false);
setHasRecordFetchedAllRecordsComponents(!hasNextPage);
}
}, [
hasNextPage,
loading,
records,
recordGroupId,
setHasRecordFetchedAllRecordsComponents,
setIsRecordTableInitialLoading,
setRecordTableData,
]);
useEffect(() => {
if (isNonEmptyString(lastShowPageRecordId)) {
const recordPosition = records.findIndex(
(record) => record.id === lastShowPageRecordId,
);
@ -53,44 +70,9 @@ export const RecordTableRecordGroupBodyEffect = () => {
const positionInPx = recordPosition * ROW_HEIGHT;
scrollToPosition(positionInPx);
setHasInitializedScroll(true);
}
}
}, [
loading,
lastShowPageRecordId,
records,
scrollToPosition,
hasInitializedScroll,
]);
useEffect(() => {
if (!loading) {
setRecordTableData({
records,
currentRecordGroupId: recordGroupId,
totalCount,
});
}
}, [records, totalCount, setRecordTableData, loading, recordGroupId]);
useEffect(() => {
const allRecordsHaveBeenFetched = !hasNextPage;
setHasRecordFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
}, [hasNextPage, setHasRecordFetchedAllRecordsComponents]);
useEffect(() => {
if (onboardingStatus !== OnboardingStatus.COMPLETED) {
return;
}
if (!hasInitialized) {
findManyRecords();
setHasInitialized(true);
}
}, [onboardingStatus, findManyRecords, hasInitialized]);
}, [lastShowPageRecordId, records, scrollToPosition]);
return <></>;
};

View File

@ -1,6 +1,5 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecordIndexTableFetchMore } from '@/object-record/record-index/hooks/useRecordIndexTableFetchMore';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@ -14,26 +13,23 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
const currentRecordGroupId = useCurrentRecordGroupId();
const { fetchMoreRecords } = useLazyLoadRecordIndexTable(objectNameSingular);
const { fetchMoreRecordsLazy } =
useRecordIndexTableFetchMore(objectNameSingular);
const hasFetchedAllRecords = useRecoilComponentFamilyValueV2(
recordIndexHasFetchedAllRecordsByGroupComponentState,
currentRecordGroupId,
);
const isLoadMoreLocked = useRecoilComponentValueV2(
isRecordIndexLoadMoreLockedComponentState,
);
const recordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector,
);
const handleLoadMore = () => {
fetchMoreRecords();
fetchMoreRecordsLazy();
};
if (hasFetchedAllRecords || isLoadMoreLocked) {
if (hasFetchedAllRecords) {
return null;
}

View File

@ -1,10 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const onEntityCountChangeComponentState = createComponentStateV2<
((entityCount?: number, currentRecordGroupId?: string) => void) | undefined
>({
key: 'onEntityCountChangeComponentState',
defaultValue: undefined,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,9 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const tableEncounteredUnrecoverableErrorComponentState =
createComponentStateV2<boolean>({
key: 'tableEncounteredUnrecoverableErrorComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,9 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const tableLastRowVisibleComponentState =
createComponentStateV2<boolean>({
key: 'tableLastRowVisibleComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,11 +1,15 @@
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useCallback } from 'react';
export const useScrollToPosition = () => {
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const scrollToPosition = (scrollPositionInPx: number) => {
scrollWrapperHTMLElement?.scrollTo({ top: scrollPositionInPx });
};
const scrollToPosition = useCallback(
(scrollPositionInPx: number) => {
scrollWrapperHTMLElement?.scrollTo({ top: scrollPositionInPx });
},
[scrollWrapperHTMLElement],
);
return { scrollToPosition };
};

View File

@ -0,0 +1,97 @@
/* eslint-disable prefer-arrow/prefer-arrow-functions */
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { ComponentFamilyReadOnlySelectorV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyReadOnlySelectorV2';
import { ComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilySelectorV2';
import { ComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateV2';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { useCallback } from 'react';
import { RecoilState, RecoilValueReadOnly, SerializableParam } from 'recoil';
export function useRecoilComponentFamilyCallbackStateV2<
ValueType,
FamilyKey extends SerializableParam,
>(
componentFamilyState: ComponentFamilyStateV2<ValueType, FamilyKey>,
instanceIdFromProps?: string,
): (familyKey: FamilyKey) => RecoilState<ValueType>;
export function useRecoilComponentFamilyCallbackStateV2<
ValueType,
FamilyKey extends SerializableParam,
>(
componentFamilySelector: ComponentFamilySelectorV2<ValueType, FamilyKey>,
instanceIdFromProps?: string,
): (familyKey: FamilyKey) => RecoilState<ValueType>;
export function useRecoilComponentFamilyCallbackStateV2<
ValueType,
FamilyKey extends SerializableParam,
>(
componentFamilyReadOnlySelector: ComponentFamilyReadOnlySelectorV2<
ValueType,
FamilyKey
>,
instanceIdFromProps?: string,
): (familyKey: FamilyKey) => RecoilValueReadOnly<ValueType>;
export function useRecoilComponentFamilyCallbackStateV2<
ValueType,
FamilyKey extends SerializableParam,
>(
componentFamilyState: ComponentFamilyStateV2<ValueType, FamilyKey>,
instanceIdFromProps?: string,
): (familyKey: FamilyKey) => RecoilState<ValueType>;
export function useRecoilComponentFamilyCallbackStateV2<
ComponentState extends
| ComponentFamilyStateV2<ValueType, FamilyKey>
| ComponentFamilySelectorV2<ValueType, FamilyKey>
| ComponentFamilyReadOnlySelectorV2<ValueType, FamilyKey>,
ValueType,
FamilyKey extends SerializableParam = never,
>(
componentState: ComponentState,
instanceIdFromProps?: string,
):
| RecoilState<ValueType>
| RecoilValueReadOnly<ValueType>
| ((familyKey: FamilyKey) => RecoilState<ValueType>)
| ((familyKey: FamilyKey) => RecoilValueReadOnly<ValueType>) {
const componentStateKey = componentState.key;
const componentInstanceContext =
globalComponentInstanceContextMap.get(componentStateKey);
if (!componentInstanceContext) {
throw new Error(
`Instance context for key "${componentStateKey}" is not defined, check the component state declaration.`,
);
}
const instanceId = useAvailableComponentInstanceIdOrThrow(
componentInstanceContext,
instanceIdFromProps,
);
return useCallback(
(familyKey: FamilyKey) => {
switch (componentState.type) {
case 'ComponentFamilyState': {
return componentState.atomFamily({
instanceId,
familyKey,
});
}
case 'ComponentFamilySelector': {
return componentState.selectorFamily({
instanceId,
familyKey,
});
}
case 'ComponentFamilyReadOnlySelector': {
return componentState.selectorFamily({
instanceId,
familyKey,
});
}
}
},
[componentState, instanceId],
);
}

View File

@ -26,9 +26,9 @@ import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFi
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isDefined } from 'twenty-shared/utils';
export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
@ -52,7 +52,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { findManyRecords } = useLazyFindManyRecords({
const { findManyRecordsLazy } = useLazyFindManyRecords({
objectNameSingular: CoreObjectNameSingular.View,
fetchPolicy: 'network-only',
});
@ -204,14 +204,14 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
await createViewSortRecords(viewSortsToCreate, newView);
}
await findManyRecords();
await findManyRecordsLazy();
set(isPersistingViewFieldsState, false);
},
[
currentViewIdCallbackState,
createOneRecord,
createViewFieldRecords,
findManyRecords,
findManyRecordsLazy,
objectMetadataItem.fields,
createViewGroupRecords,
createViewSortRecords,

View File

@ -12,7 +12,7 @@ export const useRefreshCachedViews = () => {
),
});
const { findManyRecords: refreshCachedViews } = useLazyFindManyRecords({
const { findManyRecordsLazy: refreshCachedViews } = useLazyFindManyRecords({
objectNameSingular: CoreObjectNameSingular.View,
filter: findAllViewsOperationSignature.variables.filter,
recordGqlFields: findAllViewsOperationSignature.fields,

View File

@ -12,8 +12,11 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import {
RecordTableContextProvider,
useRecordTableContextOrThrow,
} from '@/object-record/record-table/contexts/RecordTableContext';
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
@ -31,9 +34,12 @@ const InternalTableStateLoaderEffect = ({
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { recordTableId } = useRecordTableContextOrThrow();
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const { setRecordTableData } = useRecordTable();
const setRecordTableData = useSetRecordTableData({
recordTableId,
});
const view = useMemo(() => {
return {
@ -48,7 +54,6 @@ const InternalTableStateLoaderEffect = ({
loadRecordIndexStates(view, objectMetadataItem);
setRecordTableData({
records: getCompaniesMock(),
totalCount: getCompaniesMock().length,
});
}, [loadRecordIndexStates, objectMetadataItem, setRecordTableData, view]);
@ -149,12 +154,12 @@ export const RecordTableDecorator: Decorator = (Story, context) => {
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<InternalTableStateLoaderEffect
objectMetadataItem={objectMetadataItem}
/>
<InternalTableContextProviders
objectMetadataItem={objectMetadataItem}
>
<InternalTableStateLoaderEffect
objectMetadataItem={objectMetadataItem}
/>
<Story />
</InternalTableContextProviders>
</ActionMenuComponentInstanceContext.Provider>