Refetch aggregate queries on record creation/update/deletion of record (#8885)

Closes #8755.
Refetching the aggregate queries on an object following creation,
update, deletion of a record.
This commit is contained in:
Marie 2024-12-05 15:23:54 +01:00 committed by GitHub
parent 9ed9b4746a
commit 26ff344f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 590 additions and 71 deletions

View File

@ -1,3 +1,3 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS>; export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS[]>;

View File

@ -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
}
};

View File

@ -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,
}),
);
});
});

View File

@ -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,
}),
}),
);
});
});

View File

@ -9,12 +9,19 @@ import {
variables, variables,
} from '@/object-record/hooks/__mocks__/useCreateManyRecords'; } from '@/object-record/hooks/__mocks__/useCreateManyRecords';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
jest.mock('uuid', () => ({ jest.mock('uuid', () => ({
v4: jest.fn(), v4: jest.fn(),
})); }));
jest.mock('@/object-record/hooks/useRefetchAggregateQueries');
const mockRefetchAggregateQueries = jest.fn();
(useRefetchAggregateQueries as jest.Mock).mockReturnValue({
refetchAggregateQueries: mockRefetchAggregateQueries,
});
mocked(v4) mocked(v4)
.mockReturnValueOnce(variables.data[0].id) .mockReturnValueOnce(variables.data[0].id)
.mockReturnValueOnce(variables.data[1].id); .mockReturnValueOnce(variables.data[1].id);
@ -40,6 +47,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
}); });
describe('useCreateManyRecords', () => { describe('useCreateManyRecords', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
@ -57,5 +67,6 @@ describe('useCreateManyRecords', () => {
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -6,6 +6,7 @@ import {
responseData, responseData,
} from '@/object-record/hooks/__mocks__/useCreateOneRecord'; } from '@/object-record/hooks/__mocks__/useCreateOneRecord';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
@ -15,6 +16,12 @@ jest.mock('uuid', () => ({
v4: jest.fn(() => personId), v4: jest.fn(() => personId),
})); }));
jest.mock('@/object-record/hooks/useRefetchAggregateQueries');
const mockRefetchAggregateQueries = jest.fn();
(useRefetchAggregateQueries as jest.Mock).mockReturnValue({
refetchAggregateQueries: mockRefetchAggregateQueries,
});
const mocks = [ const mocks = [
{ {
request: { request: {
@ -34,6 +41,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
}); });
describe('useCreateOneRecord', () => { describe('useCreateOneRecord', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
@ -52,5 +62,6 @@ describe('useCreateOneRecord', () => {
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -6,6 +6,7 @@ import {
variables, variables,
} from '@/object-record/hooks/__mocks__/useDeleteManyRecords'; } from '@/object-record/hooks/__mocks__/useDeleteManyRecords';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { act } from 'react'; import { act } from 'react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; 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({ const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks, apolloMocks: mocks,
}); });
describe('useDeleteManyRecords', () => { describe('useDeleteManyRecords', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => useDeleteManyRecords({ objectNameSingular: 'person' }), () => useDeleteManyRecords({ objectNameSingular: 'person' }),
@ -48,5 +58,6 @@ describe('useDeleteManyRecords', () => {
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -7,6 +7,7 @@ import {
variables, variables,
} from '@/object-record/hooks/__mocks__/useDeleteOneRecord'; } from '@/object-record/hooks/__mocks__/useDeleteOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; 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({ const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks, apolloMocks: mocks,
}); });
describe('useDeleteOneRecord', () => { describe('useDeleteOneRecord', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => useDeleteOneRecord({ objectNameSingular: 'person' }), () => useDeleteOneRecord({ objectNameSingular: 'person' }),
@ -45,5 +55,6 @@ describe('useDeleteOneRecord', () => {
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -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',
);
});
});

View File

@ -5,7 +5,9 @@ import {
responseData, responseData,
variables, variables,
} from '@/object-record/hooks/__mocks__/useUpdateOneRecord'; } from '@/object-record/hooks/__mocks__/useUpdateOneRecord';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { expect } from '@storybook/test';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; 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({ const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks, apolloMocks: mocks,
}); });
@ -42,6 +50,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
const idToUpdate = '36abbb63-34ed-4a16-89f5-f549ac55d0f9'; const idToUpdate = '36abbb63-34ed-4a16-89f5-f549ac55d0f9';
describe('useUpdateOneRecord', () => { describe('useUpdateOneRecord', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => useUpdateOneRecord({ objectNameSingular: 'person' }), () => useUpdateOneRecord({ objectNameSingular: 'person' }),
@ -61,5 +72,6 @@ describe('useUpdateOneRecord', () => {
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -4,18 +4,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate';
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; 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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export type AggregateManyRecordsData = { export type AggregateRecordsData = {
[fieldName: string]: { [fieldName: string]: {
[operation in AGGREGATE_OPERATIONS]?: string | number | undefined; [operation in AGGREGATE_OPERATIONS]?: string | number | undefined;
}; };
}; };
export const useAggregateManyRecords = ({ export const useAggregateRecords = ({
objectNameSingular, objectNameSingular,
filter, filter,
recordGqlFieldsAggregate, recordGqlFieldsAggregate,
@ -30,7 +30,7 @@ export const useAggregateManyRecords = ({
objectNameSingular, objectNameSingular,
}); });
const { aggregateQuery, gqlFieldToFieldMap } = useAggregateManyRecordsQuery({ const { aggregateQuery, gqlFieldToFieldMap } = useAggregateRecordsQuery({
objectNameSingular, objectNameSingular,
recordGqlFieldsAggregate, recordGqlFieldsAggregate,
}); });
@ -45,7 +45,7 @@ export const useAggregateManyRecords = ({
}, },
); );
const formattedData: AggregateManyRecordsData = {}; const formattedData: AggregateRecordsData = {};
if (!isEmpty(data)) { if (!isEmpty(data)) {
Object.entries(data?.[objectMetadataItem.namePlural] ?? {})?.forEach( Object.entries(data?.[objectMetadataItem.namePlural] ?? {})?.forEach(

View File

@ -14,7 +14,7 @@ export type GqlFieldToFieldMap = {
]; ];
}; };
export const useAggregateManyRecordsQuery = ({ export const useAggregateRecordsQuery = ({
objectNameSingular, objectNameSingular,
recordGqlFieldsAggregate = {}, recordGqlFieldsAggregate = {},
}: { }: {
@ -34,15 +34,8 @@ export const useAggregateManyRecordsQuery = ({
const gqlFieldToFieldMap: GqlFieldToFieldMap = {}; const gqlFieldToFieldMap: GqlFieldToFieldMap = {};
Object.entries(recordGqlFieldsAggregate).forEach( Object.entries(recordGqlFieldsAggregate).forEach(
([fieldName, aggregateOperation]) => { ([fieldName, aggregateOperations]) => {
if ( aggregateOperations.forEach((aggregateOperation) => {
!isDefined(fieldName) &&
aggregateOperation === AGGREGATE_OPERATIONS.count
) {
recordGqlFields.totalCount = true;
return;
}
const fieldToQuery = const fieldToQuery =
availableAggregations[fieldName]?.[aggregateOperation]; availableAggregations[fieldName]?.[aggregateOperation];
@ -54,6 +47,7 @@ export const useAggregateManyRecordsQuery = ({
gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation];
recordGqlFields[fieldToQuery] = true; recordGqlFields[fieldToQuery] = true;
});
}, },
); );

View File

@ -11,6 +11,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation'; import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@ -51,6 +52,10 @@ export const useCreateManyRecords = <
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const createManyRecords = async ( const createManyRecords = async (
recordsToCreate: Partial<CreatedObjectRecord>[], recordsToCreate: Partial<CreatedObjectRecord>[],
upsert?: boolean, upsert?: boolean,
@ -141,6 +146,7 @@ export const useCreateManyRecords = <
throw error; throw error;
}); });
await refetchAggregateQueries();
return createdObjects.data?.[mutationResponseField] ?? []; return createdObjects.data?.[mutationResponseField] ?? [];
}; };

View File

@ -12,6 +12,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@ -55,6 +56,10 @@ export const useCreateOneRecord = <
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => { const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
setLoading(true); setLoading(true);
@ -131,6 +136,7 @@ export const useCreateOneRecord = <
throw error; throw error;
}); });
await refetchAggregateQueries();
return createdObject.data?.[mutationResponseField] ?? null; return createdObject.data?.[mutationResponseField] ?? null;
}; };

View File

@ -9,6 +9,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -51,6 +52,10 @@ export const useDeleteManyRecords = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const mutationResponseField = getDeleteManyRecordsMutationResponseField( const mutationResponseField = getDeleteManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -194,7 +199,7 @@ export const useDeleteManyRecords = ({
await sleep(options.delayInMsBetweenRequests); await sleep(options.delayInMsBetweenRequests);
} }
} }
await refetchAggregateQueries();
return deletedRecords; return deletedRecords;
}; };

View File

@ -8,6 +8,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -35,6 +36,10 @@ export const useDeleteOneRecord = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const mutationResponseField = const mutationResponseField =
getDeleteOneRecordMutationResponseField(objectNameSingular); getDeleteOneRecordMutationResponseField(objectNameSingular);
@ -126,6 +131,7 @@ export const useDeleteOneRecord = ({
throw error; throw error;
}); });
await refetchAggregateQueries();
return deletedRecord.data?.[mutationResponseField] ?? null; return deletedRecord.data?.[mutationResponseField] ?? null;
}, },
[ [
@ -135,6 +141,7 @@ export const useDeleteOneRecord = ({
mutationResponseField, mutationResponseField,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
refetchAggregateQueries,
], ],
); );

