From f414f4799b4e610e4014756ffb5b5d79242cd925 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:08:06 +0100 Subject: [PATCH] Enable switching field to group records by on existing kanban view (#17015) Fixes https://github.com/twentyhq/twenty/issues/16982 and https://github.com/twentyhq/private-issues/issues/403 Re-introducing the feature to allow to switch field to group records by on a kanban view https://github.com/user-attachments/assets/a552d3f2-7900-4771-b512-98cb99cdd8de --- .../ObjectOptionsDropdownLayoutContent.tsx | 10 +- ...jectOptionsDropdownRecordGroupsContent.tsx | 17 ++- .../useSearchRecordGroupField.test.tsx | 139 ++++++++++++++++-- .../hooks/useSearchRecordGroupField.ts | 1 + .../useSetViewTypeFromLayoutOptionsMenu.ts | 11 +- .../ViewPickerContentCreateMode.tsx | 15 +- .../components/ViewPickerContentEffect.tsx | 11 +- .../components/ViewPickerCreateButton.tsx | 8 +- .../components/ViewPickerEditButton.tsx | 8 +- .../useGetAvailableFieldsForCalendar.test.tsx | 119 +++++++++++++++ ...etAvailableFieldsToGroupRecordsBy.test.tsx | 102 +++++++++++++ ... useGetAvailableFieldsToGroupRecordsBy.ts} | 6 +- 12 files changed, 403 insertions(+), 44 deletions(-) create mode 100644 packages/twenty-front/src/modules/views/view-picker/hooks/__tests__/useGetAvailableFieldsForCalendar.test.tsx create mode 100644 packages/twenty-front/src/modules/views/view-picker/hooks/__tests__/useGetAvailableFieldsToGroupRecordsBy.test.tsx rename packages/twenty-front/src/modules/views/view-picker/hooks/{useGetAvailableFieldsForKanban.ts => useGetAvailableFieldsToGroupRecordsBy.ts} (94%) diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx index 275a37b397..aadc4c577a 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx @@ -20,7 +20,7 @@ import { type GraphQLView } from '@/views/types/GraphQLView'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; -import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useLingui } from '@lingui/react/macro'; import { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; @@ -75,8 +75,8 @@ export const ObjectOptionsDropdownLayoutContent = () => { : undefined; const { setAndPersistViewType } = useSetViewTypeFromLayoutOptionsMenu(); - const { availableFieldsForKanban, navigateToSelectSettings } = - useGetAvailableFieldsForKanban(); + const { availableFieldsForGrouping, navigateToSelectSettings } = + useGetAvailableFieldsToGroupRecordsBy(); const { availableFieldsForCalendar, navigateToDateFieldSettings } = useGetAvailableFieldsForCalendar(); const { closeDropdown } = useCloseDropdown(); @@ -85,7 +85,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { if (isDefaultView) { return; } - if (availableFieldsForKanban.length === 0) { + if (availableFieldsForGrouping.length === 0) { navigateToSelectSettings(); closeDropdown(dropdownId); return; @@ -200,7 +200,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { text={t`Not available for default view`} /> - ) : availableFieldsForKanban.length === 0 ? ( + ) : availableFieldsForGrouping.length === 0 ? ( t`Create Select...` ) : undefined } diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx index d42449155a..b7c1a0b330 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx @@ -20,6 +20,7 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useLingui } from '@lingui/react/macro'; import { IconChevronLeft, @@ -75,6 +76,11 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { handleHideEmptyRecordGroupChange, } = useRecordGroupVisibility(); + const { availableFieldsForGrouping } = + useGetAvailableFieldsToGroupRecordsBy(); + + const hasOnlyOneGroupByOption = availableFieldsForGrouping.length <= 1; + useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && @@ -116,10 +122,17 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { > {currentView?.key !== 'INDEX' && ( <> - + + !hasOnlyOneGroupByOption && + onContentChange('recordGroupFields') + } + > onContentChange('recordGroupFields')} LeftIcon={IconLayoutList} text={t`Group by`} contextualText={recordGroupFieldMetadata?.label} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx index 9022cc1223..4eaa8defc1 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx @@ -24,9 +24,9 @@ describe('useSearchRecordGroupField', () => { it('filters fields correctly based on input', () => { const fields = [ - { type: FieldMetadataType.SELECT, label: 'First' }, - { type: FieldMetadataType.SELECT, label: 'Second' }, - { type: FieldMetadataType.TEXT, label: 'Third' }, + { type: FieldMetadataType.SELECT, label: 'First', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Second', isActive: true }, + { type: FieldMetadataType.TEXT, label: 'Third', isActive: true }, ]; const mockContextValue = { objectMetadataItem: { @@ -43,15 +43,15 @@ describe('useSearchRecordGroupField', () => { }); expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ - { type: FieldMetadataType.SELECT, label: 'First' }, + { type: FieldMetadataType.SELECT, label: 'First', isActive: true }, ]); }); it('returns all select fields when search input is empty', () => { const fields = [ - { type: FieldMetadataType.SELECT, label: 'First' }, - { type: FieldMetadataType.SELECT, label: 'Second' }, - { type: FieldMetadataType.TEXT, label: 'Third' }, + { type: FieldMetadataType.SELECT, label: 'First', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Second', isActive: true }, + { type: FieldMetadataType.TEXT, label: 'Third', isActive: true }, ]; const mockContextValue = { objectMetadataItem: { @@ -64,8 +64,129 @@ describe('useSearchRecordGroupField', () => { const { result } = renderWithContext(mockContextValue); expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ - { type: FieldMetadataType.SELECT, label: 'First' }, - { type: FieldMetadataType.SELECT, label: 'Second' }, + { type: FieldMetadataType.SELECT, label: 'First', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Second', isActive: true }, ]); }); + + it('filters out inactive SELECT fields', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'Active Field', isActive: true }, + { + type: FieldMetadataType.SELECT, + label: 'Inactive Field', + isActive: false, + }, + { type: FieldMetadataType.TEXT, label: 'Text Field', isActive: true }, + ]; + const mockContextValue = { + objectMetadataItem: { + fields, + readableFields: fields, + updatableFields: fields, + }, + }; + + const { result } = renderWithContext(mockContextValue); + + expect(result.current.filteredRecordGroupFieldMetadataItems).toHaveLength( + 1, + ); + expect(result.current.filteredRecordGroupFieldMetadataItems[0].label).toBe( + 'Active Field', + ); + }); + + it('performs case-insensitive search', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'Status', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Priority', isActive: true }, + ]; + const mockContextValue = { + objectMetadataItem: { + fields, + readableFields: fields, + updatableFields: fields, + }, + }; + + const { result } = renderWithContext(mockContextValue); + + act(() => { + result.current.setRecordGroupFieldSearchInput('STATUS'); + }); + + expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ + { type: FieldMetadataType.SELECT, label: 'Status', isActive: true }, + ]); + }); + + it('returns empty array when no fields match search', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'Status', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Priority', isActive: true }, + ]; + const mockContextValue = { + objectMetadataItem: { + fields, + readableFields: fields, + updatableFields: fields, + }, + }; + + const { result } = renderWithContext(mockContextValue); + + act(() => { + result.current.setRecordGroupFieldSearchInput('nonexistent'); + }); + + expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([]); + }); + + it('returns partial matches in search', () => { + const fields = [ + { type: FieldMetadataType.SELECT, label: 'User Status', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Task Status', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Priority', isActive: true }, + ]; + const mockContextValue = { + objectMetadataItem: { + fields, + readableFields: fields, + updatableFields: fields, + }, + }; + + const { result } = renderWithContext(mockContextValue); + + act(() => { + result.current.setRecordGroupFieldSearchInput('Status'); + }); + + expect(result.current.filteredRecordGroupFieldMetadataItems).toHaveLength( + 2, + ); + expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ + { type: FieldMetadataType.SELECT, label: 'User Status', isActive: true }, + { type: FieldMetadataType.SELECT, label: 'Task Status', isActive: true }, + ]); + }); + + it('returns empty array when no SELECT fields exist', () => { + const fields = [ + { type: FieldMetadataType.TEXT, label: 'Name', isActive: true }, + { type: FieldMetadataType.NUMBER, label: 'Count', isActive: true }, + ]; + const mockContextValue = { + objectMetadataItem: { + fields, + readableFields: fields, + updatableFields: fields, + }, + }; + + const { result } = renderWithContext(mockContextValue); + + expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([]); + }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts index c5e69d63be..5dbc25ddcf 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts @@ -17,6 +17,7 @@ export const useSearchRecordGroupField = () => { return objectMetadataItem.readableFields.filter( (field) => field.type === FieldMetadataType.SELECT && + field.isActive && field.label.toLocaleLowerCase().includes(searchInputLowerCase), ); }, [objectMetadataItem.readableFields, recordGroupFieldSearchInput]); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index 3e24902c34..8c0016dcfb 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -11,7 +11,7 @@ import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; -import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { ViewCalendarLayout } from '~/generated/graphql'; @@ -19,7 +19,8 @@ import { ViewCalendarLayout } from '~/generated/graphql'; export const useSetViewTypeFromLayoutOptionsMenu = () => { const { updateCurrentView } = useUpdateCurrentView(); const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState); - const { availableFieldsForKanban } = useGetAvailableFieldsForKanban(); + const { availableFieldsForGrouping } = + useGetAvailableFieldsToGroupRecordsBy(); const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { loadRecordIndexStates } = useLoadRecordIndexStates(); @@ -60,11 +61,11 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { switch (viewType) { case ViewType.Kanban: { - if (availableFieldsForKanban.length === 0) { + if (availableFieldsForGrouping.length === 0) { throw new Error('No fields for kanban - should not happen'); } - const mainGroupByFieldMetadataId = availableFieldsForKanban[0].id; + const mainGroupByFieldMetadataId = availableFieldsForGrouping[0].id; updateCurrentViewParams.mainGroupByFieldMetadataId = mainGroupByFieldMetadataId; @@ -154,7 +155,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { } }, [ - availableFieldsForKanban, + availableFieldsForGrouping, setRecordIndexViewType, updateCurrentView, availableFieldsForCalendar, diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx index 6d07c3e416..80d16f3556 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx @@ -27,7 +27,7 @@ import { VIEW_PICKER_TYPE_SELECT_OPTIONS } from '@/views/view-picker/constants/V import { VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerViewTypeDropdownId'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; -import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerCalendarFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerCalendarFieldMetadataIdComponentState'; import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState'; @@ -92,7 +92,8 @@ export const ViewPickerContentCreateMode = () => { const { createViewFromCurrentState } = useCreateViewFromCurrentState(); - const { availableFieldsForKanban } = useGetAvailableFieldsForKanban(); + const { availableFieldsForGrouping } = + useGetAvailableFieldsToGroupRecordsBy(); const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar(); @@ -105,7 +106,7 @@ export const ViewPickerContentCreateMode = () => { if ( viewPickerType === ViewType.Kanban && - availableFieldsForKanban.length === 0 + availableFieldsForGrouping.length === 0 ) { return; } @@ -117,7 +118,7 @@ export const ViewPickerContentCreateMode = () => { viewPickerIsPersisting, createViewFromCurrentState, viewPickerType, - availableFieldsForKanban, + availableFieldsForGrouping, availableFieldsForCalendar, ], }); @@ -200,8 +201,8 @@ export const ViewPickerContentCreateMode = () => { setViewPickerMainGroupByFieldMetadataId(value); }} options={ - availableFieldsForKanban.length > 0 - ? availableFieldsForKanban.map((field) => ({ + availableFieldsForGrouping.length > 0 + ? availableFieldsForGrouping.map((field) => ({ value: field.id, label: field.label, })) @@ -210,7 +211,7 @@ export const ViewPickerContentCreateMode = () => { dropdownId={VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID} /> - {availableFieldsForKanban.length === 0 && ( + {availableFieldsForGrouping.length === 0 && ( Set up a Select field on {objectLabel} to create a Kanban diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx index d262f36860..77fcab318e 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEffect.tsx @@ -8,7 +8,7 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector'; import { viewTypeIconMapping } from '@/views/types/ViewType'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; -import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerCalendarFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerCalendarFieldMetadataIdComponentState'; import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState'; @@ -75,7 +75,8 @@ export const ViewPickerContentEffect = () => { (view) => view.id === viewPickerReferenceViewId, ); - const { availableFieldsForKanban } = useGetAvailableFieldsForKanban(); + const { availableFieldsForGrouping } = + useGetAvailableFieldsToGroupRecordsBy(); const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar(); const hasViewPermission = useHasPermissionFlag(PermissionFlagType.VIEWS); @@ -115,14 +116,14 @@ export const ViewPickerContentEffect = () => { useEffect(() => { if ( isDefined(referenceView) && - availableFieldsForKanban.length > 0 && + availableFieldsForGrouping.length > 0 && viewPickerMainGroupByFieldMetadataId === '' ) { setViewPickerMainGroupByFieldMetadataId( isDefined(referenceView.mainGroupByFieldMetadataId) && referenceView.mainGroupByFieldMetadataId !== '' ? referenceView.mainGroupByFieldMetadataId - : availableFieldsForKanban[0].id, + : availableFieldsForGrouping[0].id, ); } if ( @@ -139,7 +140,7 @@ export const ViewPickerContentEffect = () => { } }, [ referenceView, - availableFieldsForKanban, + availableFieldsForGrouping, viewPickerMainGroupByFieldMetadataId, setViewPickerMainGroupByFieldMetadataId, availableFieldsForCalendar, diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx index c58949fcf9..f86e7d9b1e 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx @@ -3,7 +3,7 @@ import { ViewType } from '@/views/types/ViewType'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; -import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerCalendarFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerCalendarFieldMetadataIdComponentState'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; @@ -14,8 +14,8 @@ import { Button } from 'twenty-ui/input'; export const ViewPickerCreateButton = () => { const { t } = useLingui(); - const { availableFieldsForKanban, navigateToSelectSettings } = - useGetAvailableFieldsForKanban(); + const { availableFieldsForGrouping, navigateToSelectSettings } = + useGetAvailableFieldsToGroupRecordsBy(); const { availableFieldsForCalendar, navigateToDateFieldSettings } = useGetAvailableFieldsForCalendar(); @@ -56,7 +56,7 @@ export const ViewPickerCreateButton = () => { if ( viewPickerType === ViewType.Kanban && - availableFieldsForKanban.length === 0 + availableFieldsForGrouping.length === 0 ) { return (