@bbearai/react-native 0.6.0 → 0.6.2

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.
Files changed (3) hide show
  1. package/dist/index.js +233 -27
  2. package/dist/index.mjs +238 -32
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12007,8 +12007,8 @@ var BugBearClient = class {
12007
12007
  return { success: false, error: rateLimit.error };
12008
12008
  }
12009
12009
  if (!userInfo) {
12010
- console.error("BugBear: No user info available, cannot submit report");
12011
- return { success: false, error: "User not authenticated" };
12010
+ console.error("BugBear: No user info available, cannot submit report. Ensure your BugBear config provides a getCurrentUser() callback that returns { id, email }.");
12011
+ return { success: false, error: "Unable to identify user. Check that your app passes a getCurrentUser callback to BugBear config." };
12012
12012
  }
12013
12013
  const testerInfo = await this.getTesterInfo();
12014
12014
  fullReport = {
@@ -12071,10 +12071,11 @@ var BugBearClient = class {
12071
12071
  const pageSize = Math.min(options?.pageSize ?? 100, 100);
12072
12072
  const from = (options?.page ?? 0) * pageSize;
12073
12073
  const to = from + pageSize - 1;
12074
- const { data, error } = await this.supabase.from("test_assignments").select(`
12074
+ const selectFields = `
12075
12075
  id,
12076
12076
  status,
12077
12077
  started_at,
12078
+ completed_at,
12078
12079
  skip_reason,
12079
12080
  is_verification,
12080
12081
  original_report_id,
@@ -12109,20 +12110,23 @@ var BugBearClient = class {
12109
12110
  color,
12110
12111
  description,
12111
12112
  login_hint
12112
- )
12113
+ ),
12114
+ platforms
12113
12115
  )
12114
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to);
12115
- if (error) {
12116
- console.error("BugBear: Failed to fetch assignments", formatPgError(error));
12116
+ `;
12117
+ const [pendingResult, completedResult] = await Promise.all([
12118
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to),
12119
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["passed", "failed", "skipped", "blocked"]).order("completed_at", { ascending: false }).limit(100)
12120
+ ]);
12121
+ if (pendingResult.error) {
12122
+ console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
12117
12123
  return [];
12118
12124
  }
12119
- const mapped = (data || []).filter((item) => {
12120
- if (!item.test_case) {
12121
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
12122
- return false;
12123
- }
12124
- return true;
12125
- }).map((item) => ({
12125
+ const allData = [
12126
+ ...pendingResult.data || [],
12127
+ ...completedResult.data || []
12128
+ ];
12129
+ const mapItem = (item) => ({
12126
12130
  id: item.id,
12127
12131
  status: item.status,
12128
12132
  startedAt: item.started_at,
@@ -12160,12 +12164,24 @@ var BugBearClient = class {
12160
12164
  color: item.test_case.role.color,
12161
12165
  description: item.test_case.role.description,
12162
12166
  loginHint: item.test_case.role.login_hint
12163
- } : void 0
12167
+ } : void 0,
12168
+ platforms: item.test_case.platforms || void 0
12164
12169
  }
12165
- }));
12170
+ });
12171
+ const mapped = allData.filter((item) => {
12172
+ if (!item.test_case) {
12173
+ console.warn("BugBear: Assignment returned without test_case", { id: item.id });
12174
+ return false;
12175
+ }
12176
+ return true;
12177
+ }).map(mapItem);
12166
12178
  mapped.sort((a, b) => {
12167
12179
  if (a.isVerification && !b.isVerification) return -1;
12168
12180
  if (!a.isVerification && b.isVerification) return 1;
12181
+ const aActive = a.status === "pending" || a.status === "in_progress";
12182
+ const bActive = b.status === "pending" || b.status === "in_progress";
12183
+ if (aActive && !bActive) return -1;
12184
+ if (!aActive && bActive) return 1;
12169
12185
  return 0;
12170
12186
  });
12171
12187
  return mapped;
@@ -12330,6 +12346,36 @@ var BugBearClient = class {
12330
12346
  async failAssignment(assignmentId) {
12331
12347
  return this.updateAssignmentStatus(assignmentId, "failed");
12332
12348
  }
12349
+ /**
12350
+ * Reopen a completed assignment — sets it back to in_progress with a fresh timer.
12351
+ * Clears completed_at and duration_seconds so it can be re-evaluated.
12352
+ */
12353
+ async reopenAssignment(assignmentId) {
12354
+ try {
12355
+ const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
12356
+ if (fetchError || !current) {
12357
+ return { success: false, error: "Assignment not found" };
12358
+ }
12359
+ if (current.status === "pending" || current.status === "in_progress") {
12360
+ return { success: true };
12361
+ }
12362
+ const { error } = await this.supabase.from("test_assignments").update({
12363
+ status: "in_progress",
12364
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
12365
+ completed_at: null,
12366
+ duration_seconds: null,
12367
+ skip_reason: null
12368
+ }).eq("id", assignmentId).eq("status", current.status);
12369
+ if (error) {
12370
+ console.error("BugBear: Failed to reopen assignment", error);
12371
+ return { success: false, error: error.message };
12372
+ }
12373
+ return { success: true };
12374
+ } catch (err) {
12375
+ const message = err instanceof Error ? err.message : "Unknown error";
12376
+ return { success: false, error: message };
12377
+ }
12378
+ }
12333
12379
  /**
12334
12380
  * Skip a test assignment with a required reason
12335
12381
  * Marks the assignment as 'skipped' and records why it was skipped
@@ -14470,6 +14516,39 @@ function TestDetailScreen({ testId, nav }) {
14470
14516
  setIsSubmitting(false);
14471
14517
  }
14472
14518
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
14519
+ const handleReopen = (0, import_react5.useCallback)(async () => {
14520
+ if (!client || !displayedAssignment || isSubmitting) return;
14521
+ import_react_native5.Keyboard.dismiss();
14522
+ setIsSubmitting(true);
14523
+ try {
14524
+ await client.reopenAssignment(displayedAssignment.id);
14525
+ await refreshAssignments();
14526
+ } finally {
14527
+ setIsSubmitting(false);
14528
+ }
14529
+ }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
14530
+ const handleChangeResult = (0, import_react5.useCallback)(async (newStatus) => {
14531
+ if (!client || !displayedAssignment || isSubmitting) return;
14532
+ import_react_native5.Keyboard.dismiss();
14533
+ setIsSubmitting(true);
14534
+ try {
14535
+ await client.reopenAssignment(displayedAssignment.id);
14536
+ await client.updateAssignmentStatus(displayedAssignment.id, newStatus);
14537
+ await refreshAssignments();
14538
+ if (newStatus === "failed") {
14539
+ nav.replace({
14540
+ name: "REPORT",
14541
+ prefill: {
14542
+ type: "test_fail",
14543
+ assignmentId: displayedAssignment.id,
14544
+ testCaseId: displayedAssignment.testCase.id
14545
+ }
14546
+ });
14547
+ }
14548
+ } finally {
14549
+ setIsSubmitting(false);
14550
+ }
14551
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
14473
14552
  const handleSkip = (0, import_react5.useCallback)(async () => {
14474
14553
  if (!client || !displayedAssignment || !selectedSkipReason) return;
14475
14554
  import_react_native5.Keyboard.dismiss();
@@ -14550,7 +14629,39 @@ function TestDetailScreen({ testId, nav }) {
14550
14629
  }
14551
14630
  },
14552
14631
  /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
14553
- ), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
14632
+ ), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.track && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailMeta }, testCase.track.icon, " ", testCase.track.name), testCase.description && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), displayedAssignment.status === "passed" || displayedAssignment.status === "failed" || displayedAssignment.status === "skipped" || displayedAssignment.status === "blocked" ? /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: [
14633
+ styles2.completedBanner,
14634
+ displayedAssignment.status === "passed" && styles2.completedBannerPass,
14635
+ displayedAssignment.status === "failed" && styles2.completedBannerFail
14636
+ ] }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.completedIcon }, displayedAssignment.status === "passed" ? "\u2705" : displayedAssignment.status === "failed" ? "\u274C" : displayedAssignment.status === "skipped" ? "\u23ED" : "\u{1F6AB}"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: [
14637
+ styles2.completedLabel,
14638
+ displayedAssignment.status === "passed" && { color: colors.green },
14639
+ displayedAssignment.status === "failed" && { color: "#fca5a5" }
14640
+ ] }, "Marked as ", displayedAssignment.status.charAt(0).toUpperCase() + displayedAssignment.status.slice(1))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: [styles2.actionButtons, { marginTop: 4 }] }, /* @__PURE__ */ import_react5.default.createElement(
14641
+ import_react_native5.TouchableOpacity,
14642
+ {
14643
+ style: [styles2.actionBtn, styles2.reopenBtn, isSubmitting && { opacity: 0.5 }],
14644
+ onPress: handleReopen,
14645
+ disabled: isSubmitting
14646
+ },
14647
+ /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.reopenBtnText }, isSubmitting ? "Reopening..." : "\u{1F504} Reopen Test")
14648
+ ), displayedAssignment.status === "passed" && /* @__PURE__ */ import_react5.default.createElement(
14649
+ import_react_native5.TouchableOpacity,
14650
+ {
14651
+ style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }],
14652
+ onPress: () => handleChangeResult("failed"),
14653
+ disabled: isSubmitting
14654
+ },
14655
+ /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.failBtnText }, "Change to Fail")
14656
+ ), displayedAssignment.status === "failed" && /* @__PURE__ */ import_react5.default.createElement(
14657
+ import_react_native5.TouchableOpacity,
14658
+ {
14659
+ style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }],
14660
+ onPress: () => handleChangeResult("passed"),
14661
+ disabled: isSubmitting
14662
+ },
14663
+ /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.passBtnText }, "Change to Pass")
14664
+ ))) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
14554
14665
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
14555
14666
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
14556
14667
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14655,6 +14766,14 @@ var styles2 = import_react_native5.StyleSheet.create({
14655
14766
  detailDesc: { fontSize: 13, color: colors.textSecondary, lineHeight: 18 },
14656
14767
  folderProgress: { marginTop: 8 },
14657
14768
  folderName: { fontSize: 12, color: colors.textMuted },
14769
+ // Completed state
14770
+ completedBanner: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, paddingVertical: 8, paddingHorizontal: 12, borderRadius: 8, marginTop: 8, marginBottom: 8, backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border },
14771
+ completedBannerPass: { backgroundColor: colors.greenDark, borderColor: colors.green },
14772
+ completedBannerFail: { backgroundColor: colors.redDark, borderColor: colors.red },
14773
+ completedIcon: { fontSize: 14 },
14774
+ completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
14775
+ reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
14776
+ reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
14658
14777
  // Action buttons
14659
14778
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
14660
14779
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -14685,10 +14804,11 @@ var styles2 = import_react_native5.StyleSheet.create({
14685
14804
  var import_react6 = __toESM(require("react"));
14686
14805
  var import_react_native6 = require("react-native");
14687
14806
  function TestListScreen({ nav }) {
14688
- const { assignments, currentAssignment, refreshAssignments, isLoading } = useBugBear();
14807
+ const { assignments, currentAssignment, refreshAssignments, dashboardUrl, isLoading } = useBugBear();
14689
14808
  const [filter, setFilter] = (0, import_react6.useState)("all");
14690
14809
  const [roleFilter, setRoleFilter] = (0, import_react6.useState)(null);
14691
14810
  const [trackFilter, setTrackFilter] = (0, import_react6.useState)(null);
14811
+ const [platformFilter, setPlatformFilter] = (0, import_react6.useState)(import_react_native6.Platform.OS === "android" ? "android" : "ios");
14692
14812
  const [searchQuery, setSearchQuery] = (0, import_react6.useState)("");
14693
14813
  const [sortMode, setSortMode] = (0, import_react6.useState)("priority");
14694
14814
  const [collapsedFolders, setCollapsedFolders] = (0, import_react6.useState)(/* @__PURE__ */ new Set());
@@ -14755,6 +14875,7 @@ function TestListScreen({ nav }) {
14755
14875
  });
14756
14876
  }, []);
14757
14877
  const filterAssignment = (0, import_react6.useCallback)((a) => {
14878
+ if (platformFilter && a.testCase.platforms && !a.testCase.platforms.includes(platformFilter)) return false;
14758
14879
  if (roleFilter && a.testCase.role?.id !== roleFilter) return false;
14759
14880
  if (trackFilter && a.testCase.track?.id !== trackFilter) return false;
14760
14881
  if (searchQuery) {
@@ -14767,7 +14888,7 @@ function TestListScreen({ nav }) {
14767
14888
  if (filter === "done") return a.status === "passed";
14768
14889
  if (filter === "reopened") return a.status === "failed";
14769
14890
  return true;
14770
- }, [roleFilter, trackFilter, searchQuery, filter]);
14891
+ }, [platformFilter, roleFilter, trackFilter, searchQuery, filter]);
14771
14892
  if (isLoading) return /* @__PURE__ */ import_react6.default.createElement(TestListScreenSkeleton, null);
14772
14893
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, null, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles3.filterBar }, [
14773
14894
  { key: "all", label: "All", count: assignments.length },
@@ -14805,7 +14926,32 @@ function TestListScreen({ nav }) {
14805
14926
  placeholderTextColor: colors.textMuted,
14806
14927
  style: styles3.searchInput
14807
14928
  }
14808
- )), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles3.trackSortRow }, availableTracks.length >= 2 && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: { flex: 1 } }, /* @__PURE__ */ import_react6.default.createElement(
14929
+ )), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles3.platformBar }, /* @__PURE__ */ import_react6.default.createElement(
14930
+ import_react_native6.TouchableOpacity,
14931
+ {
14932
+ style: [styles3.platformBtn, !platformFilter && styles3.platformBtnActive],
14933
+ onPress: () => setPlatformFilter(null)
14934
+ },
14935
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles3.platformBtnText, !platformFilter && styles3.platformBtnTextActive] }, "All")
14936
+ ), [
14937
+ { key: "web", label: "Web", icon: "\u{1F310}" },
14938
+ { key: "ios", label: "iOS", icon: "\u{1F4F1}" },
14939
+ { key: "android", label: "Android", icon: "\u{1F916}" }
14940
+ ].map((p) => {
14941
+ const isActive = platformFilter === p.key;
14942
+ return /* @__PURE__ */ import_react6.default.createElement(
14943
+ import_react_native6.TouchableOpacity,
14944
+ {
14945
+ key: p.key,
14946
+ style: [
14947
+ styles3.platformBtn,
14948
+ isActive && { backgroundColor: colors.blue + "20", borderColor: colors.blue + "60", borderWidth: 1 }
14949
+ ],
14950
+ onPress: () => setPlatformFilter(isActive ? null : p.key)
14951
+ },
14952
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles3.platformBtnText, isActive && { color: colors.blue, fontWeight: "600" }] }, p.icon, " ", p.label)
14953
+ );
14954
+ })), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles3.trackSortRow }, availableTracks.length >= 2 && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: { flex: 1 } }, /* @__PURE__ */ import_react6.default.createElement(
14809
14955
  import_react_native6.TouchableOpacity,
14810
14956
  {
14811
14957
  style: [styles3.trackBtn, !trackFilter && styles3.trackBtnActive],
@@ -14866,7 +15012,15 @@ function TestListScreen({ nav }) {
14866
15012
  ] }, badge.label))
14867
15013
  );
14868
15014
  }));
14869
- }), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.TouchableOpacity, { style: styles3.refreshBtn, onPress: refreshAssignments }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles3.refreshText }, "\u21BB", " Refresh")));
15015
+ }), dashboardUrl && /* @__PURE__ */ import_react6.default.createElement(
15016
+ import_react_native6.TouchableOpacity,
15017
+ {
15018
+ style: styles3.dashboardLink,
15019
+ onPress: () => import_react_native6.Linking.openURL(`${dashboardUrl}/test-cases`),
15020
+ activeOpacity: 0.7
15021
+ },
15022
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles3.dashboardLinkText }, "\u{1F310}", " Manage on Dashboard ", "\u2192")
15023
+ ), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.TouchableOpacity, { style: styles3.refreshBtn, onPress: refreshAssignments }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles3.refreshText }, "\u21BB", " Refresh")));
14870
15024
  }
14871
15025
  var styles3 = import_react_native6.StyleSheet.create({
14872
15026
  filterBar: { flexDirection: "row", gap: 8, marginBottom: 8 },
@@ -14905,6 +15059,11 @@ var styles3 = import_react_native6.StyleSheet.create({
14905
15059
  statusPillText: { fontSize: 10, fontWeight: "600" },
14906
15060
  searchContainer: { marginBottom: 8 },
14907
15061
  searchInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
15062
+ platformBar: { flexDirection: "row", marginBottom: 8 },
15063
+ platformBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 6, marginRight: 6, borderWidth: 1, borderColor: "transparent" },
15064
+ platformBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
15065
+ platformBtnText: { fontSize: 11, color: colors.textMuted },
15066
+ platformBtnTextActive: { color: colors.textPrimary, fontWeight: "600" },
14908
15067
  trackSortRow: { flexDirection: "row", alignItems: "center", marginBottom: 10, gap: 8 },
14909
15068
  trackBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 6, marginRight: 6, borderWidth: 1, borderColor: "transparent" },
14910
15069
  trackBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
@@ -14915,7 +15074,9 @@ var styles3 = import_react_native6.StyleSheet.create({
14915
15074
  sortBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
14916
15075
  sortBtnText: { fontSize: 11, color: colors.textMuted },
14917
15076
  sortBtnTextActive: { color: colors.textPrimary, fontWeight: "600" },
14918
- refreshBtn: { alignItems: "center", paddingVertical: 12 },
15077
+ dashboardLink: { alignItems: "center", paddingTop: 12 },
15078
+ dashboardLinkText: { fontSize: 13, fontWeight: "500", color: colors.blue },
15079
+ refreshBtn: { alignItems: "center", paddingVertical: 8 },
14919
15080
  refreshText: { fontSize: 13, color: colors.blue }
14920
15081
  });
14921
15082
 
@@ -15084,7 +15245,9 @@ var styles4 = import_react_native7.StyleSheet.create({
15084
15245
 
15085
15246
  // src/widget/ImagePickerButtons.tsx
15086
15247
  function ImagePickerButtons({ images, maxImages, onPickGallery, onPickCamera, onRemove, label }) {
15087
- if (!IMAGE_PICKER_AVAILABLE) return null;
15248
+ if (!IMAGE_PICKER_AVAILABLE) {
15249
+ return /* @__PURE__ */ import_react9.default.createElement(import_react_native8.View, { style: styles5.section }, label && /* @__PURE__ */ import_react9.default.createElement(import_react_native8.Text, { style: styles5.label }, label), /* @__PURE__ */ import_react9.default.createElement(import_react_native8.View, { style: styles5.unavailableRow }, /* @__PURE__ */ import_react9.default.createElement(import_react_native8.Text, { style: styles5.unavailableText }, "Install react-native-image-picker to enable photo attachments")));
15250
+ }
15088
15251
  return /* @__PURE__ */ import_react9.default.createElement(import_react_native8.View, { style: styles5.section }, label && /* @__PURE__ */ import_react9.default.createElement(import_react_native8.Text, { style: styles5.label }, label), /* @__PURE__ */ import_react9.default.createElement(import_react_native8.View, { style: styles5.buttonRow }, /* @__PURE__ */ import_react9.default.createElement(
15089
15252
  import_react_native8.TouchableOpacity,
15090
15253
  {
@@ -15139,6 +15302,18 @@ var styles5 = import_react_native8.StyleSheet.create({
15139
15302
  fontSize: 12,
15140
15303
  color: colors.textDim,
15141
15304
  marginLeft: 4
15305
+ },
15306
+ unavailableRow: {
15307
+ backgroundColor: colors.card,
15308
+ borderWidth: 1,
15309
+ borderColor: colors.border,
15310
+ borderRadius: 8,
15311
+ padding: 12
15312
+ },
15313
+ unavailableText: {
15314
+ fontSize: 12,
15315
+ color: colors.textDim,
15316
+ fontStyle: "italic"
15142
15317
  }
15143
15318
  });
15144
15319
 
@@ -15639,7 +15814,7 @@ var styles9 = import_react_native12.StyleSheet.create({
15639
15814
  var import_react14 = __toESM(require("react"));
15640
15815
  var import_react_native13 = require("react-native");
15641
15816
  function MessageListScreen({ nav }) {
15642
- const { threads, unreadCount, refreshThreads, isLoading } = useBugBear();
15817
+ const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading } = useBugBear();
15643
15818
  if (isLoading) return /* @__PURE__ */ import_react14.default.createElement(MessageListScreenSkeleton, null);
15644
15819
  return /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, null, /* @__PURE__ */ import_react14.default.createElement(
15645
15820
  import_react_native13.TouchableOpacity,
@@ -15657,7 +15832,15 @@ function MessageListScreen({ nav }) {
15657
15832
  },
15658
15833
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.threadLeft }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.threadInfo }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.threadTitleRow }, thread.isPinned && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.lastMessage && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
15659
15834
  /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.threadRight }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.threadTime }, formatRelativeTime(thread.lastMessageAt)), thread.unreadCount > 0 && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.unreadBadge }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.unreadText }, thread.unreadCount)), thread.priority !== "normal" && /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: [styles10.priorityDot, { backgroundColor: getPriorityColor(thread.priority) }] }))
15660
- ))), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.footer }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.TouchableOpacity, { onPress: refreshThreads }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.refreshText }, "\u21BB Refresh"))));
15835
+ ))), dashboardUrl && /* @__PURE__ */ import_react14.default.createElement(
15836
+ import_react_native13.TouchableOpacity,
15837
+ {
15838
+ style: styles10.dashboardLink,
15839
+ onPress: () => import_react_native13.Linking.openURL(`${dashboardUrl}/discussions`),
15840
+ activeOpacity: 0.7
15841
+ },
15842
+ /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
15843
+ ), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.View, { style: styles10.footer }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ import_react14.default.createElement(import_react_native13.TouchableOpacity, { onPress: refreshThreads }, /* @__PURE__ */ import_react14.default.createElement(import_react_native13.Text, { style: styles10.refreshText }, "\u21BB Refresh"))));
15661
15844
  }
15662
15845
  var styles10 = import_react_native13.StyleSheet.create({
15663
15846
  newMsgButton: { backgroundColor: colors.blue, paddingVertical: 12, borderRadius: 12, alignItems: "center", marginBottom: 16 },
@@ -15676,7 +15859,9 @@ var styles10 = import_react_native13.StyleSheet.create({
15676
15859
  unreadBadge: { backgroundColor: colors.blue, borderRadius: 10, minWidth: 20, height: 20, justifyContent: "center", alignItems: "center", paddingHorizontal: 6 },
15677
15860
  unreadText: { fontSize: 11, fontWeight: "bold", color: "#fff" },
15678
15861
  priorityDot: { width: 8, height: 8, borderRadius: 4 },
15679
- footer: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingTop: 12, paddingHorizontal: 4 },
15862
+ dashboardLink: { alignItems: "center", paddingTop: 12 },
15863
+ dashboardLinkText: { fontSize: 13, fontWeight: "500", color: colors.blue },
15864
+ footer: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingTop: 8, paddingHorizontal: 4 },
15680
15865
  footerText: { fontSize: 12, color: colors.textMuted },
15681
15866
  refreshText: { fontSize: 13, color: colors.blue }
15682
15867
  });
@@ -16179,9 +16364,18 @@ var SEVERITY_CONFIG = {
16179
16364
  low: { label: "Low", color: "#71717a", bg: "#27272a" }
16180
16365
  };
16181
16366
  function IssueDetailScreen({ nav, issue }) {
16367
+ const { dashboardUrl } = useBugBear();
16182
16368
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: "#27272a", color: "#a1a1aa" };
16183
16369
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
16184
- return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles15.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles15.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles15.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles15.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugTitle }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles15.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))));
16370
+ return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles15.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles15.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles15.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles15.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugTitle }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles15.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles15.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
16371
+ import_react_native18.TouchableOpacity,
16372
+ {
16373
+ style: styles15.dashboardLink,
16374
+ onPress: () => import_react_native18.Linking.openURL(`${dashboardUrl}/reports`),
16375
+ activeOpacity: 0.7
16376
+ },
16377
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles15.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
16378
+ ));
16185
16379
  }
16186
16380
  var styles15 = import_react_native18.StyleSheet.create({
16187
16381
  badgeRow: {
@@ -16310,6 +16504,18 @@ var styles15 = import_react_native18.StyleSheet.create({
16310
16504
  metaTextSmall: {
16311
16505
  fontSize: 11,
16312
16506
  color: colors.textDim
16507
+ },
16508
+ dashboardLink: {
16509
+ alignItems: "center",
16510
+ paddingVertical: 10,
16511
+ marginTop: 12,
16512
+ borderTopWidth: 1,
16513
+ borderTopColor: colors.border
16514
+ },
16515
+ dashboardLinkText: {
16516
+ fontSize: 13,
16517
+ fontWeight: "500",
16518
+ color: colors.blue
16313
16519
  }
16314
16520
  });
16315
16521
 
package/dist/index.mjs CHANGED
@@ -11974,8 +11974,8 @@ var BugBearClient = class {
11974
11974
  return { success: false, error: rateLimit.error };
11975
11975
  }
11976
11976
  if (!userInfo) {
11977
- console.error("BugBear: No user info available, cannot submit report");
11978
- return { success: false, error: "User not authenticated" };
11977
+ console.error("BugBear: No user info available, cannot submit report. Ensure your BugBear config provides a getCurrentUser() callback that returns { id, email }.");
11978
+ return { success: false, error: "Unable to identify user. Check that your app passes a getCurrentUser callback to BugBear config." };
11979
11979
  }
11980
11980
  const testerInfo = await this.getTesterInfo();
11981
11981
  fullReport = {
@@ -12038,10 +12038,11 @@ var BugBearClient = class {
12038
12038
  const pageSize = Math.min(options?.pageSize ?? 100, 100);
12039
12039
  const from = (options?.page ?? 0) * pageSize;
12040
12040
  const to = from + pageSize - 1;
12041
- const { data, error } = await this.supabase.from("test_assignments").select(`
12041
+ const selectFields = `
12042
12042
  id,
12043
12043
  status,
12044
12044
  started_at,
12045
+ completed_at,
12045
12046
  skip_reason,
12046
12047
  is_verification,
12047
12048
  original_report_id,
@@ -12076,20 +12077,23 @@ var BugBearClient = class {
12076
12077
  color,
12077
12078
  description,
12078
12079
  login_hint
12079
- )
12080
+ ),
12081
+ platforms
12080
12082
  )
12081
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to);
12082
- if (error) {
12083
- console.error("BugBear: Failed to fetch assignments", formatPgError(error));
12083
+ `;
12084
+ const [pendingResult, completedResult] = await Promise.all([
12085
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to),
12086
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["passed", "failed", "skipped", "blocked"]).order("completed_at", { ascending: false }).limit(100)
12087
+ ]);
12088
+ if (pendingResult.error) {
12089
+ console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
12084
12090
  return [];
12085
12091
  }
