diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx new file mode 100644 index 000000000..a6232fefa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx @@ -0,0 +1,467 @@ +import { expect } from '@storybook/test'; +import { renderHook } from '@testing-library/react'; +import { ReactNode, act } from 'react'; + +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; +import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { MockedResponse } from '@apollo/client/testing'; +import gql from 'graphql-tag'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getPeopleMock } from '~/testing/mock-data/people'; + +const recordTableId = 'people'; +const objectNameSingular = 'person'; +const onColumnsChange = jest.fn(); + +const ObjectNamePluralSetter = ({ children }: { children: ReactNode }) => { + return <>{children}; +}; + +const mocks: MockedResponse[] = [ + { + request: { + query: gql` + query FindManyPeople( + $filter: PersonFilterInput + $orderBy: [PersonOrderByInput] + $lastCursor: String + $limit: Int + ) { + people( + filter: $filter + orderBy: $orderBy + first: $limit + after: $lastCursor + ) { + edges { + node { + __typename + avatarUrl + deletedAt + id + name { + firstName + lastName + } + noteTargets { + edges { + node { + __typename + company { + __typename + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name + position + tagline + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + companyId + createdAt + deletedAt + id + note { + __typename + body + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + position + title + updatedAt + } + noteId + opportunity { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + opportunityId + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + personId + rocket { + __typename + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + position + updatedAt + } + rocketId + updatedAt + } + } + } + position + taskTargets { + edges { + node { + __typename + company { + __typename + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name + position + tagline + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + companyId + createdAt + deletedAt + id + opportunity { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + opportunityId + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + personId + rocket { + __typename + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + position + updatedAt + } + rocketId + task { + __typename + assigneeId + body + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + dueAt + id + position + status + title + updatedAt + } + taskId + updatedAt + } + } + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } + `, + variables: { + filter: {}, + orderBy: [{ position: 'AscNullsFirst' }], + }, + }, + result: jest.fn(() => ({ + data: { + people: getPeopleMock(), + }, + })), + }, +]; + +const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); + +const Wrapper = ({ children }: { children: ReactNode }) => { + return ( + + + + + + {children} + + + + + + ); +}; + +describe('useLazyLoadRecordIndexTable', () => { + it('should fetch', async () => { + const { result } = renderHook( + () => { + const { findManyRecords, ...result } = + useLazyLoadRecordIndexTable(objectNameSingular); + + return { + findManyRecords, + ...result, + }; + }, + { + wrapper: Wrapper, + }, + ); + + expect(result.current.loading).toBe(false); + + act(() => { + result.current.findManyRecords(); + }); + + expect(Array.isArray(result.current.records)).toBe(true); + expect(result.current.records.length).toBe(13); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx deleted file mode 100644 index 7008bec92..000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from '@storybook/test'; -import { renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; - -import { mocks } from '@/auth/hooks/__mocks__/useAuth'; -import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; -import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; -import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; - -const recordTableId = 'people'; -const objectNameSingular = 'person'; -const onColumnsChange = jest.fn(); - -const ObjectNamePluralSetter = ({ children }: { children: ReactNode }) => { - return <>{children}; -}; - -const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: mocks, -}); - -const Wrapper = ({ children }: { children: ReactNode }) => { - return ( - - - - - - {children} - - - - - - ); -}; - -describe('useObjectRecordTable', () => { - it('should skip fetch if currentWorkspace is undefined', async () => { - const { result } = renderHook( - () => useLoadRecordIndexTable(objectNameSingular), - { - wrapper: Wrapper, - }, - ); - - expect(result.current.loading).toBe(false); - expect(Array.isArray(result.current.records)).toBe(true); - expect(result.current.records.length).toBe(13); - }); -}); 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 5f19c4edc..977333c50 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchMoreRecordsWithPagination.ts @@ -142,7 +142,7 @@ export const useFetchMoreRecordsWithPagination = < const pageInfo = fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo; - if (isDefined(data?.[objectMetadataItem.namePlural])) { + if (isDefined(pageInfo)) { set( cursorFamilyState(queryIdentifier), pageInfo.endCursor ?? '', @@ -201,7 +201,6 @@ export const useFetchMoreRecordsWithPagination = < fetchMore, filter, orderBy, - data, onCompleted, handleFindManyRecordsError, queryIdentifier, 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 caf315296..56a226510 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -67,7 +67,7 @@ export const useLazyFindManyRecords = ({ onError: handleFindManyRecordsError, }); - const { fetchMoreRecords, totalCount, records } = + const { fetchMoreRecords, totalCount, records, hasNextPage } = useFetchMoreRecordsWithPagination({ objectNameSingular, filter, @@ -108,8 +108,9 @@ export const useLazyFindManyRecords = ({ loading, error, fetchMore, - fetchMoreRecordsWithPagination: fetchMoreRecords, + fetchMoreRecords, queryStateIdentifier: queryIdentifier, findManyRecords: findManyRecordsLazy, + hasNextPage, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts index 848cebbad..9ff30316c 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts @@ -2,7 +2,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; -import { sortedInsert } from '@/object-record/record-group/utils/sortedInsert'; +import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; @@ -54,7 +54,7 @@ export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2< isDefined(recordGroupDefinition) && recordGroupDefinition.isVisible ) { - sortedInsert(result, recordGroupDefinition, comparator); + recordGroupSortedInsert(result, recordGroupDefinition, comparator); } } diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/recordGroupSortedInsert.test.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/recordGroupSortedInsert.test.ts new file mode 100644 index 000000000..4b955fd68 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/recordGroupSortedInsert.test.ts @@ -0,0 +1,66 @@ +// export const recordGroupSortedInsert = ( +// array: T[], +// item: T, +// comparator: (a: T, b: T) => number, +// ) => { +// let low = 0; +// let high = array.length; + +import { expect } from '@storybook/test'; + +// while (low < high) { +// const mid = Math.floor((low + high) / 2); + +// if (comparator(item, array[mid]) < 0) { +// high = mid; +// } else { +// low = mid + 1; +// } +// } + +// array.splice(low, 0, item); +// }; + +import { recordGroupSortedInsert } from '../recordGroupSortedInsert'; + +describe('recordGroupSortedInsert', () => { + it('should insert an item into an empty array', () => { + const array: number[] = []; + const item = 1; + const comparator = (a: number, b: number) => a - b; + + recordGroupSortedInsert(array, item, comparator); + + expect(array).toEqual([1]); + }); + + it('should insert an item at the beginning of the array', () => { + const array = [2, 3, 4]; + const item = 1; + const comparator = (a: number, b: number) => a - b; + + recordGroupSortedInsert(array, item, comparator); + + expect(array).toEqual([1, 2, 3, 4]); + }); + + it('should insert an item at the end of the array', () => { + const array = [1, 2, 3]; + const item = 4; + const comparator = (a: number, b: number) => a - b; + + recordGroupSortedInsert(array, item, comparator); + + expect(array).toEqual([1, 2, 3, 4]); + }); + + it('should insert an item in the middle of the array', () => { + const array = [1, 3, 4]; + const item = 2; + const comparator = (a: number, b: number) => a - b; + + recordGroupSortedInsert(array, item, comparator); + + expect(array).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/recordGroupSortedInsert.ts similarity index 88% rename from packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts rename to packages/twenty-front/src/modules/object-record/record-group/utils/recordGroupSortedInsert.ts index 5a7dd1027..578dcf640 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/recordGroupSortedInsert.ts @@ -1,4 +1,4 @@ -export const sortedInsert = ( +export const recordGroupSortedInsert = ( array: T[], item: T, comparator: (a: T, b: T) => number, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index d6c6f94dc..2c8106f6d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; -import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useContext, useEffect } from 'react'; @@ -31,7 +31,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = objectNameSingular, }); - const findManyRecordsParams = useFindManyParams( + const findManyRecordsParams = useFindManyRecordIndexTableParams( objectMetadataItem?.nameSingular ?? '', objectMetadataItem?.namePlural ?? '', ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts index 523762226..bc256a8bc 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts @@ -13,7 +13,7 @@ import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRec import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; @@ -94,29 +94,24 @@ export const useExportFetchRecords = ({ objectMetadataItem, ); - const findManyRecordsParams = useFindManyParams( + const findManyRecordsParams = useFindManyRecordIndexTableParams( objectMetadataItem.nameSingular, recordIndexId, ); - const { - findManyRecords, - totalCount, - records, - fetchMoreRecordsWithPagination, - loading, - } = useLazyFindManyRecords({ - ...findManyRecordsParams, - filter: queryFilter, - limit: pageSize, - }); + const { findManyRecords, totalCount, records, fetchMoreRecords, loading } = + useLazyFindManyRecords({ + ...findManyRecordsParams, + filter: queryFilter, + limit: pageSize, + }); useEffect(() => { const fetchNextPage = async () => { setInflight(true); setPreviousRecordCount(records.length); - await fetchMoreRecordsWithPagination(); + await fetchMoreRecords(); setPageCount((state) => state + 1); setProgress({ @@ -166,7 +161,7 @@ export const useExportFetchRecords = ({ } }, [ delayMs, - fetchMoreRecordsWithPagination, + fetchMoreRecords, inflight, isDownloading, pageCount, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts similarity index 57% rename from packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts rename to packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index ccfec9af3..e77e993ab 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -1,25 +1,15 @@ -import { useRecoilValue } from 'recoil'; - -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; -import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState'; import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState'; -import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { isNull } from '@sniptt/guards'; import { useMemo } from 'react'; import { isDefined } from 'twenty-ui'; -import { WorkspaceActivationStatus } from '~/generated/graphql'; -export const useFindManyParams = ( +export const useFindManyRecordIndexTableParams = ( objectNameSingular: string, recordTableId?: string, ) => { @@ -84,51 +74,7 @@ export const useFindManyParams = ( ...recordGroupFilter, }, orderBy, - }; -}; - -export const useLoadRecordIndexTable = (objectNameSingular: string) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const { setRecordTableData, setIsRecordTableInitialLoading } = - useRecordTable(); - const currentWorkspace = useRecoilValue(currentWorkspaceState); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const params = useFindManyParams(objectNameSingular); - - const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem }); - - const { - records, - loading, - totalCount, - fetchMoreRecords, - queryStateIdentifier, - hasNextPage, - } = useFindManyRecords({ - ...params, - recordGqlFields, - onCompleted: () => { - setIsRecordTableInitialLoading(false); - }, - onError: () => { - setIsRecordTableInitialLoading(false); - }, - skip: isNull(currentWorkspaceMember), - }); - - return { - records: - currentWorkspace?.activationStatus === WorkspaceActivationStatus.Active - ? records - : SIGN_IN_BACKGROUND_MOCK_COMPANIES, - totalCount: totalCount, - loading, - fetchMoreRecords, - queryStateIdentifier, - setRecordTableData, - hasNextPage, + // If we have a current record group definition, we only want to fetch 8 records by page + ...(currentRecordGroupDefinition ? { limit: 8 } : {}), }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLazyLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLazyLoadRecordIndexTable.ts new file mode 100644 index 000000000..5b212528f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLazyLoadRecordIndexTable.ts @@ -0,0 +1,58 @@ +import { useRecoilValue } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; +import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; + +export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { setRecordTableData, setIsRecordTableInitialLoading } = + useRecordTable(); + + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const params = useFindManyRecordIndexTableParams(objectNameSingular); + + const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem }); + + const { + findManyRecords, + records, + loading, + totalCount, + fetchMoreRecords, + queryStateIdentifier, + hasNextPage, + } = useLazyFindManyRecords({ + ...params, + recordGqlFields, + onCompleted: () => { + setIsRecordTableInitialLoading(false); + }, + onError: () => { + setIsRecordTableInitialLoading(false); + }, + }); + + return { + findManyRecords, + records: + currentWorkspace?.activationStatus === WorkspaceActivationStatus.Active + ? records + : SIGN_IN_BACKGROUND_MOCK_COMPANIES, + totalCount: totalCount, + loading, + fetchMoreRecords, + queryStateIdentifier, + setRecordTableData, + hasNextPage, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState.ts new file mode 100644 index 000000000..11add135e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState.ts @@ -0,0 +1,10 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexHasFetchedAllRecordsByGroupComponentState = + createComponentFamilyStateV2({ + key: 'recordIndexHasFetchedAllRecordsByGroupComponentState', + componentInstanceContext: ViewComponentInstanceContext, + defaultValue: false, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx index 89fc0e8f3..f2c2fb0f6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx @@ -2,26 +2,30 @@ import { useContext, useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useDebouncedCallback } from 'use-debounce'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; -import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; +import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { isNonEmptyString } from '@sniptt/guards'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; import { useScrollToPosition } from '~/hooks/useScrollToPosition'; -import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState'; -import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; export const RecordTableNoRecordGroupBodyEffect = () => { const { objectNameSingular } = useContext(RecordTableContext); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const [hasInitializedScroll, setHasInitializedScroll] = useState(false); const { + findManyRecords, fetchMoreRecords, records, totalCount, @@ -29,7 +33,7 @@ export const RecordTableNoRecordGroupBodyEffect = () => { loading, queryStateIdentifier, hasNextPage, - } = useLoadRecordIndexTable(objectNameSingular); + } = useLazyLoadRecordIndexTable(objectNameSingular); const isFetchingMoreObjects = useRecoilValue( isFetchingMoreRecordsFamilyState(queryStateIdentifier), @@ -132,5 +136,13 @@ export const RecordTableNoRecordGroupBodyEffect = () => { setEncounteredUnrecoverableError, ]); + useEffect(() => { + if (isNull(currentWorkspaceMember)) { + return; + } + + findManyRecords(); + }, [currentWorkspaceMember, findManyRecords]); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx index d768a6700..90569f73e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx @@ -1,29 +1,39 @@ import { useContext, useEffect, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; -import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; +import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { isNonEmptyString } from '@sniptt/guards'; +import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; import { useScrollToPosition } from '~/hooks/useScrollToPosition'; export const RecordTableRecordGroupBodyEffect = () => { const { objectNameSingular } = useContext(RecordTableContext); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const recordGroupId = useCurrentRecordGroupId(); const [hasInitializedScroll, setHasInitializedScroll] = useState(false); - const { records, totalCount, setRecordTableData, loading, hasNextPage } = - useLoadRecordIndexTable(objectNameSingular); + const { + findManyRecords, + records, + totalCount, + setRecordTableData, + loading, + hasNextPage, + } = useLazyLoadRecordIndexTable(objectNameSingular); - const setHasRecordTableFetchedAllRecordsComponents = - useSetRecoilComponentStateV2( - hasRecordTableFetchedAllRecordsComponentStateV2, + const setHasRecordFetchedAllRecordsComponents = + useSetRecoilComponentFamilyStateV2( + recordIndexHasFetchedAllRecordsByGroupComponentState, + recordGroupId, ); const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState); @@ -65,8 +75,16 @@ export const RecordTableRecordGroupBodyEffect = () => { useEffect(() => { const allRecordsHaveBeenFetched = !hasNextPage; - setHasRecordTableFetchedAllRecordsComponents(allRecordsHaveBeenFetched); - }, [hasNextPage, setHasRecordTableFetchedAllRecordsComponents]); + setHasRecordFetchedAllRecordsComponents(allRecordsHaveBeenFetched); + }, [hasNextPage, setHasRecordFetchedAllRecordsComponents]); + + useEffect(() => { + if (isNull(currentWorkspaceMember)) { + return; + } + + findManyRecords(); + }, [currentWorkspaceMember, findManyRecords]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx index e057d2cbd..6cfa2c722 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -7,6 +7,7 @@ import { RecordTableBodyLoading } from '@/object-record/record-table/record-tabl import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection'; +import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -40,6 +41,7 @@ export const RecordTableRecordGroupsBody = () => { + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx index 65b5dd390..a5460f16d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx @@ -61,6 +61,7 @@ export const RecordTableTd = ({ hasRightBorder = true, hasBottomBorder = true, width, + colSpan, ...dragHandleProps }: { className?: string; @@ -74,6 +75,7 @@ export const RecordTableTd = ({ hasBottomBorder?: boolean; left?: number; width?: number; + colSpan?: number; } & (Partial | null)) => { const { theme } = useContext(ThemeContext); @@ -97,6 +99,7 @@ export const RecordTableTd = ({ hasRightBorder={hasRightBorder} hasBottomBorder={hasBottomBorder} width={width} + colSpan={colSpan} // eslint-disable-next-line react/jsx-props-no-spreading {...dragHandleProps} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx new file mode 100644 index 000000000..94975fe78 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx @@ -0,0 +1,68 @@ +import styled from '@emotion/styled'; + +import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; +import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useTheme } from '@emotion/react'; +import { IconComponent } from 'twenty-ui'; + +const StyledTrContainer = styled.tr` + cursor: pointer; +`; + +const StyledIconContainer = styled(RecordTableTd)` + border-right: none; + color: ${({ theme }) => theme.font.color.secondary}; + text-align: center; + vertical-align: middle; + padding-top: 3px; +`; + +const StyledRecordTableTdTextContainer = styled(RecordTableTd)` + border-right: none; + height: 32px; +`; + +const StyledText = styled.span` + color: ${({ theme }) => theme.font.color.tertiary}; + margin-left: ${({ theme }) => theme.spacing(2)}; + font-size: ${({ theme }) => theme.font.size.md}; + text-align: center; + vertical-align: middle; +`; + +const StyledEmptyTd = styled.td` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + +type RecordTableActionRowProps = { + LeftIcon: IconComponent; + text: string; + onClick?: (event?: React.MouseEvent) => void; +}; + +export const RecordTableActionRow = ({ + LeftIcon, + text, + onClick, +}: RecordTableActionRowProps) => { + const theme = useTheme(); + + const visibleColumns = useRecoilComponentValueV2( + visibleTableColumnsComponentSelector, + ); + + return ( + + + + + + + {text} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx index 4e3058011..708fa50db 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx @@ -8,6 +8,7 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; @@ -19,8 +20,8 @@ const StyledTrContainer = styled.tr` cursor: pointer; `; -const StyledChevronContainer = styled.td` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +const StyledChevronContainer = styled(RecordTableTd)` + border-right: none; color: ${({ theme }) => theme.font.color.secondary}; text-align: center; vertical-align: middle; @@ -33,10 +34,9 @@ const StyledTotalRow = styled.span` vertical-align: middle; `; -const StyledRecordGroupSection = styled.td` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - padding-bottom: 6px; - padding-top: 6px; +const StyledRecordGroupSection = styled(RecordTableTd)` + border-right: none; + height: 32px; `; const StyledEmptyTd = styled.td` @@ -79,7 +79,7 @@ export const RecordTableRecordGroupSection = () => { return ( - + { /> {recordIdsByGroup.length} - - + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx new file mode 100644 index 000000000..0418a8a46 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx @@ -0,0 +1,37 @@ +import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; +import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; +import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState'; +import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useContext } from 'react'; +import { IconArrowDown } from 'twenty-ui'; + +export const RecordTableRecordGroupSectionLoadMore = () => { + const { objectNameSingular } = useContext(RecordTableContext); + + const currentRecordGroupId = useCurrentRecordGroupId(); + + const { fetchMoreRecords } = useLazyLoadRecordIndexTable(objectNameSingular); + + const hasFetchedAllRecords = useRecoilComponentFamilyValueV2( + recordIndexHasFetchedAllRecordsByGroupComponentState, + currentRecordGroupId, + ); + + const handleLoadMore = () => { + fetchMoreRecords(); + }; + + if (hasFetchedAllRecords) { + return null; + } + + return ( + + ); +};