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
+2 -2
View File
@@ -61,8 +61,8 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 48.4,
lines: 47.0,
statements: 48,
lines: 46,
functions: 39.5,
},
},
@@ -4,20 +4,14 @@ import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/const
import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/display/states/commandMenuPinnedInlineLayoutState';
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
import { groupCommandMenuItems } from '@/command-menu-item/utils/groupCommandMenuItems';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { SIDE_PANEL_RESET_CONTEXT_TO_SELECTION } from '@/side-panel/constants/SidePanelResetContextToSelection';
import { SidePanelResetContextToSelectionButton } from '@/side-panel/pages/root/components/SidePanelResetContextToSelectionButton';
import { useFilterCommandMenuItemsWithSidePanelSearch } from '@/side-panel/pages/root/hooks/useFilterCommandMenuItemsWithSidePanelSearch';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { isNumber } from '@sniptt/guards';
import { useContext, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
export const SidePanelCommandMenuItemDisplayPage = () => {
@@ -99,24 +93,8 @@ export const SidePanelCommandMenuItemDisplayPage = () => {
...(noResults ? fallbackCommandMenuItems : []),
].map((item) => item.id);
// oxlint-disable-next-line twenty/matching-state-variable
const previousContextStoreCurrentObjectMetadataItemId =
useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
if (isDefined(previousContextStoreCurrentObjectMetadataItemId)) {
selectableItemIds.unshift(SIDE_PANEL_RESET_CONTEXT_TO_SELECTION);
}
return (
<SidePanelList selectableItemIds={selectableItemIds} noResults={noResults}>
{isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
<SidePanelGroup heading={t`Context`}>
<SidePanelResetContextToSelectionButton />
</SidePanelGroup>
)}
{matchingPinnedItems.length > 0 && (
<SidePanelGroup heading={t`Pinned`}>
{matchingPinnedItems.map((item) => (
@@ -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,
};
};
@@ -10,8 +10,6 @@ import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layo
import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
@@ -98,17 +96,6 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
store.set(contextStoreIsFullTabWidgetInEditModeState, value);
store.set(currentPageLayoutIdState.atom, value ? pageLayoutId : null);
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
if (isSidePanelOpened) {
store.set(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
}),
value,
);
}
},
[
isDashboardInEditModeState,
@@ -1,5 +1,6 @@
import { styled } from '@linaria/react';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
@@ -30,7 +31,7 @@ type SidePanelContainerProps = {
export const SidePanelContainer = ({ children }: SidePanelContainerProps) => {
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const isMobile = useIsMobile();
@@ -43,7 +44,7 @@ export const SidePanelContainer = ({ children }: SidePanelContainerProps) => {
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
@@ -54,7 +55,7 @@ export const SidePanelContainer = ({ children }: SidePanelContainerProps) => {
return (
<RecordComponentInstanceContextsWrapper componentInstanceId={recordIndexId}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID }}
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<CommandMenuComponentInstanceContext.Provider
value={{ instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID }}
@@ -1,52 +0,0 @@
import { SidePanelContextChip } from '@/side-panel/components/SidePanelContextChip';
import { SidePanelContextRecordChipAvatars } from '@/side-panel/components/SidePanelContextRecordChipAvatars';
import { getSelectedRecordsContextText } from '@/side-panel/utils/getSelectedRecordsContextText';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const SidePanelContextRecordsChip = ({
objectMetadataItemId,
instanceId,
}: {
objectMetadataItemId: string;
instanceId?: string;
}) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const allowRequestsToTwentyIcons = useAtomStateValue(
allowRequestsToTwentyIconsState,
);
const { records, loading, totalCount } =
useFindManyRecordsSelectedInContextStore({
limit: 3,
instanceId,
});
if (loading || !totalCount || records.length === 0) {
return null;
}
const Avatars = records.map((record) => (
<SidePanelContextRecordChipAvatars
objectMetadataItem={objectMetadataItem}
key={record.id}
record={record}
/>
));
return (
<SidePanelContextChip
text={getSelectedRecordsContextText(
objectMetadataItem,
records,
totalCount,
allowRequestsToTwentyIcons,
)}
Icons={Avatars}
/>
);
};
@@ -1,269 +0,0 @@
import { gql } from '@apollo/client';
import {
type Decorator,
type Meta,
type StoryObj,
} from '@storybook/react-vite';
import { SidePanelContextRecordsChip } from '@/side-panel/components/SidePanelContextRecordsChip';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
import { type RecordChipData } from '@/object-record/record-field/ui/types/RecordChipData';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ComponentDecorator } from 'twenty-ui/testing';
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
import { mockedCompanyRecords } from '~/testing/mock-data/generated/data/companies/mock-companies-data';
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
const FIND_MANY_COMPANIES = gql`
query FindManyCompanies(
$filter: CompanyFilterInput
$orderBy: [CompanyOrderByInput]
$lastCursor: String
$limit: Int
) {
companies(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
accountOwnerId
address {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
annualRecurringRevenue {
amountMicros
currencyCode
}
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
position
tagline
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`;
const companyMockObjectMetadataItem =
getTestEnrichedObjectMetadataItemsMock().find(
(item) => item.nameSingular === 'company',
);
const companyMock = mockedCompanyRecords[0];
const chipGeneratorPerObjectPerField: Record<
string,
Record<string, (record: ObjectRecord) => RecordChipData>
> = {
company: {
name: (record: ObjectRecord): RecordChipData => ({
recordId: record.id,
name: record.name as string,
avatarUrl: '',
avatarType: 'rounded',
isLabelIdentifier: true,
objectNameSingular: 'company',
}),
},
};
const identifierChipGeneratorPerObject: Record<
string,
(record: ObjectRecord) => RecordChipData
> = {
company: chipGeneratorPerObjectPerField.company.name,
};
const ChipGeneratorsDecorator: Decorator = (Story) => (
<PreComputedChipGeneratorsContext.Provider
value={{
chipGeneratorPerObjectPerField,
identifierChipGeneratorPerObject,
}}
>
<Story />
</PreComputedChipGeneratorsContext.Provider>
);
const createContextStoreWrapper = ({
companies,
componentInstanceId,
}: {
companies: typeof mockedCompanyRecords;
componentInstanceId: string;
}) => {
return getJestMetadataAndApolloMocksAndCommandMenuWrapper({
apolloMocks: [
{
request: {
query: FIND_MANY_COMPANIES,
variables: {
filter: {
id: { in: companies.map((company) => company.id) },
deletedAt: { is: 'NOT_NULL' },
},
orderBy: [{ position: 'AscNullsFirst' }],
limit: 3,
},
},
result: {
data: {
companies: {
edges: companies.slice(0, 3).map((company, index) => ({
node: company,
cursor: `cursor-${index + 1}`,
})),
pageInfo: {
hasNextPage: companies.length > 3,
hasPreviousPage: false,
startCursor: 'cursor-1',
endCursor:
companies.length > 0
? `cursor-${Math.min(companies.length, 3)}`
: null,
},
totalCount: companies.length,
},
},
},
},
],
componentInstanceId,
contextStoreCurrentObjectMetadataNameSingular:
companyMockObjectMetadataItem?.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: companies.map((company) => company.id),
},
contextStoreNumberOfSelectedRecords: companies.length,
onInitializeJotaiStore: () => {
for (const company of companies) {
jotaiStore.set(
recordStoreFamilyState.atomFamily(company.id),
company as ObjectRecord,
);
}
},
});
};
const ContextStoreDecorator: Decorator = (Story) => {
const ContextStoreWrapper = createContextStoreWrapper({
companies: [companyMock],
componentInstanceId: '1',
});
return (
<ContextStoreWrapper>
<Story />
</ContextStoreWrapper>
);
};
const meta: Meta<typeof SidePanelContextRecordsChip> = {
title: 'Modules/SidePanel/SidePanelContextRecordChip',
component: SidePanelContextRecordsChip,
decorators: [
ContextStoreDecorator,
ChipGeneratorsDecorator,
ComponentDecorator,
],
args: {
objectMetadataItemId: companyMockObjectMetadataItem?.id,
},
};
export default meta;
type Story = StoryObj<typeof SidePanelContextRecordsChip>;
export const Default: Story = {};
export const WithTwoCompanies: Story = {
decorators: [
(Story) => {
const twoCompaniesMock = mockedCompanyRecords.slice(0, 2);
const TwoCompaniesWrapper = createContextStoreWrapper({
companies: twoCompaniesMock,
componentInstanceId: '2',
});
return (
<TwoCompaniesWrapper>
<Story />
</TwoCompaniesWrapper>
);
},
],
};
export const WithTenCompanies: Story = {
decorators: [
(Story) => {
const tenCompaniesMock = mockedCompanyRecords.slice(0, 10);
const TenCompaniesWrapper = createContextStoreWrapper({
companies: tenCompaniesMock,
componentInstanceId: '3',
});
return (
<TenCompaniesWrapper>
<Story />
</TenCompaniesWrapper>
);
},
],
};
@@ -1 +0,0 @@
export const SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID = 'side-panel-previous';
@@ -1,2 +0,0 @@
export const SIDE_PANEL_RESET_CONTEXT_TO_SELECTION =
'reset-context-to-selection';
@@ -2,9 +2,7 @@ import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/side-panel/constants/SidePanelContextChipGroupsDropdownId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
@@ -21,7 +19,6 @@ import { SidePanelPages } from 'twenty-shared/types';
import { IconList } from 'twenty-ui/display';
const mockCloseDropdown = jest.fn();
const mockResetContextStoreStates = jest.fn();
const mockResetSelectedItem = jest.fn();
const mockEmitSidePanelCloseEvent = jest.fn();
@@ -31,12 +28,6 @@ jest.mock('@/ui/layout/dropdown/hooks/useCloseDropdown', () => ({
}),
}));
jest.mock('@/command-menu/hooks/useResetContextStoreStates', () => ({
useResetContextStoreStates: () => ({
resetContextStoreStates: mockResetContextStoreStates,
}),
}));
jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({
useSelectableList: () => ({
resetSelectedItem: mockResetSelectedItem,
@@ -162,20 +153,11 @@ describe('useSidePanelCloseAnimationCompleteCleanup', () => {
});
expect(mockCloseDropdown).toHaveBeenCalledTimes(1);
expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2);
expect(mockResetSelectedItem).toHaveBeenCalledTimes(1);
expect(mockEmitSidePanelCloseEvent).toHaveBeenCalledTimes(1);
expect(mockCloseDropdown).toHaveBeenCalledWith(
SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID,
);
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
1,
SIDE_PANEL_COMPONENT_INSTANCE_ID,
);
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
2,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
});
});
@@ -1,7 +1,6 @@
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndCommandMenuStates';
import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/sidePanelNavigationMorphItemsByPageState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
@@ -10,7 +9,6 @@ import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/state
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useStore } from 'jotai';
@@ -29,8 +27,6 @@ export type SidePanelNavigationStackItem = {
export const useNavigateSidePanel = () => {
const store = useStore();
const { copyContextStoreStates } = useCopyContextStoreStates();
const { sidePanelCloseAnimationCompleteCleanup } =
useSidePanelCloseAnimationCompleteCleanup();
@@ -62,15 +58,9 @@ export const useNavigateSidePanel = () => {
},
});
copyContextStoreStates({
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
instanceIdToCopyTo: SIDE_PANEL_COMPONENT_INSTANCE_ID,
});
store.set(isSidePanelOpenedState.atom, true);
store.set(hasUserSelectedSidePanelListItemState.atom, false);
}, [
copyContextStoreStates,
sidePanelCloseAnimationCompleteCleanup,
pushFocusItemToFocusStack,
store,
@@ -1,4 +1,4 @@
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
@@ -9,9 +9,7 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/side-panel/constants/SidePanelContextChipGroupsDropdownId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
import { isPageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/utils/isPageLayoutSidePanelPage';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
@@ -42,16 +40,8 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
SIDE_PANEL_SELECTABLE_LIST_ID,
);
const { resetContextStoreStates } = useResetContextStoreStates();
const { closeDropdown } = useCloseDropdown();
const resetNavigationMenuItemState = () => {
store.set(selectedNavigationMenuItemIdInEditModeState.atom, null);
store.set(pendingInsertionNavigationMenuItemState.atom, null);
store.set(addToNavPayloadRegistryState.atom, new Map());
};
const sidePanelCloseAnimationCompleteCleanup = useCallback(
(options?: { emitSidePanelCloseEvent?: boolean }) => {
closeDropdown(SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
@@ -59,19 +49,16 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
// Snapshot values before any mutations (Jotai store.get is live and
// reflects the latest state, so we capture before mutating).
const currentPage = store.get(sidePanelPageState.atom);
const targetedRecordsRule = store.get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
}),
);
const morphItemsByPage = store.get(
sidePanelNavigationMorphItemsByPageState.atom,
);
resetContextStoreStates(SIDE_PANEL_COMPONENT_INSTANCE_ID);
resetContextStoreStates(SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID);
if (isDefined(currentPage) && isPageLayoutSidePanelPage(currentPage)) {
const targetedRecordsRule = store.get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
);
if (
targetedRecordsRule.mode === 'selection' &&
targetedRecordsRule.selectedRecordIds.length === 1
@@ -121,7 +108,9 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
store.set(sidePanelShowHiddenObjectsState.atom, false);
store.set(sidePanelNavigationMorphItemsByPageState.atom, new Map());
store.set(sidePanelNavigationStackState.atom, []);
resetNavigationMenuItemState();
store.set(selectedNavigationMenuItemIdInEditModeState.atom, null);
store.set(pendingInsertionNavigationMenuItemState.atom, null);
store.set(addToNavPayloadRegistryState.atom, new Map());
resetSelectedItem();
store.set(hasUserSelectedSidePanelListItemState.atom, false);
@@ -148,13 +137,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
);
}
},
[
closeDropdown,
resetContextStoreStates,
resetNavigationMenuItemState,
resetSelectedItem,
store,
],
[closeDropdown, resetSelectedItem, store],
);
return {
@@ -1,63 +0,0 @@
import { SidePanelContextRecordsChip } from '@/side-panel/components/SidePanelContextRecordsChip';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { SIDE_PANEL_RESET_CONTEXT_TO_SELECTION } from '@/side-panel/constants/SidePanelResetContextToSelection';
import { useResetPreviousSidePanelContext } from '@/side-panel/pages/root/hooks/useResetPreviousSidePanelContext';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowBackUp } from 'twenty-ui/display';
export const SidePanelResetContextToSelectionButton = () => {
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
);
const { resetPreviousSidePanelContext } = useResetPreviousSidePanelContext();
if (
!isDefined(objectMetadataItem) ||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
) {
return null;
}
return (
<SelectableListItem
itemId={SIDE_PANEL_RESET_CONTEXT_TO_SELECTION}
onEnter={resetPreviousSidePanelContext}
>
<CommandMenuItem
id={SIDE_PANEL_RESET_CONTEXT_TO_SELECTION}
Icon={IconArrowBackUp}
label={t`Reset to`}
RightComponent={
<SidePanelContextRecordsChip
objectMetadataItemId={objectMetadataItem.id}
instanceId={SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID}
/>
}
onClick={resetPreviousSidePanelContext}
/>
</SelectableListItem>
);
};
@@ -2,17 +2,11 @@ import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuCont
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { SIDE_PANEL_RESET_CONTEXT_TO_SELECTION } from '@/side-panel/constants/SidePanelResetContextToSelection';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { SidePanelResetContextToSelectionButton } from '@/side-panel/pages/root/components/SidePanelResetContextToSelectionButton';
import { useFilterCommandMenuItemsWithSidePanelSearch } from '@/side-panel/pages/root/hooks/useFilterCommandMenuItemsWithSidePanelSearch';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
export const SidePanelRootPage = () => {
@@ -64,30 +58,14 @@ export const SidePanelRootPage = () => {
const noResults =
!matchingRecordSelectionItems.length && !matchingGlobalItems.length;
// oxlint-disable-next-line twenty/matching-state-variable
const previousContextStoreCurrentObjectMetadataItemId =
useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
const selectableItemIds = [
...matchingRecordSelectionItems,
...matchingGlobalItems,
...(noResults ? fallbackItems : []),
].map((item) => item.id);
if (isDefined(previousContextStoreCurrentObjectMetadataItemId)) {
selectableItemIds.unshift(SIDE_PANEL_RESET_CONTEXT_TO_SELECTION);
}
return (
<SidePanelList selectableItemIds={selectableItemIds} noResults={noResults}>
{isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
<SidePanelGroup heading={t`Context`}>
<SidePanelResetContextToSelectionButton />
</SidePanelGroup>
)}
{matchingRecordSelectionItems.length > 0 && (
<SidePanelGroup heading={t`Record Selection`}>
{matchingRecordSelectionItems.map((item) => (
@@ -1,21 +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 { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndCommandMenuStates';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
export const useResetPreviousSidePanelContext = () => {
const { copyContextStoreStates } = useCopyContextStoreStates();
const { resetContextStoreStates } = useResetContextStoreStates();
const resetPreviousSidePanelContext = () => {
copyContextStoreStates({
instanceIdToCopyFrom: SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
instanceIdToCopyTo: SIDE_PANEL_COMPONENT_INSTANCE_ID,
});
resetContextStoreStates(SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID);
};
return {
resetPreviousSidePanelContext,
};
};