12086
- const mapped = (data || []).filter((item) => {
12087
- if (!item.test_case) {
12088
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
12089
- return false;
12090
- }
12091
- return true;
12092
- }).map((item) => ({
12092
+ const allData = [
12093
+ ...pendingResult.data || [],
12094
+ ...completedResult.data || []
12095
+ ];
12096
+ const mapItem = (item) => ({
12093
12097
  id: item.id,
12094
12098
  status: item.status,
12095
12099
  startedAt: item.started_at,
@@ -12127,12 +12131,24 @@ var BugBearClient = class {
12127
12131
  color: item.test_case.role.color,
12128
12132
  description: item.test_case.role.description,
12129
12133
  loginHint: item.test_case.role.login_hint
12130
- } : void 0
12134
+ } : void 0,
12135
+ platforms: item.test_case.platforms || void 0
12131
12136
  }
12132
- }));
12137
+ });
12138
+ const mapped = allData.filter((item) => {
12139
+ if (!item.test_case) {
12140
+ console.warn("BugBear: Assignment returned without test_case", { id: item.id });
12141
+ return false;
12142
+ }
12143
+ return true;
12144
+ }).map(mapItem);
12133
12145
  mapped.sort((a, b) => {
12134
12146
  if (a.isVerification && !b.isVerification) return -1;
12135
12147
  if (!a.isVerification && b.isVerification) return 1;
12148
+ const aActive = a.status === "pending" || a.status === "in_progress";
12149
+ const bActive = b.status === "pending" || b.status === "in_progress";
12150
+ if (aActive && !bActive) return -1;
12151
+ if (!aActive && bActive) return 1;
12136
12152
  return 0;
12137
12153
  });
