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:
+5
-5
@@ -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
|
||||
}
|
||||
|
||||
+15
-2
@@ -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}
|
||||
|
||||
+130
-9
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
+1
@@ -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]);
|
||||
|
||||
+6
-5
@@ -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,
|
||||
|
||||
+8
-7
@@ -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
|
||||
|
||||
+6
-5
@@ -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,
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+119
@@ -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');
|
||||
});
|
||||
});
|
||||
+102
@@ -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');
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user