quick-bug-reporter-react 1.3.2 → 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 +1 -0
- package/dist/index.cjs +329 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +329 -50
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +11 -9
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
|
@@ -882,42 +882,104 @@ var CloudIntegration = class {
|
|
|
882
882
|
throw new Error("CloudIntegration: projectKey is required.");
|
|
883
883
|
}
|
|
884
884
|
this.projectKey = options.projectKey;
|
|
885
|
-
this.endpoint = options.endpoint ?? "/api/ingest";
|
|
885
|
+
this.endpoint = options.ingestUrl ?? options.endpoint ?? "/api/ingest";
|
|
886
|
+
this.appVersion = options.appVersion;
|
|
887
|
+
this.environment = options.environment;
|
|
886
888
|
this.fetchFn = options.fetchImpl ?? fetch.bind(globalThis);
|
|
887
889
|
}
|
|
888
890
|
async submit(payload, onProgress = noop3) {
|
|
889
891
|
onProgress("Preparing report\u2026");
|
|
890
|
-
const ua = payload.userAgent ||
|
|
892
|
+
const ua = payload.userAgent || getRuntimeUserAgent();
|
|
891
893
|
const browserName = parseBrowserName(ua);
|
|
894
|
+
const browserVersion = parseBrowserVersion(ua);
|
|
892
895
|
const osName = parseOsName(ua);
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
896
|
+
const osVersion = parseOsVersion(ua, osName);
|
|
897
|
+
const fd = new FormData();
|
|
898
|
+
fd.set("project_key", this.projectKey);
|
|
899
|
+
fd.set("title", payload.title);
|
|
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);
|
|
905
|
+
fd.set("provider", "cloud");
|
|
906
|
+
fd.set("capture_mode", payload.captureMode);
|
|
907
|
+
fd.set("has_screenshot", String(Boolean(payload.screenshotBlob)));
|
|
908
|
+
fd.set("has_video", String(Boolean(payload.videoBlob)));
|
|
909
|
+
fd.set("has_network_logs", String(payload.networkLogs.length > 0));
|
|
910
|
+
fd.set("has_console_logs", String(payload.consoleLogs.length > 0));
|
|
911
|
+
fd.set("js_error_count", String(payload.jsErrors.length));
|
|
912
|
+
fd.set("user_agent", ua);
|
|
913
|
+
fd.set("browser_name", browserName);
|
|
914
|
+
fd.set("browser_version", browserVersion ?? "");
|
|
915
|
+
fd.set("os_name", osName);
|
|
916
|
+
fd.set("os_version", osVersion ?? "");
|
|
917
|
+
fd.set("device_type", getDeviceType());
|
|
918
|
+
fd.set("screen_resolution", getScreenResolution());
|
|
919
|
+
fd.set("viewport", getViewport());
|
|
920
|
+
fd.set("color_scheme", payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : "");
|
|
921
|
+
fd.set("locale", payload.metadata.locale ?? "");
|
|
922
|
+
fd.set("timezone", payload.metadata.timezone ?? "");
|
|
923
|
+
fd.set("connection_type", payload.metadata.connection?.effectiveType ?? "");
|
|
924
|
+
fd.set("page_url", payload.pageUrl || "");
|
|
925
|
+
fd.set("environment", this.environment ?? getEnvironment());
|
|
926
|
+
fd.set("app_version", this.appVersion ?? "");
|
|
927
|
+
fd.set("platform", payload.metadata.platform ?? "");
|
|
928
|
+
fd.set("duration_ms", String(payload.elapsedMs));
|
|
929
|
+
fd.set("stopped_at", payload.stoppedAt || "");
|
|
930
|
+
if (payload.metadata.mobile) {
|
|
931
|
+
fd.set("platform", payload.metadata.mobile.platform);
|
|
932
|
+
fd.set("device_model", payload.metadata.mobile.deviceModel ?? "");
|
|
933
|
+
fd.set("device_brand", payload.metadata.mobile.deviceBrand ?? "");
|
|
934
|
+
fd.set("os_version", payload.metadata.mobile.osVersion ?? "");
|
|
935
|
+
fd.set("app_build_number", payload.metadata.mobile.appBuildNumber ?? "");
|
|
936
|
+
fd.set("is_emulator", String(payload.metadata.mobile.isEmulator));
|
|
937
|
+
fd.set(
|
|
938
|
+
"battery_level",
|
|
939
|
+
payload.metadata.mobile.batteryLevel == null ? "" : String(payload.metadata.mobile.batteryLevel)
|
|
940
|
+
);
|
|
941
|
+
fd.set(
|
|
942
|
+
"free_storage_mb",
|
|
943
|
+
payload.metadata.mobile.freeStorageMb == null ? "" : String(payload.metadata.mobile.freeStorageMb)
|
|
944
|
+
);
|
|
945
|
+
fd.set("invocation_method", payload.metadata.mobile.invocationMethod);
|
|
946
|
+
}
|
|
947
|
+
if (payload.screenshotBlob) {
|
|
948
|
+
fd.append("screenshot", payload.screenshotBlob, "bug-screenshot.png");
|
|
949
|
+
}
|
|
950
|
+
if (payload.videoBlob) {
|
|
951
|
+
fd.append("video", payload.videoBlob, "bug-recording.webm");
|
|
952
|
+
}
|
|
953
|
+
fd.append(
|
|
954
|
+
"network_logs",
|
|
955
|
+
new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }),
|
|
956
|
+
"network-logs.txt"
|
|
957
|
+
);
|
|
958
|
+
const parts = [];
|
|
959
|
+
if (payload.jsErrors.length > 0) {
|
|
960
|
+
parts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
|
|
961
|
+
} else {
|
|
962
|
+
parts.push("=== JavaScript Errors ===\nNo JavaScript errors captured.");
|
|
963
|
+
}
|
|
964
|
+
if (payload.consoleLogs.length > 0) {
|
|
965
|
+
parts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
|
|
966
|
+
} else {
|
|
967
|
+
parts.push("=== Console Output ===\nNo console output captured.");
|
|
968
|
+
}
|
|
969
|
+
fd.append(
|
|
970
|
+
"console_logs",
|
|
971
|
+
new Blob([parts.join("\n\n")], { type: "text/plain" }),
|
|
972
|
+
"console-logs.txt"
|
|
973
|
+
);
|
|
974
|
+
fd.append(
|
|
975
|
+
"metadata",
|
|
976
|
+
new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }),
|
|
977
|
+
"client-metadata.json"
|
|
978
|
+
);
|
|
916
979
|
onProgress("Sending report\u2026");
|
|
917
980
|
const response = await this.fetchFn(this.endpoint, {
|
|
918
981
|
method: "POST",
|
|
919
|
-
|
|
920
|
-
body: JSON.stringify(body)
|
|
982
|
+
body: fd
|
|
921
983
|
});
|
|
922
984
|
if (!response.ok) {
|
|
923
985
|
const errorBody = await response.json().catch(() => ({}));
|
|
@@ -926,12 +988,22 @@ var CloudIntegration = class {
|
|
|
926
988
|
}
|
|
927
989
|
const result = await response.json();
|
|
928
990
|
onProgress("Report submitted.");
|
|
991
|
+
const fwd = result.forwarding;
|
|
992
|
+
const externalKey = fwd?.key;
|
|
993
|
+
const externalUrl = fwd?.url;
|
|
994
|
+
const warnings = [];
|
|
995
|
+
if (result.forwarding_status === "queued") {
|
|
996
|
+
warnings.push("Tracker forwarding is running in the background.");
|
|
997
|
+
}
|
|
998
|
+
if (fwd?.error) {
|
|
999
|
+
warnings.push(`Forwarding: ${fwd.error}`);
|
|
1000
|
+
}
|
|
929
1001
|
return {
|
|
930
1002
|
provider: "cloud",
|
|
931
1003
|
issueId: result.id,
|
|
932
|
-
issueKey: `QB-${result.id.slice(0, 8)}`,
|
|
933
|
-
issueUrl: null,
|
|
934
|
-
warnings
|
|
1004
|
+
issueKey: externalKey || `QB-${result.id.slice(0, 8)}`,
|
|
1005
|
+
issueUrl: externalUrl || null,
|
|
1006
|
+
warnings
|
|
935
1007
|
};
|
|
936
1008
|
}
|
|
937
1009
|
};
|
|
@@ -943,14 +1015,50 @@ function parseBrowserName(ua) {
|
|
|
943
1015
|
if (ua.includes("Safari/") && !ua.includes("Chrome/")) return "Safari";
|
|
944
1016
|
return "Unknown";
|
|
945
1017
|
}
|
|
1018
|
+
function parseBrowserVersion(ua) {
|
|
1019
|
+
return matchVersion(ua, /Edg\/([\d.]+)/) || matchVersion(ua, /OPR\/([\d.]+)/) || matchVersion(ua, /Opera\/([\d.]+)/) || matchVersion(ua, /Chrome\/([\d.]+)/) || matchVersion(ua, /Firefox\/([\d.]+)/) || matchVersion(ua, /Version\/([\d.]+).*Safari/);
|
|
1020
|
+
}
|
|
1021
|
+
function getRuntimeUserAgent() {
|
|
1022
|
+
if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
|
|
1023
|
+
return navigator.userAgent;
|
|
1024
|
+
}
|
|
1025
|
+
return "";
|
|
1026
|
+
}
|
|
946
1027
|
function parseOsName(ua) {
|
|
947
1028
|
if (ua.includes("Windows")) return "Windows";
|
|
948
|
-
if (ua.includes("Mac OS")) return "macOS";
|
|
949
|
-
if (ua.includes("Linux")) return "Linux";
|
|
950
1029
|
if (ua.includes("Android")) return "Android";
|
|
951
1030
|
if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
1031
|
+
if (ua.includes("Mac OS")) return "macOS";
|
|
1032
|
+
if (ua.includes("Linux")) return "Linux";
|
|
952
1033
|
return "Unknown";
|
|
953
1034
|
}
|
|
1035
|
+
function parseOsVersion(ua, osName) {
|
|
1036
|
+
if (osName === "Windows") {
|
|
1037
|
+
const nt = matchVersion(ua, /Windows NT ([\d.]+)/);
|
|
1038
|
+
if (!nt) return null;
|
|
1039
|
+
if (nt.startsWith("10.0")) return "10/11";
|
|
1040
|
+
if (nt.startsWith("6.3")) return "8.1";
|
|
1041
|
+
if (nt.startsWith("6.2")) return "8";
|
|
1042
|
+
if (nt.startsWith("6.1")) return "7";
|
|
1043
|
+
return nt;
|
|
1044
|
+
}
|
|
1045
|
+
if (osName === "macOS") {
|
|
1046
|
+
const mac = matchVersion(ua, /Mac OS X ([0-9_]+)/);
|
|
1047
|
+
return mac ? mac.replace(/_/g, ".") : null;
|
|
1048
|
+
}
|
|
1049
|
+
if (osName === "Android") {
|
|
1050
|
+
return matchVersion(ua, /Android ([\d.]+)/);
|
|
1051
|
+
}
|
|
1052
|
+
if (osName === "iOS") {
|
|
1053
|
+
const ios = matchVersion(ua, /OS ([0-9_]+) like Mac OS X/);
|
|
1054
|
+
return ios ? ios.replace(/_/g, ".") : null;
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
function matchVersion(ua, pattern) {
|
|
1059
|
+
const match = ua.match(pattern);
|
|
1060
|
+
return match?.[1] ?? null;
|
|
1061
|
+
}
|
|
954
1062
|
function getDeviceType() {
|
|
955
1063
|
if (typeof window === "undefined") return "unknown";
|
|
956
1064
|
const w = window.innerWidth;
|
|
@@ -1429,6 +1537,7 @@ var BugSession = class {
|
|
|
1429
1537
|
this.autoStopTimeout = null;
|
|
1430
1538
|
this.stopInFlight = null;
|
|
1431
1539
|
this.lastArtifacts = null;
|
|
1540
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1432
1541
|
this.maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS;
|
|
1433
1542
|
this.screenRecorder = options.screenRecorder ?? new ScreenRecorder();
|
|
1434
1543
|
this.screenshotCapturer = options.screenshotCapturer ?? new ScreenshotCapturer();
|
|
@@ -1439,9 +1548,13 @@ var BugSession = class {
|
|
|
1439
1548
|
if (this.recording) {
|
|
1440
1549
|
return;
|
|
1441
1550
|
}
|
|
1551
|
+
if (this.networkLogger.isRecording()) {
|
|
1552
|
+
this.networkLogger.stop();
|
|
1553
|
+
}
|
|
1442
1554
|
this.clearAutoStopTimer();
|
|
1443
1555
|
this.networkLogger.clear();
|
|
1444
1556
|
this.lastArtifacts = null;
|
|
1557
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1445
1558
|
this.networkLogger.start();
|
|
1446
1559
|
try {
|
|
1447
1560
|
await this.screenRecorder.start({
|
|
@@ -1464,14 +1577,18 @@ var BugSession = class {
|
|
|
1464
1577
|
if (this.recording) {
|
|
1465
1578
|
await this.stop("manual");
|
|
1466
1579
|
}
|
|
1580
|
+
if (this.networkLogger.isRecording()) {
|
|
1581
|
+
this.networkLogger.stop();
|
|
1582
|
+
}
|
|
1467
1583
|
this.clearAutoStopTimer();
|
|
1468
1584
|
this.networkLogger.clear();
|
|
1469
1585
|
this.lastArtifacts = null;
|
|
1586
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1470
1587
|
const startedAtMs = Date.now();
|
|
1471
1588
|
this.networkLogger.start();
|
|
1472
1589
|
try {
|
|
1473
1590
|
const screenshotBlob = region ? await this.screenshotCapturer.captureRegion(region) : await this.screenshotCapturer.capture();
|
|
1474
|
-
const networkLogs = this.networkLogger.
|
|
1591
|
+
const networkLogs = this.networkLogger.getLogs();
|
|
1475
1592
|
const stoppedAtMs = Date.now();
|
|
1476
1593
|
const artifacts = {
|
|
1477
1594
|
videoBlob: null,
|
|
@@ -1484,10 +1601,12 @@ var BugSession = class {
|
|
|
1484
1601
|
stopReason: "manual"
|
|
1485
1602
|
};
|
|
1486
1603
|
this.lastArtifacts = artifacts;
|
|
1604
|
+
this.screenshotLogsPendingSubmit = true;
|
|
1487
1605
|
return artifacts;
|
|
1488
1606
|
} catch (error) {
|
|
1489
1607
|
this.networkLogger.stop();
|
|
1490
1608
|
this.networkLogger.clear();
|
|
1609
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1491
1610
|
throw error;
|
|
1492
1611
|
}
|
|
1493
1612
|
}
|
|
@@ -1521,17 +1640,32 @@ var BugSession = class {
|
|
|
1521
1640
|
getLastCaptureMode() {
|
|
1522
1641
|
return this.lastArtifacts?.captureMode ?? null;
|
|
1523
1642
|
}
|
|
1643
|
+
finalizeNetworkLogsForSubmit(captureMode) {
|
|
1644
|
+
if (captureMode === "screenshot" && this.screenshotLogsPendingSubmit && this.networkLogger.isRecording()) {
|
|
1645
|
+
const logs = this.networkLogger.stop();
|
|
1646
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1647
|
+
return logs;
|
|
1648
|
+
}
|
|
1649
|
+
return this.networkLogger.getLogs();
|
|
1650
|
+
}
|
|
1524
1651
|
resetArtifacts() {
|
|
1525
1652
|
this.lastArtifacts = null;
|
|
1526
1653
|
this.screenRecorder.clearLastBlob();
|
|
1654
|
+
if (this.networkLogger.isRecording()) {
|
|
1655
|
+
this.networkLogger.stop();
|
|
1656
|
+
}
|
|
1527
1657
|
this.networkLogger.clear();
|
|
1658
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1528
1659
|
}
|
|
1529
1660
|
async dispose() {
|
|
1530
1661
|
await this.stop("manual");
|
|
1531
1662
|
this.clearAutoStopTimer();
|
|
1532
1663
|
this.screenRecorder.dispose();
|
|
1533
|
-
this.networkLogger.
|
|
1664
|
+
if (this.networkLogger.isRecording()) {
|
|
1665
|
+
this.networkLogger.stop();
|
|
1666
|
+
}
|
|
1534
1667
|
this.networkLogger.clear();
|
|
1668
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1535
1669
|
}
|
|
1536
1670
|
async stopInternal(reason) {
|
|
1537
1671
|
this.clearAutoStopTimer();
|
|
@@ -1552,6 +1686,7 @@ var BugSession = class {
|
|
|
1552
1686
|
stopReason: reason
|
|
1553
1687
|
};
|
|
1554
1688
|
this.lastArtifacts = artifacts;
|
|
1689
|
+
this.screenshotLogsPendingSubmit = false;
|
|
1555
1690
|
return artifacts;
|
|
1556
1691
|
}
|
|
1557
1692
|
async handleForcedStop(reason) {
|
|
@@ -1702,9 +1837,13 @@ var BugReporter = class {
|
|
|
1702
1837
|
const payload = {
|
|
1703
1838
|
title: normalizedTitle,
|
|
1704
1839
|
description: normalizedDescription,
|
|
1840
|
+
stepsToReproduce: options.stepsToReproduce,
|
|
1841
|
+
expectedResult: options.expectedResult,
|
|
1842
|
+
actualResult: options.actualResult,
|
|
1843
|
+
additionalContext: options.additionalContext,
|
|
1705
1844
|
videoBlob: artifacts.videoBlob,
|
|
1706
1845
|
screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
|
|
1707
|
-
networkLogs: artifacts.
|
|
1846
|
+
networkLogs: this.session.finalizeNetworkLogsForSubmit(artifacts.captureMode),
|
|
1708
1847
|
consoleLogs: options.consoleLogs ?? [],
|
|
1709
1848
|
jsErrors: options.jsErrors ?? [],
|
|
1710
1849
|
captureMode: artifacts.captureMode,
|
|
@@ -2234,7 +2373,7 @@ function BugReporterProvider({
|
|
|
2234
2373
|
setScreenshotAnnotation(annotation);
|
|
2235
2374
|
}, []);
|
|
2236
2375
|
const submitReport = react.useCallback(
|
|
2237
|
-
async (title,
|
|
2376
|
+
async (title, structuredFields) => {
|
|
2238
2377
|
const reporter = getOrCreateReporter();
|
|
2239
2378
|
if (!reporter) {
|
|
2240
2379
|
setError("No bug tracker integration is configured.");
|
|
@@ -2254,6 +2393,25 @@ function BugReporterProvider({
|
|
|
2254
2393
|
setSubmissionProgress("Preparing submission\u2026");
|
|
2255
2394
|
setError(null);
|
|
2256
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";
|
|
2257
2415
|
const screenshotBlobForSubmit = draftMode === "screenshot" ? screenshotAnnotation.annotatedBlob ?? screenshotBlob : null;
|
|
2258
2416
|
const metadata = {
|
|
2259
2417
|
annotation: draftMode === "screenshot" && screenshotAnnotation.highlights.length > 0 ? {
|
|
@@ -2268,13 +2426,21 @@ function BugReporterProvider({
|
|
|
2268
2426
|
};
|
|
2269
2427
|
try {
|
|
2270
2428
|
const result = await reporter.submit(title, description, {
|
|
2429
|
+
stepsToReproduce,
|
|
2430
|
+
expectedResult,
|
|
2431
|
+
actualResult,
|
|
2432
|
+
additionalContext,
|
|
2271
2433
|
screenshotBlob: screenshotBlobForSubmit,
|
|
2272
2434
|
metadata,
|
|
2273
2435
|
consoleLogs,
|
|
2274
2436
|
jsErrors,
|
|
2275
2437
|
onProgress: setSubmissionProgress
|
|
2276
2438
|
});
|
|
2277
|
-
|
|
2439
|
+
if (result.provider === "cloud" && !result.issueUrl) {
|
|
2440
|
+
setSuccess("Report received by QuickBugs Cloud. Tracker forwarding is running in the background.");
|
|
2441
|
+
} else {
|
|
2442
|
+
setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
|
|
2443
|
+
}
|
|
2278
2444
|
clearDraft();
|
|
2279
2445
|
setIsOpen(false);
|
|
2280
2446
|
return result;
|
|
@@ -3061,6 +3227,7 @@ function providerLabel(provider) {
|
|
|
3061
3227
|
if (provider === "cloud") return "QuickBugs Cloud";
|
|
3062
3228
|
return provider;
|
|
3063
3229
|
}
|
|
3230
|
+
var CHAR_LIMIT = 4e3;
|
|
3064
3231
|
function BugReporterModal() {
|
|
3065
3232
|
const {
|
|
3066
3233
|
autoStopNotice,
|
|
@@ -3090,9 +3257,39 @@ function BugReporterModal() {
|
|
|
3090
3257
|
videoPreviewUrl
|
|
3091
3258
|
} = useBugReporter();
|
|
3092
3259
|
const [title, setTitle] = react.useState("");
|
|
3093
|
-
const [
|
|
3260
|
+
const [stepsToReproduce, setStepsToReproduce] = react.useState("");
|
|
3261
|
+
const [expectedResult, setExpectedResult] = react.useState("");
|
|
3262
|
+
const [actualResult, setActualResult] = react.useState("");
|
|
3263
|
+
const [additionalContext, setAdditionalContext] = react.useState("");
|
|
3094
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;
|
|
3095
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
|
+
};
|
|
3096
3293
|
const handleDialogOpenChange = (open) => {
|
|
3097
3294
|
if (open) {
|
|
3098
3295
|
openModal();
|
|
@@ -3103,15 +3300,24 @@ function BugReporterModal() {
|
|
|
3103
3300
|
};
|
|
3104
3301
|
const handleSubmit = async (event) => {
|
|
3105
3302
|
event.preventDefault();
|
|
3106
|
-
const result = await submitReport(title,
|
|
3303
|
+
const result = await submitReport(title, {
|
|
3304
|
+
stepsToReproduce,
|
|
3305
|
+
expectedResult,
|
|
3306
|
+
actualResult,
|
|
3307
|
+
additionalContext
|
|
3308
|
+
});
|
|
3107
3309
|
if (result) {
|
|
3108
3310
|
setTitle("");
|
|
3109
|
-
|
|
3311
|
+
setStepsToReproduce("");
|
|
3312
|
+
setExpectedResult("");
|
|
3313
|
+
setActualResult("");
|
|
3314
|
+
setAdditionalContext("");
|
|
3110
3315
|
setStep("review");
|
|
3316
|
+
setActiveTab("steps");
|
|
3111
3317
|
}
|
|
3112
3318
|
};
|
|
3113
3319
|
const hasIntegrations = availableProviders.length > 0;
|
|
3114
|
-
const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0;
|
|
3320
|
+
const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0 && !isOverLimit;
|
|
3115
3321
|
return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: isOpen, onOpenChange: handleDialogOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3116
3322
|
DialogContent,
|
|
3117
3323
|
{
|
|
@@ -3277,18 +3483,91 @@ function BugReporterModal() {
|
|
|
3277
3483
|
}
|
|
3278
3484
|
)
|
|
3279
3485
|
] }),
|
|
3280
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-
|
|
3281
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium",
|
|
3282
|
-
/* @__PURE__ */ jsxRuntime.
|
|
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(
|
|
3283
3527
|
Textarea,
|
|
3284
3528
|
{
|
|
3285
|
-
id: "bug-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
|
3290
3535
|
}
|
|
3291
|
-
)
|
|
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
|
+
] })
|
|
3292
3571
|
] }),
|
|
3293
3572
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
3294
3573
|
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", htmlFor: "bug-provider", children: "Submit to" }),
|