From ef8b0157415b3a6c9b53aadf9fdb9e26748ac9db Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Tue, 6 May 2025 15:09:57 +0530 Subject: [PATCH] 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 Co-authored-by: Lucas Bordeau --- .../hooks/useRecordGroupActions.ts | 95 ++++++++++++++----- .../useRecordGroupReorderConfirmationModal.ts | 40 ++++---- ...upReorder.ts => useReorderRecordGroups.ts} | 29 +++--- .../record-group/types/RecordGroupActions.ts | 2 + 4 files changed, 110 insertions(+), 56 deletions(-) rename packages/twenty-front/src/modules/object-record/record-group/hooks/{useRecordGroupReorder.ts => useReorderRecordGroups.ts} (87%) diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts index bc14d25d4..dd10a2294 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -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, + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts index 2831cc1f5..e91dc8637 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts @@ -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 | 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, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts similarity index 87% rename from packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts rename to packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts index 3ac521f79..1d597432c 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts @@ -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, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts index 828b63778..f6dc2bf1a 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupActions.ts @@ -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; };