View File

@ -8,6 +8,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation'; import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField'; import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -48,6 +49,10 @@ export const useDestroyManyRecords = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const mutationResponseField = getDestroyManyRecordsMutationResponseField( const mutationResponseField = getDestroyManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -127,6 +132,7 @@ export const useDestroyManyRecords = ({
} }
} }
await refetchAggregateQueries();
return destroyedRecords; return destroyedRecords;
}; };

View File

@ -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,
};
};

View File

@ -7,6 +7,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
@ -45,6 +46,10 @@ export const useUpdateOneRecord = <
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
const updateOneRecord = async ({ const updateOneRecord = async ({
idToUpdate, idToUpdate,
updateOneRecordInput, updateOneRecordInput,
@ -152,6 +157,7 @@ export const useUpdateOneRecord = <
throw error; throw error;
}); });
await refetchAggregateQueries();
return updatedRecord?.data?.[mutationResponseField] ?? null; return updatedRecord?.data?.[mutationResponseField] ?? null;
}; };

View File

@ -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 { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; 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 { 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 { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
@ -96,7 +96,7 @@ export const RecordBoardColumnHeader = () => {
}; };
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateManyRecordsForRecordBoardColumn(); useAggregateRecordsForRecordBoardColumn();
const { handleNewButtonClick } = useColumnNewCardActions( const { handleNewButtonClick } = useColumnNewCardActions(
columnDefinition.id ?? '', columnDefinition.id ?? '',

View File

@ -11,7 +11,7 @@ import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
type RecordBoardColumnHeaderAggregateDropdownProps = { type RecordBoardColumnHeaderAggregateDropdownProps = {
aggregateValue: string | number; aggregateValue?: string | number;
aggregateLabel?: string; aggregateLabel?: string;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
dropdownId: string; dropdownId: string;

View File

@ -13,13 +13,16 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
tooltip, tooltip,
}: { }: {
dropdownId: string; dropdownId: string;
value: string | number; value?: string | number;
tooltip?: string; tooltip?: string;
}) => { }) => {
return ( return (
<div id={dropdownId}> <div id={dropdownId}>
<StyledButton> <StyledButton>
<Tag text={formatNumber(Number(value))} color={'transparent'} /> <Tag
text={value ? formatNumber(Number(value)) : '-'}
color={'transparent'}
/>
<AppTooltip <AppTooltip
anchorSelect={`#${dropdownId}`} anchorSelect={`#${dropdownId}`}
content={tooltip} content={tooltip}

View File

@ -34,6 +34,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}> <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
{getAggregateOperationLabel(aggregateOperation)} {getAggregateOperationLabel(aggregateOperation)}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer>
{availableFieldsIdsForAggregateOperation.map((fieldId) => { {availableFieldsIdsForAggregateOperation.map((fieldId) => {
const fieldMetadata = objectMetadataItem.fields.find( const fieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === fieldId, (field) => field.id === fieldId,
@ -41,7 +42,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
if (!fieldMetadata) return null; if (!fieldMetadata) return null;
return ( return (
<DropdownMenuItemsContainer>
<MenuItem <MenuItem
key={fieldId} key={fieldId}
onClick={() => { onClick={() => {
@ -54,9 +54,9 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123} LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123}
text={fieldMetadata.label} text={fieldMetadata.label}
/> />
</DropdownMenuItemsContainer>
); );
})} })}
</DropdownMenuItemsContainer>
</> </>
); );
}; };

View File

@ -66,7 +66,6 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
}} }}
text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)} text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}
/> />
</DropdownMenuItemsContainer>
{Object.entries(availableAggregations).map( {Object.entries(availableAggregations).map(
([ ([
availableAggregationOperation, availableAggregationOperation,
@ -75,10 +74,8 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
isEmpty(availableAggregationFieldsIdsForOperation) ? ( isEmpty(availableAggregationFieldsIdsForOperation) ? (
<></> <></>
) : ( ) : (
<DropdownMenuItemsContainer
key={`aggregate-dropdown-menu-content-${availableAggregationOperation}`}
>
<RecordBoardColumnHeaderAggregateDropdownMenuItem <RecordBoardColumnHeaderAggregateDropdownMenuItem
key={`aggregate-dropdown-menu-content-${availableAggregationOperation}`}
onContentChange={() => { onContentChange={() => {
setAggregateOperation( setAggregateOperation(
availableAggregationOperation as AGGREGATE_OPERATIONS, availableAggregationOperation as AGGREGATE_OPERATIONS,
@ -93,9 +90,9 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
)} )}
hasSubMenu hasSubMenu
/> />
</DropdownMenuItemsContainer>
), ),
)} )}
</DropdownMenuItemsContainer>
</> </>
); );
}; };

View File

@ -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 { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate'; 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 { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const useAggregateManyRecordsForRecordBoardColumn = () => { export const useAggregateRecordsForRecordBoardColumn = () => {
const isAggregateQueryEnabled = useIsFeatureEnabled( const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED', 'IS_AGGREGATE_QUERY_ENABLED',
); );
@ -67,7 +67,7 @@ export const useAggregateManyRecordsForRecordBoardColumn = () => {
: { eq: columnDefinition.value }, : { eq: columnDefinition.value },
}; };
const { data } = useAggregateManyRecords({ const { data } = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate, recordGqlFieldsAggregate,
filter, filter,
@ -82,7 +82,7 @@ export const useAggregateManyRecordsForRecordBoardColumn = () => {
); );
return { return {
aggregateValue: value ?? recordCount, aggregateValue: isAggregateQueryEnabled ? value : recordCount,
aggregateLabel: isDefined(value) ? label : undefined, aggregateLabel: isDefined(value) ? label : undefined,
}; };
}; };

