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:
@ -0,0 +1,7 @@
|
||||
export enum AGGREGATE_OPERATIONS {
|
||||
min = 'MIN',
|
||||
max = 'MAX',
|
||||
avg = 'AVG',
|
||||
sum = 'SUM',
|
||||
count = 'COUNT',
|
||||
}
|
||||
@ -45,6 +45,12 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
this.aggregateParser.parse(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
this.parseRecordField(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
|
||||
@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { Relation } from 'typeorm';
|
||||
|
||||
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
@ -15,6 +21,10 @@ import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sy
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
registerEnumType(AGGREGATE_OPERATIONS, {
|
||||
name: 'AggregateOperations',
|
||||
});
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewField,
|
||||
namePlural: 'viewFields',
|
||||
@ -80,6 +90,52 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
view: Relation<ViewWorkspaceEntity>;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.aggregateOperation,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Aggregate operation',
|
||||
description: 'Optional aggregate operation',
|
||||
icon: 'IconCalculator',
|
||||
options: [
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.avg,
|
||||
label: 'Average',
|
||||
position: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.count,
|
||||
label: 'Count',
|
||||
position: 1,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.max,
|
||||
label: 'Maximum',
|
||||
position: 2,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.min,
|
||||
label: 'Minimum',
|
||||
position: 3,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.sum,
|
||||
label: 'Sum',
|
||||
position: 4,
|
||||
color: 'yellow',
|
||||
},
|
||||
],
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceGate({
|
||||
featureFlag: FeatureFlagKey.IsAggregateQueryEnabled,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
aggregateOperation?: AGGREGATE_OPERATIONS | null;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string;
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
@ -8,6 +10,7 @@ import {
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
@ -175,4 +178,63 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
favorites: Relation<FavoriteWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Aggregate operation',
|
||||
description: 'Optional aggregate operation',
|
||||
icon: 'IconCalculator',
|
||||
options: [
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.avg,
|
||||
label: 'Average',
|
||||
position: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.count,
|
||||
label: 'Count',
|
||||
position: 1,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.max,
|
||||
label: 'Maximum',
|
||||
position: 2,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.min,
|
||||
label: 'Minimum',
|
||||
position: 3,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.sum,
|
||||
label: 'Sum',
|
||||
position: 4,
|
||||
color: 'yellow',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${AGGREGATE_OPERATIONS.count}'`,
|
||||
})
|
||||
@WorkspaceGate({
|
||||
featureFlag: FeatureFlagKey.IsAggregateQueryEnabled,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
kanbanAggregateOperation?: AGGREGATE_OPERATIONS | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperationFieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Field metadata used for aggregate operation',
|
||||
description: 'Field metadata used for aggregate operation',
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceGate({
|
||||
featureFlag: FeatureFlagKey.IsAggregateQueryEnabled,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user