@sentry/junior-dashboard 0.268.0 → 0.269.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/README.md +7 -105
- package/dist/app.js +12 -9
- package/dist/client.js +72 -75
- package/dist/index.js +12 -9
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +2 -1
- package/src/app.ts +11 -7
- package/src/client/App.tsx +1 -5
- package/src/client/api.ts +4 -19
- package/src/client/components/AGENTS.md +65 -0
- package/src/client/components/ActorAvatar.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +1 -0
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +1 -1
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/conversations/AGENTS.md +42 -0
- package/src/client/conversations/ConversationPage.tsx +0 -27
- package/src/client/format.ts +7 -23
- package/src/client/pages/automations/AGENTS.md +34 -0
- package/src/client/pages/automations/AutomationActions.tsx +18 -20
- package/src/client/pages/automations/AutomationEditor.tsx +3 -4
- package/src/client/pages/automations/AutomationOverview.tsx +3 -3
- package/src/client/pages/automations/AutomationStatus.tsx +2 -2
- package/src/client/pages/system/AGENTS.md +12 -0
- package/src/client/review-desk/ConversationReviewBar.tsx +14 -8
- package/src/client/review-desk/ReviewDeskPage.tsx +99 -79
- package/src/client/review-desk/ReviewItemDetail.tsx +15 -7
- package/src/client/review-desk/queries.ts +25 -16
- package/src/mock-reporting/routes.ts +1 -1
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -219
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/UserInputRequestCard.tsx +0 -39
- package/src/client/conversations/userInputRequest.ts +0 -56
- package/src/client/pages/automations/README.md +0 -97
- package/src/client/pages/system/README.md +0 -23
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Automation pages
|
|
2
|
+
|
|
3
|
+
## Boundaries
|
|
4
|
+
|
|
5
|
+
- Core owns validation, edit access, creator-only rules, revision checks,
|
|
6
|
+
Schedule previews, and the Event catalog. The page shows core errors. It
|
|
7
|
+
does not repeat these rules.
|
|
8
|
+
- Preview requests never save or run work.
|
|
9
|
+
- Completed scheduled Automations are read-only.
|
|
10
|
+
|
|
11
|
+
## Editor rules
|
|
12
|
+
|
|
13
|
+
- Send only changed fields. Keep unsupported triggers and stored
|
|
14
|
+
Destinations unless the user replaces them. Keep outcome order.
|
|
15
|
+
- New messages can target only the current Destination or the creator.
|
|
16
|
+
- A public reader sees outcomes as read-only and cannot turn on creator
|
|
17
|
+
credentials. Warn that an instruction or event selector change switches to
|
|
18
|
+
system credentials.
|
|
19
|
+
- Pause, Resume, and Delete are creator-only. Hide them from other viewers.
|
|
20
|
+
- Query refreshes must not replace an unsaved draft. After a stale save, let
|
|
21
|
+
the user keep their changes or load the latest version.
|
|
22
|
+
- Block navigation with unsaved changes.
|
|
23
|
+
- Lifecycle actions and "Make active" read the latest revision first.
|
|
24
|
+
|
|
25
|
+
## Page rules
|
|
26
|
+
|
|
27
|
+
- Each Automation has one page at `/automations/:id`. Keep the list query
|
|
28
|
+
string on the way back. Drop tab-only params: `result`, `run`, `version`.
|
|
29
|
+
- `AutomationRecentRuns` shows terminal runs only. Do not infer queued or
|
|
30
|
+
running work from terminal records.
|
|
31
|
+
- Delete keeps errors in the dialog and blocks dismissal while pending.
|
|
32
|
+
|
|
33
|
+
Browser journeys: `e2e/automation-editor.spec.ts` and
|
|
34
|
+
`e2e/automations.spec.ts`.
|
|
@@ -43,7 +43,7 @@ export function AutomationActions({
|
|
|
43
43
|
const completed = automation.status === "completed";
|
|
44
44
|
const paused = automation.status === "paused";
|
|
45
45
|
const blocked = automation.status === "blocked";
|
|
46
|
-
const canPause =
|
|
46
|
+
const canPause = !completed;
|
|
47
47
|
const action = paused || blocked ? "resume" : "pause";
|
|
48
48
|
const mutation = useMutation({
|
|
49
49
|
mutationFn: async () => {
|
|
@@ -135,25 +135,23 @@ export function AutomationActions({
|
|
|
135
135
|
</Popover>
|
|
136
136
|
</>
|
|
137
137
|
) : null}
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
</Tooltip>
|
|
156
|
-
) : null}
|
|
138
|
+
<Tooltip>
|
|
139
|
+
<TooltipTrigger
|
|
140
|
+
render={
|
|
141
|
+
<Button
|
|
142
|
+
focusableWhenDisabled
|
|
143
|
+
aria-label="Delete automation"
|
|
144
|
+
size="icon"
|
|
145
|
+
variant="danger"
|
|
146
|
+
onClick={(event) => onDelete(event.currentTarget)}
|
|
147
|
+
disabled={mutation.isPending}
|
|
148
|
+
>
|
|
149
|
+
<Trash2 aria-hidden="true" size={16} />
|
|
150
|
+
</Button>
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
<TooltipContent size="compact">Delete automation</TooltipContent>
|
|
154
|
+
</Tooltip>
|
|
157
155
|
</div>
|
|
158
156
|
{mutation.error ? (
|
|
159
157
|
<div className="max-w-sm">
|
|
@@ -135,8 +135,7 @@ export function AutomationEditor(props: {
|
|
|
135
135
|
const dropsCreatorCredentials =
|
|
136
136
|
!owned &&
|
|
137
137
|
original.credentialMode === "creator" &&
|
|
138
|
-
(changes.instruction !== undefined ||
|
|
139
|
-
(original.kind === "event" && changes.trigger !== undefined));
|
|
138
|
+
(changes.instruction !== undefined || changes[triggerField] !== undefined);
|
|
140
139
|
const triggerError = fieldError(triggerField);
|
|
141
140
|
const inputState = (key: string) => ({
|
|
142
141
|
"aria-invalid": Boolean(fieldError(key)) || undefined,
|
|
@@ -426,8 +425,8 @@ export function AutomationEditor(props: {
|
|
|
426
425
|
{dropsCreatorCredentials ? (
|
|
427
426
|
<FormNotice title="Saving switches this automation to system credentials.">
|
|
428
427
|
Only {creator} can let a changed instruction
|
|
429
|
-
{original.kind === "event" ? " or trigger" : ""} use
|
|
430
|
-
connected accounts.
|
|
428
|
+
{original.kind === "event" ? " or trigger" : " or schedule"} use
|
|
429
|
+
their connected accounts.
|
|
431
430
|
</FormNotice>
|
|
432
431
|
) : null}
|
|
433
432
|
{fieldError("credentialMode") ? (
|
|
@@ -197,9 +197,9 @@ function AutomationAbout(props: { automation: AutomationSummary }) {
|
|
|
197
197
|
<p className="mt-3 mb-0 text-xs leading-relaxed text-dashboard-text-muted">
|
|
198
198
|
{automation.destination.visibility === "private"
|
|
199
199
|
? null
|
|
200
|
-
: "You can edit
|
|
201
|
-
Only {automation.createdBy} can
|
|
202
|
-
|
|
200
|
+
: "You can edit, pause, or delete it because it is in a public channel. "}
|
|
201
|
+
Only {automation.createdBy} can change where results go, turn on their
|
|
202
|
+
connected accounts, or resume or run it while it uses them.
|
|
203
203
|
</p>
|
|
204
204
|
)}
|
|
205
205
|
</RailCard>
|
|
@@ -140,8 +140,8 @@ export function AutomationAttention({
|
|
|
140
140
|
<p className="m-0 break-words">
|
|
141
141
|
{automation.statusReason ??
|
|
142
142
|
"Check the last run to see what needs fixing."}{" "}
|
|
143
|
-
{!automation.ownedByViewer
|
|
144
|
-
? `
|
|
143
|
+
{!automation.ownedByViewer && automation.credentialMode === "creator"
|
|
144
|
+
? `It uses accounts connected by ${automation.createdBy}. Ask them to resume it, or switch it to Junior's credentials first.`
|
|
145
145
|
: null}
|
|
146
146
|
</p>
|
|
147
147
|
) : null}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# System pages
|
|
2
|
+
|
|
3
|
+
- The URL selects a Workspace recipe: `/system/workspaces/:id` opens
|
|
4
|
+
`WorkspaceDetails` beside the list.
|
|
5
|
+
- The list report has no snapshots. The detail read adds the current
|
|
6
|
+
snapshot. Keep snapshot loading and errors inside the Snapshot row.
|
|
7
|
+
- Edit and Delete live in the panel header. List rows have no actions.
|
|
8
|
+
- `WorkspaceDeleteDialog` keeps errors in the dialog and blocks dismissal
|
|
9
|
+
while the request runs.
|
|
10
|
+
- Close and Escape return focus to the selected row.
|
|
11
|
+
|
|
12
|
+
Browser coverage: `e2e/system.spec.ts`.
|
|
@@ -2,10 +2,13 @@ import { ChevronRight } from "lucide-react";
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
|
|
4
4
|
import { Drawer } from "../components/Drawer";
|
|
5
|
+
import { LoadMorePagination } from "../components/Pagination";
|
|
5
6
|
import { ReviewItemCard } from "./ReviewItemCard";
|
|
6
7
|
import { useReviewItemsData } from "./queries";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** `more` marks a count of the loaded pages only, such as "50+". */
|
|
10
|
+
function itemCountLabel(count: number, more: boolean): string {
|
|
11
|
+
if (more) return `${count}+ open review items`;
|
|
9
12
|
return count === 1 ? "1 open review item" : `${count} open review items`;
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -15,14 +18,12 @@ function itemCountLabel(count: number): string {
|
|
|
15
18
|
*/
|
|
16
19
|
export function ConversationReviewBar(props: { conversationId: string }) {
|
|
17
20
|
const [open, setOpen] = useState(false);
|
|
18
|
-
const items =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
conversationId: props.conversationId,
|
|
22
|
-
view: "all",
|
|
23
|
-
}).data?.items ?? [];
|
|
21
|
+
const { items, query } = useReviewItemsData({
|
|
22
|
+
conversationId: props.conversationId,
|
|
23
|
+
});
|
|
24
24
|
if (items.length === 0) return null;
|
|
25
|
-
const
|
|
25
|
+
const more = Boolean(query.hasNextPage);
|
|
26
|
+
const label = itemCountLabel(items.length, more);
|
|
26
27
|
return (
|
|
27
28
|
<>
|
|
28
29
|
<button
|
|
@@ -54,6 +55,11 @@ export function ConversationReviewBar(props: { conversationId: string }) {
|
|
|
54
55
|
{items.map((item) => (
|
|
55
56
|
<ReviewItemCard item={item} key={item.id} />
|
|
56
57
|
))}
|
|
58
|
+
<LoadMorePagination
|
|
59
|
+
hasMore={more}
|
|
60
|
+
loading={query.isFetchingNextPage}
|
|
61
|
+
onLoadMore={() => void query.fetchNextPage()}
|
|
62
|
+
/>
|
|
57
63
|
</div>
|
|
58
64
|
</Drawer>
|
|
59
65
|
) : null}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Archive } from "lucide-react";
|
|
2
|
+
import { useId, type ReactNode } from "react";
|
|
2
3
|
import { Link, useSearchParams } from "react-router";
|
|
3
4
|
import type {
|
|
4
5
|
AutomationSummary,
|
|
5
6
|
ReviewItemPayload,
|
|
6
7
|
} from "@sentry/junior/api/schema";
|
|
7
8
|
|
|
8
|
-
import { FilterTabList } from "../components/FilterBar";
|
|
9
9
|
import { InlineError } from "../components/InlineError";
|
|
10
|
+
import { LoadMorePagination } from "../components/Pagination";
|
|
10
11
|
import { SearchInput } from "../components/SearchInput";
|
|
11
12
|
import { StatusChip } from "../components/StatusChip";
|
|
12
13
|
import { Card } from "../components/layout/Card";
|
|
@@ -14,33 +15,19 @@ import { PageHeader } from "../components/layout/PageHeader";
|
|
|
14
15
|
import { PageLayout } from "../components/layout/PageLayout";
|
|
15
16
|
import { Skeleton } from "../components/ui/skeleton";
|
|
16
17
|
import { automationPath, formatRelativeTime } from "../format";
|
|
17
|
-
import {
|
|
18
|
-
parseSearchParamEnum,
|
|
19
|
-
pathWithSearch,
|
|
20
|
-
useDebouncedSearchParam,
|
|
21
|
-
} from "../searchParams";
|
|
18
|
+
import { pathWithSearch, useDebouncedSearchParam } from "../searchParams";
|
|
22
19
|
import { cn } from "../styles";
|
|
23
20
|
import { ReviewItemDetail } from "./ReviewItemDetail";
|
|
24
21
|
import {
|
|
25
|
-
REVIEW_DESK_VIEWS,
|
|
26
22
|
useBlockedAutomationsData,
|
|
27
23
|
useReviewItemData,
|
|
28
24
|
useReviewItemsData,
|
|
29
25
|
} from "./queries";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ label: "All", value: "all" },
|
|
36
|
-
] as const;
|
|
37
|
-
|
|
38
|
-
const STATE_TABS = [
|
|
39
|
-
{ label: "Open", value: "open" },
|
|
40
|
-
{ label: "Archived", value: "archived" },
|
|
41
|
-
] as const;
|
|
42
|
-
|
|
43
|
-
const ITEM_STATES = ["open", "archived"] as const;
|
|
26
|
+
import {
|
|
27
|
+
assigneeLabel,
|
|
28
|
+
reviewItemPath,
|
|
29
|
+
reviewItemStatusLabel,
|
|
30
|
+
} from "./reviewItemFormat";
|
|
44
31
|
|
|
45
32
|
const rowClass =
|
|
46
33
|
"grid gap-0.5 rounded-md px-3 py-2 text-inherit no-underline transition-colors";
|
|
@@ -55,10 +42,23 @@ function EmptyMessage(props: { children: ReactNode }) {
|
|
|
55
42
|
);
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
function ListGroup(props: {
|
|
45
|
+
function ListGroup(props: {
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
className?: string;
|
|
48
|
+
icon?: ReactNode;
|
|
49
|
+
title: string;
|
|
50
|
+
}) {
|
|
51
|
+
const headingId = useId();
|
|
59
52
|
return (
|
|
60
|
-
<section
|
|
61
|
-
|
|
53
|
+
<section
|
|
54
|
+
aria-labelledby={headingId}
|
|
55
|
+
className={cn("grid gap-1", props.className)}
|
|
56
|
+
>
|
|
57
|
+
<h2
|
|
58
|
+
id={headingId}
|
|
59
|
+
className="m-0 flex items-center gap-1.5 px-3 font-sans text-xs font-medium text-dashboard-text-muted"
|
|
60
|
+
>
|
|
61
|
+
{props.icon}
|
|
62
62
|
{props.title}
|
|
63
63
|
</h2>
|
|
64
64
|
<ul className="m-0 grid list-none gap-0.5 p-0">{props.children}</ul>
|
|
@@ -72,6 +72,7 @@ function ItemRow(props: {
|
|
|
72
72
|
selected: boolean;
|
|
73
73
|
}) {
|
|
74
74
|
const { item } = props;
|
|
75
|
+
const closed = item.status !== "open";
|
|
75
76
|
const unassigned = !item.assignedToEveryone && item.assignees.length === 0;
|
|
76
77
|
return (
|
|
77
78
|
<li>
|
|
@@ -86,9 +87,20 @@ function ItemRow(props: {
|
|
|
86
87
|
replace
|
|
87
88
|
to={pathWithSearch(reviewItemPath(item.id), props.searchParams)}
|
|
88
89
|
>
|
|
89
|
-
<span
|
|
90
|
+
<span
|
|
91
|
+
className={cn(
|
|
92
|
+
"truncate text-sm font-medium",
|
|
93
|
+
closed && "text-dashboard-text-muted",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{item.title}
|
|
97
|
+
</span>
|
|
90
98
|
<span className="flex min-w-0 items-center gap-2 text-xs text-dashboard-text-muted">
|
|
91
|
-
{
|
|
99
|
+
{closed ? (
|
|
100
|
+
<StatusChip className="shrink-0" size="compact">
|
|
101
|
+
{reviewItemStatusLabel(item)}
|
|
102
|
+
</StatusChip>
|
|
103
|
+
) : unassigned ? (
|
|
92
104
|
<StatusChip size="compact" tone="warning">
|
|
93
105
|
Unassigned
|
|
94
106
|
</StatusChip>
|
|
@@ -140,42 +152,25 @@ function ReviewListLoading() {
|
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/**
|
|
143
|
-
* Render the review desk: open
|
|
144
|
-
*
|
|
155
|
+
* Render the review desk: the open Review items that the viewer can see,
|
|
156
|
+
* with search and the selected item beside the list. A search also finds
|
|
157
|
+
* archived items, which show in their own group.
|
|
145
158
|
*/
|
|
146
159
|
export function ReviewDeskPage() {
|
|
147
|
-
const [searchParams
|
|
148
|
-
const view =
|
|
149
|
-
parseSearchParamEnum(searchParams.get("view"), REVIEW_DESK_VIEWS) ?? "mine";
|
|
150
|
-
const state =
|
|
151
|
-
parseSearchParamEnum(searchParams.get("state"), ITEM_STATES) ?? "open";
|
|
152
|
-
// The selected item can be missing from the new list, so a tab change
|
|
153
|
-
// drops it and the first item of the new list shows.
|
|
154
|
-
function selectTab(key: "state" | "view", value: string, initial: string) {
|
|
155
|
-
setSearchParams(
|
|
156
|
-
(current) => {
|
|
157
|
-
const next = new URLSearchParams(current);
|
|
158
|
-
next.delete("item");
|
|
159
|
-
if (value === initial) next.delete(key);
|
|
160
|
-
else next.set(key, value);
|
|
161
|
-
return next;
|
|
162
|
-
},
|
|
163
|
-
{ replace: true },
|
|
164
|
-
);
|
|
165
|
-
}
|
|
160
|
+
const [searchParams] = useSearchParams();
|
|
166
161
|
const [search, setSearch, q] = useDebouncedSearchParam();
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const showBlocked = !archived && view !== "everyone" && !q;
|
|
162
|
+
const { items: list, moreArchived, query: items } = useReviewItemsData({ q });
|
|
163
|
+
const showBlocked = !q;
|
|
170
164
|
const blocked = useBlockedAutomationsData(showBlocked);
|
|
171
|
-
const list = items.data?.items ?? [];
|
|
172
165
|
const selectedId = searchParams.get("item") ?? list[0]?.id;
|
|
173
166
|
const listed = list.find((item) => item.id === selectedId);
|
|
174
167
|
// A link can select an item that is not in the current view.
|
|
175
168
|
const direct = useReviewItemData(listed ? undefined : selectedId);
|
|
176
169
|
const selected = listed ?? direct.data;
|
|
177
|
-
const
|
|
178
|
-
const
|
|
170
|
+
const open = list.filter((item) => item.status === "open");
|
|
171
|
+
const archived = list.filter((item) => item.status !== "open");
|
|
172
|
+
const actions = open.filter((item) => item.kind === "action");
|
|
173
|
+
const notices = open.filter((item) => item.kind === "notice");
|
|
179
174
|
const blockedAutomations = showBlocked
|
|
180
175
|
? (blocked.data?.automations ?? [])
|
|
181
176
|
: [];
|
|
@@ -188,30 +183,18 @@ export function ReviewDeskPage() {
|
|
|
188
183
|
description="Items that need a person, and who can act on them."
|
|
189
184
|
title="Review desk"
|
|
190
185
|
/>
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/>
|
|
198
|
-
<FilterTabList
|
|
199
|
-
ariaLabel="Item state"
|
|
200
|
-
items={STATE_TABS}
|
|
201
|
-
onChange={(value) => selectTab("state", value, "open")}
|
|
202
|
-
value={state}
|
|
203
|
-
/>
|
|
204
|
-
<SearchInput
|
|
205
|
-
className="ml-auto w-full sm:w-64"
|
|
206
|
-
label="Search review items"
|
|
207
|
-
onChange={setSearch}
|
|
208
|
-
placeholder="Search items"
|
|
209
|
-
value={search}
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
186
|
+
<SearchInput
|
|
187
|
+
label="Search review items"
|
|
188
|
+
onChange={setSearch}
|
|
189
|
+
placeholder="Search open and archived items"
|
|
190
|
+
value={search}
|
|
191
|
+
/>
|
|
212
192
|
<div className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,22rem)_minmax(0,1fr)]">
|
|
213
|
-
<Card
|
|
214
|
-
|
|
193
|
+
<Card
|
|
194
|
+
className="grid content-start gap-4 py-3 lg:self-start"
|
|
195
|
+
padding="none"
|
|
196
|
+
>
|
|
197
|
+
{items.error && !items.data ? (
|
|
215
198
|
<InlineError className="px-3">
|
|
216
199
|
Could not load review items.
|
|
217
200
|
</InlineError>
|
|
@@ -219,12 +202,12 @@ export function ReviewDeskPage() {
|
|
|
219
202
|
<ReviewListLoading />
|
|
220
203
|
) : empty ? (
|
|
221
204
|
<EmptyMessage>
|
|
222
|
-
{
|
|
205
|
+
{q ? `No items match "${q}".` : "No open items."}
|
|
223
206
|
</EmptyMessage>
|
|
224
207
|
) : (
|
|
225
208
|
<>
|
|
226
209
|
{actions.length > 0 ? (
|
|
227
|
-
<ListGroup title=
|
|
210
|
+
<ListGroup title="Needs an answer">
|
|
228
211
|
{actions.map((item) => (
|
|
229
212
|
<ItemRow
|
|
230
213
|
item={item}
|
|
@@ -247,6 +230,43 @@ export function ReviewDeskPage() {
|
|
|
247
230
|
))}
|
|
248
231
|
</ListGroup>
|
|
249
232
|
) : null}
|
|
233
|
+
<LoadMorePagination
|
|
234
|
+
className="px-3"
|
|
235
|
+
hasMore={!items.isPlaceholderData && Boolean(items.hasNextPage)}
|
|
236
|
+
loading={items.isFetchingNextPage}
|
|
237
|
+
onLoadMore={() => void items.fetchNextPage()}
|
|
238
|
+
/>
|
|
239
|
+
{items.isFetchNextPageError ? (
|
|
240
|
+
<InlineError className="px-3 text-center">
|
|
241
|
+
Could not load more items. Try again.
|
|
242
|
+
</InlineError>
|
|
243
|
+
) : null}
|
|
244
|
+
{archived.length > 0 ? (
|
|
245
|
+
<ListGroup
|
|
246
|
+
className={
|
|
247
|
+
open.length > 0
|
|
248
|
+
? "border-t border-dashboard-border-subtle pt-3"
|
|
249
|
+
: undefined
|
|
250
|
+
}
|
|
251
|
+
icon={<Archive aria-hidden="true" size={13} />}
|
|
252
|
+
title="Archived"
|
|
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>
|
|
269
|
+
) : null}
|
|
250
270
|
{blockedAutomations.length > 0 ? (
|
|
251
271
|
<ListGroup title="Blocked Automations">
|
|
252
272
|
{blockedAutomations.map((automation) => (
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Archive } from "lucide-react";
|
|
1
2
|
import type { ReactNode } from "react";
|
|
2
3
|
import { Link } from "react-router";
|
|
3
4
|
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
@@ -26,20 +27,27 @@ export function ReviewItemDetail(props: { item: ReviewItemPayload }) {
|
|
|
26
27
|
const { item } = props;
|
|
27
28
|
const subject = item.subject;
|
|
28
29
|
const conversationTitles = useLinkedConversationTitles(item.conversationIds);
|
|
30
|
+
const closed = item.status !== "open";
|
|
29
31
|
return (
|
|
30
32
|
<article className="grid min-w-0 content-start gap-5">
|
|
33
|
+
{closed ? (
|
|
34
|
+
// Search mixes open and archived items, so a closed item must be
|
|
35
|
+
// easy to tell apart at the top of the detail.
|
|
36
|
+
<p className="m-0 flex items-center gap-2 rounded-md bg-dashboard-fill-soft px-3 py-2 text-xs font-medium text-dashboard-text-muted">
|
|
37
|
+
<Archive aria-hidden="true" size={14} />
|
|
38
|
+
Archived · {reviewItemStatusLabel(item)}
|
|
39
|
+
</p>
|
|
40
|
+
) : null}
|
|
31
41
|
<header className="grid gap-1.5">
|
|
32
42
|
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
33
43
|
<h2 className="m-0 min-w-0 break-words font-display text-xl font-medium leading-snug">
|
|
34
44
|
{item.title}
|
|
35
45
|
</h2>
|
|
36
|
-
|
|
37
|
-
className="mt-1 shrink-0"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{reviewItemStatusLabel(item)}
|
|
42
|
-
</StatusChip>
|
|
46
|
+
{closed ? null : (
|
|
47
|
+
<StatusChip className="mt-1 shrink-0" size="compact" tone="accent">
|
|
48
|
+
{reviewItemStatusLabel(item)}
|
|
49
|
+
</StatusChip>
|
|
50
|
+
)}
|
|
43
51
|
</div>
|
|
44
52
|
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
45
53
|
Added {formatRelativeTime(item.createdAt)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
keepPreviousData,
|
|
4
|
+
useInfiniteQuery,
|
|
4
5
|
useMutation,
|
|
5
6
|
useQueries,
|
|
6
7
|
useQuery,
|
|
@@ -19,38 +20,46 @@ import {
|
|
|
19
20
|
import { conversationDetailTitle } from "../format";
|
|
20
21
|
import { fetchDashboardJson, post } from "../http";
|
|
21
22
|
|
|
22
|
-
/** Review desk views. Each shows open items, or archived ones. */
|
|
23
|
-
export const REVIEW_DESK_VIEWS = ["mine", "everyone", "all"] as const;
|
|
24
|
-
export type ReviewDeskView = (typeof REVIEW_DESK_VIEWS)[number];
|
|
25
|
-
|
|
26
23
|
const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
|
|
27
24
|
const COUNT_REFRESH_MS = 60_000;
|
|
28
25
|
|
|
29
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* List the open Review items that the viewer can see, one page at a time.
|
|
28
|
+
* A search also finds archived items, with the first page. `items` holds
|
|
29
|
+
* the items of every loaded page, and `query` loads more.
|
|
30
|
+
*/
|
|
30
31
|
export function useReviewItemsData(filters: {
|
|
31
|
-
archived: boolean;
|
|
32
32
|
conversationId?: string;
|
|
33
33
|
q?: string;
|
|
34
|
-
view: ReviewDeskView;
|
|
35
34
|
}) {
|
|
36
|
-
const params = new URLSearchParams(
|
|
37
|
-
if (filters.archived) params.set("archived", "true");
|
|
35
|
+
const params = new URLSearchParams();
|
|
38
36
|
if (filters.conversationId) {
|
|
39
37
|
params.set("conversationId", filters.conversationId);
|
|
40
38
|
}
|
|
41
39
|
if (filters.q) params.set("q", filters.q);
|
|
42
40
|
const search = params.toString();
|
|
43
|
-
|
|
41
|
+
const query = useInfiniteQuery({
|
|
42
|
+
initialPageParam: undefined as string | undefined,
|
|
44
43
|
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "list", search],
|
|
45
|
-
queryFn: ({ signal }) =>
|
|
46
|
-
|
|
44
|
+
queryFn: ({ pageParam, signal }) => {
|
|
45
|
+
const pageParams = new URLSearchParams(search);
|
|
46
|
+
if (pageParam) pageParams.set("cursor", pageParam);
|
|
47
|
+
return fetchDashboardJson(
|
|
47
48
|
reviewItemListSchema,
|
|
48
|
-
`/api/review-items?${
|
|
49
|
+
`/api/review-items?${pageParams.toString()}`,
|
|
49
50
|
signal,
|
|
50
|
-
)
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
51
54
|
placeholderData: keepPreviousData,
|
|
52
55
|
retry: false,
|
|
53
56
|
});
|
|
57
|
+
const pages = query.data?.pages;
|
|
58
|
+
const items = useMemo(
|
|
59
|
+
() => (pages ?? []).flatMap((page) => page.items),
|
|
60
|
+
[pages],
|
|
61
|
+
);
|
|
62
|
+
return { items, moreArchived: Boolean(pages?.[0]?.moreArchived), query };
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/** Read one Review item, for a link to an item outside the current list. */
|
|
@@ -92,7 +101,7 @@ export function useLinkedConversationTitles(
|
|
|
92
101
|
}).map((query) => query.data ?? undefined);
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
/** Count the open
|
|
104
|
+
/** Count the open action items assigned to the viewer, for the nav. */
|
|
96
105
|
export function useReviewItemCountData(enabled: boolean) {
|
|
97
106
|
return useQuery({
|
|
98
107
|
enabled,
|
|
@@ -145,7 +154,7 @@ const NO_CONVERSATIONS: ReadonlySet<string> = new Set();
|
|
|
145
154
|
|
|
146
155
|
/**
|
|
147
156
|
* Return the Conversations that wait on the viewer. Today only the review
|
|
148
|
-
* desk sets this:
|
|
157
|
+
* desk sets this: a linked open action item assigned to the viewer.
|
|
149
158
|
*/
|
|
150
159
|
export function useWaitingConversationIds(): ReadonlySet<string> {
|
|
151
160
|
const query = useQuery({
|
|
@@ -303,7 +303,7 @@ export function createMockReportingApi(): Hono<{
|
|
|
303
303
|
jsonResponse(reviewItemListSchema, { items: [] }),
|
|
304
304
|
);
|
|
305
305
|
app.get("/review-items/count", () =>
|
|
306
|
-
jsonResponse(reviewItemCountSchema, {
|
|
306
|
+
jsonResponse(reviewItemCountSchema, { needsViewer: 0 }),
|
|
307
307
|
);
|
|
308
308
|
app.get("/review-items/waiting-conversations", () =>
|
|
309
309
|
jsonResponse(reviewItemWaitingConversationsSchema, { conversationIds: [] }),
|
package/src/tailwind.css
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
@import "shadcn/tailwind.css";
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
* Dashboard design tokens. components/
|
|
7
|
+
* Dashboard design tokens. components/AGENTS.md has the token rules.
|
|
8
8
|
* Keep structural UI colors here so a second theme can redefine them.
|
|
9
9
|
* Prefer roles over opacity ladders. Status accents stay as Tailwind palette utilities.
|
|
10
10
|
*/
|