mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 09:14:07 +08:00
feat(editor): add auto-save and unsaved changes protection for diagrams (#2011)
* feat(editor): add auto-save and unsaved changes protection for diagrams * 30 seconds
This commit is contained in:
@@ -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<string>("");
|
||||
const drawioRef = useRef<DrawIoEmbedRef>(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: (
|
||||
<Text size="sm">
|
||||
{t("You have unsaved changes that will be lost.")}
|
||||
</Text>
|
||||
),
|
||||
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) {
|
||||
</div>
|
||||
</BaseBubbleMenu>
|
||||
|
||||
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Body>
|
||||
@@ -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(() => {});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user