12138
12154
  return mapped;
@@ -12297,6 +12313,36 @@ var BugBearClient = class {
12297
12313
  async failAssignment(assignmentId) {
12298
12314
  return this.updateAssignmentStatus(assignmentId, "failed");
12299
12315
  }
12316
+ /**
12317
+ * Reopen a completed assignment — sets it back to in_progress with a fresh timer.
12318
+ * Clears completed_at and duration_seconds so it can be re-evaluated.
12319
+ */
12320
+ async reopenAssignment(assignmentId) {
12321
+ try {
12322
+ const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
12323
+ if (fetchError || !current) {
12324
+ return { success: false, error: "Assignment not found" };
12325
+ }
12326
+ if (current.status === "pending" || current.status === "in_progress") {
12327
+ return { success: true };
12328
+ }
12329
+ const { error } = await this.supabase.from("test_assignments").update({
12330
+ status: "in_progress",
12331
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
12332
+ completed_at: null,
12333
+ duration_seconds: null,
12334
+ skip_reason: null
12335
+ }).eq("id", assignmentId).eq("status", current.status);
12336
+ if (error) {
12337
+ console.error("BugBear: Failed to reopen assignment", error);
12338
+ return { success: false, error: error.message };
12339
+ }
12340
+ return { success: true };
12341
+ } catch (err) {
12342
+ const message = err instanceof Error ? err.message : "Unknown error";
12343
+ return { success: false, error: message };
12344
+ }
12345
+ }
12300
12346
  /**
12301
12347
  * Skip a test assignment with a required reason
12302
12348
  * Marks the assignment as 'skipped' and records why it was skipped
@@ -13733,7 +13779,7 @@ import {
13733
13779
  StyleSheet as StyleSheet18,
13734
13780
  Dimensions as Dimensions2,
13735
13781
  KeyboardAvoidingView,
13736
- Platform as Platform4,
13782
+ Platform as Platform5,
13737
13783
  PanResponder,
13738
13784
  Animated as Animated2,
13739
13785
  ActivityIndicator as ActivityIndicator2,
@@ -14452,6 +14498,39 @@ function TestDetailScreen({ testId, nav }) {
14452
14498
  setIsSubmitting(false);
14453
14499
  }
14454
14500
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
14501
+ const handleReopen = useCallback2(async () => {
14502
+ if (!client || !displayedAssignment || isSubmitting) return;
14503
+ Keyboard.dismiss();
14504
+ setIsSubmitting(true);
14505
+ try {
14506
+ await client.reopenAssignment(displayedAssignment.id);
14507
+ await refreshAssignments();
14508
+ } finally {
14509
+ setIsSubmitting(false);
14510
+ }
14511
+ }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
14512
+ const handleChangeResult = useCallback2(async (newStatus) => {
14513
+ if (!client || !displayedAssignment || isSubmitting) return;
14514
+ Keyboard.dismiss();
14515
+ setIsSubmitting(true);
14516
+ try {
14517
+ await client.reopenAssignment(displayedAssignment.id);
14518
+ await client.updateAssignmentStatus(displayedAssignment.id, newStatus);
14519
+ await refreshAssignments();
14520
+ if (newStatus === "failed") {
14521
+ nav.replace({
14522
+ name: "REPORT",
14523
+ prefill: {
14524
+ type: "test_fail",
14525
+ assignmentId: displayedAssignment.id,
14526
+ testCaseId: displayedAssignment.testCase.id
14527
+ }
14528
+ });
14529
+ }
14530
+ } finally {
14531
+ setIsSubmitting(false);
14532
+ }
14533
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
14455
14534
  const handleSkip = useCallback2(async () => {
14456
14535
  if (!client || !displayedAssignment || !selectedSkipReason) return;
14457
14536
  Keyboard.dismiss();
@@ -14532,7 +14611,39 @@ function TestDetailScreen({ testId, nav }) {
14532
14611
  }
14533
14612
  },
14534
14613
  /* @__PURE__ */ React4.createElement(Text2, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
14535
- ), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React4.createElement(View3, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React4.createElement(View3, { style: styles2.folderProgress }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ React4.createElement(View3, { style: styles2.actionButtons }, /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ React4.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React4.createElement(View3, { style: styles2.modalOverlay }, /* @__PURE__ */ React4.createElement(View3, { style: styles2.modalContent }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React4.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
14614
+ ), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React4.createElement(View3, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.track && /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailMeta }, testCase.track.icon, " ", testCase.track.name), testCase.description && /* @__PURE__ */ React4.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React4.createElement(View3, { style: styles2.folderProgress }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), displayedAssignment.status === "passed" || displayedAssignment.status === "failed" || displayedAssignment.status === "skipped" || displayedAssignment.status === "blocked" ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(View3, { style: [
14615
+ styles2.completedBanner,
14616
+ displayedAssignment.status === "passed" && styles2.completedBannerPass,
14617
+ displayedAssignment.status === "failed" && styles2.completedBannerFail
14618
+ ] }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.completedIcon }, displayedAssignment.status === "passed" ? "\u2705" : displayedAssignment.status === "failed" ? "\u274C" : displayedAssignment.status === "skipped" ? "\u23ED" : "\u{1F6AB}"), /* @__PURE__ */ React4.createElement(Text2, { style: [
14619
+ styles2.completedLabel,
14620
+ displayedAssignment.status === "passed" && { color: colors.green },
14621
+ displayedAssignment.status === "failed" && { color: "#fca5a5" }
14622
+ ] }, "Marked as ", displayedAssignment.status.charAt(0).toUpperCase() + displayedAssignment.status.slice(1))), /* @__PURE__ */ React4.createElement(View3, { style: [styles2.actionButtons, { marginTop: 4 }] }, /* @__PURE__ */ React4.createElement(
14623
+ TouchableOpacity2,
14624
+ {
14625
+ style: [styles2.actionBtn, styles2.reopenBtn, isSubmitting && { opacity: 0.5 }],
14626
+ onPress: handleReopen,
14627
+ disabled: isSubmitting
14628
+ },
14629
+ /* @__PURE__ */ React4.createElement(Text2, { style: styles2.reopenBtnText }, isSubmitting ? "Reopening..." : "\u{1F504} Reopen Test")
14630
+ ), displayedAssignment.status === "passed" && /* @__PURE__ */ React4.createElement(
14631
+ TouchableOpacity2,
14632
+ {
14633
+ style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }],
14634
+ onPress: () => handleChangeResult("failed"),
14635
+ disabled: isSubmitting
14636
+ },
14637
+ /* @__PURE__ */ React4.createElement(Text2, { style: styles2.failBtnText }, "Change to Fail")
14638
+ ), displayedAssignment.status === "failed" && /* @__PURE__ */ React4.createElement(
14639
+ TouchableOpacity2,
14640
+ {
14641
+ style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }],
14642
+ onPress: () => handleChangeResult("passed"),
14643
+ disabled: isSubmitting
14644
+ },
14645
+ /* @__PURE__ */ React4.createElement(Text2, { style: styles2.passBtnText }, "Change to Pass")
14646
+ ))) : /* @__PURE__ */ React4.createElement(View3, { style: styles2.actionButtons }, /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ React4.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React4.createElement(View3, { style: styles2.modalOverlay }, /* @__PURE__ */ React4.createElement(View3, { style: styles2.modalContent }, /* @__PURE__ */ React4.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React4.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
14536
14647
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
14537
14648
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
14538
14649
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14637,6 +14748,14 @@ var styles2 = StyleSheet4.create({
14637
14748
  detailDesc: { fontSize: 13, color: colors.textSecondary, lineHeight: 18 },
14638
14749
  folderProgress: { marginTop: 8 },
14639
14750
  folderName: { fontSize: 12, color: colors.textMuted },
14751
+ // Completed state
14752
+ completedBanner: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, paddingVertical: 8, paddingHorizontal: 12, borderRadius: 8, marginTop: 8, marginBottom: 8, backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border },
14753
+ completedBannerPass: { backgroundColor: colors.greenDark, borderColor: colors.green },
14754
+ completedBannerFail: { backgroundColor: colors.redDark, borderColor: colors.red },
14755
+ completedIcon: { fontSize: 14 },
14756
+ completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
14757
+ reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
14758
+ reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
14640
14759
  // Action buttons
14641
14760
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
14642
14761
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -14665,12 +14784,13 @@ var styles2 = StyleSheet4.create({
14665
14784
 
14666
14785
  // src/widget/screens/TestListScreen.tsx
14667
14786
  import React5, { useState as useState3, useMemo as useMemo2, useCallback as useCallback3, useEffect as useEffect5 } from "react";
14668
- import { View as View4, Text as Text3, TouchableOpacity as TouchableOpacity3, StyleSheet as StyleSheet5, ScrollView, TextInput as TextInput2 } from "react-native";
14787
+ import { View as View4, Text as Text3, TouchableOpacity as TouchableOpacity3, StyleSheet as StyleSheet5, ScrollView, TextInput as TextInput2, Platform as Platform3, Linking as Linking2 } from "react-native";
14669
14788
  function TestListScreen({ nav }) {
14670
- const { assignments, currentAssignment, refreshAssignments, isLoading } = useBugBear();
14789
+ const { assignments, currentAssignment, refreshAssignments, dashboardUrl, isLoading } = useBugBear();
14671
14790
  const [filter, setFilter] = useState3("all");
14672
14791
  const [roleFilter, setRoleFilter] = useState3(null);
14673
14792
  const [trackFilter, setTrackFilter] = useState3(null);
14793
+ const [platformFilter, setPlatformFilter] = useState3(Platform3.OS === "android" ? "android" : "ios");
14674
14794
  const [searchQuery, setSearchQuery] = useState3("");
14675
14795
  const [sortMode, setSortMode] = useState3("priority");
14676
14796
  const [collapsedFolders, setCollapsedFolders] = useState3(/* @__PURE__ */ new Set());
@@ -14737,6 +14857,7 @@ function TestListScreen({ nav }) {
14737
14857
  });
14738
14858
  }, []);
14739
14859
  const filterAssignment = useCallback3((a) => {
14860
+ if (platformFilter && a.testCase.platforms && !a.testCase.platforms.includes(platformFilter)) return false;
14740
14861
  if (roleFilter && a.testCase.role?.id !== roleFilter) return false;
14741
14862
  if (trackFilter && a.testCase.track?.id !== trackFilter) return false;
14742
14863
  if (searchQuery) {
@@ -14749,7 +14870,7 @@ function TestListScreen({ nav }) {
14749
14870
  if (filter === "done") return a.status === "passed";
14750
14871
  if (filter === "reopened") return a.status === "failed";
14751
14872
  return true;
14752
- }, [roleFilter, trackFilter, searchQuery, filter]);
14873
+ }, [platformFilter, roleFilter, trackFilter, searchQuery, filter]);
14753
14874
  if (isLoading) return /* @__PURE__ */ React5.createElement(TestListScreenSkeleton, null);
14754
14875
  return /* @__PURE__ */ React5.createElement(View4, null, /* @__PURE__ */ React5.createElement(View4, { style: styles3.filterBar }, [
14755
14876
  { key: "all", label: "All", count: assignments.length },
@@ -14787,7 +14908,32 @@ function TestListScreen({ nav }) {
14787
14908
  placeholderTextColor: colors.textMuted,
14788
14909
  style: styles3.searchInput
14789
14910
  }
14790
- )), /* @__PURE__ */ React5.createElement(View4, { style: styles3.trackSortRow }, availableTracks.length >= 2 && /* @__PURE__ */ React5.createElement(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: { flex: 1 } }, /* @__PURE__ */ React5.createElement(
14911
+ )), /* @__PURE__ */ React5.createElement(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles3.platformBar }, /* @__PURE__ */ React5.createElement(
14912
+ TouchableOpacity3,
14913
+ {
14914
+ style: [styles3.platformBtn, !platformFilter && styles3.platformBtnActive],
14915
+ onPress: () => setPlatformFilter(null)
14916
+ },
14917
+ /* @__PURE__ */ React5.createElement(Text3, { style: [styles3.platformBtnText, !platformFilter && styles3.platformBtnTextActive] }, "All")
14918
+ ), [
14919
+ { key: "web", label: "Web", icon: "\u{1F310}" },
14920
+ { key: "ios", label: "iOS", icon: "\u{1F4F1}" },
14921
+ { key: "android", label: "Android", icon: "\u{1F916}" }
14922
+ ].map((p) => {
14923
+ const isActive = platformFilter === p.key;
14924
+ return /* @__PURE__ */ React5.createElement(
14925
+ TouchableOpacity3,
14926
+ {
14927
+ key: p.key,
14928
+ style: [
14929
+ styles3.platformBtn,
14930
+ isActive && { backgroundColor: colors.blue + "20", borderColor: colors.blue + "60", borderWidth: 1 }
14931
+ ],
14932
+ onPress: () => setPlatformFilter(isActive ? null : p.key)
14933
+ },
14934
+ /* @__PURE__ */ React5.createElement(Text3, { style: [styles3.platformBtnText, isActive && { color: colors.blue, fontWeight: "600" }] }, p.icon, " ", p.label)
14935
+ );
14936
+ })), /* @__PURE__ */ React5.createElement(View4, { style: styles3.trackSortRow }, availableTracks.length >= 2 && /* @__PURE__ */ React5.createElement(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: { flex: 1 } }, /* @__PURE__ */ React5.createElement(
14791
14937
  TouchableOpacity3,
14792
14938
  {
14793
14939
  style: [styles3.trackBtn, !trackFilter && styles3.trackBtnActive],
@@ -14848,7 +14994,15 @@ function TestListScreen({ nav }) {
14848
14994
  ] }, badge.label))
14849
14995
  );
14850
14996
  }));
