Files
twenty/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
T
Raphaël Bosi 77315b7f6e Sync record selection with the edit mode (#19276)
## PR description

The previous approach used a detached flag to represent selection mode,
which could was not synced sync with actual UI selection state. This
change ties the edit mode's selection/fallback behavior directly to
whether records are truly selected in the record index.

- Removes `commandMenuItemEditSelectionModeState` (a standalone 'none' |
'selection' atom) and replaces it with
`mainContextStoreHasSelectedRecordsSelector`, which derives selection
state from the real record selection in the context store.
- Adds `useSelectFirstRecordForEditMode` to programmatically select the
first record (table row or kanban card) when toggling to selection mode,
and useResetRecordIndexSelection to clear selection when toggling off or
exiting edit mode.
- Ensures record selection is properly cleaned up when exiting layout
customization mode or closing the side panel.

## Video QA

### Table


https://github.com/user-attachments/assets/1d845809-8369-4872-b3a9-a0281b8e464b

### Board


https://github.com/user-attachments/assets/325c2d58-4ca0-408a-ab4a-66b97d9d70de
2026-04-02 16:35:14 +00:00

119 lines
4.5 KiB
TypeScript

import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useResetRecordIndexSelection } from '@/object-record/record-index/hooks/useResetRecordIndexSelection';
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconColumnInsertRight, IconDotsVertical } from 'twenty-ui/display';
export const useSidePanelMenu = () => {
const store = useStore();
const { navigateSidePanel } = useNavigateSidePanel();
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
const { resetRecordIndexSelection } = useResetRecordIndexSelection(
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeSidePanelMenu = useCallback(() => {
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
if (isSidePanelOpened) {
const isLayoutCustomizationModeEnabled = store.get(
isLayoutCustomizationModeEnabledState.atom,
);
if (isLayoutCustomizationModeEnabled) {
resetRecordIndexSelection();
}
store.set(sidePanelNavigationStackState.atom, []);
store.set(isSidePanelOpenedState.atom, false);
store.set(isSidePanelClosingState.atom, true);
closeAnyOpenDropdown();
removeFocusItemFromFocusStackById({
focusId: SIDE_PANEL_FOCUS_ID,
});
}
}, [
closeAnyOpenDropdown,
removeFocusItemFromFocusStackById,
store,
resetRecordIndexSelection,
]);
const openSidePanelMenu = useCallback(() => {
emitSidePanelOpenEvent();
closeAnyOpenDropdown();
const isLayoutCustomizationModeEnabled = store.get(
isLayoutCustomizationModeEnabledState.atom,
);
const selectedNavigationItemId = store.get(
selectedNavigationMenuItemIdInEditModeState.atom,
);
if (
isLayoutCustomizationModeEnabled &&
isDefined(selectedNavigationItemId)
) {
navigateSidePanel({
page: SidePanelPages.NavigationMenuItemEdit,
pageTitle: t`Edit`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
} else if (isLayoutCustomizationModeEnabled) {
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New menu item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
} else {
navigateSidePanel({
page: SidePanelPages.CommandMenuDisplay,
pageTitle: t`Command Menu`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
}
}, [closeAnyOpenDropdown, navigateSidePanel, store]);
const navigateSidePanelMenu = navigateSidePanel;
const toggleSidePanelMenu = useCallback(() => {
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
store.set(sidePanelSearchState.atom, '');
store.set(sidePanelSearchObjectFilterState.atom, null);
if (isSidePanelOpened) {
closeSidePanelMenu();
} else {
openSidePanelMenu();
}
}, [closeSidePanelMenu, openSidePanelMenu, store]);
return {
openSidePanelMenu,
closeSidePanelMenu,
navigateSidePanelMenu,
toggleSidePanelMenu,
};
};