Spaces:
Sleeping
Sleeping
Claude commited on
Wire export buttons to real copy/download
Browse filesB5c: the results export bar now performs real actions instead of demo toasts.
- Copy Jira: copies Jira wiki markup to the clipboard.
- Copy GitHub issue: copies GitHub-flavored Markdown to the clipboard.
- Download CSV: downloads the regression tests as buglens-tests.csv.
Formatting is done client-side from the report payload already in the browser,
so exports work for both gallery examples and live uploaded reports (which have
no server-side id). Output mirrors buglens/render.py. Relabeled "Create Jira
issue" to "Copy Jira" β we copy markdown, we don't actually file tickets, which
keeps the UI honest.
https://claude.ai/code/session_01JE4YVLBWnk188Yu5sib59y
- buglens-ui/buglens-app.jsx +83 -3
buglens-ui/buglens-app.jsx
CHANGED
|
@@ -111,6 +111,79 @@ function IdleView({ onPick, onUpload }) {
|
|
| 111 |
|
| 112 |
const SHOT_IMG_STYLE = { width: "100%", height: "100%", objectFit: "contain", display: "block" };
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
// ββ Loading ββ
|
| 115 |
function LoadingView({ shotUrl, mockId, fileName, stageIdx }) {
|
| 116 |
return (
|
|
@@ -200,9 +273,16 @@ function ResultsView({ example, shotUrl, fileName, layout, showVision, onReset,
|
|
| 200 |
|
| 201 |
<div className="bl-export">
|
| 202 |
<span className="bl-export-l">Export</span>
|
| 203 |
-
<Btn kind="primary" icon="β"
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
</div>
|
| 207 |
</main>
|
| 208 |
</div>
|
|
|
|
| 111 |
|
| 112 |
const SHOT_IMG_STYLE = { width: "100%", height: "100%", objectFit: "contain", display: "block" };
|
| 113 |
|
| 114 |
+
// ββ Export helpers (client-side, so they work for examples and live uploads
|
| 115 |
+
// alike β the report payload is already in the browser) ββ
|
| 116 |
+
function severityLabel(s) {
|
| 117 |
+
return (window.SEVERITY && SEVERITY[s] && SEVERITY[s].label) || (s ? s[0].toUpperCase() + s.slice(1) : s);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function toJira(e) {
|
| 121 |
+
const L = [
|
| 122 |
+
`h2. ${e.title}`, "",
|
| 123 |
+
`*Severity:* ${severityLabel(e.severity)} β ${e.severityWhy}`,
|
| 124 |
+
`*Component:* ${e.app}`, "",
|
| 125 |
+
"h3. Summary", e.summary, "",
|
| 126 |
+
"h3. Steps to Reproduce",
|
| 127 |
+
];
|
| 128 |
+
e.steps.forEach((s) => L.push(`# ${s}`));
|
| 129 |
+
L.push("", "h3. Expected", e.expected, "", "h3. Actual", e.actual, "", "h3. Environment");
|
| 130 |
+
e.env.forEach((r) => L.push(`* ${r.k}: ${r.v}${r.known ? "" : " _(unconfirmed)_"}`));
|
| 131 |
+
L.push("", "h3. Missing Info (confirm before filing)");
|
| 132 |
+
e.missing.forEach((m) => L.push(`* ${m.q} β ${m.why}`));
|
| 133 |
+
L.push("", "h3. Regression Tests");
|
| 134 |
+
e.tests.forEach((t) => L.push(`* ${t.id} ${t.title}: given ${t.given}, when ${t.when}, then ${t.then}`));
|
| 135 |
+
L.push("", "h3. Edge Cases");
|
| 136 |
+
e.edges.forEach((c) => L.push(`* ${c.t}: ${c.d}`));
|
| 137 |
+
return L.join("\n");
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function toGithub(e) {
|
| 141 |
+
const L = [
|
| 142 |
+
`# ${e.title}`, "",
|
| 143 |
+
`**Severity:** ${severityLabel(e.severity)} β ${e.severityWhy} `,
|
| 144 |
+
`**Component:** ${e.app}`, "",
|
| 145 |
+
"## Summary", e.summary, "",
|
| 146 |
+
"## Steps to Reproduce",
|
| 147 |
+
];
|
| 148 |
+
e.steps.forEach((s, i) => L.push(`${i + 1}. ${s}`));
|
| 149 |
+
L.push("", "## Expected", e.expected, "", "## Actual", e.actual, "",
|
| 150 |
+
"## Environment", "", "| Field | Value | Confirmed |", "| --- | --- | --- |");
|
| 151 |
+
e.env.forEach((r) => L.push(`| ${r.k} | ${r.v} | ${r.known ? "yes" : "no"} |`));
|
| 152 |
+
L.push("", "## Missing Info (confirm before filing)");
|
| 153 |
+
e.missing.forEach((m) => L.push(`- [ ] **${m.q}** β ${m.why}`));
|
| 154 |
+
L.push("", "## Regression Tests");
|
| 155 |
+
e.tests.forEach((t) => {
|
| 156 |
+
L.push(`- **${t.id} ${t.title}**`, ` - _Given_ ${t.given}`, ` - _When_ ${t.when}`, ` - _Then_ ${t.then}`);
|
| 157 |
+
});
|
| 158 |
+
L.push("", "## Edge Cases");
|
| 159 |
+
e.edges.forEach((c) => L.push(`- **${c.t}** β ${c.d}`));
|
| 160 |
+
return L.join("\n");
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function csvCell(v) {
|
| 164 |
+
const s = String(v);
|
| 165 |
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function toCsv(e) {
|
| 169 |
+
const rows = [["id", "title", "given", "when", "then"]];
|
| 170 |
+
e.tests.forEach((t) => rows.push([t.id, t.title, t.given, t.when, t.then]));
|
| 171 |
+
return rows.map((r) => r.map(csvCell).join(",")).join("\n") + "\n";
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function copyText(text) {
|
| 175 |
+
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
|
| 176 |
+
return Promise.reject(new Error("clipboard unavailable"));
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function downloadText(name, text, type) {
|
| 180 |
+
const url = URL.createObjectURL(new Blob([text], { type }));
|
| 181 |
+
const a = document.createElement("a");
|
| 182 |
+
a.href = url; a.download = name;
|
| 183 |
+
document.body.appendChild(a); a.click(); a.remove();
|
| 184 |
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
// ββ Loading ββ
|
| 188 |
function LoadingView({ shotUrl, mockId, fileName, stageIdx }) {
|
| 189 |
return (
|
|
|
|
| 273 |
|
| 274 |
<div className="bl-export">
|
| 275 |
<span className="bl-export-l">Export</span>
|
| 276 |
+
<Btn kind="primary" icon="β"
|
| 277 |
+
onClick={() => copyText(toJira(example))
|
| 278 |
+
.then(() => push("Copied Jira markdown to clipboard"))
|
| 279 |
+
.catch(() => push("Couldnβt access the clipboard"))}>Copy Jira</Btn>
|
| 280 |
+
<Btn kind="ghost" icon="β₯"
|
| 281 |
+
onClick={() => copyText(toGithub(example))
|
| 282 |
+
.then(() => push("Copied GitHub issue to clipboard"))
|
| 283 |
+
.catch(() => push("Couldnβt access the clipboard"))}>Copy GitHub issue</Btn>
|
| 284 |
+
<Btn kind="ghost" icon="β€"
|
| 285 |
+
onClick={() => { downloadText("buglens-tests.csv", toCsv(example), "text/csv"); push("Downloaded buglens-tests.csv"); }}>Download CSV</Btn>
|
| 286 |
</div>
|
| 287 |
</main>
|
| 288 |
</div>
|