[DASHBOARDS] Manual and position-based sorting for chart widgets (#16794)

## Description

SELECT fields have a defined option order that users expect to see
reflected in charts.
This PR allows sorting by that position and also enables custom manual
ordering.

## Video QA

### Reordering on primary axis


https://github.com/user-attachments/assets/994f515e-19cb-4a5e-b745-e8c77e92ae0b


### Reordering on secondary axis


https://github.com/user-attachments/assets/444c16f2-1920-4dc4-8b42-312d520ab43b

Note: The colors in the graph will match the colors of the select
options, but this will be done in another PR

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduces new sort modes and UI for chart groupings, with full FE/BE
support and updated GraphQL schema.
> 
> - Extend `GraphOrderBy` with `FIELD_POSITION_ASC/DESC` and `MANUAL`;
add corresponding fields in configs: `primaryAxisManualSortOrder`,
`secondaryAxisManualSortOrder`, and `manualSortOrder` (pie)
> - New UI: dropdown options filtered by field type, icons, and a
draggable submenu (`ChartManualSortSubMenuContent`) to reorder select
options; integrates with widget edit flow
> - Sorting logic added/refactored: `sortChartData`,
`sortByManualOrder`, `sortBySelectOptionPosition`,
`sortLineChartSeries`, plus updates to bar/line/pie transformers to
honor new modes and manual orders
> - Default behaviors: select fields default to `FIELD_POSITION_ASC`;
query variable builders skip `orderBy` when using manual/position sorts
> - Update GraphQL generated types/fragments/queries and backend
DTOs/schemas to persist new fields; add tests for sorting utilities and
snapshots; add sorting icons in `twenty-ui`
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
78c9b56c0f1f2d45f7f8b270bb59ca599a005abe. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2026-01-06 11:52:54 +01:00
committed by GitHub
parent 221a6262f0
commit c531cfe0a4
89 changed files with 3592 additions and 683 deletions
@@ -0,0 +1,21 @@
import { FieldMetadataType } from '@/types';
import { isFieldMetadataNumericKind } from '../isFieldMetadataNumericKind';
describe('isFieldMetadataNumericKind', () => {
it.each([
FieldMetadataType.NUMBER,
FieldMetadataType.NUMERIC,
FieldMetadataType.CURRENCY,
FieldMetadataType.RATING,
FieldMetadataType.POSITION,
])('should return true for %s', (type) => {
expect(isFieldMetadataNumericKind(type)).toBe(true);
});
it.each([FieldMetadataType.TEXT, FieldMetadataType.SELECT])(
'should return false for %s',
(type) => {
expect(isFieldMetadataNumericKind(type)).toBe(false);
},
);
});
@@ -0,0 +1,18 @@
import { FieldMetadataType } from '@/types';
import { isFieldMetadataSelectKind } from '../isFieldMetadataSelectKind';
describe('isFieldMetadataSelectKind', () => {
it.each([FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT])(
'should return true for %s',
(type) => {
expect(isFieldMetadataSelectKind(type)).toBe(true);
},
);
it.each([FieldMetadataType.TEXT, FieldMetadataType.NUMBER])(
'should return false for %s',
(type) => {
expect(isFieldMetadataSelectKind(type)).toBe(false);
},
);
});
@@ -0,0 +1,19 @@
import { FieldMetadataType } from '@/types';
import { isFieldMetadataTextKind } from '../isFieldMetadataTextKind';
describe('isFieldMetadataTextKind', () => {
it.each([
FieldMetadataType.TEXT,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.RICH_TEXT_V2,
])('should return true for %s', (type) => {
expect(isFieldMetadataTextKind(type)).toBe(true);
});
it.each([FieldMetadataType.NUMBER, FieldMetadataType.SELECT])(
'should return false for %s',
(type) => {
expect(isFieldMetadataTextKind(type)).toBe(false);
},
);
});
@@ -1 +1,5 @@
export * from './isFieldMetadataDateKind';
export * from './isFieldMetadataNumericKind';
export * from './isFieldMetadataSelectKind';
export * from './isFieldMetadataTextKind';
@@ -0,0 +1,15 @@
import { FieldMetadataType } from '@/types';
const NUMBER_FIELD_TYPES: FieldMetadataType[] = [
FieldMetadataType.NUMBER,
FieldMetadataType.NUMERIC,
FieldMetadataType.CURRENCY,
FieldMetadataType.RATING,
FieldMetadataType.POSITION,
];
export const isFieldMetadataNumericKind = (
fieldMetadataType: FieldMetadataType,
): boolean => {
return NUMBER_FIELD_TYPES.includes(fieldMetadataType);
};
@@ -0,0 +1,10 @@
import { FieldMetadataType } from '@/types';
export const isFieldMetadataSelectKind = (
fieldMetadataType: FieldMetadataType,
): boolean => {
return (
fieldMetadataType === FieldMetadataType.SELECT ||
fieldMetadataType === FieldMetadataType.MULTI_SELECT
);
};
@@ -0,0 +1,13 @@
import { FieldMetadataType } from '@/types';
const TEXT_FIELD_TYPES: FieldMetadataType[] = [
FieldMetadataType.TEXT,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.RICH_TEXT_V2,
];
export const isFieldMetadataTextKind = (
fieldMetadataType: FieldMetadataType,
): boolean => {
return TEXT_FIELD_TYPES.includes(fieldMetadataType);
};