diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index 4d9a7f69c..5df2b2acb 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -7,6 +7,7 @@ import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs'; 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. @@ -61,11 +62,10 @@ export const triggerCreateRecordsOptimisticEffect = ({ rootQueryCachedObjectRecordConnection, ); - const rootQueryCachedRecordTotalCount = - readField( - 'totalCount', - rootQueryCachedObjectRecordConnection, - ) || 0; + const rootQueryCachedRecordTotalCount = readField( + 'totalCount', + rootQueryCachedObjectRecordConnection, + ); const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges ? [...rootQueryCachedRecordEdges] @@ -113,7 +113,9 @@ export const triggerCreateRecordsOptimisticEffect = ({ return { ...rootQueryCachedObjectRecordConnection, edges: nextRootQueryCachedRecordEdges, - totalCount: rootQueryCachedRecordTotalCount + 1, + totalCount: isDefined(rootQueryCachedRecordTotalCount) + ? rootQueryCachedRecordTotalCount + 1 + : undefined, }; }, }, diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts index e7b121954..1a3a1aa89 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts @@ -50,11 +50,10 @@ export const triggerDeleteRecordsOptimisticEffect = ({ rootQueryCachedObjectRecordConnection, ); - const totalCount = - readField( - 'totalCount', - rootQueryCachedObjectRecordConnection, - ) || 0; + const totalCount = readField( + 'totalCount', + rootQueryCachedObjectRecordConnection, + ); const nextCachedEdges = cachedEdges?.filter((cachedEdge) => { @@ -77,7 +76,9 @@ export const triggerDeleteRecordsOptimisticEffect = ({ return { ...rootQueryCachedObjectRecordConnection, edges: nextCachedEdges, - totalCount: totalCount - recordIdsToDelete.length, + totalCount: isDefined(totalCount) + ? totalCount - recordIdsToDelete.length + : undefined, }; }, }, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/hasPositionColumn.ts b/packages/twenty-front/src/modules/object-metadata/utils/hasPositionColumn.ts new file mode 100644 index 000000000..d681ee2a3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/hasPositionColumn.ts @@ -0,0 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const hasPositionField = (objectMetadataItem: ObjectMetadataItem) => + !objectMetadataItem.isRemote; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isAggregationEnabled.ts b/packages/twenty-front/src/modules/object-metadata/utils/isAggregationEnabled.ts new file mode 100644 index 000000000..446d08bbd --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/isAggregationEnabled.ts @@ -0,0 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const isAggregationEnabled = (objectMetadataItem: ObjectMetadataItem) => + !objectMetadataItem.isRemote; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts index 54e19160f..9677fed11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts @@ -76,7 +76,7 @@ export const useFindDuplicateRecords = ({ return { objectMetadataItem, records, - totalCount: objectRecordConnection?.totalCount || 0, + totalCount: objectRecordConnection?.totalCount, loading, error, queryStateIdentifier: findDuplicateQueryStateIdentifier, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts index bab9f4fd1..9968d2d46 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField'; import { capitalize } from '~/utils/string/capitalize'; @@ -33,11 +34,11 @@ export const useFindDuplicateRecordsQuery = ({ cursor } pageInfo { - hasNextPage + ${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''} startCursor endCursor } - totalCount + ${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''} } } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index 782761c6c..fe105deaa 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -7,6 +7,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; @@ -122,7 +123,8 @@ export const useFindManyRecords = ({ }); 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); try { @@ -137,11 +139,11 @@ export const useFindManyRecords = ({ const nextEdges = fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; - let newEdges: RecordGqlEdge[] = []; + let newEdges: RecordGqlEdge[] = previousEdges ?? []; - if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) { + if (isNonEmptyArray(nextEdges)) { newEdges = filterUniqueRecordEdgesByCursor([ - ...(prev?.[objectMetadataItem.namePlural]?.edges ?? []), + ...newEdges, ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? []), ]); @@ -199,21 +201,21 @@ export const useFindManyRecords = ({ } }, [ hasNextPage, + objectMetadataItem, + error, setIsFetchingMoreObjects, fetchMore, filter, orderBy, lastCursor, - objectMetadataItem.namePlural, - objectMetadataItem.nameSingular, - onCompleted, data, + onCompleted, setLastCursor, setHasNextPage, enqueueSnackBar, ]); - const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0; + const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount; const records = useMemo( () => diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx index 5a9dc2de6..0abde78e5 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx @@ -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 { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; @@ -8,12 +10,30 @@ const sortDefinition: SortDefinition = { 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', () => { it('should sort by recordPosition if no sorts', () => { - const fields = [{ id: 'field1', name: 'createdAt' }]; - expect(turnSortsIntoOrderBy([], fields)).toEqual({ - position: 'AscNullsFirst', - }); + const fields = [{ id: 'field1', name: 'createdAt' }] as FieldMetadataItem[]; + expect(turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, [])).toEqual( + { + position: 'AscNullsFirst', + }, + ); }); it('should create OrderByField with single sort', () => { @@ -24,8 +44,10 @@ describe('turnSortsIntoOrderBy', () => { definition: sortDefinition, }, ]; - const fields = [{ id: 'field1', name: 'field1' }]; - expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({ + const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[]; + expect( + turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), + ).toEqual({ field1: 'AscNullsFirst', position: 'AscNullsFirst', }); @@ -47,8 +69,10 @@ describe('turnSortsIntoOrderBy', () => { const fields = [ { id: 'field1', name: 'field1' }, { id: 'field2', name: 'field2' }, - ]; - expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({ + ] as FieldMetadataItem[]; + expect( + turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), + ).toEqual({ field1: 'AscNullsFirst', field2: 'DescNullsLast', position: 'AscNullsFirst', @@ -63,8 +87,21 @@ describe('turnSortsIntoOrderBy', () => { definition: sortDefinition, }, ]; - expect(turnSortsIntoOrderBy(sorts, [])).toEqual({ + expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual({ 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({}); + }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts index 944127217..fa6767d1e 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts @@ -1,4 +1,6 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { OrderBy } from '@/object-metadata/types/OrderBy'; +import { hasPositionField } from '@/object-metadata/utils/hasPositionColumn'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; import { Field } from '~/generated/graphql'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; @@ -8,9 +10,10 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { Sort } from '../types/Sort'; export const turnSortsIntoOrderBy = ( + objectMetadataItem: ObjectMetadataItem, sorts: Sort[], - fields: Pick[], ): RecordGqlOperationOrderBy => { + const fields: Pick[] = objectMetadataItem?.fields ?? []; const fieldsById = mapArrayToObject(fields, ({ id }) => id); const sortsOrderBy = Object.fromEntries( sorts @@ -29,8 +32,12 @@ export const turnSortsIntoOrderBy = ( .filter(isDefined), ); - return { - ...sortsOrderBy, - position: 'AscNullsFirst', - }; + if (hasPositionField(objectMetadataItem)) { + return { + ...sortsOrderBy, + position: 'AscNullsFirst', + }; + } + + return sortsOrderBy; }; diff --git a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx index f8370cfb2..2e13dd4b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx @@ -15,7 +15,10 @@ import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; 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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; @@ -111,7 +114,7 @@ export const useRecordActionBar = ({ const baseActions: ContextMenuEntry[] = useMemo( () => [ { - label: `${progress === undefined ? `Export` : `Export (${progress}%)`}`, + label: displayedExportProgress(progress), Icon: IconFileExport, accent: 'default', onClick: () => download(), diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index e1171a253..9cd621eff 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -48,9 +48,7 @@ export const useLoadRecordIndexBoard = ({ recordIndexFilters, objectMetadataItem?.fields ?? [], ); - const orderBy = !objectMetadataItem.isRemote - ? turnSortsIntoOrderBy(recordIndexSorts, objectMetadataItem?.fields ?? []) - : undefined; + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts); const recordIndexIsCompactModeActive = useRecoilValue( recordIndexIsCompactModeActiveState, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index e63f7e6d8..9eb0baaa3 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -29,14 +29,7 @@ export const useFindManyParams = ( objectMetadataItem?.fields ?? [], ); - if (objectMetadataItem?.isRemote) { - return { objectNameSingular, filter }; - } - - const orderBy = turnSortsIntoOrderBy( - tableSorts, - objectMetadataItem?.fields ?? [], - ); + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts); return { objectNameSingular, filter, orderBy }; }; @@ -71,7 +64,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { currentWorkspace?.activationStatus === 'active' ? records : SIGN_IN_BACKGROUND_MOCK_COMPANIES, - totalCount: totalCount || 0, + totalCount: totalCount, loading, fetchMoreRecords, queryStateIdentifier, diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 890c609c6..7ed6b1650 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -9,7 +9,10 @@ import { } from 'twenty-ui'; 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 { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; @@ -123,7 +126,7 @@ export const RecordIndexOptionsDropdownContent = ({ )} diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts index c51286e6a..1b7dbce5d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts @@ -3,9 +3,9 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin import { csvDownloader, + displayedExportProgress, download, generateCsv, - percentage, sleep, } from '../useExportTableData'; @@ -92,17 +92,24 @@ describe('csvDownloader', () => { }); }); -describe('percentage', () => { +describe('displayedExportProgress', () => { it.each([ - [20, 50, 40], - [0, 100, 0], - [10, 10, 100], - [10, 10, 100], - [7, 9, 78], + [undefined, undefined, 'percentage', 'Export'], + [20, 50, 'percentage', 'Export (40%)'], + [0, 100, 'number', 'Export (0)'], + [10, 10, 'percentage', 'Export (100%)'], + [10, 10, 'number', 'Export (10)'], + [7, 9, 'percentage', 'Export (78%)'], ])( - 'calculates the percentage %p/%p = %p', - (part, whole, expectedPercentage) => { - expect(percentage(part, whole)).toEqual(expectedPercentage); + 'displays the export progress', + (exportedRecordCount, totalRecordCount, displayType, expected) => { + expect( + displayedExportProgress({ + exportedRecordCount, + totalRecordCount, + displayType: displayType as 'percentage' | 'number', + }), + ).toEqual(expected); }, ); }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts index b0113d2ba..4316686f6 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts @@ -7,6 +7,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; @@ -30,6 +31,12 @@ type GenerateExportOptions = { type GenerateExport = (data: GenerateExportOptions) => string; +type ExportProgress = { + exportedRecordCount?: number; + totalRecordCount?: number; + displayType: 'percentage' | 'number'; +}; + export const generateCsv: GenerateExport = ({ columns, 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); }; +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) => { return (filename: string, data: GenerateExportOptions) => { const blob = new Blob([generator(data)], { type: mimeType }); @@ -110,8 +135,10 @@ export const useExportTableData = ({ const [isDownloading, setIsDownloading] = useState(false); const [inflight, setInflight] = useState(false); const [pageCount, setPageCount] = useState(0); - const [progress, setProgress] = useState(undefined); - const [hasNextPage, setHasNextPage] = useState(true); + const [progress, setProgress] = useState({ + displayType: 'number', + }); + const [previousRecordCount, setPreviousRecordCount] = useState(0); const { visibleTableColumnsSelector, selectedRowIdsSelector } = useRecordTableStates(recordIndexId); @@ -144,25 +171,31 @@ export const useExportTableData = ({ const { totalCount, records, fetchMoreRecords } = useFindManyRecords({ ...usedFindManyParams, limit: pageSize, - onCompleted: (_data, options) => { - setHasNextPage(options?.pageInfo?.hasNextPage ?? false); - }, }); useEffect(() => { - const MAXIMUM_REQUESTS = Math.min(maximumRequests, totalCount / pageSize); + const MAXIMUM_REQUESTS = isDefined(totalCount) + ? Math.min(maximumRequests, totalCount / pageSize) + : maximumRequests; const downloadCsv = (rows: object[]) => { csvDownloader(filename, { rows, columns }); setIsDownloading(false); - setProgress(undefined); + setProgress({ + displayType: 'number', + }); }; const fetchNextPage = async () => { setInflight(true); + setPreviousRecordCount(records.length); await fetchMoreRecords(); setPageCount((state) => state + 1); - setProgress(percentage(pageCount, MAXIMUM_REQUESTS)); + setProgress({ + exportedRecordCount: records.length, + totalRecordCount: totalCount, + displayType: totalCount ? 'percentage' : 'number', + }); await sleep(delayMs); setInflight(false); }; @@ -171,7 +204,10 @@ export const useExportTableData = ({ return; } - if (!hasNextPage || pageCount >= MAXIMUM_REQUESTS) { + if ( + pageCount >= MAXIMUM_REQUESTS || + records.length === previousRecordCount + ) { downloadCsv(records); } else { fetchNextPage(); @@ -180,7 +216,6 @@ export const useExportTableData = ({ delayMs, fetchMoreRecords, filename, - hasNextPage, inflight, isDownloading, pageCount, @@ -189,6 +224,7 @@ export const useExportTableData = ({ columns, maximumRequests, pageSize, + previousRecordCount, ]); return { progress, download: () => setIsDownloading(true) }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 98f0d4398..64ca715b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -8,7 +8,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type useSetRecordTableDataProps = { recordTableId?: string; - onEntityCountChange: (entityCount: number) => void; + onEntityCountChange: (entityCount?: number) => void; }; export const useSetRecordTableData = ({ @@ -24,7 +24,7 @@ export const useSetRecordTableData = ({ return useRecoilCallback( ({ set, snapshot }) => - (newEntityArray: T[], totalCount: number) => { + (newEntityArray: T[], totalCount?: number) => { for (const entity of newEntityArray) { // TODO: refactor with scoped state later const currentEntity = snapshot @@ -54,7 +54,7 @@ export const useSetRecordTableData = ({ } } - set(numberOfTableRowsState, totalCount); + set(numberOfTableRowsState, totalCount ?? 0); onEntityCountChange(totalCount); }, [ diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 4ff7c6552..785baebe1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -99,7 +99,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { const onEntityCountChange = useRecoilCallback( ({ snapshot }) => - (count: number) => { + (count?: number) => { const onEntityCountChange = getSnapshotValue( snapshot, onEntityCountChangeState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts index c5dffa386..37eb8d27b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts @@ -83,6 +83,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { snapshot, numberOfTableRowsState, ); + const currentColumnNumber = softFocusPosition.column; const currentRowNumber = softFocusPosition.row; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/onEntityCountChangeComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/onEntityCountChangeComponentState.ts index 65356e5dc..1a88ecfc0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/onEntityCountChangeComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/onEntityCountChangeComponentState.ts @@ -1,7 +1,7 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; export const onEntityCountChangeComponentState = createComponentState< - ((entityCount: number) => void) | undefined + ((entityCount?: number) => void) | undefined >({ key: 'onEntityCountChangeComponentState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts index 8836d1d1f..ea68c231c 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { capitalize } from '~/utils/string/capitalize'; @@ -36,11 +37,11 @@ query FindMany${capitalize( cursor } pageInfo { - hasNextPage + ${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''} startCursor endCursor } - totalCount + ${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''} } } `; diff --git a/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts b/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts index 02a42b251..5e0f721b6 100644 --- a/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts +++ b/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts @@ -1,7 +1,8 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const entityCountInCurrentViewComponentState = - createComponentState({ - key: 'entityCountInCurrentViewComponentState', - defaultValue: 0, - }); +export const entityCountInCurrentViewComponentState = createComponentState< + number | undefined +>({ + key: 'entityCountInCurrentViewComponentState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx index 89508dbdc..60818e493 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx @@ -15,6 +15,7 @@ import { ViewPickerListContent } from '@/views/view-picker/components/ViewPicker import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView'; +import { isDefined } from '~/utils/isDefined'; import { useViewStates } from '../../hooks/internal/useViewStates'; @@ -89,7 +90,9 @@ export const ViewPickerDropdown = () => { {currentViewWithCombinedFiltersAndSorts?.name ?? 'All'} - · {entityCountInCurrentView}{' '} + {isDefined(entityCountInCurrentView) && ( + <>· {entityCountInCurrentView} + )}