[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:
+21
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+18
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+19
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user