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
This commit is contained in:
Dayuan Jiang
2025-12-17 20:24:53 +09:00
committed by GitHub
parent 741a00db89
commit f65ef548b2
4 changed files with 47 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
"use client" "use client"
import { useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { DrawIoEmbed } from "react-drawio" import { DrawIoEmbed } from "react-drawio"
import type { ImperativePanelHandle } from "react-resizable-panels" import type { ImperativePanelHandle } from "react-resizable-panels"
import ChatPanel from "@/components/chat-panel" import ChatPanel from "@/components/chat-panel"
@@ -21,6 +21,8 @@ export default function Home() {
onDrawioLoad, onDrawioLoad,
resetDrawioReady, resetDrawioReady,
saveDiagramToStorage, saveDiagramToStorage,
showSaveDialog,
setShowSaveDialog,
} = useDiagram() } = useDiagram()
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [isChatVisible, setIsChatVisible] = useState(true) const [isChatVisible, setIsChatVisible] = useState(true)
@@ -30,6 +32,28 @@ export default function Home() {
const [closeProtection, setCloseProtection] = useState(false) const [closeProtection, setCloseProtection] = useState(false)
const chatPanelRef = useRef<ImperativePanelHandle>(null) const chatPanelRef = useRef<ImperativePanelHandle>(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 // Load preferences from localStorage after mount
useEffect(() => { useEffect(() => {
@@ -147,6 +171,12 @@ export default function Home() {
className={`h-full relative ${ className={`h-full relative ${
isMobile ? "p-1" : "p-2" isMobile ? "p-1" : "p-2"
}`} }`}
onMouseEnter={() => {
mouseOverDrawioRef.current = true
}}
onMouseLeave={() => {
mouseOverDrawioRef.current = false
}}
> >
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30"> <div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
{isLoaded ? ( {isLoaded ? (
@@ -155,6 +185,7 @@ export default function Home() {
ref={drawioRef} ref={drawioRef}
onExport={handleDiagramExport} onExport={handleDiagramExport}
onLoad={onDrawioLoad} onLoad={onDrawioLoad}
onSave={handleDrawioSave}
baseUrl={drawioBaseUrl} baseUrl={drawioBaseUrl}
urlParameters={{ urlParameters={{
ui: drawioUi, ui: drawioUi,

View File

@@ -155,12 +155,16 @@ export function ChatInput({
minimalStyle = false, minimalStyle = false,
onMinimalStyleChange = () => {}, onMinimalStyleChange = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const { diagramHistory, saveDiagramToFile } = useDiagram() const {
diagramHistory,
saveDiagramToFile,
showSaveDialog,
setShowSaveDialog,
} = useDiagram()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [showClearDialog, setShowClearDialog] = 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") // Allow retry when there's an error (even if status is still "streaming" or "submitted")
const isDisabled = const isDisabled =

View File

@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
@@ -72,6 +73,9 @@ export function SaveDialog({
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Save Diagram</DialogTitle> <DialogTitle>Save Diagram</DialogTitle>
<DialogDescription>
Choose a format and filename to save your diagram.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -27,6 +27,8 @@ interface DiagramContextType {
isDrawioReady: boolean isDrawioReady: boolean
onDrawioLoad: () => void onDrawioLoad: () => void
resetDrawioReady: () => void resetDrawioReady: () => void
showSaveDialog: boolean
setShowSaveDialog: (show: boolean) => void
} }
const DiagramContext = createContext<DiagramContextType | undefined>(undefined) const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
@@ -38,6 +40,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
{ svg: string; xml: string }[] { svg: string; xml: string }[]
>([]) >([])
const [isDrawioReady, setIsDrawioReady] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
const hasCalledOnLoadRef = useRef(false) const hasCalledOnLoadRef = useRef(false)
const drawioRef = useRef<DrawIoEmbedRef | null>(null) const drawioRef = useRef<DrawIoEmbedRef | null>(null)
const resolverRef = useRef<((value: string) => void) | null>(null) const resolverRef = useRef<((value: string) => void) | null>(null)
@@ -309,6 +312,8 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
isDrawioReady, isDrawioReady,
onDrawioLoad, onDrawioLoad,
resetDrawioReady, resetDrawioReady,
showSaveDialog,
setShowSaveDialog,
}} }}
> >
{children} {children}