@bbearai/react 0.3.1 → 0.4.1

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/dist/index.js CHANGED
@@ -1543,7 +1543,16 @@ var import_jsx_runtime4 = require("react/jsx-runtime");
1543
1543
  function TestListScreen({ nav }) {
1544
1544
  const { assignments, currentAssignment, refreshAssignments } = useBugBear();
1545
1545
  const [filter, setFilter] = (0, import_react5.useState)("all");
1546
+ const [roleFilter, setRoleFilter] = (0, import_react5.useState)(null);
1546
1547
  const [collapsedFolders, setCollapsedFolders] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
1548
+ const availableRoles = (0, import_react5.useMemo)(() => {
1549
+ const roleMap = /* @__PURE__ */ new Map();
1550
+ for (const a of assignments) {
1551
+ if (a.testCase.role) roleMap.set(a.testCase.role.id, a.testCase.role);
1552
+ }
1553
+ return Array.from(roleMap.values());
1554
+ }, [assignments]);
1555
+ const selectedRole = availableRoles.find((r) => r.id === roleFilter);
1547
1556
  const groupedAssignments = (0, import_react5.useMemo)(() => {
1548
1557
  const groups = /* @__PURE__ */ new Map();
1549
1558
  for (const assignment of assignments) {
@@ -1597,6 +1606,7 @@ function TestListScreen({ nav }) {
1597
1606
  });
1598
1607
  }, []);
1599
1608
  const filterAssignment = (a) => {
1609
+ if (roleFilter && a.testCase.role?.id !== roleFilter) return false;
1600
1610
  if (filter === "pending") return a.status === "pending" || a.status === "in_progress";
1601
1611
  if (filter === "completed") return a.status === "passed" || a.status === "failed";
1602
1612
  return true;
@@ -1629,7 +1639,7 @@ function TestListScreen({ nav }) {
1629
1639
  }
1630
1640
  };
1631
1641
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1632
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", gap: 8, marginBottom: 16 }, children: filters.map((f) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1642
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", gap: 8, marginBottom: availableRoles.length >= 2 ? 8 : 16 }, children: filters.map((f) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1633
1643
  "button",
1634
1644
  {
1635
1645
  type: "button",
@@ -1656,6 +1666,79 @@ function TestListScreen({ nav }) {
1656
1666
  },
1657
1667
  f.key
1658
1668
  )) }),
1669
+ availableRoles.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: 12 }, children: [
1670
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", marginBottom: selectedRole?.loginHint ? 8 : 0 }, children: [
1671
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1672
+ "button",
1673
+ {
1674
+ type: "button",
1675
+ onClick: () => setRoleFilter(null),
1676
+ style: {
1677
+ paddingLeft: 10,
1678
+ paddingRight: 10,
1679
+ paddingTop: 4,
1680
+ paddingBottom: 4,
1681
+ borderRadius: 6,
1682
+ backgroundColor: !roleFilter ? colors.card : "transparent",
1683
+ border: !roleFilter ? `1px solid ${colors.border}` : "1px solid transparent",
1684
+ cursor: "pointer",
1685
+ fontSize: 11,
1686
+ color: !roleFilter ? colors.textPrimary : colors.textMuted,
1687
+ fontWeight: !roleFilter ? 600 : 400
1688
+ },
1689
+ children: "All Roles"
1690
+ }
1691
+ ),
1692
+ availableRoles.map((role) => {
1693
+ const isActive = roleFilter === role.id;
1694
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1695
+ "button",
1696
+ {
1697
+ type: "button",
1698
+ onClick: () => setRoleFilter(isActive ? null : role.id),
1699
+ style: {
1700
+ display: "flex",
1701
+ alignItems: "center",
1702
+ gap: 5,
1703
+ paddingLeft: 10,
1704
+ paddingRight: 10,
1705
+ paddingTop: 4,
1706
+ paddingBottom: 4,
1707
+ borderRadius: 6,
1708
+ backgroundColor: isActive ? role.color + "20" : "transparent",
1709
+ border: isActive ? `1px solid ${role.color}60` : "1px solid transparent",
1710
+ cursor: "pointer",
1711
+ fontSize: 11,
1712
+ color: isActive ? role.color : colors.textMuted,
1713
+ fontWeight: isActive ? 600 : 400
1714
+ },
1715
+ children: [
1716
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: {
1717
+ width: 6,
1718
+ height: 6,
1719
+ borderRadius: 3,
1720
+ backgroundColor: role.color,
1721
+ flexShrink: 0
1722
+ } }),
1723
+ role.name
1724
+ ]
1725
+ },
1726
+ role.id
1727
+ );
1728
+ })
1729
+ ] }),
1730
+ selectedRole?.loginHint && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
1731
+ fontSize: 11,
1732
+ color: colors.textSecondary,
1733
+ backgroundColor: selectedRole.color + "10",
1734
+ border: `1px solid ${selectedRole.color}30`,
1735
+ borderRadius: 6,
1736
+ padding: "6px 10px"
1737
+ }, children: [
1738
+ "Log in as: ",
1739
+ selectedRole.loginHint
1740
+ ] })
1741
+ ] }),
1659
1742
  groupedAssignments.map((folder) => {
1660
1743
  const folderId = folder.group?.id || "ungrouped";
1661
1744
  const isCollapsed = collapsedFolders.has(folderId);
@@ -1828,7 +1911,26 @@ function TestListScreen({ nav }) {
1828
1911
  },
1829
1912
  children: assignment.testCase.priority
1830
1913
  }
1831
- )
1914
+ ),
1915
+ assignment.testCase.role && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1916
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 11, color: colors.textDim }, children: "\xB7" }),
1917
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: {
1918
+ display: "inline-flex",
1919
+ alignItems: "center",
1920
+ gap: 3,
1921
+ fontSize: 10,
1922
+ color: assignment.testCase.role.color,
1923
+ fontWeight: 500
1924
+ }, children: [
1925
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: {
1926
+ width: 5,
1927
+ height: 5,
1928
+ borderRadius: 3,
1929
+ backgroundColor: assignment.testCase.role.color
1930
+ } }),
1931
+ assignment.testCase.role.name
1932
+ ] })
1933
+ ] })
1832
1934
  ] })
