From a8e7d4dfc3c68bebef723c664f14dd4f50a78e19 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:18:52 +0530 Subject: [PATCH] unlock relation date fields on dashboards (#16207) --- .../page-layout/components/ChartSettings.tsx | 2 + ...GroupByFieldSelectionRelationFieldView.tsx | 8 +- .../__tests__/shouldHideChartSetting.test.ts | 46 ++++++ .../utils/shouldHideChartSetting.ts | 14 +- ...transformGroupByDataToBarChartData.test.ts | 97 +++++++++++ .../transformGroupByDataToBarChartData.ts | 5 +- ...transformGroupByDataToPieChartData.test.ts | 153 ++++++++++++++++++ .../graph/hooks/useGraphWidgetGroupByQuery.ts | 5 + .../__tests__/buildGroupByFieldObject.test.ts | 20 +++ .../formatPrimaryDimensionValues.test.ts | 58 +++++++ ...lesFromBarOrLineChartConfiguration.test.ts | 110 ++++++++++++- ...VariablesFromPieChartConfiguration.test.ts | 53 ++++++ .../__tests__/getRelationFieldOrderBy.test.ts | 58 +++++++ .../__tests__/isNestedFieldDateType.test.ts | 57 +++++++ .../graph/utils/buildGroupByFieldObject.ts | 12 ++ .../graph/utils/formatDimensionValue.ts | 20 +++ .../utils/formatPrimaryDimensionValues.ts | 9 +- ...ariablesFromBarOrLineChartConfiguration.ts | 39 ++++- ...QueryVariablesFromPieChartConfiguration.ts | 17 +- .../widgets/graph/utils/getFieldOrderBy.ts | 1 + .../graph/utils/getRelationFieldOrderBy.ts | 15 ++ .../graph/utils/isNestedFieldDateType.ts | 44 +++++ 22 files changed, 818 insertions(+), 25 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/transformGroupByDataToBarChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatPrimaryDimensionValues.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getRelationFieldOrderBy.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/isNestedFieldDateType.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/isNestedFieldDateType.ts diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx index 122ae6844a..3e31e844ca 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -96,6 +96,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { isGroupByEnabled as boolean, configuration, objectMetadataItem, + objectMetadataItems, ), ) .map((item) => item.id), @@ -144,6 +145,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { isGroupByEnabled as boolean, configuration, objectMetadataItem, + objectMetadataItems, ), ); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx index de78b7378f..78d3290a28 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx @@ -16,7 +16,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useMemo, useState } from 'react'; -import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { IconChevronLeft, useIcons } from 'twenty-ui/display'; import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation'; import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; @@ -70,11 +70,7 @@ export const ChartGroupByFieldSelectionRelationFieldView = ({ return filterBySearchQuery({ items: targetObjectMetadataItem.fields.filter( - (field) => - !field.isSystem && - !isFieldRelation(field) && - // TODO: Backend doesn't fully support date fields for relation fields yet so we hide them for now. https://github.com/twentyhq/core-team-issues/issues/1935 - !isFieldMetadataDateKind(field.type), + (field) => !field.isSystem && !isFieldRelation(field), ), searchQuery, getSearchableValues: (field) => [field.label, field.name], diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts index b930d201c1..ba55269def 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/shouldHideChartSetting.test.ts @@ -385,6 +385,28 @@ describe('shouldHideChartSetting', () => { }); describe('DATE_GRANULARITY (Pie Chart)', () => { + const relationField: any = { + id: 'relation-field-id', + name: 'company', + label: 'Company', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + }; + + const targetObjectMetadata: any = { + id: 'company-id', + nameSingular: 'company', + namePlural: 'companies', + fields: [ + { + id: 'company-created-at', + name: 'createdAt', + label: 'Created At', + type: FieldMetadataType.DATE, + }, + ], + }; + it('should show when group by field is a date field', () => { const pieChartConfig: ChartConfiguration = { __typename: 'PieChartConfiguration', @@ -435,6 +457,30 @@ describe('shouldHideChartSetting', () => { expect(result).toBe(true); }); + + it('should show when group by field is a relation date subfield', () => { + const objectMetadataItemWithRelation: ObjectMetadataItem = { + ...mockObjectMetadataItem, + fields: [...mockObjectMetadataItem.fields, relationField], + } as any; + + const pieChartConfig: ChartConfiguration = { + __typename: 'PieChartConfiguration', + groupByFieldMetadataId: relationField.id, + groupBySubFieldName: 'createdAt', + } as any; + + const result = shouldHideChartSetting( + mockDateGranularityItem, + 'object-id', + true, + pieChartConfig, + objectMetadataItemWithRelation, + [objectMetadataItemWithRelation, targetObjectMetadata] as any, + ); + + expect(result).toBe(false); + }); }); }); }); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts index 27b46baabd..2047bf3f91 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts @@ -3,12 +3,15 @@ import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layou import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType'; import { isNonEmptyString } from '@sniptt/guards'; import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; const shouldHideDateGranularityBasedOnFieldType = ( fieldMetadataId: string | undefined | null, + subFieldName: string | undefined | null, objectMetadataItem: ObjectMetadataItem, + objectMetadataItems: ObjectMetadataItem[], ): boolean => { if (!isDefined(fieldMetadataId)) { return true; @@ -22,8 +25,8 @@ const shouldHideDateGranularityBasedOnFieldType = ( return true; } - if (isFieldRelation(field)) { - return true; + if (isFieldRelation(field) && isDefined(subFieldName)) { + return !isNestedFieldDateType(field, subFieldName, objectMetadataItems); } return !isFieldMetadataDateKind(field.type); @@ -35,6 +38,7 @@ export const shouldHideChartSetting = ( isGroupByEnabled: boolean, configuration?: ChartConfiguration, objectMetadataItem?: ObjectMetadataItem, + objectMetadataItems?: ObjectMetadataItem[], ): boolean => { const hasNoObjectMetadata = !isNonEmptyString(objectMetadataId); const dependsOnSource = item?.dependsOn?.includes( @@ -53,7 +57,9 @@ export const shouldHideChartSetting = ( if (isBarOrLineChart) { return shouldHideDateGranularityBasedOnFieldType( configuration.primaryAxisGroupByFieldMetadataId, + configuration.primaryAxisGroupBySubFieldName, objectMetadataItem, + objectMetadataItems ?? [], ); } } @@ -66,7 +72,9 @@ export const shouldHideChartSetting = ( if (isBarOrLineChart) { return shouldHideDateGranularityBasedOnFieldType( configuration.secondaryAxisGroupByFieldMetadataId, + configuration.secondaryAxisGroupBySubFieldName, objectMetadataItem, + objectMetadataItems ?? [], ); } } @@ -75,7 +83,9 @@ export const shouldHideChartSetting = ( if (configuration.__typename === 'PieChartConfiguration') { return shouldHideDateGranularityBasedOnFieldType( configuration.groupByFieldMetadataId, + configuration.groupBySubFieldName, objectMetadataItem, + objectMetadataItems ?? [], ); } } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/transformGroupByDataToBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/transformGroupByDataToBarChartData.test.ts new file mode 100644 index 0000000000..7071e24811 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/transformGroupByDataToBarChartData.test.ts @@ -0,0 +1,97 @@ +import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData'; +import { + FieldMetadataType, + ObjectRecordGroupByDateGranularity, +} from 'twenty-shared/types'; +import { GraphType } from '~/generated-metadata/graphql'; +import { AxisNameDisplay } from '~/generated/graphql'; + +jest.mock( + '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData', + () => ({ + fillDateGapsInBarChartData: jest.fn(({ data }) => ({ + data, + wasTruncated: true, + })), + }), +); + +jest.mock( + '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData', + () => ({ + transformOneDimensionalGroupByToBarChartData: jest.fn(() => ({ + data: [], + indexBy: 'x', + keys: ['value'], + series: [], + hasTooManyGroups: false, + formattedToRawLookup: new Map(), + })), + }), +); + +jest.mock('@/page-layout/widgets/graph/utils/filterGroupByResults', () => ({ + filterGroupByResults: jest.fn((args) => args.rawResults), +})); + +const { fillDateGapsInBarChartData } = jest.requireMock( + '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData', +) as { fillDateGapsInBarChartData: jest.Mock }; + +describe('transformGroupByDataToBarChartData', () => { + it('fills date gaps when grouping by a relation date subfield with granularity', () => { + const groupByField = { + id: 'group-by-field', + name: 'company', + type: FieldMetadataType.RELATION, + }; + + const aggregateField = { + id: 'aggregate-field', + name: 'count', + type: FieldMetadataType.NUMBER, + }; + + const objectMetadataItem = { + id: 'obj-1', + nameSingular: 'company', + namePlural: 'companies', + fields: [groupByField, aggregateField], + } as any; + + const configuration = { + __typename: 'BarChartConfiguration', + aggregateFieldMetadataId: aggregateField.id, + aggregateOperation: 'COUNT', + graphType: GraphType.VERTICAL_BAR, + primaryAxisGroupByFieldMetadataId: groupByField.id, + primaryAxisGroupBySubFieldName: 'createdAt', + primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + axisNameDisplay: AxisNameDisplay.BOTH, + } as any; + + const groupByData = { + companiesGroupBy: [ + { groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], COUNT: 1 }, + ], + }; + + fillDateGapsInBarChartData.mockClear(); + + const result = transformGroupByDataToBarChartData({ + groupByData, + objectMetadataItem, + configuration, + aggregateOperation: 'COUNT', + }); + + expect(fillDateGapsInBarChartData).toHaveBeenCalledTimes(1); + expect(fillDateGapsInBarChartData).toHaveBeenCalledWith( + expect.objectContaining({ + dateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + hasSecondDimension: false, + }), + ); + expect(result.hasTooManyGroups).toBe(true); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts index 21f8044f39..48c08ca389 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts @@ -150,11 +150,14 @@ export const transformGroupByDataToBarChartData = ({ const showLegend = configuration.displayLegend ?? true; const isDateField = isFieldMetadataDateKind(groupByFieldX.type); + const isNestedDateField = + !isDateField && isDefined(configuration.primaryAxisDateGranularity); + const shouldApplyDateGapFill = isDateField || isNestedDateField; const omitNullValues = configuration.omitNullValues ?? false; const dateGapFillResult = - isDateField && !omitNullValues + shouldApplyDateGapFill && !omitNullValues ? fillDateGapsInBarChartData({ data: filteredResults, keys: [aggregateField.name], diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts new file mode 100644 index 0000000000..006e2c5bec --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts @@ -0,0 +1,153 @@ +import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant'; +import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant'; +import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData'; +import { + AggregateOperations, + FieldMetadataType, + GraphType, +} from '~/generated-metadata/graphql'; +import { type PieChartConfiguration } from '~/generated/graphql'; + +jest.mock( + '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues', + () => ({ + formatPrimaryDimensionValues: jest.fn(), + }), +); + +const { formatPrimaryDimensionValues } = jest.requireMock( + '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues', +) as { formatPrimaryDimensionValues: jest.Mock }; + +describe('transformGroupByDataToPieChartData', () => { + it('keeps null group buckets aligned with their aggregate values', () => { + const groupByField = { + id: 'group-by-field', + name: 'status', + type: FieldMetadataType.TEXT, + label: 'Status', + }; + + const aggregateField = { + id: 'aggregate-field', + name: 'id', + type: FieldMetadataType.UUID, + label: 'Id', + }; + + const objectMetadataItem = { + id: 'obj-1', + nameSingular: 'company', + namePlural: 'companies', + fields: [groupByField, aggregateField], + } as any; + + const configuration = { + __typename: 'PieChartConfiguration', + aggregateFieldMetadataId: aggregateField.id, + aggregateOperation: AggregateOperations.COUNT, + graphType: GraphType.PIE, + groupByFieldMetadataId: groupByField.id, + displayLegend: true, + } as PieChartConfiguration; + + const groupByData = { + companiesGroupBy: [ + { groupByDimensionValues: [null], COUNT: 2 }, + { groupByDimensionValues: ['Active'], COUNT: 5 }, + ], + }; + + formatPrimaryDimensionValues.mockReturnValue([ + { + formattedPrimaryDimensionValue: 'Not Set', + rawPrimaryDimensionValue: null, + }, + { + formattedPrimaryDimensionValue: 'Active', + rawPrimaryDimensionValue: 'Active', + }, + ]); + + const result = transformGroupByDataToPieChartData({ + groupByData, + objectMetadataItem, + configuration, + aggregateOperation: 'COUNT', + }); + + expect(result.data).toEqual([ + { id: 'Not Set', value: 2, color: GRAPH_DEFAULT_COLOR }, + { id: 'Active', value: 5, color: GRAPH_DEFAULT_COLOR }, + ]); + expect(result.formattedToRawLookup.get('Not Set')).toBeNull(); + expect(result.formattedToRawLookup.get('Active')).toBe('Active'); + expect(result.hasTooManyGroups).toBe(false); + }); + + it('honors color configuration, hides legend, and flags too many groups', () => { + const groupByField = { + id: 'group-by-field', + name: 'status', + type: FieldMetadataType.TEXT, + label: 'Status', + }; + + const aggregateField = { + id: 'aggregate-field', + name: 'id', + type: FieldMetadataType.UUID, + label: 'Id', + }; + + const objectMetadataItem = { + id: 'obj-1', + nameSingular: 'company', + namePlural: 'companies', + fields: [groupByField, aggregateField], + } as any; + + const configuration = { + __typename: 'PieChartConfiguration', + aggregateFieldMetadataId: aggregateField.id, + aggregateOperation: AggregateOperations.COUNT, + graphType: GraphType.PIE, + groupByFieldMetadataId: groupByField.id, + displayLegend: false, + color: 'red', + } as PieChartConfiguration; + + const totalResults = PIE_CHART_MAXIMUM_NUMBER_OF_SLICES + 1; + const groupByData = { + companiesGroupBy: Array.from({ length: totalResults }).map( + (_unused, index) => ({ + groupByDimensionValues: [`Group ${index}`], + COUNT: index + 1, + }), + ), + }; + + formatPrimaryDimensionValues.mockReturnValue( + Array.from({ length: totalResults }).map((_unused, index) => ({ + formattedPrimaryDimensionValue: `Group ${index}`, + rawPrimaryDimensionValue: `Group ${index}`, + })), + ); + + const result = transformGroupByDataToPieChartData({ + groupByData, + objectMetadataItem, + configuration, + aggregateOperation: 'COUNT', + }); + + expect(result.showLegend).toBe(false); + expect(result.hasTooManyGroups).toBe(true); + expect(result.data).toHaveLength(PIE_CHART_MAXIMUM_NUMBER_OF_SLICES); + expect(result.data[0]).toEqual({ + id: 'Group 0', + value: 1, + color: 'red', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts index 52ffb804b0..f1a49e4627 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts @@ -1,5 +1,6 @@ import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { generateGroupByAggregateQuery } from '@/object-record/record-aggregate/utils/generateGroupByAggregateQuery'; import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields'; import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon'; @@ -33,6 +34,8 @@ export const useGraphWidgetGroupByQuery = ({ configuration, }); + const { objectMetadataItems } = useObjectMetadataItems(); + if (!isDefined(aggregateField)) { throw new Error('Aggregate field not found'); } @@ -63,6 +66,7 @@ export const useGraphWidgetGroupByQuery = ({ const groupByQueryVariables = isPieChart(configuration) ? generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem, + objectMetadataItems, chartConfiguration: configuration, aggregateOperation: aggregateOperation, limit, @@ -70,6 +74,7 @@ export const useGraphWidgetGroupByQuery = ({ }) : generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem, + objectMetadataItems, chartConfiguration: configuration as | BarChartConfiguration | LineChartConfiguration, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts index 89097fa65a..8bcddc7ae0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts @@ -42,6 +42,26 @@ describe('buildGroupByFieldObject', () => { }); }); + it('should return date granularity for relation date field', () => { + const field = { + name: 'company', + type: FieldMetadataType.RELATION, + } as any; + + const result = buildGroupByFieldObject({ + field, + subFieldName: 'createdAt', + dateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + isNestedDateField: true, + }); + + expect(result).toEqual({ + company: { + createdAt: { granularity: ObjectRecordGroupByDateGranularity.MONTH }, + }, + }); + }); + it('should return nested object for composite fields with subfield', () => { const field = { name: 'name', diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatPrimaryDimensionValues.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatPrimaryDimensionValues.test.ts new file mode 100644 index 0000000000..c790fc4c30 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/formatPrimaryDimensionValues.test.ts @@ -0,0 +1,58 @@ +import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues'; +import { + FieldMetadataType, + ObjectRecordGroupByDateGranularity, +} from 'twenty-shared/types'; + +jest.mock('@/page-layout/widgets/graph/utils/formatDimensionValue', () => ({ + formatDimensionValue: jest.fn((args: { value: unknown }) => { + return `formatted-${String(args.value)}`; + }), +})); + +const { formatDimensionValue } = jest.requireMock( + '@/page-layout/widgets/graph/utils/formatDimensionValue', +) as { formatDimensionValue: jest.Mock }; + +describe('formatPrimaryDimensionValues', () => { + it('includes buckets where the primary dimension value is null', () => { + const result = formatPrimaryDimensionValues({ + groupByRawResults: [ + { groupByDimensionValues: [null] }, + { groupByDimensionValues: ['Active'] }, + ], + primaryAxisGroupByField: { + name: 'status', + type: FieldMetadataType.TEXT, + } as any, + }); + + expect(result).toHaveLength(2); + expect(result[0].rawPrimaryDimensionValue).toBeNull(); + expect(result[0].formattedPrimaryDimensionValue).toBe('formatted-null'); + expect(result[1].formattedPrimaryDimensionValue).toBe('formatted-Active'); + }); + + it('passes granularity and subfield to the formatter', () => { + formatDimensionValue.mockClear(); + + formatPrimaryDimensionValues({ + groupByRawResults: [ + { groupByDimensionValues: ['2024-01-15T00:00:00.000Z'] }, + ], + primaryAxisGroupByField: { + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + } as any, + primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + primaryAxisGroupBySubFieldName: 'createdAt', + }); + + expect(formatDimensionValue).toHaveBeenCalledWith({ + value: '2024-01-15T00:00:00.000Z', + fieldMetadata: expect.objectContaining({ name: 'createdAt' }), + dateGranularity: ObjectRecordGroupByDateGranularity.MONTH, + subFieldName: 'createdAt', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.test.ts index 80b9e9bc97..8984aed9cc 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.test.ts @@ -1,6 +1,9 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { generateGroupByQueryVariablesFromBarOrLineChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + FieldMetadataType, + ObjectRecordGroupByDateGranularity, +} from 'twenty-shared/types'; import { AggregateOperations, type BarChartConfiguration, @@ -67,6 +70,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.VERTICAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -81,6 +85,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.VERTICAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -97,6 +102,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.VERTICAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-4', @@ -111,6 +117,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.VERTICAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-3', @@ -128,6 +135,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.HORIZONTAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -142,6 +150,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ graphType: GraphType.HORIZONTAL_BAR, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -152,6 +161,98 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { expect(result).toMatchSnapshot(); }); }); + + it('applies date granularity when primary axis uses a relation date subfield', () => { + const relationField = { + id: 'field-rel', + name: 'company', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + }; + + const objectMetadataItem: ObjectMetadataItem = { + id: 'obj-main', + nameSingular: 'opportunity', + namePlural: 'opportunities', + fields: [relationField], + } as ObjectMetadataItem; + + const targetObjectMetadata: ObjectMetadataItem = { + id: 'obj-company', + nameSingular: 'company', + namePlural: 'companies', + fields: [ + { + id: 'company-created-at', + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + }, + ], + } as ObjectMetadataItem; + + const result = + generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ + objectMetadataItem, + objectMetadataItems: [objectMetadataItem, targetObjectMetadata], + chartConfiguration: buildBarChartConfiguration({ + graphType: GraphType.VERTICAL_BAR, + primaryAxisGroupByFieldMetadataId: relationField.id, + primaryAxisGroupBySubFieldName: 'createdAt', + }), + }); + + expect(result.groupBy[0]).toEqual({ + company: { + createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY }, + }, + }); + }); + + it('applies date granularity for relation date subfield in line charts as well', () => { + const relationField = { + id: 'field-rel', + name: 'company', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + }; + + const objectMetadataItem: ObjectMetadataItem = { + id: 'obj-main', + nameSingular: 'opportunity', + namePlural: 'opportunities', + fields: [relationField], + } as ObjectMetadataItem; + + const targetObjectMetadata: ObjectMetadataItem = { + id: 'obj-company', + nameSingular: 'company', + namePlural: 'companies', + fields: [ + { + id: 'company-created-at', + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + }, + ], + } as ObjectMetadataItem; + + const result = + generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ + objectMetadataItem, + objectMetadataItems: [objectMetadataItem, targetObjectMetadata], + chartConfiguration: buildLineChartConfiguration({ + graphType: GraphType.LINE, + primaryAxisGroupByFieldMetadataId: relationField.id, + primaryAxisGroupBySubFieldName: 'createdAt', + }), + }); + + expect(result.groupBy[0]).toEqual({ + company: { + createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY }, + }, + }); + }); }); describe('Line Chart Configuration', () => { @@ -159,6 +260,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ graphType: GraphType.LINE, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -173,6 +275,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ graphType: GraphType.LINE, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -189,6 +292,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ graphType: GraphType.LINE, primaryAxisGroupByFieldMetadataId: 'field-3', @@ -204,6 +308,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ graphType: GraphType.LINE, primaryAxisGroupByFieldMetadataId: 'field-4', @@ -218,6 +323,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { const result = generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ graphType: GraphType.LINE, primaryAxisGroupByFieldMetadataId: 'field-1', @@ -234,6 +340,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { expect(() => generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildBarChartConfiguration({ primaryAxisGroupByFieldMetadataId: 'invalid-field', }), @@ -245,6 +352,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => { expect(() => generateGroupByQueryVariablesFromBarOrLineChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildLineChartConfiguration({ primaryAxisGroupByFieldMetadataId: 'invalid-field', }), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromPieChartConfiguration.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromPieChartConfiguration.test.ts index 1fa9061e6f..a1ed8e63e8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromPieChartConfiguration.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromPieChartConfiguration.test.ts @@ -6,6 +6,7 @@ import { GraphType, type PieChartConfiguration, } from '~/generated-metadata/graphql'; +import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { generateGroupByQueryVariablesFromPieChartConfiguration } from '../generateGroupByQueryVariablesFromPieChartConfiguration'; describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { @@ -53,6 +54,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { it('should generate variables with single groupBy field', () => { const result = generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'field-1', groupBySubFieldName: null, @@ -65,6 +67,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { it('should generate variables with composite field', () => { const result = generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'field-4', groupBySubFieldName: 'firstName', @@ -77,6 +80,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { it('should generate variables with date field and granularity', () => { const result = generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'field-3', groupBySubFieldName: null, @@ -90,6 +94,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { it('should generate variables with limit', () => { const result = generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'field-1', }), @@ -103,6 +108,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { it('should generate variables with orderBy', () => { const result = generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'field-1', orderBy: GraphOrderBy.VALUE_ASC, @@ -115,11 +121,58 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => { }); }); + describe('Relation date subfield', () => { + it('applies date granularity when grouping by a relation date subfield', () => { + const relationField = { + id: 'field-rel', + name: 'company', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + }; + + const mainObjectMetadata: ObjectMetadataItem = { + id: 'obj-main', + nameSingular: 'opportunity', + namePlural: 'opportunities', + fields: [relationField], + } as ObjectMetadataItem; + + const targetObjectMetadata: ObjectMetadataItem = { + id: 'obj-company', + nameSingular: 'company', + namePlural: 'companies', + fields: [ + { + id: 'company-created-at', + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + }, + ], + } as ObjectMetadataItem; + + const result = generateGroupByQueryVariablesFromPieChartConfiguration({ + objectMetadataItem: mainObjectMetadata, + objectMetadataItems: [mainObjectMetadata, targetObjectMetadata], + chartConfiguration: buildPieChartConfiguration({ + groupByFieldMetadataId: relationField.id, + groupBySubFieldName: 'createdAt', + }), + }); + + expect(result.groupBy[0]).toEqual({ + company: { + createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY }, + }, + }); + }); + }); + describe('Error Handling', () => { it('should throw error when groupBy field not found', () => { expect(() => generateGroupByQueryVariablesFromPieChartConfiguration({ objectMetadataItem: mockObjectMetadataItem, + objectMetadataItems: [], chartConfiguration: buildPieChartConfiguration({ groupByFieldMetadataId: 'invalid-field', }), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getRelationFieldOrderBy.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getRelationFieldOrderBy.test.ts new file mode 100644 index 0000000000..b013cadec1 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getRelationFieldOrderBy.test.ts @@ -0,0 +1,58 @@ +import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy'; +import { + FieldMetadataType, + ObjectRecordGroupByDateGranularity, + OrderByDirection, +} from 'twenty-shared/types'; + +describe('getRelationFieldOrderBy', () => { + const relationField = { + name: 'company', + type: FieldMetadataType.RELATION, + } as any; + + it('returns relation id ordering when no subfield provided', () => { + const orderBy = getRelationFieldOrderBy( + relationField, + null, + OrderByDirection.AscNullsLast, + ); + + expect(orderBy).toEqual({ companyId: OrderByDirection.AscNullsLast }); + }); + + it('adds granularity when nested relation field is a date field', () => { + const orderBy = getRelationFieldOrderBy( + relationField, + 'createdAt', + OrderByDirection.DescNullsLast, + undefined, + true, + ); + + expect(orderBy).toEqual({ + company: { + createdAt: { + orderBy: OrderByDirection.DescNullsLast, + granularity: ObjectRecordGroupByDateGranularity.DAY, + }, + }, + }); + }); + + it('returns nested relation ordering without granularity for non-date subfield', () => { + const orderBy = getRelationFieldOrderBy( + relationField, + 'name', + OrderByDirection.AscNullsLast, + undefined, + false, + ); + + expect(orderBy).toEqual({ + company: { + name: OrderByDirection.AscNullsLast, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/isNestedFieldDateType.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/isNestedFieldDateType.test.ts new file mode 100644 index 0000000000..f69aec76ce --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/isNestedFieldDateType.test.ts @@ -0,0 +1,57 @@ +import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType'; +import { FieldMetadataType } from 'twenty-shared/types'; + +describe('isNestedFieldDateType', () => { + const companyObject = { + id: 'company-id', + nameSingular: 'company', + namePlural: 'companies', + fields: [ + { id: 'createdAt', name: 'createdAt', type: FieldMetadataType.DATE_TIME }, + { id: 'name', name: 'name', type: FieldMetadataType.TEXT }, + ], + } as any; + + const relationField = { + name: 'company', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + } as any; + + it('returns true for a relation subfield that is a date type', () => { + const result = isNestedFieldDateType(relationField, 'createdAt', [ + companyObject, + ]); + + expect(result).toBe(true); + }); + + it('returns false when the nested subfield is not a date type', () => { + const result = isNestedFieldDateType(relationField, 'name', [ + companyObject, + ]); + + expect(result).toBe(false); + }); + + it('returns false when subFieldName is missing', () => { + const result = isNestedFieldDateType(relationField, undefined, [ + companyObject, + ]); + + expect(result).toBe(false); + }); + + it('returns false for non-relation fields', () => { + const nonRelationField = { + name: 'status', + type: FieldMetadataType.TEXT, + } as any; + + const result = isNestedFieldDateType(nonRelationField, 'createdAt', [ + companyObject, + ]); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts index c2c0f1aab1..cfc711edc6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts @@ -20,11 +20,13 @@ export const buildGroupByFieldObject = ({ subFieldName, dateGranularity, firstDayOfTheWeek, + isNestedDateField, }: { field: FieldMetadataItem; subFieldName?: string | null; dateGranularity?: ObjectRecordGroupByDateGranularity; firstDayOfTheWeek?: number | null; + isNestedDateField?: boolean; }): GroupByFieldObject => { const isRelation = isFieldRelation(field) || isFieldMorphRelation(field); const isComposite = isCompositeFieldType(field.type); @@ -39,6 +41,16 @@ export const buildGroupByFieldObject = ({ const nestedFieldName = parts[0]; const nestedSubFieldName = parts[1]; + if (isNestedDateField === true || isDefined(dateGranularity)) { + return { + [field.name]: { + [nestedFieldName]: { + granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, + }, + }, + }; + } + if (isDefined(nestedSubFieldName)) { return { [field.name]: { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts index f43a662cfa..7200f4a3fb 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts @@ -88,6 +88,26 @@ export const formatDimensionValue = ({ return formatDateByGranularity(new Date(String(value)), dateGranularity); } + case FieldMetadataType.RELATION: { + if (isDefined(dateGranularity)) { + if ( + dateGranularity === + ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK || + dateGranularity === + ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR || + dateGranularity === + ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR + ) { + return String(value); + } + return formatDateByGranularity( + new Date(String(value)), + dateGranularity, + ); + } + return String(value); + } + case FieldMetadataType.NUMBER: case FieldMetadataType.CURRENCY: { if ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatPrimaryDimensionValues.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatPrimaryDimensionValues.ts index 68c1e48769..76b5f4dbe5 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatPrimaryDimensionValues.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatPrimaryDimensionValues.ts @@ -2,7 +2,6 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; -import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordGroupByDateGranularity } from '~/generated/graphql'; type FormatPrimaryDimensionValuesParameters = { @@ -27,12 +26,8 @@ export const formatPrimaryDimensionValues = ({ (accumulator, rawResult) => { const groupByDimensionValues = rawResult.groupByDimensionValues; - if (!isDefined(groupByDimensionValues?.[0])) { - return accumulator; - } - - const rawPrimaryDimensionValue = - groupByDimensionValues[0] as RawDimensionValue; + const rawPrimaryDimensionValue = (groupByDimensionValues?.[0] ?? + null) as RawDimensionValue; const formattedPrimaryDimensionValue = formatDimensionValue({ value: rawPrimaryDimensionValue, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts index a3e244a4db..e8d775321e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts @@ -1,6 +1,7 @@ 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 { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType'; import { type AggregateOrderByWithGroupByField, type ObjectRecordOrderByForCompositeField, @@ -20,12 +21,14 @@ import { export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ objectMetadataItem, + objectMetadataItems, chartConfiguration, aggregateOperation, limit, firstDayOfTheWeek, }: { objectMetadataItem: ObjectMetadataItem; + objectMetadataItems: ObjectMetadataItem[]; chartConfiguration: BarChartConfiguration | LineChartConfiguration; aggregateOperation?: string; limit?: number; @@ -58,33 +61,50 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type); + const isFieldXNestedDate = isNestedFieldDateType( + groupByFieldX, + groupBySubFieldNameX, + objectMetadataItems, + ); + + const shouldApplyDateGranularityX = isFieldXDate || isFieldXNestedDate; + const groupBy: Array = []; groupBy.push( buildGroupByFieldObject({ field: groupByFieldX, subFieldName: groupBySubFieldNameX, - dateGranularity: isFieldXDate + dateGranularity: shouldApplyDateGranularityX ? (chartConfiguration.primaryAxisDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, - firstDayOfTheWeek, + isNestedDateField: isFieldXNestedDate, }), ); if (isDefined(groupByFieldY)) { const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type); + const isFieldYNestedDate = isNestedFieldDateType( + groupByFieldY, + groupBySubFieldNameY, + objectMetadataItems, + ); + + const shouldApplyDateGranularityY = isFieldYDate || isFieldYNestedDate; + groupBy.push( buildGroupByFieldObject({ field: groupByFieldY, subFieldName: groupBySubFieldNameY, - dateGranularity: isFieldYDate + dateGranularity: shouldApplyDateGranularityY ? (chartConfiguration.secondaryAxisGroupByDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, firstDayOfTheWeek, + isNestedDateField: isFieldYNestedDate, }), ); } @@ -104,7 +124,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ groupByField: groupByFieldX, groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName, aggregateOperation, - dateGranularity: isFieldXDate + dateGranularity: shouldApplyDateGranularityX ? (chartConfiguration.primaryAxisDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, @@ -117,6 +137,15 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ ) { const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type); + const isFieldYNestedDateForOrderBy = isNestedFieldDateType( + groupByFieldY, + groupBySubFieldNameY, + objectMetadataItems, + ); + + const shouldApplyDateGranularityYForOrderBy = + isFieldYDateForOrderBy || isFieldYNestedDateForOrderBy; + orderBy.push( getGroupByOrderBy({ graphOrderBy: chartConfiguration.secondaryAxisOrderBy, @@ -124,7 +153,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ groupBySubFieldName: chartConfiguration.secondaryAxisGroupBySubFieldName, aggregateOperation, - dateGranularity: isFieldYDateForOrderBy + dateGranularity: shouldApplyDateGranularityYForOrderBy ? (chartConfiguration.secondaryAxisGroupByDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts index bf85dd7ea7..a47c5c6dfe 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts @@ -1,6 +1,7 @@ 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 { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType'; import { type AggregateOrderByWithGroupByField, type ObjectRecordOrderByForCompositeField, @@ -17,12 +18,14 @@ import { export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ objectMetadataItem, + objectMetadataItems, chartConfiguration, aggregateOperation, limit, firstDayOfTheWeek, }: { objectMetadataItem: ObjectMetadataItem; + objectMetadataItems: ObjectMetadataItem[]; chartConfiguration: PieChartConfiguration; aggregateOperation?: string; limit?: number; @@ -45,15 +48,23 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ const isFieldDate = isFieldMetadataDateKind(groupByField.type); + const isNestedDate = isNestedFieldDateType( + groupByField, + groupBySubFieldName, + objectMetadataItems, + ); + + const shouldApplyDateGranularity = isFieldDate || isNestedDate; + const groupBy: Array = [ buildGroupByFieldObject({ field: groupByField, subFieldName: groupBySubFieldName, - - dateGranularity: isFieldDate + dateGranularity: shouldApplyDateGranularity ? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, firstDayOfTheWeek, + isNestedDateField: isNestedDate, }), ]; @@ -72,7 +83,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ groupByField, groupBySubFieldName, aggregateOperation, - dateGranularity: isFieldDate + dateGranularity: shouldApplyDateGranularity ? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) : undefined, }), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts index c6bf3181a0..282ff9e927 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts @@ -51,6 +51,7 @@ export const getFieldOrderBy = ( groupByField, groupBySubFieldName, direction, + dateGranularity, ); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts index 214cb9d527..d3a9b5a06b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts @@ -1,5 +1,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; import { + type ObjectRecordGroupByDateGranularity, type ObjectRecordOrderByForRelationField, type ObjectRecordOrderByForScalarField, type OrderByDirection, @@ -10,6 +12,8 @@ export const getRelationFieldOrderBy = ( groupByField: FieldMetadataItem, groupBySubFieldName: string | null | undefined, direction: OrderByDirection, + dateGranularity?: ObjectRecordGroupByDateGranularity, + isNestedDateField?: boolean, ): ObjectRecordOrderByForScalarField | ObjectRecordOrderByForRelationField => { if (!isDefined(groupBySubFieldName)) { return { @@ -19,6 +23,17 @@ export const getRelationFieldOrderBy = ( const [nestedFieldName, nestedSubFieldName] = groupBySubFieldName.split('.'); + if (isNestedDateField === true || isDefined(dateGranularity)) { + return { + [groupByField.name]: { + [nestedFieldName]: { + orderBy: direction, + granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, + }, + }, + }; + } + if (!isDefined(nestedSubFieldName)) { return { [groupByField.name]: { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/isNestedFieldDateType.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/isNestedFieldDateType.ts new file mode 100644 index 0000000000..075a067690 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/isNestedFieldDateType.ts @@ -0,0 +1,44 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; + +export const isNestedFieldDateType = ( + field: FieldMetadataItem, + subFieldName: string | undefined, + objectMetadataItems: ObjectMetadataItem[], +): boolean => { + if (!isDefined(subFieldName)) { + return false; + } + + if (!isFieldRelation(field)) { + return false; + } + + const targetObjectNameSingular = + field.relation?.targetObjectMetadata?.nameSingular; + + if (!isDefined(targetObjectNameSingular)) { + return false; + } + + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === targetObjectNameSingular, + ); + + if (!isDefined(targetObjectMetadataItem)) { + return false; + } + + const nestedFieldName = subFieldName.split('.')[0]; + const nestedField = targetObjectMetadataItem.fields.find( + (f) => f.name === nestedFieldName, + ); + + if (!isDefined(nestedField)) { + return false; + } + + return isFieldMetadataDateKind(nestedField.type); +};