Upgrade blocknote dependencies from 0.31.1 to 0.47.0. (#18207)
This PR pgrades all BlockNote packages (@blocknote/core, @blocknote/react, @blocknote/mantine, @blocknote/server-util, @blocknote/xl-docx-exporter, @blocknote/xl-pdf-exporter) to 0.47.0 and adapts the codebase to the new API. ### Changes - Dependency upgrades: Bumped all BlockNote packages to 0.47.0, added required Mantine v8 peer dependencies, removed unnecessary prosemirror resolutions - Formatting toolbar: Replaced the manual reimplementation of FormattingToolbarController (which handled visibility, positioning, portal rendering, text-alignment-based placement, and a dangerouslySetInnerHTML transition trick) with BlockNote's built-in FormattingToolbarController. The toolbar buttons themselves are unchanged. - Side menu: Replaced manual drag handle menu positioning and rendering (DashboardBlockDragHandleMenu, DashboardBlockColorPicker, and their floating configs) with BlockNote's built-in SideMenuController, DragHandleButton, and DragHandleMenu components. Deleted 4 files that became dead code. - Extension API migration: Replaced deprecated editor.suggestionMenus and editor.formattingToolbar APIs with the new extension system (SuggestionMenu, useExtensionState, editor.getExtension()) - Slash menu fixes: Filtered out BlockNote's new default "File" item (added in 0.47) to avoid duplicates with our custom one; added icon mappings for new block types (Toggle List, Divider, Toggle Headings, Headings 4-6) - Server-side: Switched @blocknote/server-util to dynamic import() to handle ESM-only transitive dependencies in CJS context
This commit is contained in:
@@ -201,8 +201,6 @@
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.9.2",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"prosemirror-view": "1.40.0",
|
||||
"prosemirror-transform": "1.10.4",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@types/qs": "6.9.16"
|
||||
},
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "3.0.99",
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@blocknote/mantine": "^0.31.1",
|
||||
"@blocknote/react": "^0.31.1",
|
||||
"@blocknote/xl-docx-exporter": "^0.31.1",
|
||||
"@blocknote/xl-pdf-exporter": "^0.31.1",
|
||||
"@blocknote/mantine": "0.47.0",
|
||||
"@blocknote/react": "0.47.0",
|
||||
"@blocknote/xl-docx-exporter": "0.47.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.47.0",
|
||||
"@calcom/embed-react": "^1.5.3",
|
||||
"@cyntler/react-doc-viewer": "^1.17.0",
|
||||
"@dagrejs/dagre": "^1.1.2",
|
||||
@@ -46,6 +46,9 @@
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/detect-locale": "^5.2.0",
|
||||
"@lingui/react": "^5.1.2",
|
||||
"@mantine/core": "^8.3.11",
|
||||
"@mantine/hooks": "^8.3.11",
|
||||
"@mantine/utils": "^6.0.22",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
@@ -78,7 +81,6 @@
|
||||
"buffer": "^6.0.3",
|
||||
"cron-parser": "5.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"docx": "^9.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"graphiql": "^3.1.1",
|
||||
"graphql": "16.8.1",
|
||||
|
||||
+1
-11
@@ -1,26 +1,16 @@
|
||||
import { type BlockNoteEditor } from '@blocknote/core';
|
||||
|
||||
import {
|
||||
docxDefaultSchemaMappings,
|
||||
DOCXExporter,
|
||||
} from '@blocknote/xl-docx-exporter';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Packer } from 'docx';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
export const exportBlockNoteEditorToDocx = async (
|
||||
editor: BlockNoteEditor,
|
||||
filename: string,
|
||||
) => {
|
||||
// Polyfill needed for exportBlockNoteEditorToDocX
|
||||
if (typeof window !== 'undefined') {
|
||||
window.Buffer = Buffer;
|
||||
}
|
||||
|
||||
const exporter = new DOCXExporter(editor.schema, docxDefaultSchemaMappings);
|
||||
|
||||
const docxDocument = await exporter.toDocxJsDocument(editor.document);
|
||||
|
||||
const blob = await Packer.toBlob(docxDocument);
|
||||
const blob = await exporter.toBlob(editor.document);
|
||||
saveAs(blob, `${filename}.docx`);
|
||||
};
|
||||
|
||||
@@ -42,10 +42,10 @@ export const FileBlock = createReactBlockSpec(
|
||||
type: 'file',
|
||||
propSchema: {
|
||||
url: {
|
||||
default: '' as string,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
default: '' as string,
|
||||
default: '',
|
||||
},
|
||||
fileCategory: {
|
||||
default: 'OTHER' as AttachmentFileCategory,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MentionInlineContent } from '@/blocknote-editor/blocks/MentionInlineCon
|
||||
export const BLOCK_SCHEMA = BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
file: FileBlock,
|
||||
file: FileBlock(),
|
||||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { filterSuggestionItems } from '@blocknote/core';
|
||||
import { filterSuggestionItems } from '@blocknote/core/extensions';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import { SuggestionMenuController } from '@blocknote/react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
+19
-27
@@ -1,18 +1,16 @@
|
||||
import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
|
||||
import { SuggestionMenu } from '@blocknote/core/extensions';
|
||||
import { useExtensionState } from '@blocknote/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { isSlashMenuOpenComponentState } from '@/blocknote-editor/states/isSlashMenuOpenComponentState';
|
||||
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export type BlockEditorDropdownFocusEffectProps = {
|
||||
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
|
||||
};
|
||||
export const BlockEditorDropdownFocusEffect = () => {
|
||||
const [prevShowing, setPrevShowing] = useState<boolean | null>(null);
|
||||
|
||||
export const BlockEditorDropdownFocusEffect = ({
|
||||
editor,
|
||||
}: BlockEditorDropdownFocusEffectProps) => {
|
||||
const isSlashMenuOpenState = useAtomComponentStateCallbackState(
|
||||
isSlashMenuOpenComponentState,
|
||||
);
|
||||
@@ -25,30 +23,21 @@ export const BlockEditorDropdownFocusEffect = ({
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const updateCallBack = useCallback(
|
||||
(event: any) => {
|
||||
const eventWantsToOpen = event.show === true;
|
||||
const suggestionState = useExtensionState(SuggestionMenu);
|
||||
|
||||
const isSlashMenuShowing =
|
||||
suggestionState?.show === true && suggestionState?.triggerCharacter === '/';
|
||||
|
||||
const syncSlashMenuState = useCallback(
|
||||
(showing: boolean) => {
|
||||
const isAlreadyOpen = store.get(isSlashMenuOpenState);
|
||||
|
||||
const shouldOpen = eventWantsToOpen && !isAlreadyOpen;
|
||||
|
||||
if (shouldOpen) {
|
||||
if (showing && isAlreadyOpen !== true) {
|
||||
setActiveDropdownFocusIdAndMemorizePrevious('custom-slash-menu');
|
||||
store.set(isSlashMenuOpenState, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventWantsToClose = event.show === false;
|
||||
|
||||
const isAlreadyClosed = !isAlreadyOpen;
|
||||
|
||||
const shouldClose = eventWantsToClose && !isAlreadyClosed;
|
||||
|
||||
if (shouldClose) {
|
||||
} else if (!showing && isAlreadyOpen === true) {
|
||||
goBackToPreviousDropdownFocusId();
|
||||
store.set(isSlashMenuOpenState, false);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -59,7 +48,10 @@ export const BlockEditorDropdownFocusEffect = ({
|
||||
],
|
||||
);
|
||||
|
||||
editor.suggestionMenus.on('update /', updateCallBack);
|
||||
if (prevShowing !== isSlashMenuShowing) {
|
||||
setPrevShowing(isSlashMenuShowing);
|
||||
syncSlashMenuState(isSlashMenuShowing);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
+12
-18
@@ -1,42 +1,36 @@
|
||||
import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
|
||||
|
||||
import { useComponentsContext } from '@blocknote/react';
|
||||
import { SuggestionMenu } from '@blocknote/core/extensions';
|
||||
import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react';
|
||||
|
||||
type CustomAddBlockItemProps = {
|
||||
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
|
||||
children: React.ReactNode; // Adding the children prop
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type ContentItem = {
|
||||
type: string;
|
||||
text: string;
|
||||
styles: any;
|
||||
};
|
||||
export const CustomAddBlockItem = ({
|
||||
editor,
|
||||
children,
|
||||
}: CustomAddBlockItemProps) => {
|
||||
const Components = useComponentsContext();
|
||||
const blockNoteEditor = useBlockNoteEditor();
|
||||
|
||||
if (!Components) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
const blockIdentifier = editor.getTextCursorPosition().block;
|
||||
const currentBlockContent = blockIdentifier?.content as
|
||||
| Array<ContentItem>
|
||||
| undefined;
|
||||
const currentBlock = blockNoteEditor.getTextCursorPosition().block;
|
||||
const blockContent = currentBlock?.content;
|
||||
|
||||
const [firstElement] = currentBlockContent || [];
|
||||
const isEmpty = Array.isArray(blockContent) && blockContent.length === 0;
|
||||
|
||||
if (firstElement === undefined) {
|
||||
editor.openSuggestionMenu('/');
|
||||
} else {
|
||||
editor.openSuggestionMenu('/');
|
||||
editor.sideMenu.unfreezeMenu();
|
||||
if (isEmpty) {
|
||||
editor.setTextCursorPosition(currentBlock);
|
||||
}
|
||||
|
||||
blockNoteEditor.getExtension(SuggestionMenu)?.openSuggestionMenu('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<Components.Generic.Menu.Item onClick={handleClick}>
|
||||
{children}
|
||||
|
||||
@@ -25,15 +25,11 @@ export const CustomSideMenu = ({ editor }: CustomSideMenuProps) => {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<SideMenuController
|
||||
sideMenu={(props) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<SideMenu {...props}>
|
||||
sideMenu={() => (
|
||||
<SideMenu>
|
||||
<DragHandleButton
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
dragHandleMenu={(props) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<DragHandleMenu {...props}>
|
||||
dragHandleMenu={() => (
|
||||
<DragHandleMenu>
|
||||
<CustomAddBlockItem editor={editor}>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconPlus}
|
||||
@@ -41,17 +37,14 @@ export const CustomSideMenu = ({ editor }: CustomSideMenuProps) => {
|
||||
Variant="normal"
|
||||
/>
|
||||
</CustomAddBlockItem>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<BlockColorsItem {...props}>
|
||||
<BlockColorsItem>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconColorSwatch}
|
||||
text={t`Change Color`}
|
||||
Variant="normal"
|
||||
/>
|
||||
</BlockColorsItem>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<RemoveBlockItem {...props}>
|
||||
{' '}
|
||||
<RemoveBlockItem>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete`}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const CustomSlashMenu = ({
|
||||
const currentBlock = editor?.getTextCursorPosition()?.block;
|
||||
const blockType = currentBlock?.type;
|
||||
const headingLevel =
|
||||
blockType === 'heading' ? (currentBlock?.props?.level as number) : null;
|
||||
blockType === 'heading' ? Number(currentBlock?.props?.level) : null;
|
||||
|
||||
const getOffsetValue = (placement: string) => {
|
||||
if (!placement.startsWith('top')) return 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getDefaultReactSlashMenuItems } from '@blocknote/react';
|
||||
|
||||
import { type SuggestionItem } from '@/blocknote-editor/components/CustomSlashMenu';
|
||||
import { type SuggestionItem } from '@/blocknote-editor/types/types';
|
||||
|
||||
import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
|
||||
import {
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
IconH1,
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconH4,
|
||||
IconH5,
|
||||
IconH6,
|
||||
IconHeadphones,
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconListDetails,
|
||||
IconListNumbers,
|
||||
IconMinus,
|
||||
IconMoodSmile,
|
||||
IconPhoto,
|
||||
IconPilcrow,
|
||||
@@ -26,6 +31,14 @@ const Icons: Record<string, IconComponent> = {
|
||||
'Heading 1': IconH1,
|
||||
'Heading 2': IconH2,
|
||||
'Heading 3': IconH3,
|
||||
'Heading 4': IconH4,
|
||||
'Heading 5': IconH5,
|
||||
'Heading 6': IconH6,
|
||||
'Toggle Heading 1': IconH1,
|
||||
'Toggle Heading 2': IconH2,
|
||||
'Toggle Heading 3': IconH3,
|
||||
'Toggle List': IconListDetails,
|
||||
Divider: IconMinus,
|
||||
Quote: IconBlockquote,
|
||||
'Numbered List': IconListNumbers,
|
||||
'Bullet List': IconList,
|
||||
@@ -41,10 +54,12 @@ const Icons: Record<string, IconComponent> = {
|
||||
|
||||
export const getSlashMenu = (editor: typeof BLOCK_SCHEMA.BlockNoteEditor) => {
|
||||
const items: SuggestionItem[] = [
|
||||
...getDefaultReactSlashMenuItems(editor).map((x) => ({
|
||||
...x,
|
||||
Icon: Icons[x.title],
|
||||
})),
|
||||
...getDefaultReactSlashMenuItems(editor)
|
||||
.filter((item) => item.title !== 'File')
|
||||
.map((x) => ({
|
||||
...x,
|
||||
Icon: Icons[x.title],
|
||||
})),
|
||||
{
|
||||
title: 'File',
|
||||
aliases: ['file', 'folder'],
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
import { type Block } from '@blocknote/core';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { DashboardColorSelectionMenu } from '@/page-layout/widgets/standalone-rich-text/components/DashboardColorSelectionMenu';
|
||||
import { COLOR_PICKER_FLOATING_CONFIG } from '@/page-layout/widgets/standalone-rich-text/constants/ColorPickerFloatingConfig';
|
||||
import { type BlockNoteColor } from '@/page-layout/widgets/standalone-rich-text/types/BlockNoteColor';
|
||||
import { extractColorFromProps } from '@/page-layout/widgets/standalone-rich-text/utils/extractColorFromProps';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
type DashboardBlockColorPickerProps = {
|
||||
block: Block;
|
||||
anchorElement: HTMLElement | null;
|
||||
onClose: () => void;
|
||||
onColorSelect: (
|
||||
textColor: BlockNoteColor | undefined,
|
||||
backgroundColor: string | undefined,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const COLOR_PICKER_CLICK_OUTSIDE_ID = 'color-picker-click-outside';
|
||||
|
||||
export const DashboardBlockColorPicker = ({
|
||||
block,
|
||||
anchorElement,
|
||||
onClose,
|
||||
onColorSelect,
|
||||
}: DashboardBlockColorPickerProps) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentTextColor = extractColorFromProps(block.props, 'text');
|
||||
const currentBackgroundColor = extractColorFromProps(
|
||||
block.props,
|
||||
'background',
|
||||
);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'right-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
elements: {
|
||||
reference: anchorElement,
|
||||
},
|
||||
middleware: [
|
||||
offset(COLOR_PICKER_FLOATING_CONFIG.offsetFromMenuItem),
|
||||
flip(),
|
||||
shift({
|
||||
padding: COLOR_PICKER_FLOATING_CONFIG.boundaryPadding,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [menuRef],
|
||||
excludedClickOutsideIds: [COLOR_PICKER_CLICK_OUTSIDE_ID],
|
||||
callback: onClose,
|
||||
listenerId: 'dashboard-block-color-picker',
|
||||
});
|
||||
|
||||
const handleTextColorSelect = (color: BlockNoteColor) => {
|
||||
onColorSelect(color, undefined);
|
||||
};
|
||||
|
||||
const handleBackgroundColorSelect = (color: BlockNoteColor) => {
|
||||
onColorSelect(undefined, color);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<OverlayContainer
|
||||
ref={(node) => {
|
||||
refs.setFloating(node);
|
||||
(menuRef as React.MutableRefObject<HTMLDivElement | null>).current =
|
||||
node;
|
||||
}}
|
||||
style={floatingStyles}
|
||||
className="bn-ui-container"
|
||||
data-click-outside-id={COLOR_PICKER_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<DashboardColorSelectionMenu
|
||||
currentTextColor={currentTextColor}
|
||||
currentBackgroundColor={currentBackgroundColor}
|
||||
onTextColorSelect={handleTextColorSelect}
|
||||
onBackgroundColorSelect={handleBackgroundColorSelect}
|
||||
/>
|
||||
</OverlayContainer>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
import { type Block } from '@blocknote/core';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { IconColorSwatch, IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
import { DashboardBlockColorPicker } from '@/page-layout/widgets/standalone-rich-text/components/DashboardBlockColorPicker';
|
||||
import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema';
|
||||
import { DRAG_HANDLE_MENU_FLOATING_CONFIG } from '@/page-layout/widgets/standalone-rich-text/constants/DragHandleMenuFloatingConfig';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type DashboardBlockDragHandleMenuProps = {
|
||||
editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor;
|
||||
block: Block;
|
||||
anchorElement: HTMLElement | null;
|
||||
boundaryElement?: HTMLElement | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DRAG_HANDLE_MENU_CLICK_OUTSIDE_ID = 'drag-handle-menu-click-outside';
|
||||
|
||||
const StyledColorMenuItem = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const DashboardBlockDragHandleMenu = ({
|
||||
editor,
|
||||
block,
|
||||
anchorElement,
|
||||
boundaryElement,
|
||||
onClose,
|
||||
}: DashboardBlockDragHandleMenuProps) => {
|
||||
const { t } = useLingui();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
const [colorMenuItemElement, setColorMenuItemElement] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'right-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
elements: {
|
||||
reference: anchorElement,
|
||||
},
|
||||
middleware: [
|
||||
offset(DRAG_HANDLE_MENU_FLOATING_CONFIG.offsetFromAnchor),
|
||||
flip({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
}),
|
||||
shift({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [menuRef],
|
||||
excludedClickOutsideIds: [DRAG_HANDLE_MENU_CLICK_OUTSIDE_ID],
|
||||
callback: () => {
|
||||
if (!showColorPicker) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
listenerId: 'portaled-drag-handle-menu',
|
||||
});
|
||||
|
||||
const handleAddBlock = () => {
|
||||
const insertedBlocks = editor.insertBlocks(
|
||||
[{ type: 'paragraph' }],
|
||||
block,
|
||||
'after',
|
||||
);
|
||||
|
||||
const insertedBlock = insertedBlocks[0];
|
||||
if (isDefined(insertedBlock)) {
|
||||
editor.setTextCursorPosition(insertedBlock);
|
||||
}
|
||||
|
||||
editor.openSuggestionMenu('/');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
editor.removeBlocks([block]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleColorClick = () => {
|
||||
setShowColorPicker(true);
|
||||
};
|
||||
|
||||
const handleColorPickerClose = () => {
|
||||
setShowColorPicker(false);
|
||||
};
|
||||
|
||||
const handleColorSelect = (
|
||||
textColor: string | undefined,
|
||||
backgroundColor: string | undefined,
|
||||
) => {
|
||||
editor.updateBlock(block, {
|
||||
props: {
|
||||
...(isDefined(textColor) && { textColor }),
|
||||
...(isDefined(backgroundColor) && { backgroundColor }),
|
||||
},
|
||||
});
|
||||
setShowColorPicker(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<OverlayContainer
|
||||
ref={(node) => {
|
||||
refs.setFloating(node);
|
||||
(menuRef as React.MutableRefObject<HTMLDivElement | null>).current =
|
||||
node;
|
||||
}}
|
||||
style={floatingStyles}
|
||||
className="bn-ui-container"
|
||||
data-click-outside-id={DRAG_HANDLE_MENU_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconPlus}
|
||||
onClick={handleAddBlock}
|
||||
accent="default"
|
||||
text={t`Add Block`}
|
||||
/>
|
||||
<StyledColorMenuItem ref={setColorMenuItemElement}>
|
||||
<MenuItem
|
||||
LeftIcon={IconColorSwatch}
|
||||
onClick={handleColorClick}
|
||||
accent="default"
|
||||
text={t`Change Color`}
|
||||
/>
|
||||
</StyledColorMenuItem>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDelete}
|
||||
accent="danger"
|
||||
text={t`Delete`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
</OverlayContainer>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{showColorPicker && isDefined(colorMenuItemElement) && (
|
||||
<DashboardBlockColorPicker
|
||||
anchorElement={colorMenuItemElement}
|
||||
block={block}
|
||||
onClose={handleColorPickerClose}
|
||||
onColorSelect={handleColorSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+80
-126
@@ -1,153 +1,107 @@
|
||||
import { useBlockNoteEditor, useUIPluginState } from '@blocknote/react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { SuggestionMenu } from '@blocknote/core/extensions';
|
||||
import {
|
||||
BlockColorsItem,
|
||||
DragHandleButton,
|
||||
DragHandleMenu,
|
||||
RemoveBlockItem,
|
||||
SideMenu,
|
||||
SideMenuController,
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
} from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, useFloating } from '@floating-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { IconGripVertical } from 'twenty-ui/display';
|
||||
|
||||
import { DashboardBlockDragHandleMenu } from '@/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu';
|
||||
import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconColorSwatch, IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomSideMenuOptions } from '@/blocknote-editor/components/CustomSideMenuOptions';
|
||||
import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema';
|
||||
|
||||
type DashboardEditorSideMenuProps = {
|
||||
editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor;
|
||||
boundaryElement?: HTMLElement | null;
|
||||
};
|
||||
|
||||
const StyledSideMenuContainer = styled.div`
|
||||
display: flex;
|
||||
z-index: ${RootStackingContextZIndices.DropdownPortalBelowModal};
|
||||
`;
|
||||
|
||||
const StyledDragHandleContainerWrapper = styled.div`
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledDragHandleContainer = styled.div`
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light},
|
||||
${({ theme }) => theme.boxShadow.strong};
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDivToCreateGap = styled.div`
|
||||
width: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const DashboardEditorSideMenu = ({
|
||||
const DashboardAddBlockItem = ({
|
||||
editor,
|
||||
boundaryElement,
|
||||
}: DashboardEditorSideMenuProps) => {
|
||||
children,
|
||||
}: {
|
||||
editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const Components = useComponentsContext();
|
||||
const blockNoteEditor = useBlockNoteEditor();
|
||||
const theme = useTheme();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [dragHandleElement, setDragHandleElement] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
|
||||
const state = useUIPluginState(
|
||||
blockNoteEditor.sideMenu.onUpdate.bind(blockNoteEditor.sideMenu),
|
||||
);
|
||||
|
||||
const virtualReference = isDefined(state?.referencePos)
|
||||
? { getBoundingClientRect: () => state.referencePos }
|
||||
: null;
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'left-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
elements: {
|
||||
reference: virtualReference,
|
||||
},
|
||||
});
|
||||
|
||||
if (!state?.show || !isDefined(virtualReference)) {
|
||||
if (!Components) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
blockNoteEditor.sideMenu.freezeMenu();
|
||||
setIsMenuOpen(true);
|
||||
};
|
||||
const currentBlock = blockNoteEditor.getTextCursorPosition().block;
|
||||
|
||||
const handleDragStart = (event: React.DragEvent) => {
|
||||
blockNoteEditor.sideMenu.blockDragStart(
|
||||
{
|
||||
dataTransfer: event.dataTransfer,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
state.block,
|
||||
const insertedBlocks = editor.insertBlocks(
|
||||
[{ type: 'paragraph' }],
|
||||
currentBlock,
|
||||
'after',
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
blockNoteEditor.sideMenu.blockDragEnd();
|
||||
};
|
||||
const insertedBlock = insertedBlocks[0];
|
||||
if (isDefined(insertedBlock)) {
|
||||
editor.setTextCursorPosition(insertedBlock);
|
||||
}
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
blockNoteEditor.sideMenu.unfreezeMenu();
|
||||
blockNoteEditor.getExtension(SuggestionMenu)?.openSuggestionMenu('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<StyledSideMenuContainer
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
className="bn-ui-container"
|
||||
>
|
||||
<StyledDragHandleContainerWrapper>
|
||||
<StyledDragHandleContainer
|
||||
ref={setDragHandleElement}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<IconGripVertical
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
</StyledDragHandleContainer>
|
||||
</StyledDragHandleContainerWrapper>
|
||||
<StyledDivToCreateGap />
|
||||
</StyledSideMenuContainer>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{isMenuOpen && (
|
||||
<DashboardBlockDragHandleMenu
|
||||
editor={editor}
|
||||
block={state.block}
|
||||
anchorElement={dragHandleElement}
|
||||
boundaryElement={boundaryElement}
|
||||
onClose={handleCloseMenu}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<Components.Generic.Menu.Item onClick={handleClick}>
|
||||
{children}
|
||||
</Components.Generic.Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const DashboardEditorSideMenu = ({
|
||||
editor,
|
||||
}: DashboardEditorSideMenuProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<SideMenuController
|
||||
sideMenu={() => (
|
||||
<SideMenu>
|
||||
<DragHandleButton
|
||||
dragHandleMenu={() => (
|
||||
<DragHandleMenu>
|
||||
<DashboardAddBlockItem editor={editor}>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconPlus}
|
||||
text={t`Add Block`}
|
||||
Variant="normal"
|
||||
/>
|
||||
</DashboardAddBlockItem>
|
||||
<BlockColorsItem>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconColorSwatch}
|
||||
text={t`Change Color`}
|
||||
Variant="normal"
|
||||
/>
|
||||
</BlockColorsItem>
|
||||
<RemoveBlockItem>
|
||||
<CustomSideMenuOptions
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete`}
|
||||
Variant="danger"
|
||||
/>
|
||||
</RemoveBlockItem>
|
||||
</DragHandleMenu>
|
||||
)}
|
||||
/>
|
||||
<StyledDivToCreateGap />
|
||||
</SideMenu>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+48
-159
@@ -1,21 +1,14 @@
|
||||
import { type DefaultProps } from '@blocknote/core';
|
||||
import {
|
||||
BasicTextStyleButton,
|
||||
BlockTypeSelect,
|
||||
CreateLinkButton,
|
||||
FormattingToolbar,
|
||||
FormattingToolbarController,
|
||||
NestBlockButton,
|
||||
TextAlignButton,
|
||||
UnnestBlockButton,
|
||||
useBlockNoteEditor,
|
||||
useEditorContentOrSelectionChange,
|
||||
useUIElementPositioning,
|
||||
useUIPluginState,
|
||||
} from '@blocknote/react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { flip, FloatingPortal, offset, shift } from '@floating-ui/react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { flip, offset, shift } from '@floating-ui/react';
|
||||
|
||||
import { DashboardFormattingToolbarColorButton } from '@/page-layout/widgets/standalone-rich-text/components/DashboardFormattingToolbarColorButton';
|
||||
import { FORMATTING_TOOLBAR_FLOATING_CONFIG } from '@/page-layout/widgets/standalone-rich-text/constants/FormattingToolbarFloatingConfig';
|
||||
@@ -28,163 +21,59 @@ const StyledToolbarContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const textAlignmentToPlacement = (
|
||||
textAlignment: DefaultProps['textAlignment'],
|
||||
) => {
|
||||
switch (textAlignment) {
|
||||
case 'left':
|
||||
return 'top-start';
|
||||
case 'center':
|
||||
return 'top';
|
||||
case 'right':
|
||||
return 'top-end';
|
||||
default:
|
||||
return 'top-start';
|
||||
}
|
||||
};
|
||||
|
||||
type DashboardFormattingToolbarProps = {
|
||||
boundaryElement?: HTMLElement | null;
|
||||
};
|
||||
|
||||
// This is a copy of the BlockNote's FormattingToolbarController component with customizations.
|
||||
export const DashboardFormattingToolbar = ({
|
||||
boundaryElement,
|
||||
}: DashboardFormattingToolbarProps) => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
const toolbarContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const editor = useBlockNoteEditor();
|
||||
const theme = useTheme();
|
||||
const colorScheme = theme.name === 'light' ? 'light' : 'dark';
|
||||
|
||||
const [placement, setPlacement] = useState<'top-start' | 'top' | 'top-end'>(
|
||||
() => {
|
||||
const block = editor.getTextCursorPosition().block;
|
||||
|
||||
if (!('textAlignment' in block.props)) {
|
||||
return 'top-start';
|
||||
}
|
||||
|
||||
return textAlignmentToPlacement(
|
||||
block.props.textAlignment as DefaultProps['textAlignment'],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
useEditorContentOrSelectionChange(() => {
|
||||
const block = editor.getTextCursorPosition().block;
|
||||
|
||||
if (!('textAlignment' in block.props)) {
|
||||
setPlacement('top-start');
|
||||
} else {
|
||||
setPlacement(
|
||||
textAlignmentToPlacement(
|
||||
block.props.textAlignment as DefaultProps['textAlignment'],
|
||||
),
|
||||
);
|
||||
}
|
||||
}, editor);
|
||||
|
||||
const state = useUIPluginState(
|
||||
editor.formattingToolbar.onUpdate.bind(editor.formattingToolbar),
|
||||
);
|
||||
|
||||
const isNodeSelection =
|
||||
editor.prosemirrorView?.state.selection.toJSON().type === 'node';
|
||||
|
||||
const shouldShow = (state?.show && !isNodeSelection) || false;
|
||||
|
||||
const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning(
|
||||
shouldShow,
|
||||
state?.referencePos || null,
|
||||
3000,
|
||||
{
|
||||
placement,
|
||||
middleware: [
|
||||
offset(FORMATTING_TOOLBAR_FLOATING_CONFIG.offsetFromSelection),
|
||||
shift({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
padding: FORMATTING_TOOLBAR_FLOATING_CONFIG.boundaryPadding,
|
||||
}),
|
||||
flip({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
}),
|
||||
],
|
||||
onOpenChange: (open) => {
|
||||
if (!open) {
|
||||
editor.formattingToolbar.closeMenu();
|
||||
editor.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const combinedRef = useMemo(
|
||||
() => (node: HTMLDivElement | null) => {
|
||||
toolbarContainerRef.current = node;
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
}
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
if (!isMounted || !isDefined(state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!shouldShow && isDefined(toolbarContainerRef.current)) {
|
||||
return (
|
||||
<FloatingPortal>
|
||||
<StyledToolbarContainer
|
||||
className="bn-container bn-mantine bn-ui-container"
|
||||
data-color-scheme={colorScheme}
|
||||
data-mantine-color-scheme={colorScheme}
|
||||
ref={combinedRef}
|
||||
style={style}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: toolbarContainerRef.current.innerHTML,
|
||||
}}
|
||||
/>
|
||||
</FloatingPortal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FloatingPortal>
|
||||
<StyledToolbarContainer
|
||||
className="bn-container bn-mantine bn-ui-container"
|
||||
data-color-scheme={colorScheme}
|
||||
data-mantine-color-scheme={colorScheme}
|
||||
ref={combinedRef}
|
||||
style={style}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<FormattingToolbar>
|
||||
<BlockTypeSelect key="blockTypeSelect" />
|
||||
<BasicTextStyleButton basicTextStyle="bold" key="boldStyleButton" />
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="italic"
|
||||
key="italicStyleButton"
|
||||
/>
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="underline"
|
||||
key="underlineStyleButton"
|
||||
/>
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="strike"
|
||||
key="strikeStyleButton"
|
||||
/>
|
||||
<TextAlignButton textAlignment="left" key="textAlignLeftButton" />
|
||||
<TextAlignButton textAlignment="center" key="textAlignCenterButton" />
|
||||
<TextAlignButton textAlignment="right" key="textAlignRightButton" />
|
||||
<DashboardFormattingToolbarColorButton key="colorStyleButton" />
|
||||
<NestBlockButton key="nestBlockButton" />
|
||||
<UnnestBlockButton key="unnestBlockButton" />
|
||||
<CreateLinkButton key="createLinkButton" />
|
||||
</FormattingToolbar>
|
||||
</StyledToolbarContainer>
|
||||
</FloatingPortal>
|
||||
<StyledToolbarContainer>
|
||||
<FormattingToolbarController
|
||||
formattingToolbar={() => (
|
||||
<FormattingToolbar>
|
||||
<BlockTypeSelect key="blockTypeSelect" />
|
||||
<BasicTextStyleButton basicTextStyle="bold" key="boldStyleButton" />
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="italic"
|
||||
key="italicStyleButton"
|
||||
/>
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="underline"
|
||||
key="underlineStyleButton"
|
||||
/>
|
||||
<BasicTextStyleButton
|
||||
basicTextStyle="strike"
|
||||
key="strikeStyleButton"
|
||||
/>
|
||||
<TextAlignButton textAlignment="left" key="textAlignLeftButton" />
|
||||
<TextAlignButton
|
||||
textAlignment="center"
|
||||
key="textAlignCenterButton"
|
||||
/>
|
||||
<TextAlignButton textAlignment="right" key="textAlignRightButton" />
|
||||
<DashboardFormattingToolbarColorButton key="colorStyleButton" />
|
||||
<NestBlockButton key="nestBlockButton" />
|
||||
<UnnestBlockButton key="unnestBlockButton" />
|
||||
<CreateLinkButton key="createLinkButton" />
|
||||
</FormattingToolbar>
|
||||
)}
|
||||
floatingUIOptions={{
|
||||
useFloatingOptions: {
|
||||
middleware: [
|
||||
offset(FORMATTING_TOOLBAR_FLOATING_CONFIG.offsetFromSelection),
|
||||
shift({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
padding: FORMATTING_TOOLBAR_FLOATING_CONFIG.boundaryPadding,
|
||||
}),
|
||||
flip({
|
||||
boundary: boundaryElement ?? undefined,
|
||||
}),
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</StyledToolbarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-16
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useEditorContentOrSelectionChange,
|
||||
} from '@blocknote/react';
|
||||
import { useActiveStyles, useBlockNoteEditor } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
@@ -47,18 +44,12 @@ export const DashboardFormattingToolbarColorButton = () => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [currentTextColor, setCurrentTextColor] =
|
||||
useState<BlockNoteColor>('default');
|
||||
const [currentBackgroundColor, setCurrentBackgroundColor] =
|
||||
useState<BlockNoteColor>('default');
|
||||
|
||||
useEditorContentOrSelectionChange(() => {
|
||||
const activeStyles = editor.getActiveStyles();
|
||||
setCurrentTextColor(extractColorFromProps(activeStyles, 'text'));
|
||||
setCurrentBackgroundColor(
|
||||
extractColorFromProps(activeStyles, 'background'),
|
||||
);
|
||||
}, editor);
|
||||
const activeStyles = useActiveStyles(editor);
|
||||
const currentTextColor = extractColorFromProps(activeStyles, 'text');
|
||||
const currentBackgroundColor = extractColorFromProps(
|
||||
activeStyles,
|
||||
'background',
|
||||
);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
|
||||
+3
-6
@@ -1,4 +1,4 @@
|
||||
import { filterSuggestionItems } from '@blocknote/core';
|
||||
import { filterSuggestionItems } from '@blocknote/core/extensions';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import { SuggestionMenuController } from '@blocknote/react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@@ -25,7 +25,7 @@ type DashboardsBlockEditorProps = {
|
||||
};
|
||||
|
||||
// TODO: Refactor these BlockNote CSS overrides - some may be dead code now that we have custom components
|
||||
// (DashboardBlockDragHandleMenu, DashboardEditorSideMenu, DashboardColorSelectionMenu).
|
||||
// (DashboardEditorSideMenu, DashboardColorSelectionMenu).
|
||||
// Test removing each selector and move necessary styles to appropriate components.
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
const StyledEditor = styled.div`
|
||||
@@ -183,10 +183,7 @@ export const DashboardsBlockEditor = ({
|
||||
{!readonly && (
|
||||
<>
|
||||
<DashboardFormattingToolbar boundaryElement={boundaryElement} />
|
||||
<DashboardEditorSideMenu
|
||||
editor={editor}
|
||||
boundaryElement={boundaryElement}
|
||||
/>
|
||||
<DashboardEditorSideMenu editor={editor} />
|
||||
<SuggestionMenuController
|
||||
triggerCharacter="/"
|
||||
getItems={async (query) => {
|
||||
|
||||
+7
-9
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { type Attachment } from '@/activities/files/types/Attachment';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
@@ -68,16 +68,14 @@ export const StandaloneRichTextEditorContent = ({
|
||||
store,
|
||||
]);
|
||||
|
||||
const initialContent = useMemo(
|
||||
() => filterSupportedBlocks(parseInitialBlocknote(currentBody)),
|
||||
[currentBody],
|
||||
const [initialContent] = useState(() =>
|
||||
filterSupportedBlocks(parseInitialBlocknote(currentBody)),
|
||||
);
|
||||
|
||||
const editor = useCreateBlockNote({
|
||||
initialContent,
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
schema: DASHBOARD_BLOCK_SCHEMA,
|
||||
sideMenuDetection: 'editor',
|
||||
placeholders: {
|
||||
default: t`Enter text or type '/' for commands`,
|
||||
},
|
||||
@@ -115,7 +113,7 @@ export const StandaloneRichTextEditorContent = ({
|
||||
handleAttachmentSync(newStringifiedBody, currentBody);
|
||||
};
|
||||
|
||||
const handleBlockEditorFocus = useCallback(() => {
|
||||
const handleBlockEditorFocus = () => {
|
||||
pushFocusItemToFocusStack({
|
||||
component: {
|
||||
instanceId: widget.id,
|
||||
@@ -124,14 +122,14 @@ export const StandaloneRichTextEditorContent = ({
|
||||
focusId: widget.id,
|
||||
globalHotkeysConfig: BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG,
|
||||
});
|
||||
}, [pushFocusItemToFocusStack, widget.id]);
|
||||
};
|
||||
|
||||
const handleBlockEditorBlur = useCallback(() => {
|
||||
const handleBlockEditorBlur = () => {
|
||||
handlePersistBody.flush();
|
||||
removeFocusItemFromFocusStackById({
|
||||
focusId: widget.id,
|
||||
});
|
||||
}, [handlePersistBody, removeFocusItemFromFocusStackById, widget.id]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export const COLOR_PICKER_FLOATING_CONFIG = {
|
||||
offsetFromMenuItem: 4,
|
||||
boundaryPadding: 8,
|
||||
};
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export const DRAG_HANDLE_MENU_FLOATING_CONFIG = {
|
||||
offsetFromAnchor: 4,
|
||||
};
|
||||
+9
@@ -9,6 +9,9 @@ import {
|
||||
IconH1,
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconH4,
|
||||
IconH5,
|
||||
IconH6,
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconListNumbers,
|
||||
@@ -21,6 +24,12 @@ const Icons: Record<string, IconComponent> = {
|
||||
'Heading 1': IconH1,
|
||||
'Heading 2': IconH2,
|
||||
'Heading 3': IconH3,
|
||||
'Heading 4': IconH4,
|
||||
'Heading 5': IconH5,
|
||||
'Heading 6': IconH6,
|
||||
'Toggle Heading 1': IconH1,
|
||||
'Toggle Heading 2': IconH2,
|
||||
'Toggle Heading 3': IconH3,
|
||||
Quote: IconBlockquote,
|
||||
'Numbered List': IconListNumbers,
|
||||
'Bullet List': IconList,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@aws-sdk/credential-providers": "3.998.0",
|
||||
"@azure/msal-node": "^3.8.4",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@blocknote/server-util": "^0.31.1",
|
||||
"@blocknote/server-util": "^0.47.0",
|
||||
"@clickhouse/client": "^1.11.0",
|
||||
"@dagrejs/dagre": "^1.1.2",
|
||||
"@e2b/code-interpreter": "^1.0.4",
|
||||
|
||||
+16
-7
@@ -5,19 +5,28 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type { ServerBlockNoteEditor } from '@blocknote/server-util';
|
||||
|
||||
// Reuse a single ServerBlockNoteEditor across all calls to avoid
|
||||
// the cost of dynamic import resolution + instance creation (~90ms) on every transform.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let cachedServerBlockNoteEditor: any = null;
|
||||
let cachedServerBlockNoteEditor: ServerBlockNoteEditor | null = null;
|
||||
|
||||
const getServerBlockNoteEditor = async () => {
|
||||
if (!cachedServerBlockNoteEditor) {
|
||||
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
|
||||
// SWC compiles import() to require() in CJS mode, which breaks ESM-only
|
||||
// transitive dependencies in @blocknote/core. Native import() resolves
|
||||
// the ESM bundle path where the full chain works.
|
||||
const nativeImport = new Function('specifier', 'return import(specifier)');
|
||||
|
||||
cachedServerBlockNoteEditor = ServerBlockNoteEditor.create();
|
||||
const getServerBlockNoteEditor = async (): Promise<ServerBlockNoteEditor> => {
|
||||
if (cachedServerBlockNoteEditor) {
|
||||
return cachedServerBlockNoteEditor;
|
||||
}
|
||||
|
||||
return cachedServerBlockNoteEditor;
|
||||
const module = await nativeImport('@blocknote/server-util');
|
||||
const editor: ServerBlockNoteEditor = module.ServerBlockNoteEditor.create();
|
||||
|
||||
cachedServerBlockNoteEditor = editor;
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const transformRichTextV2Value = async (
|
||||
|
||||
@@ -200,6 +200,9 @@ export {
|
||||
IconH1,
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconH4,
|
||||
IconH5,
|
||||
IconH6,
|
||||
IconHandClick,
|
||||
IconHandMove,
|
||||
IconHeadphones,
|
||||
|
||||
@@ -275,6 +275,9 @@ export {
|
||||
IconH1,
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconH4,
|
||||
IconH5,
|
||||
IconH6,
|
||||
IconHandClick,
|
||||
IconHandMove,
|
||||
IconHeadphones,
|
||||
|
||||
Reference in New Issue
Block a user