1833
1935
  ] }),
1834
1936
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -2385,6 +2487,7 @@ function ReportScreen({ nav, prefill }) {
2385
2487
  const [affectedRoute, setAffectedRoute] = (0, import_react8.useState)("");
2386
2488
  const [submitting, setSubmitting] = (0, import_react8.useState)(false);
2387
2489
  const [error, setError] = (0, import_react8.useState)(null);
2490
+ const submittingRef = (0, import_react8.useRef)(false);
2388
2491
  const observedRoute = (0, import_react8.useRef)(
2389
2492
  typeof window !== "undefined" ? window.location.pathname : "unknown"
2390
2493
  );
@@ -2392,6 +2495,8 @@ function ReportScreen({ nav, prefill }) {
2392
2495
  const isBugType = reportType === "bug" || reportType === "test_fail";
2393
2496
  const handleSubmit = async () => {
2394
2497
  if (!client || !description.trim() || images.isUploading) return;
2498
+ if (submittingRef.current) return;
2499
+ submittingRef.current = true;
2395
2500
  setSubmitting(true);
2396
2501
  setError(null);
2397
2502
  try {
@@ -2413,17 +2518,18 @@ function ReportScreen({ nav, prefill }) {
2413
2518
  if (!result.success) {
2414
2519
  setError(result.error || "Failed to submit report. Please try again.");
2415
2520
  setSubmitting(false);
2521
+ submittingRef.current = false;
2416
2522
  return;
2417
2523
  }
2418
2524
  if (prefill?.assignmentId) {
2419
2525
  await refreshAssignments();
2420
2526
  }
2421
- setSubmitting(false);
2422
2527
  nav.replace({ name: "REPORT_SUCCESS" });
2423
2528
  } catch (err) {
2424
2529
  console.error("BugBear: Report submission error", err);
2425
2530
  setError(err instanceof Error ? err.message : "An unexpected error occurred. Please try again.");
2426
2531
  setSubmitting(false);
2532
+ submittingRef.current = false;
2427
2533
  }
2428
2534
  };
2429
2535
  const typeOptions = [
@@ -3178,7 +3284,7 @@ function ThreadDetailScreen({
3178
3284
  children: msg.content
3179
3285
  }
3180
3286
  ),
3181
- msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: msg.attachments.filter((a) => a.type === "image").map((att, idx) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3287
+ msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: msg.attachments.filter((a) => a.type === "image" && typeof a.url === "string" && /^https?:\/\//i.test(a.url)).map((att, idx) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3182
3288
  "img",
3183
3289
  {
3184
3290
  src: att.url,
package/dist/index.mjs CHANGED
@@ -1514,7 +1514,16 @@ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-run
1514
1514
  function TestListScreen({ nav }) {
1515
1515
  const { assignments, currentAssignment, refreshAssignments } = useBugBear();
1516
1516
  const [filter, setFilter] = useState3("all");
1517
+ const [roleFilter, setRoleFilter] = useState3(null);
1517
1518
  const [collapsedFolders, setCollapsedFolders] = useState3(/* @__PURE__ */ new Set());
1519
+ const availableRoles = useMemo(() => {
1520
+ const roleMap = /* @__PURE__ */ new Map();
1521
+ for (const a of assignments) {
1522
+ if (a.testCase.role) roleMap.set(a.testCase.role.id, a.testCase.role);
1523
+ }
1524
+ return Array.from(roleMap.values());
1525
+ }, [assignments]);
1526
+ const selectedRole = availableRoles.find((r) => r.id === roleFilter);
1518
1527
  const groupedAssignments = useMemo(() => {
1519
1528
  const groups = /* @__PURE__ */ new Map();
1520
1529
  for (const assignment of assignments) {
@@ -1568,6 +1577,7 @@ function TestListScreen({ nav }) {
1568
1577
  });
1569
1578
  }, []);
1570
1579
  const filterAssignment = (a) => {
1580
+ if (roleFilter && a.testCase.role?.id !== roleFilter) return false;
1571
1581
  if (filter === "pending") return a.status === "pending" || a.status === "in_progress";
1572
1582
  if (filter === "completed") return a.status === "passed" || a.status === "failed";
1573
1583
  return true;
@@ -1600,7 +1610,7 @@ function TestListScreen({ nav }) {
1600
1610
  }
1601
1611
  };
1602
1612
  return /* @__PURE__ */ jsxs3("div", { children: [
1603
- /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 8, marginBottom: 16 }, children: filters.map((f) => /* @__PURE__ */ jsxs3(
1613
+ /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 8, marginBottom: availableRoles.length >= 2 ? 8 : 16 }, children: filters.map((f) => /* @__PURE__ */ jsxs3(
1604
1614
  "button",
1605
1615
  {
1606
1616
  type: "button",
@@ -1627,6 +1637,79 @@ function TestListScreen({ nav }) {
1627
1637
  },
1628
1638
  f.key
1629
1639
  )) }),
1640
+ availableRoles.length >= 2 && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 12 }, children: [
1641
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", marginBottom: selectedRole?.loginHint ? 8 : 0 }, children: [
1642
+ /* @__PURE__ */ jsx4(
1643
+ "button",
1644
+ {
1645
+ type: "button",
1646
+ onClick: () => setRoleFilter(null),
1647
+ style: {
1648
+ paddingLeft: 10,
1649
+ paddingRight: 10,
1650
+ paddingTop: 4,
1651
+ paddingBottom: 4,
1652
+ borderRadius: 6,
1653
+ backgroundColor: !roleFilter ? colors.card : "transparent",
1654
+ border: !roleFilter ? `1px solid ${colors.border}` : "1px solid transparent",
1655
+ cursor: "pointer",
1656
+ fontSize: 11,
1657
+ color: !roleFilter ? colors.textPrimary : colors.textMuted,
1658
+ fontWeight: !roleFilter ? 600 : 400
1659
+ },
1660
+ children: "All Roles"
1661
+ }
1662
+ ),
1663
+ availableRoles.map((role) => {
1664
+ const isActive = roleFilter === role.id;
1665
+ return /* @__PURE__ */ jsxs3(
1666
+ "button",
1667
+ {
1668
+ type: "button",
1669
+ onClick: () => setRoleFilter(isActive ? null : role.id),
1670
+ style: {
1671
+ display: "flex",
1672
+ alignItems: "center",
1673
+ gap: 5,
1674
+ paddingLeft: 10,
1675
+ paddingRight: 10,
1676
+ paddingTop: 4,
1677
+ paddingBottom: 4,
1678
+ borderRadius: 6,
1679
+ backgroundColor: isActive ? role.color + "20" : "transparent",
1680
+ border: isActive ? `1px solid ${role.color}60` : "1px solid transparent",
1681
+ cursor: "pointer",
1682
+ fontSize: 11,
1683
+ color: isActive ? role.color : colors.textMuted,
1684
+ fontWeight: isActive ? 600 : 400
1685
+ },
1686
+ children: [
1687
+ /* @__PURE__ */ jsx4("span", { style: {
1688
+ width: 6,
1689
+ height: 6,
1690
+ borderRadius: 3,
1691
+ backgroundColor: role.color,
1692
+ flexShrink: 0
1693
+ } }),
1694
+ role.name
1695
+ ]
1696
+ },
1697
+ role.id
1698
+ );
1699
+ })
1700
+ ] }),
1701
+ selectedRole?.loginHint && /* @__PURE__ */ jsxs3("div", { style: {
1702
+ fontSize: 11,
1703
+ color: colors.textSecondary,
1704
+ backgroundColor: selectedRole.color + "10",
1705
+ border: `1px solid ${selectedRole.color}30`,
1706
+ borderRadius: 6,
1707
+ padding: "6px 10px"
1708
+ }, children: [
1709
+ "Log in as: ",
1710
+ selectedRole.loginHint
1711
+ ] })
1712
+ ] }),
1630
1713
  groupedAssignments.map((folder) => {
1631
1714
  const folderId = folder.group?.id || "ungrouped";
1632
1715
  const isCollapsed = collapsedFolders.has(folderId);
@@ -1799,7 +1882,26 @@ function TestListScreen({ nav }) {
1799
1882
  },
1800
1883
  children: assignment.testCase.priority
1801
1884
  }
1802
- )
1885
+ ),
1886
+ assignment.testCase.role && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1887
+ /* @__PURE__ */ jsx4("span", { style: { fontSize: 11, color: colors.textDim }, children: "\xB7" }),
1888
+ /* @__PURE__ */ jsxs3("span", { style: {
1889
+ display: "inline-flex",
1890
+ alignItems: "center",
1891
+ gap: 3,
1892
+ fontSize: 10,
1893
+ color: assignment.testCase.role.color,
1894
+ fontWeight: 500
1895
+ }, children: [
1896
+ /* @__PURE__ */ jsx4("span", { style: {
1897
+ width: 5,
1898
+ height: 5,
1899
+ borderRadius: 3,
1900
+ backgroundColor: assignment.testCase.role.color
1901
+ } }),
1902
+ assignment.testCase.role.name
1903
+ ] })
1904
+ ] })
1803
1905
  ] })
