From fa87603fd83b6ad6519a9734e584761afbcb1e57 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:58:53 +0530 Subject: [PATCH] [Dashboards] Relation fields groupby (#16093) --- .../page-layout/components/ChartSettings.tsx | 11 +- ...oupByFieldSelectionDropdownContentBase.tsx | 90 +++++++-- ...GroupByFieldSelectionRelationFieldView.tsx | 174 ++++++++++++++++++ .../hooks/useChartSettingsValues.ts | 2 + .../hooks/useGraphGroupBySortOptionLabels.ts | 1 + .../hooks/useGraphXSortOptionLabels.ts | 1 + .../utils/getFieldLabelWithSubField.ts | 41 +++-- .../utils/getRelationFieldLabel.ts | 52 ++++++ .../utils/shouldHideChartSetting.ts | 5 + ...ctOptionsDropdownCalendarFieldsContent.tsx | 8 +- .../transformGroupByDataToBarChartData.ts | 7 +- .../components/GraphWidgetPieChart.tsx | 6 +- .../__tests__/buildGroupByFieldObject.test.ts | 29 ++- .../graph/utils/buildFilterFromChartBucket.ts | 9 + .../graph/utils/buildGroupByFieldObject.ts | 37 +++- ...ariablesFromBarOrLineChartConfiguration.ts | 45 +++-- ...QueryVariablesFromPieChartConfiguration.ts | 25 ++- .../widgets/graph/utils/getFieldOrderBy.ts | 60 ++++++ .../widgets/graph/utils/getGroupByOrderBy.ts | 62 ++----- .../graph/utils/getRelationFieldOrderBy.ts | 37 ++++ .../graph/utils/mapOrderByToDirection.ts | 20 ++ .../hooks/useGetAvailableFieldsForCalendar.ts | 11 +- 22 files changed, 597 insertions(+), 136 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getRelationFieldLabel.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/mapOrderByToDirection.ts diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx index 54484f98ad..141af1dc44 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -25,12 +25,9 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; +import { isFieldMetadataDateKind } from 'twenty-shared/utils'; -import { - FieldMetadataType, - GraphType, - type PageLayoutWidget, -} from '~/generated/graphql'; +import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; const StyledCommandMenuContainer = styled.div` display: flex; @@ -136,9 +133,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { (field) => field.id === primaryAxisFieldMetadataId, ); - const isPrimaryAxisDate = - primaryAxisField?.type === FieldMetadataType.DATE || - primaryAxisField?.type === FieldMetadataType.DATE_TIME; + const isPrimaryAxisDate = isFieldMetadataDateKind(primaryAxisField?.type); const primaryAxisDateGranularity = configuration.__typename === 'BarChartConfiguration' || diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx index f6aa974cc6..bdf2ef6ab7 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx @@ -1,4 +1,5 @@ import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView'; +import { ChartGroupByFieldSelectionRelationFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView'; 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'; @@ -23,6 +24,7 @@ import { useMemo, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { MenuItemSelect } from 'twenty-ui/navigation'; +import { RelationType } from '~/generated/graphql'; import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; type ChartGroupByFieldSelectionDropdownContentBaseProps< @@ -43,6 +45,9 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < const [selectedCompositeField, setSelectedCompositeField] = useState(null); + const [selectedRelationField, setSelectedRelationField] = + useState(null); + const { objectMetadataItems } = useObjectMetadataItems(); const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); @@ -77,8 +82,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < items: sourceObjectMetadataItem?.fields || [], searchQuery, getSearchableValues: (item) => [item.label, item.name], - // TODO: remove the relation filter once group by is supported for relation fields - }).filter((field) => !isFieldRelation(field) && !field.isSystem), + }).filter((field) => { + if (field.isSystem === true) { + return false; + } + if (isFieldRelation(field)) { + return field.relation?.type === RelationType.MANY_TO_ONE; + } + return true; + }), [sourceObjectMetadataItem?.fields, searchQuery], ); @@ -94,20 +106,26 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < } const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => { + if (isFieldRelation(fieldMetadataItem)) { + setSelectedRelationField(fieldMetadataItem); + return; + } + if (isCompositeFieldType(fieldMetadataItem.type)) { setSelectedCompositeField(fieldMetadataItem); - } else { - updateCurrentWidgetConfig({ - configToUpdate: buildChartGroupByFieldConfigUpdate({ - configuration, - fieldMetadataIdKey, - subFieldNameKey, - fieldId: fieldMetadataItem.id, - subFieldName: null, - }), - }); - closeDropdown(); + return; } + + updateCurrentWidgetConfig({ + configToUpdate: buildChartGroupByFieldConfigUpdate({ + configuration, + fieldMetadataIdKey, + subFieldNameKey, + fieldId: fieldMetadataItem.id, + subFieldName: null, + }), + }); + closeDropdown(); }; const handleSelectNone = () => { @@ -123,11 +141,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < closeDropdown(); }; - const handleBack = () => { + const handleBackFromComposite = () => { setSelectedCompositeField(null); }; - const handleSelectSubField = (subFieldName: string) => { + const handleBackFromRelation = () => { + setSelectedRelationField(null); + }; + + const handleSelectCompositeSubField = (subFieldName: string) => { if (!isDefined(selectedCompositeField)) { return; } @@ -144,13 +166,41 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < closeDropdown(); }; + const handleSelectRelationSubField = (subFieldName: string) => { + if (!isDefined(selectedRelationField)) { + return; + } + + updateCurrentWidgetConfig({ + configToUpdate: buildChartGroupByFieldConfigUpdate({ + configuration, + fieldMetadataIdKey, + subFieldNameKey, + fieldId: selectedRelationField.id, + subFieldName, + }), + }); + closeDropdown(); + }; + + if (isDefined(selectedRelationField)) { + return ( + + ); + } + if (isDefined(selectedCompositeField)) { return ( ); } @@ -195,11 +245,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < text={fieldMetadataItem.label} selected={ !isCompositeFieldType(fieldMetadataItem.type) && + !isFieldRelation(fieldMetadataItem) && currentGroupByFieldMetadataId === fieldMetadataItem.id } focused={selectedItemId === fieldMetadataItem.id} LeftIcon={getIcon(fieldMetadataItem.icon)} - hasSubMenu={isCompositeFieldType(fieldMetadataItem.type)} + hasSubMenu={ + isCompositeFieldType(fieldMetadataItem.type) || + isFieldRelation(fieldMetadataItem) + } onClick={() => { handleSelectField(fieldMetadataItem); }} diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx new file mode 100644 index 0000000000..de78b7378f --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView.tsx @@ -0,0 +1,174 @@ +import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +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 { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { t } from '@lingui/core/macro'; +import { useMemo, useState } from 'react'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; +import { IconChevronLeft, useIcons } from 'twenty-ui/display'; +import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation'; +import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; + +type ChartGroupByFieldSelectionRelationFieldViewProps = { + relationField: FieldMetadataItem; + currentSubFieldName: string | undefined; + onBack: () => void; + onSelectSubField: (subFieldName: string) => void; +}; + +export const ChartGroupByFieldSelectionRelationFieldView = ({ + relationField, + currentSubFieldName, + onBack, + onSelectSubField, +}: ChartGroupByFieldSelectionRelationFieldViewProps) => { + const { getIcon } = useIcons(); + + const [searchQuery, setSearchQuery] = useState(''); + + const [selectedCompositeField, setSelectedCompositeField] = + useState(null); + + const dropdownId = useAvailableComponentInstanceIdOrThrow( + DropdownComponentInstanceContext, + ); + + const selectedItemId = useRecoilComponentValue( + selectedItemIdComponentState, + dropdownId, + ); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const targetObjectNameSingular = + relationField.relation?.targetObjectMetadata?.nameSingular; + + const targetObjectMetadataItem = useMemo( + () => + objectMetadataItems.find( + (item) => item.nameSingular === targetObjectNameSingular, + ), + [objectMetadataItems, targetObjectNameSingular], + ); + + const availableFields = useMemo(() => { + if (!isDefined(targetObjectMetadataItem)) { + return []; + } + + return filterBySearchQuery({ + items: targetObjectMetadataItem.fields.filter( + (field) => + !field.isSystem && + !isFieldRelation(field) && + // TODO: Backend doesn't fully support date fields for relation fields yet so we hide them for now. https://github.com/twentyhq/core-team-issues/issues/1935 + !isFieldMetadataDateKind(field.type), + ), + searchQuery, + getSearchableValues: (field) => [field.label, field.name], + }); + }, [targetObjectMetadataItem, searchQuery]); + + const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => { + if (isCompositeFieldType(fieldMetadataItem.type)) { + setSelectedCompositeField(fieldMetadataItem); + } else { + onSelectSubField(fieldMetadataItem.name); + } + }; + + const handleSelectCompositeSubField = (compositeSubFieldName: string) => { + if (!isDefined(selectedCompositeField)) { + return; + } + onSelectSubField(`${selectedCompositeField.name}.${compositeSubFieldName}`); + }; + + const handleBackFromComposite = () => { + setSelectedCompositeField(null); + }; + + const [currentNestedFieldName, currentNestedSubFieldName] = + currentSubFieldName?.split('.') ?? []; + + if (isDefined(selectedCompositeField)) { + return ( + + ); + } + + return ( + <> + + } + > + {relationField.label} + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + {availableFields.length === 0 ? ( + + ) : ( + field.id)} + > + {availableFields.map((fieldMetadataItem) => ( + { + handleSelectField(fieldMetadataItem); + }} + > + { + handleSelectField(fieldMetadataItem); + }} + /> + + ))} + + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts index d1b6b07809..aa760e8de6 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts @@ -84,6 +84,7 @@ export const useChartSettingsValues = ({ ? getFieldLabelWithSubField({ field: groupByFieldX, subFieldName: groupBySubFieldNameX, + objectMetadataItems, }) : undefined; @@ -181,6 +182,7 @@ export const useChartSettingsValues = ({ ? getFieldLabelWithSubField({ field: pieChartGroupByField, subFieldName: finalGroupBySubFieldNameY, + objectMetadataItems, }) : undefined; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts index 7005c192f7..75bfe6916c 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts @@ -32,6 +32,7 @@ export const useGraphGroupBySortOptionLabels = ({ const fieldLabel = getFieldLabelWithSubField({ field, subFieldName: groupBySubFieldName, + objectMetadataItems, }); switch (graphOrderBy) { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts index b0db2c50f9..deae60edeb 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts @@ -39,6 +39,7 @@ export const useGraphXSortOptionLabels = ({ const fieldLabel = getFieldLabelWithSubField({ field: groupByField, subFieldName: groupBySubFieldNameX, + objectMetadataItems, }); const aggregateField = objectMetadataItem?.fields.find( diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts index 9166c7bde4..dddea9a219 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts @@ -1,5 +1,9 @@ +import { getRelationFieldLabel } from '@/command-menu/pages/page-layout/utils/getRelationFieldLabel'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { type CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; import { t } from '@lingui/core/macro'; import { type CompositeFieldSubFieldName } from 'twenty-shared/types'; @@ -8,24 +12,31 @@ import { isDefined } from 'twenty-shared/utils'; export const getFieldLabelWithSubField = ({ field, subFieldName, + objectMetadataItems, }: { field: FieldMetadataItem | undefined; - subFieldName?: CompositeFieldSubFieldName; + subFieldName?: CompositeFieldSubFieldName | string; + objectMetadataItems?: ObjectMetadataItem[]; }): string => { - const subFieldNameLabel = - isDefined(subFieldName) && isDefined(field) - ? getCompositeSubFieldLabel( - field.type as CompositeFieldType, - subFieldName, - ) - : undefined; + if (!isDefined(field?.label)) { + return t`Field`; + } - const fieldLabel = - isDefined(subFieldNameLabel) && isDefined(field?.label) - ? `${field.label} ${subFieldNameLabel}` - : isDefined(field?.label) - ? field.label - : t`Field`; + if (!isDefined(subFieldName)) { + return field.label; + } - return fieldLabel; + if (isFieldRelation(field)) { + return getRelationFieldLabel(field, subFieldName, objectMetadataItems); + } + + if (isCompositeFieldType(field.type)) { + const subFieldLabel = getCompositeSubFieldLabel( + field.type as CompositeFieldType, + subFieldName as CompositeFieldSubFieldName, + ); + return subFieldLabel ? `${field.label} ${subFieldLabel}` : field.label; + } + + return field.label; }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getRelationFieldLabel.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getRelationFieldLabel.ts new file mode 100644 index 0000000000..04a639f9b8 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getRelationFieldLabel.ts @@ -0,0 +1,52 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { type CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { type CompositeFieldSubFieldName } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const getRelationFieldLabel = ( + field: FieldMetadataItem, + subFieldName: string, + objectMetadataItems: ObjectMetadataItem[] | undefined, +): string => { + if (!isDefined(objectMetadataItems)) { + return field.label; + } + + const targetObjectNameSingular = + field.relation?.targetObjectMetadata?.nameSingular; + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === targetObjectNameSingular, + ); + + if (!isDefined(targetObjectMetadataItem)) { + return field.label; + } + + const [nestedFieldName, nestedSubFieldName] = subFieldName.split('.'); + const nestedField = targetObjectMetadataItem.fields.find( + (f) => f.name === nestedFieldName, + ); + + if (!isDefined(nestedField)) { + return field.label; + } + + if ( + !isDefined(nestedSubFieldName) || + !isCompositeFieldType(nestedField.type) + ) { + return `${field.label} ${nestedField.label}`; + } + + const compositeSubFieldLabel = getCompositeSubFieldLabel( + nestedField.type as CompositeFieldType, + nestedSubFieldName as CompositeFieldSubFieldName, + ); + + return compositeSubFieldLabel + ? `${field.label} ${nestedField.label} ${compositeSubFieldLabel}` + : `${field.label} ${nestedField.label}`; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts index 6a57609a8b..27b46baabd 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts @@ -2,6 +2,7 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { isNonEmptyString } from '@sniptt/guards'; import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; @@ -21,6 +22,10 @@ const shouldHideDateGranularityBasedOnFieldType = ( return true; } + if (isFieldRelation(field)) { + return true; + } + return !isFieldMetadataDateKind(field.type); }; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx index 4f8dad4b52..167117840b 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx @@ -13,9 +13,9 @@ import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useG import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { useSetRecoilState } from 'recoil'; +import { isFieldMetadataDateKind } from 'twenty-shared/utils'; import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/display'; import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; export const ObjectOptionsDropdownCalendarFieldsContent = () => { const { t } = useLingui(); @@ -32,10 +32,8 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => { const setRecordIndexCalendarFieldMetadataId = useSetRecoilState( recordIndexCalendarFieldMetadataIdState, ); - const availableFieldsForCalendar = objectMetadataItem.fields.filter( - (field) => - field.type === FieldMetadataType.DATE || - field.type === FieldMetadataType.DATE_TIME, + const availableFieldsForCalendar = objectMetadataItem.fields.filter((field) => + isFieldMetadataDateKind(field.type), ); const calendarFieldMetadata = currentView?.calendarFieldMetadataId diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts index 016dd99cfe..21f8044f39 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts @@ -14,11 +14,10 @@ import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDim import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults'; import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey'; import { type BarDatum } from '@nivo/bar'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { GraphType } from '~/generated-metadata/graphql'; import { AxisNameDisplay, - FieldMetadataType, type BarChartConfiguration, } from '~/generated/graphql'; @@ -150,9 +149,7 @@ export const transformGroupByDataToBarChartData = ({ const showDataLabels = configuration.displayDataLabel ?? false; const showLegend = configuration.displayLegend ?? true; - const isDateField = - groupByFieldX.type === FieldMetadataType.DATE || - groupByFieldX.type === FieldMetadataType.DATE_TIME; + const isDateField = isFieldMetadataDateKind(groupByFieldX.type); const omitNullValues = configuration.omitNullValues ?? false; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx index 993ce75500..190bbc878d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx @@ -105,10 +105,14 @@ export const GraphWidgetPieChart = ({ const tooltipData = createTooltipData(datum); if (!isDefined(tooltipData)) return null; + const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick) + ? () => handleSliceClick(datum) + : undefined; + return ( handleSliceClick(datum)} + onGraphWidgetTooltipClick={handleTooltipClick} /> ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts index e252158827..89097fa65a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildGroupByFieldObject.test.ts @@ -4,7 +4,7 @@ import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { FieldMetadataType } from '~/generated-metadata/graphql'; describe('buildGroupByFieldObject', () => { - it('should return field with Id suffix for relation fields', () => { + it('should return field with Id suffix for relation fields without subFieldName', () => { const field = { name: 'company', type: FieldMetadataType.RELATION, @@ -15,6 +15,33 @@ describe('buildGroupByFieldObject', () => { expect(result).toEqual({ companyId: true }); }); + it('should return nested object for relation field with subFieldName', () => { + const field = { + name: 'company', + type: FieldMetadataType.RELATION, + } as any; + + const result = buildGroupByFieldObject({ field, subFieldName: 'name' }); + + expect(result).toEqual({ company: { name: true } }); + }); + + it('should return deeply nested object for relation with composite subfield', () => { + const field = { + name: 'company', + type: FieldMetadataType.RELATION, + } as any; + + const result = buildGroupByFieldObject({ + field, + subFieldName: 'address.addressCity', + }); + + expect(result).toEqual({ + company: { address: { addressCity: true } }, + }); + }); + it('should return nested object for composite fields with subfield', () => { const field = { name: 'name', diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildFilterFromChartBucket.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildFilterFromChartBucket.ts index bb624fbfb1..5ca0dc5d74 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildFilterFromChartBucket.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildFilterFromChartBucket.ts @@ -1,4 +1,6 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { buildDateFilterForDayGranularity } from '@/page-layout/widgets/graph/utils/buildDateFilterForDayGranularity'; import { buildDateRangeFiltersForGranularity } from '@/page-layout/widgets/graph/utils/buildDateRangeFiltersForGranularity'; @@ -69,6 +71,13 @@ export const buildFilterFromChartBucket = ({ ]; } + if ( + isFieldRelation(fieldMetadataItem) || + isFieldMorphRelation(fieldMetadataItem) + ) { + return []; + } + if (isFieldMetadataDateKind(fieldMetadataItem.type)) { const parsedBucketDate = new Date(String(bucketRawValue)); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts index 9bb493c05e..c2c0f1aab1 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildGroupByFieldObject.ts @@ -8,9 +8,12 @@ import { type FirstDayOfTheWeek, ObjectRecordGroupByDateGranularity, } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +export type GroupByFieldObject = Record< + string, + boolean | Record> +>; export const buildGroupByFieldObject = ({ field, @@ -22,15 +25,35 @@ export const buildGroupByFieldObject = ({ subFieldName?: string | null; dateGranularity?: ObjectRecordGroupByDateGranularity; firstDayOfTheWeek?: number | null; -}): Record> => { +}): GroupByFieldObject => { const isRelation = isFieldRelation(field) || isFieldMorphRelation(field); const isComposite = isCompositeFieldType(field.type); - const isDateField = - field.type === FieldMetadataType.DATE || - field.type === FieldMetadataType.DATE_TIME; + const isDateField = isFieldMetadataDateKind(field.type); if (isRelation) { - return { [`${field.name}Id`]: true }; + if (!isDefined(subFieldName)) { + return { [`${field.name}Id`]: true }; + } + + const parts = subFieldName.split('.'); + const nestedFieldName = parts[0]; + const nestedSubFieldName = parts[1]; + + if (isDefined(nestedSubFieldName)) { + return { + [field.name]: { + [nestedFieldName]: { + [nestedSubFieldName]: true, + }, + }, + }; + } + + return { + [field.name]: { + [nestedFieldName]: true, + }, + }; } if (isComposite) { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts index a9d901a7f5..a3e244a4db 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration.ts @@ -1,17 +1,22 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy'; import { type AggregateOrderByWithGroupByField, type ObjectRecordOrderByForCompositeField, + type ObjectRecordOrderByForRelationField, type ObjectRecordOrderByForScalarField, type ObjectRecordOrderByWithGroupByDateField, } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { type BarChartConfiguration, type LineChartConfiguration, } from '~/generated/graphql'; -import { buildGroupByFieldObject } from './buildGroupByFieldObject'; +import { + buildGroupByFieldObject, + type GroupByFieldObject, +} from './buildGroupByFieldObject'; export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ objectMetadataItem, @@ -51,27 +56,34 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ ); } - const groupBy: Array< - Record> - > = []; + const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type); + + const groupBy: Array = []; groupBy.push( buildGroupByFieldObject({ field: groupByFieldX, subFieldName: groupBySubFieldNameX, - dateGranularity: - chartConfiguration.primaryAxisDateGranularity ?? undefined, + dateGranularity: isFieldXDate + ? (chartConfiguration.primaryAxisDateGranularity ?? + GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, + firstDayOfTheWeek, }), ); if (isDefined(groupByFieldY)) { + const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type); + groupBy.push( buildGroupByFieldObject({ field: groupByFieldY, subFieldName: groupBySubFieldNameY, - dateGranularity: - chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined, + dateGranularity: isFieldYDate + ? (chartConfiguration.secondaryAxisGroupByDateGranularity ?? + GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, firstDayOfTheWeek, }), ); @@ -82,6 +94,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ | ObjectRecordOrderByForScalarField | ObjectRecordOrderByWithGroupByDateField | ObjectRecordOrderByForCompositeField + | ObjectRecordOrderByForRelationField > = []; if (isDefined(chartConfiguration.primaryAxisOrderBy)) { @@ -91,8 +104,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ groupByField: groupByFieldX, groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName, aggregateOperation, - dateGranularity: - chartConfiguration.primaryAxisDateGranularity ?? undefined, + dateGranularity: isFieldXDate + ? (chartConfiguration.primaryAxisDateGranularity ?? + GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, }), ); } @@ -100,6 +115,8 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ isDefined(groupByFieldY) && isDefined(chartConfiguration.secondaryAxisOrderBy) ) { + const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type); + orderBy.push( getGroupByOrderBy({ graphOrderBy: chartConfiguration.secondaryAxisOrderBy, @@ -107,8 +124,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({ groupBySubFieldName: chartConfiguration.secondaryAxisGroupBySubFieldName, aggregateOperation, - dateGranularity: - chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined, + dateGranularity: isFieldYDateForOrderBy + ? (chartConfiguration.secondaryAxisGroupByDateGranularity ?? + GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, }), ); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts index 24848ad402..bf85dd7ea7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromPieChartConfiguration.ts @@ -1,14 +1,19 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy'; import { type AggregateOrderByWithGroupByField, type ObjectRecordOrderByForCompositeField, + type ObjectRecordOrderByForRelationField, type ObjectRecordOrderByForScalarField, type ObjectRecordOrderByWithGroupByDateField, } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { type PieChartConfiguration } from '~/generated/graphql'; -import { buildGroupByFieldObject } from './buildGroupByFieldObject'; +import { + buildGroupByFieldObject, + type GroupByFieldObject, +} from './buildGroupByFieldObject'; export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ objectMetadataItem, @@ -38,13 +43,16 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ ); } - const groupBy: Array< - Record> - > = [ + const isFieldDate = isFieldMetadataDateKind(groupByField.type); + + const groupBy: Array = [ buildGroupByFieldObject({ field: groupByField, subFieldName: groupBySubFieldName, - dateGranularity, + + dateGranularity: isFieldDate + ? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, firstDayOfTheWeek, }), ]; @@ -54,6 +62,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ | ObjectRecordOrderByForScalarField | ObjectRecordOrderByWithGroupByDateField | ObjectRecordOrderByForCompositeField + | ObjectRecordOrderByForRelationField > = []; if (isDefined(chartConfiguration.orderBy)) { @@ -63,7 +72,9 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({ groupByField, groupBySubFieldName, aggregateOperation, - dateGranularity, + dateGranularity: isFieldDate + ? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY) + : undefined, }), ); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts new file mode 100644 index 0000000000..c6bf3181a0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getFieldOrderBy.ts @@ -0,0 +1,60 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; +import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy'; +import { + type ObjectRecordGroupByDateGranularity, + type ObjectRecordOrderByForCompositeField, + type ObjectRecordOrderByForRelationField, + type ObjectRecordOrderByForScalarField, + type ObjectRecordOrderByWithGroupByDateField, + type OrderByDirection, +} from 'twenty-shared/types'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; + +export const getFieldOrderBy = ( + groupByField: FieldMetadataItem, + groupBySubFieldName: string | null | undefined, + dateGranularity: ObjectRecordGroupByDateGranularity | undefined, + direction: OrderByDirection, +): + | ObjectRecordOrderByForScalarField + | ObjectRecordOrderByWithGroupByDateField + | ObjectRecordOrderByForCompositeField + | ObjectRecordOrderByForRelationField => { + if (isCompositeFieldType(groupByField.type)) { + if (!isDefined(groupBySubFieldName)) { + throw new Error( + `Group by subFieldName is required for composite fields (field: ${groupByField.name})`, + ); + } + return { + [groupByField.name]: { + [groupBySubFieldName]: direction, + }, + }; + } + + if (isFieldMetadataDateKind(groupByField.type)) { + return { + [groupByField.name]: { + orderBy: direction, + granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, + }, + }; + } + + if (isFieldRelation(groupByField) || isFieldMorphRelation(groupByField)) { + return getRelationFieldOrderBy( + groupByField, + groupBySubFieldName, + direction, + ); + } + + return { + [groupByField.name]: direction, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts index 50e43c1c8c..15ddfea37c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getGroupByOrderBy.ts @@ -1,36 +1,17 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; -import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; +import { getFieldOrderBy } from '@/page-layout/widgets/graph/utils/getFieldOrderBy'; +import { mapOrderByToDirection } from '@/page-layout/widgets/graph/utils/mapOrderByToDirection'; import { - OrderByDirection, type AggregateOrderByWithGroupByField, type ObjectRecordGroupByDateGranularity, type ObjectRecordOrderByForCompositeField, + type ObjectRecordOrderByForRelationField, type ObjectRecordOrderByForScalarField, type ObjectRecordOrderByWithGroupByDateField, } from 'twenty-shared/types'; -import { - assertUnreachable, - isDefined, - isFieldMetadataDateKind, -} from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { GraphOrderBy } from '~/generated/graphql'; -const mapOrderByToDirection = (orderByEnum: GraphOrderBy): OrderByDirection => { - switch (orderByEnum) { - case GraphOrderBy.FIELD_ASC: - return OrderByDirection.AscNullsLast; - case GraphOrderBy.FIELD_DESC: - return OrderByDirection.DescNullsLast; - case GraphOrderBy.VALUE_ASC: - return OrderByDirection.AscNullsLast; - case GraphOrderBy.VALUE_DESC: - return OrderByDirection.DescNullsLast; - default: - assertUnreachable(orderByEnum); - } -}; - export const getGroupByOrderBy = ({ graphOrderBy, groupByField, @@ -47,34 +28,17 @@ export const getGroupByOrderBy = ({ | AggregateOrderByWithGroupByField | ObjectRecordOrderByForScalarField | ObjectRecordOrderByWithGroupByDateField - | ObjectRecordOrderByForCompositeField => { + | ObjectRecordOrderByForCompositeField + | ObjectRecordOrderByForRelationField => { switch (graphOrderBy) { case GraphOrderBy.FIELD_ASC: - case GraphOrderBy.FIELD_DESC: { - if (isCompositeFieldType(groupByField.type)) { - if (!isDefined(groupBySubFieldName)) { - throw new Error( - `Group by subFieldName is required for composite fields (field: ${groupByField.name})`, - ); - } - return { - [groupByField.name]: { - [groupBySubFieldName]: mapOrderByToDirection(graphOrderBy), - }, - }; - } else if (isFieldMetadataDateKind(groupByField.type)) { - return { - [groupByField.name]: { - orderBy: mapOrderByToDirection(graphOrderBy), - granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY, - }, - }; - } else { - return { - [groupByField.name]: mapOrderByToDirection(graphOrderBy), - }; - } - } + case GraphOrderBy.FIELD_DESC: + return getFieldOrderBy( + groupByField, + groupBySubFieldName, + dateGranularity, + mapOrderByToDirection(graphOrderBy), + ); case GraphOrderBy.VALUE_ASC: case GraphOrderBy.VALUE_DESC: { if (!isDefined(aggregateOperation)) { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts new file mode 100644 index 0000000000..214cb9d527 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getRelationFieldOrderBy.ts @@ -0,0 +1,37 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { + type ObjectRecordOrderByForRelationField, + type ObjectRecordOrderByForScalarField, + type OrderByDirection, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const getRelationFieldOrderBy = ( + groupByField: FieldMetadataItem, + groupBySubFieldName: string | null | undefined, + direction: OrderByDirection, +): ObjectRecordOrderByForScalarField | ObjectRecordOrderByForRelationField => { + if (!isDefined(groupBySubFieldName)) { + return { + [`${groupByField.name}Id`]: direction, + }; + } + + const [nestedFieldName, nestedSubFieldName] = groupBySubFieldName.split('.'); + + if (!isDefined(nestedSubFieldName)) { + return { + [groupByField.name]: { + [nestedFieldName]: direction, + }, + }; + } + + return { + [groupByField.name]: { + [nestedFieldName]: { + [nestedSubFieldName]: direction, + }, + }, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/mapOrderByToDirection.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/mapOrderByToDirection.ts new file mode 100644 index 0000000000..a4a9553044 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/mapOrderByToDirection.ts @@ -0,0 +1,20 @@ +import { OrderByDirection } from 'twenty-shared/types'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { GraphOrderBy } from '~/generated/graphql'; + +export const mapOrderByToDirection = ( + orderByEnum: GraphOrderBy, +): OrderByDirection => { + switch (orderByEnum) { + case GraphOrderBy.FIELD_ASC: + return OrderByDirection.AscNullsLast; + case GraphOrderBy.FIELD_DESC: + return OrderByDirection.DescNullsLast; + case GraphOrderBy.VALUE_ASC: + return OrderByDirection.AscNullsLast; + case GraphOrderBy.VALUE_DESC: + return OrderByDirection.DescNullsLast; + default: + assertUnreachable(orderByEnum); + } +}; diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForCalendar.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForCalendar.ts index b2b3fdb1b1..992b45589b 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForCalendar.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForCalendar.ts @@ -6,9 +6,8 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState'; -import { SettingsPath } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { FieldMetadataType, SettingsPath } from 'twenty-shared/types'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; export const useGetAvailableFieldsForCalendar = () => { @@ -26,10 +25,8 @@ export const useGetAvailableFieldsForCalendar = () => { ); const availableFieldsForCalendar = - objectMetadataItem?.readableFields.filter( - (field) => - field.type === FieldMetadataType.DATE || - field.type === FieldMetadataType.DATE_TIME, + objectMetadataItem?.readableFields.filter((field) => + isFieldMetadataDateKind(field.type), ) ?? []; const navigate = useNavigateSettings();