diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx
new file mode 100644
index 000000000000..d34b6c83fc54
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx
@@ -0,0 +1,49 @@
+import styled from '@emotion/styled';
+
+import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry';
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useRecoilValue } from 'recoil';
+
+const StyledLabel = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.md};
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+ padding-left: ${({ theme }) => theme.spacing(2)};
+ padding-right: ${({ theme }) => theme.spacing(2)};
+`;
+
+export const ActionMenuBar = () => {
+ const contextStoreTargetedRecordIds = useRecoilValue(
+ contextStoreTargetedRecordIdsState,
+ );
+
+ const actionMenuId = useAvailableComponentInstanceIdOrThrow(
+ ActionMenuComponentInstanceContext,
+ );
+
+ const actionMenuEntries = useRecoilComponentValueV2(
+ actionMenuEntriesComponentState,
+ );
+
+ return (
+
+
+ {contextStoreTargetedRecordIds.length} selected:
+
+ {actionMenuEntries.map((entry, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx
new file mode 100644
index 000000000000..02802ec4a616
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx
@@ -0,0 +1,49 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
+import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
+
+type ActionMenuBarEntryProps = {
+ 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.tertiary};
+ }
+`;
+
+const StyledButtonLabel = styled.div`
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+ margin-left: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const ActionMenuBarEntry = ({ entry }: ActionMenuBarEntryProps) => {
+ const theme = useTheme();
+ return (
+ entry.onClick?.()}
+ >
+ {entry.Icon && }
+ {entry.label}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx
new file mode 100644
index 000000000000..fc4af90b199b
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx
@@ -0,0 +1,18 @@
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+
+export const ActionMenuConfirmationModals = () => {
+ const actionMenuEntries = useRecoilComponentValueV2(
+ actionMenuEntriesComponentState,
+ );
+
+ return (
+
+ {actionMenuEntries.map((actionMenuEntry, index) =>
+ actionMenuEntry.ConfirmationModal ? (
+
{actionMenuEntry.ConfirmationModal}
+ ) : null,
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx
new file mode 100644
index 000000000000..b443c4307c66
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx
@@ -0,0 +1,84 @@
+import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
+
+import { PositionType } from '../types/PositionType';
+
+import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
+
+type StyledContainerProps = {
+ position: PositionType;
+};
+
+const StyledContainerActionMenuDropdown = styled.div`
+ align-items: flex-start;
+ background: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.light};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ box-shadow: ${({ theme }) => theme.boxShadow.strong};
+ display: flex;
+ flex-direction: column;
+
+ left: ${(props) => `${props.position.x}px`};
+ position: fixed;
+ top: ${(props) => `${props.position.y}px`};
+
+ transform: translateX(-50%);
+ width: auto;
+`;
+
+export const ActionMenuDropdown = () => {
+ const actionMenuEntries = useRecoilComponentValueV2(
+ actionMenuEntriesComponentState,
+ );
+
+ const actionMenuId = useAvailableComponentInstanceIdOrThrow(
+ ActionMenuComponentInstanceContext,
+ );
+
+ const actionMenuDropdownPosition = useRecoilValue(
+ extractComponentState(
+ actionMenuDropdownPositionComponentState,
+ `action-menu-dropdown-${actionMenuId}`,
+ ),
+ );
+
+ //TODO: remove this
+ const width = actionMenuEntries.some(
+ (actionMenuEntry) => actionMenuEntry.label === 'Remove from favorites',
+ )
+ ? 200
+ : undefined;
+
+ return (
+
+ (
+
+ ))}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx
new file mode 100644
index 000000000000..60355cc9256f
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx
@@ -0,0 +1,46 @@
+import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
+import { useEffect } from 'react';
+import { useRecoilValue } from 'recoil';
+
+export const ActionMenuEffect = () => {
+ const contextStoreTargetedRecordIds = useRecoilValue(
+ contextStoreTargetedRecordIdsState,
+ );
+
+ const actionMenuId = useAvailableComponentInstanceIdOrThrow(
+ ActionMenuComponentInstanceContext,
+ );
+
+ const { openActionBar, closeActionBar } = useActionMenu(actionMenuId);
+
+ const isDropdownOpen = useRecoilValue(
+ extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${actionMenuId}`,
+ ),
+ );
+
+ useEffect(() => {
+ if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) {
+ // We only handle opening the ActionMenuBar here, not the Dropdown.
+ // The Dropdown is already managed by sync handlers for events like
+ // right-click to open and click outside to close.
+ openActionBar();
+ }
+ if (contextStoreTargetedRecordIds.length === 0) {
+ closeActionBar();
+ }
+ }, [
+ contextStoreTargetedRecordIds,
+ openActionBar,
+ closeActionBar,
+ isDropdownOpen,
+ ]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx
new file mode 100644
index 000000000000..3a340feb85ef
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx
@@ -0,0 +1,25 @@
+import { EmptyActionMenuEntriesEffect } from '@/action-menu/components/EmptyActionMenuEntriesEffect';
+import { NonEmptyActionMenuEntriesEffect } from '@/action-menu/components/NonEmptyActionMenuEntriesEffect';
+import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
+import { useRecoilValue } from 'recoil';
+
+export const ActionMenuEntriesProvider = () => {
+ //TODO: Refactor this
+ const contextStoreCurrentObjectMetadataId = useRecoilValue(
+ contextStoreCurrentObjectMetadataIdState,
+ );
+
+ return (
+ <>
+ {contextStoreCurrentObjectMetadataId ? (
+
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx
new file mode 100644
index 000000000000..aca1e5fdae22
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx
@@ -0,0 +1,14 @@
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { useEffect } from 'react';
+
+export const EmptyActionMenuEntriesEffect = () => {
+ const setActionMenuEntries = useSetRecoilComponentStateV2(
+ actionMenuEntriesComponentState,
+ );
+ useEffect(() => {
+ setActionMenuEntries([]);
+ }, [setActionMenuEntries]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx
new file mode 100644
index 000000000000..210db62500cd
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx
@@ -0,0 +1,28 @@
+import { useComputeActionsBasedOnContextStore } from '@/action-menu/hooks/useComputeActionsBasedOnContextStore';
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { useEffect } from 'react';
+
+export const NonEmptyActionMenuEntriesEffect = ({
+ contextStoreCurrentObjectMetadataId,
+}: {
+ contextStoreCurrentObjectMetadataId: string;
+}) => {
+ const { objectMetadataItem } = useObjectMetadataItemById({
+ objectId: contextStoreCurrentObjectMetadataId,
+ });
+ const { availableActionsInContext } = useComputeActionsBasedOnContextStore({
+ objectMetadataItem,
+ });
+
+ const setActionMenuEntries = useSetRecoilComponentStateV2(
+ actionMenuEntriesComponentState,
+ );
+
+ useEffect(() => {
+ setActionMenuEntries(availableActionsInContext);
+ }, [availableActionsInContext, setActionMenuEntries]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx
new file mode 100644
index 000000000000..907d6caf3f4f
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx
@@ -0,0 +1,101 @@
+import { expect, jest } from '@storybook/jest';
+import { Meta, StoryObj } from '@storybook/react';
+import { RecoilRoot } from 'recoil';
+
+import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar';
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { userEvent, waitFor, within } from '@storybook/test';
+import { IconCheckbox, IconTrash } from 'twenty-ui';
+
+const deleteMock = jest.fn();
+const markAsDoneMock = jest.fn();
+
+const meta: Meta = {
+ title: 'Modules/ActionMenu/ActionMenuBar',
+ component: ActionMenuBar,
+ decorators: [
+ (Story) => (
+ {
+ set(contextStoreTargetedRecordIdsState, ['1', '2', '3']);
+ set(
+ actionMenuEntriesComponentState.atomFamily({
+ instanceId: 'story-action-menu',
+ }),
+ [
+ {
+ label: 'Delete',
+ Icon: IconTrash,
+ onClick: deleteMock,
+ },
+ {
+ label: 'Mark as done',
+ Icon: IconCheckbox,
+ onClick: markAsDoneMock,
+ },
+ ],
+ );
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: 'action-bar-story-action-menu',
+ }),
+ true,
+ );
+ }}
+ >
+
+
+
+
+ ),
+ ],
+ args: {
+ actionMenuId: 'story-action-menu',
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ actionMenuId: 'story-action-menu',
+ },
+};
+
+export const WithCustomSelection: Story = {
+ args: {
+ actionMenuId: 'story-action-menu',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const selectionText = await canvas.findByText('3 selected:');
+ expect(selectionText).toBeInTheDocument();
+ },
+};
+
+export const WithButtonClicks: Story = {
+ args: {
+ actionMenuId: 'story-action-menu',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const deleteButton = await canvas.findByText('Delete');
+ await userEvent.click(deleteButton);
+
+ const markAsDoneButton = await canvas.findByText('Mark as done');
+ await userEvent.click(markAsDoneButton);
+
+ await waitFor(() => {
+ expect(deleteMock).toHaveBeenCalled();
+ expect(markAsDoneMock).toHaveBeenCalled();
+ });
+ },
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx
new file mode 100644
index 000000000000..a84e033312db
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx
@@ -0,0 +1,56 @@
+import { expect, jest } from '@storybook/jest';
+import { Meta, StoryObj } from '@storybook/react';
+import { userEvent, within } from '@storybook/testing-library';
+
+import { ComponentDecorator, IconCheckbox, IconTrash } from 'twenty-ui';
+import { ActionMenuBarEntry } from '../ActionMenuBarEntry';
+
+const meta: Meta = {
+ title: 'Modules/ActionMenu/ActionMenuBarEntry',
+ component: ActionMenuBarEntry,
+ decorators: [ComponentDecorator],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const deleteMock = jest.fn();
+const markAsDoneMock = jest.fn();
+
+export const Default: Story = {
+ args: {
+ entry: {
+ label: 'Delete',
+ Icon: IconTrash,
+ onClick: deleteMock,
+ },
+ },
+};
+
+export const WithDangerAccent: Story = {
+ args: {
+ entry: {
+ label: 'Delete',
+ Icon: IconTrash,
+ onClick: deleteMock,
+ accent: 'danger',
+ },
+ },
+};
+
+export const WithInteraction: Story = {
+ args: {
+ entry: {
+ label: 'Mark as done',
+ Icon: IconCheckbox,
+ onClick: markAsDoneMock,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = await canvas.findByText('Mark as done');
+ await userEvent.click(button);
+ expect(markAsDoneMock).toHaveBeenCalled();
+ },
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx
new file mode 100644
index 000000000000..4848f44eeec5
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx
@@ -0,0 +1,102 @@
+import { expect, jest } from '@storybook/jest';
+import { Meta, StoryObj } from '@storybook/react';
+import { userEvent, within } from '@storybook/testing-library';
+import { RecoilRoot } from 'recoil';
+
+import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown';
+import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
+import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
+import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui';
+
+const deleteMock = jest.fn();
+const markAsDoneMock = jest.fn();
+const addToFavoritesMock = jest.fn();
+
+const meta: Meta = {
+ title: 'Modules/ActionMenu/ActionMenuDropdown',
+ component: ActionMenuDropdown,
+ decorators: [
+ (Story) => (
+ {
+ set(
+ extractComponentState(
+ actionMenuDropdownPositionComponentState,
+ 'action-menu-dropdown-story',
+ ),
+ { x: 10, y: 10 },
+ );
+ set(
+ actionMenuEntriesComponentState.atomFamily({
+ instanceId: 'story-action-menu',
+ }),
+ [
+ {
+ label: 'Delete',
+ Icon: IconTrash,
+ onClick: deleteMock,
+ },
+ {
+ label: 'Mark as done',
+ Icon: IconCheckbox,
+ onClick: markAsDoneMock,
+ },
+ {
+ label: 'Add to favorites',
+ Icon: IconHeart,
+ onClick: addToFavoritesMock,
+ },
+ ],
+ );
+ set(
+ extractComponentState(
+ isDropdownOpenComponentState,
+ 'action-menu-dropdown-story-action-menu',
+ ),
+ true,
+ );
+ }}
+ >
+
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ actionMenuId: 'story',
+ },
+};
+
+export const WithInteractions: Story = {
+ args: {
+ actionMenuId: 'story',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const deleteButton = await canvas.findByText('Delete');
+ await userEvent.click(deleteButton);
+ expect(deleteMock).toHaveBeenCalled();
+
+ const markAsDoneButton = await canvas.findByText('Mark as done');
+ await userEvent.click(markAsDoneButton);
+ expect(markAsDoneMock).toHaveBeenCalled();
+
+ const addToFavoritesButton = await canvas.findByText('Add to favorites');
+ await userEvent.click(addToFavoritesButton);
+ expect(addToFavoritesMock).toHaveBeenCalled();
+ },
+};
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts
new file mode 100644
index 000000000000..0f37475adedc
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts
@@ -0,0 +1,83 @@
+import { renderHook } from '@testing-library/react';
+import { act } from 'react';
+import { useActionMenu } from '../useActionMenu';
+
+const openBottomBar = jest.fn();
+const closeBottomBar = jest.fn();
+const openDropdown = jest.fn();
+const closeDropdown = jest.fn();
+
+jest.mock('@/ui/layout/bottom-bar/hooks/useBottomBar', () => ({
+ useBottomBar: jest.fn(() => ({
+ openBottomBar: openBottomBar,
+ closeBottomBar: closeBottomBar,
+ })),
+}));
+
+jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
+ useDropdownV2: jest.fn(() => ({
+ openDropdown: openDropdown,
+ closeDropdown: closeDropdown,
+ })),
+}));
+
+describe('useActionMenu', () => {
+ const actionMenuId = 'test-action-menu';
+
+ it('should return the correct functions', () => {
+ const { result } = renderHook(() => useActionMenu(actionMenuId));
+
+ expect(result.current).toHaveProperty('openActionMenuDropdown');
+ expect(result.current).toHaveProperty('openActionBar');
+ expect(result.current).toHaveProperty('closeActionBar');
+ expect(result.current).toHaveProperty('closeActionMenuDropdown');
+ });
+
+ it('should call the correct functions when opening action menu dropdown', () => {
+ const { result } = renderHook(() => useActionMenu(actionMenuId));
+
+ act(() => {
+ result.current.openActionMenuDropdown();
+ });
+
+ expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
+ expect(openDropdown).toHaveBeenCalledWith(
+ `action-menu-dropdown-${actionMenuId}`,
+ );
+ });
+
+ it('should call the correct functions when opening action bar', () => {
+ const { result } = renderHook(() => useActionMenu(actionMenuId));
+
+ act(() => {
+ result.current.openActionBar();
+ });
+
+ expect(closeDropdown).toHaveBeenCalledWith(
+ `action-menu-dropdown-${actionMenuId}`,
+ );
+ expect(openBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
+ });
+
+ it('should call the correct function when closing action menu dropdown', () => {
+ const { result } = renderHook(() => useActionMenu(actionMenuId));
+
+ act(() => {
+ result.current.closeActionMenuDropdown();
+ });
+
+ expect(closeDropdown).toHaveBeenCalledWith(
+ `action-menu-dropdown-${actionMenuId}`,
+ );
+ });
+
+ it('should call the correct function when closing action bar', () => {
+ const { result } = renderHook(() => useActionMenu(actionMenuId));
+
+ act(() => {
+ result.current.closeActionBar();
+ });
+
+ expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
+ });
+});
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts
new file mode 100644
index 000000000000..881cadd694e1
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts
@@ -0,0 +1,32 @@
+import { useBottomBar } from '@/ui/layout/bottom-bar/hooks/useBottomBar';
+import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
+
+export const useActionMenu = (actionMenuId: string) => {
+ const { openDropdown, closeDropdown } = useDropdownV2();
+ const { openBottomBar, closeBottomBar } = useBottomBar();
+
+ const openActionMenuDropdown = () => {
+ closeBottomBar(`action-bar-${actionMenuId}`);
+ openDropdown(`action-menu-dropdown-${actionMenuId}`);
+ };
+
+ const openActionBar = () => {
+ closeDropdown(`action-menu-dropdown-${actionMenuId}`);
+ openBottomBar(`action-bar-${actionMenuId}`);
+ };
+
+ const closeActionMenuDropdown = () => {
+ closeDropdown(`action-menu-dropdown-${actionMenuId}`);
+ };
+
+ const closeActionBar = () => {
+ closeBottomBar(`action-bar-${actionMenuId}`);
+ };
+
+ return {
+ openActionMenuDropdown,
+ openActionBar,
+ closeActionBar,
+ closeActionMenuDropdown,
+ };
+};
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx b/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx
new file mode 100644
index 000000000000..0145042d2847
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx
@@ -0,0 +1,148 @@
+import { useHandleFavoriteButton } from '@/action-menu/hooks/useHandleFavoriteButton';
+import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { useFavorites } from '@/favorites/hooks/useFavorites';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
+import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
+import {
+ displayedExportProgress,
+ useExportTableData,
+} from '@/object-record/record-index/options/hooks/useExportTableData';
+import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useCallback, useMemo, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import {
+ IconFileExport,
+ IconHeart,
+ IconHeartOff,
+ IconTrash,
+ isDefined,
+} from 'twenty-ui';
+
+export const useComputeActionsBasedOnContextStore = ({
+ objectMetadataItem,
+}: {
+ objectMetadataItem: ObjectMetadataItem;
+}) => {
+ const contextStoreTargetedRecordIds = useRecoilValue(
+ contextStoreTargetedRecordIdsState,
+ );
+
+ const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
+ useState(false);
+
+ const { handleFavoriteButtonClick } = useHandleFavoriteButton(
+ contextStoreTargetedRecordIds,
+ objectMetadataItem,
+ );
+
+ const baseTableDataParams = {
+ delayMs: 100,
+ objectNameSingular: objectMetadataItem.nameSingular,
+ recordIndexId: objectMetadataItem.namePlural,
+ };
+
+ const { deleteTableData } = useDeleteTableData(baseTableDataParams);
+
+ const handleDeleteClick = useCallback(() => {
+ deleteTableData(contextStoreTargetedRecordIds);
+ }, [deleteTableData, contextStoreTargetedRecordIds]);
+
+ const { progress, download } = useExportTableData({
+ ...baseTableDataParams,
+ filename: `${objectMetadataItem.nameSingular}.csv`,
+ });
+
+ const isRemoteObject = objectMetadataItem.isRemote;
+
+ const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;
+
+ const canDelete =
+ !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
+
+ const menuActions: ActionMenuEntry[] = useMemo(
+ () =>
+ [
+ {
+ label: displayedExportProgress(progress),
+ Icon: IconFileExport,
+ accent: 'default',
+ onClick: () => download(),
+ } satisfies ActionMenuEntry,
+ canDelete
+ ? ({
+ label: 'Delete',
+ Icon: IconTrash,
+ accent: 'danger',
+ onClick: () => {
+ setIsDeleteRecordsModalOpen(true);
+ },
+ ConfirmationModal: (
+ handleDeleteClick()}
+ deleteButtonText={`Delete ${
+ numberOfSelectedRecords > 1 ? 'Records' : 'Record'
+ }`}
+ />
+ ),
+ } satisfies ActionMenuEntry)
+ : undefined,
+ ].filter(isDefined),
+ [
+ download,
+ progress,
+ canDelete,
+ handleDeleteClick,
+ isDeleteRecordsModalOpen,
+ numberOfSelectedRecords,
+ ],
+ );
+
+ const hasOnlyOneRecordSelected = contextStoreTargetedRecordIds.length === 1;
+
+ const { favorites } = useFavorites();
+
+ const isFavorite =
+ isNonEmptyString(contextStoreTargetedRecordIds[0]) &&
+ !!favorites?.find(
+ (favorite) => favorite.recordId === contextStoreTargetedRecordIds[0],
+ );
+
+ return {
+ availableActionsInContext: [
+ ...menuActions,
+ ...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
+ ? [
+ {
+ label: 'Remove from favorites',
+ Icon: IconHeartOff,
+ onClick: handleFavoriteButtonClick,
+ },
+ ]
+ : []),
+ ...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected
+ ? [
+ {
+ label: 'Add to favorites',
+ Icon: IconHeart,
+ onClick: handleFavoriteButtonClick,
+ },
+ ]
+ : []),
+ ],
+ };
+};
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts b/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts
new file mode 100644
index 000000000000..b965fe61f2d1
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts
@@ -0,0 +1,49 @@
+import { useFavorites } from '@/favorites/hooks/useFavorites';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-ui';
+
+export const useHandleFavoriteButton = (
+ selectedRecordIds: string[],
+ objectMetadataItem: ObjectMetadataItem,
+ callback?: () => void,
+) => {
+ const { createFavorite, favorites, deleteFavorite } = useFavorites();
+
+ const handleFavoriteButtonClick = useRecoilCallback(
+ ({ snapshot }) =>
+ () => {
+ if (selectedRecordIds.length > 1) {
+ return;
+ }
+
+ const selectedRecordId = selectedRecordIds[0];
+ const selectedRecord = snapshot
+ .getLoadable(recordStoreFamilyState(selectedRecordId))
+ .getValue();
+
+ const foundFavorite = favorites?.find(
+ (favorite) => favorite.recordId === selectedRecordId,
+ );
+
+ const isFavorite = !!selectedRecordId && !!foundFavorite;
+
+ if (isFavorite) {
+ deleteFavorite(foundFavorite.id);
+ } else if (isDefined(selectedRecord)) {
+ createFavorite(selectedRecord, objectMetadataItem.nameSingular);
+ }
+ callback?.();
+ },
+ [
+ callback,
+ createFavorite,
+ deleteFavorite,
+ favorites,
+ objectMetadataItem.nameSingular,
+ selectedRecordIds,
+ ],
+ );
+ return { handleFavoriteButtonClick };
+};
diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts
new file mode 100644
index 000000000000..f2f8f06b1372
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts
@@ -0,0 +1,11 @@
+import { PositionType } from '@/action-menu/types/PositionType';
+import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
+
+export const actionMenuDropdownPositionComponentState =
+ createComponentState({
+ key: 'actionMenuDropdownPositionComponentState',
+ defaultValue: {
+ x: null,
+ y: null,
+ },
+ });
diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts
new file mode 100644
index 000000000000..8ba3a259da68
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts
@@ -0,0 +1,11 @@
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+import { ActionMenuEntry } from '../types/ActionMenuEntry';
+
+export const actionMenuEntriesComponentState = createComponentStateV2<
+ ActionMenuEntry[]
+>({
+ key: 'actionMenuEntriesComponentState',
+ defaultValue: [],
+ componentInstanceContext: ActionMenuComponentInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts b/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts
new file mode 100644
index 000000000000..1e64690c744d
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const ActionMenuComponentInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts
new file mode 100644
index 000000000000..fcadbee366f0
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts
@@ -0,0 +1,3 @@
+export enum ActionBarHotkeyScope {
+ ActionBar = 'action-bar',
+}
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarItemAccent.ts b/packages/twenty-front/src/modules/action-menu/types/ActionBarItemAccent.ts
similarity index 100%
rename from packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarItemAccent.ts
rename to packages/twenty-front/src/modules/action-menu/types/ActionBarItemAccent.ts
diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts
new file mode 100644
index 000000000000..9c0e2df42edf
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts
@@ -0,0 +1,3 @@
+export enum ActionMenuDropdownHotkeyScope {
+ ActionMenuDropdown = 'action-menu-dropdown',
+}
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts
similarity index 90%
rename from packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts
rename to packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts
index 416a41419f62..d363a8fcd252 100644
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts
+++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts
@@ -3,7 +3,7 @@ import { IconComponent } from 'twenty-ui';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
-export type ContextMenuEntry = {
+export type ActionMenuEntry = {
label: string;
Icon: IconComponent;
accent?: MenuItemAccent;
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/PositionType.ts b/packages/twenty-front/src/modules/action-menu/types/PositionType.ts
similarity index 100%
rename from packages/twenty-front/src/modules/ui/navigation/context-menu/types/PositionType.ts
rename to packages/twenty-front/src/modules/action-menu/types/PositionType.ts
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts
index 6e77fc50907d..8660d7617de1 100644
--- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts
+++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts
@@ -1,5 +1,5 @@
-import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx
index 03e7e04802f9..c3f381c1bcd5 100644
--- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx
+++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx
@@ -1,6 +1,6 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
-import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
+import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { IconRefresh } from 'twenty-ui';
diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
index 45a4ca00b20b..7f74a129b652 100644
--- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
+++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
@@ -1,6 +1,6 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
-import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
+import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { IconRefresh } from 'twenty-ui';
diff --git a/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts b/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts
index e73031015a44..ea74778ff536 100644
--- a/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts
+++ b/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts
@@ -1,6 +1,6 @@
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentUserState } from '@/auth/states/currentUserState';
-import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum';
+import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useRecoilValue } from 'recoil';
diff --git a/packages/twenty-front/src/modules/information-banner/enums/InformationBannerKeys.enum.ts b/packages/twenty-front/src/modules/information-banner/types/InformationBannerKeys.ts
similarity index 100%
rename from packages/twenty-front/src/modules/information-banner/enums/InformationBannerKeys.enum.ts
rename to packages/twenty-front/src/modules/information-banner/types/InformationBannerKeys.ts
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx
index 876846f2bd4b..f3d33774b119 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx
@@ -1,5 +1,6 @@
import { renderHook } from '@testing-library/react';
+import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
@@ -25,4 +26,15 @@ describe('useObjectMetadataItem', () => {
expect(objectMetadataItem.id).toBe(opportunityObjectMetadata?.id);
});
+
+ it('should throw an error when invalid object name singular is provided', async () => {
+ expect(() =>
+ renderHook(
+ () => useObjectMetadataItem({ objectNameSingular: 'invalid-object' }),
+ {
+ wrapper: Wrapper,
+ },
+ ),
+ ).toThrow(ObjectMetadataItemNotFoundError);
+ });
});
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts
new file mode 100644
index 000000000000..c2fe8825aa68
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts
@@ -0,0 +1,44 @@
+import { renderHook } from '@testing-library/react';
+
+import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
+import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
+import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
+
+const Wrapper = getJestMetadataAndApolloMocksWrapper({
+ apolloMocks: [],
+});
+
+describe('useObjectMetadataItemById', () => {
+ const opportunityObjectMetadata = generatedMockObjectMetadataItems.find(
+ (item) => item.nameSingular === 'opportunity',
+ );
+
+ if (!opportunityObjectMetadata) {
+ throw new Error('Opportunity object metadata not found');
+ }
+
+ it('should return correct properties', async () => {
+ const { result } = renderHook(
+ () =>
+ useObjectMetadataItemById({
+ objectId: opportunityObjectMetadata.id,
+ }),
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ const { objectMetadataItem } = result.current;
+
+ expect(objectMetadataItem.id).toBe(opportunityObjectMetadata.id);
+ });
+
+ it('should throw an error when invalid ID is provided', async () => {
+ expect(() =>
+ renderHook(() => useObjectMetadataItemById({ objectId: 'invalid-id' }), {
+ wrapper: Wrapper,
+ }),
+ ).toThrow(ObjectMetadataItemNotFoundError);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts
new file mode 100644
index 000000000000..6d9730f2d6db
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts
@@ -0,0 +1,25 @@
+import { useRecoilValue } from 'recoil';
+
+import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { isDefined } from '~/utils/isDefined';
+
+export const useObjectMetadataItemById = ({
+ objectId,
+}: {
+ objectId: string;
+}) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const objectMetadataItem = objectMetadataItems.find(
+ (objectMetadataItem) => objectMetadataItem.id === objectId,
+ );
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new ObjectMetadataItemNotFoundError(objectId, objectMetadataItems);
+ }
+
+ return {
+ objectMetadataItem,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx
deleted file mode 100644
index b7cce8057efb..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { isNonEmptyString } from '@sniptt/guards';
-import { useCallback, useMemo, useState } from 'react';
-import { useRecoilCallback, useSetRecoilState } from 'recoil';
-import { IconFileExport, IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
-
-import { useFavorites } from '@/favorites/hooks/useFavorites';
-import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
-import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
-import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
-import {
- displayedExportProgress,
- useExportTableData,
-} from '@/object-record/record-index/options/hooks/useExportTableData';
-import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
-import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
-import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
-import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
-import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
-import { isDefined } from '~/utils/isDefined';
-
-type useRecordActionBarProps = {
- objectMetadataItem: ObjectMetadataItem;
- selectedRecordIds: string[];
- callback?: () => void;
- totalNumberOfRecordsSelected?: number;
-};
-
-export const useRecordActionBar = ({
- objectMetadataItem,
- selectedRecordIds,
- callback,
- totalNumberOfRecordsSelected,
-}: useRecordActionBarProps) => {
- const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
- const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
- const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
- useState(false);
-
- const { createFavorite, favorites, deleteFavorite } = useFavorites();
-
- const handleFavoriteButtonClick = useRecoilCallback(
- ({ snapshot }) =>
- () => {
- if (selectedRecordIds.length > 1) {
- return;
- }
-
- const selectedRecordId = selectedRecordIds[0];
- const selectedRecord = snapshot
- .getLoadable(recordStoreFamilyState(selectedRecordId))
- .getValue();
-
- const foundFavorite = favorites?.find(
- (favorite) => favorite.recordId === selectedRecordId,
- );
-
- const isFavorite = !!selectedRecordId && !!foundFavorite;
-
- if (isFavorite) {
- deleteFavorite(foundFavorite.id);
- } else if (isDefined(selectedRecord)) {
- createFavorite(selectedRecord, objectMetadataItem.nameSingular);
- }
- callback?.();
- },
- [
- callback,
- createFavorite,
- deleteFavorite,
- favorites,
- objectMetadataItem.nameSingular,
- selectedRecordIds,
- ],
- );
-
- const baseTableDataParams = {
- delayMs: 100,
- objectNameSingular: objectMetadataItem.nameSingular,
- recordIndexId: objectMetadataItem.namePlural,
- };
-
- const { deleteTableData } = useDeleteTableData(baseTableDataParams);
-
- const handleDeleteClick = useCallback(() => {
- deleteTableData(selectedRecordIds);
- }, [deleteTableData, selectedRecordIds]);
-
- const { progress, download } = useExportTableData({
- ...baseTableDataParams,
- filename: `${objectMetadataItem.nameSingular}.csv`,
- });
-
- const isRemoteObject = objectMetadataItem.isRemote;
-
- const numberOfSelectedRecords =
- totalNumberOfRecordsSelected ?? selectedRecordIds.length;
- const canDelete =
- !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
-
- const menuActions: ContextMenuEntry[] = useMemo(
- () =>
- [
- {
- label: displayedExportProgress(progress),
- Icon: IconFileExport,
- accent: 'default',
- onClick: () => download(),
- } satisfies ContextMenuEntry,
- canDelete
- ? ({
- label: 'Delete',
- Icon: IconTrash,
- accent: 'danger',
- onClick: () => {
- setIsDeleteRecordsModalOpen(true);
- },
- ConfirmationModal: (
- handleDeleteClick()}
- deleteButtonText={`Delete ${
- numberOfSelectedRecords > 1 ? 'Records' : 'Record'
- }`}
- />
- ),
- } satisfies ContextMenuEntry)
- : undefined,
- ].filter(isDefined),
- [
- download,
- progress,
- canDelete,
- handleDeleteClick,
- isDeleteRecordsModalOpen,
- numberOfSelectedRecords,
- ],
- );
-
- const hasOnlyOneRecordSelected = selectedRecordIds.length === 1;
-
- const isFavorite =
- isNonEmptyString(selectedRecordIds[0]) &&
- !!favorites?.find((favorite) => favorite.recordId === selectedRecordIds[0]);
-
- return {
- setContextMenuEntries: useCallback(() => {
- setContextMenuEntries([
- ...menuActions,
- ...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
- ? [
- {
- label: 'Remove from favorites',
- Icon: IconHeartOff,
- onClick: handleFavoriteButtonClick,
- },
- ]
- : []),
- ...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected
- ? [
- {
- label: 'Add to favorites',
- Icon: IconHeart,
- onClick: handleFavoriteButtonClick,
- },
- ]
- : []),
- ]);
- }, [
- menuActions,
- handleFavoriteButtonClick,
- hasOnlyOneRecordSelected,
- isFavorite,
- isRemoteObject,
- setContextMenuEntries,
- ]),
-
- setActionBarEntries: useCallback(() => {
- setActionBarEntriesState([
- /*
- {
- label: 'Actions',
- Icon: IconClick,
- subActions:
-
- /* [
- {
- label: 'Enrich',
- Icon: IconPuzzle,
- onClick: handleExecuteQuickActionOnClick,
- },
- {
- label: 'Send to mailjet',
- Icon: IconMail,
- },
- ],
- */
- ...menuActions,
- ]);
- }, [menuActions, setActionBarEntriesState]),
- };
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx
deleted file mode 100644
index 584cbeab3b5f..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useRecoilValue } from 'recoil';
-
-import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
-import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
-
-type RecordBoardActionBarProps = {
- recordBoardId: string;
-};
-
-export const RecordBoardActionBar = ({
- recordBoardId,
-}: RecordBoardActionBarProps) => {
- const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
-
- const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
-
- if (!selectedRecordIds.length) {
- return null;
- }
-
- return ;
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
index 2c3b0daf75cc..ff408eb407de 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
@@ -69,7 +69,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
useListenClickOutsideByClassName({
classNames: ['record-board-card'],
- excludeClassNames: ['action-bar', 'context-menu'],
+ excludeClassNames: ['bottom-bar', 'context-menu'],
callback: resetRecordSelection,
});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx
deleted file mode 100644
index fed35a6506d3..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useRecoilValue } from 'recoil';
-
-import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
-import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
-
-type RecordBoardContextMenuProps = {
- recordBoardId: string;
-};
-
-export const RecordBoardContextMenu = ({
- recordBoardId,
-}: RecordBoardContextMenuProps) => {
- const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
-
- const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
-
- if (!selectedRecordIds.length) {
- return null;
- }
-
- return ;
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts
index a14c85119d51..8b2d11837ae1 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts
@@ -1,17 +1,23 @@
-import { useRecoilCallback, useSetRecoilState } from 'recoil';
+import { useRecoilCallback } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
-export const useRecordBoardSelection = (recordBoardId?: string) => {
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
+export const useRecordBoardSelection = (recordBoardId: string) => {
const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } =
useRecordBoardStates(recordBoardId);
const resetRecordSelection = useRecoilCallback(
({ snapshot, set }) =>
() => {
- setContextMenuOpenState(false);
+ const isActionMenuDropdownOpenState = extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${recordBoardId}`,
+ );
+
+ set(isActionMenuDropdownOpenState, false);
+
const recordIds = snapshot
.getLoadable(selectedRecordIdsSelector())
.getValue();
@@ -21,9 +27,9 @@ export const useRecordBoardSelection = (recordBoardId?: string) => {
}
},
[
+ recordBoardId,
selectedRecordIdsSelector,
isRecordBoardCardSelectedFamilyState,
- setContextMenuOpenState,
],
);
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx
index d336467e8db3..a2ea5b03def3 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx
@@ -1,6 +1,9 @@
+import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
+import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
+import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
import {
FieldContext,
RecordUpdateHook,
@@ -17,10 +20,10 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { TextInput } from '@/ui/input/components/TextInput';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
-import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
+import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import styled from '@emotion/styled';
import { ReactNode, useContext, useState } from 'react';
import { useInView } from 'react-intersection-observer';
@@ -175,17 +178,27 @@ export const RecordBoardCard = ({
const record = useRecoilValue(recordStoreFamilyState(recordId));
- const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
+ const recordBoardId = useAvailableScopeIdOrThrow(
+ RecordBoardScopeInternalContext,
+ );
+
+ const setActionMenuDropdownPosition = useSetRecoilState(
+ extractComponentState(
+ actionMenuDropdownPositionComponentState,
+ `action-menu-dropdown-${recordBoardId}`,
+ ),
+ );
+
+ const { openActionMenuDropdown } = useActionMenu(recordBoardId);
- const handleContextMenu = (event: React.MouseEvent) => {
+ const handleActionMenuDropdown = (event: React.MouseEvent) => {
event.preventDefault();
setIsCurrentCardSelected(true);
- setContextMenuPosition({
+ setActionMenuDropdownPosition({
x: event.clientX,
y: event.clientY,
});
- setContextMenuOpenState(true);
+ openActionMenuDropdown();
};
const PreventSelectOnClickContainer = ({
@@ -235,7 +248,7 @@ export const RecordBoardCard = ({
);
return (
-
+
{!isCreating && }
-
-
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx
index c4ad79ed412f..354abcf09dab 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx
@@ -6,9 +6,7 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
-import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
-import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
@@ -79,8 +77,6 @@ export const RecordIndexBoardDataLoaderEffect = ({
setNavigationMemorizedUrl,
]);
- const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
-
useEffect(() => {
setObjectSingularName(objectNameSingular);
}, [objectNameSingular, setObjectSingularName]);
@@ -125,12 +121,6 @@ export const RecordIndexBoardDataLoaderEffect = ({
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
- const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
- objectMetadataItem,
- selectedRecordIds,
- callback: resetRecordSelection,
- });
-
const setContextStoreTargetedRecordIds = useSetRecoilState(
contextStoreTargetedRecordIdsState,
);
@@ -140,9 +130,8 @@ export const RecordIndexBoardDataLoaderEffect = ({
);
useEffect(() => {
- setActionBarEntries?.();
- setContextMenuEntries?.();
- }, [setActionBarEntries, setContextMenuEntries]);
+ setContextStoreTargetedRecordIds(selectedRecordIds);
+ }, [selectedRecordIds, setContextStoreTargetedRecordIds]);
useEffect(() => {
setContextStoreTargetedRecordIds(selectedRecordIds);
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
index 3af3237929f4..cb0d934656d0 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
@@ -23,6 +23,13 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
+
+import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar';
+import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
+import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown';
+import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect';
+import { ActionMenuEntriesProvider } from '@/action-menu/components/ActionMenuEntriesProvider';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewField } from '@/views/types/ViewField';
@@ -191,6 +198,15 @@ export const RecordIndexContainer = () => {
/>
)}
+
+
+
+
+
+
+
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx
index 50bb639f5237..f1dec5e3e7e6 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx
@@ -2,9 +2,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
-import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
-import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
import { useContext } from 'react';
type RecordIndexTableContainerProps = {
@@ -37,9 +35,7 @@ export const RecordIndexTableContainer = ({
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
/>
-
-
>
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx
index ce8bb6572d8c..ce5dc7279ded 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx
@@ -5,14 +5,10 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
-import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
-import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
-import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState';
type RecordIndexTableContainerEffectProps = {
objectNameSingular: string;
@@ -28,7 +24,6 @@ export const RecordIndexTableContainerEffect = ({
const {
setAvailableTableColumns,
setOnEntityCountChange,
- resetTableRowSelection,
selectedRowIdsSelector,
setOnToggleColumnFilter,
setOnToggleColumnSort,
@@ -58,34 +53,8 @@ export const RecordIndexTableContainerEffect = ({
setAvailableTableColumns(columnDefinitions);
}, [columnDefinitions, setAvailableTableColumns]);
- const { tableRowIdsState, hasUserSelectedAllRowsState } =
- useRecordTableStates(recordTableId);
-
- // TODO: verify this instance id works
- const entityCountInCurrentView = useRecoilComponentValueV2(
- entityCountInCurrentViewComponentState,
- recordTableId,
- );
- const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
- const tableRowIds = useRecoilValue(tableRowIdsState);
-
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
- const numSelected =
- hasUserSelectedAllRows && entityCountInCurrentView
- ? selectedRowIds.length === tableRowIds.length
- ? entityCountInCurrentView
- : entityCountInCurrentView -
- (tableRowIds.length - selectedRowIds.length) // unselected row Ids
- : selectedRowIds.length;
-
- const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
- objectMetadataItem,
- selectedRecordIds: selectedRowIds,
- callback: resetTableRowSelection,
- totalNumberOfRecordsSelected: numSelected,
- });
-
const handleToggleColumnFilter = useHandleToggleColumnFilter({
objectNameSingular,
viewBarId,
@@ -110,11 +79,6 @@ export const RecordIndexTableContainerEffect = ({
);
}, [setOnToggleColumnSort, handleToggleColumnSort]);
- useEffect(() => {
- setActionBarEntries?.();
- setContextMenuEntries?.();
- }, [setActionBarEntries, setContextMenuEntries]);
-
useEffect(() => {
setOnEntityCountChange(
() => (entityCount: number) => setRecordCountInCurrentView(entityCount),
diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx
deleted file mode 100644
index 0b2c810bc15c..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useRecoilValue } from 'recoil';
-
-import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
-import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
-import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
-import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState';
-
-export const RecordTableActionBar = ({
- recordTableId,
-}: {
- recordTableId: string;
-}) => {
- const {
- selectedRowIdsSelector,
- tableRowIdsState,
- hasUserSelectedAllRowsState,
- } = useRecordTableStates(recordTableId);
-
- // TODO: verify this instance id works
- const entityCountInCurrentView = useRecoilComponentValueV2(
- entityCountInCurrentViewComponentState,
- recordTableId,
- );
-
- const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
- const tableRowIds = useRecoilValue(tableRowIdsState);
- const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
-
- const totalNumberOfSelectedRecords =
- hasUserSelectedAllRows && entityCountInCurrentView
- ? selectedRowIds.length === tableRowIds.length
- ? entityCountInCurrentView
- : entityCountInCurrentView -
- (tableRowIds.length - selectedRowIds.length) // unselected row Ids
- : selectedRowIds.length;
-
- if (!selectedRowIds.length) {
- return null;
- }
-
- return (
-
- );
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
index 6d5c5b7e0e75..8e827ae3e4e6 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
@@ -12,7 +12,7 @@ import {
OpenTableCellArgs,
useOpenRecordTableCellV2,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
-import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu';
+import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
@@ -75,12 +75,15 @@ export const RecordTableContextProvider = ({
moveSoftFocusToCell(cellPosition);
};
- const { triggerContextMenu } = useTriggerContextMenu({
+ const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
recordTableId,
});
- const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
- triggerContextMenu(event, recordId);
+ const handleActionMenuDropdown = (
+ event: React.MouseEvent,
+ recordId: string,
+ ) => {
+ triggerActionMenuDropdown(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
@@ -99,7 +102,7 @@ export const RecordTableContextProvider = ({
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
- onContextMenu: handleContextMenu,
+ onActionMenuDropdownOpened: handleActionMenuDropdown,
onCellMouseEnter: handleContainerMouseEnter,
visibleTableColumns,
recordTableId,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx
index 8b94a325f36a..0dff4b429dca 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx
@@ -46,7 +46,7 @@ export const RecordTableInternalEffect = ({
useListenClickOutsideByClassName({
classNames: ['entity-table-cell'],
- excludeClassNames: ['action-bar', 'context-menu'],
+ excludeClassNames: ['bottom-bar', 'context-menu'],
callback: () => {
resetTableRowSelection();
},
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
index 268608e183cc..b7a46e64829d 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
@@ -81,7 +81,9 @@ export const RecordTableWithWrappers = ({
/>
{
+ resetTableRowSelection();
+ }}
onDragSelectionChange={setRowSelected}
/>
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx
index b866e344842f..b03187ceae68 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx
@@ -70,7 +70,7 @@ const meta: Meta = {
onMoveFocus: () => {},
onCloseTableCell: () => {},
onMoveSoftFocusToCell: () => {},
- onContextMenu: () => {},
+ onActionMenuDropdownOpened: () => {},
onCellMouseEnter: () => {},
visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
diff --git a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx
deleted file mode 100644
index d9f712ef82e4..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useRecoilValue } from 'recoil';
-
-import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
-import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
-
-export const RecordTableContextMenu = ({
- recordTableId,
-}: {
- recordTableId: string;
-}) => {
- const { selectedRowIdsSelector } = useRecordTableStates(recordTableId);
-
- const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
-
- if (!selectedRowIds.length) {
- return null;
- }
-
- return ;
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
index cf8a8afed861..ba2ea9f7a92d 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
@@ -24,7 +24,10 @@ export type RecordTableContextProps = {
onMoveFocus: (direction: MoveFocusDirection) => void;
onCloseTableCell: () => void;
onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
- onContextMenu: (event: React.MouseEvent, recordId: string) => void;
+ onActionMenuDropdownOpened: (
+ event: React.MouseEvent,
+ recordId: string,
+ ) => void;
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
visibleTableColumns: ColumnDefinition[];
recordTableId: string;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
index 1b6263739111..58779a443da2 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
@@ -1,7 +1,9 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useResetTableRowSelection = (recordTableId?: string) => {
const {
@@ -20,7 +22,19 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
}
set(hasUserSelectedAllRowsState, false);
+
+ const isActionMenuDropdownOpenState = extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ );
+
+ set(isActionMenuDropdownOpenState, false);
},
- [tableRowIdsState, isRowSelectedFamilyState, hasUserSelectedAllRowsState],
+ [
+ tableRowIdsState,
+ hasUserSelectedAllRowsState,
+ recordTableId,
+ isRowSelectedFamilyState,
+ ],
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
index 1e977ecaec4e..52ae0772d769 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
@@ -71,10 +71,10 @@ export const RecordTableCellBaseContainer = ({
}
};
- const { onContextMenu } = useContext(RecordTableContext);
+ const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
- const handleContextMenu = (event: React.MouseEvent) => {
- onContextMenu(event, recordId);
+ const handleActionMenuDropdown = (event: React.MouseEvent) => {
+ onActionMenuDropdownOpened(event, recordId);
};
const { hotkeyScope } = useContext(FieldContext);
@@ -87,7 +87,7 @@ export const RecordTableCellBaseContainer = ({
onMouseLeave={handleContainerMouseLeave}
onMouseMove={handleContainerMouseMove}
onClick={handleContainerClick}
- onContextMenu={handleContextMenu}
+ onContextMenu={handleActionMenuDropdown}
backgroundColorTransparentSecondary={
theme.background.transparent.secondary
}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
index 645e2bc4ab6e..459211537244 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
@@ -1,13 +1,12 @@
import styled from '@emotion/styled';
import { useCallback, useContext } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
+import { useRecoilValue } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from '@/ui/input/components/Checkbox';
-import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
const StyledContainer = styled.div`
align-items: center;
@@ -24,14 +23,12 @@ export const RecordTableCellCheckbox = () => {
const { recordId } = useContext(RecordTableRowContext);
const { isRowSelectedFamilyState } = useRecordTableStates();
- const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const handleClick = useCallback(() => {
setCurrentRowSelected(!currentRowSelected);
- setActionBarOpenState(true);
- }, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
+ }, [currentRowSelected, setCurrentRowSelected]);
return (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts
new file mode 100644
index 000000000000..0870b7bb4d5a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts
@@ -0,0 +1,67 @@
+import { useRecoilCallback } from 'recoil';
+
+import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
+import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
+import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
+import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
+
+export const useTriggerActionMenuDropdown = ({
+ recordTableId,
+}: {
+ recordTableId: string;
+}) => {
+ const triggerActionMenuDropdown = useRecoilCallback(
+ ({ set, snapshot }) =>
+ (event: React.MouseEvent, recordId: string) => {
+ event.preventDefault();
+
+ const tableScopeId = getScopeIdFromComponentId(recordTableId);
+
+ set(
+ extractComponentState(
+ actionMenuDropdownPositionComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ ),
+ {
+ x: event.clientX,
+ y: event.clientY,
+ },
+ );
+
+ const isRowSelectedFamilyState = extractComponentFamilyState(
+ isRowSelectedComponentFamilyState,
+ tableScopeId,
+ );
+
+ const isRowSelected = getSnapshotValue(
+ snapshot,
+ isRowSelectedFamilyState(recordId),
+ );
+
+ if (isRowSelected !== true) {
+ set(isRowSelectedFamilyState(recordId), true);
+ }
+
+ const isActionMenuDropdownOpenState = extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ );
+
+ const isActionBarOpenState = isBottomBarOpenedComponentState.atomFamily(
+ {
+ instanceId: `action-bar-${recordTableId}`,
+ },
+ );
+
+ set(isActionBarOpenState, false);
+ set(isActionMenuDropdownOpenState, true);
+ },
+ [recordTableId],
+ );
+
+ return { triggerActionMenuDropdown };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts
deleted file mode 100644
index b9c04a8b7769..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useRecoilCallback } from 'recoil';
-
-import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
-import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
-import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
-import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
-import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
-
-export const useTriggerContextMenu = ({
- recordTableId,
-}: {
- recordTableId: string;
-}) => {
- const triggerContextMenu = useRecoilCallback(
- ({ set, snapshot }) =>
- (event: React.MouseEvent, recordId: string) => {
- event.preventDefault();
-
- const tableScopeId = getScopeIdFromComponentId(recordTableId);
-
- set(contextMenuPositionState, {
- x: event.clientX,
- y: event.clientY,
- });
- set(contextMenuIsOpenState, true);
-
- const isRowSelectedFamilyState = extractComponentFamilyState(
- isRowSelectedComponentFamilyState,
- tableScopeId,
- );
-
- const isRowSelected = getSnapshotValue(
- snapshot,
- isRowSelectedFamilyState(recordId),
- );
-
- if (isRowSelected !== true) {
- set(isRowSelectedFamilyState(recordId), true);
- }
- },
- [recordTableId],
- );
-
- return { triggerContextMenu };
-};
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
index f7fdf0218239..2bad3fa95ffc 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
@@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
-import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions';
import { SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions';
@@ -23,14 +22,10 @@ export const SignInBackgroundMockContainerEffect = ({
recordTableId,
viewId,
}: SignInBackgroundMockContainerEffectProps) => {
- const {
- setAvailableTableColumns,
- setOnEntityCountChange,
- setTableColumns,
- resetTableRowSelection,
- } = useRecordTable({
- recordTableId,
- });
+ const { setAvailableTableColumns, setOnEntityCountChange, setTableColumns } =
+ useRecordTable({
+ recordTableId,
+ });
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
@@ -75,17 +70,6 @@ export const SignInBackgroundMockContainerEffect = ({
setTableColumns,
]);
- const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
- objectMetadataItem,
- selectedRecordIds: [],
- callback: resetTableRowSelection,
- });
-
- useEffect(() => {
- setActionBarEntries?.();
- setContextMenuEntries?.();
- }, [setActionBarEntries, setContextMenuEntries]);
-
useEffect(() => {
setOnEntityCountChange(
() => (entityCount: number) => setRecordCountInCurrentView(entityCount),
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
index 6ce3a80a5d4a..2b17a655549d 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
@@ -2,8 +2,6 @@ import styled from '@emotion/styled';
import { IconBuildingSkyscraper } from 'twenty-ui';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
-import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
-import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
import { SignInBackgroundMockContainer } from '@/sign-in-background-mock/components/SignInBackgroundMockContainer';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody';
@@ -29,8 +27,6 @@ export const SignInBackgroundMockPage = () => {
-
-
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx
new file mode 100644
index 000000000000..46ccff887a7a
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx
@@ -0,0 +1,61 @@
+import styled from '@emotion/styled';
+
+import { useBottomBarInternalHotkeyScopeManagement } from '@/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement';
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+
+const StyledContainerActionBar = styled.div`
+ align-items: center;
+ background: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ bottom: 38px;
+ box-shadow: ${({ theme }) => theme.boxShadow.strong};
+ display: flex;
+ height: 48px;
+ width: max-content;
+ left: 50%;
+ padding-left: ${({ theme }) => theme.spacing(2)};
+ padding-right: ${({ theme }) => theme.spacing(2)};
+ position: absolute;
+ top: auto;
+
+ transform: translateX(-50%);
+ z-index: 1;
+`;
+
+type BottomBarProps = {
+ bottomBarId: string;
+ bottomBarHotkeyScopeFromParent: HotkeyScope;
+ children: React.ReactNode;
+};
+
+export const BottomBar = ({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+ children,
+}: BottomBarProps) => {
+ const isBottomBarOpen = useRecoilComponentValueV2(
+ isBottomBarOpenedComponentState,
+ bottomBarId,
+ );
+
+ useBottomBarInternalHotkeyScopeManagement({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+ });
+
+ if (!isBottomBarOpen) {
+ return null;
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx
new file mode 100644
index 000000000000..8562f67cdce3
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx
@@ -0,0 +1,65 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { IconPlus } from 'twenty-ui';
+
+import { Button } from '@/ui/input/button/components/Button';
+import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import styled from '@emotion/styled';
+import { RecoilRoot } from 'recoil';
+
+const StyledContainer = styled.div`
+ display: flex;
+ gap: 10px;
+`;
+
+const meta: Meta = {
+ title: 'UI/Layout/BottomBar/BottomBar',
+ component: BottomBar,
+ args: {
+ bottomBarId: 'test',
+ bottomBarHotkeyScopeFromParent: { scope: 'test' },
+ children: (
+
+
+
+
+
+ ),
+ },
+ argTypes: {
+ bottomBarId: { control: false },
+ bottomBarHotkeyScopeFromParent: { control: false },
+ children: { control: false },
+ },
+};
+
+export default meta;
+
+export const Default: StoryObj = {
+ decorators: [
+ (Story) => (
+ {
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: 'test',
+ }),
+ true,
+ );
+ }}
+ >
+
+
+ ),
+ ],
+};
+
+export const Closed: StoryObj = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts
new file mode 100644
index 000000000000..bfae8a470fac
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts
@@ -0,0 +1,87 @@
+import { useRecoilCallback } from 'recoil';
+
+import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { isDefined } from '~/utils/isDefined';
+
+export const useBottomBar = () => {
+ const {
+ setHotkeyScopeAndMemorizePreviousScope,
+ goBackToPreviousHotkeyScope,
+ } = usePreviousHotkeyScope();
+
+ const closeBottomBar = useRecoilCallback(
+ ({ set }) =>
+ (specificComponentId: string) => {
+ goBackToPreviousHotkeyScope();
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ false,
+ );
+ },
+ [goBackToPreviousHotkeyScope],
+ );
+
+ const openBottomBar = useRecoilCallback(
+ ({ set, snapshot }) =>
+ (specificComponentId: string, customHotkeyScope?: HotkeyScope) => {
+ const bottomBarHotkeyScope = snapshot
+ .getLoadable(
+ bottomBarHotkeyComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ )
+ .getValue();
+
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ true,
+ );
+
+ if (isDefined(customHotkeyScope)) {
+ setHotkeyScopeAndMemorizePreviousScope(
+ customHotkeyScope.scope,
+ customHotkeyScope.customScopes,
+ );
+ } else if (isDefined(bottomBarHotkeyScope)) {
+ setHotkeyScopeAndMemorizePreviousScope(
+ bottomBarHotkeyScope.scope,
+ bottomBarHotkeyScope.customScopes,
+ );
+ }
+ },
+ [setHotkeyScopeAndMemorizePreviousScope],
+ );
+
+ const toggleBottomBar = useRecoilCallback(
+ ({ snapshot }) =>
+ (specificComponentId: string) => {
+ const isBottomBarOpen = snapshot
+ .getLoadable(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ )
+ .getValue();
+
+ if (isBottomBarOpen) {
+ closeBottomBar(specificComponentId);
+ } else {
+ openBottomBar(specificComponentId);
+ }
+ },
+ [closeBottomBar, openBottomBar],
+ );
+
+ return {
+ closeBottomBar,
+ openBottomBar,
+ toggleBottomBar,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts
new file mode 100644
index 000000000000..a35ccfc69ead
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+
+import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+
+export const useBottomBarInternalHotkeyScopeManagement = ({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+}: {
+ bottomBarId?: string;
+ bottomBarHotkeyScopeFromParent?: HotkeyScope;
+}) => {
+ const [bottomBarHotkeyScope, setBottomBarHotkeyScope] =
+ useRecoilComponentStateV2(bottomBarHotkeyComponentState, bottomBarId);
+
+ useEffect(() => {
+ if (!isDeeplyEqual(bottomBarHotkeyScopeFromParent, bottomBarHotkeyScope)) {
+ setBottomBarHotkeyScope(bottomBarHotkeyScopeFromParent);
+ }
+ }, [
+ bottomBarHotkeyScope,
+ bottomBarHotkeyScopeFromParent,
+ setBottomBarHotkeyScope,
+ ]);
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts
new file mode 100644
index 000000000000..89144d6c8c30
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts
@@ -0,0 +1,11 @@
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+
+export const bottomBarHotkeyComponentState = createComponentStateV2<
+ HotkeyScope | null | undefined
+>({
+ key: 'bottomBarHotkeyComponentState',
+ defaultValue: null,
+ componentInstanceContext: BottomBarInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx
new file mode 100644
index 000000000000..e2b29e54cfc3
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx
@@ -0,0 +1,3 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const BottomBarInstanceContext = createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts
new file mode 100644
index 000000000000..071ad8de53cf
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts
@@ -0,0 +1,8 @@
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+
+export const isBottomBarOpenedComponentState = createComponentStateV2({
+ key: 'isBottomBarOpenedComponentState',
+ defaultValue: false,
+ componentInstanceContext: BottomBarInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx
deleted file mode 100644
index c020f42709c6..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import styled from '@emotion/styled';
-import { useEffect, useRef } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
-
-import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-import { isDefined } from '~/utils/isDefined';
-import { ActionBarItem } from './ActionBarItem';
-
-type ActionBarProps = {
- selectedIds?: string[];
- totalNumberOfSelectedRecords?: number;
-};
-
-const StyledContainerActionBar = styled.div`
- align-items: center;
- background: ${({ theme }) => theme.background.secondary};
- border: 1px solid ${({ theme }) => theme.border.color.medium};
- border-radius: ${({ theme }) => theme.border.radius.md};
- bottom: 38px;
- box-shadow: ${({ theme }) => theme.boxShadow.strong};
- display: flex;
- height: 48px;
- width: max-content;
- left: 50%;
- padding-left: ${({ theme }) => theme.spacing(2)};
- padding-right: ${({ theme }) => theme.spacing(2)};
- position: absolute;
- top: auto;
-
- transform: translateX(-50%);
- z-index: 1;
-`;
-
-const StyledLabel = styled.div`
- color: ${({ theme }) => theme.font.color.tertiary};
- font-size: ${({ theme }) => theme.font.size.md};
- font-weight: ${({ theme }) => theme.font.weight.medium};
- padding-left: ${({ theme }) => theme.spacing(2)};
- padding-right: ${({ theme }) => theme.spacing(2)};
-`;
-
-export const ActionBar = ({
- selectedIds = [],
- totalNumberOfSelectedRecords,
-}: ActionBarProps) => {
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
-
- useEffect(() => {
- if (selectedIds && selectedIds.length > 1) {
- setContextMenuOpenState(false);
- }
- }, [selectedIds, setContextMenuOpenState]);
-
- const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
- const actionBarEntries = useRecoilValue(actionBarEntriesState);
- const wrapperRef = useRef(null);
-
- if (contextMenuIsOpen) {
- return null;
- }
-
- const selectedNumberLabel =
- totalNumberOfSelectedRecords ?? selectedIds?.length;
-
- const showSelectedNumberLabel =
- isDefined(totalNumberOfSelectedRecords) || Array.isArray(selectedIds);
-
- return (
- <>
-
- {showSelectedNumberLabel && (
- {selectedNumberLabel} selected:
- )}
- {actionBarEntries.map((item, index) => (
-
- ))}
-
-
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx
deleted file mode 100644
index dbb7623d25d4..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { IconChevronDown } from 'twenty-ui';
-
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
-import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
-
-type ActionBarItemProps = {
- item: ActionBarEntry;
-};
-
-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.tertiary};
- }
-`;
-
-const StyledButtonLabel = styled.div`
- font-weight: ${({ theme }) => theme.font.weight.medium};
- margin-left: ${({ theme }) => theme.spacing(1)};
-`;
-
-export const ActionBarItem = ({ item }: ActionBarItemProps) => {
- const theme = useTheme();
- const dropdownId = `action-bar-item-${item.label}`;
- const { toggleDropdown, closeDropdown } = useDropdown(dropdownId);
- return (
- <>
- {Array.isArray(item.subActions) ? (
-
- {item.Icon && }
- {item.label}
-
-
- }
- dropdownComponents={
-
- {item.subActions.map((subAction) => (
-
- }
- />
- ) : (
- item.onClick?.()}
- >
- {item.Icon && }
- {item.label}
-
- )}
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx
deleted file mode 100644
index 9610eb43b311..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { useSetRecoilState } from 'recoil';
-import { ComponentDecorator } from 'twenty-ui';
-
-import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
-import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
-
-import { actionBarOpenState } from '../../states/actionBarIsOpenState';
-import { ActionBar } from '../ActionBar';
-
-const FilledActionBar = () => {
- const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
- setActionBarOpenState(true);
- return ;
-};
-
-const meta: Meta = {
- title: 'UI/Navigation/ActionBar/ActionBar',
- component: FilledActionBar,
- decorators: [
- MemoryRouterDecorator,
- (Story) => (
- {}}
- >
-
-
- ),
- ComponentDecorator,
- ],
- args: { selectedIds: ['TestId'] },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts
deleted file mode 100644
index 35f8cab412b7..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { ActionBarEntry } from '../types/ActionBarEntry';
-
-export const actionBarEntriesState = createState({
- key: 'actionBarEntriesState',
- defaultValue: [],
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts
deleted file mode 100644
index 0ef918e6652e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createState } from 'twenty-ui';
-
-export const actionBarOpenState = createState({
- key: 'actionBarOpenState',
- defaultValue: false,
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts
deleted file mode 100644
index a276736cfb1c..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
-
-export type ActionBarEntry = ContextMenuEntry & {
- subActions?: ActionBarEntry[];
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx
deleted file mode 100644
index e27c6096cb07..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useRef } from 'react';
-import styled from '@emotion/styled';
-import { useRecoilValue } from 'recoil';
-
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
-import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-import { contextMenuEntriesState } from '../states/contextMenuEntriesState';
-import { contextMenuIsOpenState } from '../states/contextMenuIsOpenState';
-import { PositionType } from '../types/PositionType';
-
-import { ContextMenuItem } from './ContextMenuItem';
-
-type StyledContainerProps = {
- position: PositionType;
-};
-
-const StyledContainerContextMenu = styled.div`
- align-items: flex-start;
- background: ${({ theme }) => theme.background.secondary};
- border: 1px solid ${({ theme }) => theme.border.color.light};
- border-radius: ${({ theme }) => theme.border.radius.md};
- box-shadow: ${({ theme }) => theme.boxShadow.strong};
- display: flex;
- flex-direction: column;
- gap: 1px;
-
- left: ${(props) => `${props.position.x}px`};
- position: fixed;
- top: ${(props) => `${props.position.y}px`};
-
- transform: translateX(-50%);
- width: auto;
- z-index: 2;
-`;
-
-export const ContextMenu = () => {
- const contextMenuPosition = useRecoilValue(contextMenuPositionState);
- const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
- const contextMenuEntries = useRecoilValue(contextMenuEntriesState);
- const wrapperRef = useRef(null);
- const actionBarEntries = useRecoilValue(actionBarEntriesState);
-
- if (!contextMenuIsOpen) {
- return null;
- }
-
- const width = contextMenuEntries.some(
- (contextMenuEntry) => contextMenuEntry.label === 'Remove from favorites',
- )
- ? 200
- : undefined;
-
- return (
- <>
-
-
-
- {contextMenuEntries.map((item, index) => {
- return ;
- })}
-
-
-
-
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx
deleted file mode 100644
index 4ec9822a6750..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-
-type ContextMenuItemProps = {
- item: ContextMenuEntry;
-};
-
-export const ContextMenuItem = ({ item }: ContextMenuItemProps) => (
-
-);
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx
deleted file mode 100644
index 2b2f29e5c4ee..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { useSetRecoilState } from 'recoil';
-import { ComponentDecorator } from 'twenty-ui';
-
-import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
-import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
-
-import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState';
-import { contextMenuPositionState } from '../../states/contextMenuPositionState';
-import { ContextMenu } from '../ContextMenu';
-
-const FilledContextMenu = () => {
- const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
- setContextMenuPosition({
- x: 100,
- y: 10,
- });
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
- setContextMenuOpenState(true);
- return ;
-};
-
-const meta: Meta = {
- title: 'UI/Navigation/ContextMenu/ContextMenu',
- component: FilledContextMenu,
- decorators: [
- MemoryRouterDecorator,
- (Story) => (
- {}}
- >
-
-
- ),
- ComponentDecorator,
- ],
- args: { selectedIds: ['TestId'] },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts
deleted file mode 100644
index 1e22b562187e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { ContextMenuEntry } from '../types/ContextMenuEntry';
-
-export const contextMenuEntriesState = createState({
- key: 'contextMenuEntriesState',
- defaultValue: [],
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts
deleted file mode 100644
index d5aec39b905e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createState } from 'twenty-ui';
-
-export const contextMenuIsOpenState = createState({
- key: 'contextMenuIsOpenState',
- defaultValue: false,
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts
deleted file mode 100644
index a47df13eb017..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { PositionType } from '@/ui/navigation/context-menu/types/PositionType';
-
-export const contextMenuPositionState = createState({
- key: 'contextMenuPositionState',
- defaultValue: {
- x: null,
- y: null,
- },
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts
deleted file mode 100644
index ceae88f5beea..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type ContextMenuItemAccent = 'default' | 'danger';
diff --git a/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx
deleted file mode 100644
index becc1b72ab66..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { IconTrash } from 'twenty-ui';
-
-import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-const meta: Meta = {
- title: 'UI/Navigation/Shared/SharedNavigationModal',
- component: SharedNavigationModal,
- args: {
- actionBarEntries: [
- {
- ConfirmationModal: (
- {}}
- setIsOpen={() => {}}
- isOpen={false}
- subtitle="Subtitle"
- />
- ),
- Icon: IconTrash,
- label: 'Label',
- onClick: () => {},
- },
- ],
- customClassName: 'customClassName',
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx b/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx
deleted file mode 100644
index eeba07087165..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
-
-type SharedNavigationModalProps = {
- actionBarEntries: ActionBarEntry[];
- customClassName: string;
-};
-
-const SharedNavigationModal = ({
- actionBarEntries,
- customClassName,
-}: SharedNavigationModalProps) => {
- return (
-
- {actionBarEntries.map((actionBarEntry, index) =>
- actionBarEntry.ConfirmationModal ? (
-
{actionBarEntry.ConfirmationModal}
- ) : null,
- )}
-
- );
-};
-
-export default SharedNavigationModal;
diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
index 8c1bd4be4b79..6c3a0f96ab81 100644
--- a/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
+++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
@@ -35,7 +35,7 @@ export const isNonTextWritingKey = (key: string) => {
'Delete',
'End',
'PageDown',
- 'ContextMenu',
+ 'ActionMenuDropdown',
'PrintScreen',
'BrowserBack',
'BrowserForward',
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx
new file mode 100644
index 000000000000..e40a00da25ee
--- /dev/null
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx
@@ -0,0 +1,15 @@
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { useEffect } from 'react';
+import { useSetRecoilState } from 'recoil';
+
+export const RecordShowPageEffect = ({ recordId }: { recordId: string }) => {
+ const setContextStoreTargetedRecordIds = useSetRecoilState(
+ contextStoreTargetedRecordIdsState,
+ );
+
+ useEffect(() => {
+ setContextStoreTargetedRecordIds([recordId]);
+ }, [recordId, setContextStoreTargetedRecordIds]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
index 11774aba44bf..ba9c9dfeb224 100644
--- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
+++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
@@ -24,7 +24,7 @@ export const RecordTableDecorator: Decorator = (Story) => {
onCellMouseEnter: () => {},
onCloseTableCell: () => {},
onOpenTableCell: () => {},
- onContextMenu: () => {},
+ onActionMenuDropdownOpened: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCell: () => {},
onUpsertRecord: () => {},
diff --git a/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts b/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts
new file mode 100644
index 000000000000..84a8179ef036
--- /dev/null
+++ b/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts
@@ -0,0 +1,19 @@
+import { turnIntoEmptyStringIfWhitespacesOnly } from '../turnIntoEmptyStringIfWhitespacesOnly';
+
+describe('turnIntoEmptyStringIfWhitespacesOnly', () => {
+ it('should return an empty string for whitespace-only input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' ')).toBe('');
+ expect(turnIntoEmptyStringIfWhitespacesOnly('\t\n ')).toBe('');
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' \n\r\t')).toBe('');
+ });
+
+ it('should return the original string for non-whitespace input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly('hello')).toBe('hello');
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' hello ')).toBe(' hello ');
+ expect(turnIntoEmptyStringIfWhitespacesOnly('123')).toBe('123');
+ });
+
+ it('should handle empty string input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly('')).toBe('');
+ });
+});
diff --git a/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts b/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts
new file mode 100644
index 000000000000..fd1a2e105eaa
--- /dev/null
+++ b/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts
@@ -0,0 +1,19 @@
+import { turnIntoUndefinedIfWhitespacesOnly } from '../turnIntoUndefinedIfWhitespacesOnly';
+
+describe('turnIntoUndefinedIfWhitespacesOnly', () => {
+ it('should return undefined for whitespace-only input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly(' ')).toBeUndefined();
+ expect(turnIntoUndefinedIfWhitespacesOnly('\t\n ')).toBeUndefined();
+ expect(turnIntoUndefinedIfWhitespacesOnly(' \n\r\t')).toBeUndefined();
+ });
+
+ it('should return the original string for non-whitespace input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly('hello')).toBe('hello');
+ expect(turnIntoUndefinedIfWhitespacesOnly(' hello ')).toBe(' hello ');
+ expect(turnIntoUndefinedIfWhitespacesOnly('123')).toBe('123');
+ });
+
+ it('should handle empty string input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly('')).toBeUndefined();
+ });
+});
diff --git a/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts b/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
index 1f2d257a5917..5fb13d010bad 100644
--- a/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
+++ b/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
@@ -1,5 +1,5 @@
export const turnIntoUndefinedIfWhitespacesOnly = (
value: string,
): string | undefined => {
- return value.trim() === '' ? undefined : value.trim();
+ return value.trim() === '' ? undefined : value;
};