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

View File

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

View File

@ -1,7 +1,12 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
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;
background: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@ -10,11 +15,21 @@ const StyledChip = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
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-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
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`
@ -24,18 +39,26 @@ const StyledIconsContainer = styled.div`
export const CommandMenuContextChip = ({
Icons,
text,
onClick,
testId,
}: {
Icons: React.ReactNode[];
text?: string;
onClick?: () => void;
testId?: string;
}) => {
return (
<StyledChip>
<StyledChip
withText={isNonEmptyString(text)}
onClick={onClick}
data-testid={testId}
>
<StyledIconsContainer>
{Icons.map((Icon, index) => (
<Fragment key={index}>{Icon}</Fragment>
))}
</StyledIconsContainer>
<span>{text}</span>
{text && <span>{text}</span>}
</StyledChip>
);
};

View File

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

View File

@ -16,10 +16,13 @@ import { sleep } from '~/utils/sleep';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { HttpResponse, graphql } from 'msw';
import { FeatureFlagKey } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { getCompaniesMock } from '~/testing/mock-data/companies';
@ -29,6 +32,20 @@ const openTimeout = 50;
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) => {
return (
<RecordFiltersComponentInstanceContext.Provider
@ -62,10 +79,18 @@ const meta: Meta<typeof CommandMenu> = {
const setIsCommandMenuOpened = useSetRecoilState(
isCommandMenuOpenedState,
);
const setCommandMenuNavigationStack = useSetRecoilState(
commandMenuNavigationStackState,
);
setCurrentWorkspace(mockCurrentWorkspace);
setCurrentWorkspace(mockWorkspaceWithFeatureFlag);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setIsCommandMenuOpened(true);
setCommandMenuNavigationStack([
{
page: CommandMenuPages.Root,
},
]);
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();
act(() => {
result.current.commandMenu.openCommandMenu();
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.Root,
});
});
expect(result.current.isCommandMenuOpened).toBe(true);

View File

@ -42,8 +42,16 @@ export const useCommandMenu = () => {
const { resetContextStoreStates } = useResetContextStoreStates();
const openCommandMenu = useRecoilCallback(
({ set }) =>
({ snapshot, set }) =>
() => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
if (isCommandMenuOpened) {
return;
}
copyContextStoreStates({
instanceIdToCopyFrom: mainContextStoreComponentInstanceId,
instanceIdToCopyTo: 'command-menu',
@ -88,24 +96,6 @@ export const useCommandMenu = () => {
[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(
({ snapshot, set }) => {
return ({
@ -133,6 +123,26 @@ export const useCommandMenu = () => {
[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(
({ snapshot, set }) => {
return () => {
@ -257,7 +267,6 @@ export const useCommandMenu = () => {
);
return {
openCommandMenu,
closeCommandMenu,
navigateCommandMenu,
navigateCommandMenuHistory,

View File

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