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) => (
+
+ }
+ />
+ );
+};
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: {