[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
This commit is contained in:
Raphaël Bosi
2025-12-12 14:55:44 +01:00
committed by GitHub
parent 6acdde72ef
commit d30453f38f
8 changed files with 243 additions and 38 deletions
@@ -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 (
<DropdownMenuItemsContainer>
<SelectableList
@@ -63,11 +63,19 @@ export const buildChartGroupByFieldConfigUpdate = <
!isNewFieldDateType &&
(isBarChart || isLineChart);
const isCurrentOrderByValueBased =
existingOrderBy === GraphOrderBy.VALUE_ASC ||
existingOrderBy === GraphOrderBy.VALUE_DESC;
const shouldResetOrderBy = isNewFieldDateType && isCurrentOrderByValueBased;
const newOrderBy = shouldResetOrderBy
? GraphOrderBy.FIELD_ASC
: (existingOrderBy ?? GraphOrderBy.FIELD_ASC);
return {
...baseConfig,
primaryAxisOrderBy: isDefined(fieldId)
? (existingOrderBy ?? GraphOrderBy.FIELD_ASC)
: null,
primaryAxisOrderBy: isDefined(fieldId) ? newOrderBy : null,
primaryAxisDateGranularity: isDefined(fieldId)
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
: null,
@@ -82,11 +90,26 @@ export const buildChartGroupByFieldConfigUpdate = <
? configuration.dateGranularity
: null;
const isNewFieldDateType = isFieldOrRelationNestedFieldDateKind({
fieldId,
subFieldName,
objectMetadataItem,
objectMetadataItems,
});
const isCurrentOrderByValueBased =
existingOrderBy === GraphOrderBy.VALUE_ASC ||
existingOrderBy === GraphOrderBy.VALUE_DESC;
const shouldResetOrderBy = isNewFieldDateType && isCurrentOrderByValueBased;
const newOrderBy = shouldResetOrderBy
? GraphOrderBy.FIELD_ASC
: (existingOrderBy ?? GraphOrderBy.FIELD_ASC);
return {
...baseConfig,
orderBy: isDefined(fieldId)
? (existingOrderBy ?? GraphOrderBy.FIELD_ASC)
: null,
orderBy: isDefined(fieldId) ? newOrderBy : null,
dateGranularity: isDefined(fieldId)
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
: null,
@@ -1,4 +1,5 @@
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { GraphOrderBy } from '~/generated/graphql';
import { fillDateGapsInBarChartData } from '../fillDateGapsInBarChartData';
describe('fillDateGapsInBarChartData', () => {
@@ -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);
});
});
});
@@ -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,
});
};
@@ -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<string, GroupByRawResult>();
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();
@@ -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<string, GroupByRawResult>();
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) => {
@@ -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 };
};
@@ -226,6 +226,7 @@ export const transformGroupByDataToBarChartData = ({
dateGranularity:
primaryAxisDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
hasSecondDimension: isDefined(groupByFieldY),
orderBy: configuration.primaryAxisOrderBy,
})
: { data: filteredResults, wasTruncated: false };