From d30453f38f198d0efedea3535219458b4a26de49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:55:44 +0100 Subject: [PATCH] [DASHBOARDS] Fix date order by (#16521) ## Bug description The date order by was always set to date ascending regardless of the setting. This PR fixes this and removes the possibility to sort by value for date fields as it doesn't really make sense. ## Video QA https://github.com/user-attachments/assets/cffc69e7-bee0-4944-88bb-bdf24fe0dd54 --- .../ChartSortBySelectionDropdownContent.tsx | 77 +++++++---- .../buildChartGroupByFieldConfigUpdate.ts | 35 ++++- .../fillDateGapsInBarChartData.test.ts | 124 ++++++++++++++++++ .../utils/fillDateGapsInBarChartData.ts | 5 + ...illDateGapsInOneDimensionalBarChartData.ts | 8 +- ...illDateGapsInTwoDimensionalBarChartData.ts | 8 +- .../utils/getDateGroupsFromData.ts | 23 +++- .../transformGroupByDataToBarChartData.ts | 1 + 8 files changed, 243 insertions(+), 38 deletions(-) diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortBySelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortBySelectionDropdownContent.tsx index 2e04ff647a..cf11178514 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortBySelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortBySelectionDropdownContent.tsx @@ -3,6 +3,8 @@ import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hook import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; @@ -12,11 +14,11 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { type CompositeFieldSubFieldName } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { MenuItemSelect } from 'twenty-ui/navigation'; import { type BarChartConfiguration, - type GraphOrderBy, + GraphOrderBy, type LineChartConfiguration, } from '~/generated/graphql'; @@ -54,8 +56,48 @@ export const ChartSortBySelectionDropdownContent = () => { objectMetadataId: widgetInEditMode.objectMetadataId, }); + const { objectMetadataItems } = useObjectMetadataItems(); + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === widgetInEditMode.objectMetadataId, + ); + const isPieChart = configuration.__typename === 'PieChartConfiguration'; const isLineChart = configuration.__typename === 'LineChartConfiguration'; + const isBarChart = configuration.__typename === 'BarChartConfiguration'; + + let currentOrderBy: GraphOrderBy | undefined; + let groupByFieldMetadataId: string | undefined; + let groupBySubFieldName: string | null | undefined; + + if (isPieChart) { + currentOrderBy = configuration.orderBy ?? undefined; + groupByFieldMetadataId = configuration.groupByFieldMetadataId; + groupBySubFieldName = configuration.groupBySubFieldName; + } else { + const barOrLineChartConfiguration = configuration as + | BarChartConfiguration + | LineChartConfiguration; + currentOrderBy = + barOrLineChartConfiguration.primaryAxisOrderBy ?? undefined; + groupByFieldMetadataId = + barOrLineChartConfiguration.primaryAxisGroupByFieldMetadataId; + groupBySubFieldName = + barOrLineChartConfiguration.primaryAxisGroupBySubFieldName; + } + + const primaryAxisField = objectMetadataItem?.fields.find( + (field) => field.id === groupByFieldMetadataId, + ); + + const isPrimaryAxisDateField = + isFieldMetadataDateKind(primaryAxisField?.type) || + (isDefined(primaryAxisField) && + isRelationNestedFieldDateKind({ + relationField: primaryAxisField, + relationNestedFieldName: groupBySubFieldName ?? undefined, + objectMetadataItems, + })); const handleSelect = (orderBy: GraphOrderBy) => { if (isPieChart) { @@ -71,32 +113,21 @@ export const ChartSortBySelectionDropdownContent = () => { }; const availableOptions = X_SORT_BY_OPTIONS.filter((option) => { + const isValueSort = + option.value === GraphOrderBy.VALUE_ASC || + option.value === GraphOrderBy.VALUE_DESC; + if (isLineChart) { - return option.value !== 'VALUE_ASC' && option.value !== 'VALUE_DESC'; + return !isValueSort; } + + if ((isBarChart || isPieChart) && isPrimaryAxisDateField) { + return !isValueSort; + } + return true; }); - let currentOrderBy: GraphOrderBy | undefined; - let groupByFieldMetadataId: string | undefined; - let groupBySubFieldName: string | null | undefined; - - if (configuration.__typename === 'PieChartConfiguration') { - currentOrderBy = configuration.orderBy ?? undefined; - groupByFieldMetadataId = configuration.groupByFieldMetadataId; - groupBySubFieldName = configuration.groupBySubFieldName; - } else { - const barOrLineChartConfiguration = configuration as - | BarChartConfiguration - | LineChartConfiguration; - currentOrderBy = - barOrLineChartConfiguration.primaryAxisOrderBy ?? undefined; - groupByFieldMetadataId = - barOrLineChartConfiguration.primaryAxisGroupByFieldMetadataId; - groupBySubFieldName = - barOrLineChartConfiguration.primaryAxisGroupBySubFieldName; - } - return ( { @@ -47,6 +48,76 @@ describe('fillDateGapsInBarChartData', () => { expect(result.data).toEqual([]); expect(result.wasTruncated).toBe(false); }); + + it('returns data in descending order when orderBy is FIELD_DESC', () => { + const data = [ + { + groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], + count: 5, + }, + { + groupByDimensionValues: ['2024-01-03T00:00:00.000Z'], + count: 3, + }, + ]; + + const result = fillDateGapsInBarChartData({ + data, + keys: ['count'], + dateGranularity: ObjectRecordGroupByDateGranularity.DAY, + orderBy: GraphOrderBy.FIELD_DESC, + }); + + expect(result.data).toHaveLength(3); + expect(result.data[0]).toEqual({ + groupByDimensionValues: ['2024-01-03T00:00:00.000Z'], + count: 3, + }); + expect(result.data[1]).toEqual({ + groupByDimensionValues: ['2024-01-02T00:00:00.000Z'], + count: 0, + }); + expect(result.data[2]).toEqual({ + groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], + count: 5, + }); + expect(result.wasTruncated).toBe(false); + }); + + it('returns data in ascending order when orderBy is FIELD_ASC', () => { + const data = [ + { + groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], + count: 5, + }, + { + groupByDimensionValues: ['2024-01-03T00:00:00.000Z'], + count: 3, + }, + ]; + + const result = fillDateGapsInBarChartData({ + data, + keys: ['count'], + dateGranularity: ObjectRecordGroupByDateGranularity.DAY, + orderBy: GraphOrderBy.FIELD_ASC, + }); + + expect(result.data).toHaveLength(3); + expect(result.data[0]).toEqual({ + groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], + count: 5, + }); + expect(result.data[1]).toEqual({ + groupByDimensionValues: ['2024-01-02T00:00:00.000Z'], + count: 0, + }); + expect(result.data[2]).toEqual({ + groupByDimensionValues: ['2024-01-03T00:00:00.000Z'], + count: 3, + }); + expect(result.wasTruncated).toBe(false); + }); }); describe('two-dimensional data', () => { @@ -92,5 +163,58 @@ describe('fillDateGapsInBarChartData', () => { }); expect(result.wasTruncated).toBe(false); }); + + it('fills gaps in descending order when orderBy is FIELD_DESC', () => { + const data = [ + { + groupByDimensionValues: ['2024-01-01T00:00:00.000Z', 'A'], + count: 5, + }, + { + groupByDimensionValues: ['2024-01-03T00:00:00.000Z', 'A'], + count: 3, + }, + { + groupByDimensionValues: ['2024-01-01T00:00:00.000Z', 'B'], + count: 2, + }, + ]; + + const result = fillDateGapsInBarChartData({ + data, + keys: ['count'], + dateGranularity: ObjectRecordGroupByDateGranularity.DAY, + hasSecondDimension: true, + orderBy: GraphOrderBy.FIELD_DESC, + }); + + expect(result.data).toHaveLength(6); + + // First date group should be Jan 3 (descending) + expect(result.data[0].groupByDimensionValues[0]).toBe( + '2024-01-03T00:00:00.000Z', + ); + expect(result.data[1].groupByDimensionValues[0]).toBe( + '2024-01-03T00:00:00.000Z', + ); + + // Middle date group should be Jan 2 + expect(result.data[2].groupByDimensionValues[0]).toBe( + '2024-01-02T00:00:00.000Z', + ); + expect(result.data[3].groupByDimensionValues[0]).toBe( + '2024-01-02T00:00:00.000Z', + ); + + // Last date group should be Jan 1 + expect(result.data[4].groupByDimensionValues[0]).toBe( + '2024-01-01T00:00:00.000Z', + ); + expect(result.data[5].groupByDimensionValues[0]).toBe( + '2024-01-01T00:00:00.000Z', + ); + + expect(result.wasTruncated).toBe(false); + }); }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData.ts index eaa3713357..d77489b133 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData.ts @@ -4,12 +4,14 @@ import { fillDateGapsInTwoDimensionalBarChartData } from '@/page-layout/widgets/ import { type SupportedDateGranularity } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; +import { type GraphOrderBy } from '~/generated/graphql'; type FillDateGapsParams = { data: GroupByRawResult[]; keys: string[]; dateGranularity: ObjectRecordGroupByDateGranularity; hasSecondDimension?: boolean; + orderBy?: GraphOrderBy | null; }; export const fillDateGapsInBarChartData = ({ @@ -17,6 +19,7 @@ export const fillDateGapsInBarChartData = ({ keys, dateGranularity, hasSecondDimension = false, + orderBy, }: FillDateGapsParams): { data: GroupByRawResult[]; wasTruncated: boolean } => { if (data.length === 0) { return { data, wasTruncated: false }; @@ -31,6 +34,7 @@ export const fillDateGapsInBarChartData = ({ data, keys, dateGranularity: dateGranularity as SupportedDateGranularity, + orderBy, }); } @@ -38,5 +42,6 @@ export const fillDateGapsInBarChartData = ({ data, keys, dateGranularity: dateGranularity as SupportedDateGranularity, + orderBy, }); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData.ts index 653ffb71a0..751130fe90 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData.ts @@ -6,17 +6,20 @@ import { } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { isDefined } from 'twenty-shared/utils'; +import { type GraphOrderBy } from '~/generated/graphql'; type OneDimensionalFillParams = { data: GroupByRawResult[]; keys: string[]; dateGranularity: SupportedDateGranularity; + orderBy?: GraphOrderBy | null; }; export const fillDateGapsInOneDimensionalBarChartData = ({ data, keys, dateGranularity, + orderBy, }: OneDimensionalFillParams): FillDateGapsResult => { const existingDateGroupsMap = new Map(); const parsedDates: Date[] = []; @@ -42,10 +45,11 @@ export const fillDateGapsInOneDimensionalBarChartData = ({ return { data, wasTruncated: false }; } - const { dates: allDates, wasTruncated } = getDateGroupsFromData( + const { dates: allDates, wasTruncated } = getDateGroupsFromData({ parsedDates, dateGranularity, - ); + orderBy, + }); const filledData = allDates.map((date) => { const key = date.toISOString(); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData.ts index 3d72ca1045..3fbebbdc8e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData.ts @@ -9,17 +9,20 @@ import { } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { isDefined } from 'twenty-shared/utils'; +import { type GraphOrderBy } from '~/generated/graphql'; type TwoDimensionalFillParams = { data: GroupByRawResult[]; keys: string[]; dateGranularity: SupportedDateGranularity; + orderBy?: GraphOrderBy | null; }; export const fillDateGapsInTwoDimensionalBarChartData = ({ data, keys, dateGranularity, + orderBy, }: TwoDimensionalFillParams): FillDateGapsResult => { const existingDateGroupsMap = new Map(); const parsedDates: Date[] = []; @@ -52,10 +55,11 @@ export const fillDateGapsInTwoDimensionalBarChartData = ({ return { data, wasTruncated: false }; } - const { dates: allDates, wasTruncated } = getDateGroupsFromData( + const { dates: allDates, wasTruncated } = getDateGroupsFromData({ parsedDates, dateGranularity, - ); + orderBy, + }); const filledData = allDates.flatMap((date) => Array.from(uniqueSecondDimensionValues).map((secondDimensionValue) => { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData.ts index ac2c6efd8c..e6b50bbeb7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData.ts @@ -1,5 +1,6 @@ import { generateDateGroupsInRange } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/generateDateGroupsInRange'; import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; +import { GraphOrderBy } from '~/generated/graphql'; export type SupportedDateGranularity = | ObjectRecordGroupByDateGranularity.DAY @@ -8,10 +9,17 @@ export type SupportedDateGranularity = | ObjectRecordGroupByDateGranularity.YEAR | ObjectRecordGroupByDateGranularity.WEEK; -export const getDateGroupsFromData = ( - parsedDates: Date[], - dateGranularity: SupportedDateGranularity, -): { dates: Date[]; wasTruncated: boolean } => { +type GetDateGroupsFromDataParams = { + parsedDates: Date[]; + dateGranularity: SupportedDateGranularity; + orderBy?: GraphOrderBy | null; +}; + +export const getDateGroupsFromData = ({ + parsedDates, + dateGranularity, + orderBy, +}: GetDateGroupsFromDataParams): { dates: Date[]; wasTruncated: boolean } => { const timestamps = parsedDates.map((date) => date.getTime()); const minDate = new Date(Math.min(...timestamps)); const maxDate = new Date(Math.max(...timestamps)); @@ -22,5 +30,10 @@ export const getDateGroupsFromData = ( granularity: dateGranularity, }); - return { dates: result.dates, wasTruncated: result.wasTruncated }; + const dates = + orderBy === GraphOrderBy.FIELD_DESC + ? result.dates.toReversed() + : result.dates; + + return { dates, wasTruncated: result.wasTruncated }; }; 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 ff927f36cd..a3e1a270d8 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 @@ -226,6 +226,7 @@ export const transformGroupByDataToBarChartData = ({ dateGranularity: primaryAxisDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, hasSecondDimension: isDefined(groupByFieldY), + orderBy: configuration.primaryAxisOrderBy, }) : { data: filteredResults, wasTruncated: false };