From 0655efd9379c9b1046ad11c682441332d7093ce5 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Tue, 7 May 2024 12:30:01 +0000 Subject: [PATCH 01/33] =?UTF-8?q?=E2=98=91=EF=B8=8F=20Refacto=20"Select=20?= =?UTF-8?q?All/Unselect=20all"=20on=20indexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../__tests__/useDeleteManyRecords.test.tsx | 2 +- .../hooks/useDeleteManyRecords.ts | 80 +++--- .../object-record/hooks/useFindManyRecords.ts | 24 +- .../hooks/useLazyFindManyRecords.ts | 227 ++++++++++++++++++ .../hooks/useRecordActionBar.tsx | 46 ++-- .../RecordIndexTableContainerEffect.tsx | 18 ++ .../options/hooks/useDeleteTableData.ts | 49 ++++ .../options/hooks/useExportTableData.ts | 117 ++------- .../options/hooks/useTableData.ts | 139 +++++++++++ .../components/RecordTableActionBar.tsx | 20 +- .../action-bar/components/ActionBar.tsx | 9 +- 11 files changed, 560 insertions(+), 171 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx index 548c1f0d6aa5..6d2440e5ee95 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx @@ -49,7 +49,7 @@ describe('useDeleteManyRecords', () => { await act(async () => { const res = await result.current.deleteManyRecords(people); expect(res).toBeDefined(); - expect(res).toHaveProperty('id'); + expect(res[0]).toHaveProperty('id'); }); expect(mocks[0].result).toHaveBeenCalled(); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index f189d516da1b..8df2f28824b3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -44,41 +44,57 @@ export const useDeleteManyRecords = ({ const deleteManyRecords = async ( idsToDelete: string[], options?: DeleteManyRecordsOptions, + chunkSize = 30, ) => { - const deletedRecords = await apolloClient.mutate({ - mutation: deleteManyRecordsMutation, - variables: { - filter: { id: { in: idsToDelete } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: idsToDelete.map((idToDelete) => ({ - __typename: capitalize(objectNameSingular), - id: idToDelete, - })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); + const chunkedIds = idsToDelete.reduce((acc, id, index) => { + const chunkIndex = Math.floor(index / chunkSize); + if (!acc[chunkIndex]) { + acc[chunkIndex] = []; + } + acc[chunkIndex].push(id); + return acc; + }, []); + + const res = await Promise.all( + chunkedIds.map(async (ids) => { + const deletedRecords = await apolloClient.mutate({ + mutation: deleteManyRecordsMutation, + variables: { + filter: { id: { in: ids } }, }, - }); + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: ids.map((idToDelete) => ({ + __typename: capitalize(objectNameSingular), + id: idToDelete, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }); + + return deletedRecords.data?.[mutationResponseField] ?? null; + }), + ); - return deletedRecords.data?.[mutationResponseField] ?? null; + return res; }; return { deleteManyRecords }; 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 782761c6cd16..8f2bea605326 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -25,16 +25,7 @@ import { cursorFamilyState } from '../states/cursorFamilyState'; import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; -export const useFindManyRecords = ({ - objectNameSingular, - filter, - orderBy, - limit, - onCompleted, - skip, - recordGqlFields, - fetchPolicy, -}: ObjectMetadataItemIdentifier & +export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & RecordGqlOperationVariables & { onCompleted?: ( records: T[], @@ -46,7 +37,18 @@ export const useFindManyRecords = ({ skip?: boolean; recordGqlFields?: RecordGqlOperationGqlRecordFields; fetchPolicy?: WatchQueryFetchPolicy; - }) => { + }; + +export const useFindManyRecords = ({ + objectNameSingular, + filter, + orderBy, + limit, + onCompleted, + skip, + recordGqlFields, + fetchPolicy, +}: UseFindManyRecordsParams) => { const findManyQueryStateIdentifier = objectNameSingular + JSON.stringify(filter) + diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts new file mode 100644 index 000000000000..593e8b600d9f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -0,0 +1,227 @@ +import { useCallback, useMemo } from 'react'; +import { useLazyQuery } from '@apollo/client'; +import { isNonEmptyArray } from '@apollo/client/utilities'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; + +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; +import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; +import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords'; +import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { isDefined } from '~/utils/isDefined'; +import { logError } from '~/utils/logError'; +import { capitalize } from '~/utils/string/capitalize'; + +import { cursorFamilyState } from '../states/cursorFamilyState'; +import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; +import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; + +type UseLazyFindManyRecordsParams = Omit< + UseFindManyRecordsParams, + 'skip' +>; + +export const useLazyFindManyRecords = ({ + objectNameSingular, + filter, + orderBy, + limit, + onCompleted, + recordGqlFields, + fetchPolicy, +}: UseLazyFindManyRecordsParams) => { + const findManyQueryStateIdentifier = + objectNameSingular + + JSON.stringify(filter) + + JSON.stringify(orderBy) + + limit; + + const [lastCursor, setLastCursor] = useRecoilState( + cursorFamilyState(findManyQueryStateIdentifier), + ); + + const [hasNextPage, setHasNextPage] = useRecoilState( + hasNextPageFamilyState(findManyQueryStateIdentifier), + ); + + const setIsFetchingMoreObjects = useSetRecoilState( + isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier), + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { findManyRecordsQuery } = useFindManyRecordsQuery({ + objectNameSingular, + recordGqlFields, + }); + + const { enqueueSnackBar } = useSnackBar(); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const [findManyRecords, { data, loading, error, fetchMore }] = + useLazyQuery(findManyRecordsQuery, { + variables: { + filter, + limit, + orderBy, + }, + fetchPolicy: fetchPolicy, + onCompleted: (data) => { + if (!isDefined(data)) { + onCompleted?.([]); + } + + const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; + + const records = getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, + }); + + if (isDefined(data?.[objectMetadataItem.namePlural])) { + setLastCursor(pageInfo.endCursor ?? ''); + setHasNextPage(pageInfo.hasNextPage ?? false); + } + }, + onError: (error) => { + logError( + `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, + { + variant: 'error', + }, + ); + }, + }); + + const fetchMoreRecords = useCallback(async () => { + if (hasNextPage) { + setIsFetchingMoreObjects(true); + + try { + await fetchMore({ + variables: { + filter, + orderBy, + lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined, + }, + updateQuery: (prev, { fetchMoreResult }) => { + const previousEdges = prev?.[objectMetadataItem.namePlural]?.edges; + const nextEdges = + fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; + + let newEdges: RecordGqlEdge[] = []; + + if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) { + newEdges = filterUniqueRecordEdgesByCursor([ + ...(prev?.[objectMetadataItem.namePlural]?.edges ?? []), + ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? + []), + ]); + } + + const pageInfo = + fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; + + if (isDefined(data?.[objectMetadataItem.namePlural])) { + setLastCursor(pageInfo.endCursor ?? ''); + setHasNextPage(pageInfo.hasNextPage ?? false); + } + + const records = getRecordsFromRecordConnection({ + recordConnection: { + edges: newEdges, + pageInfo, + }, + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount, + }); + + return Object.assign({}, prev, { + [objectMetadataItem.namePlural]: { + __typename: `${capitalize( + objectMetadataItem.nameSingular, + )}Connection`, + edges: newEdges, + pageInfo: + fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural].totalCount, + }, + } as RecordGqlOperationFindManyResult); + }, + }); + } catch (error) { + logError( + `fetchMoreObjects for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`, + { + variant: 'error', + }, + ); + } finally { + setIsFetchingMoreObjects(false); + } + } + }, [ + hasNextPage, + setIsFetchingMoreObjects, + fetchMore, + filter, + orderBy, + lastCursor, + objectMetadataItem.namePlural, + objectMetadataItem.nameSingular, + onCompleted, + data, + setLastCursor, + setHasNextPage, + enqueueSnackBar, + ]); + + const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0; + + const records = useMemo( + () => + data?.[objectMetadataItem.namePlural] + ? getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) + : ([] as T[]), + + [data, objectMetadataItem.namePlural], + ); + + return { + objectMetadataItem, + records, + totalCount, + loading, + error, + fetchMoreRecords, + queryStateIdentifier: findManyQueryStateIdentifier, + findManyRecords: currentWorkspaceMember ? findManyRecords : () => {}, + }; +}; 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 f8370cfb24d9..e73f0fa7b9b6 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 @@ -13,8 +13,8 @@ import { 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 { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; import { 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'; @@ -28,12 +28,14 @@ type useRecordActionBarProps = { objectMetadataItem: ObjectMetadataItem; selectedRecordIds: string[]; callback?: () => void; + numSelected?: number; }; export const useRecordActionBar = ({ objectMetadataItem, selectedRecordIds, callback, + numSelected, }: useRecordActionBarProps) => { const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); @@ -42,10 +44,6 @@ export const useRecordActionBar = ({ const { createFavorite, favorites, deleteFavorite } = useFavorites(); - const { deleteManyRecords } = useDeleteManyRecords({ - objectNameSingular: objectMetadataItem.nameSingular, - }); - const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, }); @@ -85,10 +83,20 @@ export const useRecordActionBar = ({ ], ); + const baseTableDataParams = { + delayMs: 100, + objectNameSingular: objectMetadataItem.nameSingular, + recordIndexId: objectMetadataItem.namePlural, + }; + + const { deleteTableData } = useDeleteTableData({ + ...baseTableDataParams, + callback, + }); + const handleDeleteClick = useCallback(async () => { - callback?.(); - await deleteManyRecords(selectedRecordIds); - }, [callback, deleteManyRecords, selectedRecordIds]); + deleteTableData(); + }, [deleteTableData]); const handleExecuteQuickActionOnClick = useCallback(async () => { callback?.(); @@ -100,10 +108,8 @@ export const useRecordActionBar = ({ }, [callback, executeQuickActionOnOneRecord, selectedRecordIds]); const { progress, download } = useExportTableData({ - delayMs: 100, + ...baseTableDataParams, filename: `${objectMetadataItem.nameSingular}.csv`, - objectNameSingular: objectMetadataItem.nameSingular, - recordIndexId: objectMetadataItem.namePlural, }); const isRemoteObject = objectMetadataItem.isRemote; @@ -120,6 +126,7 @@ export const useRecordActionBar = ({ [download, progress], ); + const recordsNum = numSelected ?? selectedRecordIds.length; const deletionActions: ContextMenuEntry[] = useMemo( () => [ { @@ -131,26 +138,19 @@ export const useRecordActionBar = ({ handleDeleteClick()} - deleteButtonText={`Delete ${ - selectedRecordIds.length > 1 ? 'Records' : 'Record' - }`} + deleteButtonText={`Delete ${recordsNum > 1 ? 'Records' : 'Record'}`} /> ), }, ], - [ - handleDeleteClick, - selectedRecordIds, - isDeleteRecordsModalOpen, - setIsDeleteRecordsModalOpen, - ], + [isDeleteRecordsModalOpen, recordsNum, handleDeleteClick], ); const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index 6677d042665d..21df6e9eb611 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -6,7 +6,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; +import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { useViewStates } from '@/views/hooks/internal/useViewStates'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; type RecordIndexTableContainerEffectProps = { @@ -45,12 +47,28 @@ export const RecordIndexTableContainerEffect = ({ setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); + const { tableRowIdsState, hasUserSelectedAllRowState } = + useRecordTableStates(recordTableId); + + const { entityCountInCurrentViewState } = useViewStates(recordTableId); + const entityCountInCurrentView = useRecoilValue( + entityCountInCurrentViewState, + ); + const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const tableRowIds = useRecoilValue(tableRowIdsState); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); + const numSelected = + hasUserSelectedAllRow && selectedRowIds.length === tableRowIds.length + ? entityCountInCurrentView + : undefined; + const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ objectMetadataItem, selectedRecordIds: selectedRowIds, callback: resetTableRowSelection, + numSelected, }); const handleToggleColumnFilter = useHandleToggleColumnFilter({ diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts new file mode 100644 index 000000000000..ca7a928ec6ad --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -0,0 +1,49 @@ +import { useMemo } from 'react'; + +import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { + useTableData, + UseTableDataOptions, +} from '@/object-record/record-index/options/hooks/useTableData'; +import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +type UseDeleteTableDataOptions = Omit & { + callback?: () => void; +}; + +export const useDeleteTableData = ({ + delayMs, + maximumRequests = 100, + objectNameSingular, + pageSize = 30, + recordIndexId, + callback, +}: UseDeleteTableDataOptions) => { + const { deleteManyRecords } = useDeleteManyRecords({ objectNameSingular }); + + const deleteRecords = useMemo( + () => + (rows: ObjectRecord[], _columns: ColumnDefinition[]) => { + const recordIds = rows.map((record) => record.id); + deleteManyRecords(recordIds); + callback?.(); + }, + [callback, deleteManyRecords], + ); + + const { progress, download: deleteTableData } = useTableData({ + delayMs, + maximumRequests, + objectNameSingular, + pageSize, + recordIndexId, + callback: deleteRecords, + }); + + return { + progress, + deleteTableData, + }; +}; 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 b0113d2bab57..af848ce89c46 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 @@ -1,18 +1,15 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { json2csv } from 'json-2-csv'; -import { useRecoilValue } from 'recoil'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; +import { + useTableData, + UseTableDataOptions, +} from '@/object-record/record-index/options/hooks/useTableData'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; -import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; - -export const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - export const download = (blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); @@ -77,10 +74,6 @@ export const generateCsv: GenerateExport = ({ }); }; -export const percentage = (part: number, whole: number): number => { - return Math.round((part / whole) * 100); -}; - const downloader = (mimeType: string, generator: GenerateExport) => { return (filename: string, data: GenerateExportOptions) => { const blob = new Blob([generator(data)], { type: mimeType }); @@ -90,13 +83,8 @@ const downloader = (mimeType: string, generator: GenerateExport) => { export const csvDownloader = downloader('text/csv', generateCsv); -type UseExportTableDataOptions = { - delayMs: number; +type UseExportTableDataOptions = Omit & { filename: string; - maximumRequests?: number; - objectNameSingular: string; - pageSize?: number; - recordIndexId: string; }; export const useExportTableData = ({ @@ -107,89 +95,20 @@ export const useExportTableData = ({ pageSize = 30, recordIndexId, }: UseExportTableDataOptions) => { - 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 { visibleTableColumnsSelector, selectedRowIdsSelector } = - useRecordTableStates(recordIndexId); - - const columns = useRecoilValue(visibleTableColumnsSelector()); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - - const hasSelectedRows = selectedRowIds.length > 0; - - const findManyRecordsParams = useFindManyParams( - objectNameSingular, - recordIndexId, - ); - - const selectedFindManyParams = { - ...findManyRecordsParams, - filter: { - ...findManyRecordsParams.filter, - id: { - in: selectedRowIds, + const downloadCsv = useMemo( + () => + (rows: ObjectRecord[], columns: ColumnDefinition[]) => { + csvDownloader(filename, { rows, columns }); }, - }, - }; - - const usedFindManyParams = hasSelectedRows - ? selectedFindManyParams - : findManyRecordsParams; - - // Todo: this needs to be done on click on the Export not button, not to be reactive. Use Lazy query for example - 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 downloadCsv = (rows: object[]) => { - csvDownloader(filename, { rows, columns }); - setIsDownloading(false); - setProgress(undefined); - }; - - const fetchNextPage = async () => { - setInflight(true); - await fetchMoreRecords(); - setPageCount((state) => state + 1); - setProgress(percentage(pageCount, MAXIMUM_REQUESTS)); - await sleep(delayMs); - setInflight(false); - }; - - if (!isDownloading || inflight) { - return; - } + [filename], + ); - if (!hasNextPage || pageCount >= MAXIMUM_REQUESTS) { - downloadCsv(records); - } else { - fetchNextPage(); - } - }, [ + return useTableData({ delayMs, - fetchMoreRecords, - filename, - hasNextPage, - inflight, - isDownloading, - pageCount, - records, - totalCount, - columns, maximumRequests, + objectNameSingular, pageSize, - ]); - - return { progress, download: () => setIsDownloading(true) }; + recordIndexId, + callback: downloadCsv, + }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts new file mode 100644 index 000000000000..8b38351e8358 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +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 { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; + +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const percentage = (part: number, whole: number): number => { + return Math.round((part / whole) * 100); +}; + +export type UseTableDataOptions = { + delayMs: number; + maximumRequests?: number; + objectNameSingular: string; + pageSize?: number; + recordIndexId: string; + callback: ( + rows: ObjectRecord[], + columns: ColumnDefinition[], + ) => void; +}; + +export const useTableData = ({ + delayMs, + maximumRequests = 100, + objectNameSingular, + pageSize = 30, + recordIndexId, + callback, +}: UseTableDataOptions) => { + 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 { + visibleTableColumnsSelector, + selectedRowIdsSelector, + tableRowIdsState, + hasUserSelectedAllRowState, + } = useRecordTableStates(recordIndexId); + + const columns = useRecoilValue(visibleTableColumnsSelector()); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); + + const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const tableRowIds = useRecoilValue(tableRowIdsState); + + const hasSelectedRows = + selectedRowIds.length > 0 && + !(hasUserSelectedAllRow && selectedRowIds.length === tableRowIds.length); + + const findManyRecordsParams = useFindManyParams( + objectNameSingular, + recordIndexId, + ); + + const selectedFindManyParams = { + ...findManyRecordsParams, + filter: { + ...findManyRecordsParams.filter, + id: { + in: selectedRowIds, + }, + }, + }; + + const usedFindManyParams = hasSelectedRows + ? selectedFindManyParams + : findManyRecordsParams; + + const { findManyRecords, totalCount, records, fetchMoreRecords, loading } = + useLazyFindManyRecords({ + ...usedFindManyParams, + limit: pageSize, + onCompleted: (_data, options) => { + setHasNextPage(options?.pageInfo?.hasNextPage ?? false); + }, + }); + + useEffect(() => { + const MAXIMUM_REQUESTS = Math.min(maximumRequests, totalCount / pageSize); + + const fetchNextPage = async () => { + setInflight(true); + await fetchMoreRecords(); + setPageCount((state) => state + 1); + setProgress(percentage(pageCount, MAXIMUM_REQUESTS)); + await sleep(delayMs); + setInflight(false); + }; + + if (!isDownloading || inflight) { + return; + } + + if (!loading) { + if (!hasNextPage || pageCount >= MAXIMUM_REQUESTS) { + callback(records, columns); + setIsDownloading(false); + setProgress(undefined); + } else { + fetchNextPage(); + } + } + }, [ + delayMs, + fetchMoreRecords, + hasNextPage, + inflight, + isDownloading, + pageCount, + records, + totalCount, + columns, + maximumRequests, + pageSize, + loading, + callback, + ]); + + return { + progress, + download: () => { + setPageCount(0); + setIsDownloading(true); + findManyRecords?.(); + }, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index f7241a5e2a48..116bc81d03fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -2,19 +2,35 @@ import { useRecoilValue } from 'recoil'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; +import { useViewStates } from '@/views/hooks/internal/useViewStates'; export const RecordTableActionBar = ({ recordTableId, }: { recordTableId: string; }) => { - const { selectedRowIdsSelector } = useRecordTableStates(recordTableId); + const { + selectedRowIdsSelector, + tableRowIdsState, + hasUserSelectedAllRowState, + } = useRecordTableStates(recordTableId); + const { entityCountInCurrentViewState } = useViewStates(recordTableId); + const entityCountInCurrentView = useRecoilValue( + entityCountInCurrentViewState, + ); + const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const tableRowIds = useRecoilValue(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); + const numSelected = + hasUserSelectedAllRow && selectedRowIds.length === tableRowIds.length + ? entityCountInCurrentView + : undefined; + if (!selectedRowIds.length) { return null; } - return ; + return ; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx index f0e2a9d2f488..f14434fd718e 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx @@ -9,6 +9,7 @@ import { ActionBarItem } from './ActionBarItem'; type ActionBarProps = { selectedIds?: string[]; + numSelected?: number; }; const StyledContainerActionBar = styled.div` @@ -39,7 +40,7 @@ const StyledLabel = styled.div` padding-right: ${({ theme }) => theme.spacing(2)}; `; -export const ActionBar = ({ selectedIds }: ActionBarProps) => { +export const ActionBar = ({ selectedIds, numSelected }: ActionBarProps) => { const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState); const actionBarEntries = useRecoilValue(actionBarEntriesState); const wrapperRef = useRef(null); @@ -55,8 +56,10 @@ export const ActionBar = ({ selectedIds }: ActionBarProps) => { className="action-bar" ref={wrapperRef} > - {selectedIds && ( - {selectedIds.length} selected: + {(numSelected || selectedIds) && ( + + {numSelected ?? selectedIds?.length} selected: + )} {actionBarEntries.map((item, index) => ( From 40faff31a0b0ad06addb6b4b68579520a266f42d Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Wed, 8 May 2024 11:33:18 +0000 Subject: [PATCH 02/33] Fix jest Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../options/hooks/__tests__/useExportTableData.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 c51286e6a956..2e2f765c845e 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 @@ -1,13 +1,8 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { - csvDownloader, - download, - generateCsv, - percentage, - sleep, -} from '../useExportTableData'; +import { csvDownloader, download, generateCsv } from '../useExportTableData'; +import { percentage, sleep } from '../useTableData'; jest.useFakeTimers(); From 1cf0db6bef67868b3ae0493848b22343806c9d05 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Wed, 8 May 2024 11:51:44 +0000 Subject: [PATCH 03/33] Refactor according to self review Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../hooks/useDeleteManyRecords.ts | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 8df2f28824b3..b86ba9747b29 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -18,6 +18,16 @@ type DeleteManyRecordsOptions = { skipOptimisticEffect?: boolean; }; +const chunkArray = (array: T[], chunkSize: number): T[][] => + array.reduce((acc, item, index) => { + const chunkIndex = Math.floor(index / chunkSize); + if (!acc[chunkIndex]) { + acc[chunkIndex] = []; + } + acc[chunkIndex].push(item); + return acc; + }, []); + export const useDeleteManyRecords = ({ objectNameSingular, }: useDeleteOneRecordProps) => { @@ -41,60 +51,55 @@ export const useDeleteManyRecords = ({ objectMetadataItem.namePlural, ); + const deleteRecordsWithIds = async ( + idsToDelete: string[], + options?: DeleteManyRecordsOptions, + ) => { + const deletedRecords = await apolloClient.mutate({ + mutation: deleteManyRecordsMutation, + variables: { + filter: { id: { in: idsToDelete } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: idsToDelete.map((idToDelete) => ({ + __typename: capitalize(objectNameSingular), + id: idToDelete, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }); + + return deletedRecords.data?.[mutationResponseField] ?? null; + }; + const deleteManyRecords = async ( idsToDelete: string[], options?: DeleteManyRecordsOptions, chunkSize = 30, ) => { - const chunkedIds = idsToDelete.reduce((acc, id, index) => { - const chunkIndex = Math.floor(index / chunkSize); - if (!acc[chunkIndex]) { - acc[chunkIndex] = []; - } - acc[chunkIndex].push(id); - return acc; - }, []); - - const res = await Promise.all( - chunkedIds.map(async (ids) => { - const deletedRecords = await apolloClient.mutate({ - mutation: deleteManyRecordsMutation, - variables: { - filter: { id: { in: ids } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: ids.map((idToDelete) => ({ - __typename: capitalize(objectNameSingular), - id: idToDelete, - })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); - - return deletedRecords.data?.[mutationResponseField] ?? null; - }), + const chunkedIds = chunkArray(idsToDelete, chunkSize); + return Promise.all( + chunkedIds.map((ids) => deleteRecordsWithIds(ids, options)), ); - - return res; }; return { deleteManyRecords }; From 29dd4f6bec179bdd98aa5c452c143920eb819e58 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Thu, 16 May 2024 12:26:03 +0000 Subject: [PATCH 04/33] Refactor according to review Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../object-record/hooks/useFindManyRecords.ts | 190 ++++++++++++------ .../hooks/useLazyFindManyRecords.ts | 148 ++------------ 2 files changed, 148 insertions(+), 190 deletions(-) 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 8f2bea605326..0f0d0a492b42 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,11 +1,18 @@ import { useCallback, useMemo } from 'react'; -import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; +import { + ApolloQueryResult, + FetchMoreQueryOptions, + OperationVariables, + useQuery, + WatchQueryFetchPolicy, +} from '@apollo/client'; import { isNonEmptyArray } from '@apollo/client/utilities'; import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; @@ -39,16 +46,41 @@ export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & fetchPolicy?: WatchQueryFetchPolicy; }; -export const useFindManyRecords = ({ +type UseFindManyRecordsStateParams< + T, + TData = RecordGqlOperationFindManyResult, +> = Omit< + UseFindManyRecordsParams, + 'skip' | 'recordGqlFields' | 'fetchPolicy' +> & { + data: RecordGqlOperationFindManyResult | undefined; + fetchMore< + TFetchData = TData, + TFetchVars extends OperationVariables = OperationVariables, + >( + fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: ( + previousQueryResult: TData, + options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }, + ) => TData; + }, + ): Promise>; + objectMetadataItem: ObjectMetadataItem; +}; + +export const useFindManyRecordsState = ({ objectNameSingular, filter, orderBy, limit, onCompleted, - skip, - recordGqlFields, - fetchPolicy, -}: UseFindManyRecordsParams) => { + data, + fetchMore, + objectMetadataItem, +}: UseFindManyRecordsStateParams) => { const findManyQueryStateIdentifier = objectNameSingular + JSON.stringify(filter) + @@ -67,61 +99,7 @@ export const useFindManyRecords = ({ isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier), ); - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const { findManyRecordsQuery } = useFindManyRecordsQuery({ - objectNameSingular, - recordGqlFields, - }); - const { enqueueSnackBar } = useSnackBar(); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - - const { data, loading, error, fetchMore } = - useQuery(findManyRecordsQuery, { - skip: skip || !objectMetadataItem || !currentWorkspaceMember, - variables: { - filter, - limit, - orderBy, - }, - fetchPolicy: fetchPolicy, - onCompleted: (data) => { - if (!isDefined(data)) { - onCompleted?.([]); - } - - const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; - - const records = getRecordsFromRecordConnection({ - recordConnection: data?.[objectMetadataItem.namePlural], - }) as T[]; - - onCompleted?.(records, { - pageInfo, - totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, - }); - - if (isDefined(data?.[objectMetadataItem.namePlural])) { - setLastCursor(pageInfo.endCursor ?? ''); - setHasNextPage(pageInfo.hasNextPage ?? false); - } - }, - onError: (error) => { - logError( - `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, - { - variant: 'error', - }, - ); - }, - }); const fetchMoreRecords = useCallback(async () => { if (hasNextPage) { @@ -228,6 +206,98 @@ export const useFindManyRecords = ({ [data, objectMetadataItem.namePlural], ); + return { + findManyQueryStateIdentifier, + setLastCursor, + setHasNextPage, + fetchMoreRecords, + totalCount, + records, + }; +}; + +export const useFindManyRecords = ({ + objectNameSingular, + filter, + orderBy, + limit, + onCompleted, + skip, + recordGqlFields, + fetchPolicy, +}: UseFindManyRecordsParams) => { + const { enqueueSnackBar } = useSnackBar(); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + const { findManyRecordsQuery } = useFindManyRecordsQuery({ + objectNameSingular, + recordGqlFields, + }); + + const { data, loading, error, fetchMore } = + useQuery(findManyRecordsQuery, { + skip: skip || !objectMetadataItem || !currentWorkspaceMember, + variables: { + filter, + limit, + orderBy, + }, + fetchPolicy: fetchPolicy, + onCompleted: (data) => { + if (!isDefined(data)) { + onCompleted?.([]); + } + + const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; + + const records = getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, + }); + + if (isDefined(data?.[objectMetadataItem.namePlural])) { + setLastCursor(pageInfo.endCursor ?? ''); + setHasNextPage(pageInfo.hasNextPage ?? false); + } + }, + onError: (error) => { + logError( + `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, + { + variant: 'error', + }, + ); + }, + }); + + const { + findManyQueryStateIdentifier, + setLastCursor, + setHasNextPage, + fetchMoreRecords, + totalCount, + records, + } = useFindManyRecordsState({ + objectNameSingular, + filter, + orderBy, + limit, + onCompleted, + fetchMore, + data, + objectMetadataItem, + }); + return { objectMetadataItem, records, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index 593e8b600d9f..78f31757f409 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -1,26 +1,19 @@ -import { useCallback, useMemo } from 'react'; import { useLazyQuery } from '@apollo/client'; -import { isNonEmptyArray } from '@apollo/client/utilities'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; -import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords'; +import { + UseFindManyRecordsParams, + useFindManyRecordsState, +} from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { isDefined } from '~/utils/isDefined'; import { logError } from '~/utils/logError'; -import { capitalize } from '~/utils/string/capitalize'; - -import { cursorFamilyState } from '../states/cursorFamilyState'; -import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; -import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; type UseLazyFindManyRecordsParams = Omit< UseFindManyRecordsParams, @@ -36,24 +29,6 @@ export const useLazyFindManyRecords = ({ recordGqlFields, fetchPolicy, }: UseLazyFindManyRecordsParams) => { - const findManyQueryStateIdentifier = - objectNameSingular + - JSON.stringify(filter) + - JSON.stringify(orderBy) + - limit; - - const [lastCursor, setLastCursor] = useRecoilState( - cursorFamilyState(findManyQueryStateIdentifier), - ); - - const [hasNextPage, setHasNextPage] = useRecoilState( - hasNextPageFamilyState(findManyQueryStateIdentifier), - ); - - const setIsFetchingMoreObjects = useSetRecoilState( - isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier), - ); - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -109,110 +84,23 @@ export const useLazyFindManyRecords = ({ }, }); - const fetchMoreRecords = useCallback(async () => { - if (hasNextPage) { - setIsFetchingMoreObjects(true); - - try { - await fetchMore({ - variables: { - filter, - orderBy, - lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined, - }, - updateQuery: (prev, { fetchMoreResult }) => { - const previousEdges = prev?.[objectMetadataItem.namePlural]?.edges; - const nextEdges = - fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; - - let newEdges: RecordGqlEdge[] = []; - - if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) { - newEdges = filterUniqueRecordEdgesByCursor([ - ...(prev?.[objectMetadataItem.namePlural]?.edges ?? []), - ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? - []), - ]); - } - - const pageInfo = - fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; - - if (isDefined(data?.[objectMetadataItem.namePlural])) { - setLastCursor(pageInfo.endCursor ?? ''); - setHasNextPage(pageInfo.hasNextPage ?? false); - } - - const records = getRecordsFromRecordConnection({ - recordConnection: { - edges: newEdges, - pageInfo, - }, - }) as T[]; - - onCompleted?.(records, { - pageInfo, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount, - }); - - return Object.assign({}, prev, { - [objectMetadataItem.namePlural]: { - __typename: `${capitalize( - objectMetadataItem.nameSingular, - )}Connection`, - edges: newEdges, - pageInfo: - fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural].totalCount, - }, - } as RecordGqlOperationFindManyResult); - }, - }); - } catch (error) { - logError( - `fetchMoreObjects for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`, - { - variant: 'error', - }, - ); - } finally { - setIsFetchingMoreObjects(false); - } - } - }, [ - hasNextPage, - setIsFetchingMoreObjects, - fetchMore, + const { + findManyQueryStateIdentifier, + setLastCursor, + setHasNextPage, + fetchMoreRecords, + totalCount, + records, + } = useFindManyRecordsState({ + objectNameSingular, filter, orderBy, - lastCursor, - objectMetadataItem.namePlural, - objectMetadataItem.nameSingular, + limit, onCompleted, + fetchMore, data, - setLastCursor, - setHasNextPage, - enqueueSnackBar, - ]); - - const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0; - - const records = useMemo( - () => - data?.[objectMetadataItem.namePlural] - ? getRecordsFromRecordConnection({ - recordConnection: data?.[objectMetadataItem.namePlural], - }) - : ([] as T[]), - - [data, objectMetadataItem.namePlural], - ); + objectMetadataItem, + }); return { objectMetadataItem, From c0ca3bd249d148eddabb86a7d494f509802448c4 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Thu, 23 May 2024 10:15:40 +0000 Subject: [PATCH 05/33] Merge main Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../src/modules/object-record/hooks/useDeleteManyRecords.ts | 5 ----- .../modules/object-record/hooks/useLazyFindManyRecords.ts | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 307f1758bdeb..b86ba9747b29 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -5,7 +5,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; -import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; @@ -52,10 +51,6 @@ export const useDeleteManyRecords = ({ objectMetadataItem.namePlural, ); - const { findManyRecordsQuery } = useFindManyRecordsQuery({ - objectNameSingular, - }); - const deleteRecordsWithIds = async ( idsToDelete: string[], options?: DeleteManyRecordsOptions, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index f29bc96ce0ed..e9d25571bc8b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -97,6 +97,7 @@ export const useLazyFindManyRecords = ({ onCompleted, fetchMore, data, + error, objectMetadataItem, }); From 2477b1403ddeb0efdc0b76cd58acb9567c78a550 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 17 Jun 2024 11:21:51 +0200 Subject: [PATCH 06/33] Fixed typing problem --- .../src/modules/object-record/hooks/useFindManyRecords.ts | 3 ++- .../src/modules/object-record/utils/useFindManyRecordsUtils.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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 1d8bc297e684..5cf76253da11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,5 +1,4 @@ import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; -import { onError } from '@apollo/client/link/error'; import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -42,6 +41,7 @@ export const useFindManyRecords = ({ skip, recordGqlFields, fetchPolicy, + onError, }: UseFindManyRecordsParams) => { const { enqueueSnackBar } = useSnackBar(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); @@ -105,6 +105,7 @@ export const useFindManyRecords = ({ fetchMoreRecords, totalCount, records, + hasNextPage, } = useFindManyRecordsState({ objectNameSingular, filter, diff --git a/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts b/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts index 00ce6800851c..4b197d05b268 100644 --- a/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts +++ b/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts @@ -215,5 +215,6 @@ export const useFindManyRecordsState = ({ fetchMoreRecords, totalCount, records, + hasNextPage, }; }; From 856fa81c834fd4af86baaffc41966b4824abc7fa Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 17 Jun 2024 11:43:10 +0200 Subject: [PATCH 07/33] Fix --- .../options/hooks/__tests__/useExportTableData.test.ts | 1 - .../record-index/options/hooks/useExportTableData.ts | 1 - 2 files changed, 2 deletions(-) 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 c2aebc23e9dc..f5d35d92bcbe 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 @@ -7,7 +7,6 @@ import { download, generateCsv, } from '../useExportTableData'; -import { sleep } from '../useTableData'; jest.useFakeTimers(); 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 5added75c1c5..6d1693f29b40 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 @@ -10,7 +10,6 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { sleep } from '~/utils/sleep'; export const download = (blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); From de563bf3251046c2af8ca7a31114e1e1196d7042 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 21 Jun 2024 11:12:51 +0000 Subject: [PATCH 08/33] Merge main Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../options/hooks/__tests__/useExportTableData.test.ts | 1 - 1 file changed, 1 deletion(-) 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 c5952152c738..b7dfc586016f 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 @@ -7,7 +7,6 @@ import { download, generateCsv, } from '../useExportTableData'; -import { sleep } from '../useTableData'; jest.useFakeTimers(); From a43bf6b5e54f39cfaa91ce76fac1bcd44bc8e783 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 21 Jun 2024 15:16:01 +0000 Subject: [PATCH 09/33] Fix Select All checkbox Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../selectors/allRowsSelectedStatusComponentSelector.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts index c0a42ea81015..6ce6b42ff6c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts @@ -1,5 +1,5 @@ -import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; +import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus'; @@ -10,7 +10,7 @@ export const allRowsSelectedStatusComponentSelector = get: ({ scopeId }) => ({ get }) => { - const numberOfRows = get(numberOfTableRowsComponentState({ scopeId })); + const tableRowIds = get(tableRowIdsComponentState({ scopeId })); const selectedRowIds = get( selectedRowIdsComponentSelector({ scopeId }), @@ -21,7 +21,7 @@ export const allRowsSelectedStatusComponentSelector = const allRowsSelectedStatus = numberOfSelectedRows === 0 ? 'none' - : numberOfRows === numberOfSelectedRows + : selectedRowIds.length === tableRowIds.length ? 'all' : 'some'; From fe9c4657d7ab0be9c7dd1382fedcc092e5c9d93c Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 24 Jun 2024 15:04:44 +0200 Subject: [PATCH 10/33] Fixed bug --- .../src/modules/object-record/hooks/useFindManyRecords.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 296f274c6806..5cf76253da11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,5 +1,4 @@ import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; -import { onError } from '@apollo/client/link/error'; import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -42,6 +41,7 @@ export const useFindManyRecords = ({ skip, recordGqlFields, fetchPolicy, + onError, }: UseFindManyRecordsParams) => { const { enqueueSnackBar } = useSnackBar(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); From 922942df2c19ad9b129b702157dbff36afd127c7 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 28 Jun 2024 08:30:15 +0000 Subject: [PATCH 11/33] Revert temporary changes Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../src/modules/object-record/hooks/useFindManyRecords.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 296f274c6806..5cf76253da11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,5 +1,4 @@ import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; -import { onError } from '@apollo/client/link/error'; import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -42,6 +41,7 @@ export const useFindManyRecords = ({ skip, recordGqlFields, fetchPolicy, + onError, }: UseFindManyRecordsParams) => { const { enqueueSnackBar } = useSnackBar(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); From db93173b06c4bbfdb2116e2793c78e600f02ec87 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 28 Jun 2024 08:45:43 +0000 Subject: [PATCH 12/33] Fix linter issues Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../record-action-bar/hooks/useRecordActionBar.tsx | 8 +++++++- .../record-index/options/hooks/useTableData.ts | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) 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 f6416bba1465..65901f1cbf30 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 @@ -167,7 +167,13 @@ export const useRecordActionBar = ({ }, ] : [], - [maxRecords, isDeleteRecordsModalOpen, recordsNum, handleDeleteClick], + [ + maxRecords, + selectedRecordIds.length, + isDeleteRecordsModalOpen, + recordsNum, + handleDeleteClick, + ], ); const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts index 8893f1da1bf1..4b34bc2d5813 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -114,8 +114,6 @@ export const useTableData = ({ ? unselectedFindManyParams : findManyRecordsParams; - console.log('debug: ', { usedFindManyParams }); - const { findManyRecords, totalCount, records, fetchMoreRecords, loading } = useLazyFindManyRecords({ ...usedFindManyParams, From 72aa58cce81e72dad309c98f034acd8f34f94414 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 28 Jun 2024 09:07:28 +0000 Subject: [PATCH 13/33] Fix records count Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../record-action-bar/hooks/useRecordActionBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 65901f1cbf30..339ee1429e44 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 @@ -139,9 +139,10 @@ export const useRecordActionBar = ({ ); const recordsNum = numSelected ?? selectedRecordIds.length; + const deletionActions: ContextMenuEntry[] = useMemo( () => - maxRecords !== undefined && selectedRecordIds.length <= maxRecords + maxRecords !== undefined && recordsNum <= maxRecords ? [ { label: 'Delete', From cf1ac0ecc2f3b1095a551f49f2fa6eecfbf4a768 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Fri, 28 Jun 2024 09:12:51 +0000 Subject: [PATCH 14/33] Fix linter Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../record-action-bar/hooks/useRecordActionBar.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 339ee1429e44..2ba5d615cc04 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 @@ -168,13 +168,7 @@ export const useRecordActionBar = ({ }, ] : [], - [ - maxRecords, - selectedRecordIds.length, - isDeleteRecordsModalOpen, - recordsNum, - handleDeleteClick, - ], + [maxRecords, isDeleteRecordsModalOpen, recordsNum, handleDeleteClick], ); const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( From d5011b370ab144d52618c2b4fcdb17fdeac319b2 Mon Sep 17 00:00:00 2001 From: gitstart-twenty Date: Thu, 4 Jul 2024 12:57:24 +0000 Subject: [PATCH 15/33] Fix click outside table Co-authored-by: v1b3m Co-authored-by: Toledodev --- .../record-table/hooks/internal/useLeaveTableFocus.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 0ee3aa62d6a8..34fda8a4ef3c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -8,12 +8,15 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode'; import { useDisableSoftFocus } from './useDisableSoftFocus'; +import { useSetHasUserSelectedAllRows } from './useSetAllRowSelectedState'; export const useLeaveTableFocus = (recordTableId?: string) => { const disableSoftFocus = useDisableSoftFocus(recordTableId); const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode(recordTableId); + const selectAllRows = useSetHasUserSelectedAllRows(recordTableId); + const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); return useRecoilCallback( @@ -38,7 +41,13 @@ export const useLeaveTableFocus = (recordTableId?: string) => { closeCurrentCellInEditMode(); disableSoftFocus(); + selectAllRows(false); }, - [closeCurrentCellInEditMode, disableSoftFocus, isSoftFocusActiveState], + [ + closeCurrentCellInEditMode, + disableSoftFocus, + isSoftFocusActiveState, + selectAllRows, + ], ); }; From 7c7edaacd12feb01fe356052aaa6f21c50623f5e Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 5 Jul 2024 16:55:10 +0200 Subject: [PATCH 16/33] WIP --- .../constants/DefaultMutationBatchSize.ts | 1 + .../hooks/useDeleteManyRecords.ts | 100 ++++++++++-------- .../hooks/useRecordActionBar.tsx | 7 +- 3 files changed, 63 insertions(+), 45 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/constants/DefaultMutationBatchSize.ts diff --git a/packages/twenty-front/src/modules/object-record/constants/DefaultMutationBatchSize.ts b/packages/twenty-front/src/modules/object-record/constants/DefaultMutationBatchSize.ts new file mode 100644 index 000000000000..6864c5e05e4b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/constants/DefaultMutationBatchSize.ts @@ -0,0 +1 @@ +export const DEFAULT_MUTATION_BATCH_SIZE = 30; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index b86ba9747b29..b914cec54525 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -4,6 +4,7 @@ import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { isDefined } from '~/utils/isDefined'; @@ -51,55 +52,66 @@ export const useDeleteManyRecords = ({ objectMetadataItem.namePlural, ); - const deleteRecordsWithIds = async ( - idsToDelete: string[], - options?: DeleteManyRecordsOptions, - ) => { - const deletedRecords = await apolloClient.mutate({ - mutation: deleteManyRecordsMutation, - variables: { - filter: { id: { in: idsToDelete } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: idsToDelete.map((idToDelete) => ({ - __typename: capitalize(objectNameSingular), - id: idToDelete, - })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); - - return deletedRecords.data?.[mutationResponseField] ?? null; - }; - const deleteManyRecords = async ( idsToDelete: string[], options?: DeleteManyRecordsOptions, - chunkSize = 30, ) => { - const chunkedIds = chunkArray(idsToDelete, chunkSize); - return Promise.all( - chunkedIds.map((ids) => deleteRecordsWithIds(ids, options)), + const numberOfBatches = Math.ceil( + idsToDelete.length / DEFAULT_MUTATION_BATCH_SIZE, ); + + const deletedRecords = []; + + for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) { + const batchIds = idsToDelete.slice( + batchIndex * DEFAULT_MUTATION_BATCH_SIZE, + (batchIndex + 1) * DEFAULT_MUTATION_BATCH_SIZE, + ); + + console.log({ + batchIds, + }); + + const deletedRecordsResponse = await apolloClient.mutate({ + mutation: deleteManyRecordsMutation, + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToDelete) => ({ + __typename: capitalize(objectNameSingular), + id: idToDelete, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }); + + const deletedRecordsForThisBatch = + deletedRecordsResponse.data?.[mutationResponseField] ?? []; + + deletedRecords.push(...deletedRecordsForThisBatch); + } + + return deletedRecords; }; return { deleteManyRecords }; 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 2ba5d615cc04..688a88e2470e 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 @@ -1,6 +1,11 @@ import { useCallback, useMemo, useState } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; +import { + useRecoilCallback, + useRecoilValue, + useRecoilValue, + useSetRecoilState, +} from 'recoil'; import { IconClick, IconFileExport, From 0c76416c9d2e3330b7cc39ddf488b2a457fc1eeb Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 5 Jul 2024 17:27:42 +0200 Subject: [PATCH 17/33] Fix --- .../record-action-bar/hooks/useRecordActionBar.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 688a88e2470e..2ba5d615cc04 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 @@ -1,11 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; -import { - useRecoilCallback, - useRecoilValue, - useRecoilValue, - useSetRecoilState, -} from 'recoil'; +import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { IconClick, IconFileExport, From 4cefd9c2500ee43e13b419554d2f0a5fda608bde Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 5 Jul 2024 17:37:30 +0200 Subject: [PATCH 18/33] Fix --- .../modules/object-record/constants/DeleteMaxCount.ts | 1 + .../object-record/hooks/useDeleteManyRecords.ts | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts diff --git a/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts b/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts new file mode 100644 index 000000000000..3b1041ed3de2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts @@ -0,0 +1 @@ +export const DELETE_MAX_COUNT = 3000; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index b914cec54525..99df00f0febc 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -19,16 +19,6 @@ type DeleteManyRecordsOptions = { skipOptimisticEffect?: boolean; }; -const chunkArray = (array: T[], chunkSize: number): T[][] => - array.reduce((acc, item, index) => { - const chunkIndex = Math.floor(index / chunkSize); - if (!acc[chunkIndex]) { - acc[chunkIndex] = []; - } - acc[chunkIndex].push(item); - return acc; - }, []); - export const useDeleteManyRecords = ({ objectNameSingular, }: useDeleteOneRecordProps) => { From 1bb1d27bd4fa11254948b7422820c534b87d6670 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 5 Jul 2024 17:38:47 +0200 Subject: [PATCH 19/33] removed refetch queries --- .../object-record/hooks/useCreateManyRecords.ts | 12 +----------- .../spreadsheet-import/useSpreadsheetRecordImport.ts | 7 ------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index 9b22ce551a40..70d43617b293 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -1,8 +1,4 @@ -import { - FetchResult, - InternalRefetchQueriesInclude, - useApolloClient, -} from '@apollo/client'; +import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; @@ -22,10 +18,6 @@ type useCreateManyRecordsProps = { objectNameSingular: string; recordGqlFields?: RecordGqlOperationGqlRecordFields; skipPostOptmisticEffect?: boolean; - refetchQueries?: - | InternalRefetchQueriesInclude - | ((result: FetchResult) => InternalRefetchQueriesInclude) - | undefined; }; export const useCreateManyRecords = < @@ -34,7 +26,6 @@ export const useCreateManyRecords = < objectNameSingular, recordGqlFields, skipPostOptmisticEffect = false, - refetchQueries, }: useCreateManyRecordsProps) => { const apolloClient = useApolloClient(); @@ -122,7 +113,6 @@ export const useCreateManyRecords = < objectMetadataItems, }); }, - refetchQueries, }); return createdObjects.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts index 1a0683e05f1c..28516c7939e6 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts @@ -1,10 +1,8 @@ -import { getOperationName } from '@apollo/client/utilities'; import { isNonEmptyString } from '@sniptt/guards'; import { useIcons } from 'twenty-ui'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; -import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { getSpreadSheetValidation } from '@/object-record/spreadsheet-import/util/getSpreadSheetValidation'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { Field, SpreadsheetOptions } from '@/spreadsheet-import/types'; @@ -115,13 +113,8 @@ export const useSpreadsheetRecordImport = (objectNameSingular: string) => { } } - const { findManyRecordsQuery } = useFindManyRecordsQuery({ - objectNameSingular, - }); - const { createManyRecords } = useCreateManyRecords({ objectNameSingular, - refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''], }); const openRecordSpreadsheetImport = ( From 968046d547191faf2162da48c3aadf5c2c4fa4d4 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 10:21:33 +0200 Subject: [PATCH 20/33] WIP --- .../__tests__/useFindManyRecords.test.tsx | 2 +- .../useFetchMoreRecordsWithPagination.ts | 243 ++++++++++++++++++ .../object-record/hooks/useFindManyRecords.ts | 109 +++----- .../useHandleFindManyRecordsCompleted.ts | 53 ++++ .../hooks/useHandleFindManyRecordsError.ts | 34 +++ .../hooks/useLazyFindManyRecords.ts | 131 +++++----- .../types/OnFindManyRecordsCompleted.ts | 9 + .../types/UseFindManyRecordsParams.ts | 14 + .../object-record/utils/getQueryIdentifier.ts | 11 + .../utils/useFindManyRecordsUtils.ts | 220 ---------------- .../action-bar/types/ActionBarEntry.ts | 3 +- .../context-menu/types/ContextMenuEntry.ts | 4 +- 12 files changed, 482 insertions(+), 351 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsCompleted.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts create mode 100644 packages/twenty-front/src/modules/object-record/types/OnFindManyRecordsCompleted.ts create mode 100644 packages/twenty-front/src/modules/object-record/types/UseFindManyRecordsParams.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts delete mode 100644 packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx index 57e18be23cba..b953af77278d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx @@ -73,7 +73,7 @@ describe('useFindManyRecords', () => { return useFindManyRecords({ objectNameSingular: 'person', - onCompleted, + onCompleted: onCompleted, }); }, { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts new file mode 100644 index 000000000000..a7e34d37df25 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts @@ -0,0 +1,243 @@ +import { useMemo } from 'react'; +import { + ApolloError, + ApolloQueryResult, + FetchMoreQueryOptions, + OperationVariables, + WatchQueryFetchPolicy, +} from '@apollo/client'; +import { isNonEmptyArray } from '@apollo/client/utilities'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; +import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; +import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; +import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted'; +import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; +import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; +import { isDefined } from '~/utils/isDefined'; +import { sleep } from '~/utils/sleep'; +import { capitalize } from '~/utils/string/capitalize'; + +import { cursorFamilyState } from '../states/cursorFamilyState'; +import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; +import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; + +export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & + RecordGqlOperationVariables & { + onCompleted?: OnFindManyRecordsCompleted; + skip?: boolean; + recordGqlFields?: RecordGqlOperationGqlRecordFields; + fetchPolicy?: WatchQueryFetchPolicy; + }; + +type UseFindManyRecordsStateParams< + T, + TData = RecordGqlOperationFindManyResult, +> = Omit< + UseFindManyRecordsParams, + 'skip' | 'recordGqlFields' | 'fetchPolicy' +> & { + data: RecordGqlOperationFindManyResult | undefined; + error: ApolloError | undefined; + fetchMore< + TFetchData = TData, + TFetchVars extends OperationVariables = OperationVariables, + >( + fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: ( + previousQueryResult: TData, + options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }, + ) => TData; + }, + ): Promise>; + objectMetadataItem: ObjectMetadataItem; +}; + +export const useFetchMoreRecordsWithPagination = < + T extends ObjectRecord = ObjectRecord, +>({ + objectNameSingular, + filter, + orderBy, + limit, + data, + error, + fetchMore, + objectMetadataItem, + onCompleted, +}: UseFindManyRecordsStateParams) => { + const queryIdentifier = getQueryIdentifier({ + objectNameSingular, + filter, + limit, + orderBy, + }); + + const [hasNextPage] = useRecoilState(hasNextPageFamilyState(queryIdentifier)); + + const setIsFetchingMoreObjects = useSetRecoilState( + isFetchingMoreRecordsFamilyState(queryIdentifier), + ); + + const { handleFindManyRecordsError } = useHandleFindManyRecordsError({ + objectMetadataItem, + }); + + // TODO: put this into a util inspired from https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts + // This function is equivalent to merge function + read function in field policy + const fetchMoreRecords = useRecoilCallback( + ({ snapshot, set }) => + async () => { + const hasNextPageLocal = snapshot + .getLoadable(hasNextPageFamilyState(queryIdentifier)) + .getValue(); + + const lastCursorLocal = snapshot + .getLoadable(cursorFamilyState(queryIdentifier)) + .getValue(); + + console.log({ lastCursorLocal }); + + // Remote objects does not support hasNextPage. We cannot rely on it to fetch more records. + if ( + hasNextPageLocal || + (!isAggregationEnabled(objectMetadataItem) && !error) + ) { + setIsFetchingMoreObjects(true); + + try { + const { data: fetchMoreDataResult } = await fetchMore({ + variables: { + filter, + orderBy, + lastCursor: isNonEmptyString(lastCursorLocal) + ? lastCursorLocal + : undefined, + }, + updateQuery: (prev, { fetchMoreResult }) => { + const previousEdges = + prev?.[objectMetadataItem.namePlural]?.edges; + const nextEdges = + fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; + + let newEdges: RecordGqlEdge[] = previousEdges ?? []; + + if (isNonEmptyArray(nextEdges)) { + newEdges = filterUniqueRecordEdgesByCursor([ + ...newEdges, + ...(fetchMoreResult?.[objectMetadataItem.namePlural] + ?.edges ?? []), + ]); + } + + const pageInfo = + fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; + + if (isDefined(data?.[objectMetadataItem.namePlural])) { + console.log({ + pageInfo, + hasNextPage: pageInfo?.hasNextPage, + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural] + ?.totalCount, + data: fetchMoreResult?.[objectMetadataItem.namePlural], + }); + + set( + cursorFamilyState(queryIdentifier), + pageInfo.endCursor ?? '', + ); + set( + hasNextPageFamilyState(queryIdentifier), + pageInfo.hasNextPage ?? false, + ); + } + + const records = getRecordsFromRecordConnection({ + recordConnection: { + edges: newEdges, + pageInfo, + }, + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural] + ?.totalCount, + }); + + return Object.assign({}, prev, { + [objectMetadataItem.namePlural]: { + __typename: `${capitalize( + objectMetadataItem.nameSingular, + )}Connection`, + edges: newEdges, + pageInfo: + fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, + totalCount: + fetchMoreResult?.[objectMetadataItem.namePlural] + .totalCount, + }, + } as RecordGqlOperationFindManyResult); + }, + }); + + return { + data: fetchMoreDataResult?.[objectMetadataItem.namePlural], + }; + } catch (error) { + handleFindManyRecordsError(error as ApolloError); + } finally { + setIsFetchingMoreObjects(false); + } + + await sleep(1); + } + }, + [ + objectMetadataItem, + error, + setIsFetchingMoreObjects, + fetchMore, + filter, + orderBy, + data, + onCompleted, + handleFindManyRecordsError, + queryIdentifier, + ], + ); + + const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount; + + const records = useMemo( + () => + data?.[objectMetadataItem.namePlural] + ? getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) + : ([] as T[]), + + [data, objectMetadataItem.namePlural], + ); + + return { + fetchMoreRecords, + totalCount, + records, + hasNextPage, + }; +}; 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 5cf76253da11..00629c91a6b6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -4,29 +4,21 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; +import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; +import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted'; +import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useFindManyRecordsState } from '@/object-record/utils/useFindManyRecordsUtils'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { isDefined } from '~/utils/isDefined'; -import { logError } from '~/utils/logError'; +import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted'; +import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & RecordGqlOperationVariables & { - onCompleted?: ( - records: T[], - options?: { - pageInfo?: RecordGqlConnection['pageInfo']; - totalCount?: number; - }, - ) => void; onError?: (error?: Error) => void; + onCompleted?: OnFindManyRecordsCompleted; skip?: boolean; recordGqlFields?: RecordGqlOperationGqlRecordFields; fetchPolicy?: WatchQueryFetchPolicy; @@ -37,13 +29,12 @@ export const useFindManyRecords = ({ filter, orderBy, limit, - onCompleted, skip, recordGqlFields, fetchPolicy, onError, + onCompleted, }: UseFindManyRecordsParams) => { - const { enqueueSnackBar } = useSnackBar(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -53,6 +44,24 @@ export const useFindManyRecords = ({ recordGqlFields, }); + const { handleFindManyRecordsError } = useHandleFindManyRecordsError({ + objectMetadataItem, + handleError: onError, + }); + + const queryIdentifier = getQueryIdentifier({ + objectNameSingular, + filter, + orderBy, + limit, + }); + + const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({ + objectMetadataItem, + queryIdentifier, + onCompleted, + }); + const { data, loading, error, fetchMore } = useQuery(findManyRecordsQuery, { skip: skip || !objectMetadataItem || !currentWorkspaceMember, @@ -62,61 +71,21 @@ export const useFindManyRecords = ({ orderBy, }, fetchPolicy: fetchPolicy, - onCompleted: (data) => { - if (!isDefined(data)) { - onCompleted?.([]); - } - - const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; - - const records = getRecordsFromRecordConnection({ - recordConnection: data?.[objectMetadataItem.namePlural], - }) as T[]; - - onCompleted?.(records, { - pageInfo, - totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, - }); - - if (isDefined(data?.[objectMetadataItem.namePlural])) { - setLastCursor(pageInfo.endCursor ?? ''); - setHasNextPage(pageInfo.hasNextPage ?? false); - } - }, - onError: (error) => { - logError( - `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, - { - variant: SnackBarVariant.Error, - }, - ); - onError?.(error); - }, + onCompleted: handleFindManyRecordsCompleted, + onError: handleFindManyRecordsError, }); - const { - findManyQueryStateIdentifier, - setLastCursor, - setHasNextPage, - fetchMoreRecords, - totalCount, - records, - hasNextPage, - } = useFindManyRecordsState({ - objectNameSingular, - filter, - orderBy, - limit, - onCompleted, - fetchMore, - data, - error, - objectMetadataItem, - }); + const { fetchMoreRecords, totalCount, records, hasNextPage } = + useFetchMoreRecordsWithPagination({ + objectNameSingular, + filter, + orderBy, + limit, + fetchMore, + data, + error, + objectMetadataItem, + }); return { objectMetadataItem, @@ -125,7 +94,7 @@ export const useFindManyRecords = ({ loading, error, fetchMoreRecords, - queryStateIdentifier: findManyQueryStateIdentifier, + queryStateIdentifier: queryIdentifier, hasNextPage, }; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsCompleted.ts b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsCompleted.ts new file mode 100644 index 000000000000..f0f89bfb2e6f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsCompleted.ts @@ -0,0 +1,53 @@ +import { useRecoilState } from 'recoil'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; +import { cursorFamilyState } from '@/object-record/states/cursorFamilyState'; +import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState'; +import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted'; +import { isDefined } from '~/utils/isDefined'; + +export const useHandleFindManyRecordsCompleted = ({ + queryIdentifier, + onCompleted, + objectMetadataItem, +}: { + queryIdentifier: string; + objectMetadataItem: ObjectMetadataItem; + onCompleted?: OnFindManyRecordsCompleted; +}) => { + const [, setLastCursor] = useRecoilState(cursorFamilyState(queryIdentifier)); + + const [, setHasNextPage] = useRecoilState( + hasNextPageFamilyState(queryIdentifier), + ); + + const handleFindManyRecordsCompleted = ( + data: RecordGqlOperationFindManyResult, + ) => { + if (!isDefined(data)) { + onCompleted?.([]); + } + + const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; + + const records = getRecordsFromRecordConnection({ + recordConnection: data?.[objectMetadataItem.namePlural], + }) as T[]; + + onCompleted?.(records, { + pageInfo, + totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, + }); + + if (isDefined(data?.[objectMetadataItem.namePlural])) { + setLastCursor(pageInfo.endCursor ?? ''); + setHasNextPage(pageInfo.hasNextPage ?? false); + } + }; + + return { + handleFindManyRecordsCompleted, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts new file mode 100644 index 000000000000..1b5a8cae64ef --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts @@ -0,0 +1,34 @@ +import { ApolloError } from '@apollo/client'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { logError } from '~/utils/logError'; + +export const useHandleFindManyRecordsError = ({ + handleError, + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; + handleError?: (error?: Error) => void; +}) => { + const { enqueueSnackBar } = useSnackBar(); + + const handleFindManyRecordsError = (error: ApolloError) => { + logError( + `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, + { + variant: SnackBarVariant.Error, + }, + ); + handleError?.(error); + }; + + return { + handleFindManyRecordsError, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index 0c2a4b6422df..45f77e1bf052 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -1,18 +1,18 @@ import { useLazyQuery } from '@apollo/client'; -import { useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; +import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; +import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted'; +import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; +import { cursorFamilyState } from '@/object-record/states/cursorFamilyState'; +import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useFindManyRecordsState } from '@/object-record/utils/useFindManyRecordsUtils'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { isDefined } from '~/utils/isDefined'; -import { logError } from '~/utils/logError'; +import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; type UseLazyFindManyRecordsParams = Omit< UseFindManyRecordsParams, @@ -24,9 +24,10 @@ export const useLazyFindManyRecords = ({ filter, orderBy, limit, - onCompleted, recordGqlFields, fetchPolicy, + onCompleted, + onError, }: UseLazyFindManyRecordsParams) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -37,9 +38,30 @@ export const useLazyFindManyRecords = ({ recordGqlFields, }); - const { enqueueSnackBar } = useSnackBar(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { handleFindManyRecordsError } = useHandleFindManyRecordsError({ + objectMetadataItem, + handleError: onError, + }); + + const queryIdentifier = getQueryIdentifier({ + objectNameSingular, + filter, + orderBy, + limit, + }); + + const [, setHasNextPage] = useRecoilState( + hasNextPageFamilyState(queryIdentifier), + ); + + const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({ + objectMetadataItem, + queryIdentifier, + onCompleted, + }); + const [findManyRecords, { data, loading, error, fetchMore }] = useLazyQuery(findManyRecordsQuery, { variables: { @@ -48,59 +70,52 @@ export const useLazyFindManyRecords = ({ orderBy, }, fetchPolicy: fetchPolicy, - onCompleted: (data) => { - if (!isDefined(data)) { - onCompleted?.([]); + onCompleted: handleFindManyRecordsCompleted, + onError: handleFindManyRecordsError, + }); + + const { fetchMoreRecords, totalCount, records } = + useFetchMoreRecordsWithPagination({ + objectNameSingular, + filter, + orderBy, + limit, + onCompleted, + fetchMore, + data, + error, + objectMetadataItem, + }); + + const findManyRecordsLazy = useRecoilCallback( + ({ set }) => + async () => { + if (!currentWorkspaceMember) { + return null; } - const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo; + const result = await findManyRecords(); - const records = getRecordsFromRecordConnection({ - recordConnection: data?.[objectMetadataItem.namePlural], - }) as T[]; + const hasNextPage = + result?.data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage ?? + false; - onCompleted?.(records, { - pageInfo, - totalCount: data?.[objectMetadataItem.namePlural]?.totalCount, - }); + const lastCursor = + result?.data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor ?? + ''; - if (isDefined(data?.[objectMetadataItem.namePlural])) { - setLastCursor(pageInfo.endCursor ?? ''); - setHasNextPage(pageInfo.hasNextPage ?? false); - } - }, - onError: (error) => { - logError( - `useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`, - { - variant: SnackBarVariant.Error, - }, - ); - }, - }); + set(hasNextPageFamilyState(queryIdentifier), hasNextPage); + set(cursorFamilyState(queryIdentifier), lastCursor); - const { - findManyQueryStateIdentifier, - setLastCursor, - setHasNextPage, - fetchMoreRecords, - totalCount, - records, - } = useFindManyRecordsState({ - objectNameSingular, - filter, - orderBy, - limit, - onCompleted, - fetchMore, - data, - error, - objectMetadataItem, - }); + return result; + }, + [ + queryIdentifier, + currentWorkspaceMember, + findManyRecords, + objectMetadataItem, + ], + ); return { objectMetadataItem, @@ -109,7 +124,7 @@ export const useLazyFindManyRecords = ({ loading, error, fetchMoreRecords, - queryStateIdentifier: findManyQueryStateIdentifier, - findManyRecords: currentWorkspaceMember ? findManyRecords : () => {}, + queryStateIdentifier: queryIdentifier, + findManyRecords: findManyRecordsLazy, }; }; diff --git a/packages/twenty-front/src/modules/object-record/types/OnFindManyRecordsCompleted.ts b/packages/twenty-front/src/modules/object-record/types/OnFindManyRecordsCompleted.ts new file mode 100644 index 000000000000..97003380167d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/types/OnFindManyRecordsCompleted.ts @@ -0,0 +1,9 @@ +import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; + +export type OnFindManyRecordsCompleted = ( + records: T[], + options?: { + pageInfo?: RecordGqlConnection['pageInfo']; + totalCount?: number; + }, +) => void; diff --git a/packages/twenty-front/src/modules/object-record/types/UseFindManyRecordsParams.ts b/packages/twenty-front/src/modules/object-record/types/UseFindManyRecordsParams.ts new file mode 100644 index 000000000000..1a15c6040ecc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/types/UseFindManyRecordsParams.ts @@ -0,0 +1,14 @@ +import { WatchQueryFetchPolicy } from '@apollo/client'; + +import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; +import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted'; + +export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & + RecordGqlOperationVariables & { + onCompleted?: OnFindManyRecordsCompleted; + skip?: boolean; + recordGqlFields?: RecordGqlOperationGqlRecordFields; + fetchPolicy?: WatchQueryFetchPolicy; + }; diff --git a/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts b/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts new file mode 100644 index 000000000000..20fc7d79f921 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts @@ -0,0 +1,11 @@ +import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; + +export const getQueryIdentifier = ({ + objectNameSingular, + filter, + orderBy, + limit, +}: RecordGqlOperationVariables & { + objectNameSingular: string; +}) => + objectNameSingular + JSON.stringify(filter) + JSON.stringify(orderBy) + limit; diff --git a/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts b/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts deleted file mode 100644 index 4b197d05b268..000000000000 --- a/packages/twenty-front/src/modules/object-record/utils/useFindManyRecordsUtils.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { - ApolloError, - ApolloQueryResult, - FetchMoreQueryOptions, - OperationVariables, - WatchQueryFetchPolicy, -} from '@apollo/client'; -import { isNonEmptyArray } from '@apollo/client/utilities'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; -import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; -import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; -import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { isDefined } from '~/utils/isDefined'; -import { logError } from '~/utils/logError'; -import { capitalize } from '~/utils/string/capitalize'; - -import { cursorFamilyState } from '../states/cursorFamilyState'; -import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState'; -import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState'; - -export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & - RecordGqlOperationVariables & { - onCompleted?: ( - records: T[], - options?: { - pageInfo?: RecordGqlConnection['pageInfo']; - totalCount?: number; - }, - ) => void; - skip?: boolean; - recordGqlFields?: RecordGqlOperationGqlRecordFields; - fetchPolicy?: WatchQueryFetchPolicy; - }; - -type UseFindManyRecordsStateParams< - T, - TData = RecordGqlOperationFindManyResult, -> = Omit< - UseFindManyRecordsParams, - 'skip' | 'recordGqlFields' | 'fetchPolicy' -> & { - data: RecordGqlOperationFindManyResult | undefined; - error: ApolloError | undefined; - fetchMore< - TFetchData = TData, - TFetchVars extends OperationVariables = OperationVariables, - >( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: TData, - options: { - fetchMoreResult: TFetchData; - variables: TFetchVars; - }, - ) => TData; - }, - ): Promise>; - objectMetadataItem: ObjectMetadataItem; -}; - -export const useFindManyRecordsState = ({ - objectNameSingular, - filter, - orderBy, - limit, - onCompleted, - data, - error, - fetchMore, - objectMetadataItem, -}: UseFindManyRecordsStateParams) => { - const findManyQueryStateIdentifier = - objectNameSingular + - JSON.stringify(filter) + - JSON.stringify(orderBy) + - limit; - - const [lastCursor, setLastCursor] = useRecoilState( - cursorFamilyState(findManyQueryStateIdentifier), - ); - - const [hasNextPage, setHasNextPage] = useRecoilState( - hasNextPageFamilyState(findManyQueryStateIdentifier), - ); - - const setIsFetchingMoreObjects = useSetRecoilState( - isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier), - ); - - const { enqueueSnackBar } = useSnackBar(); - - const fetchMoreRecords = useCallback(async () => { - // Remote objects does not support hasNextPage. We cannot rely on it to fetch more records. - if (hasNextPage || (!isAggregationEnabled(objectMetadataItem) && !error)) { - setIsFetchingMoreObjects(true); - - try { - await fetchMore({ - variables: { - filter, - orderBy, - lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined, - }, - updateQuery: (prev, { fetchMoreResult }) => { - const previousEdges = prev?.[objectMetadataItem.namePlural]?.edges; - const nextEdges = - fetchMoreResult?.[objectMetadataItem.namePlural]?.edges; - - let newEdges: RecordGqlEdge[] = previousEdges ?? []; - - if (isNonEmptyArray(nextEdges)) { - newEdges = filterUniqueRecordEdgesByCursor([ - ...newEdges, - ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? - []), - ]); - } - - const pageInfo = - fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; - - if (isDefined(data?.[objectMetadataItem.namePlural])) { - setLastCursor(pageInfo.endCursor ?? ''); - setHasNextPage(pageInfo.hasNextPage ?? false); - } - - const records = getRecordsFromRecordConnection({ - recordConnection: { - edges: newEdges, - pageInfo, - }, - }) as T[]; - - onCompleted?.(records, { - pageInfo, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount, - }); - - return Object.assign({}, prev, { - [objectMetadataItem.namePlural]: { - __typename: `${capitalize( - objectMetadataItem.nameSingular, - )}Connection`, - edges: newEdges, - pageInfo: - fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural].totalCount, - }, - } as RecordGqlOperationFindManyResult); - }, - }); - } catch (error) { - logError( - `fetchMoreObjects for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`, - { - variant: SnackBarVariant.Error, - }, - ); - } finally { - setIsFetchingMoreObjects(false); - } - } - }, [ - hasNextPage, - objectMetadataItem, - error, - setIsFetchingMoreObjects, - fetchMore, - filter, - orderBy, - lastCursor, - data, - onCompleted, - setLastCursor, - setHasNextPage, - enqueueSnackBar, - ]); - - const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount; - - const records = useMemo( - () => - data?.[objectMetadataItem.namePlural] - ? getRecordsFromRecordConnection({ - recordConnection: data?.[objectMetadataItem.namePlural], - }) - : ([] as T[]), - - [data, objectMetadataItem.namePlural], - ); - - return { - findManyQueryStateIdentifier, - setLastCursor, - setHasNextPage, - fetchMoreRecords, - totalCount, - records, - hasNextPage, - }; -}; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts index f37a9b2e84cb..49dd7a50fbec 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { IconComponent } from 'twenty-ui'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; @@ -8,5 +9,5 @@ export type ActionBarEntry = { accent?: MenuItemAccent; onClick?: () => void; subActions?: ActionBarEntry[]; - ConfirmationModal?: JSX.Element; + ConfirmationModal?: ReactNode; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts index d56820d6ebe1..c5fc0394f3c4 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts @@ -1,3 +1,4 @@ +import { MouseEvent, ReactNode } from 'react'; import { IconComponent } from 'twenty-ui'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; @@ -6,5 +7,6 @@ export type ContextMenuEntry = { label: string; Icon: IconComponent; accent?: MenuItemAccent; - onClick: () => void; + onClick: (event?: MouseEvent) => void; + ConfirmationModal?: ReactNode; }; From d5f863a7dda765471fea396331529b1ca86ad902 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 10:22:22 +0200 Subject: [PATCH 21/33] WIP --- .../modules/object-record/hooks/useLazyFindManyRecords.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index 45f77e1bf052..7d9caccbfe1f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -1,5 +1,5 @@ import { useLazyQuery } from '@apollo/client'; -import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -52,10 +52,6 @@ export const useLazyFindManyRecords = ({ limit, }); - const [, setHasNextPage] = useRecoilState( - hasNextPageFamilyState(queryIdentifier), - ); - const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({ objectMetadataItem, queryIdentifier, From 6e6c7f62611dad56c50729991e2c285849edd592 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 10:57:57 +0200 Subject: [PATCH 22/33] WIP --- .../hooks/useLazyFetchAllRecordIds.ts | 117 ++++++++++++++++++ .../hooks/useLazyFindManyRecords.ts | 3 +- .../hooks/useRecordActionBar.tsx | 112 ++++++++--------- .../options/hooks/useDeleteTableData.ts | 30 +++-- .../options/hooks/useTableData.ts | 21 ++-- .../states/recordDeleteProgressState.ts | 0 6 files changed, 204 insertions(+), 79 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/options/states/recordDeleteProgressState.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts new file mode 100644 index 000000000000..e67aedaf35cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts @@ -0,0 +1,117 @@ +import { useState } from 'react'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { isDefined } from '~/utils/isDefined'; + +type UseLazyFetchAllRecordIdsParams = Omit< + UseFindManyRecordsParams, + 'skip' +>; + +type FetchAllRecordIdsStatus = 'idle' | 'loading' | 'success' | 'error'; + +export const useLazyFetchAllRecordIds = ({ + objectNameSingular, + filter, + orderBy, + callback, +}: UseLazyFetchAllRecordIdsParams & { + callback?: (recordIds: string[]) => void; +}) => { + const [fetchProgress, setFetchProgress] = useState(0); + const [fetchStatus, setFetchStatus] = + useState('idle'); + + const { fetchMore, findManyRecords } = useLazyFindManyRecords({ + objectNameSingular, + filter, + orderBy, + recordGqlFields: { id: true }, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const fetchAllRecordIds = async () => { + setFetchStatus('loading'); + setFetchProgress(0); + + if (!isDefined(findManyRecords)) { + return; + } + + const findManyRecordsDataResult = await findManyRecords(); + + const firstQueryResult = + findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural]; + + const totalCount = firstQueryResult?.totalCount ?? 1; + + const recordsCount = firstQueryResult?.edges.length ?? 0; + + const progress = Math.round((recordsCount / totalCount) * 100); + + setFetchProgress(progress); + + const recordIdSet = new Set( + firstQueryResult?.edges?.map((edge) => edge.node.id) ?? [], + ); + + console.log({ + records: recordIdSet, + queryResult: firstQueryResult, + totalCount, + progress, + }); + + const remainingCount = totalCount - recordsCount; + + const remainingPages = Math.ceil(remainingCount / 30); + + let lastCursor = firstQueryResult?.pageInfo.endCursor ?? ''; + + for (let i = 0; i < remainingPages; i++) { + const rawResult = await fetchMore?.({ + variables: { + lastCursor: lastCursor, + }, + }); + + const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural]; + + for (const edge of fetchMoreResult.edges) { + recordIdSet.add(edge.node.id); + } + + lastCursor = fetchMoreResult.pageInfo.endCursor ?? ''; + + const newProgress = Math.round((recordIdSet.size / totalCount) * 100); + + setFetchProgress(newProgress); + + console.log({ + lastCursor, + newProgress, + fetchMoreResult, + }); + } + + setFetchStatus('success'); + setFetchProgress(100); + + const recordIds = Array.from(recordIdSet); + + callback?.(recordIds); + + return recordIds; + }; + + return { + fetchAllRecordIds, + fetchProgress, + fetchStatus, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index 7d9caccbfe1f..4a4741916d5c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -119,7 +119,8 @@ export const useLazyFindManyRecords = ({ totalCount, loading, error, - fetchMoreRecords, + fetchMore, + fetchMoreRecordsWithPagination: fetchMoreRecords, queryStateIdentifier: queryIdentifier, findManyRecords: findManyRecordsLazy, }; 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 2ba5d615cc04..b44d486b3cf0 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 @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { IconClick, IconFileExport, @@ -11,9 +11,9 @@ import { IconTrash, } from 'twenty-ui'; -import { apiConfigState } from '@/client-config/states/apiConfigState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord'; import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; import { @@ -52,9 +52,6 @@ export const useRecordActionBar = ({ objectNameSingular: objectMetadataItem.nameSingular, }); - const apiConfig = useRecoilValue(apiConfigState); - const maxRecords = apiConfig?.mutationMaximumAffectedRecords; - const handleFavoriteButtonClick = useRecoilCallback( ({ snapshot }) => () => { @@ -96,19 +93,21 @@ export const useRecordActionBar = ({ recordIndexId: objectMetadataItem.namePlural, }; - const { deleteTableData } = useDeleteTableData(baseTableDataParams); + const { deleteTableData, deleteProgress } = + useDeleteTableData(baseTableDataParams); + + const handleDeleteClick = () => { + // selectedRecordIds.forEach((recordId) => { + // const foundFavorite = favorites?.find( + // (favorite) => favorite.recordId === recordId, + // ); + // if (foundFavorite !== undefined) { + // deleteFavorite(foundFavorite.id); + // } + // }); - const handleDeleteClick = useCallback(async () => { - selectedRecordIds.forEach((recordId) => { - const foundFavorite = favorites?.find( - (favorite) => favorite.recordId === recordId, - ); - if (foundFavorite !== undefined) { - deleteFavorite(foundFavorite.id); - } - }); deleteTableData(); - }, [deleteFavorite, deleteTableData, favorites, selectedRecordIds]); + }; const handleExecuteQuickActionOnClick = useCallback(async () => { callback?.(); @@ -126,7 +125,7 @@ export const useRecordActionBar = ({ const isRemoteObject = objectMetadataItem.isRemote; - const baseActions: ContextMenuEntry[] = useMemo( + const menuActions: ContextMenuEntry[] = useMemo( () => [ { label: displayedExportProgress(progress), @@ -138,38 +137,40 @@ export const useRecordActionBar = ({ [download, progress], ); - const recordsNum = numSelected ?? selectedRecordIds.length; - - const deletionActions: ContextMenuEntry[] = useMemo( - () => - maxRecords !== undefined && recordsNum <= maxRecords - ? [ - { - label: 'Delete', - Icon: IconTrash, - accent: 'danger', - onClick: () => setIsDeleteRecordsModalOpen(true), - ConfirmationModal: ( - handleDeleteClick()} - deleteButtonText={`Delete ${ - recordsNum > 1 ? 'Records' : 'Record' - }`} - /> - ), - }, - ] - : [], - [maxRecords, isDeleteRecordsModalOpen, recordsNum, handleDeleteClick], - ); + const numberOfSelectedRecords = numSelected ?? selectedRecordIds.length; + const canDelete = + !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; + + if (canDelete) { + menuActions.push({ + label: 'Delete', + Icon: IconTrash, + accent: 'danger', + onClick: (event) => { + console.log('asd'); + event?.stopPropagation(); + event?.preventDefault(); + handleDeleteClick(); + setIsDeleteRecordsModalOpen(true); + }, + ConfirmationModal: ( + handleDeleteClick()} + deleteButtonText={`Delete ${ + numberOfSelectedRecords > 1 ? 'Records' : 'Record' + }`} + /> + ), + }); + } const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( 'IS_QUICK_ACTIONS_ENABLED', @@ -184,8 +185,7 @@ export const useRecordActionBar = ({ return { setContextMenuEntries: useCallback(() => { setContextMenuEntries([ - ...(isRemoteObject ? [] : deletionActions), - ...baseActions, + ...menuActions, ...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected ? [ { @@ -206,8 +206,7 @@ export const useRecordActionBar = ({ : []), ]); }, [ - baseActions, - deletionActions, + menuActions, handleFavoriteButtonClick, hasOnlyOneRecordSelected, isFavorite, @@ -236,15 +235,12 @@ export const useRecordActionBar = ({ }, ] : []), - ...(isRemoteObject ? [] : deletionActions), - ...baseActions, + ...menuActions, ]); }, [ - baseActions, + menuActions, dataExecuteQuickActionOnmentEnabled, - deletionActions, handleExecuteQuickActionOnClick, - isRemoteObject, setActionBarEntriesState, ]), }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts index 204ca426ef99..e794028790a7 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -1,11 +1,9 @@ import { useCallback } from 'react'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useLazyFetchAllRecordIds } from '@/object-record/hooks/useLazyFetchAllRecordIds'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { - useTableData, - UseTableDataOptions, -} from '@/object-record/record-index/options/hooks/useTableData'; +import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -19,6 +17,10 @@ export const useDeleteTableData = ({ pageSize = 30, recordIndexId, }: UseDeleteTableDataOptions) => { + const { fetchAllRecordIds, fetchProgress } = useLazyFetchAllRecordIds({ + objectNameSingular, + }); + const { resetTableRowSelection } = useRecordTable({ recordTableId: recordIndexId, }); @@ -38,14 +40,16 @@ export const useDeleteTableData = ({ [deleteManyRecords, resetTableRowSelection], ); - const { getTableData: deleteTableData } = useTableData({ - delayMs, - maximumRequests, - objectNameSingular, - pageSize, - recordIndexId, - callback: deleteRecords, - }); + // const { getTableData: deleteTableData } = useTableData({ + // delayMs, + // maximumRequests, + // objectNameSingular, + // pageSize, + // recordIndexId, + // callback: deleteRecords, + // }); + + const deleteProgress = fetchProgress / 2; - return { deleteTableData }; + return { deleteTableData: fetchAllRecordIds, deleteProgress }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts index 4b34bc2d5813..2042723ebb43 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -114,11 +114,16 @@ export const useTableData = ({ ? unselectedFindManyParams : findManyRecordsParams; - const { findManyRecords, totalCount, records, fetchMoreRecords, loading } = - useLazyFindManyRecords({ - ...usedFindManyParams, - limit: pageSize, - }); + const { + findManyRecords, + totalCount, + records, + fetchMoreRecordsWithPagination, + loading, + } = useLazyFindManyRecords({ + ...usedFindManyParams, + limit: pageSize, + }); useEffect(() => { const MAXIMUM_REQUESTS = isDefined(totalCount) @@ -128,7 +133,9 @@ export const useTableData = ({ const fetchNextPage = async () => { setInflight(true); setPreviousRecordCount(records.length); - await fetchMoreRecords(); + + await fetchMoreRecordsWithPagination(); + setPageCount((state) => state + 1); setProgress({ exportedRecordCount: records.length, @@ -170,7 +177,7 @@ export const useTableData = ({ } }, [ delayMs, - fetchMoreRecords, + fetchMoreRecordsWithPagination, inflight, isDownloading, pageCount, diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/states/recordDeleteProgressState.ts b/packages/twenty-front/src/modules/object-record/record-index/options/states/recordDeleteProgressState.ts new file mode 100644 index 000000000000..e69de29bb2d1 From b676c8b70c35cf83b4cec437a97b68416ee132d6 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 17:20:15 +0200 Subject: [PATCH 23/33] WIP --- .../hooks/useDeleteManyRecords.ts | 10 +- ...llRecordIds.ts => useFetchAllRecordIds.ts} | 44 ++------- .../hooks/useRecordActionBar.tsx | 99 ++++++++++--------- .../RecordIndexTableContainerEffect.tsx | 6 +- .../options/hooks/useDeleteTableData.ts | 82 ++++++++------- .../options/hooks/useTableData.ts | 2 +- .../components/RecordTableActionBar.tsx | 2 +- .../components/RecordTableInternalEffect.tsx | 6 +- .../hooks/internal/useLeaveTableFocus.ts | 5 + .../hooks/internal/useRecordTableStates.ts | 2 +- .../internal/useResetTableRowSelection.ts | 11 ++- .../internal/useSetAllRowSelectedState.ts | 7 +- .../hooks/internal/useSetRecordTableData.ts | 2 +- .../record-table/hooks/useRecordTable.ts | 2 + .../action-bar/types/ActionBarEntry.ts | 12 +-- .../context-menu/types/ContextMenuEntry.ts | 2 +- 16 files changed, 147 insertions(+), 147 deletions(-) rename packages/twenty-front/src/modules/object-record/hooks/{useLazyFetchAllRecordIds.ts => useFetchAllRecordIds.ts} (71%) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 99df00f0febc..a9af8e812318 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -8,6 +8,7 @@ import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMu import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { isDefined } from '~/utils/isDefined'; +import { sleep } from '~/utils/sleep'; import { capitalize } from '~/utils/string/capitalize'; type useDeleteOneRecordProps = { @@ -17,6 +18,7 @@ type useDeleteOneRecordProps = { type DeleteManyRecordsOptions = { skipOptimisticEffect?: boolean; + delayInMsBetweenRequests?: number; }; export const useDeleteManyRecords = ({ @@ -58,10 +60,6 @@ export const useDeleteManyRecords = ({ (batchIndex + 1) * DEFAULT_MUTATION_BATCH_SIZE, ); - console.log({ - batchIds, - }); - const deletedRecordsResponse = await apolloClient.mutate({ mutation: deleteManyRecordsMutation, variables: { @@ -99,6 +97,10 @@ export const useDeleteManyRecords = ({ deletedRecordsResponse.data?.[mutationResponseField] ?? []; deletedRecords.push(...deletedRecordsForThisBatch); + + if (isDefined(options?.delayInMsBetweenRequests)) { + await sleep(options.delayInMsBetweenRequests); + } } return deletedRecords; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts similarity index 71% rename from packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts rename to packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts index e67aedaf35cc..895aab6b8e9f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFetchAllRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; @@ -10,20 +8,11 @@ type UseLazyFetchAllRecordIdsParams = Omit< 'skip' >; -type FetchAllRecordIdsStatus = 'idle' | 'loading' | 'success' | 'error'; - -export const useLazyFetchAllRecordIds = ({ +export const useFetchAllRecordIds = ({ objectNameSingular, filter, orderBy, - callback, -}: UseLazyFetchAllRecordIdsParams & { - callback?: (recordIds: string[]) => void; -}) => { - const [fetchProgress, setFetchProgress] = useState(0); - const [fetchStatus, setFetchStatus] = - useState('idle'); - +}: UseLazyFetchAllRecordIdsParams) => { const { fetchMore, findManyRecords } = useLazyFindManyRecords({ objectNameSingular, filter, @@ -36,11 +25,11 @@ export const useLazyFetchAllRecordIds = ({ }); const fetchAllRecordIds = async () => { - setFetchStatus('loading'); - setFetchProgress(0); - + console.log({ + objectNameSingular, + }); if (!isDefined(findManyRecords)) { - return; + return []; } const findManyRecordsDataResult = await findManyRecords(); @@ -52,21 +41,10 @@ export const useLazyFetchAllRecordIds = ({ const recordsCount = firstQueryResult?.edges.length ?? 0; - const progress = Math.round((recordsCount / totalCount) * 100); - - setFetchProgress(progress); - const recordIdSet = new Set( firstQueryResult?.edges?.map((edge) => edge.node.id) ?? [], ); - console.log({ - records: recordIdSet, - queryResult: firstQueryResult, - totalCount, - progress, - }); - const remainingCount = totalCount - recordsCount; const remainingPages = Math.ceil(remainingCount / 30); @@ -90,28 +68,18 @@ export const useLazyFetchAllRecordIds = ({ const newProgress = Math.round((recordIdSet.size / totalCount) * 100); - setFetchProgress(newProgress); - console.log({ lastCursor, newProgress, fetchMoreResult, }); } - - setFetchStatus('success'); - setFetchProgress(100); - const recordIds = Array.from(recordIdSet); - callback?.(recordIds); - return recordIds; }; return { fetchAllRecordIds, - fetchProgress, - fetchStatus, }; }; 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 b44d486b3cf0..10695af716c7 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 @@ -1,5 +1,5 @@ -import { useCallback, useMemo, useState } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; +import { useCallback, useMemo, useState } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { IconClick, @@ -93,10 +93,9 @@ export const useRecordActionBar = ({ recordIndexId: objectMetadataItem.namePlural, }; - const { deleteTableData, deleteProgress } = - useDeleteTableData(baseTableDataParams); + const { deleteTableData } = useDeleteTableData(baseTableDataParams); - const handleDeleteClick = () => { + const handleDeleteClick = useCallback(() => { // selectedRecordIds.forEach((recordId) => { // const foundFavorite = favorites?.find( // (favorite) => favorite.recordId === recordId, @@ -107,7 +106,7 @@ export const useRecordActionBar = ({ // }); deleteTableData(); - }; + }, [deleteTableData]); const handleExecuteQuickActionOnClick = useCallback(async () => { callback?.(); @@ -125,52 +124,58 @@ export const useRecordActionBar = ({ const isRemoteObject = objectMetadataItem.isRemote; - const menuActions: ContextMenuEntry[] = useMemo( - () => [ - { - label: displayedExportProgress(progress), - Icon: IconFileExport, - accent: 'default', - onClick: () => download(), - }, - ], - [download, progress], - ); - const numberOfSelectedRecords = numSelected ?? selectedRecordIds.length; const canDelete = !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; - if (canDelete) { - menuActions.push({ - label: 'Delete', - Icon: IconTrash, - accent: 'danger', - onClick: (event) => { - console.log('asd'); - event?.stopPropagation(); - event?.preventDefault(); - handleDeleteClick(); - setIsDeleteRecordsModalOpen(true); - }, - ConfirmationModal: ( - handleDeleteClick()} - deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' - }`} - /> - ), - }); - } + const menuActions: ContextMenuEntry[] = useMemo( + () => + [ + { + label: displayedExportProgress(progress), + Icon: IconFileExport, + accent: 'default', + onClick: () => download(), + } satisfies ContextMenuEntry, + canDelete + ? ({ + label: 'Delete', + Icon: IconTrash, + accent: 'danger', + onClick: () => { + setIsDeleteRecordsModalOpen(true); + handleDeleteClick(); + }, + ConfirmationModal: ( + handleDeleteClick()} + deleteButtonText={`Delete ${ + numberOfSelectedRecords > 1 ? 'Records' : 'Record' + }`} + /> + ), + } satisfies ContextMenuEntry) + : undefined, + ].filter(isDefined), + [ + download, + progress, + canDelete, + handleDeleteClick, + isDeleteRecordsModalOpen, + numberOfSelectedRecords, + ], + ); const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( 'IS_QUICK_ACTIONS_ENABLED', diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index 928d6b2aad15..b8bde9a925d0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -47,8 +47,10 @@ export const RecordIndexTableContainerEffect = ({ setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const { tableRowIdsState, hasUserSelectedAllRowState } = - useRecordTableStates(recordTableId); + const { + tableRowIdsState, + hasUserSelectedAllRowsState: hasUserSelectedAllRowState, + } = useRecordTableStates(recordTableId); const { entityCountInCurrentViewState } = useViewStates(recordTableId); const entityCountInCurrentView = useRecoilValue( diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts index e794028790a7..7dca26710695 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -1,55 +1,71 @@ -import { useCallback } from 'react'; - import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; -import { useLazyFetchAllRecordIds } from '@/object-record/hooks/useLazyFetchAllRecordIds'; -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { useRecoilValue } from 'recoil'; type UseDeleteTableDataOptions = Omit; export const useDeleteTableData = ({ - delayMs, - maximumRequests = 100, objectNameSingular, - pageSize = 30, recordIndexId, }: UseDeleteTableDataOptions) => { - const { fetchAllRecordIds, fetchProgress } = useLazyFetchAllRecordIds({ + const { fetchAllRecordIds } = useFetchAllRecordIds({ objectNameSingular, }); - const { resetTableRowSelection } = useRecordTable({ + const { + resetTableRowSelection, + selectedRowIdsSelector, + hasUserSelectedAllRowState, + } = useRecordTable({ recordTableId: recordIndexId, }); - const { deleteManyRecords } = useDeleteManyRecords({ objectNameSingular }); + const tableRowIds = useRecoilValue( + tableRowIdsComponentState({ + scopeId: getScopeIdFromComponentId(recordIndexId), + }), + ); + + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular, + }); - const deleteRecords = useCallback( - async ( - rows: ObjectRecord[], - _columns: ColumnDefinition[], - ) => { - const recordIds = rows.map((record) => record.id); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - await deleteManyRecords(recordIds); - resetTableRowSelection(); - }, - [deleteManyRecords, resetTableRowSelection], - ); + const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + + const deleteRecords = async () => { + let recordIdsToDelete = selectedRowIds; + + if (hasUserSelectedAllRow) { + const allRecordIds = await fetchAllRecordIds(); + + const unselectedRecordIds = tableRowIds.filter( + (recordId) => !selectedRowIds.includes(recordId), + ); + + recordIdsToDelete = allRecordIds.filter( + (recordId) => !unselectedRecordIds.includes(recordId), + ); + + console.log({ + unselectedRecordIds, + recordIdsToDelete, + }); + } + + console.log({ recordIdsToDelete }); - // const { getTableData: deleteTableData } = useTableData({ - // delayMs, - // maximumRequests, - // objectNameSingular, - // pageSize, - // recordIndexId, - // callback: deleteRecords, - // }); + await deleteManyRecords(recordIdsToDelete, { + delayInMsBetweenRequests: 25, + }); - const deleteProgress = fetchProgress / 2; + resetTableRowSelection(); + }; - return { deleteTableData: fetchAllRecordIds, deleteProgress }; + return { deleteTableData: deleteRecords }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts index 2042723ebb43..d7c0ed62db56 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -55,7 +55,7 @@ export const useTableData = ({ visibleTableColumnsSelector, selectedRowIdsSelector, tableRowIdsState, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState: hasUserSelectedAllRowState, } = useRecordTableStates(recordIndexId); const columns = useRecoilValue(visibleTableColumnsSelector()); diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index cc9d99c1a6f1..5dac00df5905 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -12,7 +12,7 @@ export const RecordTableActionBar = ({ const { selectedRowIdsSelector, tableRowIdsState, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState: hasUserSelectedAllRowState, } = useRecordTableStates(recordTableId); const { entityCountInCurrentViewState } = useViewStates(recordTableId); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx index 9a3f062e0f2c..77e2f228794e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx @@ -5,7 +5,10 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; -import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { + ClickOutsideMode, + useListenClickOutsideByClassName, +} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; type RecordTableInternalEffectProps = { recordTableId: string; @@ -30,6 +33,7 @@ export const RecordTableInternalEffect = ({ callback: () => { leaveTableFocus(); }, + mode: ClickOutsideMode.comparePixels, }); useScopedHotkeys( diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 34fda8a4ef3c..d1d82fa1144a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -15,6 +15,8 @@ export const useLeaveTableFocus = (recordTableId?: string) => { const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode(recordTableId); + const setHasUserSelectedAllRows = useSetHasUserSelectedAllRows(recordTableId); + const selectAllRows = useSetHasUserSelectedAllRows(recordTableId); const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); @@ -22,6 +24,7 @@ export const useLeaveTableFocus = (recordTableId?: string) => { return useRecoilCallback( ({ snapshot }) => () => { + console.log('useLeaveTableFocus'); const isSoftFocusActive = getSnapshotValue( snapshot, isSoftFocusActiveState, @@ -41,6 +44,7 @@ export const useLeaveTableFocus = (recordTableId?: string) => { closeCurrentCellInEditMode(); disableSoftFocus(); + setHasUserSelectedAllRows(false); selectAllRows(false); }, [ @@ -48,6 +52,7 @@ export const useLeaveTableFocus = (recordTableId?: string) => { disableSoftFocus, isSoftFocusActiveState, selectAllRows, + setHasUserSelectedAllRows, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 1e178da5bd7a..1fbc4a8bd810 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -109,7 +109,7 @@ export const useRecordTableStates = (recordTableId?: string) => { isRowSelectedComponentFamilyState, scopeId, ), - hasUserSelectedAllRowState: extractComponentState( + hasUserSelectedAllRowsState: extractComponentState( hasUserSelectedAllRowsComponentState, scopeId, ), diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index 7e86f581909c..1b6263739111 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -4,8 +4,11 @@ import { useRecordTableStates } from '@/object-record/record-table/hooks/interna import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; export const useResetTableRowSelection = (recordTableId?: string) => { - const { tableRowIdsState, isRowSelectedFamilyState } = - useRecordTableStates(recordTableId); + const { + tableRowIdsState, + isRowSelectedFamilyState, + hasUserSelectedAllRowsState, + } = useRecordTableStates(recordTableId); return useRecoilCallback( ({ snapshot, set }) => @@ -15,7 +18,9 @@ export const useResetTableRowSelection = (recordTableId?: string) => { for (const rowId of tableRowIds) { set(isRowSelectedFamilyState(rowId), false); } + + set(hasUserSelectedAllRowsState, false); }, - [tableRowIdsState, isRowSelectedFamilyState], + [tableRowIdsState, isRowSelectedFamilyState, hasUserSelectedAllRowsState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetAllRowSelectedState.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetAllRowSelectedState.ts index 4544c4b71f95..5df32d9006d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetAllRowSelectedState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetAllRowSelectedState.ts @@ -3,14 +3,13 @@ import { useRecoilCallback } from 'recoil'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; export const useSetHasUserSelectedAllRows = (recordTableId?: string) => { - const { hasUserSelectedAllRowState: hasUserSelectedAllRowFamilyState } = - useRecordTableStates(recordTableId); + const { hasUserSelectedAllRowsState } = useRecordTableStates(recordTableId); return useRecoilCallback( ({ set }) => (selected: boolean) => { - set(hasUserSelectedAllRowFamilyState, selected); + set(hasUserSelectedAllRowsState, selected); }, - [hasUserSelectedAllRowFamilyState], + [hasUserSelectedAllRowsState], ); }; 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 64ca715b896a..127436bb4a02 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 @@ -19,7 +19,7 @@ export const useSetRecordTableData = ({ tableRowIdsState, numberOfTableRowsState, isRowSelectedFamilyState, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState: hasUserSelectedAllRowState, } = useRecordTableStates(recordTableId); return useRecoilCallback( 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 785baebe165c..10474b31bafa 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 @@ -45,6 +45,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { onToggleColumnFilterState, onToggleColumnSortState, pendingRecordIdState, + hasUserSelectedAllRowsState: hasUserSelectedAllRowState, } = useRecordTableStates(recordTableId); const setAvailableTableColumns = useRecoilCallback( @@ -226,5 +227,6 @@ export const useRecordTable = (props?: useRecordTableProps) => { setOnToggleColumnFilter, setOnToggleColumnSort, setPendingRecordId, + hasUserSelectedAllRowState, }; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts index 49dd7a50fbec..a276736cfb1c 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts @@ -1,13 +1,5 @@ -import { ReactNode } from 'react'; -import { IconComponent } from 'twenty-ui'; +import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry'; -import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; - -export type ActionBarEntry = { - label: string; - Icon: IconComponent; - accent?: MenuItemAccent; - onClick?: () => void; +export type ActionBarEntry = ContextMenuEntry & { subActions?: ActionBarEntry[]; - ConfirmationModal?: ReactNode; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts index c5fc0394f3c4..416a41419f62 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts @@ -7,6 +7,6 @@ export type ContextMenuEntry = { label: string; Icon: IconComponent; accent?: MenuItemAccent; - onClick: (event?: MouseEvent) => void; + onClick?: (event?: MouseEvent) => void; ConfirmationModal?: ReactNode; }; From 1ae7a4f22f0d080ec8694bc3359c0e9933d6e642 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:00:23 +0200 Subject: [PATCH 24/33] Fixed bug --- .../object-record/hooks/useFetchAllRecordIds.ts | 10 ++++++++-- .../record-index/options/hooks/useDeleteTableData.ts | 11 ++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts index 895aab6b8e9f..8d878d223dfc 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts @@ -1,6 +1,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { useCallback } from 'react'; import { isDefined } from '~/utils/isDefined'; type UseLazyFetchAllRecordIdsParams = Omit< @@ -24,7 +25,7 @@ export const useFetchAllRecordIds = ({ objectNameSingular, }); - const fetchAllRecordIds = async () => { + const fetchAllRecordIds = useCallback(async () => { console.log({ objectNameSingular, }); @@ -77,7 +78,12 @@ export const useFetchAllRecordIds = ({ const recordIds = Array.from(recordIdSet); return recordIds; - }; + }, [ + fetchMore, + findManyRecords, + objectMetadataItem.namePlural, + objectNameSingular, + ]); return { fetchAllRecordIds, diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts index 7dca26710695..567fad157b82 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -51,20 +51,13 @@ export const useDeleteTableData = ({ recordIdsToDelete = allRecordIds.filter( (recordId) => !unselectedRecordIds.includes(recordId), ); - - console.log({ - unselectedRecordIds, - recordIdsToDelete, - }); } - console.log({ recordIdsToDelete }); + resetTableRowSelection(); await deleteManyRecords(recordIdsToDelete, { - delayInMsBetweenRequests: 25, + delayInMsBetweenRequests: 50, }); - - resetTableRowSelection(); }; return { deleteTableData: deleteRecords }; From 5cf64448be9f87ccbf5265b2aafab7e3e857c444 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:09:08 +0200 Subject: [PATCH 25/33] Cleaned --- .../object-record/constants/DeleteMaxCount.ts | 2 +- .../__tests__/useFindManyRecords.test.tsx | 4 ++-- .../hooks/useFetchAllRecordIds.ts | 19 ++----------------- .../useFetchMoreRecordsWithPagination.ts | 16 +--------------- .../hooks/internal/useLeaveTableFocus.ts | 1 - 5 files changed, 6 insertions(+), 36 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts b/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts index 3b1041ed3de2..0f1b3a7c3bdf 100644 --- a/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts +++ b/packages/twenty-front/src/modules/object-record/constants/DeleteMaxCount.ts @@ -1 +1 @@ -export const DELETE_MAX_COUNT = 3000; +export const DELETE_MAX_COUNT = 10000; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx index b953af77278d..6b7d71c18b5d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; import { RecoilRoot, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -73,7 +73,7 @@ describe('useFindManyRecords', () => { return useFindManyRecords({ objectNameSingular: 'person', - onCompleted: onCompleted, + onCompleted, }); }, { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts index 8d878d223dfc..272ac42aa67d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts @@ -26,9 +26,6 @@ export const useFetchAllRecordIds = ({ }); const fetchAllRecordIds = useCallback(async () => { - console.log({ - objectNameSingular, - }); if (!isDefined(findManyRecords)) { return []; } @@ -66,24 +63,12 @@ export const useFetchAllRecordIds = ({ } lastCursor = fetchMoreResult.pageInfo.endCursor ?? ''; - - const newProgress = Math.round((recordIdSet.size / totalCount) * 100); - - console.log({ - lastCursor, - newProgress, - fetchMoreResult, - }); } + const recordIds = Array.from(recordIdSet); return recordIds; - }, [ - fetchMore, - findManyRecords, - objectMetadataItem.namePlural, - objectNameSingular, - ]); + }, [fetchMore, findManyRecords, objectMetadataItem.namePlural]); return { fetchAllRecordIds, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts index a7e34d37df25..2cc6be1c0b87 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import { ApolloError, ApolloQueryResult, @@ -8,6 +7,7 @@ import { } from '@apollo/client'; import { isNonEmptyArray } from '@apollo/client/utilities'; import { isNonEmptyString } from '@sniptt/guards'; +import { useMemo } from 'react'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -24,7 +24,6 @@ import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyReco import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor'; import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; import { isDefined } from '~/utils/isDefined'; -import { sleep } from '~/utils/sleep'; import { capitalize } from '~/utils/string/capitalize'; import { cursorFamilyState } from '../states/cursorFamilyState'; @@ -108,8 +107,6 @@ export const useFetchMoreRecordsWithPagination = < .getLoadable(cursorFamilyState(queryIdentifier)) .getValue(); - console.log({ lastCursorLocal }); - // Remote objects does not support hasNextPage. We cannot rely on it to fetch more records. if ( hasNextPageLocal || @@ -146,15 +143,6 @@ export const useFetchMoreRecordsWithPagination = < fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; if (isDefined(data?.[objectMetadataItem.namePlural])) { - console.log({ - pageInfo, - hasNextPage: pageInfo?.hasNextPage, - totalCount: - fetchMoreResult?.[objectMetadataItem.namePlural] - ?.totalCount, - data: fetchMoreResult?.[objectMetadataItem.namePlural], - }); - set( cursorFamilyState(queryIdentifier), pageInfo.endCursor ?? '', @@ -203,8 +191,6 @@ export const useFetchMoreRecordsWithPagination = < } finally { setIsFetchingMoreObjects(false); } - - await sleep(1); } }, [ diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index d1d82fa1144a..0a52eb083239 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -24,7 +24,6 @@ export const useLeaveTableFocus = (recordTableId?: string) => { return useRecoilCallback( ({ snapshot }) => () => { - console.log('useLeaveTableFocus'); const isSoftFocusActive = getSnapshotValue( snapshot, isSoftFocusActiveState, From c04d308601e948dad4adb40ebd8488a198c03d0f Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:35:13 +0200 Subject: [PATCH 26/33] Fixed favorites --- .../record-action-bar/hooks/useRecordActionBar.tsx | 9 --------- .../record-index/options/hooks/useDeleteTableData.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) 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 10695af716c7..a623c6b1c4a3 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 @@ -96,15 +96,6 @@ export const useRecordActionBar = ({ const { deleteTableData } = useDeleteTableData(baseTableDataParams); const handleDeleteClick = useCallback(() => { - // selectedRecordIds.forEach((recordId) => { - // const foundFavorite = favorites?.find( - // (favorite) => favorite.recordId === recordId, - // ); - // if (foundFavorite !== undefined) { - // deleteFavorite(foundFavorite.id); - // } - // }); - deleteTableData(); }, [deleteTableData]); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts index 567fad157b82..287c22dec427 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -1,3 +1,4 @@ +import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; @@ -33,6 +34,7 @@ export const useDeleteTableData = ({ const { deleteManyRecords } = useDeleteManyRecords({ objectNameSingular, }); + const { favorites, deleteFavorite } = useFavorites(); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); @@ -55,6 +57,16 @@ export const useDeleteTableData = ({ resetTableRowSelection(); + for (const recordIdToDelete of recordIdsToDelete) { + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === recordIdToDelete, + ); + + if (foundFavorite !== undefined) { + deleteFavorite(foundFavorite.id); + } + } + await deleteManyRecords(recordIdsToDelete, { delayInMsBetweenRequests: 50, }); From e4bd470846e80df40376fb886fe94e4f6a03ecb0 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:37:48 +0200 Subject: [PATCH 27/33] Naming fix --- .../action-bar/components/RecordTableActionBar.tsx | 9 +++++++-- .../navigation/action-bar/components/ActionBar.tsx | 14 +++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index 5dac00df5905..537226990da9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -23,7 +23,7 @@ export const RecordTableActionBar = ({ const tableRowIds = useRecoilValue(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const numSelected = + const totalRecordsSelectedNumber = hasUserSelectedAllRow && entityCountInCurrentView ? selectedRowIds.length === tableRowIds.length ? entityCountInCurrentView @@ -35,5 +35,10 @@ export const RecordTableActionBar = ({ return null; } - return ; + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx index d386fccb7d00..1903035efb81 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx @@ -1,5 +1,5 @@ -import { useEffect, useRef } from 'react'; import styled from '@emotion/styled'; +import { useEffect, useRef } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; @@ -10,7 +10,7 @@ import { ActionBarItem } from './ActionBarItem'; type ActionBarProps = { selectedIds?: string[]; - numSelected?: number; + totalSelectedRecordsNumber?: number; }; const StyledContainerActionBar = styled.div` @@ -43,7 +43,7 @@ const StyledLabel = styled.div` export const ActionBar = ({ selectedIds = [], - numSelected, + totalSelectedRecordsNumber, }: ActionBarProps) => { const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); @@ -61,6 +61,8 @@ export const ActionBar = ({ return null; } + const selectedNumberLabel = totalSelectedRecordsNumber ?? selectedIds?.length; + return ( <> - {(numSelected || selectedIds) && ( - - {numSelected ?? selectedIds?.length} selected: - + {(totalSelectedRecordsNumber || selectedIds) && ( + {selectedNumberLabel} selected: )} {actionBarEntries.map((item, index) => ( From 9714cd684a95020e1c95720eafe4156bceaae851 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:40:13 +0200 Subject: [PATCH 28/33] Fixed typo --- .../components/RecordIndexTableContainerEffect.tsx | 10 ++++------ .../record-index/options/hooks/useDeleteTableData.ts | 6 +++--- .../record-index/options/hooks/useTableData.ts | 8 ++++---- .../action-bar/components/RecordTableActionBar.tsx | 6 +++--- .../hooks/internal/useSetRecordTableData.ts | 6 +++--- .../object-record/record-table/hooks/useRecordTable.ts | 4 ++-- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index b8bde9a925d0..cd945779a2c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -47,22 +47,20 @@ export const RecordIndexTableContainerEffect = ({ setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const { - tableRowIdsState, - hasUserSelectedAllRowsState: hasUserSelectedAllRowState, - } = useRecordTableStates(recordTableId); + const { tableRowIdsState, hasUserSelectedAllRowsState } = + useRecordTableStates(recordTableId); const { entityCountInCurrentViewState } = useViewStates(recordTableId); const entityCountInCurrentView = useRecoilValue( entityCountInCurrentViewState, ); - const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const tableRowIds = useRecoilValue(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const numSelected = - hasUserSelectedAllRow && entityCountInCurrentView + hasUserSelectedAllRows && entityCountInCurrentView ? selectedRowIds.length === tableRowIds.length ? entityCountInCurrentView : entityCountInCurrentView - diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts index 287c22dec427..8c2fca1b9ede 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts @@ -20,7 +20,7 @@ export const useDeleteTableData = ({ const { resetTableRowSelection, selectedRowIdsSelector, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, } = useRecordTable({ recordTableId: recordIndexId, }); @@ -38,12 +38,12 @@ export const useDeleteTableData = ({ const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const deleteRecords = async () => { let recordIdsToDelete = selectedRowIds; - if (hasUserSelectedAllRow) { + if (hasUserSelectedAllRows) { const allRecordIds = await fetchAllRecordIds(); const unselectedRecordIds = tableRowIds.filter( diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts index d7c0ed62db56..b389d906dbb8 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -55,22 +55,22 @@ export const useTableData = ({ visibleTableColumnsSelector, selectedRowIdsSelector, tableRowIdsState, - hasUserSelectedAllRowsState: hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, } = useRecordTableStates(recordIndexId); const columns = useRecoilValue(visibleTableColumnsSelector()); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const tableRowIds = useRecoilValue(tableRowIdsState); // user has checked select all and then unselected some rows const userHasUnselectedSomeRows = - hasUserSelectedAllRow && selectedRowIds.length < tableRowIds.length; + hasUserSelectedAllRows && selectedRowIds.length < tableRowIds.length; const hasSelectedRows = selectedRowIds.length > 0 && - !(hasUserSelectedAllRow && selectedRowIds.length === tableRowIds.length); + !(hasUserSelectedAllRows && selectedRowIds.length === tableRowIds.length); const unselectedRowIds = useMemo( () => diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index 537226990da9..ecf073e95980 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -12,19 +12,19 @@ export const RecordTableActionBar = ({ const { selectedRowIdsSelector, tableRowIdsState, - hasUserSelectedAllRowsState: hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, } = useRecordTableStates(recordTableId); const { entityCountInCurrentViewState } = useViewStates(recordTableId); const entityCountInCurrentView = useRecoilValue( entityCountInCurrentViewState, ); - const hasUserSelectedAllRow = useRecoilValue(hasUserSelectedAllRowState); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const tableRowIds = useRecoilValue(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const totalRecordsSelectedNumber = - hasUserSelectedAllRow && entityCountInCurrentView + hasUserSelectedAllRows && entityCountInCurrentView ? selectedRowIds.length === tableRowIds.length ? entityCountInCurrentView : entityCountInCurrentView - 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 127436bb4a02..eeaf15147c4a 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 @@ -19,7 +19,7 @@ export const useSetRecordTableData = ({ tableRowIdsState, numberOfTableRowsState, isRowSelectedFamilyState, - hasUserSelectedAllRowsState: hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, } = useRecordTableStates(recordTableId); return useRecoilCallback( @@ -39,7 +39,7 @@ export const useSetRecordTableData = ({ const hasUserSelectedAllRows = getSnapshotValue( snapshot, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, ); const entityIds = newEntityArray.map((entity) => entity.id); @@ -62,7 +62,7 @@ export const useSetRecordTableData = ({ tableRowIdsState, onEntityCountChange, isRowSelectedFamilyState, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, ], ); }; 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 10474b31bafa..6cad63df5448 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 @@ -45,7 +45,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { onToggleColumnFilterState, onToggleColumnSortState, pendingRecordIdState, - hasUserSelectedAllRowsState: hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, } = useRecordTableStates(recordTableId); const setAvailableTableColumns = useRecoilCallback( @@ -227,6 +227,6 @@ export const useRecordTable = (props?: useRecordTableProps) => { setOnToggleColumnFilter, setOnToggleColumnSort, setPendingRecordId, - hasUserSelectedAllRowState, + hasUserSelectedAllRowsState, }; }; From 1156e08d8f8dbdb795b831f79942e18abecc1a6e Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:41:49 +0200 Subject: [PATCH 29/33] Fxed naming --- .../record-action-bar/hooks/useRecordActionBar.tsx | 7 ++++--- .../components/RecordIndexTableContainerEffect.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) 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 a623c6b1c4a3..490467dae757 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 @@ -32,14 +32,14 @@ type useRecordActionBarProps = { objectMetadataItem: ObjectMetadataItem; selectedRecordIds: string[]; callback?: () => void; - numSelected?: number; + totalNumberOfRecordsSelected?: number; }; export const useRecordActionBar = ({ objectMetadataItem, selectedRecordIds, callback, - numSelected, + totalNumberOfRecordsSelected, }: useRecordActionBarProps) => { const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); @@ -115,7 +115,8 @@ export const useRecordActionBar = ({ const isRemoteObject = objectMetadataItem.isRemote; - const numberOfSelectedRecords = numSelected ?? selectedRecordIds.length; + const numberOfSelectedRecords = + totalNumberOfRecordsSelected ?? selectedRecordIds.length; const canDelete = !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index cd945779a2c6..32ae2ffc5091 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -71,7 +71,7 @@ export const RecordIndexTableContainerEffect = ({ objectMetadataItem, selectedRecordIds: selectedRowIds, callback: resetTableRowSelection, - numSelected, + totalNumberOfRecordsSelected: numSelected, }); const handleToggleColumnFilter = useHandleToggleColumnFilter({ From 8ffbbdfec334e55ab23818d01ad79b898c417089 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 18:45:09 +0200 Subject: [PATCH 30/33] Fix --- .../action-bar/components/RecordTableActionBar.tsx | 4 ++-- .../navigation/action-bar/components/ActionBar.tsx | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index ecf073e95980..f41025398c22 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -23,7 +23,7 @@ export const RecordTableActionBar = ({ const tableRowIds = useRecoilValue(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const totalRecordsSelectedNumber = + const totalNumberOfSelectedRecords = hasUserSelectedAllRows && entityCountInCurrentView ? selectedRowIds.length === tableRowIds.length ? entityCountInCurrentView @@ -38,7 +38,7 @@ export const RecordTableActionBar = ({ return ( ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx index 1903035efb81..1cb6a4af4f80 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx @@ -6,11 +6,12 @@ import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionB import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal'; +import { isDefined } from '~/utils/isDefined'; import { ActionBarItem } from './ActionBarItem'; type ActionBarProps = { selectedIds?: string[]; - totalSelectedRecordsNumber?: number; + totalNumberOfSelectedRecords?: number; }; const StyledContainerActionBar = styled.div` @@ -43,7 +44,7 @@ const StyledLabel = styled.div` export const ActionBar = ({ selectedIds = [], - totalSelectedRecordsNumber, + totalNumberOfSelectedRecords, }: ActionBarProps) => { const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); @@ -61,7 +62,11 @@ export const ActionBar = ({ return null; } - const selectedNumberLabel = totalSelectedRecordsNumber ?? selectedIds?.length; + const selectedNumberLabel = + totalNumberOfSelectedRecords ?? selectedIds?.length; + + const showSelectedNumberLabel = + isDefined(totalNumberOfSelectedRecords) || Array.isArray(selectedIds); return ( <> @@ -70,7 +75,7 @@ export const ActionBar = ({ className="action-bar" ref={wrapperRef} > - {(totalSelectedRecordsNumber || selectedIds) && ( + {showSelectedNumberLabel && ( {selectedNumberLabel} selected: )} {actionBarEntries.map((item, index) => ( From 0cc7e64dcb39798c24b6343aa0319269460e2741 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 9 Jul 2024 20:44:30 +0200 Subject: [PATCH 31/33] Fixed test --- .../hooks/__tests__/useDeleteManyRecords.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx index 6d2440e5ee95..1c7dd33d33e6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; import { RecoilRoot } from 'recoil'; import { @@ -23,7 +23,7 @@ const mocks = [ }, result: jest.fn(() => ({ data: { - deletePeople: responseData, + deletePeople: [responseData], }, })), }, From 2521676ca9157c83cec45574c5e9de227e62e231 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 10 Jul 2024 13:26:41 +0200 Subject: [PATCH 32/33] Fixed coverage with test on new useFetchAllRecordIds and other utils --- ...etObjectMetadataItemBySingularName.test.ts | 17 +++ .../isObjectRecordConnection.test.ts | 27 +++++ .../constants/DefaultQueryPageSize.ts | 1 + .../graphql/types/RecordGqlConnection.ts | 1 + .../hooks/__mocks__/useFetchAllRecordIds.ts | 81 +++++++++++++ .../__tests__/useFetchAllRecordIds.test.tsx | 107 ++++++++++++++++++ .../hooks/useFetchAllRecordIds.ts | 8 +- .../hooks/useLazyFindManyRecords.ts | 16 +-- .../src/testing/mock-data/metadata.ts | 1 + .../src/testing/mock-data/people.ts | 4 +- 10 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/utils/__tests__/isObjectRecordConnection.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFetchAllRecordIds.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts new file mode 100644 index 000000000000..07e88a26f066 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts @@ -0,0 +1,17 @@ +import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName'; +import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; + +const mockObjectMetadataItems = getObjectMetadataItemsMock(); + +describe('getObjectMetadataItemBySingularName', () => { + it('should work as expected', () => { + const firstObjectMetadataItem = mockObjectMetadataItems[0]; + + const foundObjectMetadataItem = getObjectMetadataItemByNameSingular({ + objectMetadataItems: mockObjectMetadataItems, + objectNameSingular: firstObjectMetadataItem.nameSingular, + }); + + expect(foundObjectMetadataItem.id).toEqual(firstObjectMetadataItem.id); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/isObjectRecordConnection.test.ts b/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/isObjectRecordConnection.test.ts new file mode 100644 index 000000000000..bbe8b38db7bb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/isObjectRecordConnection.test.ts @@ -0,0 +1,27 @@ +import { peopleQueryResult } from '~/testing/mock-data/people'; + +import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection'; + +describe('isObjectRecordConnection', () => { + it('should work with query result', () => { + const validQueryResult = peopleQueryResult.people; + + const isValidQueryResult = isObjectRecordConnection( + 'person', + validQueryResult, + ); + + expect(isValidQueryResult).toEqual(true); + }); + + it('should fail with invalid result', () => { + const invalidResult = { test: 123 }; + + const isValidQueryResult = isObjectRecordConnection( + 'person', + invalidResult, + ); + + expect(isValidQueryResult).toEqual(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts b/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts new file mode 100644 index 000000000000..b6c9ce631c82 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts @@ -0,0 +1 @@ +export const DEFAULT_QUERY_PAGE_SIZE = 30; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts index 543a55b2fc48..df64c8e35fe2 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts @@ -6,6 +6,7 @@ export type RecordGqlConnection = { __typename?: string; edges: RecordGqlEdge[]; pageInfo: { + __typename?: string; hasNextPage?: boolean; hasPreviousPage?: boolean; startCursor?: Nullable; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFetchAllRecordIds.ts new file mode 100644 index 000000000000..8b25676b6a20 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFetchAllRecordIds.ts @@ -0,0 +1,81 @@ +import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; +import { gql } from '@apollo/client'; + +import { peopleQueryResult } from '~/testing/mock-data/people'; + + +export const query = gql` + query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) { + people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ + edges { + node { + __typename + id + } + cursor + } + pageInfo { + hasNextPage + startCursor + endCursor + } + totalCount + } + } +`; + +export const mockPageSize = 2; + +export const peopleMockWithIdsOnly: RecordGqlConnection = { ...peopleQueryResult.people,edges: peopleQueryResult.people.edges.map((edge) => ({ ...edge, node: { __typename: 'Person', id: edge.node.id } })) }; + +export const firstRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize].cursor; +export const secondRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor; +export const thirdRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor; + +export const variablesFirstRequest = { + filter: undefined, + limit: undefined, + orderBy: undefined +}; + +export const variablesSecondRequest = { + filter: undefined, + limit: undefined, + orderBy: undefined, + lastCursor: firstRequestLastCursor +}; + +export const variablesThirdRequest = { + filter: undefined, + limit: undefined, + orderBy: undefined, + lastCursor: secondRequestLastCursor +} + +const paginateRequestResponse = (response: RecordGqlConnection, start: number, end: number, hasNextPage: boolean, totalCount: number) => { + return { + ...response, + edges: [ + ...response.edges.slice(start, end) + ], + pageInfo: { + ...response.pageInfo, + startCursor: response.edges[start].cursor, + endCursor: response.edges[end].cursor, + hasNextPage, + } satisfies RecordGqlConnection['pageInfo'], + totalCount, + } +} + +export const responseFirstRequest = { + people: paginateRequestResponse(peopleMockWithIdsOnly, 0, mockPageSize, true, 6), +}; + +export const responseSecondRequest = { + people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize, mockPageSize * 2, true, 6), +}; + +export const responseThirdRequest = { + people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize * 2, mockPageSize * 3, false, 6), +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx new file mode 100644 index 000000000000..fc32df77ebb0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx @@ -0,0 +1,107 @@ +import { MockedProvider } from '@apollo/client/testing'; +import { act, renderHook } from '@testing-library/react'; +import { ReactNode, useEffect } from 'react'; +import { RecoilRoot, useRecoilState } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { + mockPageSize, + peopleMockWithIdsOnly, + query, + responseFirstRequest, + responseSecondRequest, + responseThirdRequest, + variablesFirstRequest, + variablesSecondRequest, + variablesThirdRequest, +} from '@/object-record/hooks/__mocks__/useFetchAllRecordIds'; +import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; +import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext'; + +const mocks = [ + { + delay: 100, + request: { + query, + variables: variablesFirstRequest, + }, + result: jest.fn(() => ({ + data: responseFirstRequest, + })), + }, + { + delay: 100, + request: { + query, + variables: variablesSecondRequest, + }, + result: jest.fn(() => ({ + data: responseSecondRequest, + })), + }, + { + delay: 100, + request: { + query, + variables: variablesThirdRequest, + }, + result: jest.fn(() => ({ + data: responseThirdRequest, + })), + }, +]; + +describe('useFetchAllRecordIds', () => { + it('fetches all record ids with fetch more synchronous loop', async () => { + const Wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + + ); + + const { result } = renderHook( + () => { + const [, setObjectMetadataItems] = useRecoilState( + objectMetadataItemsState, + ); + + useEffect(() => { + setObjectMetadataItems(getObjectMetadataItemsMock()); + }, [setObjectMetadataItems]); + + return useFetchAllRecordIds({ + objectNameSingular: 'person', + pageSize: mockPageSize, + }); + }, + { + wrapper: Wrapper, + }, + ); + + const { fetchAllRecordIds } = result.current; + + let recordIds: string[] = []; + + await act(async () => { + recordIds = await fetchAllRecordIds(); + }); + + expect(mocks[0].result).toHaveBeenCalled(); + expect(mocks[1].result).toHaveBeenCalled(); + expect(mocks[2].result).toHaveBeenCalled(); + + expect(recordIds).toEqual( + peopleMockWithIdsOnly.edges.map((edge) => edge.node.id).slice(0, 6), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts index 272ac42aa67d..0e70a8ed7b68 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts @@ -1,4 +1,5 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { useCallback } from 'react'; @@ -7,12 +8,13 @@ import { isDefined } from '~/utils/isDefined'; type UseLazyFetchAllRecordIdsParams = Omit< UseFindManyRecordsParams, 'skip' ->; +> & { pageSize?: number }; export const useFetchAllRecordIds = ({ objectNameSingular, filter, orderBy, + pageSize = DEFAULT_QUERY_PAGE_SIZE, }: UseLazyFetchAllRecordIdsParams) => { const { fetchMore, findManyRecords } = useLazyFindManyRecords({ objectNameSingular, @@ -45,7 +47,7 @@ export const useFetchAllRecordIds = ({ const remainingCount = totalCount - recordsCount; - const remainingPages = Math.ceil(remainingCount / 30); + const remainingPages = Math.ceil(remainingCount / pageSize); let lastCursor = firstQueryResult?.pageInfo.endCursor ?? ''; @@ -68,7 +70,7 @@ export const useFetchAllRecordIds = ({ const recordIds = Array.from(recordIdSet); return recordIds; - }, [fetchMore, findManyRecords, objectMetadataItem.namePlural]); + }, [fetchMore, findManyRecords, objectMetadataItem.namePlural, pageSize]); return { fetchAllRecordIds, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index 4a4741916d5c..caf315296d4c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -1,7 +1,6 @@ import { useLazyQuery } from '@apollo/client'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { useRecoilCallback } from 'recoil'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; @@ -38,8 +37,6 @@ export const useLazyFindManyRecords = ({ recordGqlFields, }); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const { handleFindManyRecordsError } = useHandleFindManyRecordsError({ objectMetadataItem, handleError: onError, @@ -86,10 +83,6 @@ export const useLazyFindManyRecords = ({ const findManyRecordsLazy = useRecoilCallback( ({ set }) => async () => { - if (!currentWorkspaceMember) { - return null; - } - const result = await findManyRecords(); const hasNextPage = @@ -105,12 +98,7 @@ export const useLazyFindManyRecords = ({ return result; }, - [ - queryIdentifier, - currentWorkspaceMember, - findManyRecords, - objectMetadataItem, - ], + [queryIdentifier, findManyRecords, objectMetadataItem], ); return { diff --git a/packages/twenty-front/src/testing/mock-data/metadata.ts b/packages/twenty-front/src/testing/mock-data/metadata.ts index 94b5f910c24c..d6ec68da1d70 100644 --- a/packages/twenty-front/src/testing/mock-data/metadata.ts +++ b/packages/twenty-front/src/testing/mock-data/metadata.ts @@ -7,6 +7,7 @@ import { } from '~/generated-metadata/graphql'; import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result'; +// TODO: replace with new mock const customObjectMetadataItemEdge: ObjectEdge = { __typename: 'objectEdge', node: { diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index e0aaa78486c3..89bff03b013e 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -1,3 +1,5 @@ +import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; + export const getPeopleMock = () => { const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node); @@ -22,7 +24,7 @@ export const mockedEmptyPersonData = { __typename: 'Person', }; -export const peopleQueryResult = { +export const peopleQueryResult: { people: RecordGqlConnection } = { people: { __typename: 'PersonConnection', totalCount: 15, From 80621c6e5b2be58090d6bff318405fe0cd07f9ce Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 10 Jul 2024 17:05:12 +0200 Subject: [PATCH 33/33] Fix lint --- .../src/modules/object-record/constants/DefaultQueryPageSize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts b/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts index b6c9ce631c82..cb8e9f295d4d 100644 --- a/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts +++ b/packages/twenty-front/src/modules/object-record/constants/DefaultQueryPageSize.ts @@ -1 +1 @@ -export const DEFAULT_QUERY_PAGE_SIZE = 30; \ No newline at end of file +export const DEFAULT_QUERY_PAGE_SIZE = 30;