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
This commit is contained in:
Marie
2026-01-08 14:08:06 +01:00
committed by GitHub
parent c207214f0b
commit f414f4799b
12 changed files with 403 additions and 44 deletions
@@ -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
}
@@ -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' && (
<>
<SelectableListItem itemId="GroupBy">
<SelectableListItem
itemId="GroupBy"
onEnter={() =>
!hasOnlyOneGroupByOption &&
onContentChange('recordGroupFields')
}
>
<MenuItem
focused={selectedItemId === 'GroupBy'}
disabled
disabled={hasOnlyOneGroupByOption}
onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList}
text={t`Group by`}
contextualText={recordGroupFieldMetadata?.label}
@@ -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([]);
});
});
@@ -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]);
@@ -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,
@@ -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}
/>
</ViewPickerSelectContainer>
{availableFieldsForKanban.length === 0 && (
{availableFieldsForGrouping.length === 0 && (
<StyledFieldAvailableContainer>
<Trans>
Set up a Select field on {objectLabel} to create a Kanban
@@ -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,
@@ -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 (
<Button
@@ -2,7 +2,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { ViewType } from '@/views/types/ViewType';
import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState';
import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState';
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 { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
import { viewPickerMainGroupByFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerMainGroupByFieldMetadataIdComponentState';
@@ -11,8 +11,8 @@ import { t } from '@lingui/core/macro';
import { Button } from 'twenty-ui/input';
export const ViewPickerEditButton = () => {
const { availableFieldsForKanban, navigateToSelectSettings } =
useGetAvailableFieldsForKanban();
const { availableFieldsForGrouping, navigateToSelectSettings } =
useGetAvailableFieldsToGroupRecordsBy();
const { viewPickerMode } = useViewPickerMode();
const viewPickerType = useRecoilComponentValue(viewPickerTypeComponentState);
@@ -44,7 +44,7 @@ export const ViewPickerEditButton = () => {
if (
viewPickerType === ViewType.Kanban &&
availableFieldsForKanban.length === 0
availableFieldsForGrouping.length === 0
) {
return (
<Button
@@ -0,0 +1,119 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const mockObjectMetadataId = 'test-object-id';
const mockViewInstanceId = 'test-view-instance-id';
const createMockObjectMetadataItem = (fields: any[]) => ({
id: mockObjectMetadataId,
namePlural: 'testObjects',
nameSingular: 'testObject',
readableFields: fields,
fields,
});
const createWrapper = (objectMetadataItems: any[]) => {
return ({ children }: { children: ReactNode }) => (
<RecoilRoot
initializeState={(snapshot: MutableSnapshot) => {
snapshot.set(objectMetadataItemsState, objectMetadataItems);
snapshot.set(
viewObjectMetadataIdComponentState.atomFamily({
instanceId: mockViewInstanceId,
}),
mockObjectMetadataId,
);
}}
>
<MemoryRouter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: mockViewInstanceId }}
>
{children}
</ViewComponentInstanceContext.Provider>
</MemoryRouter>
</RecoilRoot>
);
};
describe('useGetAvailableFieldsForCalendar', () => {
it('should return only DATE and DATE_TIME fields', () => {
const fields = [
{
id: '1',
type: FieldMetadataType.DATE,
label: 'Due Date',
isActive: true,
},
{
id: '2',
type: FieldMetadataType.DATE_TIME,
label: 'Created At',
isActive: true,
},
{
id: '3',
type: FieldMetadataType.TEXT,
label: 'Name',
isActive: true,
},
{
id: '4',
type: FieldMetadataType.NUMBER,
label: 'Count',
isActive: true,
},
];
const objectMetadataItems = [createMockObjectMetadataItem(fields)];
const wrapper = createWrapper(objectMetadataItems);
const { result } = renderHook(() => useGetAvailableFieldsForCalendar(), {
wrapper,
});
expect(result.current.availableFieldsForCalendar).toHaveLength(2);
expect(result.current.availableFieldsForCalendar).toEqual([
{
id: '1',
type: FieldMetadataType.DATE,
label: 'Due Date',
isActive: true,
},
{
id: '2',
type: FieldMetadataType.DATE_TIME,
label: 'Created At',
isActive: true,
},
]);
});
it('should return the navigateToDateFieldSettings function', () => {
const fields = [
{
id: '1',
type: FieldMetadataType.DATE,
label: 'Due Date',
isActive: true,
},
];
const objectMetadataItems = [createMockObjectMetadataItem(fields)];
const wrapper = createWrapper(objectMetadataItems);
const { result } = renderHook(() => useGetAvailableFieldsForCalendar(), {
wrapper,
});
expect(result.current.navigateToDateFieldSettings).toBeDefined();
expect(typeof result.current.navigateToDateFieldSettings).toBe('function');
});
});
@@ -0,0 +1,102 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy';
import { renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const mockObjectMetadataId = 'test-object-id';
const mockViewInstanceId = 'test-view-instance-id';
const createMockObjectMetadataItem = (fields: any[]) => ({
id: mockObjectMetadataId,
namePlural: 'testObjects',
nameSingular: 'testObject',
readableFields: fields,
fields,
});
const createWrapper = (objectMetadataItems: any[]) => {
return ({ children }: { children: ReactNode }) => (
<RecoilRoot
initializeState={(snapshot: MutableSnapshot) => {
snapshot.set(objectMetadataItemsState, objectMetadataItems);
snapshot.set(
viewObjectMetadataIdComponentState.atomFamily({
instanceId: mockViewInstanceId,
}),
mockObjectMetadataId,
);
}}
>
<MemoryRouter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: mockViewInstanceId }}
>
{children}
</ViewComponentInstanceContext.Provider>
</MemoryRouter>
</RecoilRoot>
);
};
describe('useGetAvailableFieldsToGroupRecordsBy', () => {
it('should filter out inactive SELECT fields', () => {
const fields = [
{
id: '1',
type: FieldMetadataType.SELECT,
label: 'Active Status',
isActive: true,
},
{
id: '2',
type: FieldMetadataType.SELECT,
label: 'Inactive Status',
isActive: false,
},
];
const objectMetadataItems = [createMockObjectMetadataItem(fields)];
const wrapper = createWrapper(objectMetadataItems);
const { result } = renderHook(
() => useGetAvailableFieldsToGroupRecordsBy(),
{
wrapper,
},
);
expect(result.current.availableFieldsForGrouping).toHaveLength(1);
expect(result.current.availableFieldsForGrouping[0].label).toBe(
'Active Status',
);
});
it('should return the navigateToSelectSettings function', () => {
const fields = [
{
id: '1',
type: FieldMetadataType.SELECT,
label: 'Status',
isActive: true,
},
];
const objectMetadataItems = [createMockObjectMetadataItem(fields)];
const wrapper = createWrapper(objectMetadataItems);
const { result } = renderHook(
() => useGetAvailableFieldsToGroupRecordsBy(),
{
wrapper,
},
);
expect(result.current.navigateToSelectSettings).toBeDefined();
expect(typeof result.current.navigateToSelectSettings).toBe('function');
});
});
@@ -11,7 +11,7 @@ import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const useGetAvailableFieldsForKanban = () => {
export const useGetAvailableFieldsToGroupRecordsBy = () => {
const viewObjectMetadataId = useRecoilComponentValue(
viewObjectMetadataIdComponentState,
);
@@ -25,7 +25,7 @@ export const useGetAvailableFieldsForKanban = () => {
(objectMetadata) => objectMetadata.id === viewObjectMetadataId,
);
const availableFieldsForKanban =
const availableFieldsForGrouping =
objectMetadataItem?.readableFields.filter(
(field) =>
field.type === FieldMetadataType.SELECT && field.isActive === true,
@@ -58,7 +58,7 @@ export const useGetAvailableFieldsForKanban = () => {
]);
return {
availableFieldsForKanban,
availableFieldsForGrouping,
navigateToSelectSettings,
};
};