320 new command menu navigation bar buttons (#10018)
Closes https://github.com/twentyhq/core-team-issues/issues/320 https://github.com/user-attachments/assets/8082e986-07fd-46fb-9652-ad006aa9dac8
This commit is contained in:
@ -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" />
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user