mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
parent
f482b459a9
commit
5586270df4
@ -13,13 +13,14 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useCallback, useContext, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
import { IconTrash, isDefined } from 'twenty-ui';
|
import { IconTrash, isDefined } from 'twenty-ui';
|
||||||
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
|
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||||
|
|
||||||
export const useDeleteMultipleRecordsAction = ({
|
export const useDeleteMultipleRecordsAction = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -60,15 +61,18 @@ export const useDeleteMultipleRecordsAction = ({
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { fetchAllRecordIds } = useFetchAllRecordIds({
|
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
filter: graphqlFilter,
|
filter: graphqlFilter,
|
||||||
|
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||||
|
recordGqlFields: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { closeRightDrawer } = useRightDrawer();
|
const { closeRightDrawer } = useRightDrawer();
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(async () => {
|
const handleDeleteClick = useCallback(async () => {
|
||||||
const recordIdsToDelete = await fetchAllRecordIds();
|
const recordsToDelete = await fetchAllRecordIds();
|
||||||
|
const recordIdsToDelete = recordsToDelete.map((record) => record.id);
|
||||||
|
|
||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
|
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
|
||||||
import {
|
|
||||||
mockPageSize,
|
|
||||||
peopleMockWithIdsOnly,
|
|
||||||
query,
|
|
||||||
responseFirstRequest,
|
|
||||||
responseSecondRequest,
|
|
||||||
responseThirdRequest,
|
|
||||||
variablesFirstRequest,
|
|
||||||
variablesSecondRequest,
|
|
||||||
variablesThirdRequest,
|
|
||||||
} from '@/object-record/hooks/__mocks__/useFetchAllRecordIds';
|
|
||||||
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
|
||||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
|
||||||
|
|
||||||
const mocks = [
|
|
||||||
{
|
|
||||||
delay: 100,
|
|
||||||
request: {
|
|
||||||
query,
|
|
||||||
variables: variablesFirstRequest,
|
|
||||||
},
|
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: responseFirstRequest,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
delay: 100,
|
|
||||||
request: {
|
|
||||||
query,
|
|
||||||
variables: variablesSecondRequest,
|
|
||||||
},
|
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: responseSecondRequest,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
delay: 100,
|
|
||||||
request: {
|
|
||||||
query,
|
|
||||||
variables: variablesThirdRequest,
|
|
||||||
},
|
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: responseThirdRequest,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
|
||||||
apolloMocks: mocks,
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useFetchAllRecordIds', () => {
|
|
||||||
it('fetches all record ids with fetch more synchronous loop', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => {
|
|
||||||
const [, setObjectMetadataItems] = useRecoilState(
|
|
||||||
objectMetadataItemsState,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setObjectMetadataItems(generatedMockObjectMetadataItems);
|
|
||||||
}, [setObjectMetadataItems]);
|
|
||||||
|
|
||||||
return useFetchAllRecordIds({
|
|
||||||
objectNameSingular: 'person',
|
|
||||||
pageSize: mockPageSize,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { fetchAllRecordIds } = result.current;
|
|
||||||
|
|
||||||
let recordIds: string[] = [];
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
recordIds = await fetchAllRecordIds();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
|
||||||
expect(mocks[1].result).toHaveBeenCalled();
|
|
||||||
expect(mocks[2].result).toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(recordIds).toEqual(
|
|
||||||
peopleMockWithIdsOnly.edges.map((edge) => edge.node.id).slice(0, 6),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -0,0 +1,173 @@
|
|||||||
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { expect } from '@storybook/test';
|
||||||
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
|
import { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
||||||
|
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
||||||
|
|
||||||
|
const defaultResponseData = {
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: '',
|
||||||
|
endCursor: '',
|
||||||
|
},
|
||||||
|
totalCount: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPerson = {
|
||||||
|
__typename: 'Person',
|
||||||
|
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||||
|
whatsapp: {
|
||||||
|
primaryPhoneNumber: '+1',
|
||||||
|
primaryPhoneCountryCode: '234-567-890',
|
||||||
|
additionalPhones: [],
|
||||||
|
},
|
||||||
|
linkedinLink: {
|
||||||
|
primaryLinkUrl: 'https://www.linkedin.com',
|
||||||
|
primaryLinkLabel: 'linkedin',
|
||||||
|
secondaryLinks: ['https://www.linkedin.com'],
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
firstName: 'firstName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
primaryEmail: 'email',
|
||||||
|
additionalEmails: [],
|
||||||
|
},
|
||||||
|
position: 'position',
|
||||||
|
createdBy: {
|
||||||
|
source: 'source',
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
avatarUrl: 'avatarUrl',
|
||||||
|
jobTitle: 'jobTitle',
|
||||||
|
xLink: {
|
||||||
|
primaryLinkUrl: 'https://www.linkedin.com',
|
||||||
|
primaryLinkLabel: 'linkedin',
|
||||||
|
secondaryLinks: ['https://www.linkedin.com'],
|
||||||
|
},
|
||||||
|
performanceRating: 1,
|
||||||
|
createdAt: '2021-08-03T19:20:06.000Z',
|
||||||
|
phones: {
|
||||||
|
primaryPhoneNumber: '+1',
|
||||||
|
primaryPhoneCountryCode: '234-567-890',
|
||||||
|
additionalPhones: [],
|
||||||
|
},
|
||||||
|
id: '123',
|
||||||
|
city: 'city',
|
||||||
|
companyId: '1',
|
||||||
|
intro: 'intro',
|
||||||
|
deletedAt: null,
|
||||||
|
workPreference: 'workPreference',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mock: MockedResponse = {
|
||||||
|
request: {
|
||||||
|
query: gql`
|
||||||
|
query FindManyPeople(
|
||||||
|
$filter: PersonFilterInput
|
||||||
|
$orderBy: [PersonOrderByInput]
|
||||||
|
$lastCursor: String
|
||||||
|
$limit: Int
|
||||||
|
) {
|
||||||
|
people(
|
||||||
|
filter: $filter
|
||||||
|
orderBy: $orderBy
|
||||||
|
first: $limit
|
||||||
|
after: $lastCursor
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
limit: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: jest.fn(() => ({
|
||||||
|
data: {
|
||||||
|
people: {
|
||||||
|
...defaultResponseData,
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: mockPerson,
|
||||||
|
cursor: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: mockPerson,
|
||||||
|
cursor: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||||
|
apolloMocks: [mock],
|
||||||
|
componentInstanceId: 'recordIndexId',
|
||||||
|
contextStoreTargetedRecordsRule: {
|
||||||
|
mode: 'selection',
|
||||||
|
selectedRecordIds: [],
|
||||||
|
},
|
||||||
|
contextStoreCurrentObjectMetadataNameSingular: 'person',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useLazyFetchAllRecords', () => {
|
||||||
|
const objectNameSingular = 'person';
|
||||||
|
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === objectNameSingular,
|
||||||
|
);
|
||||||
|
if (!objectMetadataItem) {
|
||||||
|
throw new Error('Object metadata item not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle one single page', async () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() =>
|
||||||
|
useLazyFetchAllRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
limit: 30,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let res: any;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
res = result.current.fetchAllRecords();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isDownloading).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isDownloading).toBe(false);
|
||||||
|
expect(result.current.progress).toEqual({ displayType: 'number' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.progress).toEqual({ displayType: 'number' });
|
||||||
|
|
||||||
|
const finalResult = await res;
|
||||||
|
|
||||||
|
expect(finalResult).toEqual([mockPerson, mockPerson]);
|
||||||
|
});
|
||||||
|
});
|
@ -1,88 +0,0 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
|
||||||
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
|
||||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
type UseLazyFetchAllRecordIdsParams<T> = Omit<
|
|
||||||
UseFindManyRecordsParams<T>,
|
|
||||||
'skip'
|
|
||||||
> & { pageSize?: number };
|
|
||||||
|
|
||||||
export const useFetchAllRecordIds = <T>({
|
|
||||||
objectNameSingular,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
pageSize = DEFAULT_QUERY_PAGE_SIZE,
|
|
||||||
}: UseLazyFetchAllRecordIdsParams<T>) => {
|
|
||||||
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
|
|
||||||
objectNameSingular,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit: pageSize,
|
|
||||||
recordGqlFields: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAllRecordIds = useCallback(async () => {
|
|
||||||
if (!isDefined(findManyRecords)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const findManyRecordsDataResult = await findManyRecords();
|
|
||||||
|
|
||||||
const firstQueryResult =
|
|
||||||
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
|
||||||
|
|
||||||
const totalCount = firstQueryResult?.totalCount ?? 0;
|
|
||||||
|
|
||||||
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
|
||||||
|
|
||||||
const recordIdSet = new Set(
|
|
||||||
firstQueryResult?.edges?.map((edge) => edge.node.id) ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const remainingCount = totalCount - recordsCount;
|
|
||||||
|
|
||||||
const remainingPages = Math.ceil(remainingCount / pageSize);
|
|
||||||
|
|
||||||
let lastCursor = firstQueryResult?.pageInfo.endCursor ?? null;
|
|
||||||
|
|
||||||
for (let pageIndex = 0; pageIndex < remainingPages; pageIndex++) {
|
|
||||||
if (lastCursor === null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawResult = await fetchMore?.({
|
|
||||||
variables: {
|
|
||||||
lastCursor: lastCursor,
|
|
||||||
limit: pageSize,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
|
|
||||||
|
|
||||||
for (const edge of fetchMoreResult.edges) {
|
|
||||||
recordIdSet.add(edge.node.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fetchMoreResult.pageInfo.hasNextPage === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCursor = fetchMoreResult.pageInfo.endCursor ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordIds = Array.from(recordIdSet);
|
|
||||||
|
|
||||||
return recordIds;
|
|
||||||
}, [fetchMore, findManyRecords, objectMetadataItem.namePlural, pageSize]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
fetchAllRecordIds,
|
|
||||||
};
|
|
||||||
};
|
|
@ -0,0 +1,141 @@
|
|||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||||
|
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||||
|
|
||||||
|
type UseLazyFetchAllRecordIdsParams<T> = Omit<
|
||||||
|
UseFindManyRecordsParams<T>,
|
||||||
|
'skip'
|
||||||
|
> & {
|
||||||
|
pageSize?: number;
|
||||||
|
delayMs?: number;
|
||||||
|
maximumRequests?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExportProgress = {
|
||||||
|
exportedRecordCount?: number;
|
||||||
|
totalRecordCount?: number;
|
||||||
|
displayType: 'percentage' | 'number';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLazyFetchAllRecords = <T>({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit = DEFAULT_QUERY_PAGE_SIZE,
|
||||||
|
delayMs = 0,
|
||||||
|
maximumRequests = 100,
|
||||||
|
recordGqlFields,
|
||||||
|
}: UseLazyFetchAllRecordIdsParams<T>) => {
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [progress, setProgress] = useState<ExportProgress>({
|
||||||
|
displayType: 'number',
|
||||||
|
});
|
||||||
|
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAllRecords = useCallback(async () => {
|
||||||
|
if (!isDefined(findManyRecords)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
setIsDownloading(true);
|
||||||
|
|
||||||
|
const findManyRecordsDataResult = await findManyRecords();
|
||||||
|
|
||||||
|
const firstQueryResult =
|
||||||
|
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
||||||
|
|
||||||
|
const totalCount = firstQueryResult?.totalCount ?? 0;
|
||||||
|
|
||||||
|
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
||||||
|
|
||||||
|
const records = firstQueryResult?.edges?.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
setProgress({
|
||||||
|
exportedRecordCount: recordsCount,
|
||||||
|
totalRecordCount: totalCount,
|
||||||
|
displayType: totalCount ? 'percentage' : 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainingCount = totalCount - recordsCount;
|
||||||
|
|
||||||
|
const remainingPages = Math.ceil(remainingCount / limit);
|
||||||
|
|
||||||
|
let lastCursor = firstQueryResult?.pageInfo.endCursor ?? null;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let pageIndex = 0;
|
||||||
|
pageIndex < Math.min(maximumRequests, remainingPages);
|
||||||
|
pageIndex++
|
||||||
|
) {
|
||||||
|
if (lastCursor === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(fetchMore)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayMs > 0) {
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawResult = await fetchMore({
|
||||||
|
variables: {
|
||||||
|
lastCursor: lastCursor,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
|
||||||
|
|
||||||
|
for (const edge of fetchMoreResult.edges) {
|
||||||
|
records.push(edge.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress({
|
||||||
|
exportedRecordCount: records.length,
|
||||||
|
totalRecordCount: totalCount,
|
||||||
|
displayType: totalCount ? 'percentage' : 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fetchMoreResult.pageInfo.hasNextPage === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCursor = fetchMoreResult.pageInfo.endCursor ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDownloading(false);
|
||||||
|
setProgress({
|
||||||
|
displayType: 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
return records;
|
||||||
|
}, [
|
||||||
|
delayMs,
|
||||||
|
fetchMore,
|
||||||
|
findManyRecords,
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
limit,
|
||||||
|
maximumRequests,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
progress,
|
||||||
|
isDownloading,
|
||||||
|
fetchAllRecords,
|
||||||
|
};
|
||||||
|
};
|
@ -6,26 +6,14 @@ import {
|
|||||||
useExportFetchRecords,
|
useExportFetchRecords,
|
||||||
} from '../useExportFetchRecords';
|
} from '../useExportFetchRecords';
|
||||||
|
|
||||||
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
|
||||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { expect } from '@storybook/test';
|
import { expect } from '@storybook/test';
|
||||||
import gql from 'graphql-tag';
|
|
||||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
const defaultResponseData = {
|
|
||||||
pageInfo: {
|
|
||||||
hasNextPage: false,
|
|
||||||
hasPreviousPage: false,
|
|
||||||
startCursor: '',
|
|
||||||
endCursor: '',
|
|
||||||
},
|
|
||||||
totalCount: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPerson = {
|
const mockPerson = {
|
||||||
__typename: 'Person',
|
__typename: 'Person',
|
||||||
@ -76,62 +64,8 @@ const mockPerson = {
|
|||||||
workPreference: 'workPreference',
|
workPreference: 'workPreference',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mocks: MockedResponse[] = [
|
const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||||
{
|
apolloMocks: [],
|
||||||
request: {
|
|
||||||
query: gql`
|
|
||||||
query FindManyPeople(
|
|
||||||
$filter: PersonFilterInput
|
|
||||||
$orderBy: [PersonOrderByInput]
|
|
||||||
$lastCursor: String
|
|
||||||
$limit: Int
|
|
||||||
) {
|
|
||||||
people(
|
|
||||||
filter: $filter
|
|
||||||
orderBy: $orderBy
|
|
||||||
first: $limit
|
|
||||||
after: $lastCursor
|
|
||||||
) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
hasPreviousPage
|
|
||||||
startCursor
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
|
||||||
filter: {},
|
|
||||||
limit: 30,
|
|
||||||
orderBy: [{ position: 'AscNullsFirst' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: {
|
|
||||||
people: {
|
|
||||||
...defaultResponseData,
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: mockPerson,
|
|
||||||
cursor: '1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const WrapperWithResponse = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
|
||||||
apolloMocks: mocks,
|
|
||||||
componentInstanceId: 'recordIndexId',
|
componentInstanceId: 'recordIndexId',
|
||||||
contextStoreTargetedRecordsRule: {
|
contextStoreTargetedRecordsRule: {
|
||||||
mode: 'selection',
|
mode: 'selection',
|
||||||
@ -140,43 +74,36 @@ const WrapperWithResponse = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
|||||||
contextStoreCurrentObjectMetadataNameSingular: 'person',
|
contextStoreCurrentObjectMetadataNameSingular: 'person',
|
||||||
});
|
});
|
||||||
|
|
||||||
const graphqlEmptyResponse = [
|
jest.mock('@/object-record/hooks/useLazyFetchAllRecords', () => ({
|
||||||
{
|
useLazyFetchAllRecords: jest.fn(),
|
||||||
...mocks[0],
|
}));
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: {
|
|
||||||
people: {
|
|
||||||
...defaultResponseData,
|
|
||||||
edges: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const WrapperWithEmptyResponse =
|
|
||||||
getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
|
||||||
apolloMocks: graphqlEmptyResponse,
|
|
||||||
componentInstanceId: 'recordIndexId',
|
|
||||||
contextStoreTargetedRecordsRule: {
|
|
||||||
mode: 'selection',
|
|
||||||
selectedRecordIds: [],
|
|
||||||
},
|
|
||||||
contextStoreCurrentObjectMetadataNameSingular: 'person',
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useRecordData', () => {
|
describe('useRecordData', () => {
|
||||||
const recordIndexId = 'people';
|
const recordIndexId = 'people';
|
||||||
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||||
(item) => item.nameSingular === 'person',
|
(item) => item.nameSingular === 'person',
|
||||||
);
|
);
|
||||||
|
let mockFetchAllRecords: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the hook's implementation
|
||||||
|
mockFetchAllRecords = jest.fn();
|
||||||
|
(useLazyFetchAllRecords as jest.Mock).mockReturnValue({
|
||||||
|
progress: 100,
|
||||||
|
isDownloading: false,
|
||||||
|
fetchAllRecords: mockFetchAllRecords, // Mock the function
|
||||||
|
});
|
||||||
|
});
|
||||||
if (!objectMetadataItem) {
|
if (!objectMetadataItem) {
|
||||||
throw new Error('Object metadata item not found');
|
throw new Error('Object metadata item not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('data fetching', () => {
|
describe('data fetching', () => {
|
||||||
it('should handle no records', async () => {
|
it('should handle no records', async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
mockFetchAllRecords.mockReturnValue([]);
|
||||||
|
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useExportFetchRecords({
|
useExportFetchRecords({
|
||||||
@ -188,7 +115,7 @@ describe('useRecordData', () => {
|
|||||||
viewType: ViewType.Kanban,
|
viewType: ViewType.Kanban,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
wrapper: WrapperWithEmptyResponse,
|
wrapper: Wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -203,6 +130,7 @@ describe('useRecordData', () => {
|
|||||||
|
|
||||||
it('should call the callback function with fetched data', async () => {
|
it('should call the callback function with fetched data', async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
mockFetchAllRecords.mockReturnValue([mockPerson]);
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useExportFetchRecords({
|
useExportFetchRecords({
|
||||||
@ -212,7 +140,7 @@ describe('useRecordData', () => {
|
|||||||
pageSize: 30,
|
pageSize: 30,
|
||||||
delayMs: 0,
|
delayMs: 0,
|
||||||
}),
|
}),
|
||||||
{ wrapper: WrapperWithResponse },
|
{ wrapper: Wrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -226,6 +154,7 @@ describe('useRecordData', () => {
|
|||||||
|
|
||||||
it('should call the callback function with kanban field included as column if view type is kanban', async () => {
|
it('should call the callback function with kanban field included as column if view type is kanban', async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
mockFetchAllRecords.mockReturnValue([mockPerson]);
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] =
|
const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] =
|
||||||
@ -254,7 +183,7 @@ describe('useRecordData', () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
wrapper: WrapperWithResponse,
|
wrapper: Wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -316,6 +245,7 @@ describe('useRecordData', () => {
|
|||||||
|
|
||||||
it('should not call the callback function with kanban field included as column if view type is table', async () => {
|
it('should not call the callback function with kanban field included as column if view type is table', async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
mockFetchAllRecords.mockReturnValue([mockPerson]);
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] =
|
const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] =
|
||||||
@ -345,7 +275,7 @@ describe('useRecordData', () => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
wrapper: WrapperWithResponse,
|
wrapper: Wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
|
||||||
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
|
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
|
||||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||||
@ -17,6 +13,7 @@ import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/
|
|||||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
|
|
||||||
export const sleep = (ms: number) =>
|
export const sleep = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@ -38,12 +35,6 @@ export type UseRecordDataOptions = {
|
|||||||
viewType?: ViewType;
|
viewType?: ViewType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExportProgress = {
|
|
||||||
exportedRecordCount?: number;
|
|
||||||
totalRecordCount?: number;
|
|
||||||
displayType: 'percentage' | 'number';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useExportFetchRecords = ({
|
export const useExportFetchRecords = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
delayMs,
|
delayMs,
|
||||||
@ -53,14 +44,6 @@ export const useExportFetchRecords = ({
|
|||||||
callback,
|
callback,
|
||||||
viewType = ViewType.Table,
|
viewType = ViewType.Table,
|
||||||
}: UseRecordDataOptions) => {
|
}: UseRecordDataOptions) => {
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
const [inflight, setInflight] = useState(false);
|
|
||||||
const [pageCount, setPageCount] = useState(0);
|
|
||||||
const [progress, setProgress] = useState<ExportProgress>({
|
|
||||||
displayType: 'number',
|
|
||||||
});
|
|
||||||
const [previousRecordCount, setPreviousRecordCount] = useState(0);
|
|
||||||
|
|
||||||
const { hiddenBoardFields } = useObjectOptionsForBoard({
|
const { hiddenBoardFields } = useObjectOptionsForBoard({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
recordBoardId: recordIndexId,
|
recordBoardId: recordIndexId,
|
||||||
@ -99,49 +82,6 @@ export const useExportFetchRecords = ({
|
|||||||
recordIndexId,
|
recordIndexId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { findManyRecords, totalCount, records, fetchMoreRecords, loading } =
|
|
||||||
useLazyFindManyRecords({
|
|
||||||
...findManyRecordsParams,
|
|
||||||
filter: queryFilter,
|
|
||||||
limit: pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchNextPage = async () => {
|
|
||||||
setInflight(true);
|
|
||||||
setPreviousRecordCount(records.length);
|
|
||||||
|
|
||||||
await fetchMoreRecords();
|
|
||||||
|
|
||||||
setPageCount((state) => state + 1);
|
|
||||||
setProgress({
|
|
||||||
exportedRecordCount: records.length,
|
|
||||||
totalRecordCount: totalCount,
|
|
||||||
displayType: totalCount ? 'percentage' : 'number',
|
|
||||||
});
|
|
||||||
await sleep(delayMs);
|
|
||||||
setInflight(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isDownloading || inflight || loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
pageCount >= maximumRequests ||
|
|
||||||
(isDefined(totalCount) && records.length >= totalCount)
|
|
||||||
) {
|
|
||||||
setPageCount(0);
|
|
||||||
|
|
||||||
const complete = () => {
|
|
||||||
setPageCount(0);
|
|
||||||
setPreviousRecordCount(0);
|
|
||||||
setIsDownloading(false);
|
|
||||||
setProgress({
|
|
||||||
displayType: 'number',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalColumns = [
|
const finalColumns = [
|
||||||
...columns,
|
...columns,
|
||||||
...(hiddenKanbanFieldColumn && viewType === ViewType.Kanban
|
...(hiddenKanbanFieldColumn && viewType === ViewType.Kanban
|
||||||
@ -149,42 +89,24 @@ export const useExportFetchRecords = ({
|
|||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const res = callback(records, finalColumns);
|
const { progress, isDownloading, fetchAllRecords } = useLazyFetchAllRecords({
|
||||||
|
...findManyRecordsParams,
|
||||||
if (res instanceof Promise) {
|
filter: queryFilter,
|
||||||
res.then(complete);
|
limit: pageSize,
|
||||||
} else {
|
|
||||||
complete();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchNextPage();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
delayMs,
|
delayMs,
|
||||||
fetchMoreRecords,
|
|
||||||
inflight,
|
|
||||||
isDownloading,
|
|
||||||
pageCount,
|
|
||||||
records,
|
|
||||||
totalCount,
|
|
||||||
columns,
|
|
||||||
maximumRequests,
|
maximumRequests,
|
||||||
pageSize,
|
});
|
||||||
loading,
|
|
||||||
callback,
|
const getTableData = async () => {
|
||||||
previousRecordCount,
|
const result = await fetchAllRecords();
|
||||||
hiddenKanbanFieldColumn,
|
if (result.length > 0) {
|
||||||
viewType,
|
callback(result, finalColumns);
|
||||||
]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
progress,
|
progress,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
getTableData: () => {
|
getTableData: getTableData,
|
||||||
setPageCount(0);
|
|
||||||
setPreviousRecordCount(0);
|
|
||||||
setIsDownloading(true);
|
|
||||||
findManyRecords?.();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user