Marie
2025-01-06 17:57:32 +01:00
committed by GitHub
parent b22a598d7d
commit a9b95bcf03
30 changed files with 503 additions and 328 deletions

View File

@ -1,7 +1,8 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
import { FieldMetadataType } from '~/generated/graphql';
@ -9,59 +10,135 @@ const AMOUNT_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
const PRICE_FIELD_ID = '9d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0b';
const NAME_FIELD_ID = '5d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0c';
describe('getAvailableFieldsIdsForAggregationFromObjectFields', () => {
const mockFields = [
{ id: AMOUNT_FIELD_ID, type: FieldMetadataType.Number, name: 'amount' },
{ id: PRICE_FIELD_ID, type: FieldMetadataType.Currency, name: 'price' },
{ id: NAME_FIELD_ID, type: FieldMetadataType.Text, name: 'name' },
];
const FIELDS_MOCKS = [
{ id: AMOUNT_FIELD_ID, type: FieldMetadataType.Number, name: 'amount' },
{ id: PRICE_FIELD_ID, type: FieldMetadataType.Currency, name: 'price' },
{ id: NAME_FIELD_ID, type: FieldMetadataType.Text, name: 'name' },
];
it('should correctly map fields to available aggregate operations', () => {
jest.mock(
'@/object-record/utils/getAvailableAggregationsFromObjectFields',
() => ({
getAvailableAggregationsFromObjectFields: jest.fn().mockReturnValue({
amount: {
[AGGREGATE_OPERATIONS.sum]: 'sumAmount',
[AGGREGATE_OPERATIONS.avg]: 'avgAmount',
[AGGREGATE_OPERATIONS.min]: 'minAmount',
[AGGREGATE_OPERATIONS.max]: 'maxAmount',
[AGGREGATE_OPERATIONS.count]: 'totalCount',
[AGGREGATE_OPERATIONS.countUniqueValues]: 'countUniqueValuesAmount',
[AGGREGATE_OPERATIONS.countEmpty]: 'countEmptyAmount',
[AGGREGATE_OPERATIONS.countNotEmpty]: 'countNotEmptyAmount',
[AGGREGATE_OPERATIONS.percentageEmpty]: 'percentageEmptyAmount',
[AGGREGATE_OPERATIONS.percentageNotEmpty]: 'percentageNotEmptyAmount',
},
price: {
[AGGREGATE_OPERATIONS.sum]: 'sumPriceAmountMicros',
[AGGREGATE_OPERATIONS.avg]: 'avgPriceAmountMicros',
[AGGREGATE_OPERATIONS.min]: 'minPriceAmountMicros',
[AGGREGATE_OPERATIONS.max]: 'maxPriceAmountMicros',
[AGGREGATE_OPERATIONS.count]: 'totalCount',
[AGGREGATE_OPERATIONS.countUniqueValues]:
'countUniqueValuesPriceAmountMicros',
[AGGREGATE_OPERATIONS.countEmpty]: 'countEmptyPriceAmountMicros',
[AGGREGATE_OPERATIONS.countNotEmpty]: 'countNotEmptyPriceAmountMicros',
[AGGREGATE_OPERATIONS.percentageEmpty]:
'percentageEmptyPriceAmountMicros',
[AGGREGATE_OPERATIONS.percentageNotEmpty]:
'percentageNotEmptyPriceAmountMicros',
},
name: {
[AGGREGATE_OPERATIONS.count]: 'totalCount',
[AGGREGATE_OPERATIONS.countUniqueValues]: 'countUniqueValuesName',
[AGGREGATE_OPERATIONS.countEmpty]: 'countEmptyName',
[AGGREGATE_OPERATIONS.countNotEmpty]: 'countNotEmptyName',
[AGGREGATE_OPERATIONS.percentageEmpty]: 'percentageEmptyName',
[AGGREGATE_OPERATIONS.percentageNotEmpty]: 'percentageNotEmptyName',
},
}),
}),
);
describe('getAvailableFieldsIdsForAggregationFromObjectFields', () => {
it('should handle empty fields array', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields(
mockFields as FieldMetadataItem[],
[],
COUNT_AGGREGATE_OPERATION_OPTIONS,
);
expect(result[AGGREGATE_OPERATIONS.sum]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
]);
expect(result[AGGREGATE_OPERATIONS.avg]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
]);
expect(result[AGGREGATE_OPERATIONS.min]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
]);
expect(result[AGGREGATE_OPERATIONS.max]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
]);
});
it('should exclude non-numeric fields', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields([
{ id: NAME_FIELD_ID, type: FieldMetadataType.Text } as FieldMetadataItem,
]);
Object.values(AGGREGATE_OPERATIONS).forEach((operation) => {
if (!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(operation)) {
expect(
result[operation as AggregateOperationsOmittingStandardOperations],
).toEqual([]);
}
COUNT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toEqual([]);
});
});
it('should handle empty fields array', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields([]);
describe('with count aggregate operations', () => {
it('should include all fields', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields(
FIELDS_MOCKS as FieldMetadataItem[],
COUNT_AGGREGATE_OPERATION_OPTIONS,
);
Object.values(AGGREGATE_OPERATIONS).forEach((operation) => {
if (!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(operation)) {
expect(
result[operation as AggregateOperationsOmittingStandardOperations],
).toEqual([]);
}
COUNT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
NAME_FIELD_ID,
]);
});
PERCENT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
});
});
describe('with percentage aggregate operations', () => {
it('should include all fields', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields(
FIELDS_MOCKS as FieldMetadataItem[],
PERCENT_AGGREGATE_OPERATION_OPTIONS,
);
PERCENT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toEqual([
AMOUNT_FIELD_ID,
PRICE_FIELD_ID,
NAME_FIELD_ID,
]);
});
COUNT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
});
});
describe('with non standard aggregate operations', () => {
it('should exclude non-numeric fields', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields(
FIELDS_MOCKS as FieldMetadataItem[],
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
);
COUNT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
PERCENT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toBeUndefined();
});
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => {
expect(result[operation]).toEqual([AMOUNT_FIELD_ID, PRICE_FIELD_ID]);
});
});
});
});

