Raphaël Bosi
2025-02-05 14:25:29 +01:00
committed by GitHub
parent 36d148d5e5
commit 5c24cf4084
8 changed files with 183 additions and 42 deletions

View File

@ -1,4 +1,5 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconLayoutSidebarRightExpand, getOsControlSymbol } from 'twenty-ui'; import { IconLayoutSidebarRightExpand, getOsControlSymbol } from 'twenty-ui';
@ -38,11 +39,17 @@ const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
export const RecordIndexActionMenuBarAllActionsButton = () => { export const RecordIndexActionMenuBarAllActionsButton = () => {
const theme = useTheme(); const theme = useTheme();
const { openCommandMenu } = useCommandMenu(); const { navigateCommandMenu } = useCommandMenu();
return ( return (
<> <>
<StyledSeparator size="md" /> <StyledSeparator size="md" />
<StyledButton onClick={() => openCommandMenu()}> <StyledButton
onClick={() =>
navigateCommandMenu({
page: CommandMenuPages.Root,
})
}
>
<IconLayoutSidebarRightExpand size={theme.icon.size.md} /> <IconLayoutSidebarRightExpand size={theme.icon.size.md} />
<StyledButtonLabel>All Actions</StyledButtonLabel> <StyledButtonLabel>All Actions</StyledButtonLabel>
<StyledSeparator size="sm" /> <StyledSeparator size="sm" />

View File

@ -1,4 +1,5 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter'; import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter'; import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey'; import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
@ -91,7 +92,10 @@ export const CommandMenuContainer = ({
value={{ value={{
isInRightDrawer: false, isInRightDrawer: false,
onActionExecutedCallback: ({ key }) => { onActionExecutedCallback: ({ key }) => {
if (key !== RecordAgnosticActionsKey.SEARCH_RECORDS) { if (
key !== RecordAgnosticActionsKey.SEARCH_RECORDS &&
key !== NoSelectionRecordActionKeys.CREATE_NEW_RECORD
) {
toggleCommandMenu(); toggleCommandMenu();
} }
}, },

View File

@ -1,7 +1,12 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime'; import { Fragment } from 'react/jsx-runtime';
import { isDefined } from 'twenty-shared';
const StyledChip = styled.div` const StyledChip = styled.button<{
withText: boolean;
onClick?: () => void;
}>`
align-items: center; align-items: center;
background: ${({ theme }) => theme.background.transparent.light}; background: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
@ -10,11 +15,21 @@ const StyledChip = styled.div`
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(6)}; height: ${({ theme }) => theme.spacing(6)};
padding: 0 ${({ theme }) => theme.spacing(2)}; /* If the chip has text, we add extra padding to have a more balanced design */
padding: 0
${({ theme, withText }) => (withText ? theme.spacing(2) : theme.spacing(1))};
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg}; line-height: ${({ theme }) => theme.text.lineHeight.lg};
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
&:hover {
background: ${({ onClick, theme }) =>
isDefined(onClick)
? theme.background.transparent.medium
: theme.background.transparent.light};
}
`; `;
const StyledIconsContainer = styled.div` const StyledIconsContainer = styled.div`
@ -24,18 +39,26 @@ const StyledIconsContainer = styled.div`
export const CommandMenuContextChip = ({ export const CommandMenuContextChip = ({
Icons, Icons,
text, text,
onClick,
testId,
}: { }: {
Icons: React.ReactNode[]; Icons: React.ReactNode[];
text?: string; text?: string;
onClick?: () => void;
testId?: string;
}) => { }) => {
return ( return (
<StyledChip> <StyledChip
withText={isNonEmptyString(text)}
onClick={onClick}
data-testid={testId}
>
<StyledIconsContainer> <StyledIconsContainer>
{Icons.map((Icon, index) => ( {Icons.map((Icon, index) => (
<Fragment key={index}>{Icon}</Fragment> <Fragment key={index}>{Icon}</Fragment>
))} ))}
</StyledIconsContainer> </StyledIconsContainer>
<span>{text}</span> {text && <span>{text}</span>}
</StyledChip> </StyledChip>
); );
}; };

View File

@ -9,12 +9,21 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { IconX, LightIconButton, useIsMobile } from 'twenty-ui'; import {
Button,
IconChevronLeft,
IconX,
LightIconButton,
getOsControlSymbol,
useIsMobile,
} from 'twenty-ui';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledInputContainer = styled.div` const StyledInputContainer = styled.div`
align-items: center; align-items: center;
@ -81,7 +90,7 @@ export const CommandMenuTopBar = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { closeCommandMenu } = useCommandMenu(); const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu();
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState, contextStoreCurrentObjectMetadataIdComponentState,
@ -93,9 +102,22 @@ export const CommandMenuTopBar = () => {
const theme = useTheme(); const theme = useTheme();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return ( return (
<StyledInputContainer> <StyledInputContainer>
<StyledContentContainer> <StyledContentContainer>
{isCommandMenuV2Enabled && (
<CommandMenuContextChip
Icons={[<IconChevronLeft size={theme.icon.size.sm} />]}
onClick={() => {
goBackFromCommandMenu();
}}
testId="command-menu-go-back-button"
/>
)}
{commandMenuPage !== CommandMenuPages.SearchRecords && {commandMenuPage !== CommandMenuPages.SearchRecords &&
isDefined(contextStoreCurrentObjectMetadataId) && ( isDefined(contextStoreCurrentObjectMetadataId) && (
<CommandMenuContextRecordChip <CommandMenuContextRecordChip
@ -120,14 +142,29 @@ export const CommandMenuTopBar = () => {
)} )}
</StyledContentContainer> </StyledContentContainer>
{!isMobile && ( {!isMobile && (
<StyledCloseButtonContainer> <>
<LightIconButton {isCommandMenuV2Enabled ? (
accent={'tertiary'} <Button
size={'medium'} Icon={IconX}
Icon={IconX} dataTestId="page-header-close-command-menu-button"
onClick={closeCommandMenu} size={'small'}
/> variant="secondary"
</StyledCloseButtonContainer> accent="default"
hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel="Close command menu"
onClick={closeCommandMenu}
/>
) : (
<StyledCloseButtonContainer>
<LightIconButton
accent={'tertiary'}
size={'medium'}
Icon={IconX}
onClick={closeCommandMenu}
/>
</StyledCloseButtonContainer>
)}
</>
)} )}
</StyledInputContainer> </StyledInputContainer>
); );

View File

@ -16,10 +16,13 @@ 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 { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { HttpResponse, graphql } from 'msw'; import { HttpResponse, graphql } from 'msw';
import { FeatureFlagKey } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { getCompaniesMock } from '~/testing/mock-data/companies'; import { getCompaniesMock } from '~/testing/mock-data/companies';
@ -29,6 +32,20 @@ const openTimeout = 50;
const companiesMock = getCompaniesMock(); const companiesMock = getCompaniesMock();
// Mock workspace with feature flag enabled
const mockWorkspaceWithFeatureFlag = {
...mockCurrentWorkspace,
featureFlags: [
...(mockCurrentWorkspace.featureFlags || []),
{
id: 'mock-id',
key: FeatureFlagKey.IsCommandMenuV2Enabled,
value: true,
workspaceId: mockCurrentWorkspace.id,
},
],
};
const ContextStoreDecorator: Decorator = (Story) => { const ContextStoreDecorator: Decorator = (Story) => {
return ( return (
<RecordFiltersComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
@ -62,10 +79,18 @@ const meta: Meta<typeof CommandMenu> = {
const setIsCommandMenuOpened = useSetRecoilState( const setIsCommandMenuOpened = useSetRecoilState(
isCommandMenuOpenedState, isCommandMenuOpenedState,
); );
const setCommandMenuNavigationStack = useSetRecoilState(
commandMenuNavigationStackState,
);
setCurrentWorkspace(mockCurrentWorkspace); setCurrentWorkspace(mockWorkspaceWithFeatureFlag);
setCurrentWorkspaceMember(mockedWorkspaceMemberData); setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setIsCommandMenuOpened(true); setIsCommandMenuOpened(true);
setCommandMenuNavigationStack([
{
page: CommandMenuPages.Root,
},
]);
return <Story />; return <Story />;
}, },
@ -168,3 +193,28 @@ export const NoResultsSearchFallback: Story = {
}, },
}, },
}; };
export const GoBack: Story = {
play: async () => {
const canvas = within(document.body);
const goBackButton = await canvas.findByTestId(
'command-menu-go-back-button',
);
await userEvent.click(goBackButton);
await expect(goBackButton).not.toBeVisible();
},
};
export const ClickOnSearchRecordsAndGoBack: Story = {
play: async () => {
const canvas = within(document.body);
const searchRecordsButton = await canvas.findByText('Search records');
await userEvent.click(searchRecordsButton);
await sleep(openTimeout);
const goBackButton = await canvas.findByTestId(
'command-menu-go-back-button',
);
await userEvent.click(goBackButton);
expect(await canvas.findByText('Search records')).toBeVisible();
},
};

View File

@ -53,7 +53,9 @@ describe('useCommandMenu', () => {
const { result } = renderHooks(); const { result } = renderHooks();
act(() => { act(() => {
result.current.commandMenu.openCommandMenu(); result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.Root,
});
}); });
expect(result.current.isCommandMenuOpened).toBe(true); expect(result.current.isCommandMenuOpened).toBe(true);

View File

@ -42,8 +42,16 @@ export const useCommandMenu = () => {
const { resetContextStoreStates } = useResetContextStoreStates(); const { resetContextStoreStates } = useResetContextStoreStates();
const openCommandMenu = useRecoilCallback( const openCommandMenu = useRecoilCallback(
({ set }) => ({ snapshot, set }) =>
() => { () => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
if (isCommandMenuOpened) {
return;
}
copyContextStoreStates({ copyContextStoreStates({
instanceIdToCopyFrom: mainContextStoreComponentInstanceId, instanceIdToCopyFrom: mainContextStoreComponentInstanceId,
instanceIdToCopyTo: 'command-menu', instanceIdToCopyTo: 'command-menu',
@ -88,24 +96,6 @@ export const useCommandMenu = () => {
[goBackToPreviousHotkeyScope, resetContextStoreStates, resetSelectedItem], [goBackToPreviousHotkeyScope, resetContextStoreStates, resetSelectedItem],
); );
const toggleCommandMenu = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
set(commandMenuSearchState, '');
if (isCommandMenuOpened) {
closeCommandMenu();
} else {
openCommandMenu();
}
},
[closeCommandMenu, openCommandMenu],
);
const navigateCommandMenu = useRecoilCallback( const navigateCommandMenu = useRecoilCallback(
({ snapshot, set }) => { ({ snapshot, set }) => {
return ({ return ({
@ -133,6 +123,26 @@ export const useCommandMenu = () => {
[openCommandMenu], [openCommandMenu],
); );
const toggleCommandMenu = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
set(commandMenuSearchState, '');
if (isCommandMenuOpened) {
closeCommandMenu();
} else {
navigateCommandMenu({
page: CommandMenuPages.Root,
});
}
},
[closeCommandMenu, navigateCommandMenu],
);
const goBackFromCommandMenu = useRecoilCallback( const goBackFromCommandMenu = useRecoilCallback(
({ snapshot, set }) => { ({ snapshot, set }) => {
return () => { return () => {
@ -257,7 +267,6 @@ export const useCommandMenu = () => {
); );
return { return {
openCommandMenu,
closeCommandMenu, closeCommandMenu,
navigateCommandMenu, navigateCommandMenu,
navigateCommandMenuHistory, navigateCommandMenuHistory,

View File

@ -7,11 +7,12 @@ import {
} from 'twenty-ui'; } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
export const PageHeaderOpenCommandMenuButton = () => { export const PageHeaderOpenCommandMenuButton = () => {
const { openCommandMenu } = useCommandMenu(); const { navigateCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled( const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled, FeatureFlagKey.IsCommandMenuV2Enabled,
@ -30,7 +31,11 @@ export const PageHeaderOpenCommandMenuButton = () => {
accent="default" accent="default"
hotkeys={[getOsControlSymbol(), 'K']} hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel="Open command menu" ariaLabel="Open command menu"
onClick={openCommandMenu} onClick={() => {
navigateCommandMenu({
page: CommandMenuPages.Root,
});
}}
/> />
) : ( ) : (
<IconButton <IconButton
@ -39,7 +44,11 @@ export const PageHeaderOpenCommandMenuButton = () => {
dataTestId="more-showpage-button" dataTestId="more-showpage-button"
accent="default" accent="default"
variant="secondary" variant="secondary"
onClick={openCommandMenu} onClick={() => {
navigateCommandMenu({
page: CommandMenuPages.Root,
});
}}
/> />
)} )}
</> </>