14851
- }), /* @__PURE__ */ React5.createElement(TouchableOpacity3, { style: styles3.refreshBtn, onPress: refreshAssignments }, /* @__PURE__ */ React5.createElement(Text3, { style: styles3.refreshText }, "\u21BB", " Refresh")));
14997
+ }), dashboardUrl && /* @__PURE__ */ React5.createElement(
14998
+ TouchableOpacity3,
14999
+ {
15000
+ style: styles3.dashboardLink,
15001
+ onPress: () => Linking2.openURL(`${dashboardUrl}/test-cases`),
15002
+ activeOpacity: 0.7
15003
+ },
15004
+ /* @__PURE__ */ React5.createElement(Text3, { style: styles3.dashboardLinkText }, "\u{1F310}", " Manage on Dashboard ", "\u2192")
15005
+ ), /* @__PURE__ */ React5.createElement(TouchableOpacity3, { style: styles3.refreshBtn, onPress: refreshAssignments }, /* @__PURE__ */ React5.createElement(Text3, { style: styles3.refreshText }, "\u21BB", " Refresh")));
14852
15006
  }
14853
15007
  var styles3 = StyleSheet5.create({
14854
15008
  filterBar: { flexDirection: "row", gap: 8, marginBottom: 8 },
@@ -14887,6 +15041,11 @@ var styles3 = StyleSheet5.create({
14887
15041
  statusPillText: { fontSize: 10, fontWeight: "600" },
14888
15042
  searchContainer: { marginBottom: 8 },
14889
15043
  searchInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
15044
+ platformBar: { flexDirection: "row", marginBottom: 8 },
15045
+ platformBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 6, marginRight: 6, borderWidth: 1, borderColor: "transparent" },
15046
+ platformBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
15047
+ platformBtnText: { fontSize: 11, color: colors.textMuted },
15048
+ platformBtnTextActive: { color: colors.textPrimary, fontWeight: "600" },
14890
15049
  trackSortRow: { flexDirection: "row", alignItems: "center", marginBottom: 10, gap: 8 },
14891
15050
  trackBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 6, marginRight: 6, borderWidth: 1, borderColor: "transparent" },
14892
15051
  trackBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
@@ -14897,7 +15056,9 @@ var styles3 = StyleSheet5.create({
14897
15056
  sortBtnActive: { backgroundColor: colors.card, borderColor: colors.border },
14898
15057
  sortBtnText: { fontSize: 11, color: colors.textMuted },
14899
15058
  sortBtnTextActive: { color: colors.textPrimary, fontWeight: "600" },
14900
- refreshBtn: { alignItems: "center", paddingVertical: 12 },
15059
+ dashboardLink: { alignItems: "center", paddingTop: 12 },
15060
+ dashboardLinkText: { fontSize: 13, fontWeight: "500", color: colors.blue },
15061
+ refreshBtn: { alignItems: "center", paddingVertical: 8 },
14901
15062
  refreshText: { fontSize: 13, color: colors.blue }
14902
15063
  });
14903
15064
 
@@ -15066,7 +15227,9 @@ var styles4 = StyleSheet6.create({
15066
15227
 
15067
15228
  // src/widget/ImagePickerButtons.tsx
15068
15229
  function ImagePickerButtons({ images, maxImages, onPickGallery, onPickCamera, onRemove, label }) {
15069
- if (!IMAGE_PICKER_AVAILABLE) return null;
15230
+ if (!IMAGE_PICKER_AVAILABLE) {
15231
+ return /* @__PURE__ */ React7.createElement(View6, { style: styles5.section }, label && /* @__PURE__ */ React7.createElement(Text5, { style: styles5.label }, label), /* @__PURE__ */ React7.createElement(View6, { style: styles5.unavailableRow }, /* @__PURE__ */ React7.createElement(Text5, { style: styles5.unavailableText }, "Install react-native-image-picker to enable photo attachments")));
15232
+ }
15070
15233
  return /* @__PURE__ */ React7.createElement(View6, { style: styles5.section }, label && /* @__PURE__ */ React7.createElement(Text5, { style: styles5.label }, label), /* @__PURE__ */ React7.createElement(View6, { style: styles5.buttonRow }, /* @__PURE__ */ React7.createElement(
15071
15234
  TouchableOpacity5,
15072
15235
  {
@@ -15121,6 +15284,18 @@ var styles5 = StyleSheet7.create({
15121
15284
  fontSize: 12,
15122
15285
  color: colors.textDim,
15123
15286
  marginLeft: 4
15287
+ },
15288
+ unavailableRow: {
15289
+ backgroundColor: colors.card,
15290
+ borderWidth: 1,
15291
+ borderColor: colors.border,
15292
+ borderRadius: 8,
15293
+ padding: 12
15294
+ },
15295
+ unavailableText: {
15296
+ fontSize: 12,
15297
+ color: colors.textDim,
15298
+ fontStyle: "italic"
15124
15299
  }
15125
15300
  });
15126
15301
 
@@ -15619,9 +15794,9 @@ var styles9 = StyleSheet11.create({
15619
15794
 
15620
15795
  // src/widget/screens/MessageListScreen.tsx
15621
15796
  import React12 from "react";
15622
- import { View as View11, Text as Text10, TouchableOpacity as TouchableOpacity9, StyleSheet as StyleSheet12 } from "react-native";
15797
+ import { View as View11, Text as Text10, TouchableOpacity as TouchableOpacity9, StyleSheet as StyleSheet12, Linking as Linking3 } from "react-native";
15623
15798
  function MessageListScreen({ nav }) {
15624
- const { threads, unreadCount, refreshThreads, isLoading } = useBugBear();
15799
+ const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading } = useBugBear();
15625
15800
  if (isLoading) return /* @__PURE__ */ React12.createElement(MessageListScreenSkeleton, null);
15626
15801
  return /* @__PURE__ */ React12.createElement(View11, null, /* @__PURE__ */ React12.createElement(
15627
15802
  TouchableOpacity9,
@@ -15639,7 +15814,15 @@ function MessageListScreen({ nav }) {
15639
15814
  },
15640
15815
  /* @__PURE__ */ React12.createElement(View11, { style: styles10.threadLeft }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ React12.createElement(View11, { style: styles10.threadInfo }, /* @__PURE__ */ React12.createElement(View11, { style: styles10.threadTitleRow }, thread.isPinned && /* @__PURE__ */ React12.createElement(Text10, { style: styles10.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ React12.createElement(Text10, { style: styles10.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.lastMessage && /* @__PURE__ */ React12.createElement(Text10, { style: styles10.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
15641
15816
  /* @__PURE__ */ React12.createElement(View11, { style: styles10.threadRight }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.threadTime }, formatRelativeTime(thread.lastMessageAt)), thread.unreadCount > 0 && /* @__PURE__ */ React12.createElement(View11, { style: styles10.unreadBadge }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.unreadText }, thread.unreadCount)), thread.priority !== "normal" && /* @__PURE__ */ React12.createElement(View11, { style: [styles10.priorityDot, { backgroundColor: getPriorityColor(thread.priority) }] }))
15642
- ))), /* @__PURE__ */ React12.createElement(View11, { style: styles10.footer }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ React12.createElement(TouchableOpacity9, { onPress: refreshThreads }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.refreshText }, "\u21BB Refresh"))));
15817
+ ))), dashboardUrl && /* @__PURE__ */ React12.createElement(
15818
+ TouchableOpacity9,
15819
+ {
15820
+ style: styles10.dashboardLink,
15821
+ onPress: () => Linking3.openURL(`${dashboardUrl}/discussions`),
15822
+ activeOpacity: 0.7
15823
+ },
15824
+ /* @__PURE__ */ React12.createElement(Text10, { style: styles10.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
15825
+ ), /* @__PURE__ */ React12.createElement(View11, { style: styles10.footer }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ React12.createElement(TouchableOpacity9, { onPress: refreshThreads }, /* @__PURE__ */ React12.createElement(Text10, { style: styles10.refreshText }, "\u21BB Refresh"))));
15643
15826
  }
15644
15827
  var styles10 = StyleSheet12.create({
15645
15828
  newMsgButton: { backgroundColor: colors.blue, paddingVertical: 12, borderRadius: 12, alignItems: "center", marginBottom: 16 },
@@ -15658,7 +15841,9 @@ var styles10 = StyleSheet12.create({
15658
15841
  unreadBadge: { backgroundColor: colors.blue, borderRadius: 10, minWidth: 20, height: 20, justifyContent: "center", alignItems: "center", paddingHorizontal: 6 },
15659
15842
  unreadText: { fontSize: 11, fontWeight: "bold", color: "#fff" },
15660
15843
  priorityDot: { width: 8, height: 8, borderRadius: 4 },
15661
- footer: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingTop: 12, paddingHorizontal: 4 },
15844
+ dashboardLink: { alignItems: "center", paddingTop: 12 },
15845
+ dashboardLinkText: { fontSize: 13, fontWeight: "500", color: colors.blue },
15846
+ footer: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingTop: 8, paddingHorizontal: 4 },
15662
15847
  footerText: { fontSize: 12, color: colors.textMuted },
15663
15848
  refreshText: { fontSize: 13, color: colors.blue }
15664
15849
  });
@@ -16139,7 +16324,7 @@ var styles14 = StyleSheet16.create({
16139
16324
 
16140
16325
  // src/widget/screens/IssueDetailScreen.tsx
16141
16326
  import React17 from "react";
16142
- import { View as View16, Text as Text15, Image as Image3, StyleSheet as StyleSheet17, Linking as Linking2, TouchableOpacity as TouchableOpacity14 } from "react-native";
16327
+ import { View as View16, Text as Text15, Image as Image3, StyleSheet as StyleSheet17, Linking as Linking4, TouchableOpacity as TouchableOpacity14 } from "react-native";
16143
16328
  var STATUS_LABELS = {
16144
16329
  new: { label: "New", bg: "#1e3a5f", color: "#60a5fa" },
16145
16330
  triaging: { label: "Triaging", bg: "#1e3a5f", color: "#60a5fa" },
@@ -16161,9 +16346,18 @@ var SEVERITY_CONFIG = {
16161
16346
  low: { label: "Low", color: "#71717a", bg: "#27272a" }
16162
16347
  };
16163
16348
  function IssueDetailScreen({ nav, issue }) {
16349
+ const { dashboardUrl } = useBugBear();
16164
16350
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: "#27272a", color: "#a1a1aa" };
16165
16351
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
16166
- return /* @__PURE__ */ React17.createElement(View16, null, /* @__PURE__ */ React17.createElement(View16, { style: styles15.badgeRow }, /* @__PURE__ */ React17.createElement(View16, { style: [styles15.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles15.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ React17.createElement(View16, { style: [styles15.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles15.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.title }, issue.title), issue.route && /* @__PURE__ */ React17.createElement(Text15, { style: styles15.route }, issue.route), issue.description && /* @__PURE__ */ React17.createElement(View16, { style: styles15.descriptionCard }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ React17.createElement(View16, { style: styles15.verifiedCard }, /* @__PURE__ */ React17.createElement(View16, { style: styles15.verifiedHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedIcon }, "\u2705"), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ React17.createElement(View16, { style: styles15.originalBugCard }, /* @__PURE__ */ React17.createElement(View16, { style: styles15.originalBugHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugTitle }, "Original Bug")), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ React17.createElement(View16, { style: styles15.screenshotSection }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ React17.createElement(View16, { style: styles15.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ React17.createElement(TouchableOpacity14, { key: i, onPress: () => Linking2.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ React17.createElement(Image3, { source: { uri: url }, style: styles15.screenshotThumb }))))), /* @__PURE__ */ React17.createElement(View16, { style: styles15.metaSection }, issue.reporterName && /* @__PURE__ */ React17.createElement(Text15, { style: styles15.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))));
16352
+ return /* @__PURE__ */ React17.createElement(View16, null, /* @__PURE__ */ React17.createElement(View16, { style: styles15.badgeRow }, /* @__PURE__ */ React17.createElement(View16, { style: [styles15.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles15.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ React17.createElement(View16, { style: [styles15.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles15.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.title }, issue.title), issue.route && /* @__PURE__ */ React17.createElement(Text15, { style: styles15.route }, issue.route), issue.description && /* @__PURE__ */ React17.createElement(View16, { style: styles15.descriptionCard }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ React17.createElement(View16, { style: styles15.verifiedCard }, /* @__PURE__ */ React17.createElement(View16, { style: styles15.verifiedHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedIcon }, "\u2705"), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ React17.createElement(View16, { style: styles15.originalBugCard }, /* @__PURE__ */ React17.createElement(View16, { style: styles15.originalBugHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugTitle }, "Original Bug")), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ React17.createElement(View16, { style: styles15.screenshotSection }, /* @__PURE__ */ React17.createElement(Text15, { style: styles15.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ React17.createElement(View16, { style: styles15.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ React17.createElement(TouchableOpacity14, { key: i, onPress: () => Linking4.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ React17.createElement(Image3, { source: { uri: url }, style: styles15.screenshotThumb }))))), /* @__PURE__ */ React17.createElement(View16, { style: styles15.metaSection }, issue.reporterName && /* @__PURE__ */ React17.createElement(Text15, { style: styles15.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ React17.createElement(Text15, { style: styles15.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ React17.createElement(
16353
+ TouchableOpacity14,
16354
+ {
16355
+ style: styles15.dashboardLink,
16356
+ onPress: () => Linking4.openURL(`${dashboardUrl}/reports`),
16357
+ activeOpacity: 0.7
16358
+ },
16359
+ /* @__PURE__ */ React17.createElement(Text15, { style: styles15.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
16360
+ ));
16167
16361
  }
16168
16362
  var styles15 = StyleSheet17.create({
16169
16363
  badgeRow: {
@@ -16292,6 +16486,18 @@ var styles15 = StyleSheet17.create({
16292
16486
  metaTextSmall: {
16293
16487
  fontSize: 11,
16294
16488
  color: colors.textDim
16489
+ },
16490
+ dashboardLink: {
16491
+ alignItems: "center",
16492
+ paddingVertical: 10,
16493
+ marginTop: 12,
16494
+ borderTopWidth: 1,
16495
+ borderTopColor: colors.border
16496
+ },
16497
+ dashboardLinkText: {
16498
+ fontSize: 13,
16499
+ fontWeight: "500",
16500
+ color: colors.blue
16295
16501
  }
16296
16502
  });
16297
16503
 
@@ -16479,7 +16685,7 @@ function BugBearButton({
16479
16685
  /* @__PURE__ */ React18.createElement(
16480
16686
  KeyboardAvoidingView,
16481
16687
  {
16482
- behavior: Platform4.OS === "ios" ? "padding" : "height",
16688
+ behavior: Platform5.OS === "ios" ? "padding" : "height",
16483
16689
  style: styles16.modalOverlay
16484
16690
  },
16485
16691
  /* @__PURE__ */ React18.createElement(View17, { style: styles16.modalContainer }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.header }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerLeft }, canGoBack ? /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerNavRow }, /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.pop(), style: styles16.backButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.backText }, "\u2190 Back")), /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.reset(), style: styles16.homeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.homeText }, "\u{1F3E0}"))) : /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerTitleRow }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: handleClose, style: styles16.closeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.closeText }, "\u2715"))), /* @__PURE__ */ React18.createElement(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "BugBear React Native components for mobile apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",