Migrate right drawer record page to the command menu (#9459)

Closes #9423



https://github.com/user-attachments/assets/0d93f170-8c4f-43ff-a0ca-3d2874d44820
This commit is contained in:
Raphaël Bosi
2025-01-09 09:58:14 +01:00
committed by GitHub
parent e0e436a51d
commit a2f2f4148a
37 changed files with 400 additions and 232 deletions

View File

@ -392,6 +392,7 @@ export enum FeatureFlagKey {
IsAggregateQueryEnabled = 'IsAggregateQueryEnabled', IsAggregateQueryEnabled = 'IsAggregateQueryEnabled',
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled', IsCopilotEnabled = 'IsCopilotEnabled',
IsCrmMigrationEnabled = 'IsCrmMigrationEnabled', IsCrmMigrationEnabled = 'IsCrmMigrationEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled',
@ -400,7 +401,6 @@ export enum FeatureFlagKey {
IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled',
IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled',
IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled',
IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled',
IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsSsoEnabled = 'IsSSOEnabled', IsSsoEnabled = 'IsSSOEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',

View File

@ -324,6 +324,7 @@ export enum FeatureFlagKey {
IsAggregateQueryEnabled = 'IsAggregateQueryEnabled', IsAggregateQueryEnabled = 'IsAggregateQueryEnabled',
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled', IsCopilotEnabled = 'IsCopilotEnabled',
IsCrmMigrationEnabled = 'IsCrmMigrationEnabled', IsCrmMigrationEnabled = 'IsCrmMigrationEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled',
@ -332,7 +333,6 @@ export enum FeatureFlagKey {
IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled',
IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled',
IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled',
IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled',
IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsSsoEnabled = 'IsSSOEnabled', IsSsoEnabled = 'IsSSOEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',

View File

@ -35,8 +35,8 @@ export const RecordActionMenuEntriesSetter = () => {
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
if ( if (
@ -53,7 +53,7 @@ export const RecordActionMenuEntriesSetter = () => {
const actionConfig = getActionConfig( const actionConfig = getActionConfig(
objectMetadataItem, objectMetadataItem,
isPageHeaderV2Enabled, isCommandMenuV2Enabled,
); );
const actionsToRegister = isDefined(viewType) const actionsToRegister = isDefined(viewType)

View File

@ -8,7 +8,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActionConfig = ( export const getActionConfig = (
objectMetadataItem: ObjectMetadataItem, objectMetadataItem: ObjectMetadataItem,
isPageHeaderV2Enabled: boolean, isCommandMenuV2Enabled: boolean,
) => { ) => {
switch (objectMetadataItem.nameSingular) { switch (objectMetadataItem.nameSingular) {
case CoreObjectNameSingular.Workflow: case CoreObjectNameSingular.Workflow:
@ -18,7 +18,7 @@ export const getActionConfig = (
case CoreObjectNameSingular.WorkflowRun: case CoreObjectNameSingular.WorkflowRun:
return WORKFLOW_RUNS_ACTIONS_CONFIG; return WORKFLOW_RUNS_ACTIONS_CONFIG;
default: default:
return isPageHeaderV2Enabled return isCommandMenuV2Enabled
? DEFAULT_ACTIONS_CONFIG_V2 ? DEFAULT_ACTIONS_CONFIG_V2
: DEFAULT_ACTIONS_CONFIG_V1; : DEFAULT_ACTIONS_CONFIG_V1;
} }

View File

@ -25,8 +25,8 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -54,7 +54,7 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
}, },
}} }}
> >
{isPageHeaderV2Enabled ? ( {isCommandMenuV2Enabled ? (
<>{!isMobile && <RecordIndexActionMenuButtons />}</> <>{!isMobile && <RecordIndexActionMenuButtons />}</>
) : ( ) : (
<RecordIndexActionMenuBar /> <RecordIndexActionMenuBar />

View File

@ -33,8 +33,8 @@ export const RecordShowActionMenu = ({
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
// TODO: refactor RecordShowPageBaseHeader to use the context store // TODO: refactor RecordShowPageBaseHeader to use the context store
@ -48,7 +48,7 @@ export const RecordShowActionMenu = ({
onActionExecutedCallback: () => {}, onActionExecutedCallback: () => {},
}} }}
> >
{isPageHeaderV2Enabled ? ( {isCommandMenuV2Enabled ? (
<RecordShowActionMenuButtons /> <RecordShowActionMenuButtons />
) : ( ) : (
<RecordShowPageBaseHeader <RecordShowPageBaseHeader

View File

@ -26,9 +26,12 @@ import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task'; import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import '@blocknote/core/fonts/inter.css'; import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
import '@blocknote/react/style.css'; import '@blocknote/react/style.css';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type ActivityRichTextEditorProps = { type ActivityRichTextEditorProps = {
activityId: string; activityId: string;
@ -46,6 +49,10 @@ export const ActivityRichTextEditor = ({
const cache = useApolloClient().cache; const cache = useApolloClient().cache;
const activity = activityInStore as Task | Note | null; const activity = activityInStore as Task | Note | null;
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { objectMetadataItem: objectMetadataItemActivity } = const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({ useObjectMetadataItem({
objectNameSingular: activityObjectNameSingular, objectNameSingular: activityObjectNameSingular,
@ -245,7 +252,9 @@ export const ActivityRichTextEditor = ({
editor.setTextCursorPosition(newBlockId, 'end'); editor.setTextCursorPosition(newBlockId, 'end');
editor.focus(); editor.focus();
}, },
RightDrawerHotkeyScope.RightDrawer, isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
[], [],
{ {
preventDefault: false, preventDefault: false,

View File

@ -7,6 +7,9 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useOpenActivityRightDrawer = ({ export const useOpenActivityRightDrawer = ({
objectNameSingular, objectNameSingular,
@ -25,6 +28,10 @@ export const useOpenActivityRightDrawer = ({
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (activityId: string) => { return (activityId: string) => {
if ( if (
isRightDrawerOpen && isRightDrawerOpen &&
@ -34,7 +41,12 @@ export const useOpenActivityRightDrawer = ({
return; return;
} }
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); if (isCommandMenuV2Enabled) {
setHotkeyScope(AppHotkeyScope.CommandMenuOpen, { goto: false });
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
}
setViewableRecordId(activityId); setViewableRecordId(activityId);
setViewableRecordNameSingular(objectNameSingular); setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord); openRightDrawer(RightDrawerPages.ViewRecord);

View File

@ -17,6 +17,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawer = ({ export const useOpenCreateActivityDrawer = ({
@ -60,6 +63,10 @@ export const useOpenCreateActivityDrawer = ({
isUpsertingActivityInDBState, isUpsertingActivityInDBState,
); );
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const openCreateActivityDrawer = async ({ const openCreateActivityDrawer = async ({
targetableObjects, targetableObjects,
customAssignee, customAssignee,
@ -108,7 +115,12 @@ export const useOpenCreateActivityDrawer = ({
setActivityTargetableEntityArray([]); setActivityTargetableEntityArray([]);
} }
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); if (isCommandMenuV2Enabled) {
setHotkeyScope(AppHotkeyScope.CommandMenuOpen, { goto: false });
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
}
setViewableRecordId(activity.id); setViewableRecordId(activity.id);
setIsUpsertingActivityInDB(false); setIsUpsertingActivityInDB(false);

View File

@ -1,23 +1,19 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup'; import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands'; import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { Command } from '@/command-menu/types/Command'; import { Command } from '@/command-menu/types/Command';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRef } from 'react'; import { useRecoilValue } from 'recoil';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
const MOBILE_NAVIGATION_BAR_HEIGHT = 64; const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
@ -27,21 +23,6 @@ type CommandGroupConfig = {
items?: Command[]; items?: Command[];
}; };
const StyledCommandMenu = styled.div`
background: ${({ theme }) => theme.background.secondary};
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
font-family: ${({ theme }) => theme.font.family};
height: 100%;
overflow: hidden;
padding: 0;
position: fixed;
right: 0%;
top: 0%;
width: ${() => (useIsMobile() ? '100%' : '500px')};
z-index: 1000;
`;
const StyledList = styled.div` const StyledList = styled.div`
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
overscroll-behavior: contain; overscroll-behavior: contain;
@ -75,24 +56,12 @@ const StyledEmpty = styled.div`
`; `;
export const CommandMenu = () => { export const CommandMenu = () => {
const { onItemClick, closeCommandMenu } = useCommandMenu(); const { onItemClick } = useCommandMenuOnItemClick();
const commandMenuRef = useRef<HTMLDivElement>(null);
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState( const commandMenuSearch = useRecoilValue(commandMenuSearchState);
commandMenuSearchState,
);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
useCommandMenuHotKeys();
useListenClickOutside({
refs: [commandMenuRef],
callback: closeCommandMenu,
listenerId: 'COMMAND_MENU_LISTENER_ID',
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
});
const { const {
isNoResults, isNoResults,
isLoading, isLoading,
@ -184,74 +153,66 @@ export const CommandMenu = () => {
<CommandMenuDefaultSelectionEffect <CommandMenuDefaultSelectionEffect
selectableItemIds={selectableItemIds} selectableItemIds={selectableItemIds}
/> />
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
<CommandMenuTopBar
commandMenuSearch={commandMenuSearch}
setCommandMenuSearch={setCommandMenuSearch}
/>
<StyledList>
<ScrollWrapper
contextProviderName="commandMenu"
componentInstanceId={`scroll-wrapper-command-menu`}
>
<StyledInnerList isMobile={isMobile}>
<SelectableList
selectableListId="command-menu-list"
selectableItemIdArray={selectableItemIds}
hotkeyScope={AppHotkeyScope.CommandMenu}
onEnter={(itemId) => {
const command = selectableItems.find(
(item) => item.id === itemId,
);
if (isDefined(command)) { <StyledList>
const { <ScrollWrapper
to, contextProviderName="commandMenu"
onCommandClick, componentInstanceId={`scroll-wrapper-command-menu`}
shouldCloseCommandMenuOnClick, >
} = command; <StyledInnerList isMobile={isMobile}>
<SelectableList
selectableListId="command-menu-list"
selectableItemIdArray={selectableItemIds}
hotkeyScope={AppHotkeyScope.CommandMenu}
onEnter={(itemId) => {
const command = selectableItems.find(
(item) => item.id === itemId,
);
onItemClick({ if (isDefined(command)) {
shouldCloseCommandMenuOnClick, const { to, onCommandClick, shouldCloseCommandMenuOnClick } =
onClick: onCommandClick, command;
to,
}); onItemClick({
} shouldCloseCommandMenuOnClick,
}} onClick: onCommandClick,
> to,
{isNoResults && !isLoading && ( });
<StyledEmpty>No results found</StyledEmpty> }
)} }}
{commandGroups.map(({ heading, items }) => >
items?.length ? ( {isNoResults && !isLoading && (
<CommandGroup heading={heading} key={heading}> <StyledEmpty>No results found</StyledEmpty>
{items.map((item) => { )}
return ( {commandGroups.map(({ heading, items }) =>
<SelectableItem itemId={item.id} key={item.id}> items?.length ? (
<CommandMenuItem <CommandGroup heading={heading} key={heading}>
key={item.id} {items.map((item) => {
id={item.id} return (
Icon={item.Icon} <SelectableItem itemId={item.id} key={item.id}>
label={item.label} <CommandMenuItem
to={item.to} key={item.id}
onClick={item.onCommandClick} id={item.id}
firstHotKey={item.firstHotKey} Icon={item.Icon}
secondHotKey={item.secondHotKey} label={item.label}
shouldCloseCommandMenuOnClick={ to={item.to}
item.shouldCloseCommandMenuOnClick onClick={item.onCommandClick}
} firstHotKey={item.firstHotKey}
/> secondHotKey={item.secondHotKey}
</SelectableItem> shouldCloseCommandMenuOnClick={
); item.shouldCloseCommandMenuOnClick
})} }
</CommandGroup> />
) : null, </SelectableItem>
)} );
</SelectableList> })}
</StyledInnerList> </CommandGroup>
</ScrollWrapper> ) : null,
</StyledList> )}
</StyledCommandMenu> </SelectableList>
</StyledInnerList>
</ScrollWrapper>
</StyledList>
</> </>
); );
}; };

View File

@ -3,35 +3,56 @@ import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useIsMobile } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
export const CommandMenuContainer = () => { const StyledCommandMenu = styled.div`
const { toggleCommandMenu } = useCommandMenu(); background: ${({ theme }) => theme.background.secondary};
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); border-left: 1px solid ${({ theme }) => theme.border.color.medium};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
font-family: ${({ theme }) => theme.font.family};
height: 100%;
overflow: hidden;
padding: 0;
position: fixed;
right: 0%;
top: 0%;
width: ${() => (useIsMobile() ? '100%' : '500px')};
z-index: 30;
`;
export const CommandMenuContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
const isWorkflowEnabled = useIsFeatureEnabled( const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useScopedHotkeys( const commandMenuRef = useRef<HTMLDivElement>(null);
'ctrl+k,meta+k',
() => { useCommandMenuHotKeys();
closeKeyboardShortcutMenu();
toggleCommandMenu(); useListenClickOutside({
}, refs: [commandMenuRef],
AppHotkeyScope.CommandMenu, callback: closeCommandMenu,
[toggleCommandMenu], listenerId: 'COMMAND_MENU_LISTENER_ID',
); hotkeyScope: AppHotkeyScope.CommandMenuOpen,
});
return ( return (
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
@ -49,7 +70,11 @@ export const CommandMenuContainer = () => {
<RecordActionMenuEntriesSetter /> <RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />} {isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<ActionMenuConfirmationModals /> <ActionMenuConfirmationModals />
{isCommandMenuOpened && <CommandMenu />} {isCommandMenuOpened && (
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
{children}
</StyledCommandMenu>
)}
</ActionMenuContext.Provider> </ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider> </ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider> </ContextStoreComponentInstanceContext.Provider>

View File

@ -2,10 +2,9 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconArrowUpRight, IconComponent, MenuItemCommand } from 'twenty-ui'; import { IconArrowUpRight, IconComponent, MenuItemCommand } from 'twenty-ui';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useCommandMenu } from '../hooks/useCommandMenu';
export type CommandMenuItemProps = { export type CommandMenuItemProps = {
label: string; label: string;
to?: string; to?: string;
@ -27,7 +26,7 @@ export const CommandMenuItem = ({
secondHotKey, secondHotKey,
shouldCloseCommandMenuOnClick, shouldCloseCommandMenuOnClick,
}: CommandMenuItemProps) => { }: CommandMenuItemProps) => {
const { onItemClick } = useCommandMenu(); const { onItemClick } = useCommandMenuOnItemClick();
if (isNonEmptyString(to) && !Icon) { if (isNonEmptyString(to) && !Icon) {
Icon = IconArrowUpRight; Icon = IconArrowUpRight;

View File

@ -0,0 +1,4 @@
export enum CommandMenuPages {
Root = 'root',
ViewRecord = 'view-record',
}

View File

@ -0,0 +1,23 @@
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const CommandMenuRouter = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuPageComponent = isDefined(commandMenuPage) ? (
COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage)
) : (
<></>
);
return (
<CommandMenuContainer>
<CommandMenuTopBar />
{commandMenuPageComponent}
</CommandMenuContainer>
);
};

View File

@ -2,9 +2,11 @@ import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandM
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui'; import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui';
const StyledInputContainer = styled.div` const StyledInputContainer = styled.div`
@ -50,15 +52,11 @@ const StyledCloseButtonContainer = styled.div`
justify-content: center; justify-content: center;
`; `;
type CommandMenuTopBarProps = { export const CommandMenuTopBar = () => {
commandMenuSearch: string; const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
setCommandMenuSearch: (search: string) => void; commandMenuSearchState,
}; );
export const CommandMenuTopBar = ({
commandMenuSearch,
setCommandMenuSearch,
}: CommandMenuTopBarProps) => {
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCommandMenuSearch(event.target.value); setCommandMenuSearch(event.target.value);
}; };

View File

@ -16,6 +16,7 @@ import {
import { sleep } from '~/utils/sleep'; import { sleep } from '~/utils/sleep';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
@ -43,7 +44,7 @@ const ContextStoreDecorator: Decorator = (Story) => {
const meta: Meta<typeof CommandMenu> = { const meta: Meta<typeof CommandMenu> = {
title: 'Modules/CommandMenu/CommandMenu', title: 'Modules/CommandMenu/CommandMenu',
component: CommandMenu, component: CommandMenuRouter,
decorators: [ decorators: [
(Story) => { (Story) => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);

View File

@ -0,0 +1,11 @@
import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
export const COMMAND_MENU_PAGES_CONFIG = new Map<
CommandMenuPages,
React.ReactNode
>([
[CommandMenuPages.Root, <CommandMenu />],
[CommandMenuPages.ViewRecord, <RightDrawerRecord />],
]);

View File

@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil'; import { RecoilRoot, useRecoilValue } from 'recoil';
@ -69,20 +69,4 @@ describe('useCommandMenu', () => {
expect(result.current.isCommandMenuOpened).toBe(false); expect(result.current.isCommandMenuOpened).toBe(false);
}); });
it('onItemClick', () => {
const { result } = renderHooks();
const onClickMock = jest.fn();
act(() => {
result.current.commandMenu.onItemClick({
shouldCloseCommandMenuOnClick: true,
onClick: onClickMock,
to: '/test',
});
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -0,0 +1,54 @@
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useCommandMenuOnItemClick } from '../useCommandMenuOnItemClick';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
{children}
</MemoryRouter>
</RecoilRoot>
);
const renderHooks = () => {
const { result } = renderHook(
() => {
const { onItemClick } = useCommandMenuOnItemClick();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
return {
onItemClick,
isCommandMenuOpened,
};
},
{
wrapper: Wrapper,
},
);
return { result };
};
describe('useCommandMenuOnItemClick', () => {
it('onItemClick', () => {
const { result } = renderHooks();
const onClickMock = jest.fn();
act(() => {
result.current.onItemClick({
shouldCloseCommandMenuOnClick: true,
onClick: onClickMock,
to: '/test',
});
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,6 +1,3 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
@ -10,6 +7,8 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
@ -17,10 +16,10 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
export const useCommandMenu = () => { export const useCommandMenu = () => {
const navigate = useNavigate();
const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState);
const { resetSelectedItem } = useSelectableList('command-menu-list'); const { resetSelectedItem } = useSelectableList('command-menu-list');
const { const {
@ -212,6 +211,8 @@ export const useCommandMenu = () => {
); );
if (isCommandMenuOpened) { if (isCommandMenuOpened) {
set(viewableRecordIdState, null);
set(commandMenuPageState, CommandMenuPages.Root);
setIsCommandMenuOpened(false); setIsCommandMenuOpened(false);
resetSelectedItem(); resetSelectedItem();
goBackToPreviousHotkeyScope(); goBackToPreviousHotkeyScope();
@ -238,39 +239,21 @@ export const useCommandMenu = () => {
[closeCommandMenu, openCommandMenu], [closeCommandMenu, openCommandMenu],
); );
const onItemClick = useCallback( const openRecordInCommandMenu = useRecoilCallback(
({ ({ set }) => {
shouldCloseCommandMenuOnClick, return (recordId: string) => {
onClick, openCommandMenu();
to, set(commandMenuPageState, CommandMenuPages.ViewRecord);
}: { set(viewableRecordIdState, recordId);
shouldCloseCommandMenuOnClick?: boolean; };
onClick?: () => void;
to?: string;
}) => {
if (
isDefined(shouldCloseCommandMenuOnClick) &&
shouldCloseCommandMenuOnClick
) {
toggleCommandMenu();
}
if (isDefined(onClick)) {
onClick();
return;
}
if (isNonEmptyString(to)) {
navigate(to);
return;
}
}, },
[navigate, toggleCommandMenu], [openCommandMenu],
); );
return { return {
openCommandMenu, openCommandMenu,
closeCommandMenu, closeCommandMenu,
openRecordInCommandMenu,
toggleCommandMenu, toggleCommandMenu,
onItemClick,
}; };
}; };

View File

@ -2,6 +2,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -10,7 +11,7 @@ import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
export const useCommandMenuHotKeys = () => { export const useCommandMenuHotKeys = () => {
const { closeCommandMenu } = useCommandMenu(); const { closeCommandMenu, toggleCommandMenu } = useCommandMenu();
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
@ -24,6 +25,18 @@ export const useCommandMenuHotKeys = () => {
'command-menu', 'command-menu',
); );
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
useScopedHotkeys(
'ctrl+k,meta+k',
() => {
closeKeyboardShortcutMenu();
toggleCommandMenu();
},
AppHotkeyScope.CommandMenu,
[toggleCommandMenu],
);
useScopedHotkeys( useScopedHotkeys(
[Key.Escape], [Key.Escape],
() => { () => {

View File

@ -0,0 +1,41 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useCommandMenuOnItemClick = () => {
const { toggleCommandMenu } = useCommandMenu();
const navigate = useNavigate();
const onItemClick = useCallback(
({
shouldCloseCommandMenuOnClick,
onClick,
to,
}: {
shouldCloseCommandMenuOnClick?: boolean;
onClick?: () => void;
to?: string;
}) => {
if (
isDefined(shouldCloseCommandMenuOnClick) &&
shouldCloseCommandMenuOnClick
) {
toggleCommandMenu();
}
if (isDefined(onClick)) {
onClick();
return;
}
if (isNonEmptyString(to)) {
navigate(to);
return;
}
},
[navigate, toggleCommandMenu],
);
return { onItemClick };
};

View File

@ -0,0 +1,7 @@
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { createState } from '@ui/utilities/state/utils/createState';
export const commandMenuPageState = createState<CommandMenuPages>({
key: 'command-menu/commandMenuPageState',
defaultValue: CommandMenuPages.Root,
});

View File

@ -13,13 +13,13 @@ export const PageFavoriteButton = ({
}: PageFavoriteButtonProps) => { }: PageFavoriteButtonProps) => {
const title = isFavorite ? 'Remove from favorites' : 'Add to favorites'; const title = isFavorite ? 'Remove from favorites' : 'Add to favorites';
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
return ( return (
<> <>
{isPageHeaderV2Enabled ? ( {isCommandMenuV2Enabled ? (
<Button <Button
Icon={isFavorite ? IconHeartOff : IconHeart} Icon={isFavorite ? IconHeartOff : IconHeart}
dataTestId="favorite-button" dataTestId="favorite-button"

View File

@ -162,8 +162,8 @@ export const RecordIndexContainer = () => {
contextStoreTargetedRecordsRuleComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
return ( return (
@ -256,7 +256,7 @@ export const RecordIndexContainer = () => {
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} /> <RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
</StyledContainerWithPadding> </StyledContainerWithPadding>
)} )}
{!isPageHeaderV2Enabled && ( {!isCommandMenuV2Enabled && (
<RecordIndexActionMenu indexId={recordIndexId} /> <RecordIndexActionMenu indexId={recordIndexId} />
)} )}
</RecordFieldValueSelectorContextProvider> </RecordFieldValueSelectorContextProvider>

View File

@ -36,8 +36,8 @@ export const RecordIndexPageHeader = () => {
contextStoreNumberOfSelectedRecordsComponentState, contextStoreNumberOfSelectedRecordsComponentState,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
const isObjectMetadataItemReadOnly = const isObjectMetadataItemReadOnly =
@ -45,7 +45,7 @@ export const RecordIndexPageHeader = () => {
isObjectMetadataReadOnly(objectMetadataItem); isObjectMetadataReadOnly(objectMetadataItem);
const shouldDisplayAddButton = const shouldDisplayAddButton =
(numberOfSelectedRecords === 0 || !isPageHeaderV2Enabled) && (numberOfSelectedRecords === 0 || !isCommandMenuV2Enabled) &&
!isObjectMetadataItemReadOnly; !isObjectMetadataItemReadOnly;
const isTable = recordIndexViewType === ViewType.Table; const isTable = recordIndexViewType === ViewType.Table;
@ -65,7 +65,7 @@ export const RecordIndexPageHeader = () => {
<RecordIndexPageKanbanAddButton /> <RecordIndexPageKanbanAddButton />
))} ))}
{isPageHeaderV2Enabled && ( {isCommandMenuV2Enabled && (
<> <>
<RecordIndexActionMenu indexId={recordIndexId} /> <RecordIndexActionMenu indexId={recordIndexId} />
<PageHeaderOpenCommandMenuButton /> <PageHeaderOpenCommandMenuButton />

View File

@ -10,14 +10,22 @@ import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordSh
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledRightDrawerRecord = styled.div` const StyledRightDrawerRecord = styled.div<{ hasTopBar: boolean }>`
height: ${({ theme }) => height: ${({ theme, hasTopBar }) =>
useIsMobile() ? `calc(100% - ${theme.spacing(16)})` : '100%'}; hasTopBar ? `calc(100% - ${theme.spacing(16)})` : '100%'};
`; `;
export const RightDrawerRecord = () => { export const RightDrawerRecord = () => {
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isMobile = useIsMobile();
const hasTopBar = isCommandMenuV2Enabled || isMobile;
const viewableRecordNameSingular = useRecoilValue( const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState, viewableRecordNameSingularState,
); );
@ -48,7 +56,7 @@ export const RightDrawerRecord = () => {
<ActionMenuComponentInstanceContext.Provider <ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{ instanceId: `record-show-${objectRecordId}` }}
> >
<StyledRightDrawerRecord> <StyledRightDrawerRecord hasTopBar={hasTopBar}>
<RecordFieldValueSelectorContextProvider> <RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && ( {!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} /> <RecordValueSetterEffect recordId={objectRecordId} />

View File

@ -212,7 +212,10 @@ export const RecordDetailRelationSection = ({
<> <>
<RelationFromManyFieldInputMultiRecordsEffect /> <RelationFromManyFieldInputMultiRecordsEffect />
<MultiRecordSelect <MultiRecordSelect
onCreate={createNewRecordAndOpenRightDrawer} onCreate={() => {
closeDropdown();
createNewRecordAndOpenRightDrawer?.();
}}
onChange={updateRelation} onChange={updateRelation}
onSubmit={closeDropdown} onSubmit={closeDropdown}
dropdownPlacement={dropdownPlacement} dropdownPlacement={dropdownPlacement}

View File

@ -7,15 +7,15 @@ import { FeatureFlagKey } from '~/generated/graphql';
export const PageHeaderOpenCommandMenuButton = () => { export const PageHeaderOpenCommandMenuButton = () => {
const { openCommandMenu } = useCommandMenu(); const { openCommandMenu } = useCommandMenu();
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
<> <>
{isPageHeaderV2Enabled ? ( {isCommandMenuV2Enabled ? (
<Button <Button
Icon={IconDotsVertical} Icon={IconDotsVertical}
dataTestId="page-header-open-command-menu-button" dataTestId="page-header-open-command-menu-button"

View File

@ -1,5 +1,5 @@
import { AuthModal } from '@/auth/components/AuthModal'; import { AuthModal } from '@/auth/components/AuthModal';
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer'; import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu'; import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer'; import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
@ -82,7 +82,7 @@ export const DefaultLayout = () => {
<StyledLayout> <StyledLayout>
{!showAuthModal && ( {!showAuthModal && (
<> <>
<CommandMenuContainer /> <CommandMenuRouter />
<KeyboardShortcutMenu /> <KeyboardShortcutMenu />
</> </>
)} )}

View File

@ -7,14 +7,14 @@ type PageAddButtonProps = {
}; };
export const PageAddButton = ({ onClick }: PageAddButtonProps) => { export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
<> <>
{isPageHeaderV2Enabled ? ( {isCommandMenuV2Enabled ? (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
dataTestId="add-button" dataTestId="add-button"

View File

@ -111,8 +111,8 @@ export const PageHeader = ({
isNavigationDrawerExpandedState, isNavigationDrawerExpandedState,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
return ( return (
@ -133,7 +133,7 @@ export const PageHeader = ({
)} )}
<StyledTopBarIconStyledTitleContainer> <StyledTopBarIconStyledTitleContainer>
{!isPageHeaderV2Enabled && hasPaginationButtons && ( {!isCommandMenuV2Enabled && hasPaginationButtons && (
<> <>
<IconButton <IconButton
Icon={IconChevronUp} Icon={IconChevronUp}

View File

@ -3,10 +3,15 @@ import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages'; import { RightDrawerPages } from '../types/RightDrawerPages';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
export const useRightDrawer = () => { export const useRightDrawer = () => {
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
@ -14,14 +19,29 @@ export const useRightDrawer = () => {
const rightDrawerPage = useRecoilValue(rightDrawerPageState); const rightDrawerPage = useRecoilValue(rightDrawerPageState);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { openCommandMenu } = useCommandMenu();
const openRightDrawer = useRecoilCallback( const openRightDrawer = useRecoilCallback(
({ set }) => ({ set }) =>
(rightDrawerPage: RightDrawerPages) => { (rightDrawerPage: RightDrawerPages) => {
if (
isCommandMenuV2Enabled &&
rightDrawerPage === RightDrawerPages.ViewRecord
) {
set(commandMenuPageState, CommandMenuPages.ViewRecord);
openCommandMenu();
return;
}
set(rightDrawerPageState, rightDrawerPage); set(rightDrawerPageState, rightDrawerPage);
set(isRightDrawerOpenState, true); set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, false); set(isRightDrawerMinimizedState, false);
}, },
[], [isCommandMenuV2Enabled, openCommandMenu],
); );
const closeRightDrawer = useRecoilCallback( const closeRightDrawer = useRecoilCallback(

View File

@ -53,8 +53,8 @@ export const ShowPageAddButton = ({
closeDropdown(); closeDropdown();
}; };
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
if ( if (
@ -72,7 +72,7 @@ export const ShowPageAddButton = ({
<Dropdown <Dropdown
dropdownId={SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID} dropdownId={SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID}
clickableComponent={ clickableComponent={
isPageHeaderV2Enabled ? ( isCommandMenuV2Enabled ? (
<Button <Button
Icon={IconPlus} Icon={IconPlus}
dataTestId="add-button" dataTestId="add-button"

View File

@ -40,8 +40,8 @@ export const RecordShowPage = () => {
parameters.objectRecordId ?? '', parameters.objectRecordId ?? '',
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
); );
return ( return (
@ -63,18 +63,18 @@ export const RecordShowPage = () => {
headerIcon={headerIcon} headerIcon={headerIcon}
> >
<> <>
{!isPageHeaderV2Enabled && {!isCommandMenuV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && ( objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} /> <RecordShowPageWorkflowHeader workflowId={objectRecordId} />
)} )}
{!isPageHeaderV2Enabled && {!isCommandMenuV2Enabled &&
objectNameSingular === objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && ( CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader <RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId} workflowVersionId={objectRecordId}
/> />
)} )}
{(isPageHeaderV2Enabled || {(isCommandMenuV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow && (objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !== objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && ( CoreObjectNameSingular.WorkflowVersion)) && (

View File

@ -81,7 +81,7 @@ export const seedFeatureFlags = async (
value: true, value: true,
}, },
{ {
key: FeatureFlagKey.IsPageHeaderV2Enabled, key: FeatureFlagKey.IsCommandMenuV2Enabled,
workspaceId: workspaceId, workspaceId: workspaceId,
value: true, value: true,
}, },

View File

@ -15,7 +15,7 @@ export enum FeatureFlagKey {
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED', IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED',
IsViewGroupsEnabled = 'IS_VIEW_GROUPS_ENABLED', IsViewGroupsEnabled = 'IS_VIEW_GROUPS_ENABLED',
IsPageHeaderV2Enabled = 'IS_PAGE_HEADER_V2_ENABLED', IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED',
IsCrmMigrationEnabled = 'IS_CRM_MIGRATION_ENABLED', IsCrmMigrationEnabled = 'IS_CRM_MIGRATION_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED', IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
} }