7338 refactor actionbar and contextmenu to use the context store (#7462)
Closes #7338
This commit is contained in:
@ -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 (
|
||||
<BottomBarInstanceContext.Provider value={{ instanceId: bottomBarId }}>
|
||||
<StyledContainerActionBar data-select-disable className="bottom-bar">
|
||||
{children}
|
||||
</StyledContainerActionBar>
|
||||
</BottomBarInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -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<typeof BottomBar> = {
|
||||
title: 'UI/Layout/BottomBar/BottomBar',
|
||||
component: BottomBar,
|
||||
args: {
|
||||
bottomBarId: 'test',
|
||||
bottomBarHotkeyScopeFromParent: { scope: 'test' },
|
||||
children: (
|
||||
<StyledContainer>
|
||||
<Button title="Test 1" Icon={IconPlus} />
|
||||
<Button title="Test 2" Icon={IconPlus} />
|
||||
<Button title="Test 3" Icon={IconPlus} />
|
||||
</StyledContainer>
|
||||
),
|
||||
},
|
||||
argTypes: {
|
||||
bottomBarId: { control: false },
|
||||
bottomBarHotkeyScopeFromParent: { control: false },
|
||||
children: { control: false },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof BottomBar> = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
isBottomBarOpenedComponentState.atomFamily({
|
||||
instanceId: 'test',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</RecoilRoot>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Closed: StoryObj<typeof BottomBar> = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilRoot>
|
||||
<Story />
|
||||
</RecoilRoot>
|
||||
),
|
||||
],
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
]);
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const BottomBarInstanceContext = createComponentInstanceContext();
|
||||
@ -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<boolean>({
|
||||
key: 'isBottomBarOpenedComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: BottomBarInstanceContext,
|
||||
});
|
||||
@ -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<HTMLDivElement>(null);
|
||||
|
||||
if (contextMenuIsOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedNumberLabel =
|
||||
totalNumberOfSelectedRecords ?? selectedIds?.length;
|
||||
|
||||
const showSelectedNumberLabel =
|
||||
isDefined(totalNumberOfSelectedRecords) || Array.isArray(selectedIds);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainerActionBar
|
||||
data-select-disable
|
||||
className="action-bar"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
{showSelectedNumberLabel && (
|
||||
<StyledLabel>{selectedNumberLabel} selected:</StyledLabel>
|
||||
)}
|
||||
{actionBarEntries.map((item, index) => (
|
||||
<ActionBarItem key={index} item={item} />
|
||||
))}
|
||||
</StyledContainerActionBar>
|
||||
<SharedNavigationModal
|
||||
actionBarEntries={actionBarEntries}
|
||||
customClassName="action-bar"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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) ? (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="top-start"
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
clickableComponent={
|
||||
<StyledButton
|
||||
accent={item.accent ?? 'default'}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{item.Icon && <item.Icon size={theme.icon.size.md} />}
|
||||
<StyledButtonLabel>{item.label}</StyledButtonLabel>
|
||||
<IconChevronDown size={theme.icon.size.md} />
|
||||
</StyledButton>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{item.subActions.map((subAction) => (
|
||||
<MenuItem
|
||||
key={subAction.label}
|
||||
text={subAction.label}
|
||||
LeftIcon={subAction.Icon}
|
||||
onClick={() => {
|
||||
closeDropdown();
|
||||
subAction.onClick?.();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<StyledButton
|
||||
accent={item.accent ?? 'default'}
|
||||
onClick={() => item.onClick?.()}
|
||||
>
|
||||
{item.Icon && <item.Icon size={theme.icon.size.md} />}
|
||||
<StyledButtonLabel>{item.label}</StyledButtonLabel>
|
||||
</StyledButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 <ActionBar />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ActionBar> = {
|
||||
title: 'UI/Navigation/ActionBar/ActionBar',
|
||||
component: FilledActionBar,
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<RecordTableScope
|
||||
recordTableScopeId="companies"
|
||||
onColumnsChange={() => {}}
|
||||
>
|
||||
<Story />
|
||||
</RecordTableScope>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
args: { selectedIds: ['TestId'] },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ActionBar>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -1,8 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { ActionBarEntry } from '../types/ActionBarEntry';
|
||||
|
||||
export const actionBarEntriesState = createState<ActionBarEntry[]>({
|
||||
key: 'actionBarEntriesState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const actionBarOpenState = createState<boolean>({
|
||||
key: 'actionBarOpenState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,5 +0,0 @@
|
||||
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
|
||||
|
||||
export type ActionBarEntry = ContextMenuEntry & {
|
||||
subActions?: ActionBarEntry[];
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export type ActionBarItemAccent = 'standard' | 'danger';
|
||||
@ -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<StyledContainerProps>`
|
||||
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<HTMLDivElement>(null);
|
||||
const actionBarEntries = useRecoilValue(actionBarEntriesState);
|
||||
|
||||
if (!contextMenuIsOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = contextMenuEntries.some(
|
||||
(contextMenuEntry) => contextMenuEntry.label === 'Remove from favorites',
|
||||
)
|
||||
? 200
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainerContextMenu
|
||||
className="context-menu"
|
||||
ref={wrapperRef}
|
||||
position={contextMenuPosition}
|
||||
>
|
||||
<DropdownMenu data-select-disable width={width}>
|
||||
<DropdownMenuItemsContainer>
|
||||
{contextMenuEntries.map((item, index) => {
|
||||
return <ContextMenuItem key={index} item={item} />;
|
||||
})}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</StyledContainerContextMenu>
|
||||
<SharedNavigationModal
|
||||
actionBarEntries={actionBarEntries}
|
||||
customClassName="context-menu"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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) => (
|
||||
<MenuItem
|
||||
LeftIcon={item.Icon}
|
||||
onClick={item.onClick}
|
||||
accent={item.accent}
|
||||
text={item.label}
|
||||
/>
|
||||
);
|
||||
@ -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 <ContextMenu />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ContextMenu> = {
|
||||
title: 'UI/Navigation/ContextMenu/ContextMenu',
|
||||
component: FilledContextMenu,
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
(Story) => (
|
||||
<RecordTableScope
|
||||
recordTableScopeId="companies"
|
||||
onColumnsChange={() => {}}
|
||||
>
|
||||
<Story />
|
||||
</RecordTableScope>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
args: { selectedIds: ['TestId'] },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ContextMenu>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -1,8 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { ContextMenuEntry } from '../types/ContextMenuEntry';
|
||||
|
||||
export const contextMenuEntriesState = createState<ContextMenuEntry[]>({
|
||||
key: 'contextMenuEntriesState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const contextMenuIsOpenState = createState<boolean>({
|
||||
key: 'contextMenuIsOpenState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { PositionType } from '@/ui/navigation/context-menu/types/PositionType';
|
||||
|
||||
export const contextMenuPositionState = createState<PositionType>({
|
||||
key: 'contextMenuPositionState',
|
||||
defaultValue: {
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
||||
|
||||
export type ContextMenuEntry = {
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
accent?: MenuItemAccent;
|
||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||
ConfirmationModal?: ReactNode;
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export type ContextMenuItemAccent = 'default' | 'danger';
|
||||
@ -1,4 +0,0 @@
|
||||
export type PositionType = {
|
||||
x: number | null;
|
||||
y: number | 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<typeof SharedNavigationModal> = {
|
||||
title: 'UI/Navigation/Shared/SharedNavigationModal',
|
||||
component: SharedNavigationModal,
|
||||
args: {
|
||||
actionBarEntries: [
|
||||
{
|
||||
ConfirmationModal: (
|
||||
<ConfirmationModal
|
||||
title="Title"
|
||||
deleteButtonText="Delete"
|
||||
onConfirmClick={() => {}}
|
||||
setIsOpen={() => {}}
|
||||
isOpen={false}
|
||||
subtitle="Subtitle"
|
||||
/>
|
||||
),
|
||||
Icon: IconTrash,
|
||||
label: 'Label',
|
||||
onClick: () => {},
|
||||
},
|
||||
],
|
||||
customClassName: 'customClassName',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SharedNavigationModal>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -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 (
|
||||
<div data-select-disable className={customClassName}>
|
||||
{actionBarEntries.map((actionBarEntry, index) =>
|
||||
actionBarEntry.ConfirmationModal ? (
|
||||
<div key={index}>{actionBarEntry.ConfirmationModal}</div>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedNavigationModal;
|
||||
@ -35,7 +35,7 @@ export const isNonTextWritingKey = (key: string) => {
|
||||
'Delete',
|
||||
'End',
|
||||
'PageDown',
|
||||
'ContextMenu',
|
||||
'ActionMenuDropdown',
|
||||
'PrintScreen',
|
||||
'BrowserBack',
|
||||
'BrowserForward',
|
||||
|
||||
Reference in New Issue
Block a user