[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,123 @@
import { type DropResult } from '@hello-pangea/dnd';
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 { getManualSortOrderFromConfig } from '@/command-menu/pages/page-layout/utils/getManualSortOrderFromConfig';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { sortOptionsForManualOrder } from '@/page-layout/widgets/graph/utils/sortOptionsForManualOrder';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/components';
import { IconChevronLeft } from 'twenty-ui/display';
import { MenuItemDraggable } from 'twenty-ui/navigation';
import { type WidgetConfiguration } from '~/generated/graphql';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
type ChartManualSortSubMenuContentProps = {
fieldMetadataItem: FieldMetadataItem;
axis: 'primary' | 'secondary';
onBack: () => void;
};
export const ChartManualSortSubMenuContent = ({
fieldMetadataItem,
axis,
onBack,
}: ChartManualSortSubMenuContentProps) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
// TODO: Remove this cast when FieldsConfiguration and FieldConfiguration are in the backend
const configuration = widgetInEditMode?.configuration as WidgetConfiguration;
const options = fieldMetadataItem.options ?? [];
const currentManualSortOrder = getManualSortOrderFromConfig(
configuration,
axis,
);
const sortedOptions = sortOptionsForManualOrder(
options,
currentManualSortOrder,
);
const handleDragEnd = (result: DropResult) => {
if (!isDefined(result.destination)) {
return;
}
const reorderedOptions = moveArrayItem(sortedOptions, {
fromIndex: result.source.index,
toIndex: result.destination.index,
});
const newManualSortOrder = reorderedOptions.map((option) => option.value);
const configKey = isWidgetConfigurationOfType(
configuration,
'PieChartConfiguration',
)
? 'manualSortOrder'
: axis === 'primary'
? 'primaryAxisManualSortOrder'
: 'secondaryAxisManualSortOrder';
updateCurrentWidgetConfig({
configToUpdate: { [configKey]: newManualSortOrder },
});
};
return (
<>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={onBack}
Icon={IconChevronLeft}
/>
}
>
{t`Reorder options`}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<DraggableList
onDragEnd={handleDragEnd}
draggableItems={
<>
{sortedOptions.map((option, index) => (
<DraggableItem
key={option.value}
draggableId={option.value}
index={index}
isDragDisabled={sortedOptions.length === 1}
itemComponent={
<MenuItemDraggable
showGrip
isDragDisabled={sortedOptions.length === 1}
text={
<Tag
preventShrink
color={option.color}
text={option.label}
/>
}
/>
}
/>
))}
</>
}
/>
</DropdownMenuItemsContainer>
</>
);
};
@@ -1,9 +1,16 @@
import { useState } from 'react';
import { ChartManualSortSubMenuContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
import { AGGREGATE_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/AggregateSortByOptions';
import { useGraphGroupBySortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
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 { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
@@ -13,13 +20,20 @@ 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 { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type GraphOrderBy } from '~/generated/graphql';
import {
GraphOrderBy,
type GraphOrderBy as GraphOrderByType,
} from '~/generated/graphql';
export const ChartSortByGroupByFieldDropdownContent = () => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const configuration = widgetInEditMode?.configuration;
@@ -34,6 +48,7 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
@@ -43,33 +58,76 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
dropdownId,
);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === widgetInEditMode.objectMetadataId,
);
const { closeDropdown } = useCloseDropdown();
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
updateCurrentWidgetConfig({
configToUpdate: { secondaryAxisOrderBy: orderBy },
});
closeDropdown();
};
const secondaryAxisField = objectMetadataItem?.fields.find(
(field) => field.id === configuration.secondaryAxisGroupByFieldMetadataId,
);
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
objectMetadataId: widgetInEditMode.objectMetadataId,
});
if (!isDefined(secondaryAxisField)) {
return null;
}
const handleSelectSortOption = (orderBy: GraphOrderByType) => {
const configToUpdate: Record<string, unknown> = {
secondaryAxisOrderBy: orderBy,
};
if (orderBy === GraphOrderBy.MANUAL) {
const existingManualSortOrder =
configuration.secondaryAxisManualSortOrder;
if (!isDefined(existingManualSortOrder)) {
configToUpdate.secondaryAxisManualSortOrder = getDefaultManualSortOrder(
secondaryAxisField?.options,
);
}
updateCurrentWidgetConfig({ configToUpdate });
setIsSubMenuOpen(true);
return;
}
if (configuration.secondaryAxisOrderBy === GraphOrderBy.MANUAL) {
configToUpdate.secondaryAxisManualSortOrder = null;
}
updateCurrentWidgetConfig({ configToUpdate });
closeDropdown();
};
const availableOptions = filterSortOptionsByFieldType({
options: AGGREGATE_SORT_BY_OPTIONS,
fieldType: secondaryAxisField?.type,
});
if (isSubMenuOpen && isDefined(secondaryAxisField)) {
return (
<ChartManualSortSubMenuContent
fieldMetadataItem={secondaryAxisField}
axis="secondary"
onBack={() => setIsSubMenuOpen(false)}
/>
);
}
return (
<>
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={AGGREGATE_SORT_BY_OPTIONS.map(
(option) => option.value,
)}
>
{AGGREGATE_SORT_BY_OPTIONS.map((sortOption) => (
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={availableOptions.map((option) => option.value)}
>
{availableOptions.map((sortOption) => {
const isManualOption = sortOption.value === GraphOrderBy.MANUAL;
return (
<SelectableListItem
key={sortOption.value}
itemId={sortOption.value}
@@ -87,15 +145,22 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
configuration.secondaryAxisOrderBy === sortOption.value
}
focused={selectedItemId === sortOption.value}
LeftIcon={sortOption.icon}
LeftIcon={
sortOption.icon ??
getSortIconForFieldType({
fieldType: secondaryAxisField?.type,
orderBy: sortOption.value,
})
}
hasSubMenu={isManualOption}
onClick={() => {
handleSelectSortOption(sortOption.value);
}}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</>
);
})}
</SelectableList>
</DropdownMenuItemsContainer>
);
};
@@ -1,11 +1,16 @@
import { useState } from 'react';
import { ChartManualSortSubMenuContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
import { X_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/XSortByOptions';
import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels';
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 { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
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';
@@ -15,7 +20,7 @@ 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, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
import {
type BarChartConfiguration,
@@ -24,8 +29,23 @@ import {
} from '~/generated/graphql';
export const ChartSortBySelectionDropdownContent = () => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const configuration = widgetInEditMode?.configuration;
const isPieChart = isWidgetConfigurationOfType(
@@ -49,25 +69,10 @@ export const ChartSortBySelectionDropdownContent = () => {
throw new Error('No data source in chart');
}
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
objectMetadataId: widgetInEditMode.objectMetadataId,
});
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === widgetInEditMode.objectMetadataId,
);
@@ -96,44 +101,69 @@ export const ChartSortBySelectionDropdownContent = () => {
(field) => field.id === groupByFieldMetadataId,
);
const isPrimaryAxisDateField =
isFieldMetadataDateKind(primaryAxisField?.type) ||
(isDefined(primaryAxisField) &&
isRelationNestedFieldDateKind({
relationField: primaryAxisField,
relationNestedFieldName: groupBySubFieldName ?? undefined,
objectMetadataItems,
}));
if (!isDefined(primaryAxisField)) {
return null;
}
const existingManualSortOrder = isPieChart
? configuration.manualSortOrder
: (configuration as BarChartConfiguration | LineChartConfiguration)
.primaryAxisManualSortOrder;
const handleSelect = (orderBy: GraphOrderBy) => {
if (isPieChart) {
updateCurrentWidgetConfig({
configToUpdate: { orderBy },
});
} else {
updateCurrentWidgetConfig({
configToUpdate: { primaryAxisOrderBy: orderBy },
});
const configToUpdate: Record<string, unknown> = {};
if (orderBy === GraphOrderBy.MANUAL) {
const orderByKey = isPieChart ? 'orderBy' : 'primaryAxisOrderBy';
const manualSortOrderKey = isPieChart
? 'manualSortOrder'
: 'primaryAxisManualSortOrder';
configToUpdate[orderByKey] = orderBy;
if (!isDefined(existingManualSortOrder)) {
configToUpdate[manualSortOrderKey] = getDefaultManualSortOrder(
primaryAxisField?.options,
);
}
updateCurrentWidgetConfig({ configToUpdate });
setIsSubMenuOpen(true);
return;
}
if (currentOrderBy === GraphOrderBy.MANUAL) {
const manualSortOrderKey = isPieChart
? 'manualSortOrder'
: 'primaryAxisManualSortOrder';
configToUpdate[manualSortOrderKey] = null;
}
if (isPieChart) {
configToUpdate.orderBy = orderBy;
} else {
configToUpdate.primaryAxisOrderBy = orderBy;
}
updateCurrentWidgetConfig({ configToUpdate });
closeDropdown();
};
const availableOptions = X_SORT_BY_OPTIONS.filter((option) => {
const isValueSort =
option.value === GraphOrderBy.VALUE_ASC ||
option.value === GraphOrderBy.VALUE_DESC;
if (isLineChart) {
return !isValueSort;
}
if ((isBarChart || isPieChart) && isPrimaryAxisDateField) {
return !isValueSort;
}
return true;
const availableOptions = filterSortOptionsByFieldType({
options: X_SORT_BY_OPTIONS,
fieldType: primaryAxisField?.type,
});
if (isSubMenuOpen && isDefined(primaryAxisField)) {
return (
<ChartManualSortSubMenuContent
fieldMetadataItem={primaryAxisField}
axis={'primary'}
onBack={() => setIsSubMenuOpen(false)}
/>
);
}
return (
<DropdownMenuItemsContainer>
<SelectableList
@@ -141,35 +171,46 @@ export const ChartSortBySelectionDropdownContent = () => {
focusId={dropdownId}
selectableItemIdArray={availableOptions.map((option) => option.value)}
>
{availableOptions.map((sortOption) => (
<SelectableListItem
key={sortOption.value}
itemId={sortOption.value}
onEnter={() => {
handleSelect(sortOption.value);
}}
>
<MenuItemSelect
text={getXSortOptionLabel({
graphOrderBy: sortOption.value,
groupByFieldMetadataIdX: groupByFieldMetadataId ?? '',
groupBySubFieldNameX: groupBySubFieldName as
| CompositeFieldSubFieldName
| undefined,
aggregateFieldMetadataId:
configuration.aggregateFieldMetadataId ?? undefined,
aggregateOperation:
configuration.aggregateOperation ?? undefined,
})}
selected={currentOrderBy === sortOption.value}
focused={selectedItemId === sortOption.value}
LeftIcon={sortOption.icon}
onClick={() => {
{availableOptions.map((sortOption) => {
const isManualOption = sortOption.value === GraphOrderBy.MANUAL;
return (
<SelectableListItem
key={sortOption.value}
itemId={sortOption.value}
onEnter={() => {
handleSelect(sortOption.value);
}}
/>
</SelectableListItem>
))}
>
<MenuItemSelect
text={getXSortOptionLabel({
graphOrderBy: sortOption.value,
groupByFieldMetadataIdX: groupByFieldMetadataId ?? '',
groupBySubFieldNameX: groupBySubFieldName as
| CompositeFieldSubFieldName
| undefined,
aggregateFieldMetadataId:
configuration.aggregateFieldMetadataId ?? undefined,
aggregateOperation:
configuration.aggregateOperation ?? undefined,
})}
selected={currentOrderBy === sortOption.value}
focused={selectedItemId === sortOption.value}
LeftIcon={
sortOption.icon ??
getSortIconForFieldType({
fieldType: primaryAxisField?.type,
orderBy: sortOption.value,
})
}
hasSubMenu={isManualOption}
onClick={() => {
handleSelect(sortOption.value);
}}
/>
</SelectableListItem>
);
})}
</SelectableList>
</DropdownMenuItemsContainer>
);
@@ -1,13 +1,29 @@
import { IconArrowDown, IconArrowUp } from 'twenty-ui/display';
import {
IconHandMove,
IconSortAscending,
IconSortDescending,
} from 'twenty-ui/display';
import { GraphOrderBy } from '~/generated/graphql';
export const AGGREGATE_SORT_BY_OPTIONS = [
{
value: GraphOrderBy.FIELD_POSITION_ASC,
icon: IconSortAscending,
},
{
value: GraphOrderBy.FIELD_POSITION_DESC,
icon: IconSortDescending,
},
{
value: GraphOrderBy.FIELD_ASC,
icon: IconArrowUp,
icon: null,
},
{
value: GraphOrderBy.FIELD_DESC,
icon: IconArrowDown,
icon: null,
},
{
value: GraphOrderBy.MANUAL,
icon: IconHandMove,
},
];
@@ -1,19 +1,34 @@
import {
IconArrowDown,
IconArrowUp,
type IconComponent,
IconHandMove,
IconSortAscending,
IconSortDescending,
IconTrendingDown,
IconTrendingUp,
} from 'twenty-ui/display';
import { GraphOrderBy } from '~/generated/graphql';
export const X_SORT_BY_OPTIONS = [
type XSortByOption = {
value: GraphOrderBy;
icon: IconComponent | null;
};
export const X_SORT_BY_OPTIONS: XSortByOption[] = [
{
value: GraphOrderBy.FIELD_POSITION_ASC,
icon: IconSortAscending,
},
{
value: GraphOrderBy.FIELD_POSITION_DESC,
icon: IconSortDescending,
},
{
value: GraphOrderBy.FIELD_ASC,
icon: IconArrowUp,
icon: null,
},
{
value: GraphOrderBy.FIELD_DESC,
icon: IconArrowDown,
icon: null,
},
{
value: GraphOrderBy.VALUE_ASC,
@@ -23,4 +38,8 @@ export const X_SORT_BY_OPTIONS = [
value: GraphOrderBy.VALUE_DESC,
icon: IconTrendingDown,
},
{
value: GraphOrderBy.MANUAL,
icon: IconHandMove,
},
];
@@ -1,8 +1,10 @@
import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/utils/getFieldLabelWithSubField';
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
export const useGraphGroupBySortOptionLabels = ({
@@ -35,13 +37,24 @@ export const useGraphGroupBySortOptionLabels = ({
objectMetadataItems,
});
const groupBySortLabelSuffix = getSortLabelSuffixForFieldType({
fieldType: field?.type,
orderBy: graphOrderBy,
});
switch (graphOrderBy) {
case GraphOrderBy.FIELD_ASC:
return `${fieldLabel} ${t`Ascending`}`;
case GraphOrderBy.FIELD_DESC:
return `${fieldLabel} ${t`Descending`}`;
default:
case GraphOrderBy.FIELD_POSITION_ASC:
case GraphOrderBy.FIELD_POSITION_DESC:
return `${fieldLabel} ${groupBySortLabelSuffix}`;
case GraphOrderBy.VALUE_ASC:
case GraphOrderBy.VALUE_DESC:
return '';
case GraphOrderBy.MANUAL:
return t`Manual`;
default:
assertUnreachable(graphOrderBy);
}
};
@@ -1,4 +1,5 @@
import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/utils/getFieldLabelWithSubField';
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
@@ -53,15 +54,27 @@ export const useGraphXSortOptionLabels = ({
? getAggregateOperationLabel(aggregateOperation)
: t`Value`;
const groupBySortLabelSuffix = getSortLabelSuffixForFieldType({
fieldType: groupByField?.type,
orderBy: graphOrderBy,
});
const aggregateSortLabelSuffix = getSortLabelSuffixForFieldType({
fieldType: aggregateField?.type,
orderBy: graphOrderBy,
});
switch (graphOrderBy) {
case GraphOrderBy.FIELD_ASC:
return `${fieldLabel} ${t`Ascending`}`;
case GraphOrderBy.FIELD_DESC:
return `${fieldLabel} ${t`Descending`}`;
case GraphOrderBy.FIELD_POSITION_ASC:
case GraphOrderBy.FIELD_POSITION_DESC:
return `${fieldLabel} ${groupBySortLabelSuffix}`;
case GraphOrderBy.VALUE_ASC:
return `${valueLabel} ${t`Ascending`}`;
case GraphOrderBy.VALUE_DESC:
return `${valueLabel} ${t`Descending`}`;
return `${valueLabel} ${aggregateSortLabelSuffix}`;
case GraphOrderBy.MANUAL:
return t`Manual`;
default:
assertUnreachable(graphOrderBy);
}
@@ -1,12 +1,12 @@
import { type TypedBarChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedBarChartConfiguration';
import { type TypedPieChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedPieChartConfiguration';
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import {
BarChartGroupMode,
GraphOrderBy,
WidgetConfigurationType,
} from '~/generated/graphql';
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
describe('buildChartGroupByFieldConfigUpdate', () => {
it('sets default orderBy and dateGranularity for primary axis', () => {
@@ -79,4 +79,22 @@ describe('buildChartGroupByFieldConfigUpdate', () => {
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
});
});
it('resets orderBy to default when field changes', () => {
const result = buildChartGroupByFieldConfigUpdate({
configuration: {
__typename: 'BarChartConfiguration',
configurationType: WidgetConfigurationType.BAR_CHART,
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
} as TypedBarChartConfiguration,
fieldMetadataIdKey: 'primaryAxisGroupByFieldMetadataId',
subFieldNameKey: 'primaryAxisGroupBySubFieldName',
fieldId: 'new-field-id',
subFieldName: null,
});
expect(result).toMatchObject({
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
});
});
});
@@ -0,0 +1,65 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
import { GraphOrderBy } from '~/generated/graphql';
describe('filterSortOptionsByFieldType', () => {
const allOptions = [
{ value: GraphOrderBy.FIELD_POSITION_ASC },
{ value: GraphOrderBy.FIELD_POSITION_DESC },
{ value: GraphOrderBy.FIELD_ASC },
{ value: GraphOrderBy.FIELD_DESC },
{ value: GraphOrderBy.VALUE_ASC },
{ value: GraphOrderBy.VALUE_DESC },
{ value: GraphOrderBy.MANUAL },
];
describe('select field', () => {
it('should include all options for select field', () => {
const result = filterSortOptionsByFieldType({
options: allOptions,
fieldType: FieldMetadataType.SELECT,
});
expect(result).toHaveLength(7);
});
});
describe('date field', () => {
it('should exclude value sorts for date select field', () => {
const result = filterSortOptionsByFieldType({
options: allOptions,
fieldType: FieldMetadataType.DATE,
});
expect(result).not.toContainEqual({ value: GraphOrderBy.VALUE_ASC });
expect(result).not.toContainEqual({ value: GraphOrderBy.VALUE_DESC });
expect(result).not.toContainEqual({ value: GraphOrderBy.MANUAL });
expect(result).not.toContainEqual({
value: GraphOrderBy.FIELD_POSITION_ASC,
});
expect(result).not.toContainEqual({
value: GraphOrderBy.FIELD_POSITION_DESC,
});
expect(result).toHaveLength(2);
});
});
describe('non-select, non-date field', () => {
it('should exclude manual and position sorts', () => {
const result = filterSortOptionsByFieldType({
options: allOptions,
fieldType: FieldMetadataType.TEXT,
});
expect(result).not.toContainEqual({ value: GraphOrderBy.MANUAL });
expect(result).not.toContainEqual({
value: GraphOrderBy.FIELD_POSITION_ASC,
});
expect(result).not.toContainEqual({
value: GraphOrderBy.FIELD_POSITION_DESC,
});
expect(result).toHaveLength(4);
});
});
});
@@ -0,0 +1,36 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getChartDefaultOrderByForFieldType } from '@/command-menu/pages/page-layout/utils/getChartDefaultOrderByForFieldType';
import { GraphOrderBy } from '~/generated/graphql';
describe('getChartDefaultOrderByForFieldType', () => {
it('should return FIELD_POSITION_ASC for SELECT field type', () => {
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.SELECT)).toBe(
GraphOrderBy.FIELD_POSITION_ASC,
);
});
it('should return FIELD_ASC for TEXT field type', () => {
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.TEXT)).toBe(
GraphOrderBy.FIELD_ASC,
);
});
it('should return FIELD_ASC for NUMBER field type', () => {
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.NUMBER)).toBe(
GraphOrderBy.FIELD_ASC,
);
});
it('should return FIELD_ASC for DATE field type', () => {
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.DATE)).toBe(
GraphOrderBy.FIELD_ASC,
);
});
it('should return FIELD_ASC for MULTI_SELECT field type', () => {
expect(
getChartDefaultOrderByForFieldType(FieldMetadataType.MULTI_SELECT),
).toBe(GraphOrderBy.FIELD_ASC);
});
});
@@ -0,0 +1,29 @@
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
describe('getDefaultManualSortOrder', () => {
it('should return empty array for null options', () => {
expect(getDefaultManualSortOrder(null)).toEqual([]);
});
it('should return empty array for undefined options', () => {
expect(getDefaultManualSortOrder(undefined)).toEqual([]);
});
it('should return empty array for empty options', () => {
expect(getDefaultManualSortOrder([])).toEqual([]);
});
it('should return values sorted by position', () => {
const options = [
{ value: 'third', position: 2 },
{ value: 'first', position: 0 },
{ value: 'second', position: 1 },
];
expect(getDefaultManualSortOrder(options)).toEqual([
'first',
'second',
'third',
]);
});
});
@@ -0,0 +1,112 @@
import { getManualSortOrderFromConfig } from '@/command-menu/pages/page-layout/utils/getManualSortOrderFromConfig';
import { expect } from '@storybook/test';
import {
WidgetConfigurationType,
type BarChartConfiguration,
type LineChartConfiguration,
type PieChartConfiguration,
} from '~/generated/graphql';
describe('getManualSortOrderFromConfig', () => {
describe('pie chart configuration', () => {
it('should return manualSortOrder for pie axis', () => {
const config = {
__typename: 'PieChartConfiguration' as const,
manualSortOrder: ['a', 'b', 'c'],
} as PieChartConfiguration;
expect(getManualSortOrderFromConfig(config)).toEqual(['a', 'b', 'c']);
});
it('should return undefined for null manualSortOrder', () => {
const config = {
__typename: 'PieChartConfiguration' as const,
manualSortOrder: null,
} as PieChartConfiguration;
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
});
it('should return undefined when manualSortOrder is not in config', () => {
const config = {
__typename: 'PieChartConfiguration',
configurationType: WidgetConfigurationType.PIE_CHART,
} as PieChartConfiguration;
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
});
it('should return undefined for wrong typename', () => {
const config = {
__typename: 'BarChartConfiguration',
configurationType: WidgetConfigurationType.BAR_CHART,
manualSortOrder: ['a', 'b', 'c'],
} as unknown as BarChartConfiguration;
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
});
});
describe('bar chart configuration', () => {
it('should return primaryAxisManualSortOrder for primary axis', () => {
const config = {
__typename: 'BarChartConfiguration' as const,
primaryAxisManualSortOrder: ['x', 'y', 'z'],
} as BarChartConfiguration;
expect(getManualSortOrderFromConfig(config, 'primary')).toEqual([
'x',
'y',
'z',
]);
});
it('should return secondaryAxisManualSortOrder for secondary axis', () => {
const config = {
__typename: 'BarChartConfiguration' as const,
secondaryAxisManualSortOrder: ['1', '2', '3'],
} as BarChartConfiguration;
expect(getManualSortOrderFromConfig(config, 'secondary')).toEqual([
'1',
'2',
'3',
]);
});
it('should return undefined for null primaryAxisManualSortOrder', () => {
const config = {
__typename: 'BarChartConfiguration' as const,
primaryAxisManualSortOrder: null,
} as BarChartConfiguration;
expect(getManualSortOrderFromConfig(config, 'primary')).toBeUndefined();
});
});
describe('line chart configuration', () => {
it('should return primaryAxisManualSortOrder for primary axis', () => {
const config = {
__typename: 'LineChartConfiguration' as const,
primaryAxisManualSortOrder: ['a', 'b'],
} as LineChartConfiguration;
expect(getManualSortOrderFromConfig(config, 'primary')).toEqual([
'a',
'b',
]);
});
it('should return secondaryAxisManualSortOrder for secondary axis', () => {
const config = {
__typename: 'LineChartConfiguration' as const,
secondaryAxisManualSortOrder: ['c', 'd'],
} as LineChartConfiguration;
expect(getManualSortOrderFromConfig(config, 'secondary')).toEqual([
'c',
'd',
]);
});
});
});
@@ -0,0 +1,208 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
IconSortAscending,
IconSortAscendingLetters,
IconSortAscendingNumbers,
IconSortDescending,
IconSortDescendingLetters,
IconSortDescendingNumbers,
} from 'twenty-ui/display';
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
import { GraphOrderBy } from '~/generated/graphql';
describe('getSortIconForFieldType', () => {
describe('position sort', () => {
it('should return IconSortAscending for FIELD_POSITION_ASC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.SELECT,
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
}),
).toBe(IconSortAscending);
});
it('should return IconSortDescending for FIELD_POSITION_DESC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.SELECT,
orderBy: GraphOrderBy.FIELD_POSITION_DESC,
}),
).toBe(IconSortDescending);
});
});
describe('text field types', () => {
it('should return IconSortAscendingLetters for TEXT field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.TEXT,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingLetters);
});
it('should return IconSortDescendingLetters for TEXT field descending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.TEXT,
orderBy: GraphOrderBy.FIELD_DESC,
}),
).toBe(IconSortDescendingLetters);
});
it('should return IconSortAscendingLetters for RICH_TEXT field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.RICH_TEXT,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingLetters);
});
it('should return IconSortAscendingLetters for RICH_TEXT_V2 field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.RICH_TEXT_V2,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingLetters);
});
});
describe('number field types', () => {
it('should return IconSortAscendingNumbers for NUMBER field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.NUMBER,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingNumbers);
});
it('should return IconSortDescendingNumbers for NUMBER field descending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.NUMBER,
orderBy: GraphOrderBy.FIELD_DESC,
}),
).toBe(IconSortDescendingNumbers);
});
it('should return IconSortAscendingNumbers for CURRENCY field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.CURRENCY,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingNumbers);
});
it('should return IconSortAscendingNumbers for RATING field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.RATING,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingNumbers);
});
});
describe('select field type', () => {
it('should return IconSortAscendingLetters for SELECT field with FIELD_ASC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.SELECT,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscendingLetters);
});
it('should return IconSortDescendingLetters for SELECT field with FIELD_DESC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.SELECT,
orderBy: GraphOrderBy.FIELD_DESC,
}),
).toBe(IconSortDescendingLetters);
});
});
describe('undefined field type', () => {
it('should return IconSortAscending for undefined field type ascending', () => {
expect(
getSortIconForFieldType({
fieldType: undefined,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscending);
});
it('should return IconSortDescending for undefined field type descending', () => {
expect(
getSortIconForFieldType({
fieldType: undefined,
orderBy: GraphOrderBy.FIELD_DESC,
}),
).toBe(IconSortDescending);
});
});
describe('value-based sort', () => {
it('should return IconSortAscending for VALUE_ASC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.NUMBER,
orderBy: GraphOrderBy.VALUE_ASC,
}),
).toBe(IconSortAscendingNumbers);
});
it('should return IconSortDescending for VALUE_DESC', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.NUMBER,
orderBy: GraphOrderBy.VALUE_DESC,
}),
).toBe(IconSortDescendingNumbers);
});
it('should return IconSortAscending for VALUE_ASC with undefined field type', () => {
expect(
getSortIconForFieldType({
fieldType: undefined,
orderBy: GraphOrderBy.VALUE_ASC,
}),
).toBe(IconSortAscending);
});
it('should return IconSortDescending for VALUE_DESC with undefined field type', () => {
expect(
getSortIconForFieldType({
fieldType: undefined,
orderBy: GraphOrderBy.VALUE_DESC,
}),
).toBe(IconSortDescending);
});
});
describe('default cases', () => {
it('should return IconSortAscending for DATE field ascending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.DATE,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe(IconSortAscending);
});
it('should return IconSortDescending for DATE field descending', () => {
expect(
getSortIconForFieldType({
fieldType: FieldMetadataType.DATE,
orderBy: GraphOrderBy.FIELD_DESC,
}),
).toBe(IconSortDescending);
});
});
});
@@ -0,0 +1,32 @@
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
import { FieldMetadataType } from 'twenty-shared/types';
import { GraphOrderBy } from '~/generated/graphql';
describe('getSortLabelSuffixForFieldType', () => {
it('returns alphabetical for TEXT field with FIELD_ASC', () => {
expect(
getSortLabelSuffixForFieldType({
fieldType: FieldMetadataType.TEXT,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe('alphabetical');
});
it('returns ascending for NUMBER field with FIELD_ASC', () => {
expect(
getSortLabelSuffixForFieldType({
fieldType: FieldMetadataType.NUMBER,
orderBy: GraphOrderBy.FIELD_ASC,
}),
).toBe('ascending');
});
it('returns position ascending for SELECT field with FIELD_POSITION_ASC', () => {
expect(
getSortLabelSuffixForFieldType({
fieldType: FieldMetadataType.SELECT,
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
}),
).toBe('position ascending');
});
});
@@ -1,4 +1,5 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { getChartDefaultOrderByForFieldType } from '@/command-menu/pages/page-layout/utils/getChartDefaultOrderByForFieldType';
import { isFieldOrRelationNestedFieldDateKind } from '@/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -51,10 +52,15 @@ export const buildChartGroupByFieldConfigUpdate = <
'PieChartConfiguration',
);
if (isPrimaryAxis) {
const existingOrderBy =
isBarChart || isLineChart ? configuration.primaryAxisOrderBy : null;
const fieldMetadataItem = objectMetadataItem?.fields?.find(
(field) => field.id === fieldId,
);
const defaultOrderBy = isDefined(fieldMetadataItem?.type)
? getChartDefaultOrderByForFieldType(fieldMetadataItem?.type)
: GraphOrderBy.FIELD_ASC;
if (isPrimaryAxis) {
const existingDateGranularity =
isBarChart || isLineChart
? configuration.primaryAxisDateGranularity
@@ -73,19 +79,9 @@ 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) ? newOrderBy : null,
primaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
primaryAxisDateGranularity: isDefined(fieldId)
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
: null,
@@ -94,32 +90,13 @@ export const buildChartGroupByFieldConfigUpdate = <
}
if (isPieChartGroupBy) {
const existingOrderBy = isPieChart ? configuration.orderBy : null;
const existingDateGranularity = isPieChart
? 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) ? newOrderBy : null,
orderBy: isDefined(fieldId) ? defaultOrderBy : null,
dateGranularity: isDefined(fieldId)
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
: null,
@@ -133,9 +110,7 @@ export const buildChartGroupByFieldConfigUpdate = <
if (isBarChart) {
return {
...baseConfig,
secondaryAxisOrderBy: isDefined(fieldId)
? (configuration.secondaryAxisOrderBy ?? GraphOrderBy.FIELD_ASC)
: null,
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
secondaryAxisGroupByDateGranularity: isDefined(fieldId)
? (configuration.secondaryAxisGroupByDateGranularity ??
ObjectRecordGroupByDateGranularity.DAY)
@@ -149,9 +124,7 @@ export const buildChartGroupByFieldConfigUpdate = <
if (isLineChart) {
return {
...baseConfig,
secondaryAxisOrderBy: isDefined(fieldId)
? (configuration.secondaryAxisOrderBy ?? GraphOrderBy.FIELD_ASC)
: null,
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
secondaryAxisGroupByDateGranularity: isDefined(fieldId)
? (configuration.secondaryAxisGroupByDateGranularity ??
ObjectRecordGroupByDateGranularity.DAY)
@@ -0,0 +1,50 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import {
isFieldMetadataDateKind,
isFieldMetadataSelectKind,
} from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/display';
import { GraphOrderBy } from '~/generated/graphql';
export type SortOption = {
value: GraphOrderBy;
icon?: IconComponent | null;
};
type FilterSortOptionsParams = {
options: SortOption[];
fieldType: FieldMetadataType;
};
export const filterSortOptionsByFieldType = ({
options,
fieldType,
}: FilterSortOptionsParams): SortOption[] => {
return options.filter((option) => {
const isValueSort =
option.value === GraphOrderBy.VALUE_ASC ||
option.value === GraphOrderBy.VALUE_DESC;
const isManualSort = option.value === GraphOrderBy.MANUAL;
const isPositionSort =
option.value === GraphOrderBy.FIELD_POSITION_ASC ||
option.value === GraphOrderBy.FIELD_POSITION_DESC;
const isSelectField = isFieldMetadataSelectKind(fieldType);
if (isManualSort && !isSelectField) {
return false;
}
if (isPositionSort && !isSelectField) {
return false;
}
if (isFieldMetadataDateKind(fieldType)) {
return !isValueSort;
}
return true;
});
};
@@ -0,0 +1,12 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { GraphOrderBy } from '~/generated/graphql';
export const getChartDefaultOrderByForFieldType = (
fieldType: FieldMetadataType,
): GraphOrderBy => {
const isSelectField = fieldType === FieldMetadataType.SELECT;
return isSelectField
? GraphOrderBy.FIELD_POSITION_ASC
: GraphOrderBy.FIELD_ASC;
};
@@ -0,0 +1,16 @@
type FieldOption = {
value: string;
position?: number | null;
};
export const getDefaultManualSortOrder = (
options: FieldOption[] | null | undefined,
): string[] => {
if (!options) return [];
const sortedByPosition = options.toSorted(
(a, b) => (a.position ?? 0) - (b.position ?? 0),
);
return sortedByPosition.map((option) => option.value);
};
@@ -0,0 +1,29 @@
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { isDefined } from 'twenty-shared/utils';
import { type WidgetConfiguration } from '~/generated/graphql';
export const getManualSortOrderFromConfig = (
configuration: WidgetConfiguration,
axis?: 'primary' | 'secondary',
): string[] | undefined => {
if (isWidgetConfigurationOfType(configuration, 'PieChartConfiguration')) {
return configuration.manualSortOrder ?? undefined;
}
if (!isDefined(axis)) {
return undefined;
}
if (
isWidgetConfigurationOfType(configuration, 'BarChartConfiguration') ||
isWidgetConfigurationOfType(configuration, 'LineChartConfiguration')
) {
if (axis === 'primary') {
return configuration.primaryAxisManualSortOrder ?? undefined;
}
return configuration.secondaryAxisManualSortOrder ?? undefined;
}
return undefined;
};
@@ -0,0 +1,57 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
isFieldMetadataNumericKind,
isFieldMetadataTextKind,
} from 'twenty-shared/utils';
import {
type IconComponent,
IconSortAscending,
IconSortAscendingLetters,
IconSortAscendingNumbers,
IconSortDescending,
IconSortDescendingLetters,
IconSortDescendingNumbers,
} from 'twenty-ui/display';
import { GraphOrderBy } from '~/generated/graphql';
export const getSortIconForFieldType = ({
fieldType,
orderBy,
}: {
fieldType: FieldMetadataType | undefined;
orderBy: GraphOrderBy;
}): IconComponent => {
const isAscending =
orderBy === GraphOrderBy.FIELD_ASC ||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
orderBy === GraphOrderBy.VALUE_ASC;
const isPositionSort =
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
orderBy === GraphOrderBy.FIELD_POSITION_DESC;
if (isPositionSort) {
return isAscending ? IconSortAscending : IconSortDescending;
}
if (!fieldType) {
return isAscending ? IconSortAscending : IconSortDescending;
}
if (isFieldMetadataTextKind(fieldType)) {
return isAscending ? IconSortAscendingLetters : IconSortDescendingLetters;
}
if (isFieldMetadataNumericKind(fieldType)) {
return isAscending ? IconSortAscendingNumbers : IconSortDescendingNumbers;
}
if (
fieldType === FieldMetadataType.SELECT &&
(orderBy === GraphOrderBy.FIELD_ASC || orderBy === GraphOrderBy.FIELD_DESC)
) {
return isAscending ? IconSortAscendingLetters : IconSortDescendingLetters;
}
return isAscending ? IconSortAscending : IconSortDescending;
};
@@ -0,0 +1,54 @@
import { t } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import {
isDefined,
isFieldMetadataNumericKind,
isFieldMetadataTextKind,
} from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
export const getSortLabelSuffixForFieldType = ({
fieldType,
orderBy,
}: {
fieldType: FieldMetadataType | undefined;
orderBy: GraphOrderBy;
}): string => {
const isAscending =
orderBy === GraphOrderBy.FIELD_ASC ||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
orderBy === GraphOrderBy.VALUE_ASC;
if (!isDefined(fieldType)) {
return isAscending ? t`ascending` : t`descending`;
}
if (isFieldMetadataTextKind(fieldType)) {
return isAscending ? t`alphabetical` : t`reverse alphabetical`;
}
if (isFieldMetadataNumericKind(fieldType)) {
return isAscending ? t`ascending` : t`descending`;
}
if (fieldType === FieldMetadataType.SELECT) {
if (
orderBy === GraphOrderBy.FIELD_ASC ||
orderBy === GraphOrderBy.FIELD_DESC
) {
return isAscending ? t`alphabetical` : t`reverse alphabetical`;
}
if (
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
orderBy === GraphOrderBy.FIELD_POSITION_DESC
) {
return isAscending ? t`position ascending` : t`position descending`;
}
return isAscending ? t`ascending` : t`descending`;
}
return isAscending ? t`ascending` : t`descending`;
};