feat(ui): wire Export button — POST /sync/export + toast

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 — <detail>
This commit is contained in:
mAi
2026-05-16 01:35:50 +02:00
parent 275cb5a55a
commit 8a6e8c8406
3 changed files with 56 additions and 3 deletions

View File

@@ -22,9 +22,8 @@
<div class="topbar-spacer"></div>
<button type="button" id="btn-apply-template" class="btn">Apply template…</button>
<button type="button" id="btn-solve" class="btn btn-primary">Solve</button>
<button type="button" id="btn-export" class="btn" disabled title="Slice 8">
Export
</button>
<button type="button" id="btn-export" class="btn">Export</button>
<span id="toast" class="toast" hidden></span>
</header>
<main class="layout">

View File

@@ -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 → <a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`,
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;

View File

@@ -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