feat: record group fetch more (#8868)
Fix #8756 This PR is adding a load more button when we're grouping by a field in table view. Only 8 records are printed at first, and a click on `Load more` is needed to show more records. <img width="1347" alt="Screenshot 2024-12-04 at 11 54 15 AM" src="https://github.com/user-attachments/assets/4ad6ad4f-8de9-424d-b7b6-5f82f28c53df"> <img width="1352" alt="Screenshot 2024-12-04 at 11 54 23 AM" src="https://github.com/user-attachments/assets/2a94b4ac-7285-4ba2-9cff-d2f653e36302"> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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 (
|
||||
<HookMockWrapper>
|
||||
<ObjectNamePluralSetter>
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'instanceId' }}
|
||||
>
|
||||
<RecordTableComponentInstance
|
||||
recordTableId={recordTableId}
|
||||
onColumnsChange={onColumnsChange}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}>
|
||||
{children}
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableComponentInstance>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</ObjectNamePluralSetter>
|
||||
</HookMockWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -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 (
|
||||
<HookMockWrapper>
|
||||
<ObjectNamePluralSetter>
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'instanceId' }}
|
||||
>
|
||||
<RecordTableComponentInstance
|
||||
recordTableId={recordTableId}
|
||||
onColumnsChange={onColumnsChange}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}>
|
||||
{children}
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableComponentInstance>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</ObjectNamePluralSetter>
|
||||
</HookMockWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -67,7 +67,7 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onError: handleFindManyRecordsError,
|
||||
});
|
||||
|
||||
const { fetchMoreRecords, totalCount, records } =
|
||||
const { fetchMoreRecords, totalCount, records, hasNextPage } =
|
||||
useFetchMoreRecordsWithPagination<T>({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
@ -108,8 +108,9 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
loading,
|
||||
error,
|
||||
fetchMore,
|
||||
fetchMoreRecordsWithPagination: fetchMoreRecords,
|
||||
fetchMoreRecords,
|
||||
queryStateIdentifier: queryIdentifier,
|
||||
findManyRecords: findManyRecordsLazy,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user