c531cfe0a4
## 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>
176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
|
import { GRAPH_DEFAULT_ORDER_BY } from '@/page-layout/widgets/graph/constants/GraphDefaultOrderBy';
|
|
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
|
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
|
import {
|
|
type AggregateOrderByWithGroupByField,
|
|
type ObjectRecordOrderByForCompositeField,
|
|
type ObjectRecordOrderByForRelationField,
|
|
type ObjectRecordOrderByForScalarField,
|
|
type ObjectRecordOrderByWithGroupByDateField,
|
|
} from 'twenty-shared/types';
|
|
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
|
import {
|
|
type BarChartConfiguration,
|
|
type LineChartConfiguration,
|
|
} from '~/generated/graphql';
|
|
import {
|
|
buildGroupByFieldObject,
|
|
type GroupByFieldObject,
|
|
} from './buildGroupByFieldObject';
|
|
|
|
export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
|
objectMetadataItem,
|
|
objectMetadataItems,
|
|
chartConfiguration,
|
|
aggregateOperation,
|
|
limit,
|
|
firstDayOfTheWeek,
|
|
userTimeZone,
|
|
}: {
|
|
objectMetadataItem: ObjectMetadataItem;
|
|
objectMetadataItems: ObjectMetadataItem[];
|
|
chartConfiguration: BarChartConfiguration | LineChartConfiguration;
|
|
aggregateOperation?: string;
|
|
limit?: number;
|
|
firstDayOfTheWeek?: number;
|
|
userTimeZone?: string;
|
|
}) => {
|
|
const groupByFieldXId = chartConfiguration.primaryAxisGroupByFieldMetadataId;
|
|
|
|
const groupByFieldYId =
|
|
chartConfiguration.secondaryAxisGroupByFieldMetadataId;
|
|
|
|
const groupBySubFieldNameX =
|
|
chartConfiguration.primaryAxisGroupBySubFieldName ?? undefined;
|
|
|
|
const groupBySubFieldNameY =
|
|
chartConfiguration.secondaryAxisGroupBySubFieldName ?? undefined;
|
|
|
|
const groupByFieldX = objectMetadataItem.fields.find(
|
|
(field) => field.id === groupByFieldXId,
|
|
);
|
|
|
|
const groupByFieldY = isDefined(groupByFieldYId)
|
|
? objectMetadataItem.fields.find((field) => field.id === groupByFieldYId)
|
|
: undefined;
|
|
|
|
if (!isDefined(groupByFieldX) || !isDefined(groupByFieldXId)) {
|
|
throw new Error(
|
|
`Field with id ${groupByFieldXId} not found in object metadata`,
|
|
);
|
|
}
|
|
|
|
const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type);
|
|
|
|
const isFieldXNestedDate = isRelationNestedFieldDateKind({
|
|
relationField: groupByFieldX,
|
|
relationNestedFieldName: groupBySubFieldNameX,
|
|
objectMetadataItems,
|
|
});
|
|
|
|
const shouldApplyDateGranularityX = isFieldXDate || isFieldXNestedDate;
|
|
|
|
const groupBy: Array<GroupByFieldObject> = [];
|
|
|
|
groupBy.push(
|
|
buildGroupByFieldObject({
|
|
field: groupByFieldX,
|
|
subFieldName: groupBySubFieldNameX,
|
|
dateGranularity: shouldApplyDateGranularityX
|
|
? (chartConfiguration.primaryAxisDateGranularity ??
|
|
GRAPH_DEFAULT_DATE_GRANULARITY)
|
|
: undefined,
|
|
firstDayOfTheWeek,
|
|
isNestedDateField: isFieldXNestedDate,
|
|
timeZone: userTimeZone,
|
|
}),
|
|
);
|
|
|
|
if (isDefined(groupByFieldY)) {
|
|
const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type);
|
|
|
|
const isFieldYNestedDate = isRelationNestedFieldDateKind({
|
|
relationField: groupByFieldY,
|
|
relationNestedFieldName: groupBySubFieldNameY,
|
|
objectMetadataItems,
|
|
});
|
|
|
|
const shouldApplyDateGranularityY = isFieldYDate || isFieldYNestedDate;
|
|
|
|
groupBy.push(
|
|
buildGroupByFieldObject({
|
|
field: groupByFieldY,
|
|
subFieldName: groupBySubFieldNameY,
|
|
dateGranularity: shouldApplyDateGranularityY
|
|
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
|
GRAPH_DEFAULT_DATE_GRANULARITY)
|
|
: undefined,
|
|
firstDayOfTheWeek,
|
|
isNestedDateField: isFieldYNestedDate,
|
|
timeZone: userTimeZone,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const orderBy: Array<
|
|
| AggregateOrderByWithGroupByField
|
|
| ObjectRecordOrderByForScalarField
|
|
| ObjectRecordOrderByWithGroupByDateField
|
|
| ObjectRecordOrderByForCompositeField
|
|
| ObjectRecordOrderByForRelationField
|
|
> = [];
|
|
|
|
const primaryAxisOrderBy = getGroupByOrderBy({
|
|
graphOrderBy:
|
|
chartConfiguration.primaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
|
groupByField: groupByFieldX,
|
|
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
|
|
aggregateOperation,
|
|
dateGranularity: shouldApplyDateGranularityX
|
|
? (chartConfiguration.primaryAxisDateGranularity ??
|
|
GRAPH_DEFAULT_DATE_GRANULARITY)
|
|
: undefined,
|
|
});
|
|
|
|
if (isDefined(primaryAxisOrderBy)) {
|
|
orderBy.push(primaryAxisOrderBy);
|
|
}
|
|
|
|
if (isDefined(groupByFieldY)) {
|
|
const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type);
|
|
|
|
const isFieldYNestedDateForOrderBy = isRelationNestedFieldDateKind({
|
|
relationField: groupByFieldY,
|
|
relationNestedFieldName: groupBySubFieldNameY,
|
|
objectMetadataItems,
|
|
});
|
|
|
|
const shouldApplyDateGranularityYForOrderBy =
|
|
isFieldYDateForOrderBy || isFieldYNestedDateForOrderBy;
|
|
|
|
const secondaryAxisOrderBy = getGroupByOrderBy({
|
|
graphOrderBy:
|
|
chartConfiguration.secondaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
|
groupByField: groupByFieldY,
|
|
groupBySubFieldName: chartConfiguration.secondaryAxisGroupBySubFieldName,
|
|
aggregateOperation,
|
|
dateGranularity: shouldApplyDateGranularityYForOrderBy
|
|
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
|
GRAPH_DEFAULT_DATE_GRANULARITY)
|
|
: undefined,
|
|
});
|
|
|
|
if (isDefined(secondaryAxisOrderBy)) {
|
|
orderBy.push(secondaryAxisOrderBy);
|
|
}
|
|
}
|
|
|
|
return {
|
|
groupBy,
|
|
orderBy,
|
|
...(isDefined(limit) && { limit }),
|
|
};
|
|
};
|