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:
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
export const hasPositionField = (objectMetadataItem: ObjectMetadataItem) =>
|
||||||
|
!objectMetadataItem.isRemote;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
export const isAggregationEnabled = (objectMetadataItem: ObjectMetadataItem) =>
|
||||||
|
!objectMetadataItem.isRemote;
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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' : ''}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -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(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@ -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({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) };
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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' : ''}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user