@sentry/junior-dashboard 0.269.0 → 0.271.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/dist/app.js +2 -2
- package/dist/client.js +78 -75
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/client/App.tsx +103 -65
- package/src/client/components/AGENTS.md +30 -59
- package/src/client/components/AutomationCard.tsx +2 -1
- package/src/client/components/ProfileMenu.tsx +0 -24
- package/src/client/components/UserInputForm.tsx +143 -39
- package/src/client/components/layout/PageHeader.tsx +25 -10
- package/src/client/conversations/AGENTS.md +12 -37
- package/src/client/conversations/ConversationBrief.tsx +55 -36
- package/src/client/conversations/ConversationComposer.tsx +27 -8
- package/src/client/conversations/ConversationEventLog.tsx +1 -0
- package/src/client/conversations/ConversationHomeList.tsx +43 -26
- package/src/client/conversations/ConversationPage.tsx +137 -40
- package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
- package/src/client/conversations/ConversationSidebar.tsx +55 -25
- package/src/client/conversations/ConversationTranscript.tsx +3 -2
- package/src/client/conversations/ConversationWatches.tsx +4 -1
- package/src/client/conversations/ConversationWorkspace.tsx +43 -25
- package/src/client/conversations/EventDetails.tsx +6 -9
- package/src/client/conversations/PendingMailboxStack.tsx +5 -2
- package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +9 -3
- package/src/client/conversations/TranscriptMessageView.tsx +6 -5
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
- package/src/client/conversations/TranscriptRows.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
- package/src/client/conversations/TranscriptToolView.tsx +2 -1
- package/src/client/conversations/TranscriptView.tsx +6 -6
- package/src/client/conversations/conversationOutbox.ts +13 -3
- package/src/client/conversations/conversationRoutes.ts +30 -3
- package/src/client/conversations/conversationSections.ts +36 -8
- package/src/client/conversations/eventTranscript.ts +1 -0
- package/src/client/conversations/transcriptBottomPinning.ts +171 -81
- package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
- package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
- package/src/client/display-text/DisplayText.tsx +68 -0
- package/src/client/display-text/emoji.ts +54 -0
- package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
- package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
- package/src/client/display-text/mentions.tsx +148 -0
- package/src/client/display-text/plainText.ts +29 -0
- package/src/client/pages/automations/AGENTS.md +6 -25
- package/src/client/pages/automations/AutomationFilters.tsx +6 -6
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
- package/src/client/pages/code/RepositoryTabs.tsx +6 -1
- package/src/client/pages/memory/MemoryDetails.tsx +5 -13
- package/src/client/pages/settings/ConnectionsPage.tsx +198 -0
- package/src/client/pages/settings/McpSettingsPage.tsx +77 -0
- package/src/client/pages/settings/PersonalTokensPage.tsx +198 -0
- package/src/client/pages/{SettingsPage.tsx → settings/SettingsPage.tsx} +54 -62
- package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
- package/src/client/review-desk/ReviewDeskPage.tsx +202 -241
- package/src/client/review-desk/ReviewDeskSidebar.tsx +253 -0
- package/src/client/review-desk/ReviewItemActions.tsx +6 -2
- package/src/client/review-desk/ReviewItemCard.tsx +58 -17
- package/src/client/review-desk/ReviewItemContext.tsx +108 -0
- package/src/client/review-desk/ReviewItemDetails.tsx +153 -0
- package/src/client/review-desk/ReviewItemView.tsx +159 -0
- package/src/client/review-desk/UnsentAnswers.tsx +148 -0
- package/src/client/review-desk/queries.ts +4 -2
- package/src/client/review-desk/reviewItemFormat.ts +83 -1
- package/src/client/components/charts/AGENTS.md +0 -15
- package/src/client/conversations/TranscriptText.tsx +0 -47
- package/src/client/conversations/conversationActivityPreview.ts +0 -16
- package/src/client/conversations/transcriptMentions.tsx +0 -134
- package/src/client/pages/ConnectionsPage.tsx +0 -207
- package/src/client/pages/PersonalTokensPage.tsx +0 -205
- package/src/client/pages/system/AGENTS.md +0 -12
- package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
|
@@ -1,297 +1,258 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
AutomationSummary,
|
|
6
|
-
ReviewItemPayload,
|
|
7
|
-
} from "@sentry/junior/api/schema";
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
8
4
|
|
|
9
5
|
import { InlineError } from "../components/InlineError";
|
|
10
6
|
import { LoadMorePagination } from "../components/Pagination";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { Card } from "../components/layout/Card";
|
|
14
|
-
import { PageHeader } from "../components/layout/PageHeader";
|
|
15
|
-
import { PageLayout } from "../components/layout/PageLayout";
|
|
7
|
+
import { useDetailsPanel } from "../components/DetailsPanel";
|
|
8
|
+
import { Button } from "../components/ui/button";
|
|
16
9
|
import { Skeleton } from "../components/ui/skeleton";
|
|
17
|
-
import {
|
|
10
|
+
import { useDocumentTitle } from "../documentTitle";
|
|
18
11
|
import { pathWithSearch, useDebouncedSearchParam } from "../searchParams";
|
|
19
12
|
import { cn } from "../styles";
|
|
20
|
-
import {
|
|
13
|
+
import { ReviewDeskSidebar, type InboxGroupKey } from "./ReviewDeskSidebar";
|
|
14
|
+
import { ReviewItemDetails } from "./ReviewItemDetails";
|
|
15
|
+
import { ReviewItemView } from "./ReviewItemView";
|
|
21
16
|
import {
|
|
22
17
|
useBlockedAutomationsData,
|
|
18
|
+
useLinkedConversationTitles,
|
|
23
19
|
useReviewItemData,
|
|
24
20
|
useReviewItemsData,
|
|
25
21
|
} from "./queries";
|
|
26
|
-
import {
|
|
27
|
-
assigneeLabel,
|
|
28
|
-
reviewItemPath,
|
|
29
|
-
reviewItemStatusLabel,
|
|
30
|
-
} from "./reviewItemFormat";
|
|
31
|
-
|
|
32
|
-
const rowClass =
|
|
33
|
-
"grid gap-0.5 rounded-md px-3 py-2 text-inherit no-underline transition-colors";
|
|
22
|
+
import { inboxGroups, nextInboxItem, reviewItemPath } from "./reviewItemFormat";
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function EmptyMessage(props: { children: ReactNode }) {
|
|
38
|
-
return (
|
|
39
|
-
<p className="m-0 px-4 py-8 text-center text-sm text-dashboard-text-muted">
|
|
40
|
-
{props.children}
|
|
41
|
-
</p>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
24
|
+
const shellClass =
|
|
25
|
+
"grid h-full min-h-0 w-full overflow-hidden bg-dashboard-bg md:grid-cols-[20rem_minmax(0,1fr)]";
|
|
44
26
|
|
|
45
|
-
function
|
|
46
|
-
children: ReactNode;
|
|
27
|
+
function InboxEmpty(props: {
|
|
47
28
|
className?: string;
|
|
48
|
-
|
|
49
|
-
title: string;
|
|
29
|
+
onShowArchived?: () => void;
|
|
50
30
|
}) {
|
|
51
|
-
const headingId = useId();
|
|
52
31
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"grid content-center justify-items-center gap-3 px-4 py-12 text-center",
|
|
35
|
+
props.className,
|
|
36
|
+
)}
|
|
56
37
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{props.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function ItemRow(props: {
|
|
70
|
-
item: ReviewItemPayload;
|
|
71
|
-
searchParams: URLSearchParams;
|
|
72
|
-
selected: boolean;
|
|
73
|
-
}) {
|
|
74
|
-
const { item } = props;
|
|
75
|
-
const closed = item.status !== "open";
|
|
76
|
-
const unassigned = !item.assignedToEveryone && item.assignees.length === 0;
|
|
77
|
-
return (
|
|
78
|
-
<li>
|
|
79
|
-
<Link
|
|
80
|
-
aria-current={props.selected ? "true" : undefined}
|
|
81
|
-
className={cn(
|
|
82
|
-
rowClass,
|
|
83
|
-
props.selected
|
|
84
|
-
? "bg-dashboard-accent/[0.08]"
|
|
85
|
-
: "hover:bg-dashboard-fill-soft",
|
|
86
|
-
)}
|
|
87
|
-
replace
|
|
88
|
-
to={pathWithSearch(reviewItemPath(item.id), props.searchParams)}
|
|
89
|
-
>
|
|
90
|
-
<span
|
|
91
|
-
className={cn(
|
|
92
|
-
"truncate text-sm font-medium",
|
|
93
|
-
closed && "text-dashboard-text-muted",
|
|
94
|
-
)}
|
|
95
|
-
>
|
|
96
|
-
{item.title}
|
|
97
|
-
</span>
|
|
98
|
-
<span className="flex min-w-0 items-center gap-2 text-xs text-dashboard-text-muted">
|
|
99
|
-
{closed ? (
|
|
100
|
-
<StatusChip className="shrink-0" size="compact">
|
|
101
|
-
{reviewItemStatusLabel(item)}
|
|
102
|
-
</StatusChip>
|
|
103
|
-
) : unassigned ? (
|
|
104
|
-
<StatusChip size="compact" tone="warning">
|
|
105
|
-
Unassigned
|
|
106
|
-
</StatusChip>
|
|
107
|
-
) : (
|
|
108
|
-
<span className="truncate">{assigneeLabel(item)}</span>
|
|
109
|
-
)}
|
|
110
|
-
<span className="shrink-0">
|
|
111
|
-
{formatRelativeTime(item.closedAt ?? item.updatedAt)}
|
|
112
|
-
</span>
|
|
113
|
-
</span>
|
|
114
|
-
</Link>
|
|
115
|
-
</li>
|
|
38
|
+
<p className="m-0 font-display text-lg font-medium text-dashboard-text">
|
|
39
|
+
Nothing needs you
|
|
40
|
+
</p>
|
|
41
|
+
{props.onShowArchived ? (
|
|
42
|
+
<Button onClick={props.onShowArchived} variant="quiet">
|
|
43
|
+
See archived items
|
|
44
|
+
</Button>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
116
47
|
);
|
|
117
48
|
}
|
|
118
49
|
|
|
119
|
-
|
|
50
|
+
/** Keep the inbox frame while the dashboard shell loads. */
|
|
51
|
+
export function ReviewDeskLoading() {
|
|
120
52
|
return (
|
|
121
|
-
<
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
53
|
+
<div className={shellClass}>
|
|
54
|
+
<aside
|
|
55
|
+
aria-busy="true"
|
|
56
|
+
aria-label="Loading review items"
|
|
57
|
+
className="grid content-start gap-2 border-r border-dashboard-border bg-dashboard-surface-panel px-5 py-3"
|
|
125
58
|
>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function ReviewListLoading() {
|
|
138
|
-
return (
|
|
139
|
-
<div
|
|
140
|
-
aria-busy="true"
|
|
141
|
-
aria-label="Loading review items"
|
|
142
|
-
className="grid gap-2"
|
|
143
|
-
>
|
|
144
|
-
{[0, 1, 2, 3].map((row) => (
|
|
145
|
-
<div className="grid gap-1.5 px-3 py-2" key={row}>
|
|
146
|
-
<Skeleton className="h-4 w-3/4" />
|
|
147
|
-
<Skeleton className="h-3 w-1/3" />
|
|
148
|
-
</div>
|
|
149
|
-
))}
|
|
59
|
+
<Skeleton className="h-5 w-16" />
|
|
60
|
+
<Skeleton className="h-8 w-full" />
|
|
61
|
+
{[0, 1, 2, 3].map((row) => (
|
|
62
|
+
<div className="grid gap-1 py-2" key={row}>
|
|
63
|
+
<Skeleton className="h-4 w-4/5" />
|
|
64
|
+
<Skeleton className="h-3 w-2/5 opacity-70" />
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</aside>
|
|
150
68
|
</div>
|
|
151
69
|
);
|
|
152
70
|
}
|
|
153
71
|
|
|
154
72
|
/**
|
|
155
|
-
* Render the
|
|
156
|
-
*
|
|
157
|
-
*
|
|
73
|
+
* Render the inbox: Review items that the viewer can see in a sidebar, and
|
|
74
|
+
* one selected item with the end of its Conversation, where the viewer
|
|
75
|
+
* decides it. Details show beside the item, or in a sheet on small screens.
|
|
158
76
|
*/
|
|
159
77
|
export function ReviewDeskPage() {
|
|
78
|
+
useDocumentTitle("Inbox");
|
|
160
79
|
const [searchParams] = useSearchParams();
|
|
161
80
|
const [search, setSearch, q] = useDebouncedSearchParam();
|
|
162
|
-
const {
|
|
81
|
+
const {
|
|
82
|
+
items: list,
|
|
83
|
+
moreArchived,
|
|
84
|
+
query: items,
|
|
85
|
+
} = useReviewItemsData({ includeArchived: true, q });
|
|
163
86
|
const showBlocked = !q;
|
|
164
87
|
const blocked = useBlockedAutomationsData(showBlocked);
|
|
165
|
-
const
|
|
88
|
+
const blockedAutomations = showBlocked
|
|
89
|
+
? (blocked.data?.automations ?? [])
|
|
90
|
+
: [];
|
|
91
|
+
const groups = useMemo(() => inboxGroups(list), [list]);
|
|
92
|
+
const open = [...groups.needsYou, ...groups.fyi];
|
|
93
|
+
|
|
94
|
+
// Archived items stay out of the way until a search or the reader asks.
|
|
95
|
+
const [collapsed, setCollapsed] = useState<ReadonlySet<InboxGroupKey>>(
|
|
96
|
+
() => new Set(["archived"]),
|
|
97
|
+
);
|
|
98
|
+
const toggle = (key: InboxGroupKey) =>
|
|
99
|
+
setCollapsed((current) => {
|
|
100
|
+
const next = new Set(current);
|
|
101
|
+
if (next.has(key)) next.delete(key);
|
|
102
|
+
else next.add(key);
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
const shownCollapsed = useMemo(() => {
|
|
106
|
+
if (!q || !collapsed.has("archived")) return collapsed;
|
|
107
|
+
const next = new Set(collapsed);
|
|
108
|
+
next.delete("archived");
|
|
109
|
+
return next;
|
|
110
|
+
}, [collapsed, q]);
|
|
111
|
+
const showArchived =
|
|
112
|
+
groups.archived.length > 0 && shownCollapsed.has("archived")
|
|
113
|
+
? () => toggle("archived")
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
116
|
+
const itemParam = searchParams.get("item") ?? undefined;
|
|
117
|
+
// Wide screens select the item that waited longest. Small screens show
|
|
118
|
+
// the list until the reader picks an item.
|
|
119
|
+
const selectedId = itemParam ?? open[0]?.id;
|
|
166
120
|
const listed = list.find((item) => item.id === selectedId);
|
|
167
121
|
// A link can select an item that is not in the current view.
|
|
168
122
|
const direct = useReviewItemData(listed ? undefined : selectedId);
|
|
169
123
|
const selected = listed ?? direct.data;
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
124
|
+
const nextItem = selected ? nextInboxItem(groups, selected) : undefined;
|
|
125
|
+
|
|
126
|
+
// Read source titles only for the rows on screen and the selected item.
|
|
127
|
+
const sourceIds = useMemo(() => {
|
|
128
|
+
const shown = [
|
|
129
|
+
...(shownCollapsed.has("needs-you") ? [] : groups.needsYou),
|
|
130
|
+
...(shownCollapsed.has("fyi") ? [] : groups.fyi),
|
|
131
|
+
...(shownCollapsed.has("archived") ? [] : groups.archived),
|
|
132
|
+
...(selected ? [selected] : []),
|
|
133
|
+
];
|
|
134
|
+
return [
|
|
135
|
+
...new Set(
|
|
136
|
+
shown
|
|
137
|
+
.filter((item) => !item.automationId)
|
|
138
|
+
.map((item) => item.addedByConversationId),
|
|
139
|
+
),
|
|
140
|
+
];
|
|
141
|
+
}, [groups, selected, shownCollapsed]);
|
|
142
|
+
const sourceTitles = useLinkedConversationTitles(sourceIds);
|
|
143
|
+
const sourceTitle = (item: ReviewItemPayload) =>
|
|
144
|
+
sourceTitles[sourceIds.indexOf(item.addedByConversationId)];
|
|
145
|
+
|
|
146
|
+
const panelFits = useDetailsPanel();
|
|
147
|
+
const [detailsChoice, setDetailsChoice] = useState<boolean>();
|
|
148
|
+
const detailsOpen = Boolean(selected) && (detailsChoice ?? panelFits);
|
|
149
|
+
|
|
150
|
+
const listParams = new URLSearchParams(searchParams);
|
|
151
|
+
listParams.delete("item");
|
|
152
|
+
const loaded = Boolean(items.data);
|
|
177
153
|
const empty =
|
|
178
|
-
|
|
154
|
+
loaded && !q && open.length === 0 && blockedAutomations.length === 0;
|
|
179
155
|
|
|
180
156
|
return (
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
className="grid content-start gap-4 py-3 lg:self-start"
|
|
195
|
-
padding="none"
|
|
196
|
-
>
|
|
197
|
-
{items.error && !items.data ? (
|
|
198
|
-
<InlineError className="px-3">
|
|
199
|
-
Could not load review items.
|
|
200
|
-
</InlineError>
|
|
201
|
-
) : !items.data ? (
|
|
202
|
-
<ReviewListLoading />
|
|
203
|
-
) : empty ? (
|
|
204
|
-
<EmptyMessage>
|
|
205
|
-
{q ? `No items match "${q}".` : "No open items."}
|
|
206
|
-
</EmptyMessage>
|
|
207
|
-
) : (
|
|
157
|
+
<div className={shellClass}>
|
|
158
|
+
<div
|
|
159
|
+
className={cn(
|
|
160
|
+
"h-full min-h-0 overflow-hidden",
|
|
161
|
+
itemParam && "max-md:hidden",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
<ReviewDeskSidebar
|
|
165
|
+
archived={groups.archived}
|
|
166
|
+
blocked={blockedAutomations}
|
|
167
|
+
collapsed={shownCollapsed}
|
|
168
|
+
error={Boolean(items.error && !items.data)}
|
|
169
|
+
footer={
|
|
208
170
|
<>
|
|
209
|
-
{actions.length > 0 ? (
|
|
210
|
-
<ListGroup title="Needs an answer">
|
|
211
|
-
{actions.map((item) => (
|
|
212
|
-
<ItemRow
|
|
213
|
-
item={item}
|
|
214
|
-
key={item.id}
|
|
215
|
-
searchParams={searchParams}
|
|
216
|
-
selected={item.id === selected?.id}
|
|
217
|
-
/>
|
|
218
|
-
))}
|
|
219
|
-
</ListGroup>
|
|
220
|
-
) : null}
|
|
221
|
-
{notices.length > 0 ? (
|
|
222
|
-
<ListGroup title="Notices">
|
|
223
|
-
{notices.map((item) => (
|
|
224
|
-
<ItemRow
|
|
225
|
-
item={item}
|
|
226
|
-
key={item.id}
|
|
227
|
-
searchParams={searchParams}
|
|
228
|
-
selected={item.id === selected?.id}
|
|
229
|
-
/>
|
|
230
|
-
))}
|
|
231
|
-
</ListGroup>
|
|
232
|
-
) : null}
|
|
233
171
|
<LoadMorePagination
|
|
234
|
-
className="
|
|
172
|
+
className="mx-3 mt-2"
|
|
235
173
|
hasMore={!items.isPlaceholderData && Boolean(items.hasNextPage)}
|
|
236
174
|
loading={items.isFetchingNextPage}
|
|
237
175
|
onLoadMore={() => void items.fetchNextPage()}
|
|
238
176
|
/>
|
|
239
177
|
{items.isFetchNextPageError ? (
|
|
240
|
-
<InlineError className="px-3
|
|
178
|
+
<InlineError className="px-3 py-2">
|
|
241
179
|
Could not load more items. Try again.
|
|
242
180
|
</InlineError>
|
|
243
181
|
) : null}
|
|
244
|
-
{
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
>
|
|
254
|
-
{archived.map((item) => (
|
|
255
|
-
<ItemRow
|
|
256
|
-
item={item}
|
|
257
|
-
key={item.id}
|
|
258
|
-
searchParams={searchParams}
|
|
259
|
-
selected={item.id === selected?.id}
|
|
260
|
-
/>
|
|
261
|
-
))}
|
|
262
|
-
{moreArchived ? (
|
|
263
|
-
<li className="px-3 py-1 text-xs text-dashboard-text-muted">
|
|
264
|
-
Only the newest archived matches show. Add words to the
|
|
265
|
-
search to find older items.
|
|
266
|
-
</li>
|
|
267
|
-
) : null}
|
|
268
|
-
</ListGroup>
|
|
182
|
+
{moreArchived && !shownCollapsed.has("archived") ? (
|
|
183
|
+
<p className="m-0 px-3 py-2 text-xs text-dashboard-text-muted">
|
|
184
|
+
Search to find older archived items.
|
|
185
|
+
</p>
|
|
186
|
+
) : null}
|
|
187
|
+
{loaded && q && list.length === 0 ? (
|
|
188
|
+
<p className="m-0 px-3 py-8 text-center text-sm text-dashboard-text-muted">
|
|
189
|
+
No items match "{q}".
|
|
190
|
+
</p>
|
|
269
191
|
) : null}
|
|
270
|
-
{
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
key={automation.id}
|
|
276
|
-
/>
|
|
277
|
-
))}
|
|
278
|
-
</ListGroup>
|
|
192
|
+
{empty ? (
|
|
193
|
+
<InboxEmpty
|
|
194
|
+
className="md:hidden"
|
|
195
|
+
onShowArchived={showArchived}
|
|
196
|
+
/>
|
|
279
197
|
) : null}
|
|
280
198
|
</>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
</Card>
|
|
199
|
+
}
|
|
200
|
+
fyi={groups.fyi}
|
|
201
|
+
loading={!items.data && !items.error}
|
|
202
|
+
needsYou={groups.needsYou}
|
|
203
|
+
onSearchChange={setSearch}
|
|
204
|
+
onToggle={toggle}
|
|
205
|
+
search={search}
|
|
206
|
+
searchParams={listParams}
|
|
207
|
+
selectedId={selected?.id}
|
|
208
|
+
selectedShown={Boolean(itemParam)}
|
|
209
|
+
sourceTitle={sourceTitle}
|
|
210
|
+
/>
|
|
294
211
|
</div>
|
|
295
|
-
|
|
212
|
+
<section
|
|
213
|
+
aria-label="Selected review item"
|
|
214
|
+
className={cn(
|
|
215
|
+
"grid min-h-0 grid-rows-[minmax(0,1fr)] overflow-hidden",
|
|
216
|
+
!itemParam && "max-md:hidden",
|
|
217
|
+
detailsOpen && "xl:grid-cols-[minmax(0,1fr)_25rem]",
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{selected ? (
|
|
221
|
+
<>
|
|
222
|
+
<ReviewItemView
|
|
223
|
+
backTo={pathWithSearch("/review", listParams)}
|
|
224
|
+
detailsOpen={detailsOpen}
|
|
225
|
+
item={selected}
|
|
226
|
+
// A key per item resets the answer form and its send lock, so
|
|
227
|
+
// one item's answer can never reach another item.
|
|
228
|
+
key={selected.id}
|
|
229
|
+
next={
|
|
230
|
+
nextItem
|
|
231
|
+
? {
|
|
232
|
+
title: nextItem.title,
|
|
233
|
+
to: pathWithSearch(
|
|
234
|
+
reviewItemPath(nextItem.id),
|
|
235
|
+
listParams,
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
: undefined
|
|
239
|
+
}
|
|
240
|
+
onDetailsClick={() => setDetailsChoice(!detailsOpen)}
|
|
241
|
+
sourceTitle={sourceTitle(selected)}
|
|
242
|
+
/>
|
|
243
|
+
{detailsOpen ? (
|
|
244
|
+
<ReviewItemDetails
|
|
245
|
+
item={selected}
|
|
246
|
+
onClose={() => setDetailsChoice(false)}
|
|
247
|
+
/>
|
|
248
|
+
) : null}
|
|
249
|
+
</>
|
|
250
|
+
) : direct.error ? (
|
|
251
|
+
<InlineError className="p-7">Review item not found.</InlineError>
|
|
252
|
+
) : empty ? (
|
|
253
|
+
<InboxEmpty onShowArchived={showArchived} />
|
|
254
|
+
) : null}
|
|
255
|
+
</section>
|
|
256
|
+
</div>
|
|
296
257
|
);
|
|
297
258
|
}
|