From 131511a94e80b86a511849adaf535dd29172c093 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 12 Mar 2026 02:25:20 +0000 Subject: [PATCH] feat(editor): add auto-save and unsaved changes protection for diagrams --- .../editor/components/drawio/drawio-menu.tsx | 131 +++++++++++---- .../editor/components/drawio/drawio-view.tsx | 119 ++++++++++--- .../components/excalidraw/excalidraw-menu.tsx | 146 +++++++++++----- .../components/excalidraw/excalidraw-view.tsx | 157 +++++++++++++----- 4 files changed, 424 insertions(+), 129 deletions(-) diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index bdd1461b..8cda0348 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -1,6 +1,6 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; import { EditorMenuProps, @@ -9,6 +9,7 @@ import { import { ActionIcon, Modal, + Text, Tooltip, useComputedColorScheme, } from "@mantine/core"; @@ -29,10 +30,12 @@ import { DrawIoEmbed, DrawIoEmbedRef, EventExit, + EventExport, EventSave, } from "react-drawio"; import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils"; import { IAttachment } from "@/features/attachments/types/attachment.types"; +import { modals } from "@mantine/modals"; import classes from "../common/toolbar-menu.module.css"; export function DrawioMenu({ editor }: EditorMenuProps) { @@ -41,6 +44,8 @@ export function DrawioMenu({ editor }: EditorMenuProps) { const [initialXML, setInitialXML] = useState(""); const drawioRef = useRef(null); const computedColorScheme = useComputedColorScheme(); + const isDirtyRef = useRef(false); + const isSavingRef = useRef(false); const editorState = useEditorState({ editor, @@ -131,33 +136,13 @@ export function DrawioMenu({ editor }: EditorMenuProps) { editor.commands.deleteSelection(); }, [editor]); - const handleOpen = useCallback(async () => { - if (!editorState?.src) return; + const saveData = useCallback(async (svgXml: string) => { + if (isSavingRef.current) return; + + isSavingRef.current = true; try { - const url = getFileUrl(editorState.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 = () => { - const base64data = (reader.result || "") as string; - setInitialXML(base64data); - }; - } catch (err) { - console.error(err); - } finally { - open(); - } - }, [editorState?.src, open]); - - const handleSave = useCallback( - async (data: EventSave) => { - const svgString = decodeBase64ToSvgString(data.xml); + const svgString = decodeBase64ToSvgString(svgXml); const fileName = "diagram.drawio.svg"; const drawioSVGFile = await svgStringToFile(svgString, fileName); @@ -179,10 +164,85 @@ export function DrawioMenu({ editor }: EditorMenuProps) { attachmentId: attachment.id, }); + isDirtyRef.current = false; + } finally { + isSavingRef.current = false; + } + }, [editor, editorState?.attachmentId]); + + const handleClose = useCallback(() => { + if (!isDirtyRef.current) { close(); - }, - [editor, editorState?.attachmentId, close], - ); + return; + } + + modals.openConfirmModal({ + title: t("Unsaved changes"), + children: ( + + {t("You have unsaved changes that will be lost.")} + + ), + centered: true, + labels: { confirm: t("Discard"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => { + isDirtyRef.current = false; + close(); + }, + }); + }, [close, t]); + + const handleOpen = useCallback(async () => { + if (!editorState?.src) return; + + try { + const url = getFileUrl(editorState.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 = () => { + const base64data = (reader.result || "") as string; + setInitialXML(base64data); + }; + } catch (err) { + console.error(err); + } finally { + isDirtyRef.current = false; + open(); + } + }, [editorState?.src, open]); + + useEffect(() => { + if (!opened) return; + + const interval = setInterval(() => { + if (isDirtyRef.current && !isSavingRef.current && drawioRef.current) { + drawioRef.current.exportDiagram({ format: "xmlsvg" }); + } + }, 60_000); + + return () => clearInterval(interval); + }, [opened]); + + useEffect(() => { + if (!opened) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + handleClose(); + } + }; + + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [opened, handleClose]); return ( <> @@ -276,7 +336,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) { - + @@ -285,6 +345,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) { ref={drawioRef} xml={initialXML} baseUrl={getDrawioUrl()} + autosave urlParameters={{ ui: computedColorScheme === "light" ? "kennedy" : "dark", spin: true, @@ -296,13 +357,19 @@ export function DrawioMenu({ editor }: EditorMenuProps) { if (data.parentEvent !== "save") { return; } - handleSave(data); + saveData(data.xml).then(() => close()).catch(() => {}); }} onClose={(data: EventExit) => { if (data.parentEvent) { return; } - close(); + handleClose(); + }} + onAutoSave={() => { + isDirtyRef.current = true; + }} + onExport={(data: EventExport) => { + saveData(data.data).catch(() => {}); }} /> diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index 0b1580ec..00531afa 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -6,7 +6,7 @@ import { Text, useComputedColorScheme, } from "@mantine/core"; -import { useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { useDisclosure } from "@mantine/hooks"; import { getDrawioUrl } from "@/lib/config.ts"; @@ -14,6 +14,7 @@ import { DrawIoEmbed, DrawIoEmbedRef, EventExit, + EventExport, EventSave, } from "react-drawio"; import { IAttachment } from "@/features/attachments/types/attachment.types"; @@ -21,6 +22,7 @@ import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils"; import clsx from "clsx"; import { IconEdit } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; +import { modals } from "@mantine/modals"; export default function DrawioView(props: NodeViewProps) { const { t } = useTranslation(); @@ -30,42 +32,108 @@ export default function DrawioView(props: NodeViewProps) { const [initialXML, setInitialXML] = useState(""); const [opened, { open, close }] = useDisclosure(false); const computedColorScheme = useComputedColorScheme(); + const isDirtyRef = useRef(false); + const isSavingRef = useRef(false); const handleOpen = async () => { if (!editor.isEditable) { return; } + isDirtyRef.current = false; open(); }; - const handleSave = async (data: EventSave) => { - const svgString = decodeBase64ToSvgString(data.xml); - const fileName = "diagram.drawio.svg"; - const drawioSVGFile = await svgStringToFile(svgString, fileName); + const saveData = async (svgXml: string, updateSrc = true) => { + if (isSavingRef.current) return; - //@ts-ignore - const pageId = editor.storage?.pageId; + isSavingRef.current = true; - let attachment: IAttachment = null; - if (attachmentId) { - attachment = await uploadFile(drawioSVGFile, pageId, attachmentId); - } else { - attachment = await uploadFile(drawioSVGFile, pageId); + try { + const svgString = decodeBase64ToSvgString(svgXml); + const fileName = "diagram.drawio.svg"; + const drawioSVGFile = await svgStringToFile(svgString, fileName); + + //@ts-ignore + const pageId = editor.storage?.pageId; + + let attachment: IAttachment = null; + if (attachmentId) { + attachment = await uploadFile(drawioSVGFile, pageId, attachmentId); + } else { + attachment = await uploadFile(drawioSVGFile, pageId); + } + + if (updateSrc) { + updateAttributes({ + src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, + title: attachment.fileName, + size: attachment.fileSize, + attachmentId: attachment.id, + }); + } else { + updateAttributes({ + attachmentId: attachment.id, + }); + } + + isDirtyRef.current = false; + } finally { + isSavingRef.current = false; + } + }; + + const handleClose = useCallback(() => { + if (!isDirtyRef.current) { + close(); + return; } - updateAttributes({ - src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, - title: attachment.fileName, - size: attachment.fileSize, - attachmentId: attachment.id, + modals.openConfirmModal({ + title: t("Unsaved changes"), + children: ( + + {t("You have unsaved changes that will be lost.")} + + ), + centered: true, + labels: { confirm: t("Discard"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => { + isDirtyRef.current = false; + close(); + }, }); + }, [close, t]); - close(); - }; + useEffect(() => { + if (!opened) return; + + const interval = setInterval(() => { + if (isDirtyRef.current && !isSavingRef.current && drawioRef.current) { + drawioRef.current.exportDiagram({ format: "xmlsvg" }); + } + }, 60_000); + + return () => clearInterval(interval); + }, [opened]); + + useEffect(() => { + if (!opened) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + handleClose(); + } + }; + + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [opened, handleClose]); return ( - + @@ -74,6 +142,7 @@ export default function DrawioView(props: NodeViewProps) { ref={drawioRef} xml={initialXML} baseUrl={getDrawioUrl()} + autosave urlParameters={{ ui: computedColorScheme === "light" ? "kennedy" : "dark", spin: true, @@ -85,13 +154,19 @@ export default function DrawioView(props: NodeViewProps) { if (data.parentEvent !== "save") { return; } - handleSave(data); + saveData(data.xml, true).then(() => close()).catch(() => {}); }} onClose={(data: EventExit) => { if (data.parentEvent) { return; } - close(); + handleClose(); + }} + onAutoSave={() => { + isDirtyRef.current = true; + }} + onExport={(data: EventExport) => { + saveData(data.data, false).catch(() => {}); }} /> diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index 6e5852a3..c9ae7c08 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -1,6 +1,6 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import { lazy, Suspense, useCallback, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; import { EditorMenuProps, @@ -10,9 +10,11 @@ import { ActionIcon, Button, Group, + Text, Tooltip, useComputedColorScheme, } from "@mantine/core"; +import { modals } from "@mantine/modals"; import { useDisclosure } from "@mantine/hooks"; import clsx from "clsx"; import { @@ -52,6 +54,10 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { }); const [excalidrawData, setExcalidrawData] = useState(null); const computedColorScheme = useComputedColorScheme(); + const isDirtyRef = useRef(false); + const isSavingRef = useRef(false); + const isInitialLoadRef = useRef(true); + const lastFingerprintRef = useRef(""); const editorState = useEditorState({ editor, @@ -160,57 +166,109 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { } catch (err) { console.error(err); } finally { + isDirtyRef.current = false; + isInitialLoadRef.current = true; open(); } }, [editorState?.src, open]); - const handleSave = useCallback(async () => { - if (!excalidrawAPI) { + const saveData = useCallback(async () => { + if (!excalidrawAPI || isSavingRef.current) { return; } - const { exportToSvg } = await import("@excalidraw/excalidraw"); + isSavingRef.current = true; - const svg = await exportToSvg({ - elements: excalidrawAPI?.getSceneElements(), - appState: { - exportEmbedScene: true, - exportWithDarkMode: false, - }, - files: excalidrawAPI?.getFiles(), - }); + try { + const { exportToSvg } = await import("@excalidraw/excalidraw"); - const serializer = new XMLSerializer(); - let svgString = serializer.serializeToString(svg); + const svg = await exportToSvg({ + elements: excalidrawAPI?.getSceneElements(), + appState: { + exportEmbedScene: true, + exportWithDarkMode: false, + }, + files: excalidrawAPI?.getFiles(), + }); - svgString = svgString.replace( - /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, - "https://unpkg.com/@excalidraw/excalidraw@latest", - ); + const serializer = new XMLSerializer(); + let svgString = serializer.serializeToString(svg); - const fileName = "diagram.excalidraw.svg"; - const excalidrawSvgFile = await svgStringToFile(svgString, fileName); + svgString = svgString.replace( + /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, + "https://unpkg.com/@excalidraw/excalidraw@latest", + ); - // @ts-ignore - const pageId = editor.storage?.pageId; - const attachmentId = editorState?.attachmentId; + const fileName = "diagram.excalidraw.svg"; + const excalidrawSvgFile = await svgStringToFile(svgString, fileName); - let attachment: IAttachment = null; - if (attachmentId) { - attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); - } else { - attachment = await uploadFile(excalidrawSvgFile, pageId); + // @ts-ignore + const pageId = editor.storage?.pageId; + const attachmentId = editorState?.attachmentId; + + let attachment: IAttachment = null; + if (attachmentId) { + attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); + } else { + attachment = await uploadFile(excalidrawSvgFile, pageId); + } + + editor.commands.updateAttributes("excalidraw", { + src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, + title: attachment.fileName, + size: attachment.fileSize, + attachmentId: attachment.id, + }); + + isDirtyRef.current = false; + } finally { + isSavingRef.current = false; + } + }, [editor, excalidrawAPI, editorState?.attachmentId]); + + const handleSaveAndExit = useCallback(async () => { + try { + await saveData(); + close(); + } catch { + // save failed, modal stays open + } + }, [saveData, close]); + + const handleClose = useCallback(() => { + if (!isDirtyRef.current) { + close(); + return; } - editor.commands.updateAttributes("excalidraw", { - src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, - title: attachment.fileName, - size: attachment.fileSize, - attachmentId: attachment.id, + modals.openConfirmModal({ + title: t("Unsaved changes"), + children: ( + + {t("You have unsaved changes that will be lost.")} + + ), + centered: true, + labels: { confirm: t("Discard"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => { + isDirtyRef.current = false; + close(); + }, }); + }, [close, t]); - close(); - }, [editor, excalidrawAPI, editorState?.attachmentId, close]); + useEffect(() => { + if (!opened) return; + + const interval = setInterval(() => { + if (isDirtyRef.current && !isSavingRef.current) { + saveData().catch(() => {}); + } + }, 60_000); + + return () => clearInterval(interval); + }, [opened, saveData]); return ( <> @@ -317,7 +375,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { zIndex: 200, }} isOpen={opened} - onRequestClose={close} + onRequestClose={handleClose} disableCloseOnBgClick={true} contentProps={{ style: { @@ -332,10 +390,10 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { bg="var(--mantine-color-body)" p="xs" > - - @@ -343,6 +401,18 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { setExcalidrawAPI(api)} + onChange={(elements, _appState, files) => { + const fingerprint = `${elements.length}:${elements.reduce((s, e) => s + (e.version || 0), 0)}:${Object.keys(files).length}`; + if (isInitialLoadRef.current) { + lastFingerprintRef.current = fingerprint; + isInitialLoadRef.current = false; + return; + } + if (fingerprint !== lastFingerprintRef.current) { + lastFingerprintRef.current = fingerprint; + isDirtyRef.current = true; + } + }} initialData={{ ...excalidrawData, scrollToContent: true, diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index 51ff5b06..add0ad0c 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -7,7 +7,14 @@ import { Text, useComputedColorScheme, } from "@mantine/core"; -import { lazy, Suspense, useState } from "react"; +import { + lazy, + Suspense, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { svgStringToFile } from "@/lib"; import { useDisclosure } from "@mantine/hooks"; @@ -20,6 +27,7 @@ import { IconEdit } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useHandleLibrary } from "@excalidraw/excalidraw"; import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts"; +import { modals } from "@mantine/modals"; const ExcalidrawComponent = lazy(() => import("@excalidraw/excalidraw").then((module) => ({ @@ -42,59 +50,122 @@ export default function ExcalidrawView(props: NodeViewProps) { const [opened, { open, close }] = useDisclosure(false); const computedColorScheme = useComputedColorScheme(); + const isDirtyRef = useRef(false); + const isSavingRef = useRef(false); + const isInitialLoadRef = useRef(true); + const lastFingerprintRef = useRef(""); + const handleOpen = async () => { if (!editor.isEditable) { return; } + isDirtyRef.current = false; + isInitialLoadRef.current = true; open(); }; - const handleSave = async () => { - if (!excalidrawAPI) { + const saveData = useCallback(async (updateSrc = true) => { + if (!excalidrawAPI || isSavingRef.current) { return; } - const { exportToSvg } = await import("@excalidraw/excalidraw"); + isSavingRef.current = true; - const svg = await exportToSvg({ - elements: excalidrawAPI?.getSceneElements(), - appState: { - exportEmbedScene: true, - exportWithDarkMode: false, - }, - files: excalidrawAPI?.getFiles(), - }); + try { + const { exportToSvg } = await import("@excalidraw/excalidraw"); - const serializer = new XMLSerializer(); - let svgString = serializer.serializeToString(svg); + const svg = await exportToSvg({ + elements: excalidrawAPI?.getSceneElements(), + appState: { + exportEmbedScene: true, + exportWithDarkMode: false, + }, + files: excalidrawAPI?.getFiles(), + }); - svgString = svgString.replace( - /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, - "https://unpkg.com/@excalidraw/excalidraw@latest", - ); + const serializer = new XMLSerializer(); + let svgString = serializer.serializeToString(svg); - const fileName = "diagram.excalidraw.svg"; - const excalidrawSvgFile = await svgStringToFile(svgString, fileName); + svgString = svgString.replace( + /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, + "https://unpkg.com/@excalidraw/excalidraw@latest", + ); - // @ts-ignore - const pageId = editor.storage?.pageId; + const fileName = "diagram.excalidraw.svg"; + const excalidrawSvgFile = await svgStringToFile(svgString, fileName); - let attachment: IAttachment = null; - if (attachmentId) { - attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); - } else { - attachment = await uploadFile(excalidrawSvgFile, pageId); + // @ts-ignore + const pageId = editor.storage?.pageId; + + let attachment: IAttachment = null; + if (attachmentId) { + attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); + } else { + attachment = await uploadFile(excalidrawSvgFile, pageId); + } + + if (updateSrc) { + updateAttributes({ + src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, + title: attachment.fileName, + size: attachment.fileSize, + attachmentId: attachment.id, + }); + } else { + updateAttributes({ + attachmentId: attachment.id, + }); + } + + isDirtyRef.current = false; + } finally { + isSavingRef.current = false; + } + }, [excalidrawAPI, editor, attachmentId, updateAttributes]); + + const handleSaveAndExit = useCallback(async () => { + try { + await saveData(); + close(); + } catch { + /* empty */ + } + }, [saveData, close]); + + const handleClose = useCallback(() => { + if (!isDirtyRef.current) { + close(); + return; } - updateAttributes({ - src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, - title: attachment.fileName, - size: attachment.fileSize, - attachmentId: attachment.id, + modals.openConfirmModal({ + title: t("Unsaved changes"), + children: ( + + {t("You have unsaved changes that will be lost.")} + + ), + centered: true, + labels: { confirm: t("Discard"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => { + isDirtyRef.current = false; + close(); + }, }); + }, [close, t]); - close(); - }; + useEffect(() => { + if (!opened) return; + + const interval = setInterval(() => { + if (isDirtyRef.current && !isSavingRef.current) { + saveData(false).catch(() => {}); + } + }, 60_000); + + return () => clearInterval(interval); + }, [opened, saveData]); return ( @@ -105,7 +176,7 @@ export default function ExcalidrawView(props: NodeViewProps) { zIndex: 200, }} isOpen={opened} - onRequestClose={close} + onRequestClose={handleClose} disableCloseOnBgClick={true} contentProps={{ style: { @@ -120,10 +191,10 @@ export default function ExcalidrawView(props: NodeViewProps) { bg="var(--mantine-color-body)" p="xs" > - - @@ -131,6 +202,18 @@ export default function ExcalidrawView(props: NodeViewProps) { setExcalidrawAPI(api)} + onChange={(elements, _appState, files) => { + const fingerprint = `${elements.length}:${elements.reduce((s, e) => s + (e.version || 0), 0)}:${Object.keys(files).length}`; + if (isInitialLoadRef.current) { + lastFingerprintRef.current = fingerprint; + isInitialLoadRef.current = false; + return; + } + if (fingerprint !== lastFingerprintRef.current) { + lastFingerprintRef.current = fingerprint; + isDirtyRef.current = true; + } + }} initialData={{ ...excalidrawData, scrollToContent: true,