@sentry/junior-dashboard 0.273.0 → 0.274.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.
@@ -12,6 +12,12 @@ type StoredConversationStates = Record<string, StoredConversationState>;
12
12
 
13
13
  type ConversationFinishedIndicators = {
14
14
  finishedConversationIds: ReadonlySet<string>;
15
+ /**
16
+ * The selected conversation when it was unread at the moment it opened.
17
+ * Lists keep it in its unread place until another conversation opens, so
18
+ * the row does not move under the pointer.
19
+ */
20
+ heldConversationId?: string;
15
21
  markRead(conversationId: string, lastReadAt: string): void;
16
22
  };
17
23
 
@@ -23,6 +29,23 @@ export function useConversationFinishedIndicators(
23
29
  pruneMissing: boolean,
24
30
  ): ConversationFinishedIndicators {
25
31
  const [storedStates, setStoredStates] = useState(readStoredStates);
32
+ const [held, setHeld] = useState<{ heldId?: string; selectedId?: string }>(
33
+ {},
34
+ );
35
+ // Read the unread state when the selection changes, before the effect
36
+ // below or the opened page marks it read. Hold only a row that the list
37
+ // already showed. A deep link has no row under the pointer, and the
38
+ // selected row never leads its section, so it does not move.
39
+ if (held.selectedId !== selectedId) {
40
+ const selected = conversations.find(({ id }) => id === selectedId);
41
+ setHeld({
42
+ heldId:
43
+ selected && finishedSinceRead(storedStates, selected)
44
+ ? selected.id
45
+ : undefined,
46
+ selectedId,
47
+ });
48
+ }
26
49
 
27
50
  useEffect(() => {
28
51
  if (!ready) return;
@@ -55,6 +78,7 @@ export function useConversationFinishedIndicators(
55
78
  () => finishedConversationIds(storedStates, conversations, selectedId),
56
79
  [conversations, selectedId, storedStates],
57
80
  ),
81
+ heldConversationId: held.heldId,
58
82
  markRead,
59
83
  };
60
84
  }
@@ -67,19 +91,27 @@ export function finishedConversationIds(
67
91
  ): ReadonlySet<string> {
68
92
  return new Set(
69
93
  conversations
70
- .filter((conversation) => {
71
- const state = states[conversation.id];
72
- return (
94
+ .filter(
95
+ (conversation) =>
73
96
  conversation.id !== selectedId &&
74
- conversation.status === "completed" &&
75
- state !== undefined &&
76
- isAfter(conversation.lastSeenAt, state.lastReadAt)
77
- );
78
- })
97
+ finishedSinceRead(states, conversation),
98
+ )
79
99
  .map((conversation) => conversation.id),
80
100
  );
81
101
  }
82
102
 
103
+ function finishedSinceRead(
104
+ states: StoredConversationStates,
105
+ conversation: Conversation,
106
+ ): boolean {
107
+ const state = states[conversation.id];
108
+ return (
109
+ conversation.status === "completed" &&
110
+ state !== undefined &&
111
+ isAfter(conversation.lastSeenAt, state.lastReadAt)
112
+ );
113
+ }
114
+
83
115
  /** Record reads and remove conversations outside the visible list. */
84
116
  export function reconcileStoredStates(
85
117
  current: StoredConversationStates,
@@ -12,12 +12,14 @@ import {
12
12
  import { Button } from "../../components/ui/button";
13
13
  import { Input } from "../../components/ui/input";
14
14
  import { Textarea } from "../../components/ui/textarea";
15
+ import { Checkbox } from "../../components/ui/checkbox";
15
16
  import { Field, FieldDescription, FieldLabel } from "../../components/ui/field";
16
17
  import { RadioGroup, RadioGroupItem } from "../../components/ui/radio-group";
17
18
  import {
18
19
  useConversationStartPosition,
19
20
  type ConversationStartPosition,
20
21
  } from "../../conversations/transcriptReadPosition";
22
+ import { useComposerAutocorrect } from "../../conversations/composerAutocorrect";
21
23
  import { InlineError } from "../../components/InlineError";
22
24
  import { Card } from "../../components/layout/Card";
23
25
  import { DashboardApiError, fetchDashboardJson, patch, put } from "../../http";
@@ -105,6 +107,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
105
107
  </Card>
106
108
  </form>
107
109
  <ConversationStartSetting />
110
+ <ComposerAutocorrectSetting />
108
111
  <CustomizationForm />
109
112
  </SettingsPageLayout>
110
113
  );
@@ -166,6 +169,31 @@ function ConversationStartSetting() {
166
169
  );
167
170
  }
168
171
 
172
+ /** Let the user turn on keyboard autocorrect in the composer on this browser. */
173
+ function ComposerAutocorrectSetting() {
174
+ const [autocorrect, setAutocorrect] = useComposerAutocorrect();
175
+ return (
176
+ <Card className="mt-6 mb-0" padding="md" variant="raised">
177
+ <h2 className="m-0 text-lg font-bold">Message box</h2>
178
+ <label className="mt-4 flex cursor-pointer items-start gap-3">
179
+ <Checkbox
180
+ checked={autocorrect}
181
+ className="mt-0.5"
182
+ onCheckedChange={setAutocorrect}
183
+ />
184
+ <span className="grid gap-0.5">
185
+ <span className="text-sm font-medium">Autocorrect</span>
186
+ <span className="text-xs text-dashboard-text-muted">
187
+ Let your keyboard correct spelling and capitalize sentences as you
188
+ type. This can change ids, paths, and code. This choice applies to
189
+ this browser.
190
+ </span>
191
+ </span>
192
+ </label>
193
+ </Card>
194
+ );
195
+ }
196
+
169
197
  /**
170
198
  * Let the user save text that changes how the agent replies to them. The
171
199
  * server returns 404 when the app does not enable this experiment.
@@ -1,10 +1,14 @@
1
1
  import { ChevronRight } from "lucide-react";
2
- import { useState } from "react";
2
+ import { useSearchParams } from "react-router";
3
3
 
4
4
  import { Drawer } from "../components/Drawer";
5
5
  import { LoadMorePagination } from "../components/Pagination";
6
6
  import { ReviewItemCard } from "./ReviewItemCard";
7
7
  import { useReviewItemsData } from "./queries";
8
+ import {
9
+ CONVERSATION_REVIEW_OPEN,
10
+ CONVERSATION_REVIEW_PARAM,
11
+ } from "../conversations/conversationRoutes";
8
12
 
9
13
  /** `more` marks a count of the loaded pages only, such as "50+". */
10
14
  function itemCountLabel(count: number, more: boolean): string {
@@ -15,9 +19,23 @@ function itemCountLabel(count: number, more: boolean): string {
15
19
  /**
16
20
  * Show how many open Review items are linked to a Conversation, above its
17
21
  * message box. The bar opens a side panel where the viewer can act on each.
22
+ * The URL owns the panel, so a sidebar link can open it.
18
23
  */
19
24
  export function ConversationReviewBar(props: { conversationId: string }) {
20
- const [open, setOpen] = useState(false);
25
+ const [searchParams, setSearchParams] = useSearchParams();
26
+ const open =
27
+ searchParams.get(CONVERSATION_REVIEW_PARAM) === CONVERSATION_REVIEW_OPEN;
28
+ const setOpen = (value: boolean) =>
29
+ setSearchParams(
30
+ (current) => {
31
+ const next = new URLSearchParams(current);
32
+ if (value)
33
+ next.set(CONVERSATION_REVIEW_PARAM, CONVERSATION_REVIEW_OPEN);
34
+ else next.delete(CONVERSATION_REVIEW_PARAM);
35
+ return next;
36
+ },
37
+ { replace: true },
38
+ );
21
39
  const { items, query } = useReviewItemsData({
22
40
  conversationId: props.conversationId,
23
41
  });
@@ -153,13 +153,20 @@ export function useReviewItemAction() {
153
153
  });
154
154
  }
155
155
 
156
- const NO_CONVERSATIONS: ReadonlySet<string> = new Set();
156
+ /** Newest open Review item title and item count for one waiting Conversation. */
157
+ export type WaitingConversation = { count: number; title: string };
158
+
159
+ const NO_WAITING: ReadonlyMap<string, WaitingConversation> = new Map();
157
160
 
158
161
  /**
159
- * Return the Conversations that wait on the viewer. Today only the review
160
- * desk sets this: a linked open action item assigned to the viewer.
162
+ * Return the Conversations that wait on the viewer, by Conversation id. Today
163
+ * only the review desk sets this: a linked open action item assigned to the
164
+ * viewer.
161
165
  */
162
- export function useWaitingConversationIds(): ReadonlySet<string> {
166
+ export function useWaitingConversations(): ReadonlyMap<
167
+ string,
168
+ WaitingConversation
169
+ > {
163
170
  const query = useQuery({
164
171
  queryKey: [...REVIEW_ITEMS_QUERY_KEY, "waiting-conversations"],
165
172
  queryFn: ({ signal }) =>
@@ -171,6 +178,23 @@ export function useWaitingConversationIds(): ReadonlySet<string> {
171
178
  refetchInterval: COUNT_REFRESH_MS,
172
179
  retry: false,
173
180
  });
174
- const ids = query.data?.conversationIds;
175
- return useMemo(() => (ids ? new Set(ids) : NO_CONVERSATIONS), [ids]);
181
+ const conversations = query.data?.conversations;
182
+ return useMemo(
183
+ () =>
184
+ conversations
185
+ ? new Map(
186
+ conversations.map(({ conversationId, count, title }) => [
187
+ conversationId,
188
+ { count, title },
189
+ ]),
190
+ )
191
+ : NO_WAITING,
192
+ [conversations],
193
+ );
194
+ }
195
+
196
+ /** Return the ids of the Conversations that wait on the viewer. */
197
+ export function useWaitingConversationIds(): ReadonlySet<string> {
198
+ const waiting = useWaitingConversations();
199
+ return useMemo(() => new Set(waiting.keys()), [waiting]);
176
200
  }
@@ -331,7 +331,7 @@ export function createMockReportingApi(): Hono<{
331
331
  jsonResponse(reviewItemCountSchema, { needsViewer: 0 }),
332
332
  );
333
333
  app.get("/review-items/waiting-conversations", () =>
334
- jsonResponse(reviewItemWaitingConversationsSchema, { conversationIds: [] }),
334
+ jsonResponse(reviewItemWaitingConversationsSchema, { conversations: [] }),
335
335
  );
336
336
  app.get("/review-items/:id", () =>
337
337
  errorResponse("Review item not found.", 404),