@sentry/junior-dashboard 0.267.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 +47 -8
- package/dist/client.js +74 -78
- package/dist/index.js +47 -8
- 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 +20 -4
- 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 +3 -1
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/button.tsx +11 -1
- package/src/client/conversations/AGENTS.md +42 -0
- package/src/client/conversations/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +4 -27
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +3 -0
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +142 -144
- package/src/client/conversations/turnContext.ts +15 -0
- package/src/client/format.ts +28 -28
- package/src/client/pages/SettingsPage.tsx +103 -3
- 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/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/pages/system/AGENTS.md +12 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +68 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +297 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +123 -0
- package/src/client/review-desk/queries.ts +173 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/mock-reporting/fixtures.ts +10 -0
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -215
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/UserInputRequestCard.tsx +0 -244
- 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
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { useEffect, useState, type FormEvent } from "react";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
USER_CUSTOMIZATION_MAX_CHARS,
|
|
6
|
+
userCustomizationSchema,
|
|
7
|
+
} from "@sentry/junior/api/schema";
|
|
4
8
|
import {
|
|
5
9
|
dashboardIdentitySchema,
|
|
6
10
|
type DashboardIdentity,
|
|
7
11
|
} from "../../api/schema";
|
|
8
12
|
import { Button } from "../components/ui/button";
|
|
9
13
|
import { Input } from "../components/ui/input";
|
|
14
|
+
import { Textarea } from "../components/ui/textarea";
|
|
10
15
|
import { Field, FieldDescription, FieldLabel } from "../components/ui/field";
|
|
11
16
|
import { InlineError } from "../components/InlineError";
|
|
12
17
|
import { Card } from "../components/layout/Card";
|
|
13
18
|
import { useDocumentTitle } from "../documentTitle";
|
|
14
|
-
import { patch } from "../http";
|
|
19
|
+
import { DashboardApiError, fetchDashboardJson, patch, put } from "../http";
|
|
15
20
|
import { dashboardContainerClass } from "../styles";
|
|
16
21
|
import type { DashboardCoreData } from "../types";
|
|
17
22
|
|
|
18
23
|
const dashboardCoreQueryKey = ["dashboard", "core"] as const;
|
|
24
|
+
const userCustomizationQueryKey = ["dashboard", "user-customization"] as const;
|
|
25
|
+
const USER_CUSTOMIZATION_PATH = "/api/user-customization";
|
|
19
26
|
|
|
20
27
|
type SettingsPageProps = {
|
|
21
28
|
identity: DashboardIdentity;
|
|
@@ -58,7 +65,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
|
|
|
58
65
|
<section className="mx-auto w-full max-w-3xl">
|
|
59
66
|
<h1 className="m-0 text-2xl font-bold">Settings</h1>
|
|
60
67
|
<p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
|
|
61
|
-
Manage
|
|
68
|
+
Manage your profile and preferences.
|
|
62
69
|
</p>
|
|
63
70
|
|
|
64
71
|
<form className="mt-6" onSubmit={submit}>
|
|
@@ -97,7 +104,100 @@ export function SettingsPage({ identity }: SettingsPageProps) {
|
|
|
97
104
|
</div>
|
|
98
105
|
</Card>
|
|
99
106
|
</form>
|
|
107
|
+
<CustomizationForm />
|
|
100
108
|
</section>
|
|
101
109
|
</div>
|
|
102
110
|
);
|
|
103
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Let the user save text that changes how the agent replies to them. The
|
|
115
|
+
* server returns 404 when the app does not enable this experiment.
|
|
116
|
+
*/
|
|
117
|
+
function CustomizationForm() {
|
|
118
|
+
const queryClient = useQueryClient();
|
|
119
|
+
const [text, setText] = useState("");
|
|
120
|
+
const customizationQuery = useQuery({
|
|
121
|
+
queryKey: userCustomizationQueryKey,
|
|
122
|
+
queryFn: ({ signal }) =>
|
|
123
|
+
fetchDashboardJson(
|
|
124
|
+
userCustomizationSchema,
|
|
125
|
+
USER_CUSTOMIZATION_PATH,
|
|
126
|
+
signal,
|
|
127
|
+
),
|
|
128
|
+
retry: false,
|
|
129
|
+
});
|
|
130
|
+
const savedText = customizationQuery.data?.customization;
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (savedText !== undefined) setText(savedText);
|
|
133
|
+
}, [savedText]);
|
|
134
|
+
const save = useMutation({
|
|
135
|
+
mutationFn: (customization: string) =>
|
|
136
|
+
put(userCustomizationSchema, USER_CUSTOMIZATION_PATH, { customization }),
|
|
137
|
+
onSuccess: (updated) => {
|
|
138
|
+
queryClient.setQueryData(userCustomizationQueryKey, updated);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
if (
|
|
142
|
+
customizationQuery.error instanceof DashboardApiError &&
|
|
143
|
+
customizationQuery.error.status === 404
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (savedText === undefined) {
|
|
148
|
+
return customizationQuery.isError ? (
|
|
149
|
+
<InlineError className="mt-6">
|
|
150
|
+
Could not load your customization.
|
|
151
|
+
</InlineError>
|
|
152
|
+
) : null;
|
|
153
|
+
}
|
|
154
|
+
const canSave =
|
|
155
|
+
text.trim() !== savedText &&
|
|
156
|
+
text.trim().length <= USER_CUSTOMIZATION_MAX_CHARS &&
|
|
157
|
+
!save.isPending;
|
|
158
|
+
|
|
159
|
+
function submit(event: FormEvent<HTMLFormElement>) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
if (canSave) save.mutate(text.trim());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<form className="mt-6" onSubmit={submit}>
|
|
166
|
+
<Card className="mb-0" padding="md" variant="raised">
|
|
167
|
+
<h2 className="m-0 text-lg font-bold">Customization</h2>
|
|
168
|
+
<Field className="mt-5">
|
|
169
|
+
<FieldLabel htmlFor="user-customization">
|
|
170
|
+
How should replies to you change?
|
|
171
|
+
</FieldLabel>
|
|
172
|
+
<Textarea
|
|
173
|
+
id="user-customization"
|
|
174
|
+
maxLength={USER_CUSTOMIZATION_MAX_CHARS}
|
|
175
|
+
onChange={(event) => {
|
|
176
|
+
setText(event.target.value);
|
|
177
|
+
if (save.isError || save.isSuccess) save.reset();
|
|
178
|
+
}}
|
|
179
|
+
placeholder="Keep replies short and simple."
|
|
180
|
+
value={text}
|
|
181
|
+
/>
|
|
182
|
+
<FieldDescription>
|
|
183
|
+
Added to each message you send. Do not put secrets here. (
|
|
184
|
+
{text.trim().length}/{USER_CUSTOMIZATION_MAX_CHARS})
|
|
185
|
+
</FieldDescription>
|
|
186
|
+
</Field>
|
|
187
|
+
<div className="mt-5 flex items-center gap-3">
|
|
188
|
+
<Button disabled={!canSave} type="submit">
|
|
189
|
+
{save.isPending ? "Saving…" : "Save customization"}
|
|
190
|
+
</Button>
|
|
191
|
+
{save.isSuccess ? (
|
|
192
|
+
<p className="m-0 text-sm text-emerald-300">Changes saved.</p>
|
|
193
|
+
) : null}
|
|
194
|
+
{save.isError ? (
|
|
195
|
+
<InlineError>
|
|
196
|
+
Could not save your customization. Try again.
|
|
197
|
+
</InlineError>
|
|
198
|
+
) : null}
|
|
199
|
+
</div>
|
|
200
|
+
</Card>
|
|
201
|
+
</form>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -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}
|
|
@@ -69,6 +69,7 @@ export function RecentConversationRow(props: { conversation: Conversation }) {
|
|
|
69
69
|
finishedSinceSeen={false}
|
|
70
70
|
isPrivate={conversation.visibility === "private"}
|
|
71
71
|
status={visualStatusForConversation(conversation)}
|
|
72
|
+
waitingOnViewer={false}
|
|
72
73
|
/>
|
|
73
74
|
</span>
|
|
74
75
|
<div className="min-w-0 truncate font-display text-sm text-dashboard-text">
|
|
@@ -18,6 +18,7 @@ import { Card } from "../../components/layout/Card";
|
|
|
18
18
|
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
|
|
19
19
|
import { buildConversations, getDashboardTimeZone } from "../../format";
|
|
20
20
|
import { useDebouncedValue } from "../../debouncedValue";
|
|
21
|
+
import { useWaitingConversationIds } from "../../review-desk/queries";
|
|
21
22
|
import { CodeActivityChart } from "./CodeActivityChart";
|
|
22
23
|
import { CodeChangeRow } from "./CodeChangeRow";
|
|
23
24
|
import { repositoryActivity } from "./RepositorySections";
|
|
@@ -32,6 +33,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
32
33
|
const search = useDebouncedValue(query.trim());
|
|
33
34
|
const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
|
|
34
35
|
const conversations = buildConversations(feed.data?.conversations ?? []);
|
|
36
|
+
const waitingConversationIds = useWaitingConversationIds();
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
setPage(1);
|
|
37
39
|
}, [search]);
|
|
@@ -59,6 +61,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
59
61
|
finishedConversationIds={noFinishedConversations}
|
|
60
62
|
loading={!feed.data}
|
|
61
63
|
timeZone={getDashboardTimeZone()}
|
|
64
|
+
waitingConversationIds={waitingConversationIds}
|
|
62
65
|
/>
|
|
63
66
|
)}
|
|
64
67
|
<PagePagination
|
|
@@ -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`.
|
package/src/client/primaryNav.ts
CHANGED
|
@@ -9,6 +9,7 @@ const AUTH_PRIMARY_NAV_PREFIXES = [
|
|
|
9
9
|
"/memories",
|
|
10
10
|
"/settings",
|
|
11
11
|
"/plugins/",
|
|
12
|
+
"/review",
|
|
12
13
|
] as const;
|
|
13
14
|
|
|
14
15
|
/** True when the path is only available to a signed-in viewer. */
|
|
@@ -24,6 +25,8 @@ export function buildPrimaryNavItems(input: {
|
|
|
24
25
|
loggedIn: boolean;
|
|
25
26
|
pathname: string;
|
|
26
27
|
primaryUserPages: PluginUserPageLink[];
|
|
28
|
+
/** Open Review items assigned to the viewer. */
|
|
29
|
+
reviewCount?: number;
|
|
27
30
|
userPagesPending: boolean;
|
|
28
31
|
}): DashboardHeaderNavItem[] {
|
|
29
32
|
const memoryNavPage = input.primaryUserPages.find(
|
|
@@ -32,8 +35,8 @@ export function buildPrimaryNavItems(input: {
|
|
|
32
35
|
const otherPrimaryUserPages = input.primaryUserPages.filter(
|
|
33
36
|
(page) => !(page.pluginName === "memory" && page.id === "memories"),
|
|
34
37
|
);
|
|
35
|
-
// Auth-only routes already imply the signed-in primary nav. Hold
|
|
36
|
-
// Memories slots while core/user-pages catch up so the header does not jump.
|
|
38
|
+
// Auth-only routes already imply the signed-in primary nav. Hold Review
|
|
39
|
+
// desk, Tasks, and Memories slots while core/user-pages catch up so the header does not jump.
|
|
37
40
|
const reserveAuthPrimaryNav =
|
|
38
41
|
input.loading && isAuthPrimaryNavPath(input.pathname);
|
|
39
42
|
const showTasksNav = input.loggedIn || reserveAuthPrimaryNav;
|
|
@@ -43,6 +46,16 @@ export function buildPrimaryNavItems(input: {
|
|
|
43
46
|
(input.loggedIn && input.userPagesPending);
|
|
44
47
|
|
|
45
48
|
return [
|
|
49
|
+
...(showTasksNav
|
|
50
|
+
? [
|
|
51
|
+
{
|
|
52
|
+
count: input.reviewCount,
|
|
53
|
+
key: "review",
|
|
54
|
+
label: "Review desk",
|
|
55
|
+
to: "/review",
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
46
59
|
{ key: "code", label: "Code", to: "/code" },
|
|
47
60
|
...(showTasksNav
|
|
48
61
|
? [{ key: "automations", label: "Automations", to: "/automations" }]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Drawer } from "../components/Drawer";
|
|
5
|
+
import { LoadMorePagination } from "../components/Pagination";
|
|
6
|
+
import { ReviewItemCard } from "./ReviewItemCard";
|
|
7
|
+
import { useReviewItemsData } from "./queries";
|
|
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`;
|
|
12
|
+
return count === 1 ? "1 open review item" : `${count} open review items`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show how many open Review items are linked to a Conversation, above its
|
|
17
|
+
* message box. The bar opens a side panel where the viewer can act on each.
|
|
18
|
+
*/
|
|
19
|
+
export function ConversationReviewBar(props: { conversationId: string }) {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const { items, query } = useReviewItemsData({
|
|
22
|
+
conversationId: props.conversationId,
|
|
23
|
+
});
|
|
24
|
+
if (items.length === 0) return null;
|
|
25
|
+
const more = Boolean(query.hasNextPage);
|
|
26
|
+
const label = itemCountLabel(items.length, more);
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<button
|
|
30
|
+
className="mb-2 flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] px-3 py-2 text-left font-sans text-sm text-dashboard-text transition-colors hover:bg-violet-300/[0.1]"
|
|
31
|
+
onClick={() => setOpen(true)}
|
|
32
|
+
type="button"
|
|
33
|
+
>
|
|
34
|
+
<span className="flex items-center gap-2">
|
|
35
|
+
<span
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
className="size-2 shrink-0 rounded-full bg-violet-300"
|
|
38
|
+
/>
|
|
39
|
+
{label}
|
|
40
|
+
</span>
|
|
41
|
+
<ChevronRight aria-hidden="true" size={16} />
|
|
42
|
+
</button>
|
|
43
|
+
{open ? (
|
|
44
|
+
<Drawer
|
|
45
|
+
closeLabel="Close review items"
|
|
46
|
+
header={
|
|
47
|
+
<h2 className="m-0 font-sans text-base font-semibold">{label}</h2>
|
|
48
|
+
}
|
|
49
|
+
label="Review items"
|
|
50
|
+
onClose={() => setOpen(false)}
|
|
51
|
+
openKey={props.conversationId}
|
|
52
|
+
width="narrow"
|
|
53
|
+
>
|
|
54
|
+
<div className="grid gap-3">
|
|
55
|
+
{items.map((item) => (
|
|
56
|
+
<ReviewItemCard item={item} key={item.id} />
|
|
57
|
+
))}
|
|
58
|
+
<LoadMorePagination
|
|
59
|
+
hasMore={more}
|
|
60
|
+
loading={query.isFetchingNextPage}
|
|
61
|
+
onLoadMore={() => void query.fetchNextPage()}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</Drawer>
|
|
65
|
+
) : null}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|