[DASHBOARDS] Add split multi-value fields setting for charts (#17907)
closes https://github.com/twentyhq/twenty/issues/17890 https://github.com/user-attachments/assets/dbf2ebe6-c2da-44d0-846e-d7fa9fac5f43
This commit is contained in:
+1
@@ -6,6 +6,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
|
||||
export type GroupByRegularField = {
|
||||
fieldMetadata: FlatFieldMetadata;
|
||||
subFieldName?: string;
|
||||
shouldUnnest?: boolean;
|
||||
};
|
||||
|
||||
export type GroupByDateField = {
|
||||
|
||||
+46
-1
@@ -3,7 +3,10 @@ import {
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type GroupByDateField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByRegularField,
|
||||
} from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { getGroupByExpression } from 'src/engine/api/common/common-query-runners/utils/get-group-by-expression.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
@@ -23,6 +26,16 @@ const buildGroupByDateField = (
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const buildGroupByRegularField = (
|
||||
overrides: Partial<GroupByRegularField> = {},
|
||||
): GroupByRegularField => ({
|
||||
fieldMetadata: {
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
name: 'tags',
|
||||
} as FlatFieldMetadata,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getGroupByExpression', () => {
|
||||
const columnNameWithQuotes = '"company"."createdAt"';
|
||||
|
||||
@@ -126,4 +139,36 @@ describe('getGroupByExpression', () => {
|
||||
expect(result).not.toContain('AT TIME ZONE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array unnest handling', () => {
|
||||
const arrayColumnNameWithQuotes = '"company"."tags"';
|
||||
|
||||
it('should unnest array fields when split mode is enabled', () => {
|
||||
const groupByField = buildGroupByRegularField({
|
||||
shouldUnnest: true,
|
||||
});
|
||||
|
||||
const result = getGroupByExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes: arrayColumnNameWithQuotes,
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`UNNEST(CASE WHEN CARDINALITY(${arrayColumnNameWithQuotes}) > 0 THEN ${arrayColumnNameWithQuotes} ELSE ARRAY[${arrayColumnNameWithQuotes}[1]] END)`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep plain column expression when split mode is disabled', () => {
|
||||
const groupByField = buildGroupByRegularField({
|
||||
shouldUnnest: false,
|
||||
});
|
||||
|
||||
const result = getGroupByExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes: arrayColumnNameWithQuotes,
|
||||
});
|
||||
|
||||
expect(result).toBe(arrayColumnNameWithQuotes);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
@@ -30,6 +30,10 @@ export const getGroupByExpression = ({
|
||||
if (
|
||||
!(isGroupByDateField(groupByField) || isGroupByRelationField(groupByField))
|
||||
) {
|
||||
if ('shouldUnnest' in groupByField && groupByField.shouldUnnest) {
|
||||
return `UNNEST(CASE WHEN CARDINALITY(${columnNameWithQuotes}) > 0 THEN ${columnNameWithQuotes} ELSE ARRAY[${columnNameWithQuotes}[1]] END)`;
|
||||
}
|
||||
|
||||
return columnNameWithQuotes;
|
||||
}
|
||||
|
||||
|
||||
+15
-8
@@ -12,16 +12,16 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order';
|
||||
import { getOptionalOrderByCasting } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/get-optional-order-by-casting.util';
|
||||
import { parseCompositeFieldForOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util';
|
||||
import { prepareForOrderByRelationFieldParsing } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util';
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByField,
|
||||
type GroupByRegularField,
|
||||
} from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { getGroupByOrderExpression } from 'src/engine/api/common/common-query-runners/utils/get-group-by-order-expression.util';
|
||||
import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order';
|
||||
import { getOptionalOrderByCasting } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/get-optional-order-by-casting.util';
|
||||
import { parseCompositeFieldForOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util';
|
||||
import { prepareForOrderByRelationFieldParsing } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import {
|
||||
type AggregationField,
|
||||
@@ -31,12 +31,12 @@ import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-er
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
import { type OrderByClause } from './types/order-by-condition.type';
|
||||
|
||||
@@ -387,11 +387,11 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
fieldMetadata: FlatFieldMetadata;
|
||||
}): Record<string, OrderByClause> | null => {
|
||||
const fieldIsInGroupBy = groupByFields.some(
|
||||
const groupByField = groupByFields.find(
|
||||
(groupByField) => groupByField.fieldMetadata.id === fieldMetadata.id,
|
||||
);
|
||||
|
||||
if (!fieldIsInGroupBy) {
|
||||
if (!isDefined(groupByField)) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a field that is not an aggregate nor in groupBy criteria: ${fieldMetadata.name}.`,
|
||||
);
|
||||
@@ -404,8 +404,15 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnNameWithQuotes = `"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"`;
|
||||
|
||||
const expression = getGroupByOrderExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes,
|
||||
});
|
||||
|
||||
return {
|
||||
[`"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]:
|
||||
[`${expression}${orderByCasting}`]:
|
||||
convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
};
|
||||
|
||||
+14
@@ -93,6 +93,20 @@ export const parseGroupByArgs = (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array unnest fields
|
||||
if (
|
||||
typeof fieldNames[fieldName] === 'object' &&
|
||||
fieldNames[fieldName] !== null &&
|
||||
'unnest' in fieldNames[fieldName]
|
||||
) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
subFieldName: undefined,
|
||||
shouldUnnest: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular fields and composite fields
|
||||
if (fieldNames[fieldName] === true) {
|
||||
groupByFields.push({
|
||||
|
||||
Reference in New Issue
Block a user