From 8a6e8c8406a15dc830ffef99ab421c9187e6a00d Mon Sep 17 00:00:00 2001 From: mAi Date: Sat, 16 May 2026 01:35:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20wire=20Export=20button=20=E2=80=94?= =?UTF-8?q?=20POST=20/sync/export=20+=20toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export button is no longer disabled. On click it POSTs to the export endpoint and shows a toast next to the button: ✓ Exported · open in mxdrw (with viewer URL) ✗ Export failed — --- web/static/index.html | 5 ++--- web/static/main.js | 36 ++++++++++++++++++++++++++++++++++++ web/static/style.css | 18 ++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/web/static/index.html b/web/static/index.html index 3ab32ff..db62865 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -22,9 +22,8 @@
- + +
diff --git a/web/static/main.js b/web/static/main.js index fbd4764..353c796 100644 --- a/web/static/main.js +++ b/web/static/main.js @@ -128,6 +128,7 @@ const solveProject = (pid, preview) => api("POST", `/projects/${pid}/solve${pre const portsAndResolve = (pid, devId, body) => api("POST", `/projects/${pid}/devices/${devId}/ports-and-resolve`, body); const listSetupTemplates = () => api("GET", `/setup-templates`); const applyTemplate = (pid, body) => api("POST", `/projects/${pid}/apply-template`, body); +const syncExport = (pid) => api("POST", `/projects/${pid}/sync/export`, {}); // ---------- DOM helpers ---------- // @@ -2112,6 +2113,40 @@ function renderTemplatePreview(preview, templateIDStr) { `; } +// ---------- export flow ---------- // + +let toastTimer = null; + +function showToast(kind, html, holdMs = 5000) { + const t = $("#toast"); + t.className = "toast " + (kind || ""); + t.innerHTML = html; + setHidden(t, false); + if (toastTimer) clearTimeout(toastTimer); + toastTimer = setTimeout(() => { setHidden(t, true); t.innerHTML = ""; }, holdMs); +} + +async function exportCurrentProject() { + if (!state.active) { alert("Pick a project first"); return; } + const btn = $("#btn-export"); + btn.disabled = true; + showToast("", "Exporting…", 30000); + try { + const res = await syncExport(state.active.id); + const url = res.url ?? "(no url)"; + const count = res.element_count ?? 0; + showToast("ok", + `Exported ${count} elements → ${escapeHtml(url)}`, + 8000); + } catch (e) { + // Surface mxdrw unreachability or the upstream error verbatim. + const detail = typeof e.details === "object" ? JSON.stringify(e.details) : (e.details ?? ""); + showToast("error", `Export failed: ${escapeHtml(e.message)}${detail ? ` (${escapeHtml(String(detail))})` : ""}`, 12000); + } finally { + btn.disabled = false; + } +} + // ---------- boot ---------- // async function boot() { @@ -2133,6 +2168,7 @@ async function boot() { }); $("#btn-solve").addEventListener("click", openSolveModal); $("#btn-apply-template").addEventListener("click", openApplyTemplateModal); + $("#btn-export").addEventListener("click", exportCurrentProject); $("#project-select").addEventListener("change", (e) => { const v = /** @type {HTMLSelectElement} */ (e.target).value; diff --git a/web/static/style.css b/web/static/style.css index 54c7501..85af292 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -236,6 +236,24 @@ body { filter: drop-shadow(0 0 4px var(--accent)); } +/* Header toast — slice 8 export feedback */ +.toast { + display: inline-block; + margin-left: 12px; + font-size: 13px; + padding: 4px 10px; + border-radius: var(--radius); + background: var(--surface-2); + color: var(--text); + max-width: 420px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.toast.ok { background: #e8f5e9; color: #1b5e20; } +.toast.error { background: #fdecea; color: #911313; } +.toast a { color: inherit; text-decoration: underline; } + /* IO markers — diamonds. Power-by-convention, so the default fill is the Power cable_type colour (#e03131). Rotated 45° rect is the easiest way to draw a diamond that still hit-tests at the rotated