Sync command menu with main context store (#19650)

## PR description

- The command menu in the side panel now reads directly from
`MAIN_CONTEXT_STORE_INSTANCE_ID` instead of snapshotting the main
context store into a separate side-panel instance when opening. This
keeps the command menu always in sync with the current page state
(selection, filters, view, etc.).
- Removed the broadening/reset-to-selection feature (Backspace to clear
context, "Reset to" button) since the command menu no longer maintains
its own copy of the context.

## Video QA


https://github.com/user-attachments/assets/5d5bc664-b6d4-431d-a271-6ce23d8a4ae0
This commit is contained in:
Raphaël Bosi
2026-04-13 18:08:15 +02:00
committed by GitHub
parent 123d6241d7
commit fb772c7695
19 changed files with 17 additions and 1144 deletions
@@ -1,195 +0,0 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
import { mockedPersonRecords } from '~/testing/mock-data/generated/data/people/mock-people-data';
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
const personMockObjectMetadataItem =
getTestEnrichedObjectMetadataItemsMock().find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = [...mockedPersonRecords];
jotaiStore.set(
recordStoreFamilyState.atomFamily(peopleMock[0].id),
peopleMock[0],
);
jotaiStore.set(
recordStoreFamilyState.atomFamily(peopleMock[1].id),
peopleMock[1],
);
const wrapper = getJestMetadataAndApolloMocksAndCommandMenuWrapper({
apolloMocks: [],
componentInstanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
},
contextStoreNumberOfSelectedRecords: 2,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
describe('useSetGlobalCommandMenuContext', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should reset all command menu context states', () => {
const { result } = renderHook(
() => {
const { setGlobalCommandMenuContext } =
useSetGlobalCommandMenuContext();
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue(
contextStoreNumberOfSelectedRecordsComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
const contextStoreFilters = useAtomComponentStateValue(
contextStoreFiltersComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
const contextStoreFilterGroups = useAtomComponentStateValue(
contextStoreFilterGroupsComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
const contextStoreAnyFieldFilterValue = useAtomComponentStateValue(
contextStoreAnyFieldFilterValueComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
const contextStoreCurrentViewType = useAtomComponentStateValue(
contextStoreCurrentViewTypeComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
return {
setGlobalCommandMenuContext,
contextStoreTargetedRecordsRule,
contextStoreNumberOfSelectedRecords,
contextStoreFilters,
contextStoreFilterGroups,
contextStoreCurrentViewType,
contextStoreAnyFieldFilterValue,
};
},
{
wrapper,
},
);
expect(result.current.contextStoreTargetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
});
expect(result.current.contextStoreNumberOfSelectedRecords).toBe(2);
expect(result.current.contextStoreFilters).toEqual([]);
expect(result.current.contextStoreAnyFieldFilterValue).toEqual('');
expect(result.current.contextStoreCurrentViewType).toBe(
ContextStoreViewType.Table,
);
const sidePanelPageInfo = jotaiStore.get(sidePanelPageInfoState.atom);
expect(sidePanelPageInfo).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
const hasUserSelectedSidePanelListItem = jotaiStore.get(
hasUserSelectedSidePanelListItemState.atom,
);
expect(hasUserSelectedSidePanelListItem).toBe(false);
act(() => {
result.current.setGlobalCommandMenuContext();
});
expect(result.current.contextStoreTargetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [],
});
expect(result.current.contextStoreNumberOfSelectedRecords).toBe(0);
expect(result.current.contextStoreFilters).toEqual([]);
expect(result.current.contextStoreAnyFieldFilterValue).toEqual('');
expect(result.current.contextStoreCurrentViewType).toBe(
ContextStoreViewType.Table,
);
const sidePanelPageInfoAfter = jotaiStore.get(sidePanelPageInfoState.atom);
expect(sidePanelPageInfoAfter).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
const hasUserSelectedSidePanelListItemAfter = jotaiStore.get(
hasUserSelectedSidePanelListItemState.atom,
);
expect(hasUserSelectedSidePanelListItemAfter).toBe(false);
});
it('should copy context store states to previous instance before resetting', () => {
const { result } = renderHook(
() => {
const { setGlobalCommandMenuContext } =
useSetGlobalCommandMenuContext();
// oxlint-disable-next-line twenty/matching-state-variable
const previousTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
// oxlint-disable-next-line twenty/matching-state-variable
const previousNumberOfSelectedRecords = useAtomComponentStateValue(
contextStoreNumberOfSelectedRecordsComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
return {
setGlobalCommandMenuContext,
previousTargetedRecordsRule,
previousNumberOfSelectedRecords,
};
},
{
wrapper,
},
);
act(() => {
result.current.setGlobalCommandMenuContext();
});
expect(result.current.previousTargetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
});
expect(result.current.previousNumberOfSelectedRecords).toBe(2);
});
});
@@ -1,7 +1,4 @@
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useOpenAskAIPageInSidePanel } from '@/side-panel/hooks/useOpenAskAIPageInSidePanel';
import { useOpenRecordsSearchPageInSidePanel } from '@/side-panel/hooks/useOpenRecordsSearchPageInSidePanel';
@@ -11,7 +8,6 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
@@ -29,8 +25,6 @@ export const useCommandMenuHotKeys = () => {
const { goBackFromSidePanel, goBackOneSubPageOrMainPage } =
useSidePanelHistory();
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
@@ -39,11 +33,6 @@ export const useCommandMenuHotKeys = () => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
useGlobalHotkeys({
keys: ['ctrl+k', 'meta+k'],
callback: () => {
@@ -99,27 +88,12 @@ export const useCommandMenuHotKeys = () => {
return;
}
if (
sidePanelPage === SidePanelPages.CommandMenuDisplay &&
!(
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0
)
) {
setGlobalCommandMenuContext();
}
if (sidePanelPage !== SidePanelPages.CommandMenuDisplay) {
goBackOneSubPageOrMainPage();
}
},
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [
sidePanelPage,
sidePanelSearch,
contextStoreTargetedRecordsRule,
goBackOneSubPageOrMainPage,
setGlobalCommandMenuContext,
],
dependencies: [sidePanelPage, sidePanelSearch, goBackOneSubPageOrMainPage],
options: {
preventDefault: false,
enableOnFormTags: false,
@@ -1,144 +0,0 @@
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useCallback } from 'react';
import { useStore } from 'jotai';
export const useCopyContextStoreStates = () => {
const store = useStore();
const copyContextStoreStates = useCallback(
({
instanceIdToCopyFrom,
instanceIdToCopyTo,
}: {
instanceIdToCopyFrom: string;
instanceIdToCopyTo: string;
}) => {
const contextStoreCurrentObjectMetadataItemId = store.get(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentObjectMetadataItemId,
);
const contextStoreTargetedRecordsRule = store.get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreTargetedRecordsRule,
);
const contextStoreNumberOfSelectedRecords = store.get(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreNumberOfSelectedRecords,
);
const contextStoreFilters = store.get(
contextStoreFiltersComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreFiltersComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreFilters,
);
const contextStoreFilterGroups = store.get(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreFilterGroups,
);
const contextStoreAnyFieldFilterValue = store.get(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreAnyFieldFilterValue,
);
const contextStoreCurrentViewId = store.get(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentViewId,
);
const contextStoreCurrentViewType = store.get(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentViewType,
);
const contextStoreIsFullTabWidgetInEditMode = store.get(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
);
store.set(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreIsFullTabWidgetInEditMode,
);
},
[store],
);
return { copyContextStoreStates };
};
@@ -1,71 +0,0 @@
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useCallback } from 'react';
import { useStore } from 'jotai';
export const useResetContextStoreStates = () => {
const store = useStore();
const resetContextStoreStates = useCallback(
(instanceId: string) => {
store.set(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId,
}),
undefined,
);
store.set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId,
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
store.set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId,
}),
0,
);
store.set(
contextStoreFiltersComponentState.atomFamily({
instanceId,
}),
[],
);
store.set(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId,
}),
[],
);
store.set(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId,
}),
'',
);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId,
}),
undefined,
);
},
[store],
);
return { resetContextStoreStates };
};
@@ -1,182 +0,0 @@
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { atom, useStore } from 'jotai';
import { useCallback } from 'react';
export const useSetGlobalCommandMenuContext = () => {
const store = useStore();
const setGlobalCommandMenuContext = useCallback(() => {
store.set(
atom(null, (get, batchSet) => {
const fromId = SIDE_PANEL_COMPONENT_INSTANCE_ID;
const toId = SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID;
batchSet(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreFiltersComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreFiltersComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: toId,
}),
get(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: fromId,
}),
),
);
batchSet(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: fromId,
}),
{ mode: 'selection', selectedRecordIds: [] },
);
batchSet(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: fromId,
}),
0,
);
batchSet(
contextStoreFiltersComponentState.atomFamily({
instanceId: fromId,
}),
[],
);
batchSet(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: fromId,
}),
[],
);
batchSet(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: fromId,
}),
'',
);
batchSet(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: fromId,
}),
ContextStoreViewType.Table,
);
batchSet(sidePanelPageInfoState.atom, {
title: undefined,
Icon: undefined,
instanceId: '',
});
batchSet(hasUserSelectedSidePanelListItemState.atom, false);
}),
);
}, [store]);
return {
setGlobalCommandMenuContext,
};
};