mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
fix(mcp-server): the download waits for a page export's projection to end, and the dialog's styles are bundled
This commit is contained in:
@@ -4,7 +4,7 @@ import { CanvasStage } from "@/components/canvas/canvas-stage"
|
||||
import { CompareDialog } from "@/components/canvas/compare-dialog"
|
||||
import { VersionStrip } from "@/components/canvas/version-strip"
|
||||
import { VersionsProvider } from "@/components/canvas/versions-context"
|
||||
import { SaveDialog } from "@/components/save-dialog"
|
||||
import { type ExportFormat, SaveDialog } from "@/components/save-dialog"
|
||||
import { AppToaster } from "@/components/workspace/app-toaster"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
@@ -29,7 +29,7 @@ const iconButton =
|
||||
export function CanvasShell({ config }: { config: ShellConfig }) {
|
||||
const dict = useDictionary()
|
||||
const isDark = useSettingsStore((s) => s.isDark)
|
||||
const { saveDiagramToFile } = useDiagram()
|
||||
const { saveDiagramToFile, hasPendingLoad } = useDiagram()
|
||||
const { status, sync } = useMcpSync(config)
|
||||
const versions = useNodeVersions(sync)
|
||||
const [panelOpen, setPanelOpen] = useState(true)
|
||||
@@ -47,6 +47,24 @@ export function CanvasShell({ config }: { config: ShellConfig }) {
|
||||
const hasVersions = versions.versions.length > 0
|
||||
const showPanel = panelOpen && hasVersions
|
||||
|
||||
// draw.io exports what it shows: while a page export's one-page
|
||||
// projection is on the canvas, or a full load has yet to reach the
|
||||
// editor, that is not the document. Both end within seconds (the
|
||||
// export's deadline is 15 s); the save waits for them, up to 20 s
|
||||
const save = (filename: string, format: ExportFormat, tries = 0) => {
|
||||
const canvasBusy = sync?.read().projectionActive || hasPendingLoad()
|
||||
if (canvasBusy && tries < 66) {
|
||||
setTimeout(() => save(filename, format, tries + 1), 300)
|
||||
return
|
||||
}
|
||||
saveDiagramToFile(
|
||||
filename,
|
||||
format,
|
||||
undefined,
|
||||
dict.save.savedSuccessfully,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VersionsProvider value={versions}>
|
||||
<div className="flex h-dvh flex-col bg-background text-foreground">
|
||||
@@ -136,14 +154,7 @@ export function CanvasShell({ config }: { config: ShellConfig }) {
|
||||
<SaveDialog
|
||||
open={saveOpen}
|
||||
onOpenChange={setSaveOpen}
|
||||
onSave={(filename, format) =>
|
||||
saveDiagramToFile(
|
||||
filename,
|
||||
format,
|
||||
undefined,
|
||||
dict.save.savedSuccessfully,
|
||||
)
|
||||
}
|
||||
onSave={save}
|
||||
defaultFilename={`diagram-${new Date().toLocaleDateString("sv-SE")}`}
|
||||
/>
|
||||
<AppToaster />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@source "../../../components/ui";
|
||||
@source "../../../components/ai-elements";
|
||||
@source "../../../components/workspace/app-toaster.tsx";
|
||||
@source "../../../components/save-dialog.tsx";
|
||||
|
||||
/* System fonts stand in for the web fonts the Next.js layout loads */
|
||||
:root {
|
||||
|
||||
@@ -349,6 +349,12 @@ test("the download button saves the diagram as a file", async ({ page }) => {
|
||||
await page.getByTestId("download").click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
// The dialog's utility classes are in the shell's stylesheet (shell.css
|
||||
// names the shared component with @source): the formats in two columns
|
||||
const columns = await dialog
|
||||
.getByRole("radiogroup")
|
||||
.evaluate((el) => getComputedStyle(el).gridTemplateColumns)
|
||||
expect(columns.split(" ")).toHaveLength(2)
|
||||
// .drawio is the default format
|
||||
const download = page.waitForEvent("download")
|
||||
await dialog.getByRole("button", { name: "Export" }).click()
|
||||
@@ -363,6 +369,53 @@ test("the download button saves the diagram as a file", async ({ page }) => {
|
||||
expect(saved).toContain('name="Page-1"')
|
||||
})
|
||||
|
||||
test("the download waits for a page export's projection, and saves every page", async ({
|
||||
page,
|
||||
}) => {
|
||||
const frame = await openShell(page)
|
||||
await toolText("create_new_diagram", { xml: box("2", "Stay here", 40) })
|
||||
await expect(frame.getByText("Stay here")).toBeVisible({ timeout: 10_000 })
|
||||
await toolText("add_page", {
|
||||
name: "Elsewhere",
|
||||
xml: `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="e1" value="OnElsewhere" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>`,
|
||||
})
|
||||
await expect(
|
||||
frame
|
||||
.locator(".geTabContainer")
|
||||
.getByText("Elsewhere", { exact: true }),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
await page.getByTestId("download").click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
// The export's result is held up for 2 s: the one-page projection of
|
||||
// the other page is surely still on the canvas when Export is clicked
|
||||
await page.route("**/api/state", async (route) => {
|
||||
const request = route.request()
|
||||
if (
|
||||
request.method() === "POST" &&
|
||||
(request.postData() ?? "").includes('"exportData"')
|
||||
) {
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
}
|
||||
await route.continue()
|
||||
})
|
||||
const exported = toolText("export_diagram", {
|
||||
path: join(dataDir, "elsewhere.svg"),
|
||||
page_name: "Elsewhere",
|
||||
})
|
||||
await expect(frame.getByText("OnElsewhere")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
const download = page.waitForEvent("download")
|
||||
await dialog.getByRole("button", { name: "Export" }).click()
|
||||
// Saved once the user's document is back: both pages, not the projection
|
||||
const saved = readFileSync(await (await download).path(), "utf8")
|
||||
expect(saved.match(/<diagram\b/g)).toHaveLength(2)
|
||||
expect(saved).toContain('name="Page-1"')
|
||||
expect(saved).toContain('name="Elsewhere"')
|
||||
expect(await exported).toContain("exported successfully")
|
||||
})
|
||||
|
||||
// --- Version cards from the server's History (shell/node-versions-source.ts) ---
|
||||
|
||||
test("the version cards follow the server's History; undo, redo and restore go through it", async ({
|
||||
|
||||
Reference in New Issue
Block a user