Fix COUNT operation on view group aggregate header (#9789)

Fixes
[sentry](https://twenty-v7.sentry.io/issues/6235128210/?referrer=discord&notification_uuid=898a081c-f8c7-42b8-b598-7660470a1975&alert_rule_id=15135099&alert_type=issue)

In a [previous work](https://github.com/twentyhq/twenty/pull/9749) I set
the default field to run totalCount aggregate operation on to the "name"
field, which I was wrong think was present on all objects.
This commit is contained in:
Marie
2025-01-22 15:05:38 +01:00
committed by GitHub
parent 8213995887
commit d759559506
9 changed files with 78 additions and 78 deletions

View File

@ -6,7 +6,6 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/Agg
import { FieldMetadataType } from '~/generated-metadata/graphql';
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
const MOCK_KANBAN_FIELD = 'stage';
describe('buildRecordGqlFieldsAggregateForView', () => {
const mockObjectMetadata: ObjectMetadataItem = {
@ -53,7 +52,6 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
const result = buildRecordGqlFieldsAggregateForView({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: kanbanAggregateOperation,
fieldNameForCount: MOCK_KANBAN_FIELD,
});
expect(result).toEqual({
@ -70,11 +68,10 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
const result = buildRecordGqlFieldsAggregateForView({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: operation,
fieldNameForCount: MOCK_KANBAN_FIELD,
});
expect(result).toEqual({
[MOCK_KANBAN_FIELD]: [AGGREGATE_OPERATIONS.count],
id: [AGGREGATE_OPERATIONS.count],
});
});
@ -88,7 +85,6 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
buildRecordGqlFieldsAggregateForView({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: operation,
fieldNameForCount: MOCK_KANBAN_FIELD,
}),
).toThrow(
`No field found to compute aggregate operation ${AGGREGATE_OPERATIONS.sum} on object ${mockObjectMetadata.nameSingular}`,

View File

@ -9,7 +9,6 @@ import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constant
import { FieldMetadataType } from '~/generated/graphql';
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
const MOCK_KANBAN_FIELD_NAME = 'stage';
describe('computeAggregateValueAndLabel', () => {
const mockObjectMetadata: ObjectMetadataItem = {
@ -36,7 +35,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -55,7 +53,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -93,7 +90,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithPercentageField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -131,7 +127,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDecimalsField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -166,7 +161,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -201,7 +195,6 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
@ -214,7 +207,7 @@ describe('computeAggregateValueAndLabel', () => {
it('should default to count when field not found', () => {
const mockData = {
[MOCK_KANBAN_FIELD_NAME]: {
id: {
[AGGREGATE_OPERATIONS.count]: 42,
},
} as AggregateRecordsData;
@ -222,7 +215,6 @@ describe('computeAggregateValueAndLabel', () => {
const result = computeAggregateValueAndLabel({
data: mockData,
objectMetadataItem: mockObjectMetadata,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});

View File

@ -2,16 +2,15 @@ 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 { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from 'twenty-shared';
import { isDefined } from '~/utils/isDefined';
export const buildRecordGqlFieldsAggregateForView = ({
objectMetadataItem,
recordIndexKanbanAggregateOperation,
fieldNameForCount,
}: {
objectMetadataItem: ObjectMetadataItem;
recordIndexKanbanAggregateOperation: KanbanAggregateOperation;
fieldNameForCount: string;
}): RecordGqlFieldsAggregate => {
let recordGqlFieldsAggregate = {};
@ -31,7 +30,9 @@ export const buildRecordGqlFieldsAggregateForView = ({
);
} else {
recordGqlFieldsAggregate = {
[fieldNameForCount]: [AGGREGATE_OPERATIONS.count],
[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]: [
AGGREGATE_OPERATIONS.count,
],
};
}
} else {

View File

@ -9,6 +9,7 @@ import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty';
import { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number';
@ -21,7 +22,6 @@ export const computeAggregateValueAndLabel = ({
objectMetadataItem,
fieldMetadataId,
aggregateOperation,
fallbackFieldName,
dateFormat,
timeFormat,
timeZone,
@ -30,7 +30,6 @@ export const computeAggregateValueAndLabel = ({
objectMetadataItem: ObjectMetadataItem;
fieldMetadataId?: string | null;
aggregateOperation?: ExtendedAggregateOperations | null;
fallbackFieldName?: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
timeZone: string;
@ -43,11 +42,11 @@ export const computeAggregateValueAndLabel = ({
);
if (!isDefined(field)) {
if (!fallbackFieldName) {
throw new Error('Missing fallback field name');
}
return {
value: data?.[fallbackFieldName]?.[AGGREGATE_OPERATIONS.count],
value:
data?.[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]?.[
AGGREGATE_OPERATIONS.count
],
label: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`,
labelWithFieldName: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`,
};

View File

@ -21,7 +21,6 @@ type UseAggregateRecordsProps = {
export const useAggregateRecordsForHeader = ({
objectMetadataItem,
additionalFilters = {},
fallbackFieldName,
}: UseAggregateRecordsProps) => {
const recordIndexViewFilterGroups = useRecoilValue(
recordIndexViewFilterGroupsState,
@ -47,7 +46,6 @@ export const useAggregateRecordsForHeader = ({
const recordGqlFieldsAggregate = buildRecordGqlFieldsAggregateForView({
objectMetadataItem,
recordIndexKanbanAggregateOperation,
fieldNameForCount: fallbackFieldName,
});
const { data } = useAggregateRecords({
@ -61,7 +59,6 @@ export const useAggregateRecordsForHeader = ({
objectMetadataItem,
fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId,
aggregateOperation: recordIndexKanbanAggregateOperation?.operation,
fallbackFieldName,
dateFormat,
timeFormat,
timeZone,

View File

@ -2,7 +2,11 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
import {
capitalize,
FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION,
isFieldMetadataDateKind,
} from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type NameForAggregation = {
@ -16,59 +20,66 @@ type Aggregations = {
export const getAvailableAggregationsFromObjectFields = (
fields: FieldMetadataItem[],
): Aggregations => {
return fields.reduce<Record<string, NameForAggregation>>((acc, field) => {
if (field.isSystem === true) {
return acc;
}
return fields.reduce<Record<string, NameForAggregation>>(
(acc, field) => {
if (field.isSystem === true) {
return acc;
}
if (field.type === FieldMetadataType.RELATION) {
acc[field.name] = {
[AGGREGATE_OPERATIONS.count]: 'totalCount',
};
return acc;
}
if (field.type === FieldMetadataType.RELATION) {
acc[field.name] = {
[AGGREGATE_OPERATIONS.countUniqueValues]: `countUniqueValues${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.countEmpty]: `countEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.countNotEmpty]: `countNotEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.percentageEmpty]: `percentageEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.percentageNotEmpty]: `percentageNotEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.count]: 'totalCount',
};
if (field.type === FieldMetadataType.NUMBER) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.sum]: `sum${capitalize(field.name)}`,
};
}
if (field.type === FieldMetadataType.CURRENCY) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.sum]: `sum${capitalize(field.name)}AmountMicros`,
};
}
if (isFieldMetadataDateKind(field.type) === true) {
acc[field.name] = {
...acc[field.name],
[DATE_AGGREGATE_OPERATIONS.earliest]: `min${capitalize(field.name)}`,
[DATE_AGGREGATE_OPERATIONS.latest]: `max${capitalize(field.name)}`,
};
}
if (acc[field.name] === undefined) {
acc[field.name] = {};
}
return acc;
}
acc[field.name] = {
[AGGREGATE_OPERATIONS.countUniqueValues]: `countUniqueValues${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.countEmpty]: `countEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.countNotEmpty]: `countNotEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.percentageEmpty]: `percentageEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.percentageNotEmpty]: `percentageNotEmpty${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.count]: 'totalCount',
};
if (field.type === FieldMetadataType.NUMBER) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.sum]: `sum${capitalize(field.name)}`,
};
}
if (field.type === FieldMetadataType.CURRENCY) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.sum]: `sum${capitalize(field.name)}AmountMicros`,
};
}
if (isFieldMetadataDateKind(field.type) === true) {
acc[field.name] = {
...acc[field.name],
[DATE_AGGREGATE_OPERATIONS.earliest]: `min${capitalize(field.name)}`,
[DATE_AGGREGATE_OPERATIONS.latest]: `max${capitalize(field.name)}`,
};
}
if (acc[field.name] === undefined) {
acc[field.name] = {};
}
return acc;
}, {});
},
{
[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]: {
[AGGREGATE_OPERATIONS.count]: 'totalCount',
},
},
);
};

View File

@ -3,6 +3,7 @@ import { GraphQLISODateTime } from '@nestjs/graphql';
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import {
capitalize,
FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION,
FieldMetadataType,
isFieldMetadataDateKind,
} from 'twenty-shared';
@ -176,7 +177,7 @@ export const getAvailableAggregationsFromObjectFields = (
totalCount: {
type: GraphQLInt,
description: `Total number of records in the connection`,
fromField: 'id',
fromField: FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION,
fromFieldType: FieldMetadataType.UUID,
aggregateOperation: AGGREGATE_OPERATIONS.count,
},

View File

@ -0,0 +1 @@
export const FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION = 'id';

View File

@ -1,3 +1,4 @@
export * from './constants/FieldForTotalCountAggregateOperation';
export * from './constants/TwentyCompaniesBaseUrl';
export * from './constants/TwentyIconsBaseUrl';
export * from './types/FieldMetadataType';
@ -5,3 +6,4 @@ export * from './utils/fieldMetadata/isFieldMetadataDateKind';
export * from './utils/image/getImageAbsoluteURI';
export * from './utils/strings';
export * from './workspace';