quick-bug-reporter-react 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ Drop-in bug reporter for React apps — screenshot capture, video recording, ann
7
7
  - **Screenshot capture** — Full-page or region selection via `html2canvas-pro`
8
8
  - **Video recording** — Screen + microphone via `MediaRecorder` API
9
9
  - **Annotation** — Drag-to-highlight on captured screenshots
10
+ - **Structured bug reports** — Tab-based UI for Steps/Expected/Actual/Context (4000 char total)
10
11
  - **Network logging** — Automatic fetch interception during capture
11
12
  - **Console capture** — Automatic console log and JS error capture
12
13
  - **Integrations** — Linear and Jira (direct API or backend proxy)
package/dist/index.cjs CHANGED
@@ -898,6 +898,10 @@ var CloudIntegration = class {
898
898
  fd.set("project_key", this.projectKey);
899
899
  fd.set("title", payload.title);
900
900
  fd.set("description", payload.description || "");
901
+ if (payload.stepsToReproduce) fd.set("steps_to_reproduce", payload.stepsToReproduce);
902
+ if (payload.expectedResult) fd.set("expected_result", payload.expectedResult);
903
+ if (payload.actualResult) fd.set("actual_result", payload.actualResult);
904
+ if (payload.additionalContext) fd.set("additional_context", payload.additionalContext);
901
905
  fd.set("provider", "cloud");
902
906
  fd.set("capture_mode", payload.captureMode);
903
907
  fd.set("has_screenshot", String(Boolean(payload.screenshotBlob)));
@@ -1833,6 +1837,10 @@ var BugReporter = class {
1833
1837
  const payload = {
1834
1838
  title: normalizedTitle,
1835
1839
  description: normalizedDescription,
1840
+ stepsToReproduce: options.stepsToReproduce,
1841
+ expectedResult: options.expectedResult,
1842
+ actualResult: options.actualResult,
1843
+ additionalContext: options.additionalContext,
1836
1844
  videoBlob: artifacts.videoBlob,
1837
1845
  screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
1838
1846
  networkLogs: this.session.finalizeNetworkLogsForSubmit(artifacts.captureMode),
@@ -2365,7 +2373,7 @@ function BugReporterProvider({
2365
2373
  setScreenshotAnnotation(annotation);
2366
2374
  }, []);
2367
2375
  const submitReport = react.useCallback(
2368
- async (title, description) => {
2376
+ async (title, structuredFields) => {
2369
2377
  const reporter = getOrCreateReporter();
2370
2378
  if (!reporter) {
2371
2379
  setError("No bug tracker integration is configured.");
@@ -2385,6 +2393,25 @@ function BugReporterProvider({
2385
2393
  setSubmissionProgress("Preparing submission\u2026");
2386
2394
  setError(null);
2387
2395
  setSuccess(null);
2396
+ const { stepsToReproduce, expectedResult, actualResult, additionalContext } = structuredFields;
2397
+ const sections = [];
2398
+ if (stepsToReproduce.trim()) {
2399
+ sections.push(`## Steps to Reproduce
2400
+ ${stepsToReproduce.trim()}`);
2401
+ }
2402
+ if (expectedResult.trim()) {
2403
+ sections.push(`## Expected Result
2404
+ ${expectedResult.trim()}`);
2405
+ }
2406
+ if (actualResult.trim()) {
2407
+ sections.push(`## Actual Result
2408
+ ${actualResult.trim()}`);
2409
+ }
2410
+ if (additionalContext.trim()) {
2411
+ sections.push(`## Additional Context
2412
+ ${additionalContext.trim()}`);
2413
+ }
2414
+ const description = sections.length > 0 ? sections.join("\n\n") : "No description provided";
2388
2415
  const screenshotBlobForSubmit = draftMode === "screenshot" ? screenshotAnnotation.annotatedBlob ?? screenshotBlob : null;
2389
2416
  const metadata = {
2390
2417
  annotation: draftMode === "screenshot" && screenshotAnnotation.highlights.length > 0 ? {
@@ -2399,6 +2426,10 @@ function BugReporterProvider({
2399
2426
  };
2400
2427
  try {
2401
2428
  const result = await reporter.submit(title, description, {
2429
+ stepsToReproduce,
2430
+ expectedResult,
2431
+ actualResult,
2432
+ additionalContext,
2402
2433
  screenshotBlob: screenshotBlobForSubmit,
2403
2434
  metadata,
2404
2435
  consoleLogs,
@@ -3196,6 +3227,7 @@ function providerLabel(provider) {
3196
3227
  if (provider === "cloud") return "QuickBugs Cloud";
3197
3228
  return provider;
3198
3229
  }
3230
+ var CHAR_LIMIT = 4e3;
3199
3231
  function BugReporterModal() {
3200
3232
  const {
3201
3233
  autoStopNotice,
@@ -3225,9 +3257,39 @@ function BugReporterModal() {
3225
3257
  videoPreviewUrl
3226
3258
  } = useBugReporter();
3227
3259
  const [title, setTitle] = react.useState("");
3228
- const [description, setDescription] = react.useState("");
3260
+ const [stepsToReproduce, setStepsToReproduce] = react.useState("");
3261
+ const [expectedResult, setExpectedResult] = react.useState("");
3262
+ const [actualResult, setActualResult] = react.useState("");
3263
+ const [additionalContext, setAdditionalContext] = react.useState("");
3229
3264
  const [step, setStep] = react.useState("review");
3265
+ const [activeTab, setActiveTab] = react.useState("steps");
3266
+ const totalChars = stepsToReproduce.length + expectedResult.length + actualResult.length + additionalContext.length;
3267
+ const isOverLimit = totalChars > CHAR_LIMIT;
3230
3268
  const elapsedLabel = react.useMemo(() => formatElapsed2(elapsedMs), [elapsedMs]);
3269
+ const handleStepsKeyDown = (event) => {
3270
+ if (event.key === "Enter" && !event.shiftKey) {
3271
+ event.preventDefault();
3272
+ const textarea = event.currentTarget;
3273
+ const cursorPos = textarea.selectionStart;
3274
+ const textBeforeCursor = stepsToReproduce.substring(0, cursorPos);
3275
+ const textAfterCursor = stepsToReproduce.substring(cursorPos);
3276
+ const lines = textBeforeCursor.split("\n");
3277
+ const currentLine = lines[lines.length - 1];
3278
+ const numberMatch = currentLine.match(/^(\d+)\.\s/);
3279
+ const nextNumber = numberMatch ? parseInt(numberMatch[1]) + 1 : lines.length > 0 && stepsToReproduce.trim() === "" ? 1 : lines.length + 1;
3280
+ const newText = textBeforeCursor + "\n" + nextNumber + ". " + textAfterCursor;
3281
+ setStepsToReproduce(newText);
3282
+ setTimeout(() => {
3283
+ const newCursorPos = cursorPos + ("\n" + nextNumber + ". ").length;
3284
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
3285
+ }, 0);
3286
+ }
3287
+ };
3288
+ const handleStepsFocus = () => {
3289
+ if (stepsToReproduce.trim() === "") {
3290
+ setStepsToReproduce("1. ");
3291
+ }
3292
+ };
3231
3293
  const handleDialogOpenChange = (open) => {
3232
3294
  if (open) {
3233
3295
  openModal();
@@ -3238,15 +3300,24 @@ function BugReporterModal() {
3238
3300
  };
3239
3301
  const handleSubmit = async (event) => {
3240
3302
  event.preventDefault();
3241
- const result = await submitReport(title, description);
3303
+ const result = await submitReport(title, {
3304
+ stepsToReproduce,
3305
+ expectedResult,
3306
+ actualResult,
3307
+ additionalContext
3308
+ });
3242
3309
  if (result) {
3243
3310
  setTitle("");
3244
- setDescription("");
3311
+ setStepsToReproduce("");
3312
+ setExpectedResult("");
3313
+ setActualResult("");
3314
+ setAdditionalContext("");
3245
3315
  setStep("review");
3316
+ setActiveTab("steps");
3246
3317
  }
3247
3318
  };
3248
3319
  const hasIntegrations = availableProviders.length > 0;
3249
- const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0;
3320
+ const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0 && !isOverLimit;
3250
3321
  return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: isOpen, onOpenChange: handleDialogOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(
3251
3322
  DialogContent,
3252
3323
  {
@@ -3412,18 +3483,91 @@ function BugReporterModal() {
3412
3483
  }
3413
3484
  )
3414
3485
  ] }),
3415
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5 sm:col-span-2", children: [
3416
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", htmlFor: "bug-description", children: "Quick note" }),
3417
- /* @__PURE__ */ jsxRuntime.jsx(
3486
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 sm:col-span-2", children: [
3487
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", children: "Bug Details" }),
3488
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1 border-b border-gray-200", children: [
3489
+ /* @__PURE__ */ jsxRuntime.jsx(
3490
+ "button",
3491
+ {
3492
+ type: "button",
3493
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "steps" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3494
+ onClick: () => setActiveTab("steps"),
3495
+ children: "Steps"
3496
+ }
3497
+ ),
3498
+ /* @__PURE__ */ jsxRuntime.jsx(
3499
+ "button",
3500
+ {
3501
+ type: "button",
3502
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "expected" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3503
+ onClick: () => setActiveTab("expected"),
3504
+ children: "Expected"
3505
+ }
3506
+ ),
3507
+ /* @__PURE__ */ jsxRuntime.jsx(
3508
+ "button",
3509
+ {
3510
+ type: "button",
3511
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "actual" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3512
+ onClick: () => setActiveTab("actual"),
3513
+ children: "Actual"
3514
+ }
3515
+ ),
3516
+ /* @__PURE__ */ jsxRuntime.jsx(
3517
+ "button",
3518
+ {
3519
+ type: "button",
3520
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "context" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3521
+ onClick: () => setActiveTab("context"),
3522
+ children: "Context"
3523
+ }
3524
+ )
3525
+ ] }),
3526
+ activeTab === "steps" && /* @__PURE__ */ jsxRuntime.jsx(
3418
3527
  Textarea,
3419
3528
  {
3420
- id: "bug-description",
3421
- maxLength: 4e3,
3422
- placeholder: "What did you expect, what happened, and any quick repro steps.",
3423
- value: description,
3424
- onChange: (event) => setDescription(event.target.value)
3529
+ id: "bug-steps",
3530
+ placeholder: "Press Enter to start numbering steps...",
3531
+ value: stepsToReproduce,
3532
+ onChange: (event) => setStepsToReproduce(event.target.value),
3533
+ onKeyDown: handleStepsKeyDown,
3534
+ onFocus: handleStepsFocus
3425
3535
  }
3426
- )
3536
+ ),
3537
+ activeTab === "expected" && /* @__PURE__ */ jsxRuntime.jsx(
3538
+ Textarea,
3539
+ {
3540
+ id: "bug-expected",
3541
+ placeholder: "Describe what should happen...",
3542
+ value: expectedResult,
3543
+ onChange: (event) => setExpectedResult(event.target.value)
3544
+ }
3545
+ ),
3546
+ activeTab === "actual" && /* @__PURE__ */ jsxRuntime.jsx(
3547
+ Textarea,
3548
+ {
3549
+ id: "bug-actual",
3550
+ placeholder: "Describe what actually happened...",
3551
+ value: actualResult,
3552
+ onChange: (event) => setActualResult(event.target.value)
3553
+ }
3554
+ ),
3555
+ activeTab === "context" && /* @__PURE__ */ jsxRuntime.jsx(
3556
+ Textarea,
3557
+ {
3558
+ id: "bug-context",
3559
+ placeholder: "Any additional information, workarounds, or notes...",
3560
+ value: additionalContext,
3561
+ onChange: (event) => setAdditionalContext(event.target.value)
3562
+ }
3563
+ ),
3564
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: `text-xs ${isOverLimit ? "text-red-600 font-medium" : "text-gray-500"}`, children: [
3565
+ totalChars,
3566
+ "/",
3567
+ CHAR_LIMIT,
3568
+ " characters ",
3569
+ isOverLimit && "(over limit)"
3570
+ ] })
3427
3571
  ] }),
3428
3572
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
3429
3573
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", htmlFor: "bug-provider", children: "Submit to" }),