[Dashboards] Relation fields groupby (#16093)
This commit is contained in:
+28
-1
@@ -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',
|
||||
|
||||
+9
@@ -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));
|
||||
|
||||
|
||||
+30
-7
@@ -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) {
|
||||
|
||||
+32
-13
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
+18
-7
@@ -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,
|
||||
};
|
||||
};
|
||||
+13
-49
@@ -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)) {
|
||||
|
||||
+37
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
+20
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user