Claude commited on
Commit
ae635a0
Β·
unverified Β·
1 Parent(s): 5cdf6b3

Wire export buttons to real copy/download

Browse files

B5c: 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

Files changed (1) hide show
  1. 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="β†—" onClick={() => push("Sent to Jira β€” JIRA-4821 created (demo)")}>Create Jira issue</Btn>
204
- <Btn kind="ghost" icon="βŒ₯" onClick={() => push("Opened GitHub issue draft (demo)")}>GitHub issue</Btn>
205
- <Btn kind="ghost" icon="β–€" onClick={() => push("Downloaded buglens-report.csv (demo)")}>CSV</Btn>
 
 
 
 
 
 
 
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>