1804
1906
  ] }),
1805
1907
  /* @__PURE__ */ jsx4(
@@ -2356,6 +2458,7 @@ function ReportScreen({ nav, prefill }) {
2356
2458
  const [affectedRoute, setAffectedRoute] = useState6("");
2357
2459
  const [submitting, setSubmitting] = useState6(false);
2358
2460
  const [error, setError] = useState6(null);
2461
+ const submittingRef = useRef2(false);
2359
2462
  const observedRoute = useRef2(
2360
2463
  typeof window !== "undefined" ? window.location.pathname : "unknown"
2361
2464
  );
@@ -2363,6 +2466,8 @@ function ReportScreen({ nav, prefill }) {
2363
2466
  const isBugType = reportType === "bug" || reportType === "test_fail";
2364
2467
  const handleSubmit = async () => {
2365
2468
  if (!client || !description.trim() || images.isUploading) return;
2469
+ if (submittingRef.current) return;
2470
+ submittingRef.current = true;
2366
2471
  setSubmitting(true);
2367
2472
  setError(null);
2368
2473
  try {
@@ -2384,17 +2489,18 @@ function ReportScreen({ nav, prefill }) {
2384
2489
  if (!result.success) {
2385
2490
  setError(result.error || "Failed to submit report. Please try again.");
2386
2491
  setSubmitting(false);
2492
+ submittingRef.current = false;
2387
2493
  return;
2388
2494
  }
2389
2495
  if (prefill?.assignmentId) {
2390
2496
  await refreshAssignments();
2391
2497
  }
2392
- setSubmitting(false);
2393
2498
  nav.replace({ name: "REPORT_SUCCESS" });
2394
2499
  } catch (err) {
2395
2500
  console.error("BugBear: Report submission error", err);
2396
2501
  setError(err instanceof Error ? err.message : "An unexpected error occurred. Please try again.");
2397
2502
  setSubmitting(false);
2503
+ submittingRef.current = false;
2398
2504
  }
2399
2505
  };
2400
2506
  const typeOptions = [
@@ -3149,7 +3255,7 @@ function ThreadDetailScreen({
3149
3255
  children: msg.content
3150
3256
  }
3151
3257
  ),
3152
- msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ jsx11("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: msg.attachments.filter((a) => a.type === "image").map((att, idx) => /* @__PURE__ */ jsx11(
3258
+ msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ jsx11("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: msg.attachments.filter((a) => a.type === "image" && typeof a.url === "string" && /^https?:\/\//i.test(a.url)).map((att, idx) => /* @__PURE__ */ jsx11(
3153
3259
  "img",
3154
3260
  {
3155
3261
  src: att.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "BugBear React components for web apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,7 +41,7 @@
41
41
  "prepublishOnly": "npm run build"
42
42
  },
43
43
  "dependencies": {
44
- "@bbearai/core": "^0.3.0"
44
+ "@bbearai/core": "^0.4.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": "^18.0.0 || ^19.0.0",