View File

@ -57,7 +57,7 @@ describe('buildRecordGqlFieldsAggregate', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
amount: AGGREGATE_OPERATIONS.sum, amount: [AGGREGATE_OPERATIONS.sum],
}); });
}); });
@ -74,7 +74,7 @@ describe('buildRecordGqlFieldsAggregate', () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
[MOCK_KANBAN_FIELD]: AGGREGATE_OPERATIONS.count, [MOCK_KANBAN_FIELD]: [AGGREGATE_OPERATIONS.count],
}); });
}); });

View File

@ -1,4 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -11,7 +12,7 @@ export const buildRecordGqlFieldsAggregate = ({
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
recordIndexKanbanAggregateOperation: KanbanAggregateOperation; recordIndexKanbanAggregateOperation: KanbanAggregateOperation;
kanbanFieldName: string; kanbanFieldName: string;
}) => { }): RecordGqlFieldsAggregate => {
let recordGqlFieldsAggregate = {}; let recordGqlFieldsAggregate = {};
const kanbanAggregateOperationFieldName = objectMetadataItem.fields?.find( const kanbanAggregateOperationFieldName = objectMetadataItem.fields?.find(
@ -30,14 +31,15 @@ export const buildRecordGqlFieldsAggregate = ({
); );
} else { } else {
recordGqlFieldsAggregate = { recordGqlFieldsAggregate = {
[kanbanFieldName]: AGGREGATE_OPERATIONS.count, [kanbanFieldName]: [AGGREGATE_OPERATIONS.count],
}; };
} }
} else { } else {
recordGqlFieldsAggregate = { recordGqlFieldsAggregate = {
[kanbanAggregateOperationFieldName]: [kanbanAggregateOperationFieldName]: [
recordIndexKanbanAggregateOperation?.operation ?? recordIndexKanbanAggregateOperation?.operation ??
AGGREGATE_OPERATIONS.count, AGGREGATE_OPERATIONS.count,
],
}; };
} }

