From e8bf81de5b62dcd506ccd88570dd0227bdfdcab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:08:09 +0100 Subject: [PATCH] 8172 update the right drawer action menu to open with command o (#8375) Closes #8172 - Added a shortcut property to the button component - Displays the actions inside a dropdown - The dropdown is toggled either by clicking on the button or with the `command + O` shortcut https://github.com/user-attachments/assets/4c4c88fa-85dc-404e-bb42-f2b0d57c8960 --- .../RecordShowActionMenuBarEntry.tsx | 56 ------------ .../RecordShowRightDrawerActionMenu.tsx | 4 +- .../RecordShowRightDrawerActionMenuBar.tsx | 21 ----- .../RightDrawerActionMenuDropdown.tsx | 87 +++++++++++++++++++ .../RecordShowActionMenuBar.stories.tsx | 19 ++-- ...ightDrawerActionMenuDropdownHotkeyScope.ts | 3 + ...tionMenuDropdownIdFromActionMenuId.test.ts | 9 ++ ...werActionMenuDropdownIdFromActionMenuId.ts | 5 ++ .../activities/components/RichTextEditor.tsx | 4 + .../src/input/button/components/Button.tsx | 21 +++++ .../components/__stories__/Button.stories.tsx | 32 +++++++ 11 files changed, 177 insertions(+), 84 deletions(-) delete mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx delete mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts create mode 100644 packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts create mode 100644 packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx deleted file mode 100644 index b075565f4..000000000 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { MOBILE_VIEWPORT, MenuItemAccent } from 'twenty-ui'; - -type RecordShowActionMenuBarEntryProps = { - entry: ActionMenuEntry; -}; - -const StyledButton = styled.div<{ accent: MenuItemAccent }>` - border-radius: ${({ theme }) => theme.border.radius.sm}; - color: ${(props) => - props.accent === 'danger' - ? props.theme.color.red - : props.theme.font.color.secondary}; - cursor: pointer; - display: flex; - justify-content: center; - - padding: ${({ theme }) => theme.spacing(2)}; - transition: background 0.1s ease; - user-select: none; - - &:hover { - background: ${({ theme, accent }) => - accent === 'danger' - ? theme.background.danger - : theme.background.transparent.light}; - } - - @media (max-width: ${MOBILE_VIEWPORT}px) { - padding: ${({ theme }) => theme.spacing(1)}; - } -`; - -const StyledButtonLabel = styled.div` - font-weight: ${({ theme }) => theme.font.weight.medium}; - margin-left: ${({ theme }) => theme.spacing(1)}; -`; - -// For now, this component is the same as RecordIndexActionMenuBarEntry but they -// will probably diverge in the future -export const RecordShowActionMenuBarEntry = ({ - entry, -}: RecordShowActionMenuBarEntryProps) => { - const theme = useTheme(); - return ( - entry.onClick?.()} - > - {entry.Icon && } - {entry.label} - - ); -}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx index 510104926..ba964f27a 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx @@ -1,7 +1,7 @@ import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; -import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar'; +import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; @@ -21,7 +21,7 @@ export const RecordShowRightDrawerActionMenu = () => { onActionExecutedCallback: () => {}, }} > - + diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx deleted file mode 100644 index 8f9540e4d..000000000 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordShowActionMenuBarEntry'; -import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; - -export const RecordShowRightDrawerActionMenuBar = () => { - const actionMenuEntries = useRecoilComponentValueV2( - actionMenuEntriesComponentSelector, - ); - - const standardActionMenuEntries = actionMenuEntries.filter( - (actionMenuEntry) => actionMenuEntry.type === 'standard', - ); - - return ( - <> - {standardActionMenuEntries.map((actionMenuEntry) => ( - - ))} - - ); -}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx new file mode 100644 index 000000000..ed3c1ee22 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx @@ -0,0 +1,87 @@ +import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { RightDrawerActionMenuDropdownHotkeyScope } from '@/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope'; +import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useTheme } from '@emotion/react'; +import { Key } from 'ts-key-enum'; +import { Button, MenuItem } from 'twenty-ui'; + +export const RightDrawerActionMenuDropdown = () => { + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentSelector, + ); + + const actionMenuId = useAvailableComponentInstanceIdOrThrow( + ActionMenuComponentInstanceContext, + ); + + const { closeDropdown, openDropdown } = useDropdownV2(); + + const theme = useTheme(); + + useScopedHotkeys( + [Key.Escape, 'ctrl+o,meta+o'], + () => { + closeDropdown( + getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId), + ); + }, + RightDrawerActionMenuDropdownHotkeyScope.RightDrawerActionMenuDropdown, + [closeDropdown], + ); + + useScopedHotkeys( + ['ctrl+o,meta+o'], + () => { + openDropdown( + getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId), + ); + }, + RightDrawerHotkeyScope.RightDrawer, + [openDropdown], + ); + + return ( + } + dropdownPlacement="top-end" + dropdownOffset={{ + y: parseInt(theme.spacing(2)), + }} + dropdownComponents={ + + {actionMenuEntries.map((item, index) => ( + { + closeDropdown( + getRightDrawerActionMenuDropdownIdFromActionMenuId( + actionMenuId, + ), + ); + item.onClick?.(); + }} + text={item.label} + /> + ))} + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx index a1f4422b6..4a86046bd 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx @@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { RecoilRoot } from 'recoil'; -import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar'; +import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; @@ -21,9 +21,9 @@ const deleteMock = jest.fn(); const addToFavoritesMock = jest.fn(); const exportMock = jest.fn(); -const meta: Meta = { - title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar', - component: RecordShowRightDrawerActionMenuBar, +const meta: Meta = { + title: 'Modules/ActionMenu/RightDrawerActionMenuDropdown', + component: RightDrawerActionMenuDropdown, decorators: [ (Story) => ( = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { @@ -113,12 +113,21 @@ export const WithButtonClicks: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); + let actionButton = await canvas.findByText('Actions'); + await userEvent.click(actionButton); + const deleteButton = await canvas.findByText('Delete'); await userEvent.click(deleteButton); + actionButton = await canvas.findByText('Actions'); + await userEvent.click(actionButton); + const addToFavoritesButton = await canvas.findByText('Add to favorites'); await userEvent.click(addToFavoritesButton); + actionButton = await canvas.findByText('Actions'); + await userEvent.click(actionButton); + const exportButton = await canvas.findByText('Export'); await userEvent.click(exportButton); diff --git a/packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts new file mode 100644 index 000000000..74505c320 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum RightDrawerActionMenuDropdownHotkeyScope { + RightDrawerActionMenuDropdown = 'right-drawer-action-menu-dropdown', +} diff --git a/packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts new file mode 100644 index 000000000..209bdf99a --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts @@ -0,0 +1,9 @@ +import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '../getRightDrawerActionMenuDropdownIdFromActionMenuId'; + +describe('getRightDrawerActionMenuDropdownIdFromActionMenuId', () => { + it('should return the right drawer action menu dropdown id', () => { + expect( + getRightDrawerActionMenuDropdownIdFromActionMenuId('action-menu-id'), + ).toBe('right-drawer-action-menu-dropdown-action-menu-id'); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts b/packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts new file mode 100644 index 000000000..8e1d49133 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts @@ -0,0 +1,5 @@ +export const getRightDrawerActionMenuDropdownIdFromActionMenuId = ( + actionMenuId: string, +) => { + return `right-drawer-action-menu-dropdown-${actionMenuId}`; +}; diff --git a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx index 26dd4204e..bff2bef3c 100644 --- a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx @@ -350,6 +350,10 @@ export const RichTextEditor = ({ editor.focus(); }, RightDrawerHotkeyScope.RightDrawer, + [], + { + preventDefault: false, + }, ); const handleBlockEditorFocus = () => { diff --git a/packages/twenty-ui/src/input/button/components/Button.tsx b/packages/twenty-ui/src/input/button/components/Button.tsx index 5869b12fa..d9fae551b 100644 --- a/packages/twenty-ui/src/input/button/components/Button.tsx +++ b/packages/twenty-ui/src/input/button/components/Button.tsx @@ -29,6 +29,7 @@ export type ButtonProps = { to?: string; target?: string; dataTestId?: string; + shortcut?: string; } & React.ComponentProps<'button'>; const StyledButton = styled('button', { @@ -358,6 +359,19 @@ const StyledSoonPill = styled(Pill)` margin-left: auto; `; +const StyledShortcutLabel = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + +const StyledSeparator = styled.div<{ buttonSize: ButtonSize }>` + background: ${({ theme }) => theme.border.color.light}; + height: ${({ theme, buttonSize }) => + theme.spacing(buttonSize === 'small' ? 3 : 4)}; + margin: 0 ${({ theme }) => theme.spacing(1)}; + width: 1px; +`; + export const Button = ({ className, Icon, @@ -376,6 +390,7 @@ export const Button = ({ to, target, dataTestId, + shortcut, }: ButtonProps) => { const theme = useTheme(); @@ -399,6 +414,12 @@ export const Button = ({ > {Icon && } {title} + {shortcut && ( + <> + + {shortcut} + + )} {soon && } ); diff --git a/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx index d67f0d399..5ad32a17e 100644 --- a/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx @@ -23,6 +23,7 @@ type Story = StoryObj; export const Default: Story = { argTypes: { + shortcut: { control: false }, Icon: { control: false }, }, args: { @@ -54,6 +55,7 @@ export const Catalog: CatalogStory = { soon: { control: false }, position: { control: false }, className: { control: false }, + shortcut: { control: false }, }, parameters: { pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, @@ -126,6 +128,7 @@ export const SoonCatalog: CatalogStory = { soon: { control: false }, position: { control: false }, className: { control: false }, + shortcut: { control: false }, }, parameters: { pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, @@ -197,6 +200,7 @@ export const PositionCatalog: CatalogStory = { fullWidth: { control: false }, soon: { control: false }, position: { control: false }, + shortcut: { control: false }, }, parameters: { pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, @@ -262,6 +266,34 @@ export const PositionCatalog: CatalogStory = { decorators: [CatalogDecorator], }; +export const ShortcutCatalog: CatalogStory = { + args: { title: 'Actions', shortcut: '⌘O' }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + focus: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + shortcut: { control: false }, + }, + parameters: { + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'sizes', + values: ['small', 'medium'] satisfies ButtonSize[], + props: (size: ButtonSize) => ({ size }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + export const FullWidth: Story = { args: { title: 'Filter', Icon: IconSearch, fullWidth: true }, argTypes: {