From ee9c5724229a3b5ecaa4da030eda9144dd516808 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:45:55 +0000 Subject: [PATCH 1/3] fix(dashboard): Remove queued messages individually Co-Authored-By: David Cramer --- .../client/conversations/ConversationPage.tsx | 18 ++- .../conversations/PendingMailboxStack.tsx | 118 ++++++++++++------ .../tests/pending-mailbox-stack.test.tsx | 30 ++--- 3 files changed, 100 insertions(+), 66 deletions(-) diff --git a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx index 6b1e25379a..20083ef902 100644 --- a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx +++ b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx @@ -294,22 +294,18 @@ function ConversationReplyFooter(props: { const cancellableMessageIds = props.pendingMessages .filter((message) => message.clientStatus === undefined) .map((message) => message.inboundMessageId); - const cancellableMessageIdsRef = useRef(cancellableMessageIds); - cancellableMessageIdsRef.current = cancellableMessageIds; - const hasSendingOutboxMessage = props.pendingMessages.some( - (message) => message.clientStatus === "sending", - ); const pendingGeneratedAtRef = useRef(props.pendingGeneratedAt); pendingGeneratedAtRef.current = props.pendingGeneratedAt; - const onCancelQueue = useCallback(() => { - const inboundMessageIds = cancellableMessageIdsRef.current; + const onCancelMessage = useCallback((message: ConversationMailboxMessage) => { const receivedBefore = pendingGeneratedAtRef.current; - if (!receivedBefore || inboundMessageIds.length === 0) return; + if (!receivedBefore) return; cancelPendingMessagesRef.current.mutate({ - inboundMessageIds, + inboundMessageIds: [message.inboundMessageId], receivedBefore, }); }, []); + const cancelTargetInboundMessageId = + cancelPendingMessages.variables?.inboundMessageIds[0]; const cancelError = Boolean( cancelPendingMessages.error && cancelPendingMessages.variables?.inboundMessageIds.some((id) => @@ -336,15 +332,15 @@ function ConversationReplyFooter(props: {
void; showSlack: boolean; }) { const delivery = pendingDeliveryMeta(props.message); @@ -78,13 +82,45 @@ function PendingMetaIcons(props: { strokeWidth={2.2} /> + {props.onCancel ? ( + + + + ) : null} ); } function PendingRow(props: { + cancelDisabled: boolean; + cancelError: boolean; + cancelPending: boolean; conversation: ConversationTranscript; message: ConversationMailboxMessage; + onCancel?(message: ConversationMailboxMessage): void; onRetry?(message: ConversationMailboxMessage): void; }) { const text = props.message.text ?? ""; @@ -119,7 +155,16 @@ function PendingRow(props: { } leftClassName="text-sm leading-snug text-dashboard-text" right={ - + props.onCancel?.(props.message) : undefined + } + showSlack={showSlack} + /> } /> {redacted ? ( @@ -150,8 +195,6 @@ function PendingRow(props: { } function ExpandQueuedMessagesButton(props: { - /** When the cancel bar already shows the total count, avoid repeating it on mobile. */ - countShownInCancelBar: boolean; expanded: boolean; hiddenCount: number; onClick(): void; @@ -166,10 +209,7 @@ function ExpandQueuedMessagesButton(props: { ? `${props.hiddenCount} more queued messages` : totalLabel; // Mobile collapses previews and uses the total count as the expand control. - // When cancel already owns that count, keep a distinct expand action label. - const mobileCollapsedLabel = props.countShownInCancelBar - ? "Show queued messages" - : totalLabel; + const mobileCollapsedLabel = totalLabel; const label = props.expanded ? "Show fewer queued messages" : moreLabel; return ( @@ -195,9 +235,10 @@ function ExpandQueuedMessagesButton(props: { export function PendingMailboxStack(props: { cancelError?: boolean; cancelPending?: boolean; + cancelTargetInboundMessageId?: string; conversation: ConversationTranscript; messages: readonly ConversationMailboxMessage[]; - onCancelQueue?: () => void; + onCancelMessage?: (message: ConversationMailboxMessage) => void; onRetry?(message: ConversationMailboxMessage): void; }): ReactNode { const [expanded, setExpanded] = useState(false); @@ -219,50 +260,34 @@ export function PendingMailboxStack(props: { const visibleRows = showCollapsed ? previewRows : rows; const hiddenCount = Math.max(0, rows.length - COLLAPSED_PENDING_ROW_COUNT); const toggleExpanded = () => setExpanded((value) => !value); - const cancellableCount = rows.filter( - (message) => message.clientStatus === undefined, - ).length; - const hasSendingRow = rows.some( - (message) => message.clientStatus === "sending", - ); - const showCancel = - cancellableCount > 0 && !hasSendingRow && Boolean(props.onCancelQueue); - const countLabel = - rows.length === 1 ? "1 queued message" : `${rows.length} queued messages`; return (
- {showCancel ? ( -
-
- {countLabel} -
- -
- ) : null} - {showCancel && props.cancelError ? ( -
- Could not cancel queued messages. Try again. -
- ) : null} {showCollapsed ? ( // Desktop keeps a two-row preview; mobile collapses to the control only.
{previewRows.map((message, index) => ( ))} @@ -270,16 +295,29 @@ export function PendingMailboxStack(props: { ) : ( visibleRows.map((message, index) => ( )) )} {canCollapse ? ( { - it("shows cancel only when an accepted mailbox row exists", () => { +describe("PendingMailboxStack remove control", () => { + it("shows remove only for an accepted mailbox row", () => { const accepted = renderToStaticMarkup( undefined} + onCancelMessage={() => undefined} />, ); const localOnly = renderToStaticMarkup( @@ -59,15 +59,15 @@ describe("PendingMailboxStack cancel control", () => { messageId: "client:1", }), ]} - onCancelQueue={() => undefined} + onCancelMessage={() => undefined} />, ); - expect(accepted).toContain("Cancel queue"); - expect(localOnly).not.toContain("Cancel queue"); + expect(accepted).toContain("Remove queued message"); + expect(localOnly).not.toContain("Remove queued message"); }); - it("hides cancel while a local send can still become accepted", () => { + it("keeps remove available for accepted rows while a local send is pending", () => { const html = renderToStaticMarkup( { messageId: "client:2", }), ]} - onCancelQueue={() => undefined} + onCancelMessage={() => undefined} />, ); - expect(html).not.toContain("Cancel queue"); + expect(html.match(/aria-label="Remove queued message"/g)).toHaveLength(1); }); - it("hides a stale cancel error when only local outbox rows remain", () => { + it("does not show remove for failed local outbox rows", () => { const html = renderToStaticMarkup( { messageId: "client:retry-1", }), ]} - onCancelQueue={() => undefined} + onCancelMessage={() => undefined} />, ); - expect(html).not.toContain("Could not cancel queued messages"); + expect(html).not.toContain("Remove queued message"); }); - it("does not repeat the queue count on the mobile expand control when cancel shows it", () => { + it("uses the queue count on the mobile expand control", () => { const messages = Array.from({ length: 5 }, (_, index) => message({ inboundMessageId: `accepted-${index + 1}`, @@ -118,12 +118,12 @@ describe("PendingMailboxStack cancel control", () => { undefined} + onCancelMessage={() => undefined} />, ); expect(html).toContain("5 queued messages"); - expect(html).toContain("Show queued messages"); + expect(html).not.toContain("Show queued messages"); expect(html.match(/5 queued messages/g)).toHaveLength(1); }); }); From ce896a1301198a978681341003f78d3e8b150fa9 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:57:33 +0000 Subject: [PATCH 2/3] test(dashboard): Update mobile queue expand label Match the collapsed expand control to the total queue count after removing the cancel-queue bar. --- packages/junior-dashboard/e2e/conversations.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/junior-dashboard/e2e/conversations.spec.ts b/packages/junior-dashboard/e2e/conversations.spec.ts index c8a0fcf129..3d82f8b42c 100644 --- a/packages/junior-dashboard/e2e/conversations.spec.ts +++ b/packages/junior-dashboard/e2e/conversations.spec.ts @@ -428,11 +428,11 @@ test("opens and closes a conversation in the mobile workspace", async ({ const pending = page.getByLabel("Pending messages"); await expect(pending).toBeVisible(); - // Cancel bar owns the total count on mobile; expand uses a distinct label. - await expect(pending.getByText("5 queued messages")).toHaveCount(1); - const expand = pending.getByRole("button", { name: "Show queued messages" }); + // Mobile collapsed control owns the total count (no separate cancel bar). + const expand = pending.getByRole("button", { name: "5 queued messages" }); await expect(expand).toBeVisible(); await expect(expand).toHaveAttribute("aria-expanded", "false"); + await expect(pending.getByText("5 queued messages")).toHaveCount(1); await expect( pending.getByText("Also check the canary traffic from the last deploy."), ).toBeHidden(); From 599a2ad7868b350fb5a882921693a4776435a0d2 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:05:14 +0000 Subject: [PATCH 3/3] fix(dashboard): Surface queue remove errors on mobile Show an inline remove failure on the target row so mobile can see it without tooltips, and keep in-flight remove state when send starts. --- .../client/conversations/ConversationPage.tsx | 5 ++++- .../conversations/PendingMailboxStack.tsx | 20 ++++++++++--------- .../tests/pending-mailbox-stack.test.tsx | 16 +++++++++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx index 20083ef902..a33c6ee7db 100644 --- a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx +++ b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx @@ -288,7 +288,10 @@ function ConversationReplyFooter(props: { onPinRequestRef.current(); }, []); const onSubmitStart = useCallback(() => { - cancelPendingMessagesRef.current.reset(); + // Keep an in-flight remove intact so optimistic cache rollback stays coherent. + if (!cancelPendingMessagesRef.current.isPending) { + cancelPendingMessagesRef.current.reset(); + } onPinRequestRef.current(); }, []); const cancellableMessageIds = props.pendingMessages diff --git a/packages/junior-dashboard/src/client/conversations/PendingMailboxStack.tsx b/packages/junior-dashboard/src/client/conversations/PendingMailboxStack.tsx index c4f8b2592b..7c24e854e4 100644 --- a/packages/junior-dashboard/src/client/conversations/PendingMailboxStack.tsx +++ b/packages/junior-dashboard/src/client/conversations/PendingMailboxStack.tsx @@ -83,16 +83,13 @@ function PendingMetaIcons(props: { /> {props.onCancel ? ( - +
) : null} + {props.cancelError ? ( +

+ Could not remove. Try again. +

+ ) : null} ); } diff --git a/packages/junior-dashboard/tests/pending-mailbox-stack.test.tsx b/packages/junior-dashboard/tests/pending-mailbox-stack.test.tsx index b094e83c0f..e39b4b488c 100644 --- a/packages/junior-dashboard/tests/pending-mailbox-stack.test.tsx +++ b/packages/junior-dashboard/tests/pending-mailbox-stack.test.tsx @@ -104,6 +104,22 @@ describe("PendingMailboxStack remove control", () => { ); expect(html).not.toContain("Remove queued message"); + expect(html).not.toContain("Could not remove. Try again."); + }); + + it("shows an inline remove error on the target accepted row", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html).toContain("Could not remove. Try again."); + expect(html).toContain('aria-label="Could not remove. Try again."'); }); it("uses the queue count on the mobile expand control", () => {