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 };