Display and update aggregate queries in kanban views (#8833)

Closes #8752, #8753, #8754

Implements usage of aggregate queries in kanban views.

https://github.com/user-attachments/assets/732590ca-2785-4c57-82d5-d999a2279e92

TO DO

1. write tests + storybook
2. Fix values displayed should have the same format as defined in number
fields + Fix display for amountMicros

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie
2024-12-03 22:46:57 +01:00
committed by GitHub
parent 5e891a135b
commit 2fc247cb21
67 changed files with 1670 additions and 104 deletions

View File

@ -0,0 +1,7 @@
export enum AGGREGATE_OPERATIONS {
min = 'MIN',
max = 'MAX',
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
}

View File

@ -45,6 +45,12 @@ export class GraphqlQuerySelectedFieldsParser {
return accumulator;
}
this.aggregateParser.parse(
graphqlSelectedFields,
fieldMetadataMapByName,
accumulator,
);
this.parseRecordField(
graphqlSelectedFields,
fieldMetadataMapByName,

View File

@ -1,5 +1,6 @@
import { SelectQueryBuilder } from 'typeorm';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import { formatColumnNameFromCompositeFieldAndSubfield } from 'src/engine/twenty-orm/utils/format-column-name-from-composite-field-and-subfield.util';
import { isDefined } from 'src/utils/is-defined';
@ -19,7 +20,7 @@ export class ProcessAggregateHelper {
)) {
if (
!isDefined(aggregatedField?.fromField) ||
!isDefined(aggregatedField?.aggregationOperation)
!isDefined(aggregatedField?.aggregateOperation)
) {
continue;
}
@ -28,10 +29,17 @@ export class ProcessAggregateHelper {
aggregatedField.fromField,
aggregatedField.fromSubField,
);
const operation = aggregatedField.aggregationOperation;
if (
!Object.values(AGGREGATE_OPERATIONS).includes(
aggregatedField.aggregateOperation,
)
) {
continue;
}
queryBuilder.addSelect(
`${operation}("${columnName}")`,
`${aggregatedField.aggregateOperation}("${columnName}")`,
`${aggregatedFieldName}`,
);
}

View File

@ -4,23 +4,16 @@ import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
enum AGGREGATION_OPERATIONS {
min = 'MIN',
max = 'MAX',
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
}
export type AggregationField = {
type: GraphQLScalarType;
description: string;
fromField: string;
fromSubField?: string;
aggregationOperation: AGGREGATION_OPERATIONS;
aggregateOperation: AGGREGATE_OPERATIONS;
};
export const getAvailableAggregationsFromObjectFields = (
@ -31,7 +24,7 @@ export const getAvailableAggregationsFromObjectFields = (
type: GraphQLInt,
description: `Total number of records in the connection`,
fromField: 'id',
aggregationOperation: AGGREGATION_OPERATIONS.count,
aggregateOperation: AGGREGATE_OPERATIONS.count,
};
if (field.type === FieldMetadataType.DATE_TIME) {
@ -39,14 +32,14 @@ export const getAvailableAggregationsFromObjectFields = (
type: GraphQLISODateTime,
description: `Oldest date contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.min,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Most recent date contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.max,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
}
@ -55,38 +48,62 @@ export const getAvailableAggregationsFromObjectFields = (
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.min,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Maximum amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.max,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
acc[`avg${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.avg,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
acc[`sum${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
aggregationOperation: AGGREGATION_OPERATIONS.sum,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
}
if (field.type === FieldMetadataType.CURRENCY) {
acc[`min${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Maximal amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
acc[`sum${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
acc[`avg${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregationOperation: AGGREGATION_OPERATIONS.avg,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
}

View File

@ -380,6 +380,7 @@ export const VIEW_FIELD_STANDARD_FIELD_IDS = {
size: '20202020-6fab-4bd0-ae72-20f3ee39d581',
position: '20202020-19e5-4e4c-8c15-3a96d1fd0650',
view: '20202020-e8da-4521-afab-d6d231f9fa18',
aggregateOperation: '20202020-2cd7-4f94-ae83-4a14f5731a04',
};
export const VIEW_GROUP_STANDARD_FIELD_IDS = {
@ -420,6 +421,9 @@ export const VIEW_STANDARD_FIELD_IDS = {
key: '20202020-298e-49fa-9f4a-7b416b110443',
icon: '20202020-1f08-4fd9-929b-cbc07f317166',
kanbanFieldMetadataId: '20202020-d09b-4f65-ac42-06a2f20ba0e8',
kanbanAggregateOperation: '20202020-8da2-45de-a731-61bed84b17a8',
kanbanAggregateOperationFieldMetadataId:
'20202020-b1b3-4bf3-85e4-dc7d58aa9b02',
position: '20202020-e9db-4303-b271-e8250c450172',
isCompact: '20202020-674e-4314-994d-05754ea7b22b',
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',