Allow moving columns left/right in Kanban view (#11827)

### Demo


https://github.com/user-attachments/assets/9bc81c33-454d-4581-b06e-dbc0ea79a8dd

Closes #11791

---------

Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Abdul Rahman
2025-05-06 15:09:57 +05:30
committed by GitHub
parent e92117d556
commit ef8b015741
4 changed files with 110 additions and 56 deletions

View File

@ -1,21 +1,31 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
import { SettingsPath } from '@/types/SettingsPath';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { t } from '@lingui/core/macro';
import { isUndefined } from '@sniptt/guards';
import { useCallback, useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
IconArrowLeft,
IconArrowRight,
IconEyeOff,
IconSettings,
} from 'twenty-ui/display';
import { SettingPermissionType } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { IconEyeOff, IconSettings } from 'twenty-ui/display';
type UseRecordGroupActionsParams = {
viewType: ViewType;
@ -50,6 +60,16 @@ export const useRecordGroupActions = ({
navigationMemorizedUrlState,
);
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
viewType,
);
const { reorderRecordGroups } = useReorderRecordGroups({
viewBarId: objectMetadataItem.id,
viewType,
});
const navigateToSelectSettings = useCallback(() => {
setNavigationMemorizedUrl(location.pathname + location.search);
@ -73,33 +93,60 @@ export const useRecordGroupActions = ({
const hasAccessToDataModelSettings = useHasSettingsPermission(
SettingPermissionType.DATA_MODEL,
);
const currentIndex = visibleRecordGroupIds.findIndex(
(id) => id === recordGroupDefinition.id,
);
const isCurrentRecordGroupNotFound = currentIndex === -1;
const recordGroupActions: RecordGroupAction[] = [];
if (hasAccessToDataModelSettings) {
recordGroupActions.push({
const recordGroupActions: RecordGroupAction[] = [
{
id: 'edit',
label: 'Edit',
label: t`Edit`,
icon: IconSettings,
position: 0,
callback: () => {
navigateToSelectSettings();
},
});
}
recordGroupActions.push({
id: 'hide',
label: 'Hide',
icon: IconEyeOff,
position: 1,
callback: () => {
handleRecordGroupVisibilityChange({
...recordGroupDefinition,
isVisible: false,
});
condition: hasAccessToDataModelSettings,
callback: navigateToSelectSettings,
},
});
{
id: 'moveRight',
label: t`Move right`,
icon: IconArrowRight,
condition:
!isCurrentRecordGroupNotFound &&
currentIndex < visibleRecordGroupIds.length - 1,
position: 1,
callback: () =>
reorderRecordGroups({
fromIndex: currentIndex,
toIndex: currentIndex + 1,
}),
},
{
id: 'moveLeft',
label: t`Move left`,
icon: IconArrowLeft,
condition: !isCurrentRecordGroupNotFound && currentIndex > 0,
position: 2,
callback: () =>
reorderRecordGroups({
fromIndex: currentIndex,
toIndex: currentIndex - 1,
}),
},
{
id: 'hide',
label: t`Hide`,
icon: IconEyeOff,
position: 3,
callback: () =>
handleRecordGroupVisibilityChange({
...recordGroupDefinition,
isVisible: false,
}),
},
];
return recordGroupActions;
return recordGroupActions.filter(
({ condition }) => isUndefined(condition) || condition !== false,
);
};

View File

@ -1,4 +1,4 @@
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
@ -30,15 +30,24 @@ export const useRecordGroupReorderConfirmationModal = ({
isRecordGroupReorderConfirmationModalVisibleState,
);
const [pendingDragEndReorder, setPendingDragEndReorder] =
const [pendingDragEndHandlerParams, setPendingDragEndHandlerParams] =
useState<Parameters<OnDragEndResponder> | null>(null);
const { handleOrderChange: handleRecordGroupOrderChange } =
useRecordGroupReorder({
viewBarId: recordIndexId,
viewType,
});
const { reorderRecordGroups } = useReorderRecordGroups({
viewBarId: recordIndexId,
viewType,
});
const handleDragEnd: OnDragEndResponder = (result) => {
if (!result.destination) {
return;
}
reorderRecordGroups({
fromIndex: result.source.index - 1,
toIndex: result.destination.index - 1,
});
};
const isDragableSortRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupIsDraggableSortComponentSelector,
);
@ -47,32 +56,29 @@ export const useRecordGroupReorderConfirmationModal = ({
recordIndexRecordGroupSortComponentState,
);
const handleRecordGroupOrderChangeWithModal: OnDragEndResponder = (
result,
provided,
) => {
const handleDragEndWithModal: OnDragEndResponder = (result, provided) => {
if (!isDragableSortRecordGroup) {
setIsRecordGroupReorderConfirmationModalVisible(true);
setActiveDropdownFocusIdAndMemorizePrevious(null);
setPendingDragEndReorder([result, provided]);
setPendingDragEndHandlerParams([result, provided]);
} else {
handleRecordGroupOrderChange(result, provided);
handleDragEnd(result, provided);
}
};
const handleConfirmClick = () => {
if (!pendingDragEndReorder) {
if (!pendingDragEndHandlerParams) {
throw new Error('pendingDragEndReorder is not set');
}
setRecordGroupSort(RecordGroupSort.Manual);
setPendingDragEndReorder(null);
handleRecordGroupOrderChange(...pendingDragEndReorder);
setPendingDragEndHandlerParams(null);
handleDragEnd(...pendingDragEndHandlerParams);
goBackToPreviousDropdownFocusId();
};
return {
handleRecordGroupOrderChangeWithModal,
handleRecordGroupOrderChangeWithModal: handleDragEndWithModal,
handleRecordGroupReorderConfirmClick: handleConfirmClick,
};
};

View File

@ -1,5 +1,3 @@
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
@ -11,19 +9,24 @@ import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups
import { ViewType } from '@/views/types/ViewType';
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from 'twenty-shared/utils';
type UseRecordGroupHandlersParams = {
type UseReorderRecordGroupsParams = {
viewBarId: string;
viewType: ViewType;
};
export const useRecordGroupReorder = ({
type ReorderRecordGroupsParams = {
fromIndex: number;
toIndex: number;
};
export const useReorderRecordGroups = ({
viewBarId,
viewType,
}: UseRecordGroupHandlersParams) => {
}: UseReorderRecordGroupsParams) => {
const { setRecordGroups } = useSetRecordGroups();
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
@ -33,13 +36,9 @@ export const useRecordGroupReorder = ({
const { saveViewGroups } = useSaveCurrentViewGroups();
const handleOrderChange: OnDragEndResponder = useRecoilCallback(
const reorderRecordGroups = useRecoilCallback(
({ snapshot }) =>
(result) => {
if (!result.destination) {
return;
}
({ fromIndex, toIndex }: ReorderRecordGroupsParams) => {
const visibleRecordGroupIds = getSnapshotValue(
snapshot,
visibleRecordGroupIdsFamilySelector(viewType),
@ -48,8 +47,8 @@ export const useRecordGroupReorder = ({
const reorderedVisibleRecordGroupIds = moveArrayItem(
visibleRecordGroupIds,
{
fromIndex: result.source.index - 1,
toIndex: result.destination.index - 1,
fromIndex,
toIndex,
},
);
@ -96,6 +95,6 @@ export const useRecordGroupReorder = ({
);
return {
handleOrderChange,
reorderRecordGroups,
};
};

View File

@ -1,8 +1,10 @@
import { IconComponent } from 'twenty-ui/display';
export type RecordGroupAction = {
id: string;
label: string;
icon: IconComponent;
position: number;
callback: () => void;
condition?: boolean;
};