271 remove is command menu v2 enabled (#10809)

Closes https://github.com/twentyhq/core-team-issues/issues/271

This PR
- Removes the feature flag IS_COMMAND_MENU_V2_ENABLED
- Removes all old Right drawer components
- Removes the Action menu bar
- Removes unused Copilot page
This commit is contained in:
Raphaël Bosi
2025-03-12 16:26:29 +01:00
committed by GitHub
parent 1b0413bf8b
commit daa501549e
124 changed files with 281 additions and 4222 deletions

View File

@ -1,10 +1,7 @@
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { Key } from 'ts-key-enum';
import { Button, getOsControlSymbol } from 'twenty-ui';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const CmdEnterActionButton = ({
title,
@ -15,15 +12,10 @@ export const CmdEnterActionButton = ({
onClick: () => void;
disabled?: boolean;
}) => {
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
useScopedHotkeys(
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
() => onClick(),
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[onClick],
);

View File

@ -3,10 +3,8 @@ import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
@ -21,10 +19,6 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
contextStoreCurrentObjectMetadataItemComponentState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
@ -54,14 +48,9 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
},
}}
>
{isCommandMenuV2Enabled ? (
<>{!isMobile && <RecordIndexActionMenuButtons />}</>
) : (
<RecordIndexActionMenuBar />
)}
{!isMobile && <RecordIndexActionMenuButtons />}
<RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter />
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (

View File

@ -1,51 +0,0 @@
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
const StyledLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const RecordIndexActionMenuBar = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (contextStoreNumberOfSelectedRecords === 0) {
return null;
}
return (
<BottomBar
bottomBarId={getActionBarIdFromActionMenuId(actionMenuId)}
bottomBarHotkeyScopeFromParent={{ scope: ActionBarHotkeyScope.ActionBar }}
>
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} />
))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar>
);
};

View File

@ -1,76 +0,0 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
export const RecordIndexActionMenuEffect = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
const { openActionBar, closeActionBar } = useActionMenu(actionMenuId);
// Using closeActionBar here was causing a bug because it goes back to the
// previous hotkey scope, and we don't want that here.
const setIsBottomBarOpened = useSetRecoilComponentStateV2(
isBottomBarOpenedComponentState,
getActionBarIdFromActionMenuId(actionMenuId),
);
const isDropdownOpen = useRecoilValue(
extractComponentState(
isDropdownOpenComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuId),
),
);
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => {
if (
contextStoreNumberOfSelectedRecords > 0 &&
!isDropdownOpen &&
!isRightDrawerOpen &&
!isCommandMenuOpened
) {
// We only handle opening the ActionMenuBar here, not the Dropdown.
// The Dropdown is already managed by sync handlers for events like
// right-click to open and click outside to close.
openActionBar();
}
if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) {
closeActionBar();
}
}, [
contextStoreNumberOfSelectedRecords,
openActionBar,
closeActionBar,
isDropdownOpen,
isRightDrawerOpen,
isCommandMenuOpened,
]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
setIsBottomBarOpened(false);
}
}, [isRightDrawerOpen, isCommandMenuOpened, setIsBottomBarOpened]);
return null;
};

View File

@ -5,40 +5,19 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen
import { RecordShowActionMenuButtons } from '@/action-menu/components/RecordShowActionMenuButtons';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
export const RecordShowActionMenu = ({
isFavorite,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}: {
isFavorite: boolean;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
handleFavoriteButtonClick: () => void;
}) => {
export const RecordShowActionMenu = () => {
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
<>
{contextStoreCurrentObjectMetadataItem && (
@ -48,19 +27,7 @@ export const RecordShowActionMenu = ({
onActionExecutedCallback: () => {},
}}
>
{isCommandMenuV2Enabled ? (
<RecordShowActionMenuButtons />
) : (
<RecordShowPageBaseHeader
{...{
isFavorite,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}}
/>
)}
<RecordShowActionMenuButtons />
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
<RecordAgnosticActionMenuEntriesSetter />

View File

@ -6,17 +6,14 @@ import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-men
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { i18n } from '@lingui/core';
import { Key } from 'ts-key-enum';
import { Button, MenuItem, getOsControlSymbol } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { Button, getOsControlSymbol, MenuItem } from 'twenty-ui';
export const RightDrawerActionMenuDropdown = () => {
const actionMenuEntries = useRecoilComponentValueV2(
@ -31,10 +28,6 @@ export const RightDrawerActionMenuDropdown = () => {
const theme = useTheme();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
useScopedHotkeys(
[Key.Escape, 'ctrl+o,meta+o'],
() => {
@ -53,9 +46,7 @@ export const RightDrawerActionMenuDropdown = () => {
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId),
);
},
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[openDropdown],
);

View File

@ -1,141 +0,0 @@
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { msg } from '@lingui/core/macro';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/test';
import { RecoilRoot } from 'recoil';
import { IconTrash, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const deleteMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar,
decorators: [
RouterDecorator,
I18nFrontDecorator,
(Story) => (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily(
{
instanceId: 'story-action-menu',
},
),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: msg`Delete`,
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId:
getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
),
],
args: {
actionMenuId: 'story-action-menu',
},
};
export default meta;
type Story = StoryObj<typeof RecordIndexActionMenuBar>;
export const Default: Story = {
args: {
actionMenuId: 'story-action-menu',
},
};
export const WithCustomSelection: Story = {
args: {
actionMenuId: 'story-action-menu',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const selectionText = await canvas.findByText('3 selected:');
expect(selectionText).toBeInTheDocument();
},
};
export const WithButtonClicks: Story = {
args: {
actionMenuId: 'story-action-menu',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton);
await waitFor(() => {
expect(deleteMock).toHaveBeenCalled();
});
},
};