mirror of
https://github.com/docmost/docmost.git
synced 2025-11-12 17:42:36 +10:00
feat: draw.io (diagrams.net) integration (#215)
* draw.io init * updates
This commit is contained in:
@ -38,6 +38,7 @@
|
|||||||
"react-arborist": "^3.4.0",
|
"react-arborist": "^3.4.0",
|
||||||
"react-clear-modal": "^2.0.9",
|
"react-clear-modal": "^2.0.9",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-drawio": "^0.2.0",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-moveable": "^0.56.0",
|
"react-moveable": "^0.56.0",
|
||||||
|
|||||||
20
apps/client/src/components/icons/icon-drawio.tsx
Normal file
20
apps/client/src/components/icons/icon-drawio.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconDrawio({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="#F08705"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path d="M19.69 13.419h-2.527l-2.667-4.555a1.292 1.292 0 001.035-1.28V4.16c0-.725-.576-1.312-1.302-1.312H9.771c-.726 0-1.312.576-1.312 1.301v3.435c0 .619.426 1.152 1.034 1.28l-2.666 4.555H4.309c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312h4.458c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312h-.437l2.645-4.523h2.059l2.656 4.523h-.438c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312H19.7c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312zM24 22.976c0 .565-.459 1.024-1.013 1.024H1.024A1.022 1.022 0 010 22.987V1.024C0 .459.459 0 1.013 0h21.963C23.541 0 24 .459 24 1.013z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconDrawio;
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
BubbleMenu as BaseBubbleMenu,
|
||||||
|
findParentNode,
|
||||||
|
posToDOMRect,
|
||||||
|
} from '@tiptap/react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { sticky } from 'tippy.js';
|
||||||
|
import { Node as PMNode } from 'prosemirror-model';
|
||||||
|
import {
|
||||||
|
EditorMenuProps,
|
||||||
|
ShouldShowProps,
|
||||||
|
} from '@/features/editor/components/table/types/types.ts';
|
||||||
|
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx';
|
||||||
|
|
||||||
|
export function DrawioMenu({ editor }: EditorMenuProps) {
|
||||||
|
const shouldShow = useCallback(
|
||||||
|
({ state }: ShouldShowProps) => {
|
||||||
|
if (!state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.isActive('drawio') && editor.getAttributes('drawio')?.src;
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getReferenceClientRect = useCallback(() => {
|
||||||
|
const { selection } = editor.state;
|
||||||
|
const predicate = (node: PMNode) => node.type.name === 'drawio';
|
||||||
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
|
return dom.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const onWidthChange = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
editor.commands.updateAttributes('drawio', { width: `${value}%` });
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBubbleMenu
|
||||||
|
editor={editor}
|
||||||
|
pluginKey={`drawio-menu}`}
|
||||||
|
updateDelay={0}
|
||||||
|
tippyOptions={{
|
||||||
|
getReferenceClientRect,
|
||||||
|
offset: [0, 8],
|
||||||
|
zIndex: 99,
|
||||||
|
popperOptions: {
|
||||||
|
modifiers: [{ name: 'flip', enabled: false }],
|
||||||
|
},
|
||||||
|
plugins: [sticky],
|
||||||
|
sticky: 'popper',
|
||||||
|
}}
|
||||||
|
shouldShow={shouldShow}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editor.getAttributes('drawio')?.width && (
|
||||||
|
<NodeWidthResize
|
||||||
|
onChange={onWidthChange}
|
||||||
|
value={parseInt(editor.getAttributes('drawio').width)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</BaseBubbleMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DrawioMenu;
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
||||||
|
import { ActionIcon, Card, Image, Modal, Text } from '@mantine/core';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { uploadFile } from '@/features/page/services/page-service.ts';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { getFileUrl } from '@/lib/config.ts';
|
||||||
|
import {
|
||||||
|
DrawIoEmbed,
|
||||||
|
DrawIoEmbedRef,
|
||||||
|
EventExit,
|
||||||
|
EventSave,
|
||||||
|
} from 'react-drawio';
|
||||||
|
import { IAttachment } from '@/lib/types';
|
||||||
|
import { decodeBase64ToSvgString, svgStringToFile } from '@/lib/utils';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export default function DrawioView(props: NodeViewProps) {
|
||||||
|
const { node, updateAttributes, editor, selected } = props;
|
||||||
|
const { src, title, width, attachmentId } = node.attrs;
|
||||||
|
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
||||||
|
const [initialXML, setInitialXML] = useState<string>('');
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
|
const handleOpen = async () => {
|
||||||
|
if (!editor.isEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (src) {
|
||||||
|
const url = getFileUrl(src);
|
||||||
|
const request = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
const blob = await request.blob();
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
reader.onloadend = () => {
|
||||||
|
let base64data = (reader.result || '') as string;
|
||||||
|
setInitialXML(base64data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (data: EventSave) => {
|
||||||
|
const svgString = decodeBase64ToSvgString(data.xml);
|
||||||
|
|
||||||
|
const fileName = 'diagram.drawio.svg';
|
||||||
|
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
||||||
|
|
||||||
|
const pageId = editor.storage?.pageId;
|
||||||
|
|
||||||
|
let attachment: IAttachment = null;
|
||||||
|
|
||||||
|
if (attachmentId) {
|
||||||
|
attachment = await uploadFile(drawioSVGFile, pageId, attachmentId);
|
||||||
|
} else {
|
||||||
|
attachment = await uploadFile(drawioSVGFile, pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttributes({
|
||||||
|
src: `/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`,
|
||||||
|
title: attachment.fileName,
|
||||||
|
size: attachment.fileSize,
|
||||||
|
attachmentId: attachment.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||||
|
<Modal.Overlay />
|
||||||
|
<Modal.Content style={{ overflow: 'hidden' }}>
|
||||||
|
<Modal.Body>
|
||||||
|
<div style={{ height: '100vh' }}>
|
||||||
|
<DrawIoEmbed
|
||||||
|
ref={drawioRef}
|
||||||
|
xml={initialXML}
|
||||||
|
urlParameters={{
|
||||||
|
ui: 'kennedy',
|
||||||
|
spin: true,
|
||||||
|
libraries: true,
|
||||||
|
saveAndExit: true,
|
||||||
|
noSaveBtn: true,
|
||||||
|
}}
|
||||||
|
onSave={(data: EventSave) => {
|
||||||
|
// If the save is triggered by another event, then do nothing
|
||||||
|
if (data.parentEvent !== 'save') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSave(data);
|
||||||
|
}}
|
||||||
|
onClose={(data: EventExit) => {
|
||||||
|
// If the exit is triggered by another event, then do nothing
|
||||||
|
if (data.parentEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
|
||||||
|
{src ? (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Image
|
||||||
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
|
radius="md"
|
||||||
|
fit="contain"
|
||||||
|
w={width}
|
||||||
|
src={getFileUrl(src)}
|
||||||
|
alt={title}
|
||||||
|
className={clsx(
|
||||||
|
selected ? 'ProseMirror-selectednode' : '',
|
||||||
|
'alignCenter'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selected && (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={handleOpen}
|
||||||
|
variant="default"
|
||||||
|
color="gray"
|
||||||
|
mx="xs"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
|
p="xs"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
withBorder
|
||||||
|
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ActionIcon variant="transparent" color="gray">
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<Text component="span" size="lg" c="dimmed">
|
||||||
|
Double-click to edit drawio diagram
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -36,15 +36,16 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let data = null;
|
|
||||||
if (src) {
|
if (src) {
|
||||||
const url = getFileUrl(src);
|
const url = getFileUrl(src);
|
||||||
const request = await fetch(url, { credentials: 'include' });
|
const request = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
data = await loadFromBlob(await request.blob(), null, null);
|
const data = await loadFromBlob(await request.blob(), null, null);
|
||||||
|
setExcalidrawData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setExcalidrawData(data);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -69,7 +70,10 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
let svgString = serializer.serializeToString(svg);
|
let svgString = serializer.serializeToString(svg);
|
||||||
|
|
||||||
svgString = svgString.replace(/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, 'https://unpkg.com/@excalidraw/excalidraw@latest');
|
svgString = svgString.replace(
|
||||||
|
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
|
||||||
|
'https://unpkg.com/@excalidraw/excalidraw@latest'
|
||||||
|
);
|
||||||
|
|
||||||
const fileName = 'diagram.excalidraw.svg';
|
const fileName = 'diagram.excalidraw.svg';
|
||||||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
||||||
|
|||||||
@ -17,122 +17,123 @@ import {
|
|||||||
IconPhoto,
|
IconPhoto,
|
||||||
IconTable,
|
IconTable,
|
||||||
IconTypography,
|
IconTypography,
|
||||||
} from "@tabler/icons-react";
|
} from '@tabler/icons-react';
|
||||||
import {
|
import {
|
||||||
CommandProps,
|
CommandProps,
|
||||||
SlashMenuGroupedItemsType,
|
SlashMenuGroupedItemsType,
|
||||||
} from "@/features/editor/components/slash-menu/types";
|
} from '@/features/editor/components/slash-menu/types';
|
||||||
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
import { uploadImageAction } from '@/features/editor/components/image/upload-image-action.tsx';
|
||||||
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
import { uploadVideoAction } from '@/features/editor/components/video/upload-video-action.tsx';
|
||||||
import { uploadAttachmentAction } from "@/features/editor/components/attachment/upload-attachment-action.tsx";
|
import { uploadAttachmentAction } from '@/features/editor/components/attachment/upload-attachment-action.tsx';
|
||||||
import IconExcalidraw from "@/components/icons/icon-excalidraw";
|
import IconExcalidraw from '@/components/icons/icon-excalidraw';
|
||||||
import IconMermaid from "@/components/icons/icon-mermaid";
|
import IconMermaid from '@/components/icons/icon-mermaid';
|
||||||
|
import IconDrawio from '@/components/icons/icon-drawio';
|
||||||
|
|
||||||
const CommandGroups: SlashMenuGroupedItemsType = {
|
const CommandGroups: SlashMenuGroupedItemsType = {
|
||||||
basic: [
|
basic: [
|
||||||
{
|
{
|
||||||
title: "Text",
|
title: 'Text',
|
||||||
description: "Just start typing with plain text.",
|
description: 'Just start typing with plain text.',
|
||||||
searchTerms: ["p", "paragraph"],
|
searchTerms: ['p', 'paragraph'],
|
||||||
icon: IconTypography,
|
icon: IconTypography,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
.toggleNode("paragraph", "paragraph")
|
.toggleNode('paragraph', 'paragraph')
|
||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "To-do list",
|
title: 'To-do list',
|
||||||
description: "Track tasks with a to-do list.",
|
description: 'Track tasks with a to-do list.',
|
||||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
searchTerms: ['todo', 'task', 'list', 'check', 'checkbox'],
|
||||||
icon: IconCheckbox,
|
icon: IconCheckbox,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Heading 1",
|
title: 'Heading 1',
|
||||||
description: "Big section heading.",
|
description: 'Big section heading.',
|
||||||
searchTerms: ["title", "big", "large"],
|
searchTerms: ['title', 'big', 'large'],
|
||||||
icon: IconH1,
|
icon: IconH1,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
.setNode("heading", { level: 1 })
|
.setNode('heading', { level: 1 })
|
||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Heading 2",
|
title: 'Heading 2',
|
||||||
description: "Medium section heading.",
|
description: 'Medium section heading.',
|
||||||
searchTerms: ["subtitle", "medium"],
|
searchTerms: ['subtitle', 'medium'],
|
||||||
icon: IconH2,
|
icon: IconH2,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
.setNode("heading", { level: 2 })
|
.setNode('heading', { level: 2 })
|
||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Heading 3",
|
title: 'Heading 3',
|
||||||
description: "Small section heading.",
|
description: 'Small section heading.',
|
||||||
searchTerms: ["subtitle", "small"],
|
searchTerms: ['subtitle', 'small'],
|
||||||
icon: IconH3,
|
icon: IconH3,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
.setNode("heading", { level: 3 })
|
.setNode('heading', { level: 3 })
|
||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bullet list",
|
title: 'Bullet list',
|
||||||
description: "Create a simple bullet list.",
|
description: 'Create a simple bullet list.',
|
||||||
searchTerms: ["unordered", "point", "list"],
|
searchTerms: ['unordered', 'point', 'list'],
|
||||||
icon: IconList,
|
icon: IconList,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Numbered list",
|
title: 'Numbered list',
|
||||||
description: "Create a list with numbering.",
|
description: 'Create a list with numbering.',
|
||||||
searchTerms: ["numbered", "ordered", "list"],
|
searchTerms: ['numbered', 'ordered', 'list'],
|
||||||
icon: IconListNumbers,
|
icon: IconListNumbers,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quote",
|
title: 'Quote',
|
||||||
description: "Create block quote.",
|
description: 'Create block quote.',
|
||||||
searchTerms: ["blockquote", "quotes"],
|
searchTerms: ['blockquote', 'quotes'],
|
||||||
icon: IconBlockquote,
|
icon: IconBlockquote,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
|
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Code",
|
title: 'Code',
|
||||||
description: "Capture a code snippet.",
|
description: 'Capture a code snippet.',
|
||||||
searchTerms: ["codeblock"],
|
searchTerms: ['codeblock'],
|
||||||
icon: IconCode,
|
icon: IconCode,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Image",
|
title: 'Image',
|
||||||
description: "Upload any image from your device.",
|
description: 'Upload any image from your device.',
|
||||||
searchTerms: ["photo", "picture", "media"],
|
searchTerms: ['photo', 'picture', 'media'],
|
||||||
icon: IconPhoto,
|
icon: IconPhoto,
|
||||||
command: ({ editor, range }) => {
|
command: ({ editor, range }) => {
|
||||||
editor.chain().focus().deleteRange(range).run();
|
editor.chain().focus().deleteRange(range).run();
|
||||||
@ -141,9 +142,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
if (!pageId) return;
|
if (!pageId) return;
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
const input = document.createElement("input");
|
const input = document.createElement('input');
|
||||||
input.type = "file";
|
input.type = 'file';
|
||||||
input.accept = "image/*";
|
input.accept = 'image/*';
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
@ -157,9 +158,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Video",
|
title: 'Video',
|
||||||
description: "Upload any video from your device.",
|
description: 'Upload any video from your device.',
|
||||||
searchTerms: ["video", "mp4", "media"],
|
searchTerms: ['video', 'mp4', 'media'],
|
||||||
icon: IconMovie,
|
icon: IconMovie,
|
||||||
command: ({ editor, range }) => {
|
command: ({ editor, range }) => {
|
||||||
editor.chain().focus().deleteRange(range).run();
|
editor.chain().focus().deleteRange(range).run();
|
||||||
@ -168,9 +169,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
if (!pageId) return;
|
if (!pageId) return;
|
||||||
|
|
||||||
// upload video
|
// upload video
|
||||||
const input = document.createElement("input");
|
const input = document.createElement('input');
|
||||||
input.type = "file";
|
input.type = 'file';
|
||||||
input.accept = "video/*";
|
input.accept = 'video/*';
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
@ -182,9 +183,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "File attachment",
|
title: 'File attachment',
|
||||||
description: "Upload any file from your device.",
|
description: 'Upload any file from your device.',
|
||||||
searchTerms: ["file", "attachment", "upload", "pdf", "csv", "zip"],
|
searchTerms: ['file', 'attachment', 'upload', 'pdf', 'csv', 'zip'],
|
||||||
icon: IconPaperclip,
|
icon: IconPaperclip,
|
||||||
command: ({ editor, range }) => {
|
command: ({ editor, range }) => {
|
||||||
editor.chain().focus().deleteRange(range).run();
|
editor.chain().focus().deleteRange(range).run();
|
||||||
@ -193,16 +194,16 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
if (!pageId) return;
|
if (!pageId) return;
|
||||||
|
|
||||||
// upload file
|
// upload file
|
||||||
const input = document.createElement("input");
|
const input = document.createElement('input');
|
||||||
input.type = "file";
|
input.type = 'file';
|
||||||
input.accept = "";
|
input.accept = '';
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
const pos = editor.view.state.selection.from;
|
const pos = editor.view.state.selection.from;
|
||||||
if (file.type.includes("image/*")) {
|
if (file.type.includes('image/*')) {
|
||||||
uploadImageAction(file, editor.view, pos, pageId);
|
uploadImageAction(file, editor.view, pos, pageId);
|
||||||
} else if (file.type.includes("video/*")) {
|
} else if (file.type.includes('video/*')) {
|
||||||
uploadVideoAction(file, editor.view, pos, pageId);
|
uploadVideoAction(file, editor.view, pos, pageId);
|
||||||
} else {
|
} else {
|
||||||
uploadAttachmentAction(file, editor.view, pos, pageId);
|
uploadAttachmentAction(file, editor.view, pos, pageId);
|
||||||
@ -213,9 +214,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Table",
|
title: 'Table',
|
||||||
description: "Insert a table.",
|
description: 'Insert a table.',
|
||||||
searchTerms: ["table", "rows", "columns"],
|
searchTerms: ['table', 'rows', 'columns'],
|
||||||
icon: IconTable,
|
icon: IconTable,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor
|
editor
|
||||||
@ -226,43 +227,43 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
.run(),
|
.run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Toggle block",
|
title: 'Toggle block',
|
||||||
description: "Insert collapsible block.",
|
description: 'Insert collapsible block.',
|
||||||
searchTerms: ["collapsible", "block", "toggle", "details", "expand"],
|
searchTerms: ['collapsible', 'block', 'toggle', 'details', 'expand'],
|
||||||
icon: IconCaretRightFilled,
|
icon: IconCaretRightFilled,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleDetails().run(),
|
editor.chain().focus().deleteRange(range).toggleDetails().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Callout",
|
title: 'Callout',
|
||||||
description: "Insert callout notice.",
|
description: 'Insert callout notice.',
|
||||||
searchTerms: [
|
searchTerms: [
|
||||||
"callout",
|
'callout',
|
||||||
"notice",
|
'notice',
|
||||||
"panel",
|
'panel',
|
||||||
"info",
|
'info',
|
||||||
"warning",
|
'warning',
|
||||||
"success",
|
'success',
|
||||||
"error",
|
'error',
|
||||||
"danger",
|
'danger',
|
||||||
],
|
],
|
||||||
icon: IconInfoCircle,
|
icon: IconInfoCircle,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleCallout().run(),
|
editor.chain().focus().deleteRange(range).toggleCallout().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Math inline",
|
title: 'Math inline',
|
||||||
description: "Insert inline math equation.",
|
description: 'Insert inline math equation.',
|
||||||
searchTerms: [
|
searchTerms: [
|
||||||
"math",
|
'math',
|
||||||
"inline",
|
'inline',
|
||||||
"mathinline",
|
'mathinline',
|
||||||
"inlinemath",
|
'inlinemath',
|
||||||
"inline math",
|
'inline math',
|
||||||
"equation",
|
'equation',
|
||||||
"katex",
|
'katex',
|
||||||
"latex",
|
'latex',
|
||||||
"tex",
|
'tex',
|
||||||
],
|
],
|
||||||
icon: IconMathFunction,
|
icon: IconMathFunction,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
@ -275,39 +276,47 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
.run(),
|
.run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Math block",
|
title: 'Math block',
|
||||||
description: "Insert math equation",
|
description: 'Insert math equation',
|
||||||
searchTerms: [
|
searchTerms: [
|
||||||
"math",
|
'math',
|
||||||
"block",
|
'block',
|
||||||
"mathblock",
|
'mathblock',
|
||||||
"block math",
|
'block math',
|
||||||
"equation",
|
'equation',
|
||||||
"katex",
|
'katex',
|
||||||
"latex",
|
'latex',
|
||||||
"tex",
|
'tex',
|
||||||
],
|
],
|
||||||
icon: IconMath,
|
icon: IconMath,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).setMathBlock().run(),
|
editor.chain().focus().deleteRange(range).setMathBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Mermaid diagram",
|
title: 'Mermaid diagram',
|
||||||
description: "Insert mermaid diagram",
|
description: 'Insert mermaid diagram',
|
||||||
searchTerms: ["mermaid", "diagrams", "chart", "uml"],
|
searchTerms: ['mermaid', 'diagrams', 'chart', 'uml'],
|
||||||
icon: IconMermaid,
|
icon: IconMermaid,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
.setCodeBlock({ language: "mermaid" })
|
.setCodeBlock({ language: 'mermaid' })
|
||||||
.run(),
|
.run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Excalidraw diagram",
|
title: 'Draw.io (diagrams.net) ',
|
||||||
description: "Draw and sketch excalidraw diagrams",
|
description: 'Insert and design Drawio diagrams',
|
||||||
searchTerms: ["diagrams", "draw", "sketch"],
|
searchTerms: ['drawio', 'diagrams', 'charts', 'uml'],
|
||||||
|
icon: IconDrawio,
|
||||||
|
command: ({ editor, range }: CommandProps) =>
|
||||||
|
editor.chain().focus().deleteRange(range).setDrawio().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Excalidraw diagram',
|
||||||
|
description: 'Draw and sketch excalidraw diagrams',
|
||||||
|
searchTerms: ['diagrams', 'draw', 'sketch'],
|
||||||
icon: IconExcalidraw,
|
icon: IconExcalidraw,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).setExcalidraw().run(),
|
editor.chain().focus().deleteRange(range).setExcalidraw().run(),
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
Selection,
|
Selection,
|
||||||
Attachment,
|
Attachment,
|
||||||
CustomCodeBlock,
|
CustomCodeBlock,
|
||||||
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
} from "@docmost/editor-ext";
|
} from "@docmost/editor-ext";
|
||||||
import {
|
import {
|
||||||
@ -50,6 +51,7 @@ import { common, createLowlight } from "lowlight";
|
|||||||
import VideoView from "@/features/editor/components/video/video-view.tsx";
|
import VideoView from "@/features/editor/components/video/video-view.tsx";
|
||||||
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
|
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
|
||||||
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
||||||
|
import DrawioView from "../components/drawio/drawio-view";
|
||||||
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
||||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||||
|
|
||||||
@ -154,6 +156,9 @@ export const mainExtensions = [
|
|||||||
Attachment.configure({
|
Attachment.configure({
|
||||||
view: AttachmentView,
|
view: AttachmentView,
|
||||||
}),
|
}),
|
||||||
|
Drawio.configure({
|
||||||
|
view: DrawioView,
|
||||||
|
}),
|
||||||
Excalidraw.configure({
|
Excalidraw.configure({
|
||||||
view: ExcalidrawView,
|
view: ExcalidrawView,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import {
|
|||||||
} from "@/features/editor/components/common/file-upload-handler.tsx";
|
} from "@/features/editor/components/common/file-upload-handler.tsx";
|
||||||
import LinkMenu from "@/features/editor/components/link/link-menu.tsx";
|
import LinkMenu from "@/features/editor/components/link/link-menu.tsx";
|
||||||
import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu";
|
import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu";
|
||||||
|
import DrawioMenu from "./components/drawio/drawio-menu";
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -173,6 +174,7 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
<VideoMenu editor={editor} />
|
<VideoMenu editor={editor} />
|
||||||
<CalloutMenu editor={editor} />
|
<CalloutMenu editor={editor} />
|
||||||
<ExcalidrawMenu editor={editor} />
|
<ExcalidrawMenu editor={editor} />
|
||||||
|
<DrawioMenu editor={editor} />
|
||||||
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-image, .node-video, .node-excalidraw{
|
.node-image, .node-video, .node-excalidraw, .node-drawio {
|
||||||
&.ProseMirror-selectednode {
|
&.ProseMirror-selectednode {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,3 +52,12 @@ export async function svgStringToFile(
|
|||||||
const blob = new Blob([svgString], { type: "image/svg+xml" });
|
const blob = new Blob([svgString], { type: "image/svg+xml" });
|
||||||
return new File([blob], fileName, { type: "image/svg+xml" });
|
return new File([blob], fileName, { type: "image/svg+xml" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decodeBase64ToSvgString(base64Data: string): string {
|
||||||
|
const base64Prefix = 'data:image/svg+xml;base64,';
|
||||||
|
if (base64Data.startsWith(base64Prefix)) {
|
||||||
|
base64Data = base64Data.replace(base64Prefix, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return atob(base64Data);
|
||||||
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
TiptapVideo,
|
TiptapVideo,
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
Attachment,
|
Attachment,
|
||||||
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
} from '@docmost/editor-ext';
|
} from '@docmost/editor-ext';
|
||||||
import { generateText, JSONContent } from '@tiptap/core';
|
import { generateText, JSONContent } from '@tiptap/core';
|
||||||
@ -69,6 +70,7 @@ export const tiptapExtensions = [
|
|||||||
Callout,
|
Callout,
|
||||||
Attachment,
|
Attachment,
|
||||||
CustomCodeBlock,
|
CustomCodeBlock,
|
||||||
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
|
|||||||
@ -11,5 +11,7 @@ export * from "./lib/media-utils";
|
|||||||
export * from "./lib/link";
|
export * from "./lib/link";
|
||||||
export * from "./lib/selection";
|
export * from "./lib/selection";
|
||||||
export * from "./lib/attachment";
|
export * from "./lib/attachment";
|
||||||
export * from "./lib/custom-code-block";
|
export * from "./lib/custom-code-block"
|
||||||
|
export * from "./lib/drawio";
|
||||||
export * from "./lib/excalidraw";
|
export * from "./lib/excalidraw";
|
||||||
|
|
||||||
|
|||||||
124
packages/editor-ext/src/lib/drawio.ts
Normal file
124
packages/editor-ext/src/lib/drawio.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
export interface DrawioOptions {
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
view: any;
|
||||||
|
}
|
||||||
|
export interface DrawioAttributes {
|
||||||
|
src?: string;
|
||||||
|
title?: string;
|
||||||
|
size?: number;
|
||||||
|
width?: string;
|
||||||
|
align?: string;
|
||||||
|
attachmentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
drawio: {
|
||||||
|
setDrawio: (attributes?: DrawioAttributes) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Drawio = Node.create<DrawioOptions>({
|
||||||
|
name: "drawio",
|
||||||
|
inline: false,
|
||||||
|
group: "block",
|
||||||
|
isolating: true,
|
||||||
|
atom: true,
|
||||||
|
defining: true,
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
view: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: '',
|
||||||
|
parseHTML: (element) => element.getAttribute('data-src'),
|
||||||
|
renderHTML: (attributes) => ({
|
||||||
|
'data-src': attributes.src,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: undefined,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-title'),
|
||||||
|
renderHTML: (attributes: DrawioAttributes) => ({
|
||||||
|
'data-title': attributes.title,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
parseHTML: (element) => element.getAttribute('data-width'),
|
||||||
|
renderHTML: (attributes: DrawioAttributes) => ({
|
||||||
|
'data-width': attributes.width,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-size'),
|
||||||
|
renderHTML: (attributes: DrawioAttributes) => ({
|
||||||
|
'data-size': attributes.size,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
default: 'center',
|
||||||
|
parseHTML: (element) => element.getAttribute('data-align'),
|
||||||
|
renderHTML: (attributes: DrawioAttributes) => ({
|
||||||
|
'data-align': attributes.align,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
attachmentId: {
|
||||||
|
default: undefined,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-attachment-id'),
|
||||||
|
renderHTML: (attributes: DrawioAttributes) => ({
|
||||||
|
'data-attachment-id': attributes.attachmentId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `div[data-type="${this.name}"]`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes(
|
||||||
|
{ 'data-type': this.name },
|
||||||
|
this.options.HTMLAttributes,
|
||||||
|
HTMLAttributes
|
||||||
|
),
|
||||||
|
['img', { src: HTMLAttributes['data-src'], alt: HTMLAttributes['data-title'], width: HTMLAttributes['data-width'] }],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setDrawio:
|
||||||
|
(attrs: DrawioAttributes) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: "drawio",
|
||||||
|
attrs: attrs,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(this.options.view);
|
||||||
|
},
|
||||||
|
});
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -261,6 +261,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
react-drawio:
|
||||||
|
specifier: ^0.2.0
|
||||||
|
version: 0.2.0(react@18.3.1)
|
||||||
react-error-boundary:
|
react-error-boundary:
|
||||||
specifier: ^4.0.13
|
specifier: ^4.0.13
|
||||||
version: 4.0.13(react@18.3.1)
|
version: 4.0.13(react@18.3.1)
|
||||||
@ -7408,6 +7411,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
|
react-drawio@0.2.0:
|
||||||
|
resolution: {integrity: sha512-LQRk8miMq7ats+ram6M9DjR77ur1PaweWMf26mWQha9nvBMC98KcT1OyjISvOMhU+v1JCdUqMkTuGMNMD9nMew==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
react-email@2.1.4:
|
react-email@2.1.4:
|
||||||
resolution: {integrity: sha512-YKZ4jhkalWcNyaw4qyI//+QeTeUxe/ptqI+wSc4wVIoHzqffAWoV5x/jBpFex3FQ636xVIDFrvGq39rUVL7zSQ==}
|
resolution: {integrity: sha512-YKZ4jhkalWcNyaw4qyI//+QeTeUxe/ptqI+wSc4wVIoHzqffAWoV5x/jBpFex3FQ636xVIDFrvGq39rUVL7zSQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -16737,6 +16745,10 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
|
react-drawio@0.2.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
react-email@2.1.4(@swc/helpers@0.5.11)(eslint@9.5.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.11))(@types/node@20.14.9)(typescript@5.5.2)):
|
react-email@2.1.4(@swc/helpers@0.5.11)(eslint@9.5.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.11))(@types/node@20.14.9)(typescript@5.5.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.24.5
|
'@babel/core': 7.24.5
|
||||||
|
|||||||
Reference in New Issue
Block a user