[Dashboards] Relation fields groupby (#16093)

This commit is contained in:
nitin
2025-11-28 00:58:53 +05:30
committed by GitHub
parent f23aa632a7
commit fa87603fd8
22 changed files with 597 additions and 136 deletions
@@ -4,7 +4,7 @@ import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('buildGroupByFieldObject', () => {
it('should return field with Id suffix for relation fields', () => {
it('should return field with Id suffix for relation fields without subFieldName', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
@@ -15,6 +15,33 @@ describe('buildGroupByFieldObject', () => {
expect(result).toEqual({ companyId: true });
});
it('should return nested object for relation field with subFieldName', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
} as any;
const result = buildGroupByFieldObject({ field, subFieldName: 'name' });
expect(result).toEqual({ company: { name: true } });
});
it('should return deeply nested object for relation with composite subfield', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
} as any;
const result = buildGroupByFieldObject({
field,
subFieldName: 'address.addressCity',
});
expect(result).toEqual({
company: { address: { addressCity: true } },
});
});
it('should return nested object for composite fields with subfield', () => {
const field = {
name: 'name',
@@ -1,4 +1,6 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { buildDateFilterForDayGranularity } from '@/page-layout/widgets/graph/utils/buildDateFilterForDayGranularity';
import { buildDateRangeFiltersForGranularity } from '@/page-layout/widgets/graph/utils/buildDateRangeFiltersForGranularity';
@@ -69,6 +71,13 @@ export const buildFilterFromChartBucket = ({
];
}
if (
isFieldRelation(fieldMetadataItem) ||
isFieldMorphRelation(fieldMetadataItem)
) {
return [];
}
if (isFieldMetadataDateKind(fieldMetadataItem.type)) {
const parsedBucketDate = new Date(String(bucketRawValue));
@@ -8,9 +8,12 @@ import {
type FirstDayOfTheWeek,
ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type GroupByFieldObject = Record<
string,
boolean | Record<string, boolean | string | Record<string, boolean | string>>
>;
export const buildGroupByFieldObject = ({
field,
@@ -22,15 +25,35 @@ export const buildGroupByFieldObject = ({
subFieldName?: string | null;
dateGranularity?: ObjectRecordGroupByDateGranularity;
firstDayOfTheWeek?: number | null;
}): Record<string, boolean | Record<string, boolean | string>> => {
}): GroupByFieldObject => {
const isRelation = isFieldRelation(field) || isFieldMorphRelation(field);
const isComposite = isCompositeFieldType(field.type);
const isDateField =
field.type === FieldMetadataType.DATE ||
field.type === FieldMetadataType.DATE_TIME;
const isDateField = isFieldMetadataDateKind(field.type);
if (isRelation) {
return { [`${field.name}Id`]: true };
if (!isDefined(subFieldName)) {
return { [`${field.name}Id`]: true };
}
const parts = subFieldName.split('.');
const nestedFieldName = parts[0];
const nestedSubFieldName = parts[1];
if (isDefined(nestedSubFieldName)) {
return {
[field.name]: {
[nestedFieldName]: {
[nestedSubFieldName]: true,
},
},
};
}
return {
[field.name]: {
[nestedFieldName]: true,
},
};
}
if (isComposite) {
@@ -1,17 +1,22 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
import {
type AggregateOrderByWithGroupByField,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import {
type BarChartConfiguration,
type LineChartConfiguration,
} from '~/generated/graphql';
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
import {
buildGroupByFieldObject,
type GroupByFieldObject,
} from './buildGroupByFieldObject';
export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
objectMetadataItem,
@@ -51,27 +56,34 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
);
}
const groupBy: Array<
Record<string, boolean | Record<string, boolean | string>>
> = [];
const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type);
const groupBy: Array<GroupByFieldObject> = [];
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldX,
subFieldName: groupBySubFieldNameX,
dateGranularity:
chartConfiguration.primaryAxisDateGranularity ?? undefined,
dateGranularity: isFieldXDate
? (chartConfiguration.primaryAxisDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
);
if (isDefined(groupByFieldY)) {
const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type);
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldY,
subFieldName: groupBySubFieldNameY,
dateGranularity:
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
dateGranularity: isFieldYDate
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
);
@@ -82,6 +94,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField
> = [];
if (isDefined(chartConfiguration.primaryAxisOrderBy)) {
@@ -91,8 +104,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
groupByField: groupByFieldX,
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
aggregateOperation,
dateGranularity:
chartConfiguration.primaryAxisDateGranularity ?? undefined,
dateGranularity: isFieldXDate
? (chartConfiguration.primaryAxisDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -100,6 +115,8 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
isDefined(groupByFieldY) &&
isDefined(chartConfiguration.secondaryAxisOrderBy)
) {
const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type);
orderBy.push(
getGroupByOrderBy({
graphOrderBy: chartConfiguration.secondaryAxisOrderBy,
@@ -107,8 +124,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
groupBySubFieldName:
chartConfiguration.secondaryAxisGroupBySubFieldName,
aggregateOperation,
dateGranularity:
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
dateGranularity: isFieldYDateForOrderBy
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -1,14 +1,19 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
import {
type AggregateOrderByWithGroupByField,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { type PieChartConfiguration } from '~/generated/graphql';
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
import {
buildGroupByFieldObject,
type GroupByFieldObject,
} from './buildGroupByFieldObject';
export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
objectMetadataItem,
@@ -38,13 +43,16 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
);
}
const groupBy: Array<
Record<string, boolean | Record<string, boolean | string>>
> = [
const isFieldDate = isFieldMetadataDateKind(groupByField.type);
const groupBy: Array<GroupByFieldObject> = [
buildGroupByFieldObject({
field: groupByField,
subFieldName: groupBySubFieldName,
dateGranularity,
dateGranularity: isFieldDate
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
];
@@ -54,6 +62,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField
> = [];
if (isDefined(chartConfiguration.orderBy)) {
@@ -63,7 +72,9 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
groupByField,
groupBySubFieldName,
aggregateOperation,
dateGranularity,
dateGranularity: isFieldDate
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -0,0 +1,60 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy';
import {
type ObjectRecordGroupByDateGranularity,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
type OrderByDirection,
} from 'twenty-shared/types';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
export const getFieldOrderBy = (
groupByField: FieldMetadataItem,
groupBySubFieldName: string | null | undefined,
dateGranularity: ObjectRecordGroupByDateGranularity | undefined,
direction: OrderByDirection,
):
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField => {
if (isCompositeFieldType(groupByField.type)) {
if (!isDefined(groupBySubFieldName)) {
throw new Error(
`Group by subFieldName is required for composite fields (field: ${groupByField.name})`,
);
}
return {
[groupByField.name]: {
[groupBySubFieldName]: direction,
},
};
}
if (isFieldMetadataDateKind(groupByField.type)) {
return {
[groupByField.name]: {
orderBy: direction,
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
},
};
}
if (isFieldRelation(groupByField) || isFieldMorphRelation(groupByField)) {
return getRelationFieldOrderBy(
groupByField,
groupBySubFieldName,
direction,
);
}
return {
[groupByField.name]: direction,
};
};
@@ -1,36 +1,17 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getFieldOrderBy } from '@/page-layout/widgets/graph/utils/getFieldOrderBy';
import { mapOrderByToDirection } from '@/page-layout/widgets/graph/utils/mapOrderByToDirection';
import {
OrderByDirection,
type AggregateOrderByWithGroupByField,
type ObjectRecordGroupByDateGranularity,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import {
assertUnreachable,
isDefined,
isFieldMetadataDateKind,
} from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
const mapOrderByToDirection = (orderByEnum: GraphOrderBy): OrderByDirection => {
switch (orderByEnum) {
case GraphOrderBy.FIELD_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.FIELD_DESC:
return OrderByDirection.DescNullsLast;
case GraphOrderBy.VALUE_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.VALUE_DESC:
return OrderByDirection.DescNullsLast;
default:
assertUnreachable(orderByEnum);
}
};
export const getGroupByOrderBy = ({
graphOrderBy,
groupByField,
@@ -47,34 +28,17 @@ export const getGroupByOrderBy = ({
| AggregateOrderByWithGroupByField
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField => {
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField => {
switch (graphOrderBy) {
case GraphOrderBy.FIELD_ASC:
case GraphOrderBy.FIELD_DESC: {
if (isCompositeFieldType(groupByField.type)) {
if (!isDefined(groupBySubFieldName)) {
throw new Error(
`Group by subFieldName is required for composite fields (field: ${groupByField.name})`,
);
}
return {
[groupByField.name]: {
[groupBySubFieldName]: mapOrderByToDirection(graphOrderBy),
},
};
} else if (isFieldMetadataDateKind(groupByField.type)) {
return {
[groupByField.name]: {
orderBy: mapOrderByToDirection(graphOrderBy),
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
},
};
} else {
return {
[groupByField.name]: mapOrderByToDirection(graphOrderBy),
};
}
}
case GraphOrderBy.FIELD_DESC:
return getFieldOrderBy(
groupByField,
groupBySubFieldName,
dateGranularity,
mapOrderByToDirection(graphOrderBy),
);
case GraphOrderBy.VALUE_ASC:
case GraphOrderBy.VALUE_DESC: {
if (!isDefined(aggregateOperation)) {
@@ -0,0 +1,37 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import {
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type OrderByDirection,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const getRelationFieldOrderBy = (
groupByField: FieldMetadataItem,
groupBySubFieldName: string | null | undefined,
direction: OrderByDirection,
): ObjectRecordOrderByForScalarField | ObjectRecordOrderByForRelationField => {
if (!isDefined(groupBySubFieldName)) {
return {
[`${groupByField.name}Id`]: direction,
};
}
const [nestedFieldName, nestedSubFieldName] = groupBySubFieldName.split('.');
if (!isDefined(nestedSubFieldName)) {
return {
[groupByField.name]: {
[nestedFieldName]: direction,
},
};
}
return {
[groupByField.name]: {
[nestedFieldName]: {
[nestedSubFieldName]: direction,
},
},
};
};
@@ -0,0 +1,20 @@
import { OrderByDirection } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
export const mapOrderByToDirection = (
orderByEnum: GraphOrderBy,
): OrderByDirection => {
switch (orderByEnum) {
case GraphOrderBy.FIELD_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.FIELD_DESC:
return OrderByDirection.DescNullsLast;
case GraphOrderBy.VALUE_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.VALUE_DESC:
return OrderByDirection.DescNullsLast;
default:
assertUnreachable(orderByEnum);
}
};