View File

@ -1,5 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { isDefined } from '~/utils/isDefined';
export const computeAggregateValueAndLabel = ( export const computeAggregateValueAndLabel = (
data: AggregateManyRecordsData, data: AggregateRecordsData,
objectMetadataItem: ObjectMetadataItem, objectMetadataItem: ObjectMetadataItem,
recordIndexKanbanAggregateOperation: KanbanAggregateOperation, recordIndexKanbanAggregateOperation: KanbanAggregateOperation,
kanbanFieldName: string, kanbanFieldName: string,

View File

@ -35,7 +35,7 @@ describe('generateAggregateQuery', () => {
const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim(); const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim();
expect(normalizedQuery).toBe( 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(); const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim();
expect(normalizedQuery).toBe( expect(normalizedQuery).toBe(
'query AggregateManyPeople($filter: PersonFilterInput) { people(filter: $filter) { id } }', 'query AggregatePeople($filter: PersonFilterInput) { people(filter: $filter) { id } }',
); );
}); });
}); });

View File

@ -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',
);
});
});

View File

@ -2,6 +2,7 @@ import gql from 'graphql-tag';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
export const generateAggregateQuery = ({ export const generateAggregateQuery = ({
@ -17,7 +18,7 @@ export const generateAggregateQuery = ({
.join('\n '); .join('\n ');
return gql` return gql`
query AggregateMany${capitalize(objectMetadataItem.namePlural)}($filter: ${capitalize( query ${getAggregateQueryName(objectMetadataItem.namePlural)}($filter: ${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}FilterInput) { )}FilterInput) {
${objectMetadataItem.namePlural}(filter: $filter) { ${objectMetadataItem.namePlural}(filter: $filter) {

View File

@ -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)}`;
};