diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts index 7217a34dc..990a73181 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts @@ -1,3 +1,3 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; -export type RecordGqlFieldsAggregate = Record; +export type RecordGqlFieldsAggregate = Record; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts new file mode 100644 index 000000000..b590efc57 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const AGGREGATE_QUERY = gql` + query AggregateOpportunities($filter: OpportunityFilterInput) { + opportunities(filter: $filter) { + totalCount + sumAmount + avgAmount + } + } +`; + +export const mockResponse = { + opportunities: { + totalCount: 42, + sumAmount: 1000000, + avgAmount: 23800 + } +}; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx new file mode 100644 index 000000000..5eab4810f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx @@ -0,0 +1,129 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { + AGGREGATE_QUERY, + mockResponse, +} from '@/object-record/hooks/__mocks__/useAggregateRecords'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { useQuery } from '@apollo/client'; +import { renderHook } from '@testing-library/react'; + +// Mocks +jest.mock('@apollo/client'); +jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); +jest.mock('@/object-record/hooks/useAggregateRecordsQuery'); + +const mockObjectMetadataItem = { + nameSingular: 'opportunity', + namePlural: 'opportunities', +}; + +const mockGqlFieldToFieldMap = { + sumAmount: ['amount', AGGREGATE_OPERATIONS.sum], + avgAmount: ['amount', AGGREGATE_OPERATIONS.avg], + totalCount: ['name', AGGREGATE_OPERATIONS.count], +}; + +describe('useAggregateRecords', () => { + beforeEach(() => { + (useObjectMetadataItem as jest.Mock).mockReturnValue({ + objectMetadataItem: mockObjectMetadataItem, + }); + + (useAggregateRecordsQuery as jest.Mock).mockReturnValue({ + aggregateQuery: AGGREGATE_QUERY, + gqlFieldToFieldMap: mockGqlFieldToFieldMap, + }); + + (useQuery as jest.Mock).mockReturnValue({ + data: mockResponse, + loading: false, + error: undefined, + }); + }); + + it('should format data correctly', () => { + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum, AGGREGATE_OPERATIONS.avg], + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.data).toEqual({ + amount: { + [AGGREGATE_OPERATIONS.sum]: 1000000, + [AGGREGATE_OPERATIONS.avg]: 23800, + }, + name: { + [AGGREGATE_OPERATIONS.count]: 42, + }, + }); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeUndefined(); + }); + + it('should handle loading state', () => { + (useQuery as jest.Mock).mockReturnValue({ + data: undefined, + loading: true, + error: undefined, + }); + + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.data).toEqual({}); + expect(result.current.loading).toBe(true); + }); + + it('should handle error state', () => { + const mockError = new Error('Query failed'); + (useQuery as jest.Mock).mockReturnValue({ + data: undefined, + loading: false, + error: mockError, + }); + + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.data).toEqual({}); + expect(result.current.error).toBe(mockError); + }); + + it('should skip query when specified', () => { + renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + skip: true, + }), + ); + + expect(useQuery).toHaveBeenCalledWith( + AGGREGATE_QUERY, + expect.objectContaining({ + skip: true, + }), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx new file mode 100644 index 000000000..6157455bf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx @@ -0,0 +1,144 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery'; +import { renderHook } from '@testing-library/react'; +import { FieldMetadataType } from '~/generated/graphql'; + +jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); +jest.mock('@/object-record/utils/generateAggregateQuery'); + +const mockObjectMetadataItem: ObjectMetadataItem = { + nameSingular: 'company', + namePlural: 'companies', + id: 'test-id', + labelSingular: 'Company', + labelPlural: 'Companies', + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fields: [ + { + id: 'field-1', + name: 'amount', + label: 'Amount', + type: FieldMetadataType.Number, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, + { + id: 'field-2', + name: 'name', + label: 'Name', + type: FieldMetadataType.Text, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, + ], + indexMetadatas: [], + isLabelSyncedWithName: true, + isRemote: false, + isSystem: false, +}; + +describe('useAggregateRecordsQuery', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useObjectMetadataItem as jest.Mock).mockReturnValue({ + objectMetadataItem: mockObjectMetadataItem, + }); + + (generateAggregateQuery as jest.Mock).mockReturnValue({ + loc: { + source: { + body: 'query AggregateCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { totalCount } }', + }, + }, + }); + }); + + it('should handle simple count operation', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toEqual({ + totalCount: ['name', 'COUNT'], + }); + expect(generateAggregateQuery).toHaveBeenCalledWith({ + objectMetadataItem: mockObjectMetadataItem, + recordGqlFields: { + totalCount: true, + }, + }); + }); + + it('should handle field aggregation', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toEqual({ + sumAmount: ['amount', 'SUM'], + }); + expect(generateAggregateQuery).toHaveBeenCalledWith( + expect.objectContaining({ + recordGqlFields: expect.objectContaining({ + sumAmount: true, + }), + }), + ); + }); + + it('should throw error for invalid aggregation operation', () => { + expect(() => + renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + name: [AGGREGATE_OPERATIONS.sum], + }, + }), + ), + ).toThrow(); + }); + + it('should handle multiple aggregations', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toHaveProperty('sumAmount'); + expect(generateAggregateQuery).toHaveBeenCalledWith( + expect.objectContaining({ + recordGqlFields: expect.objectContaining({ + totalCount: true, + sumAmount: true, + }), + }), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx index 761680c43..82ff949fe 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx @@ -9,12 +9,19 @@ import { variables, } from '@/object-record/hooks/__mocks__/useCreateManyRecords'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('uuid', () => ({ v4: jest.fn(), })); +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + mocked(v4) .mockReturnValueOnce(variables.data[0].id) .mockReturnValueOnce(variables.data[1].id); @@ -40,6 +47,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ }); describe('useCreateManyRecords', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => @@ -57,5 +67,6 @@ describe('useCreateManyRecords', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx index db26b8605..0caea681d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx @@ -6,6 +6,7 @@ import { responseData, } from '@/object-record/hooks/__mocks__/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; @@ -15,6 +16,12 @@ jest.mock('uuid', () => ({ v4: jest.fn(() => personId), })); +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const mocks = [ { request: { @@ -34,6 +41,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ }); describe('useCreateOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => @@ -52,5 +62,6 @@ describe('useCreateOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); 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 89d5d1205..2f6f68b15 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 @@ -6,6 +6,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { act } from 'react'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; @@ -28,11 +29,20 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); describe('useDeleteManyRecords', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useDeleteManyRecords({ objectNameSingular: 'person' }), @@ -48,5 +58,6 @@ describe('useDeleteManyRecords', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx index 0c347d309..1a5b65bc9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx @@ -7,6 +7,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; @@ -25,11 +26,20 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); describe('useDeleteOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useDeleteOneRecord({ objectNameSingular: 'person' }), @@ -45,5 +55,6 @@ describe('useDeleteOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx new file mode 100644 index 000000000..78d56ced2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx @@ -0,0 +1,77 @@ +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useApolloClient } from '@apollo/client'; +import { renderHook } from '@testing-library/react'; + +jest.mock('@apollo/client', () => ({ + useApolloClient: jest.fn(), +})); + +jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({ + useIsFeatureEnabled: jest.fn(), +})); + +describe('useRefetchAggregateQueries', () => { + const mockRefetchQueries = jest.fn(); + const mockApolloClient = { + refetchQueries: mockRefetchQueries, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useApolloClient as jest.Mock).mockReturnValue(mockApolloClient); + }); + + it('should refetch queries when feature flag is enabled', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(true); + const objectMetadataNamePlural = 'opportunities'; + const expectedQueryName = getAggregateQueryName(objectMetadataNamePlural); + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + await result.current.refetchAggregateQueries(); + + // Assert + expect(mockRefetchQueries).toHaveBeenCalledTimes(1); + expect(mockRefetchQueries).toHaveBeenCalledWith({ + include: [expectedQueryName], + }); + }); + + it('should not refetch queries when feature flag is disabled', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(false); + const objectMetadataNamePlural = 'opportunities'; + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + await result.current.refetchAggregateQueries(); + + // Assert + expect(mockRefetchQueries).not.toHaveBeenCalled(); + }); + + it('should handle errors during refetch', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(true); + const error = new Error('Refetch failed'); + mockRefetchQueries.mockRejectedValue(error); + const objectMetadataNamePlural = 'opportunities'; + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + + // Assert + await expect(result.current.refetchAggregateQueries()).rejects.toThrow( + 'Refetch failed', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx index d32ef3750..557892a1f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx @@ -5,7 +5,9 @@ import { responseData, variables, } from '@/object-record/hooks/__mocks__/useUpdateOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { expect } from '@storybook/test'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; @@ -35,6 +37,12 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); @@ -42,6 +50,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ const idToUpdate = '36abbb63-34ed-4a16-89f5-f549ac55d0f9'; describe('useUpdateOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useUpdateOneRecord({ objectNameSingular: 'person' }), @@ -61,5 +72,6 @@ describe('useUpdateOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts similarity index 85% rename from packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts rename to packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts index 8fc9e313f..2a3b63578 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts @@ -4,18 +4,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; -import { useAggregateManyRecordsQuery } from '@/object-record/hooks/useAggregateManyRecordsQuery'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-ui'; -export type AggregateManyRecordsData = { +export type AggregateRecordsData = { [fieldName: string]: { [operation in AGGREGATE_OPERATIONS]?: string | number | undefined; }; }; -export const useAggregateManyRecords = ({ +export const useAggregateRecords = ({ objectNameSingular, filter, recordGqlFieldsAggregate, @@ -30,7 +30,7 @@ export const useAggregateManyRecords = ({ objectNameSingular, }); - const { aggregateQuery, gqlFieldToFieldMap } = useAggregateManyRecordsQuery({ + const { aggregateQuery, gqlFieldToFieldMap } = useAggregateRecordsQuery({ objectNameSingular, recordGqlFieldsAggregate, }); @@ -45,7 +45,7 @@ export const useAggregateManyRecords = ({ }, ); - const formattedData: AggregateManyRecordsData = {}; + const formattedData: AggregateRecordsData = {}; if (!isEmpty(data)) { Object.entries(data?.[objectMetadataItem.namePlural] ?? {})?.forEach( diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts similarity index 70% rename from packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts rename to packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts index ae47c346a..ea87546fe 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts @@ -14,7 +14,7 @@ export type GqlFieldToFieldMap = { ]; }; -export const useAggregateManyRecordsQuery = ({ +export const useAggregateRecordsQuery = ({ objectNameSingular, recordGqlFieldsAggregate = {}, }: { @@ -34,26 +34,20 @@ export const useAggregateManyRecordsQuery = ({ const gqlFieldToFieldMap: GqlFieldToFieldMap = {}; Object.entries(recordGqlFieldsAggregate).forEach( - ([fieldName, aggregateOperation]) => { - if ( - !isDefined(fieldName) && - aggregateOperation === AGGREGATE_OPERATIONS.count - ) { - recordGqlFields.totalCount = true; - return; - } + ([fieldName, aggregateOperations]) => { + aggregateOperations.forEach((aggregateOperation) => { + const fieldToQuery = + availableAggregations[fieldName]?.[aggregateOperation]; - const fieldToQuery = - availableAggregations[fieldName]?.[aggregateOperation]; + if (!isDefined(fieldToQuery)) { + throw new Error( + `Cannot query operation ${aggregateOperation} on field ${fieldName}`, + ); + } + gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; - if (!isDefined(fieldToQuery)) { - throw new Error( - `Cannot query operation ${aggregateOperation} on field ${fieldName}`, - ); - } - gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; - - recordGqlFields[fieldToQuery] = true; + recordGqlFields[fieldToQuery] = true; + }); }, ); 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 c236baede..d2cb0c69e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -11,6 +11,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; @@ -51,6 +52,10 @@ export const useCreateManyRecords = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const createManyRecords = async ( recordsToCreate: Partial[], upsert?: boolean, @@ -141,6 +146,7 @@ export const useCreateManyRecords = < throw error; }); + await refetchAggregateQueries(); return createdObjects.data?.[mutationResponseField] ?? []; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index 73c9cd989..3660dbd7e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -12,6 +12,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; @@ -55,6 +56,10 @@ export const useCreateOneRecord = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const createOneRecord = async (input: Partial) => { setLoading(true); @@ -131,6 +136,7 @@ export const useCreateOneRecord = < throw error; }); + await refetchAggregateQueries(); return createdObject.data?.[mutationResponseField] ?? null; }; 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 192b642bf..404320943 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -9,6 +9,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { useRecoilValue } from 'recoil'; @@ -51,6 +52,10 @@ export const useDeleteManyRecords = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDeleteManyRecordsMutationResponseField( objectMetadataItem.namePlural, ); @@ -194,7 +199,7 @@ export const useDeleteManyRecords = ({ await sleep(options.delayInMsBetweenRequests); } } - + await refetchAggregateQueries(); return deletedRecords; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 969b83011..604b68c62 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -8,6 +8,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { capitalize } from '~/utils/string/capitalize'; @@ -35,6 +36,10 @@ export const useDeleteOneRecord = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDeleteOneRecordMutationResponseField(objectNameSingular); @@ -126,6 +131,7 @@ export const useDeleteOneRecord = ({ throw error; }); + await refetchAggregateQueries(); return deletedRecord.data?.[mutationResponseField] ?? null; }, [ @@ -135,6 +141,7 @@ export const useDeleteOneRecord = ({ mutationResponseField, objectMetadataItem, objectMetadataItems, + refetchAggregateQueries, ], ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 788950844..64fad6633 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -8,6 +8,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; @@ -48,6 +49,10 @@ export const useDestroyManyRecords = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDestroyManyRecordsMutationResponseField( objectMetadataItem.namePlural, ); @@ -127,6 +132,7 @@ export const useDestroyManyRecords = ({ } } + await refetchAggregateQueries(); return destroyedRecords; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts new file mode 100644 index 000000000..dee30f513 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts @@ -0,0 +1,27 @@ +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useApolloClient } from '@apollo/client'; + +export const useRefetchAggregateQueries = ({ + objectMetadataNamePlural, +}: { + objectMetadataNamePlural: string; +}) => { + const apolloClient = useApolloClient(); + const isAggregateQueryEnabled = useIsFeatureEnabled( + 'IS_AGGREGATE_QUERY_ENABLED', + ); + const refetchAggregateQueries = async () => { + if (isAggregateQueryEnabled) { + const queryName = getAggregateQueryName(objectMetadataNamePlural); + + await apolloClient.refetchQueries({ + include: [queryName], + }); + } + }; + + return { + refetchAggregateQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 8f77eaaee..4127bab0b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -7,6 +7,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; @@ -45,6 +46,10 @@ export const useUpdateOneRecord = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const updateOneRecord = async ({ idToUpdate, updateOneRecordInput, @@ -152,6 +157,7 @@ export const useUpdateOneRecord = < throw error; }); + await refetchAggregateQueries(); return updatedRecord?.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index f07b3e4cd..6935b6b35 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -6,7 +6,7 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { useAggregateManyRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn'; +import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; @@ -96,7 +96,7 @@ export const RecordBoardColumnHeader = () => { }; const { aggregateValue, aggregateLabel } = - useAggregateManyRecordsForRecordBoardColumn(); + useAggregateRecordsForRecordBoardColumn(); const { handleNewButtonClick } = useColumnNewCardActions( columnDefinition.id ?? '', diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx index bb1f48a8a..ce8911d97 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx @@ -11,7 +11,7 @@ import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; type RecordBoardColumnHeaderAggregateDropdownProps = { - aggregateValue: string | number; + aggregateValue?: string | number; aggregateLabel?: string; objectMetadataItem: ObjectMetadataItem; dropdownId: string; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx index d9e52f729..dd12c11f7 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx @@ -13,13 +13,16 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({ tooltip, }: { dropdownId: string; - value: string | number; + value?: string | number; tooltip?: string; }) => { return (
- + { {getAggregateOperationLabel(aggregateOperation)} - {availableFieldsIdsForAggregateOperation.map((fieldId) => { - const fieldMetadata = objectMetadataItem.fields.find( - (field) => field.id === fieldId, - ); + + {availableFieldsIdsForAggregateOperation.map((fieldId) => { + const fieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === fieldId, + ); - if (!fieldMetadata) return null; - return ( - + if (!fieldMetadata) return null; + return ( { @@ -54,9 +54,9 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123} text={fieldMetadata.label} /> - - ); - })} + ); + })} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx index 1d36b8aeb..eb1ab6dcb 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx @@ -66,19 +66,16 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { }} text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)} /> - - {Object.entries(availableAggregations).map( - ([ - availableAggregationOperation, - availableAggregationFieldsIdsForOperation, - ]) => - isEmpty(availableAggregationFieldsIdsForOperation) ? ( - <> - ) : ( - + {Object.entries(availableAggregations).map( + ([ + availableAggregationOperation, + availableAggregationFieldsIdsForOperation, + ]) => + isEmpty(availableAggregationFieldsIdsForOperation) ? ( + <> + ) : ( { setAggregateOperation( availableAggregationOperation as AGGREGATE_OPERATIONS, @@ -93,9 +90,9 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { )} hasSubMenu /> - - ), - )} + ), + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts rename to packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index 5a7c5560b..b16db61e0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -1,4 +1,4 @@ -import { useAggregateManyRecords } from '@/object-record/hooks/useAggregateManyRecords'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate'; @@ -13,7 +13,7 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; -export const useAggregateManyRecordsForRecordBoardColumn = () => { +export const useAggregateRecordsForRecordBoardColumn = () => { const isAggregateQueryEnabled = useIsFeatureEnabled( 'IS_AGGREGATE_QUERY_ENABLED', ); @@ -67,7 +67,7 @@ export const useAggregateManyRecordsForRecordBoardColumn = () => { : { eq: columnDefinition.value }, }; - const { data } = useAggregateManyRecords({ + const { data } = useAggregateRecords({ objectNameSingular: objectMetadataItem.nameSingular, recordGqlFieldsAggregate, filter, @@ -82,7 +82,7 @@ export const useAggregateManyRecordsForRecordBoardColumn = () => { ); return { - aggregateValue: value ?? recordCount, + aggregateValue: isAggregateQueryEnabled ? value : recordCount, aggregateLabel: isDefined(value) ? label : undefined, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts index 62696fcd6..85e4c5994 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts @@ -57,7 +57,7 @@ describe('buildRecordGqlFieldsAggregate', () => { }); expect(result).toEqual({ - amount: AGGREGATE_OPERATIONS.sum, + amount: [AGGREGATE_OPERATIONS.sum], }); }); @@ -74,7 +74,7 @@ describe('buildRecordGqlFieldsAggregate', () => { }); expect(result).toEqual({ - [MOCK_KANBAN_FIELD]: AGGREGATE_OPERATIONS.count, + [MOCK_KANBAN_FIELD]: [AGGREGATE_OPERATIONS.count], }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts index f7bca4930..d759868f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts @@ -1,4 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { isDefined } from '~/utils/isDefined'; @@ -11,7 +12,7 @@ export const buildRecordGqlFieldsAggregate = ({ objectMetadataItem: ObjectMetadataItem; recordIndexKanbanAggregateOperation: KanbanAggregateOperation; kanbanFieldName: string; -}) => { +}): RecordGqlFieldsAggregate => { let recordGqlFieldsAggregate = {}; const kanbanAggregateOperationFieldName = objectMetadataItem.fields?.find( @@ -30,14 +31,15 @@ export const buildRecordGqlFieldsAggregate = ({ ); } else { recordGqlFieldsAggregate = { - [kanbanFieldName]: AGGREGATE_OPERATIONS.count, + [kanbanFieldName]: [AGGREGATE_OPERATIONS.count], }; } } else { recordGqlFieldsAggregate = { - [kanbanAggregateOperationFieldName]: + [kanbanAggregateOperationFieldName]: [ recordIndexKanbanAggregateOperation?.operation ?? - AGGREGATE_OPERATIONS.count, + AGGREGATE_OPERATIONS.count, + ], }; } diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts index 9140d8405..52ef2b86d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts @@ -1,5 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { AggregateManyRecordsData } from '@/object-record/hooks/useAggregateManyRecords'; +import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; @@ -8,7 +8,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const computeAggregateValueAndLabel = ( - data: AggregateManyRecordsData, + data: AggregateRecordsData, objectMetadataItem: ObjectMetadataItem, recordIndexKanbanAggregateOperation: KanbanAggregateOperation, kanbanFieldName: string, diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts index a1f6c7aae..4ce449957 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts @@ -35,7 +35,7 @@ describe('generateAggregateQuery', () => { const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim(); expect(normalizedQuery).toBe( - 'query AggregateManyCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { id name createdAt } }', + 'query AggregateCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { id name createdAt } }', ); }); @@ -69,7 +69,7 @@ describe('generateAggregateQuery', () => { const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim(); expect(normalizedQuery).toBe( - 'query AggregateManyPeople($filter: PersonFilterInput) { people(filter: $filter) { id } }', + 'query AggregatePeople($filter: PersonFilterInput) { people(filter: $filter) { id } }', ); }); }); diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts new file mode 100644 index 000000000..b6ca9e632 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts @@ -0,0 +1,23 @@ +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; + +describe('getAggregateQueryName', () => { + it('should return the correct aggregate query name for a valid plural name', () => { + expect(getAggregateQueryName('opportunities')).toBe( + 'AggregateOpportunities', + ); + expect(getAggregateQueryName('companies')).toBe('AggregateCompanies'); + expect(getAggregateQueryName('people')).toBe('AggregatePeople'); + }); + + it('should throw an error when input is undefined', () => { + expect(() => getAggregateQueryName(undefined as any)).toThrow( + 'objectMetadataNamePlural is required', + ); + }); + + it('should throw an error when input is null', () => { + expect(() => getAggregateQueryName(null as any)).toThrow( + 'objectMetadataNamePlural is required', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts index 476d6fc37..d6feb05e5 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; import { capitalize } from '~/utils/string/capitalize'; export const generateAggregateQuery = ({ @@ -17,7 +18,7 @@ export const generateAggregateQuery = ({ .join('\n '); return gql` - query AggregateMany${capitalize(objectMetadataItem.namePlural)}($filter: ${capitalize( + query ${getAggregateQueryName(objectMetadataItem.namePlural)}($filter: ${capitalize( objectMetadataItem.nameSingular, )}FilterInput) { ${objectMetadataItem.namePlural}(filter: $filter) { diff --git a/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts new file mode 100644 index 000000000..e8b48509d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts @@ -0,0 +1,11 @@ +import { isDefined } from '~/utils/isDefined'; +import { capitalize } from '~/utils/string/capitalize'; + +export const getAggregateQueryName = ( + objectMetadataNamePlural: string, +): string => { + if (!isDefined(objectMetadataNamePlural)) { + throw new Error('objectMetadataNamePlural is required'); + } + return `Aggregate${capitalize(objectMetadataNamePlural)}`; +};