New sidemenu for notes editor (#6527)
@Bonapara @lucasbordeau This PR addresses issue #6489: - Created an entire sidemenu for the block editor. ## Review Request Please review the implementation of the custom sidemenu. ## Outstanding Issues 1. Sidemenu Positioning: - The current placement is determined by the BlockNote package. - I need assistance in positioning it according to the Figma designs. - Attempted adding margin to the sidemenu, but this solution doesn't scale well across different screen sizes. 2. Props Spreading in `CustomSidemenu.tsx`: - Unable to avoid props spreading due to the third-party BlockNote components. - Added eslint-disable comments as a temporary solution. Your insights on these challenges would be greatly appreciated, especially regarding the sidemenu positioning and any potential alternatives to props spreading. https://github.com/user-attachments/assets/4914a037-a115-4189-88bc-a41d121d309d --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -7,6 +7,7 @@ import { ClipboardEvent } from 'react';
|
|||||||
|
|
||||||
import { blockSchema } from '@/activities/blocks/schema';
|
import { blockSchema } from '@/activities/blocks/schema';
|
||||||
import { getSlashMenu } from '@/activities/blocks/slashMenu';
|
import { getSlashMenu } from '@/activities/blocks/slashMenu';
|
||||||
|
import { CustomSideMenu } from '@/ui/input/editor/components/CustomSideMenu';
|
||||||
import {
|
import {
|
||||||
CustomSlashMenu,
|
CustomSlashMenu,
|
||||||
SuggestionItem,
|
SuggestionItem,
|
||||||
@ -35,7 +36,12 @@ const StyledEditor = styled.div`
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
& .mantine-ActionIcon-icon {
|
& .mantine-ActionIcon-icon {
|
||||||
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
& .bn-editor {
|
||||||
|
padding-inline: 36px;
|
||||||
}
|
}
|
||||||
& .bn-container .bn-drag-handle {
|
& .bn-container .bn-drag-handle {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@ -45,6 +51,48 @@ const StyledEditor = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
& .bn-drag-handle-menu {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
|
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
|
||||||
|
box-shadow:
|
||||||
|
0px 2px 4px rgba(0, 0, 0, 0.04),
|
||||||
|
2px 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
min-width: 160px;
|
||||||
|
min-height: 96px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
left: 26px;
|
||||||
|
}
|
||||||
|
& .mantine-Menu-item {
|
||||||
|
background-color: transparent;
|
||||||
|
min-width: 152px;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
|
font-style: normal;
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
}
|
||||||
|
& .mantine-ActionIcon-root:hover {
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 4px rgba(0, 0, 0, 0.08),
|
||||||
|
0px 2px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
background: ${({ theme }) => theme.background.transparent.primary};
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
|
& .bn-side-menu .mantine-UnstyledButton-root:not(.mantine-Menu-item) svg {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bn-mantine .bn-side-menu > [draggable='true'] {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
& .bn-color-picker-dropdown {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const BlockEditor = ({
|
export const BlockEditor = ({
|
||||||
@ -83,7 +131,9 @@ export const BlockEditor = ({
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
theme={blockNoteTheme}
|
theme={blockNoteTheme}
|
||||||
slashMenu={false}
|
slashMenu={false}
|
||||||
|
sideMenu={false}
|
||||||
>
|
>
|
||||||
|
<CustomSideMenu editor={editor} />
|
||||||
<SuggestionMenuController
|
<SuggestionMenuController
|
||||||
triggerCharacter={'/'}
|
triggerCharacter={'/'}
|
||||||
getItems={async (query) =>
|
getItems={async (query) =>
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { blockSchema } from '@/activities/blocks/schema';
|
||||||
|
|
||||||
|
import { useComponentsContext } from '@blocknote/react';
|
||||||
|
|
||||||
|
type CustomAddBlockItemProps = {
|
||||||
|
editor: typeof blockSchema.BlockNoteEditor;
|
||||||
|
children: React.ReactNode; // Adding the children prop
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentItem = {
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
styles: any;
|
||||||
|
};
|
||||||
|
export const CustomAddBlockItem = ({
|
||||||
|
editor,
|
||||||
|
children,
|
||||||
|
}: CustomAddBlockItemProps) => {
|
||||||
|
const Components = useComponentsContext();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const blockIdentifier = editor.getTextCursorPosition().block;
|
||||||
|
const currentBlockContent = blockIdentifier?.content as
|
||||||
|
| Array<ContentItem>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const [firstElement] = currentBlockContent || [];
|
||||||
|
|
||||||
|
if (firstElement === undefined) {
|
||||||
|
editor.openSelectionMenu('/');
|
||||||
|
} else {
|
||||||
|
editor.sideMenu.addBlock();
|
||||||
|
editor.openSelectionMenu('/');
|
||||||
|
editor.sideMenu.unfreezeMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Components.Generic.Menu.Item onClick={handleClick}>
|
||||||
|
{children}
|
||||||
|
</Components.Generic.Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { blockSchema } from '@/activities/blocks/schema';
|
||||||
|
import { CustomAddBlockItem } from '@/ui/input/editor/components/CustomAddBlockItem';
|
||||||
|
import { CustomSideMenuOptions } from '@/ui/input/editor/components/CustomSideMenuOptions';
|
||||||
|
import {
|
||||||
|
BlockColorsItem,
|
||||||
|
DragHandleButton,
|
||||||
|
DragHandleMenu,
|
||||||
|
RemoveBlockItem,
|
||||||
|
SideMenu,
|
||||||
|
SideMenuController,
|
||||||
|
} from '@blocknote/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconColorSwatch, IconPlus, IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
|
type CustomSideMenuProps = {
|
||||||
|
editor: typeof blockSchema.BlockNoteEditor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDivToCreateGap = styled.div`
|
||||||
|
width: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CustomSideMenu = ({ editor }: CustomSideMenuProps) => {
|
||||||
|
return (
|
||||||
|
<SideMenuController
|
||||||
|
sideMenu={(props) => (
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
<SideMenu {...props}>
|
||||||
|
<DragHandleButton
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
dragHandleMenu={(props) => (
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
<DragHandleMenu {...props}>
|
||||||
|
<CustomAddBlockItem editor={editor}>
|
||||||
|
<CustomSideMenuOptions
|
||||||
|
LeftIcon={IconPlus}
|
||||||
|
text={'Add Block'}
|
||||||
|
Variant="normal"
|
||||||
|
/>
|
||||||
|
</CustomAddBlockItem>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<BlockColorsItem {...props}>
|
||||||
|
<CustomSideMenuOptions
|
||||||
|
LeftIcon={IconColorSwatch}
|
||||||
|
text={'Change Color'}
|
||||||
|
Variant="normal"
|
||||||
|
/>
|
||||||
|
</BlockColorsItem>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<RemoveBlockItem {...props}>
|
||||||
|
{' '}
|
||||||
|
<CustomSideMenuOptions
|
||||||
|
LeftIcon={IconTrash}
|
||||||
|
text={'Delete'}
|
||||||
|
Variant="danger"
|
||||||
|
/>
|
||||||
|
</RemoveBlockItem>
|
||||||
|
</DragHandleMenu>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<StyledDivToCreateGap />
|
||||||
|
</SideMenu>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<{ Variant: Variants }>`
|
||||||
|
color: ${({ theme, Variant }) =>
|
||||||
|
Variant === 'danger' ? theme.color.red : 'inherit'};
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextContainer = styled.div``;
|
||||||
|
|
||||||
|
type CustomSideMenuOptionsProps = {
|
||||||
|
LeftIcon: IconComponent; // Any valid React node (e.g., a component)
|
||||||
|
Variant: Variants;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Variants = 'normal' | 'danger';
|
||||||
|
|
||||||
|
export const CustomSideMenuOptions = ({
|
||||||
|
LeftIcon,
|
||||||
|
Variant,
|
||||||
|
text,
|
||||||
|
}: CustomSideMenuOptionsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<StyledContainer Variant={Variant}>
|
||||||
|
<LeftIcon
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
></LeftIcon>
|
||||||
|
<StyledTextContainer>{text}</StyledTextContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,7 +4,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledShowPageActivityContainer = styled.div`
|
const StyledShowPageActivityContainer = styled.div`
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
margin-top: ${({ theme }) => theme.spacing(6)};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
export const ShowPageActivityContainer = ({
|
export const ShowPageActivityContainer = ({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { MouseEvent } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
import { HOVER_BACKGROUND, IconComponent } from 'twenty-ui';
|
import { HOVER_BACKGROUND, IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||||
|
|||||||
Reference in New Issue
Block a user