From f65ef548b22755dd002107543c9370f1ce377b8b Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:24:53 +0900 Subject: [PATCH] fix: make draw.io built-in save button work with mouse tracking (#296) - Add showSaveDialog state to DiagramContext for shared state - Add mouse tracking to only respond to save events when mouse is over draw.io panel - Prevents save dialog from opening when clicking Send in chat panel - Add DialogDescription to SaveDialog for accessibility --- app/page.tsx | 33 ++++++++++++++++++++++++++++++++- components/chat-input.tsx | 8 ++++++-- components/save-dialog.tsx | 4 ++++ contexts/diagram-context.tsx | 5 +++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 09ce7a1..40c3ba9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ "use client" -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { DrawIoEmbed } from "react-drawio" import type { ImperativePanelHandle } from "react-resizable-panels" import ChatPanel from "@/components/chat-panel" @@ -21,6 +21,8 @@ export default function Home() { onDrawioLoad, resetDrawioReady, saveDiagramToStorage, + showSaveDialog, + setShowSaveDialog, } = useDiagram() const [isMobile, setIsMobile] = useState(false) const [isChatVisible, setIsChatVisible] = useState(true) @@ -30,6 +32,28 @@ export default function Home() { const [closeProtection, setCloseProtection] = useState(false) const chatPanelRef = useRef(null) + const isSavingRef = useRef(false) + const mouseOverDrawioRef = useRef(false) + + // Reset saving flag when dialog closes (with delay to ignore lingering save events from draw.io) + useEffect(() => { + if (!showSaveDialog) { + const timeout = setTimeout(() => { + isSavingRef.current = false + }, 1000) + return () => clearTimeout(timeout) + } + }, [showSaveDialog]) + + // Handle save from draw.io's built-in save button + // Note: draw.io sends save events for various reasons (focus changes, etc.) + // We use mouse position to determine if the user is interacting with draw.io + const handleDrawioSave = useCallback(() => { + if (!mouseOverDrawioRef.current) return + if (isSavingRef.current) return + isSavingRef.current = true + setShowSaveDialog(true) + }, [setShowSaveDialog]) // Load preferences from localStorage after mount useEffect(() => { @@ -147,6 +171,12 @@ export default function Home() { className={`h-full relative ${ isMobile ? "p-1" : "p-2" }`} + onMouseEnter={() => { + mouseOverDrawioRef.current = true + }} + onMouseLeave={() => { + mouseOverDrawioRef.current = false + }} >
{isLoaded ? ( @@ -155,6 +185,7 @@ export default function Home() { ref={drawioRef} onExport={handleDiagramExport} onLoad={onDrawioLoad} + onSave={handleDrawioSave} baseUrl={drawioBaseUrl} urlParameters={{ ui: drawioUi, diff --git a/components/chat-input.tsx b/components/chat-input.tsx index 9843484..b63bbd1 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -155,12 +155,16 @@ export function ChatInput({ minimalStyle = false, onMinimalStyleChange = () => {}, }: ChatInputProps) { - const { diagramHistory, saveDiagramToFile } = useDiagram() + const { + diagramHistory, + saveDiagramToFile, + showSaveDialog, + setShowSaveDialog, + } = useDiagram() const textareaRef = useRef(null) const fileInputRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [showClearDialog, setShowClearDialog] = useState(false) - const [showSaveDialog, setShowSaveDialog] = useState(false) // Allow retry when there's an error (even if status is still "streaming" or "submitted") const isDisabled = diff --git a/components/save-dialog.tsx b/components/save-dialog.tsx index 4314fa6..8204e20 100644 --- a/components/save-dialog.tsx +++ b/components/save-dialog.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button" import { Dialog, DialogContent, + DialogDescription, DialogFooter, DialogHeader, DialogTitle, @@ -72,6 +73,9 @@ export function SaveDialog({ Save Diagram + + Choose a format and filename to save your diagram. +
diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index 69abdc1..a7cd4ab 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -27,6 +27,8 @@ interface DiagramContextType { isDrawioReady: boolean onDrawioLoad: () => void resetDrawioReady: () => void + showSaveDialog: boolean + setShowSaveDialog: (show: boolean) => void } const DiagramContext = createContext(undefined) @@ -38,6 +40,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { { svg: string; xml: string }[] >([]) const [isDrawioReady, setIsDrawioReady] = useState(false) + const [showSaveDialog, setShowSaveDialog] = useState(false) const hasCalledOnLoadRef = useRef(false) const drawioRef = useRef(null) const resolverRef = useRef<((value: string) => void) | null>(null) @@ -309,6 +312,8 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { isDrawioReady, onDrawioLoad, resetDrawioReady, + showSaveDialog, + setShowSaveDialog, }} > {children}