View File

@ -1,26 +1,58 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
describe('initializeAvailableFieldsForAggregateOperationMap', () => {
it('should initialize empty arrays for each aggregate operation', () => {
const result = initializeAvailableFieldsForAggregateOperationMap();
it('should initialize empty arrays for each non standard aggregate operation', () => {
const result = initializeAvailableFieldsForAggregateOperationMap(
Object.keys(
FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION,
) as AGGREGATE_OPERATIONS[],
);
expect(Object.keys(result)).toEqual(
Object.keys(FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION),
Object.keys(FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION),
);
Object.values(result).forEach((array) => {
expect(array).toEqual([]);
});
});
it('should not include count operation', () => {
const result = initializeAvailableFieldsForAggregateOperationMap();
it('should not include count operation when called with non standard aggregate operations', () => {
const result = initializeAvailableFieldsForAggregateOperationMap(
Object.keys(
FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION,
) as AGGREGATE_OPERATIONS[],
);
expect(
result[
AGGREGATE_OPERATIONS.count as AggregateOperationsOmittingStandardOperations
],
).toBeUndefined();
});
it('should include count operation when called with count aggregate operations', () => {
const result = initializeAvailableFieldsForAggregateOperationMap(
COUNT_AGGREGATE_OPERATION_OPTIONS,
);
expect(result[AGGREGATE_OPERATIONS.count]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.countEmpty]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.countNotEmpty]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.countUniqueValues]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.min]).toBeUndefined();
expect(result[AGGREGATE_OPERATIONS.percentageEmpty]).toBeUndefined();
});
it('should include percent operation when called with count aggregate operations', () => {
const result = initializeAvailableFieldsForAggregateOperationMap(
PERCENT_AGGREGATE_OPERATION_OPTIONS,
);
expect(result[AGGREGATE_OPERATIONS.percentageEmpty]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.percentageNotEmpty]).toEqual([]);
expect(result[AGGREGATE_OPERATIONS.count]).toBeUndefined();
expect(result[AGGREGATE_OPERATIONS.min]).toBeUndefined();
});
});

View File

@ -1,6 +1,5 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { getColumnNameForAggregateOperation } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { capitalize } from '~/utils/string/capitalize';
@ -16,6 +15,10 @@ export const getAvailableAggregationsFromObjectFields = (
fields: FieldMetadataItem[],
): Aggregations => {
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',
@ -23,28 +26,15 @@ export const getAvailableAggregationsFromObjectFields = (
return acc;
}
const columnName = getColumnNameForAggregateOperation(
field.name,
field.type,
);
acc[field.name] = {
[AGGREGATE_OPERATIONS.countUniqueValues]: `countUniqueValues${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.countEmpty]: `countEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.countNotEmpty]: `countNotEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.percentageEmpty]: `percentageEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.percentageNotEmpty]: `percentageNotEmpty${capitalize(columnName)}`,
[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.DateTime) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
};
}
if (field.type === FieldMetadataType.Number) {
acc[field.name] = {
...acc[field.name],

View File

@ -1,32 +1,30 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
import { isDefined } from '~/utils/isDefined';
export const getAvailableFieldsIdsForAggregationFromObjectFields = (
fields: FieldMetadataItem[],
targetAggregateOperations: AGGREGATE_OPERATIONS[],
): AvailableFieldsForAggregateOperation => {
const aggregationMap = initializeAvailableFieldsForAggregateOperationMap();
const aggregationMap = initializeAvailableFieldsForAggregateOperationMap(
targetAggregateOperations,
);
const allAggregations = getAvailableAggregationsFromObjectFields(fields);
return fields.reduce((acc, field) => {
Object.keys(FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION).forEach(
(aggregateOperation) => {
const typedAggregateOperation =
aggregateOperation as AggregateOperationsOmittingStandardOperations;
if (isDefined(allAggregations[field.name])) {
Object.keys(allAggregations[field.name]).forEach((aggregation) => {
const typedAggregateOperation = aggregation as AGGREGATE_OPERATIONS;
if (
isFieldTypeValidForAggregateOperation(
field.type,
typedAggregateOperation,
)
) {
if (targetAggregateOperations.includes(typedAggregateOperation)) {
acc[typedAggregateOperation]?.push(field.id);
}
},
);
});
}
return acc;
}, aggregationMap);
};

View File

@ -1,13 +1,14 @@
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
export const initializeAvailableFieldsForAggregateOperationMap =
(): AvailableFieldsForAggregateOperation => {
return Object.keys(FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION).reduce(
(acc, operation) => ({
...acc,
[operation]: [],
}),
{},
);
};
export const initializeAvailableFieldsForAggregateOperationMap = (
aggregateOperations: AGGREGATE_OPERATIONS[],
): AvailableFieldsForAggregateOperation => {
return aggregateOperations.reduce(
(acc, operation) => ({
...acc,
[operation]: [],
}),
{},
);
};

View File

@ -1,4 +1,4 @@
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -6,7 +6,7 @@ export const isFieldTypeValidForAggregateOperation = (
fieldType: FieldMetadataType,
aggregateOperation: AggregateOperationsOmittingStandardOperations,
): boolean => {
return FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION[aggregateOperation].includes(
fieldType,
);
return FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION[
aggregateOperation
].includes(fieldType);
};