@sentry/junior-dashboard 0.231.0 → 0.232.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.231.0",
3
+ "version": "0.232.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,8 +34,8 @@
34
34
  "react-router": "^7.18.2",
35
35
  "shiki": "4.1.0",
36
36
  "zod": "^4.5.4",
37
- "@sentry/junior": "0.231.0",
38
- "@sentry/junior-plugin-api": "0.231.0"
37
+ "@sentry/junior": "0.232.0",
38
+ "@sentry/junior-plugin-api": "0.232.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@oxlint/plugins": "1.79.0",
@@ -6,6 +6,7 @@ import { ButtonLink } from "../components/Button";
6
6
  import { Drawer } from "../components/Drawer";
7
7
  import { SegmentedTabs } from "../components/SegmentedTabs";
8
8
  import { ConversationMemories } from "./ConversationMemories";
9
+ import { ShareConversationButton } from "./ShareConversationButton";
9
10
 
10
11
  const tabs = [
11
12
  { label: "Details", value: "details" },
@@ -85,9 +86,8 @@ export function ConversationDetailsDrawer(props: {
85
86
  No additional conversation details.
86
87
  </p>
87
88
  )}
88
- {tab === "details" &&
89
- (props.lastActivityAt || props.sentryConversationUrl) ? (
90
- <footer className="mt-5 grid gap-3 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
89
+ {tab === "details" ? (
90
+ <footer className="mt-5 grid gap-6 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
91
91
  {props.lastActivityAt ? (
92
92
  <div className="flex flex-wrap items-baseline justify-between gap-2">
93
93
  <span>Last activity</span>
@@ -102,20 +102,26 @@ export function ConversationDetailsDrawer(props: {
102
102
  </time>
103
103
  </div>
104
104
  ) : null}
105
- {props.sentryConversationUrl ? (
106
- <ButtonLink
107
- className="w-fit focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
108
- to={props.sentryConversationUrl}
109
- rel="noopener noreferrer"
110
- target="_blank"
111
- >
112
- Open in Sentry
113
- <ExternalLink
114
- aria-hidden="true"
115
- className="size-3.5 shrink-0"
116
- />
117
- </ButtonLink>
118
- ) : null}
105
+ <div className="flex flex-wrap items-center gap-2">
106
+ <ShareConversationButton
107
+ key={props.conversationId}
108
+ conversationId={props.conversationId}
109
+ />
110
+ {props.sentryConversationUrl ? (
111
+ <ButtonLink
112
+ className="w-fit focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
113
+ to={props.sentryConversationUrl}
114
+ rel="noopener noreferrer"
115
+ target="_blank"
116
+ >
117
+ Open in Sentry
118
+ <ExternalLink
119
+ aria-hidden="true"
120
+ className="size-3.5 shrink-0"
121
+ />
122
+ </ButtonLink>
123
+ ) : null}
124
+ </div>
119
125
  </footer>
120
126
  ) : null}
121
127
  </SegmentedTabs>
@@ -111,6 +111,7 @@ export function ConversationHeader(props: {
111
111
  <div className="absolute right-0 top-[calc(100%+0.35rem)] z-40 w-56 rounded-xl border border-white/[0.08] bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl">
112
112
  <ConversationHeaderActions
113
113
  archive={props.archive}
114
+ conversationId={props.conversationId}
114
115
  copyAction={menuCopyAction}
115
116
  detailsOpen={detailsOpen}
116
117
  layout="menu"
@@ -185,6 +186,7 @@ export function ConversationHeader(props: {
185
186
  </div>
186
187
  <ConversationHeaderActions
187
188
  archive={props.archive}
189
+ conversationId={props.conversationId}
188
190
  copyAction={props.copyAction}
189
191
  detailsOpen={detailsOpen}
190
192
  onDetailsClick={() => setDetailsOpen(true)}
@@ -14,6 +14,7 @@ import { Button, ToggleButton } from "../components/Button";
14
14
  import { IconButtonTooltip } from "../components/Tooltip";
15
15
  import { cn } from "../styles";
16
16
  import type { TranscriptViewMode } from "./transcriptRenderModel";
17
+ import { ShareConversationButton } from "./ShareConversationButton";
17
18
 
18
19
  export type ConversationArchiveAction = {
19
20
  archived: boolean;
@@ -26,6 +27,7 @@ export type ConversationArchiveAction = {
26
27
  /** Show conversation actions in the header or mobile menu. */
27
28
  export function ConversationHeaderActions(props: {
28
29
  archive: ConversationArchiveAction;
30
+ conversationId: string;
29
31
  copyAction?: ReactNode;
30
32
  detailsOpen: boolean;
31
33
  /** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
@@ -65,6 +67,11 @@ export function ConversationHeaderActions(props: {
65
67
  >
66
68
  <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
67
69
  </MenuActionButton>
70
+ <ShareConversationButton
71
+ key={props.conversationId}
72
+ conversationId={props.conversationId}
73
+ layout="menu"
74
+ />
68
75
  {props.copyAction}
69
76
  <ArchiveConversationButton {...props.archive} layout="menu" />
70
77
  <MenuActionButton
@@ -0,0 +1,101 @@
1
+ import { useState } from "react";
2
+ import { useHref } from "react-router";
3
+ import { Check, Share2 } from "lucide-react";
4
+
5
+ import { Button } from "../components/Button";
6
+ import { conversationPath } from "./conversationRoutes";
7
+
8
+ /** Share a conversation link without changing who can access it. */
9
+ export function ShareConversationButton(props: {
10
+ conversationId: string;
11
+ layout?: "button" | "menu";
12
+ }) {
13
+ const href = useHref(conversationPath(props.conversationId));
14
+ const [status, setStatus] = useState<
15
+ "idle" | "sharing" | "copied" | "copy-failed" | "share-failed"
16
+ >("idle");
17
+ const label =
18
+ status === "copied"
19
+ ? "Link copied"
20
+ : status === "copy-failed"
21
+ ? "Copy failed — retry"
22
+ : status === "share-failed"
23
+ ? "Share failed — retry"
24
+ : "Share";
25
+ const Icon = status === "copied" ? Check : Share2;
26
+ const feedback =
27
+ status === "copied"
28
+ ? "Conversation link copied. Access has not changed."
29
+ : status === "copy-failed"
30
+ ? "Could not copy the link. Try again or copy it from the address bar."
31
+ : status === "share-failed"
32
+ ? "Could not share the link. Try again or copy it from the address bar."
33
+ : "";
34
+
35
+ async function shareLink() {
36
+ setStatus("sharing");
37
+ const url = new URL(href, window.location.origin).href;
38
+ // Touch-first devices use the native sheet, including tablets. Desktop
39
+ // browsers keep copy-link behavior even when they support native sharing.
40
+ if (
41
+ window.matchMedia("(pointer: coarse)").matches &&
42
+ typeof navigator.share === "function"
43
+ ) {
44
+ try {
45
+ await navigator.share({ url });
46
+ setStatus("idle");
47
+ } catch (error) {
48
+ // Dismissing the sheet must not copy a link or show an error.
49
+ setStatus(
50
+ error instanceof DOMException && error.name === "AbortError"
51
+ ? "idle"
52
+ : "share-failed",
53
+ );
54
+ }
55
+ return;
56
+ }
57
+
58
+ try {
59
+ await navigator.clipboard.writeText(url);
60
+ setStatus("copied");
61
+ } catch {
62
+ setStatus("copy-failed");
63
+ }
64
+ }
65
+
66
+ const content = (
67
+ <>
68
+ <Icon aria-hidden="true" size={16} strokeWidth={2} />
69
+ <span>{label}</span>
70
+ </>
71
+ );
72
+ const button =
73
+ props.layout === "menu" ? (
74
+ <button
75
+ aria-label={label}
76
+ className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:bg-dashboard-fill-hover focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
77
+ disabled={status === "sharing"}
78
+ onClick={() => void shareLink()}
79
+ type="button"
80
+ >
81
+ {content}
82
+ </button>
83
+ ) : (
84
+ <Button
85
+ aria-label={label}
86
+ disabled={status === "sharing"}
87
+ onClick={() => void shareLink()}
88
+ >
89
+ {content}
90
+ </Button>
91
+ );
92
+
93
+ return (
94
+ <>
95
+ {button}
96
+ <span className="sr-only" role="status">
97
+ {feedback}
98
+ </span>
99
+ </>
100
+ );
101
+ }