Update action menu bar (#8178)

Closes #8023
This commit is contained in:
Raphaël Bosi
2024-10-30 17:22:42 +01:00
committed by GitHub
parent 7dfde04957
commit 7a5d52e88d
6 changed files with 69 additions and 33 deletions

View File

@@ -102,6 +102,7 @@ export const DeleteRecordsActionEffect = ({
position, position,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true,
onClick: () => { onClick: () => {
setIsDeleteRecordsModalOpen(true); setIsDeleteRecordsModalOpen(true);
}, },

View File

@@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
@@ -30,7 +31,9 @@ export const RecordIndexActionMenuBar = () => {
actionMenuEntriesComponentSelector, actionMenuEntriesComponentSelector,
); );
if (actionMenuEntries.length === 0) { const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (pinnedEntries.length === 0) {
return null; return null;
} }
@@ -42,9 +45,10 @@ export const RecordIndexActionMenuBar = () => {
}} }}
> >
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel> <StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{actionMenuEntries.map((entry, index) => ( {pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} /> <RecordIndexActionMenuBarEntry key={index} entry={entry} />
))} ))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar> </BottomBar>
); );
}; };

View File

@@ -0,0 +1,53 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconLayoutSidebarRightExpand } from 'twenty-ui';
const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none;
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledButtonLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledShortcutLabel = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
background: ${({ theme }) => theme.border.color.light};
height: ${({ theme, size }) => theme.spacing(size === 'sm' ? 4 : 8)};
margin: 0 ${({ theme }) => theme.spacing(1)};
width: 1px;
`;
export const RecordIndexActionMenuBarAllActionsButton = () => {
const theme = useTheme();
const { openCommandMenu } = useCommandMenu();
return (
<>
<StyledSeparator size="md" />
<StyledButton onClick={() => openCommandMenu()}>
<IconLayoutSidebarRightExpand size={theme.icon.size.md} />
<StyledButtonLabel>All Actions</StyledButtonLabel>
<StyledSeparator size="sm" />
<StyledShortcutLabel>K</StyledShortcutLabel>
</StyledButton>
</>
);
};

View File

@@ -2,31 +2,24 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
type RecordIndexActionMenuBarEntryProps = { type RecordIndexActionMenuBarEntryProps = {
entry: ActionMenuEntry; entry: ActionMenuEntry;
}; };
const StyledButton = styled.div<{ accent: MenuItemAccent }>` const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) => color: ${({ theme }) => theme.font.color.secondary};
props.accent === 'danger'
? props.theme.color.red
: props.theme.font.color.secondary};
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
transition: background 0.1s ease; transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none; user-select: none;
&:hover { &:hover {
background: ${({ theme, accent }) => background: ${({ theme }) => theme.background.tertiary};
accent === 'danger'
? theme.background.danger
: theme.background.tertiary};
} }
`; `;
@@ -40,10 +33,7 @@ export const RecordIndexActionMenuBarEntry = ({
}: RecordIndexActionMenuBarEntryProps) => { }: RecordIndexActionMenuBarEntryProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<StyledButton <StyledButton onClick={() => entry.onClick?.()}>
accent={entry.accent ?? 'default'}
onClick={() => entry.onClick?.()}
>
{entry.Icon && <entry.Icon size={theme.icon.size.md} />} {entry.Icon && <entry.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{entry.label}</StyledButtonLabel> <StyledButtonLabel>{entry.label}</StyledButtonLabel>
</StyledButton> </StyledButton>

View File

@@ -10,15 +10,15 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test'; import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui'; import { IconTrash, RouterDecorator } from 'twenty-ui';
const deleteMock = jest.fn(); const deleteMock = jest.fn();
const markAsDoneMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = { const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar', title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar, component: RecordIndexActionMenuBar,
decorators: [ decorators: [
RouterDecorator,
(Story) => ( (Story) => (
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }} value={{ instanceId: 'story-action-menu' }}
@@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
[ [
'delete', 'delete',
{ {
isPinned: true,
key: 'delete', key: 'delete',
label: 'Delete', label: 'Delete',
position: 0, position: 0,
@@ -55,16 +56,6 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
onClick: deleteMock, onClick: deleteMock,
}, },
], ],
[
'markAsDone',
{
key: 'markAsDone',
label: 'Mark as done',
position: 1,
Icon: IconCheckbox,
onClick: markAsDoneMock,
},
],
]), ]),
); );
set( set(
@@ -120,12 +111,8 @@ export const WithButtonClicks: Story = {
const deleteButton = await canvas.findByText('Delete'); const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton); await userEvent.click(deleteButton);
const markAsDoneButton = await canvas.findByText('Mark as done');
await userEvent.click(markAsDoneButton);
await waitFor(() => { await waitFor(() => {
expect(deleteMock).toHaveBeenCalled(); expect(deleteMock).toHaveBeenCalled();
expect(markAsDoneMock).toHaveBeenCalled();
}); });
}, },
}; };

View File

@@ -8,6 +8,7 @@ export type ActionMenuEntry = {
label: string; label: string;
position: number; position: number;
Icon: IconComponent; Icon: IconComponent;
isPinned?: boolean;
accent?: MenuItemAccent; accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void; onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode; ConfirmationModal?: ReactNode;