Sync stripe tables (#5475)

Stripe tables do not support `hasNextPage` and `totalCount`. This may be
because of stripe wrapper do not properly support `COUNT` request.
Waiting on pg_graphql answer
[here](https://github.com/supabase/pg_graphql/issues/519).

This PR:
- removes `totalCount` and `hasNextPage` form queries for remote
objects. Even if it works for postgres, this may really be inefficient
- adapt the `fetchMore` functions so it works despite `hasNextPage`
missing
- remove `totalCount` display for remotes
- fix `orderBy`

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-05-22 11:20:44 +02:00
committed by GitHub
parent 35c1f97511
commit 2e79bcc70b
22 changed files with 191 additions and 87 deletions

View File

@ -7,6 +7,7 @@ import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { isDefined } from '~/utils/isDefined';
/* /*
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are. TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
@ -61,11 +62,10 @@ export const triggerCreateRecordsOptimisticEffect = ({
rootQueryCachedObjectRecordConnection, rootQueryCachedObjectRecordConnection,
); );
const rootQueryCachedRecordTotalCount = const rootQueryCachedRecordTotalCount = readField<number | undefined>(
readField<number>( 'totalCount',
'totalCount', rootQueryCachedObjectRecordConnection,
rootQueryCachedObjectRecordConnection, );
) || 0;
const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges
? [...rootQueryCachedRecordEdges] ? [...rootQueryCachedRecordEdges]
@ -113,7 +113,9 @@ export const triggerCreateRecordsOptimisticEffect = ({
return { return {
...rootQueryCachedObjectRecordConnection, ...rootQueryCachedObjectRecordConnection,
edges: nextRootQueryCachedRecordEdges, edges: nextRootQueryCachedRecordEdges,
totalCount: rootQueryCachedRecordTotalCount + 1, totalCount: isDefined(rootQueryCachedRecordTotalCount)
? rootQueryCachedRecordTotalCount + 1
: undefined,
}; };
}, },
}, },

View File

@ -50,11 +50,10 @@ export const triggerDeleteRecordsOptimisticEffect = ({
rootQueryCachedObjectRecordConnection, rootQueryCachedObjectRecordConnection,
); );
const totalCount = const totalCount = readField<number | undefined>(
readField<number>( 'totalCount',
'totalCount', rootQueryCachedObjectRecordConnection,
rootQueryCachedObjectRecordConnection, );
) || 0;
const nextCachedEdges = const nextCachedEdges =
cachedEdges?.filter((cachedEdge) => { cachedEdges?.filter((cachedEdge) => {
@ -77,7 +76,9 @@ export const triggerDeleteRecordsOptimisticEffect = ({
return { return {
...rootQueryCachedObjectRecordConnection, ...rootQueryCachedObjectRecordConnection,
edges: nextCachedEdges, edges: nextCachedEdges,
totalCount: totalCount - recordIdsToDelete.length, totalCount: isDefined(totalCount)
? totalCount - recordIdsToDelete.length
: undefined,
}; };
}, },
}, },

View File

@ -0,0 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const hasPositionField = (objectMetadataItem: ObjectMetadataItem) =>
!objectMetadataItem.isRemote;

View File

@ -0,0 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const isAggregationEnabled = (objectMetadataItem: ObjectMetadataItem) =>
!objectMetadataItem.isRemote;

View File

@ -76,7 +76,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
return { return {
objectMetadataItem, objectMetadataItem,
records, records,
totalCount: objectRecordConnection?.totalCount || 0, totalCount: objectRecordConnection?.totalCount,
loading, loading,
error, error,
queryStateIdentifier: findDuplicateQueryStateIdentifier, queryStateIdentifier: findDuplicateQueryStateIdentifier,

View File

@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField'; import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -33,11 +34,11 @@ export const useFindDuplicateRecordsQuery = ({
cursor cursor
} }
pageInfo { pageInfo {
hasNextPage ${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''}
startCursor startCursor
endCursor endCursor
} }
totalCount ${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''}
} }
} }
`; `;

View File

@ -7,6 +7,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
@ -122,7 +123,8 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
}); });
const fetchMoreRecords = useCallback(async () => { const fetchMoreRecords = useCallback(async () => {
if (hasNextPage) { // Remote objects does not support hasNextPage. We cannot rely on it to fetch more records.
if (hasNextPage || (!isAggregationEnabled(objectMetadataItem) && !error)) {
setIsFetchingMoreObjects(true); setIsFetchingMoreObjects(true);
try { try {
@ -137,11 +139,11 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const nextEdges = const nextEdges =
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
let newEdges: RecordGqlEdge[] = []; let newEdges: RecordGqlEdge[] = previousEdges ?? [];
if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) { if (isNonEmptyArray(nextEdges)) {
newEdges = filterUniqueRecordEdgesByCursor([ newEdges = filterUniqueRecordEdgesByCursor([
...(prev?.[objectMetadataItem.namePlural]?.edges ?? []), ...newEdges,
...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ??
[]), []),
]); ]);
@ -199,21 +201,21 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
} }
}, [ }, [
hasNextPage, hasNextPage,
objectMetadataItem,
error,
setIsFetchingMoreObjects, setIsFetchingMoreObjects,
fetchMore, fetchMore,
filter, filter,
orderBy, orderBy,
lastCursor, lastCursor,
objectMetadataItem.namePlural,
objectMetadataItem.nameSingular,
onCompleted,
data, data,
onCompleted,
setLastCursor, setLastCursor,
setHasNextPage, setHasNextPage,
enqueueSnackBar, enqueueSnackBar,
]); ]);
const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0; const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount;
const records = useMemo( const records = useMemo(
() => () =>

View File

@ -1,3 +1,5 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
@ -8,12 +10,30 @@ const sortDefinition: SortDefinition = {
iconName: 'icon', iconName: 'icon',
}; };
const objectMetadataItem: ObjectMetadataItem = {
id: 'object1',
fields: [],
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
nameSingular: 'object1',
namePlural: 'object1s',
icon: 'icon',
isActive: true,
isSystem: false,
isCustom: false,
isRemote: false,
labelPlural: 'object1s',
labelSingular: 'object1',
};
describe('turnSortsIntoOrderBy', () => { describe('turnSortsIntoOrderBy', () => {
it('should sort by recordPosition if no sorts', () => { it('should sort by recordPosition if no sorts', () => {
const fields = [{ id: 'field1', name: 'createdAt' }]; const fields = [{ id: 'field1', name: 'createdAt' }] as FieldMetadataItem[];
expect(turnSortsIntoOrderBy([], fields)).toEqual({ expect(turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, [])).toEqual(
position: 'AscNullsFirst', {
}); position: 'AscNullsFirst',
},
);
}); });
it('should create OrderByField with single sort', () => { it('should create OrderByField with single sort', () => {
@ -24,8 +44,10 @@ describe('turnSortsIntoOrderBy', () => {
definition: sortDefinition, definition: sortDefinition,
}, },
]; ];
const fields = [{ id: 'field1', name: 'field1' }]; const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[];
expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({ expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts),
).toEqual({
field1: 'AscNullsFirst', field1: 'AscNullsFirst',
position: 'AscNullsFirst', position: 'AscNullsFirst',
}); });
@ -47,8 +69,10 @@ describe('turnSortsIntoOrderBy', () => {
const fields = [ const fields = [
{ id: 'field1', name: 'field1' }, { id: 'field1', name: 'field1' },
{ id: 'field2', name: 'field2' }, { id: 'field2', name: 'field2' },
]; ] as FieldMetadataItem[];
expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({ expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts),
).toEqual({
field1: 'AscNullsFirst', field1: 'AscNullsFirst',
field2: 'DescNullsLast', field2: 'DescNullsLast',
position: 'AscNullsFirst', position: 'AscNullsFirst',
@ -63,8 +87,21 @@ describe('turnSortsIntoOrderBy', () => {
definition: sortDefinition, definition: sortDefinition,
}, },
]; ];
expect(turnSortsIntoOrderBy(sorts, [])).toEqual({ expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual({
position: 'AscNullsFirst', position: 'AscNullsFirst',
}); });
}); });
it('should not return position for remotes', () => {
const sorts: Sort[] = [
{
fieldMetadataId: 'invalidField',
direction: 'asc',
definition: sortDefinition,
},
];
expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, isRemote: true }, sorts),
).toEqual({});
});
}); });

View File

@ -1,4 +1,6 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy'; import { OrderBy } from '@/object-metadata/types/OrderBy';
import { hasPositionField } from '@/object-metadata/utils/hasPositionColumn';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { Field } from '~/generated/graphql'; import { Field } from '~/generated/graphql';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
@ -8,9 +10,10 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { Sort } from '../types/Sort'; import { Sort } from '../types/Sort';
export const turnSortsIntoOrderBy = ( export const turnSortsIntoOrderBy = (
objectMetadataItem: ObjectMetadataItem,
sorts: Sort[], sorts: Sort[],
fields: Pick<Field, 'id' | 'name'>[],
): RecordGqlOperationOrderBy => { ): RecordGqlOperationOrderBy => {
const fields: Pick<Field, 'id' | 'name'>[] = objectMetadataItem?.fields ?? [];
const fieldsById = mapArrayToObject(fields, ({ id }) => id); const fieldsById = mapArrayToObject(fields, ({ id }) => id);
const sortsOrderBy = Object.fromEntries( const sortsOrderBy = Object.fromEntries(
sorts sorts
@ -29,8 +32,12 @@ export const turnSortsIntoOrderBy = (
.filter(isDefined), .filter(isDefined),
); );
return { if (hasPositionField(objectMetadataItem)) {
...sortsOrderBy, return {
position: 'AscNullsFirst', ...sortsOrderBy,
}; position: 'AscNullsFirst',
};
}
return sortsOrderBy;
}; };

View File

@ -15,7 +15,10 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord'; import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
import { useExportTableData } from '@/object-record/record-index/options/hooks/useExportTableData'; import {
displayedExportProgress,
useExportTableData,
} from '@/object-record/record-index/options/hooks/useExportTableData';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
@ -111,7 +114,7 @@ export const useRecordActionBar = ({
const baseActions: ContextMenuEntry[] = useMemo( const baseActions: ContextMenuEntry[] = useMemo(
() => [ () => [
{ {
label: `${progress === undefined ? `Export` : `Export (${progress}%)`}`, label: displayedExportProgress(progress),
Icon: IconFileExport, Icon: IconFileExport,
accent: 'default', accent: 'default',
onClick: () => download(), onClick: () => download(),

View File

@ -48,9 +48,7 @@ export const useLoadRecordIndexBoard = ({
recordIndexFilters, recordIndexFilters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );
const orderBy = !objectMetadataItem.isRemote const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
? turnSortsIntoOrderBy(recordIndexSorts, objectMetadataItem?.fields ?? [])
: undefined;
const recordIndexIsCompactModeActive = useRecoilValue( const recordIndexIsCompactModeActive = useRecoilValue(
recordIndexIsCompactModeActiveState, recordIndexIsCompactModeActiveState,

View File

@ -29,14 +29,7 @@ export const useFindManyParams = (
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );
if (objectMetadataItem?.isRemote) { const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts);
return { objectNameSingular, filter };
}
const orderBy = turnSortsIntoOrderBy(
tableSorts,
objectMetadataItem?.fields ?? [],
);
return { objectNameSingular, filter, orderBy }; return { objectNameSingular, filter, orderBy };
}; };
@ -71,7 +64,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => {
currentWorkspace?.activationStatus === 'active' currentWorkspace?.activationStatus === 'active'
? records ? records
: SIGN_IN_BACKGROUND_MOCK_COMPANIES, : SIGN_IN_BACKGROUND_MOCK_COMPANIES,
totalCount: totalCount || 0, totalCount: totalCount,
loading, loading,
fetchMoreRecords, fetchMoreRecords,
queryStateIdentifier, queryStateIdentifier,

View File

@ -9,7 +9,10 @@ import {
} from 'twenty-ui'; } from 'twenty-ui';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { useExportTableData } from '@/object-record/record-index/options/hooks/useExportTableData'; import {
displayedExportProgress,
useExportTableData,
} from '@/object-record/record-index/options/hooks/useExportTableData';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
@ -123,7 +126,7 @@ export const RecordIndexOptionsDropdownContent = ({
<MenuItem <MenuItem
onClick={download} onClick={download}
LeftIcon={IconFileExport} LeftIcon={IconFileExport}
text={progress === undefined ? `Export` : `Export (${progress}%)`} text={displayedExportProgress(progress)}
/> />
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
)} )}

View File

@ -3,9 +3,9 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin
import { import {
csvDownloader, csvDownloader,
displayedExportProgress,
download, download,
generateCsv, generateCsv,
percentage,
sleep, sleep,
} from '../useExportTableData'; } from '../useExportTableData';
@ -92,17 +92,24 @@ describe('csvDownloader', () => {
}); });
}); });
describe('percentage', () => { describe('displayedExportProgress', () => {
it.each([ it.each([
[20, 50, 40], [undefined, undefined, 'percentage', 'Export'],
[0, 100, 0], [20, 50, 'percentage', 'Export (40%)'],
[10, 10, 100], [0, 100, 'number', 'Export (0)'],
[10, 10, 100], [10, 10, 'percentage', 'Export (100%)'],
[7, 9, 78], [10, 10, 'number', 'Export (10)'],
[7, 9, 'percentage', 'Export (78%)'],
])( ])(
'calculates the percentage %p/%p = %p', 'displays the export progress',
(part, whole, expectedPercentage) => { (exportedRecordCount, totalRecordCount, displayType, expected) => {
expect(percentage(part, whole)).toEqual(expectedPercentage); expect(
displayedExportProgress({
exportedRecordCount,
totalRecordCount,
displayType: displayType as 'percentage' | 'number',
}),
).toEqual(expected);
}, },
); );
}); });

View File

@ -7,6 +7,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable';
@ -30,6 +31,12 @@ type GenerateExportOptions = {
type GenerateExport = (data: GenerateExportOptions) => string; type GenerateExport = (data: GenerateExportOptions) => string;
type ExportProgress = {
exportedRecordCount?: number;
totalRecordCount?: number;
displayType: 'percentage' | 'number';
};
export const generateCsv: GenerateExport = ({ export const generateCsv: GenerateExport = ({
columns, columns,
rows, rows,
@ -77,10 +84,28 @@ export const generateCsv: GenerateExport = ({
}); });
}; };
export const percentage = (part: number, whole: number): number => { const percentage = (part: number, whole: number): number => {
return Math.round((part / whole) * 100); return Math.round((part / whole) * 100);
}; };
export const displayedExportProgress = (progress?: ExportProgress): string => {
if (isUndefinedOrNull(progress?.exportedRecordCount)) {
return 'Export';
}
if (
progress.displayType === 'percentage' &&
isDefined(progress?.totalRecordCount)
) {
return `Export (${percentage(
progress.exportedRecordCount,
progress.totalRecordCount,
)}%)`;
}
return `Export (${progress.exportedRecordCount})`;
};
const downloader = (mimeType: string, generator: GenerateExport) => { const downloader = (mimeType: string, generator: GenerateExport) => {
return (filename: string, data: GenerateExportOptions) => { return (filename: string, data: GenerateExportOptions) => {
const blob = new Blob([generator(data)], { type: mimeType }); const blob = new Blob([generator(data)], { type: mimeType });
@ -110,8 +135,10 @@ export const useExportTableData = ({
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
const [inflight, setInflight] = useState(false); const [inflight, setInflight] = useState(false);
const [pageCount, setPageCount] = useState(0); const [pageCount, setPageCount] = useState(0);
const [progress, setProgress] = useState<number | undefined>(undefined); const [progress, setProgress] = useState<ExportProgress>({
const [hasNextPage, setHasNextPage] = useState(true); displayType: 'number',
});
const [previousRecordCount, setPreviousRecordCount] = useState(0);
const { visibleTableColumnsSelector, selectedRowIdsSelector } = const { visibleTableColumnsSelector, selectedRowIdsSelector } =
useRecordTableStates(recordIndexId); useRecordTableStates(recordIndexId);
@ -144,25 +171,31 @@ export const useExportTableData = ({
const { totalCount, records, fetchMoreRecords } = useFindManyRecords({ const { totalCount, records, fetchMoreRecords } = useFindManyRecords({
...usedFindManyParams, ...usedFindManyParams,
limit: pageSize, limit: pageSize,
onCompleted: (_data, options) => {
setHasNextPage(options?.pageInfo?.hasNextPage ?? false);
},
}); });
useEffect(() => { useEffect(() => {
const MAXIMUM_REQUESTS = Math.min(maximumRequests, totalCount / pageSize); const MAXIMUM_REQUESTS = isDefined(totalCount)
? Math.min(maximumRequests, totalCount / pageSize)
: maximumRequests;
const downloadCsv = (rows: object[]) => { const downloadCsv = (rows: object[]) => {
csvDownloader(filename, { rows, columns }); csvDownloader(filename, { rows, columns });
setIsDownloading(false); setIsDownloading(false);
setProgress(undefined); setProgress({
displayType: 'number',
});
}; };
const fetchNextPage = async () => { const fetchNextPage = async () => {
setInflight(true); setInflight(true);
setPreviousRecordCount(records.length);
await fetchMoreRecords(); await fetchMoreRecords();
setPageCount((state) => state + 1); setPageCount((state) => state + 1);
setProgress(percentage(pageCount, MAXIMUM_REQUESTS)); setProgress({
exportedRecordCount: records.length,
totalRecordCount: totalCount,
displayType: totalCount ? 'percentage' : 'number',
});
await sleep(delayMs); await sleep(delayMs);
setInflight(false); setInflight(false);
}; };
@ -171,7 +204,10 @@ export const useExportTableData = ({
return; return;
} }
if (!hasNextPage || pageCount >= MAXIMUM_REQUESTS) { if (
pageCount >= MAXIMUM_REQUESTS ||
records.length === previousRecordCount
) {
downloadCsv(records); downloadCsv(records);
} else { } else {
fetchNextPage(); fetchNextPage();
@ -180,7 +216,6 @@ export const useExportTableData = ({
delayMs, delayMs,
fetchMoreRecords, fetchMoreRecords,
filename, filename,
hasNextPage,
inflight, inflight,
isDownloading, isDownloading,
pageCount, pageCount,
@ -189,6 +224,7 @@ export const useExportTableData = ({
columns, columns,
maximumRequests, maximumRequests,
pageSize, pageSize,
previousRecordCount,
]); ]);
return { progress, download: () => setIsDownloading(true) }; return { progress, download: () => setIsDownloading(true) };

View File

@ -8,7 +8,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type useSetRecordTableDataProps = { type useSetRecordTableDataProps = {
recordTableId?: string; recordTableId?: string;
onEntityCountChange: (entityCount: number) => void; onEntityCountChange: (entityCount?: number) => void;
}; };
export const useSetRecordTableData = ({ export const useSetRecordTableData = ({
@ -24,7 +24,7 @@ export const useSetRecordTableData = ({
return useRecoilCallback( return useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
<T extends ObjectRecord>(newEntityArray: T[], totalCount: number) => { <T extends ObjectRecord>(newEntityArray: T[], totalCount?: number) => {
for (const entity of newEntityArray) { for (const entity of newEntityArray) {
// TODO: refactor with scoped state later // TODO: refactor with scoped state later
const currentEntity = snapshot const currentEntity = snapshot
@ -54,7 +54,7 @@ export const useSetRecordTableData = ({
} }
} }
set(numberOfTableRowsState, totalCount); set(numberOfTableRowsState, totalCount ?? 0);
onEntityCountChange(totalCount); onEntityCountChange(totalCount);
}, },
[ [

View File

@ -99,7 +99,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
const onEntityCountChange = useRecoilCallback( const onEntityCountChange = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
(count: number) => { (count?: number) => {
const onEntityCountChange = getSnapshotValue( const onEntityCountChange = getSnapshotValue(
snapshot, snapshot,
onEntityCountChangeState, onEntityCountChangeState,

View File

@ -83,6 +83,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => {
snapshot, snapshot,
numberOfTableRowsState, numberOfTableRowsState,
); );
const currentColumnNumber = softFocusPosition.column; const currentColumnNumber = softFocusPosition.column;
const currentRowNumber = softFocusPosition.row; const currentRowNumber = softFocusPosition.row;

View File

@ -1,7 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const onEntityCountChangeComponentState = createComponentState< export const onEntityCountChangeComponentState = createComponentState<
((entityCount: number) => void) | undefined ((entityCount?: number) => void) | undefined
>({ >({
key: 'onEntityCountChangeComponentState', key: 'onEntityCountChangeComponentState',
defaultValue: undefined, defaultValue: undefined,

View File

@ -1,6 +1,7 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -36,11 +37,11 @@ query FindMany${capitalize(
cursor cursor
} }
pageInfo { pageInfo {
hasNextPage ${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''}
startCursor startCursor
endCursor endCursor
} }
totalCount ${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''}
} }
} }
`; `;

View File

@ -1,7 +1,8 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const entityCountInCurrentViewComponentState = export const entityCountInCurrentViewComponentState = createComponentState<
createComponentState<number>({ number | undefined
key: 'entityCountInCurrentViewComponentState', >({
defaultValue: 0, key: 'entityCountInCurrentViewComponentState',
}); defaultValue: undefined,
});

View File

@ -15,6 +15,7 @@ import { ViewPickerListContent } from '@/views/view-picker/components/ViewPicker
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView'; import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView';
import { isDefined } from '~/utils/isDefined';
import { useViewStates } from '../../hooks/internal/useViewStates'; import { useViewStates } from '../../hooks/internal/useViewStates';
@ -89,7 +90,9 @@ export const ViewPickerDropdown = () => {
{currentViewWithCombinedFiltersAndSorts?.name ?? 'All'} {currentViewWithCombinedFiltersAndSorts?.name ?? 'All'}
</StyledViewName> </StyledViewName>
<StyledDropdownLabelAdornments> <StyledDropdownLabelAdornments>
· {entityCountInCurrentView}{' '} {isDefined(entityCountInCurrentView) && (
<>· {entityCountInCurrentView} </>
)}
<IconChevronDown size={theme.icon.size.sm} /> <IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments> </StyledDropdownLabelAdornments>
</StyledDropdownButtonContainer> </StyledDropdownButtonContainer>