part 3 of on click bar to filters: add sort plus some normalizations (#16075)

This commit is contained in:
nitin
2025-11-26 19:48:48 +05:30
committed by GitHub
parent dc2c2c413c
commit 6fcb05d9b3
25 changed files with 942 additions and 80 deletions
@@ -2,12 +2,13 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/Char
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { lazy, Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { getAppPath } from 'twenty-shared/utils';
import {
type PageLayoutWidget,
type PieChartConfiguration,
@@ -31,6 +32,7 @@ export const GraphWidgetPieChartRenderer = ({
loading,
hasTooManyGroups,
objectMetadataItem,
formattedToRawLookup,
showDataLabels,
} = useGraphPieChartWidgetData({
objectMetadataItemId: widget.objectMetadataId,
@@ -38,6 +40,7 @@ export const GraphWidgetPieChartRenderer = ({
});
const navigate = useNavigate();
const configuration = widget.configuration as PieChartConfiguration;
const indexViewId = useRecoilValue(
coreIndexViewIdFromObjectMetadataItemFamilySelector({
@@ -45,16 +48,28 @@ export const GraphWidgetPieChartRenderer = ({
}),
);
const handleSliceClick = (_datum: PieChartDataItem) => {
return navigate(
getAppPath(
AppPath.RecordIndexPage,
{
objectNamePlural: objectMetadataItem.namePlural,
},
isDefined(indexViewId) ? { viewId: indexViewId } : undefined,
),
const handleSliceClick = (datum: PieChartDataItem) => {
const rawValue = formattedToRawLookup.get(datum.id) ?? null;
const drilldownQueryParams = buildChartDrilldownQueryParams({
objectMetadataItem,
configuration,
clickedData: {
primaryBucketRawValue: rawValue,
},
viewId: indexViewId,
timezone: configuration.timezone ?? undefined,
});
const url = getAppPath(
AppPath.RecordIndexPage,
{
objectNamePlural: objectMetadataItem.namePlural,
},
Object.fromEntries(drilldownQueryParams),
);
return navigate(url);
};
if (loading) {
@@ -4,6 +4,7 @@ import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData';
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { useMemo } from 'react';
import { type PieChartConfiguration } from '~/generated/graphql';
@@ -18,6 +19,7 @@ type UseGraphPieChartWidgetDataResult = {
error?: Error;
hasTooManyGroups: boolean;
objectMetadataItem: ObjectMetadataItem;
formattedToRawLookup: Map<string, RawDimensionValue>;
showDataLabels: boolean;
};
@@ -6,8 +6,10 @@ import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
import { isDefined } from 'twenty-shared/utils';
import { type PieChartConfiguration } from '~/generated/graphql';
@@ -21,11 +23,13 @@ type TransformGroupByDataToPieChartDataParams = {
type TransformGroupByDataToPieChartDataResult = {
data: PieChartDataItem[];
hasTooManyGroups: boolean;
formattedToRawLookup: Map<string, RawDimensionValue>;
};
const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = {
data: [],
hasTooManyGroups: false,
formattedToRawLookup: new Map(),
};
export const transformGroupByDataToPieChartData = ({
@@ -65,17 +69,18 @@ export const transformGroupByDataToPieChartData = ({
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
);
const data: PieChartDataItem[] = limitedResults.map((result) => {
const dimensionValues = result.groupByDimensionValues;
const formattedValues = formatPrimaryDimensionValues({
groupByRawResults: limitedResults,
primaryAxisGroupByField: groupByField,
primaryAxisDateGranularity: configuration.dateGranularity ?? undefined,
primaryAxisGroupBySubFieldName:
configuration.groupBySubFieldName ?? undefined,
});
const label = isDefined(dimensionValues?.[0])
? formatDimensionValue({
value: dimensionValues[0],
fieldMetadata: groupByField,
dateGranularity: configuration.dateGranularity ?? undefined,
subFieldName: configuration.groupBySubFieldName ?? undefined,
})
: '';
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
const data: PieChartDataItem[] = limitedResults.map((result, index) => {
const label = formattedValues[index]?.formattedPrimaryDimensionValue ?? '';
const value = computeAggregateValueFromGroupByResult({
rawResult: result,
@@ -97,5 +102,6 @@ export const transformGroupByDataToPieChartData = ({
return {
data,
hasTooManyGroups: rawResults.length > PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
formattedToRawLookup,
};
};
@@ -3,11 +3,15 @@ import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDim
import {
type BarChartConfiguration,
type LineChartConfiguration,
type PieChartConfiguration,
} from '~/generated/graphql';
export type BuildChartDrilldownQueryParamsInput = {
objectMetadataItem: ObjectMetadataItem;
configuration: BarChartConfiguration | LineChartConfiguration;
configuration:
| BarChartConfiguration
| LineChartConfiguration
| PieChartConfiguration;
clickedData: {
primaryBucketRawValue: RawDimensionValue;
};
@@ -0,0 +1,111 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildSortsForChartFieldOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartFieldOrderBy';
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { FieldMetadataType } from 'twenty-shared/types';
import { GraphOrderBy } from '~/generated/graphql';
describe('buildSortsForChartFieldOrderBy', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'obj-1',
nameSingular: 'opportunity',
namePlural: 'opportunities',
fields: [
{
id: 'field-status',
name: 'status',
type: FieldMetadataType.SELECT,
label: 'Status',
},
{
id: 'field-createdAt',
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
label: 'Created At',
},
{
id: 'field-address',
name: 'address',
type: FieldMetadataType.ADDRESS,
label: 'Address',
},
],
} as ObjectMetadataItem;
it('should return ASC sort for FIELD_ASC orderBy', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'field-createdAt',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.FIELD_ASC,
};
const result = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({ fieldName: 'createdAt', direction: 'ASC' });
});
it('should return DESC sort for FIELD_DESC orderBy', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'field-status',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.FIELD_DESC,
};
const result = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({ fieldName: 'status', direction: 'DESC' });
});
it('should include subfield for composite fields', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'field-address',
groupBySubFieldName: 'addressCity',
orderBy: GraphOrderBy.FIELD_ASC,
};
const result = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({
fieldName: 'address.addressCity',
direction: 'ASC',
});
});
it('should handle composite field without subfield', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'field-address',
groupBySubFieldName: null,
orderBy: GraphOrderBy.FIELD_ASC,
};
const result = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({ fieldName: 'address', direction: 'ASC' });
});
it('should return null when groupBy field not found', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'non-existent',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.FIELD_ASC,
};
const result = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toBeNull();
});
});
@@ -0,0 +1,69 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildSortsForChartValueOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartValueOrderBy';
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { FieldMetadataType } from 'twenty-shared/types';
import { GraphOrderBy } from '~/generated/graphql';
describe('buildSortsForChartValueOrderBy', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'obj-1',
nameSingular: 'opportunity',
namePlural: 'opportunities',
fields: [
{
id: 'field-amount',
name: 'amount',
type: FieldMetadataType.NUMBER,
label: 'Amount',
},
],
} as ObjectMetadataItem;
it('should return ASC sort for VALUE_ASC orderBy', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'some-field',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.VALUE_ASC,
};
const result = buildSortsForChartValueOrderBy({
normalizedFields,
aggregateFieldMetadataId: 'field-amount',
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({ fieldName: 'amount', direction: 'ASC' });
});
it('should return DESC sort for VALUE_DESC orderBy', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'some-field',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.VALUE_DESC,
};
const result = buildSortsForChartValueOrderBy({
normalizedFields,
aggregateFieldMetadataId: 'field-amount',
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual({ fieldName: 'amount', direction: 'DESC' });
});
it('should return null when aggregate field not found', () => {
const normalizedFields: NormalizedChartConfigurationFields = {
groupByFieldMetadataId: 'some-field',
groupBySubFieldName: undefined,
orderBy: GraphOrderBy.VALUE_ASC,
};
const result = buildSortsForChartValueOrderBy({
normalizedFields,
aggregateFieldMetadataId: 'non-existent',
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toBeNull();
});
});
@@ -0,0 +1,120 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildSortsFromChartConfig } from '@/page-layout/widgets/graph/utils/buildSortsFromChartConfig';
import { FieldMetadataType } from 'twenty-shared/types';
import {
type BarChartConfiguration,
type PieChartConfiguration,
GraphOrderBy,
} from '~/generated/graphql';
describe('buildSortsFromChartConfig', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'obj-1',
nameSingular: 'opportunity',
namePlural: 'opportunities',
fields: [
{
id: 'field-status',
name: 'status',
type: FieldMetadataType.SELECT,
label: 'Status',
},
{
id: 'field-amount',
name: 'amount',
type: FieldMetadataType.NUMBER,
label: 'Amount',
},
{
id: 'field-createdAt',
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
label: 'Created At',
},
],
} as ObjectMetadataItem;
describe('integration', () => {
it('should handle field-based sorting end-to-end for Bar charts', () => {
const config = {
__typename: 'BarChartConfiguration',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'field-createdAt',
aggregateFieldMetadataId: 'field-amount',
} as BarChartConfiguration;
const result = buildSortsFromChartConfig({
configuration: config,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual([{ fieldName: 'createdAt', direction: 'ASC' }]);
});
it('should handle value-based sorting end-to-end for Pie charts', () => {
const config = {
__typename: 'PieChartConfiguration',
orderBy: GraphOrderBy.VALUE_DESC,
groupByFieldMetadataId: 'field-status',
aggregateFieldMetadataId: 'field-amount',
} as PieChartConfiguration;
const result = buildSortsFromChartConfig({
configuration: config,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual([{ fieldName: 'amount', direction: 'DESC' }]);
});
});
describe('edge cases', () => {
it('should return empty array when orderBy is undefined', () => {
const config = {
__typename: 'BarChartConfiguration',
primaryAxisOrderBy: undefined,
primaryAxisGroupByFieldMetadataId: 'field-createdAt',
aggregateFieldMetadataId: 'field-amount',
} as BarChartConfiguration;
const result = buildSortsFromChartConfig({
configuration: config,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual([]);
});
it('should return empty array when orderBy is null', () => {
const config = {
__typename: 'PieChartConfiguration',
orderBy: null,
groupByFieldMetadataId: 'field-status',
aggregateFieldMetadataId: 'field-amount',
} as PieChartConfiguration;
const result = buildSortsFromChartConfig({
configuration: config,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual([]);
});
it('should return empty array when field not found', () => {
const config = {
__typename: 'BarChartConfiguration',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'non-existent',
aggregateFieldMetadataId: 'field-amount',
} as BarChartConfiguration;
const result = buildSortsFromChartConfig({
configuration: config,
objectMetadataItem: mockObjectMetadataItem,
});
expect(result).toEqual([]);
});
});
});
@@ -0,0 +1,135 @@
import {
AggregateOperations,
GraphOrderBy,
GraphType,
ObjectRecordGroupByDateGranularity,
} from '~/generated-metadata/graphql';
import { normalizeChartConfigurationFields } from '../normalizeChartConfigurationFields';
describe('normalizeChartConfigurationFields', () => {
describe('Bar and Line charts (with primaryAxis prefix)', () => {
it('should extract fields from Bar chart configuration', () => {
const barConfig = {
__typename: 'BarChartConfiguration',
primaryAxisGroupByFieldMetadataId: 'field-123',
primaryAxisGroupBySubFieldName: 'subField',
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
aggregateFieldMetadataId: 'aggregate-456',
aggregateOperation: AggregateOperations.SUM,
graphType: GraphType.VERTICAL_BAR,
} as any;
const result = normalizeChartConfigurationFields(barConfig);
expect(result.groupByFieldMetadataId).toBe('field-123');
expect(result.groupBySubFieldName).toBe('subField');
expect(result.dateGranularity).toBe(
ObjectRecordGroupByDateGranularity.MONTH,
);
expect(result.orderBy).toBe(GraphOrderBy.FIELD_ASC);
});
it('should extract fields from Line chart configuration', () => {
const lineConfig = {
__typename: 'LineChartConfiguration',
primaryAxisGroupByFieldMetadataId: 'field-789',
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
aggregateFieldMetadataId: 'aggregate-012',
aggregateOperation: AggregateOperations.COUNT,
graphType: GraphType.LINE,
} as any;
const result = normalizeChartConfigurationFields(lineConfig);
expect(result.groupByFieldMetadataId).toBe('field-789');
expect(result.orderBy).toBe(GraphOrderBy.VALUE_DESC);
expect(result.groupBySubFieldName).toBeUndefined();
expect(result.dateGranularity).toBeUndefined();
});
});
describe('Pie charts (without prefix)', () => {
it('should extract fields from Pie chart configuration', () => {
const pieConfig = {
__typename: 'PieChartConfiguration',
groupByFieldMetadataId: 'pie-field',
groupBySubFieldName: 'pieSubField',
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
orderBy: GraphOrderBy.FIELD_DESC,
aggregateFieldMetadataId: 'pie-aggregate',
aggregateOperation: AggregateOperations.AVG,
graphType: GraphType.PIE,
} as any;
const result = normalizeChartConfigurationFields(pieConfig);
expect(result.groupByFieldMetadataId).toBe('pie-field');
expect(result.groupBySubFieldName).toBe('pieSubField');
expect(result.dateGranularity).toBe(
ObjectRecordGroupByDateGranularity.DAY,
);
expect(result.orderBy).toBe(GraphOrderBy.FIELD_DESC);
});
it('should handle minimal Pie chart configuration', () => {
const pieConfig = {
__typename: 'PieChartConfiguration',
groupByFieldMetadataId: 'minimal-field',
aggregateFieldMetadataId: 'minimal-aggregate',
aggregateOperation: AggregateOperations.COUNT,
graphType: GraphType.PIE,
} as any;
const result = normalizeChartConfigurationFields(pieConfig);
expect(result.groupByFieldMetadataId).toBe('minimal-field');
expect(result.groupBySubFieldName).toBeUndefined();
expect(result.dateGranularity).toBeUndefined();
expect(result.orderBy).toBeUndefined();
});
});
describe('Edge cases', () => {
it('should return empty object for configuration without recognized fields', () => {
const unknownConfig = {
someOtherField: 'value',
aggregateFieldMetadataId: 'ignored',
} as any;
const result = normalizeChartConfigurationFields(unknownConfig);
expect(result).toEqual({});
});
it('should use __typename to determine fields even when both patterns exist', () => {
const mixedConfig = {
__typename: 'BarChartConfiguration',
primaryAxisGroupByFieldMetadataId: 'primary-field',
groupByFieldMetadataId: 'fallback-field',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
orderBy: GraphOrderBy.FIELD_DESC,
} as any;
const result = normalizeChartConfigurationFields(mixedConfig);
expect(result.groupByFieldMetadataId).toBe('primary-field');
expect(result.orderBy).toBe(GraphOrderBy.FIELD_ASC);
});
it('should extract Pie fields when __typename is PieChartConfiguration even if primaryAxis fields exist', () => {
const mixedConfig = {
__typename: 'PieChartConfiguration',
primaryAxisGroupByFieldMetadataId: 'primary-field',
groupByFieldMetadataId: 'pie-field',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
orderBy: GraphOrderBy.FIELD_DESC,
} as any;
const result = normalizeChartConfigurationFields(mixedConfig);
expect(result.groupByFieldMetadataId).toBe('pie-field');
expect(result.orderBy).toBe(GraphOrderBy.FIELD_DESC);
});
});
});
@@ -1,6 +1,8 @@
import { type BuildChartDrilldownQueryParamsInput } from '@/page-layout/widgets/graph/types/BuildChartDrilldownQueryParamsInput';
import { buildFilterFromChartBucket } from '@/page-layout/widgets/graph/utils/buildFilterFromChartBucket';
import { buildFilterQueryParams } from '@/page-layout/widgets/graph/utils/buildFilterQueryParams';
import { buildSortsFromChartConfig } from '@/page-layout/widgets/graph/utils/buildSortsFromChartConfig';
import { normalizeChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { isDefined } from 'twenty-shared/utils';
export const buildChartDrilldownQueryParams = ({
@@ -24,16 +26,21 @@ export const buildChartDrilldownQueryParams = ({
});
}
const primaryField = objectMetadataItem.fields.find(
(field) => field.id === configuration.primaryAxisGroupByFieldMetadataId,
);
const { groupByFieldMetadataId, dateGranularity, groupBySubFieldName } =
normalizeChartConfigurationFields(configuration);
const primaryField = isDefined(groupByFieldMetadataId)
? objectMetadataItem.fields.find(
(field) => field.id === groupByFieldMetadataId,
)
: undefined;
if (isDefined(primaryField)) {
const primaryFilters = buildFilterFromChartBucket({
fieldMetadataItem: primaryField,
bucketRawValue: clickedData.primaryBucketRawValue,
dateGranularity: configuration.primaryAxisDateGranularity,
subFieldName: configuration.primaryAxisGroupBySubFieldName,
dateGranularity,
subFieldName: groupBySubFieldName,
timezone,
});
@@ -45,6 +52,15 @@ export const buildChartDrilldownQueryParams = ({
});
}
const sorts = buildSortsFromChartConfig({
configuration,
objectMetadataItem,
});
sorts.forEach((sort) => {
drilldownQueryParams.append(`sort[${sort.fieldName}]`, sort.direction);
});
if (isDefined(viewId)) {
drilldownQueryParams.set('viewId', viewId);
}
@@ -0,0 +1,49 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
type ChartSort = {
fieldName: string;
direction: 'ASC' | 'DESC';
};
type BuildSortsForChartFieldOrderByParams = {
normalizedFields: NormalizedChartConfigurationFields;
objectMetadataItem: ObjectMetadataItem;
};
export const buildSortsForChartFieldOrderBy = ({
normalizedFields,
objectMetadataItem,
}: BuildSortsForChartFieldOrderByParams): ChartSort | null => {
const { groupByFieldMetadataId, groupBySubFieldName, orderBy } =
normalizedFields;
if (
orderBy !== GraphOrderBy.FIELD_ASC &&
orderBy !== GraphOrderBy.FIELD_DESC
) {
return null;
}
const primaryField = groupByFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === groupByFieldMetadataId,
)
: undefined;
if (!isDefined(primaryField)) {
return null;
}
const fieldName = isNonEmptyString(groupBySubFieldName)
? `${primaryField.name}.${groupBySubFieldName}`
: primaryField.name;
return {
fieldName,
direction: orderBy === GraphOrderBy.FIELD_ASC ? 'ASC' : 'DESC',
};
};
@@ -0,0 +1,43 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { isDefined } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
type ChartSort = {
fieldName: string;
direction: 'ASC' | 'DESC';
};
type BuildSortsForChartValueOrderByParams = {
normalizedFields: NormalizedChartConfigurationFields;
aggregateFieldMetadataId: string;
objectMetadataItem: ObjectMetadataItem;
};
export const buildSortsForChartValueOrderBy = ({
normalizedFields,
aggregateFieldMetadataId,
objectMetadataItem,
}: BuildSortsForChartValueOrderByParams): ChartSort | null => {
const { orderBy } = normalizedFields;
if (
orderBy !== GraphOrderBy.VALUE_ASC &&
orderBy !== GraphOrderBy.VALUE_DESC
) {
return null;
}
const aggregateField = objectMetadataItem.fields.find(
(field) => field.id === aggregateFieldMetadataId,
);
if (!isDefined(aggregateField)) {
return null;
}
return {
fieldName: aggregateField.name,
direction: orderBy === GraphOrderBy.VALUE_ASC ? 'ASC' : 'DESC',
};
};
@@ -0,0 +1,51 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildSortsForChartFieldOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartFieldOrderBy';
import { buildSortsForChartValueOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartValueOrderBy';
import { normalizeChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
import { isDefined } from 'twenty-shared/utils';
import {
type BarChartConfiguration,
type LineChartConfiguration,
type PieChartConfiguration,
} from '~/generated/graphql';
type ChartSort = {
fieldName: string;
direction: 'ASC' | 'DESC';
};
type BuildSortsFromChartConfigParams = {
configuration:
| BarChartConfiguration
| LineChartConfiguration
| PieChartConfiguration;
objectMetadataItem: ObjectMetadataItem;
};
export const buildSortsFromChartConfig = ({
configuration,
objectMetadataItem,
}: BuildSortsFromChartConfigParams): ChartSort[] => {
const normalizedFields = normalizeChartConfigurationFields(configuration);
const fieldSort = buildSortsForChartFieldOrderBy({
normalizedFields,
objectMetadataItem,
});
if (isDefined(fieldSort)) {
return [fieldSort];
}
const valueSort = buildSortsForChartValueOrderBy({
normalizedFields,
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
objectMetadataItem,
});
if (isDefined(valueSort)) {
return [valueSort];
}
return [];
};
@@ -0,0 +1,44 @@
import {
type BarChartConfiguration,
type GraphOrderBy,
type LineChartConfiguration,
type ObjectRecordGroupByDateGranularity,
type PieChartConfiguration,
} from '~/generated-metadata/graphql';
export type NormalizedChartConfigurationFields = {
groupByFieldMetadataId?: string;
groupBySubFieldName?: string | null;
dateGranularity?: ObjectRecordGroupByDateGranularity | null;
orderBy?: GraphOrderBy | null;
};
export const normalizeChartConfigurationFields = (
configuration:
| BarChartConfiguration
| LineChartConfiguration
| PieChartConfiguration,
): NormalizedChartConfigurationFields => {
if (
configuration.__typename === 'BarChartConfiguration' ||
configuration.__typename === 'LineChartConfiguration'
) {
return {
groupByFieldMetadataId: configuration.primaryAxisGroupByFieldMetadataId,
groupBySubFieldName: configuration.primaryAxisGroupBySubFieldName,
dateGranularity: configuration.primaryAxisDateGranularity,
orderBy: configuration.primaryAxisOrderBy,
};
}
if (configuration.__typename === 'PieChartConfiguration') {
return {
groupByFieldMetadataId: configuration.groupByFieldMetadataId,
groupBySubFieldName: configuration.groupBySubFieldName,
dateGranularity: configuration.dateGranularity,
orderBy: configuration.orderBy,
};
}
return {};
};