From 074cc113acad100af00369e45f1947ec62c461e7 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 5 Feb 2025 11:59:38 +0100 Subject: [PATCH] Implement query variables in useCombinedFindManyRecords (#10015) Implements filtering, ordering and cursor filtering for the hook useCombinedFindManyRecords, because it was not implemented, which was misleading because variables could be passed to it. The difficult part was to make sure that the cursor filtering was working, both before and after a cursor, because it was only hard coded for last cursor (equivalent to after). The duplicate limit parameter in the type RecordGqlOperationVariables was merged into one limit parameter, because it was making the developer guess how both could be handled. This single limit parameter can be used for either : general limit without cursor, first records from after cursor, last records until before cursor. Since those cases are exclusive it's better to have only one limit parameter and have an internal logic handling those cases. Tests were added on the relevant parts, especially useCombinedFindManyRecordsQueryVariables which requires its own unit test to handle this cursor + limit logic. Record show page pagination was tested to make sure removing the duplicate limit parameter had no impact. --- .../types/RecordGqlOperationVariables.ts | 1 - .../object-record/hooks/useFindManyRecords.ts | 2 +- .../useCombinedFindManyRecords.test.tsx | 570 ++++++++++++++++++ ...binedFindManyRecordsQueryVariables.test.ts | 191 ++++++ .../hooks/useCombinedFindManyRecords.ts | 6 + ...seCombinedFindManyRecordsQueryVariables.ts | 75 +++ ...useGenerateCombinedFindManyRecordsQuery.ts | 39 +- ...mbinedFindManyRecordsQueryFilteringPart.ts | 15 + .../hooks/useRecordShowPagePagination.ts | 4 +- 9 files changed, 877 insertions(+), 26 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables.ts create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart.ts diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts index 3abc3358a..c8ae83ae7 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts @@ -9,6 +9,5 @@ export type RecordGqlOperationVariables = { cursorFilter?: { cursor: string; cursorDirection: QueryCursorDirection; - limit: number; }; }; 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 65295a050..3d8e2329f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -82,7 +82,7 @@ export const useFindManyRecords = ({ : {}), orderBy, lastCursor: cursorFilter?.cursor ?? undefined, - limit: cursorFilter?.limit ?? limit, + limit, }, fetchPolicy: fetchPolicy, onCompleted: handleFindManyRecordsCompleted, diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx new file mode 100644 index 000000000..5a03a45a0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx @@ -0,0 +1,570 @@ +import { gql } from '@apollo/client'; +import { renderHook, waitFor } from '@testing-library/react'; +import { useSetRecoilState } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; +import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; +import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords'; +import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +jest.mock( + '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery', + () => ({ + useGenerateCombinedFindManyRecordsQuery: jest.fn(), + }), +); + +const mockQuery = gql` + query CombinedFindManyRecords( + $filterPerson: PersonFilterInput + $filterCompany: CompanyFilterInput + $orderByPerson: [PersonOrderByInput] + $orderByCompany: [CompanyOrderByInput] + $firstPerson: Int + $lastPerson: Int + $afterPerson: String + $beforePerson: String + $firstCompany: Int + $lastCompany: Int + $afterCompany: String + $beforeCompany: String + $limitPerson: Int + $limitCompany: Int + ) { + people( + filter: $filterPerson + orderBy: $orderByPerson + first: $firstPerson + after: $afterPerson + last: $lastPerson + before: $beforePerson + limit: $limitPerson + ) { + edges { + node { + __typename + id + name { + firstName + lastName + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + companies( + filter: $filterCompany + orderBy: $orderByCompany + first: $firstCompany + after: $afterCompany + last: $lastCompany + before: $beforeCompany + limit: $limitCompany + ) { + edges { + node { + __typename + id + name + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } +`; + +type RenderUseCombinedFindManyRecordsHookParams = { + operationSignatures: RecordGqlOperationSignature[]; + mockVariables?: Record; + mockResponseData?: Record; + skip?: boolean; + expectedResult?: Record; + mockQueryResult?: any; +}; + +const renderUseCombinedFindManyRecordsHook = async ({ + operationSignatures, + mockVariables = {}, + mockResponseData, + skip = false, + expectedResult = {}, + mockQueryResult = mockQuery, +}: RenderUseCombinedFindManyRecordsHookParams) => { + (useGenerateCombinedFindManyRecordsQuery as jest.Mock).mockReturnValue( + mockQueryResult, + ); + + const mocks = [ + { + request: { + query: mockQuery, + variables: mockVariables, + }, + result: { + data: mockResponseData, + }, + }, + ]; + + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems(generatedMockObjectMetadataItems); + + return useCombinedFindManyRecords({ + operationSignatures, + skip, + }); + }, + { + wrapper: getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks }), + }, + ); + + expect(result.current.loading).toBe(!skip); + expect(result.current.result).toEqual({}); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.result).toEqual(expectedResult); + + return result; +}; + +describe('useCombinedFindManyRecords', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return records for multiple objects', async () => { + const mockResponseData = { + people: { + edges: [ + { + node: { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + cursor: 'cursor1', + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'cursor1', + endCursor: 'cursor1', + }, + totalCount: 1, + }, + companies: { + edges: [ + { + node: { + __typename: 'Company', + id: '1', + name: 'Twenty', + }, + cursor: 'cursor1', + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'cursor1', + endCursor: 'cursor1', + }, + totalCount: 1, + }, + }; + + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: {}, + }, + { + objectNameSingular: 'company', + fields: { + id: true, + name: true, + } as RecordGqlFields, + variables: {}, + }, + ], + mockResponseData, + expectedResult: { + people: [ + { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + companies: [ + { + __typename: 'Company', + id: '1', + name: 'Twenty', + }, + ], + }, + }); + }); + + it('should handle forward pagination with after cursor and first limit', async () => { + const mockResponseData = { + people: { + edges: [ + { + node: { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + cursor: 'cursor1', + }, + ], + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'cursor1', + endCursor: 'cursor1', + }, + totalCount: 10, + }, + }; + + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: { + limit: 1, + cursorFilter: { + cursor: 'previousCursor', + cursorDirection: 'after', + }, + }, + }, + ], + mockVariables: { + firstPerson: 1, + afterPerson: 'previousCursor', + }, + mockResponseData, + expectedResult: { + people: [ + { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + }, + }); + }); + + it('should handle backward pagination with before cursor and last limit', async () => { + const mockResponseData = { + people: { + edges: [ + { + node: { + __typename: 'Person', + id: '2', + name: { + firstName: 'Jane', + lastName: 'Smith', + }, + }, + cursor: 'cursor2', + }, + ], + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'cursor2', + endCursor: 'cursor2', + }, + totalCount: 10, + }, + }; + + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: { + limit: 1, + cursorFilter: { + cursor: 'nextCursor', + cursorDirection: 'before', + }, + }, + }, + ], + mockVariables: { + lastPerson: 1, + beforePerson: 'nextCursor', + }, + mockResponseData, + expectedResult: { + people: [ + { + __typename: 'Person', + id: '2', + name: { + firstName: 'Jane', + lastName: 'Smith', + }, + }, + ], + }, + }); + }); + + it('should handle limit-based pagination without cursor', async () => { + const mockResponseData = { + people: { + edges: [ + { + node: { + __typename: 'Person', + id: '3', + name: { + firstName: 'Alice', + lastName: 'Johnson', + }, + }, + cursor: 'cursor3', + }, + ], + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'cursor3', + endCursor: 'cursor3', + }, + totalCount: 10, + }, + }; + + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: { + limit: 1, + }, + }, + ], + mockVariables: { + limitPerson: 1, + }, + mockResponseData, + expectedResult: { + people: [ + { + __typename: 'Person', + id: '3', + name: { + firstName: 'Alice', + lastName: 'Johnson', + }, + }, + ], + }, + }); + }); + + it('should handle multiple objects with different pagination strategies', async () => { + const mockResponseData = { + people: { + edges: [ + { + node: { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + cursor: 'cursor1', + }, + ], + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'cursor1', + endCursor: 'cursor1', + }, + totalCount: 10, + }, + companies: { + edges: [ + { + node: { + __typename: 'Company', + id: '1', + name: 'Twenty', + }, + cursor: 'cursor1', + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'cursor1', + endCursor: 'cursor1', + }, + totalCount: 1, + }, + }; + + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: { + limit: 1, + cursorFilter: { + cursor: 'previousCursor', + cursorDirection: 'after', + }, + }, + }, + { + objectNameSingular: 'company', + fields: { + id: true, + name: true, + } as RecordGqlFields, + variables: { + limit: 1, + }, + }, + ], + mockVariables: { + firstPerson: 1, + afterPerson: 'previousCursor', + limitCompany: 1, + }, + mockResponseData, + expectedResult: { + people: [ + { + __typename: 'Person', + id: '1', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + companies: [ + { + __typename: 'Company', + id: '1', + name: 'Twenty', + }, + ], + }, + }); + }); + + it('should handle empty operation signatures', async () => { + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [], + mockResponseData: {}, + expectedResult: {}, + }); + }); + + it('should handle skip flag', async () => { + await renderUseCombinedFindManyRecordsHook({ + operationSignatures: [ + { + objectNameSingular: 'person', + fields: { + id: true, + } as RecordGqlFields, + variables: {}, + }, + ], + skip: true, + mockResponseData: {}, + expectedResult: {}, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts new file mode 100644 index 000000000..e6257fe8d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts @@ -0,0 +1,191 @@ +import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; +import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; +import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables'; + +describe('useCombinedFindManyRecordsQueryVariables', () => { + it('should generate variables with after cursor and first limit', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: { + firstName: true, + lastName: true, + }, + } as RecordGqlFields, + variables: { + filter: { id: { eq: '123' } }, + orderBy: [{ createdAt: 'AscNullsLast' }], + limit: 10, + cursorFilter: { + cursor: 'cursor123', + cursorDirection: 'after', + }, + }, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({ + filterPerson: { id: { eq: '123' } }, + orderByPerson: [{ createdAt: 'AscNullsLast' }], + afterPerson: 'cursor123', + firstPerson: 10, + }); + }); + + it('should generate variables with before cursor and last limit', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: true, + } as RecordGqlFields, + variables: { + filter: { id: { eq: '123' } }, + orderBy: [{ createdAt: 'AscNullsLast' }], + limit: 10, + cursorFilter: { + cursor: 'cursor123', + cursorDirection: 'before', + }, + }, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({ + filterPerson: { id: { eq: '123' } }, + orderByPerson: [{ createdAt: 'AscNullsLast' }], + beforePerson: 'cursor123', + lastPerson: 10, + }); + }); + + it('should generate variables with limit only (no cursor)', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + name: true, + } as RecordGqlFields, + variables: { + filter: { id: { eq: '123' } }, + orderBy: [{ createdAt: 'AscNullsLast' }], + limit: 10, + }, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({ + filterPerson: { id: { eq: '123' } }, + orderByPerson: [{ createdAt: 'AscNullsLast' }], + limitPerson: 10, + }); + }); + + it('should handle multiple objects with different pagination strategies', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + } as RecordGqlFields, + variables: { + filter: { id: { eq: '123' } }, + limit: 10, + cursorFilter: { + cursor: 'cursor123', + cursorDirection: 'after', + }, + }, + }, + { + objectNameSingular: 'company', + fields: { + id: true, + } as RecordGqlFields, + variables: { + filter: { name: { eq: 'Twenty' } }, + limit: 20, + }, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({ + filterPerson: { id: { eq: '123' } }, + afterPerson: 'cursor123', + firstPerson: 10, + filterCompany: { name: { eq: 'Twenty' } }, + limitCompany: 20, + }); + }); + + it('should handle empty operation signatures', () => { + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures: [], + }); + + expect(result).toEqual({}); + }); + + it('should handle empty variables', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + } as RecordGqlFields, + variables: {}, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({}); + }); + + it('should handle cursor without limit', () => { + const operationSignatures: RecordGqlOperationSignature[] = [ + { + objectNameSingular: 'person', + fields: { + id: true, + } as RecordGqlFields, + variables: { + cursorFilter: { + cursor: 'cursor123', + cursorDirection: 'after', + }, + }, + }, + ]; + + const result = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + + expect(result).toEqual({ + afterPerson: 'cursor123', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts index b147b5535..bdfc71f2b 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts @@ -3,6 +3,7 @@ import { useQuery } from '@apollo/client'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; +import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables'; import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; @@ -17,10 +18,15 @@ export const useCombinedFindManyRecords = ({ operationSignatures, }); + const queryVariables = useCombinedFindManyRecordsQueryVariables({ + operationSignatures, + }); + const { data, loading } = useQuery( findManyQuery ?? EMPTY_QUERY, { skip, + variables: queryVariables, }, ); diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables.ts new file mode 100644 index 000000000..bbe9db107 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables.ts @@ -0,0 +1,75 @@ +import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; +import { isNonEmptyString } from '@sniptt/guards'; +import { capitalize, isDefined } from 'twenty-shared'; +import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; + +export const useCombinedFindManyRecordsQueryVariables = ({ + operationSignatures, +}: { + operationSignatures: RecordGqlOperationSignature[]; +}) => { + if (!isNonEmptyArray(operationSignatures)) { + return {}; + } + + return operationSignatures.reduce( + (acc, { objectNameSingular, variables }) => { + const capitalizedName = capitalize(objectNameSingular); + + const filter = isDefined(variables?.filter) + ? { [`filter${capitalizedName}`]: variables.filter } + : {}; + + const orderBy = isDefined(variables?.orderBy) + ? { [`orderBy${capitalizedName}`]: variables.orderBy } + : {}; + + let limit = {}; + + const hasLimit = isDefined(variables.limit) && variables.limit > 0; + + const cursorDirection = variables.cursorFilter?.cursorDirection; + + let cursorFilter = {}; + + if (isNonEmptyString(variables.cursorFilter?.cursor)) { + if (cursorDirection === 'after') { + cursorFilter = { + [`after${capitalizedName}`]: variables.cursorFilter?.cursor, + }; + + if (hasLimit) { + cursorFilter = { + ...cursorFilter, + [`first${capitalizedName}`]: variables.limit, + }; + } + } else if (cursorDirection === 'before') { + cursorFilter = { + [`before${capitalizedName}`]: variables.cursorFilter?.cursor, + }; + + if (hasLimit) { + cursorFilter = { + ...cursorFilter, + [`last${capitalizedName}`]: variables.limit, + }; + } + } + } else if (hasLimit) { + limit = { + [`limit${capitalizedName}`]: variables.limit, + }; + } + + return { + ...acc, + ...filter, + ...orderBy, + ...limit, + ...cursorFilter, + }; + }, + {}, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts index 41edb6257..a276476dd 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts @@ -6,6 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart'; import { capitalize } from 'twenty-shared'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; @@ -38,10 +39,10 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ ) .join(', '); - const lastCursorPerMetadataItemArray = operationSignatures + const cursorFilteringPerMetadataItemArray = operationSignatures .map( ({ objectNameSingular }) => - `$lastCursor${capitalize(objectNameSingular)}: String`, + `$after${capitalize(objectNameSingular)}: String, $before${capitalize(objectNameSingular)}: String, $first${capitalize(objectNameSingular)}: Int, $last${capitalize(objectNameSingular)}: Int`, ) .join(', '); @@ -52,48 +53,42 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ ) .join(', '); - const queryKeyWithObjectMetadataItemArray = operationSignatures.map( - (queryKey) => { + const queryOperationSignatureWithObjectMetadataItemArray = + operationSignatures.map((operationSignature) => { const objectMetadataItem = objectMetadataItems.find( (objectMetadataItem) => - objectMetadataItem.nameSingular === queryKey.objectNameSingular, + objectMetadataItem.nameSingular === + operationSignature.objectNameSingular, ); if (isUndefined(objectMetadataItem)) { throw new Error( - `Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`, + `Object metadata item not found for object name singular: ${operationSignature.objectNameSingular}`, ); } - return { ...queryKey, objectMetadataItem }; - }, - ); + return { operationSignature, objectMetadataItem }; + }); return gql` query CombinedFindManyRecords( ${filterPerMetadataItemArray}, ${orderByPerMetadataItemArray}, - ${lastCursorPerMetadataItemArray}, + ${cursorFilteringPerMetadataItemArray}, ${limitPerMetadataItemArray} ) { - ${queryKeyWithObjectMetadataItemArray + ${queryOperationSignatureWithObjectMetadataItemArray .map( - ({ objectMetadataItem, fields }) => - `${objectMetadataItem.namePlural}(filter: $filter${capitalize( - objectMetadataItem.nameSingular, - )}, orderBy: $orderBy${capitalize( - objectMetadataItem.nameSingular, - )}, first: $limit${capitalize( - objectMetadataItem.nameSingular, - )}, after: $lastCursor${capitalize( - objectMetadataItem.nameSingular, - )}){ + ({ objectMetadataItem, operationSignature }) => + `${getCombinedFindManyRecordsQueryFilteringPart( + objectMetadataItem, + )} { edges { node ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems: objectMetadataItems, objectMetadataItem, recordGqlFields: - fields ?? + operationSignature.fields ?? generateDepthOneRecordGqlFields({ objectMetadataItem, }), diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart.ts new file mode 100644 index 000000000..1353d0555 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart.ts @@ -0,0 +1,15 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { capitalize } from 'twenty-shared'; + +export const getCombinedFindManyRecordsQueryFilteringPart = ( + objectMetadataItem: ObjectMetadataItem, +) => { + return `${objectMetadataItem.namePlural}( + filter: $filter${capitalize(objectMetadataItem.nameSingular)}, + orderBy: $orderBy${capitalize(objectMetadataItem.nameSingular)}, + after: $after${capitalize(objectMetadataItem.nameSingular)}, + before: $before${capitalize(objectMetadataItem.nameSingular)}, + first: $first${capitalize(objectMetadataItem.nameSingular)}, + last: $last${capitalize(objectMetadataItem.nameSingular)}, + limit: $limit${capitalize(objectMetadataItem.nameSingular)})`; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts index 1f3f149dd..460f60a1a 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts @@ -67,11 +67,11 @@ export const useRecordShowPagePagination = ( id: { neq: objectRecordId }, }, orderBy, + limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined, cursorFilter: isNonEmptyString(currentRecordCursorFromRequest) ? { cursorDirection: 'before', cursor: currentRecordCursorFromRequest, - limit: 1, } : undefined, objectNameSingular, @@ -90,11 +90,11 @@ export const useRecordShowPagePagination = ( }, fetchPolicy: 'network-only', orderBy, + limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined, cursorFilter: currentRecordCursorFromRequest ? { cursorDirection: 'after', cursor: currentRecordCursorFromRequest, - limit: 1, } : undefined, objectNameSingular,