From b2090ec351d1e3c0c72308e61fc5fb326acc15e8 Mon Sep 17 00:00:00 2001
From: Dotta
Date: Tue, 16 Jun 2026 14:11:40 +0000
Subject: [PATCH] Show document annotation save failures
---
ui/src/components/DocumentAnnotationPanel.tsx | 44 ++++--
.../IssueDocumentAnnotations.test.tsx | 126 +++++++++++++++++-
2 files changed, 151 insertions(+), 19 deletions(-)
diff --git a/ui/src/components/DocumentAnnotationPanel.tsx b/ui/src/components/DocumentAnnotationPanel.tsx
index 4ffa4d4bef5..25903d11756 100644
--- a/ui/src/components/DocumentAnnotationPanel.tsx
+++ b/ui/src/components/DocumentAnnotationPanel.tsx
@@ -105,6 +105,7 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
const queryClient = useQueryClient();
const [composerValue, setComposerValue] = useState("");
const [replyDrafts, setReplyDrafts] = useState>({});
+ const [mutationError, setMutationError] = useState(null);
const composerRef = useRef(null);
const bodyTestId = props.isMobile ? "document-annotation-panel" : undefined;
const annotationTarget = useMemo(() => {
@@ -182,6 +183,7 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
onMutate: async (body: string) => {
const anchor = props.pendingAnchor;
if (!anchor || !props.baseRevisionId) return undefined;
+ setMutationError(null);
await queryClient.cancelQueries({ queryKey: annotationsQueryKey });
const previous = queryClient.getQueryData(annotationsQueryKey);
const optimisticThread = buildOptimisticThread({
@@ -199,16 +201,16 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
annotationsQueryKey,
(current) => [...(current ?? []), optimisticThread],
);
- // Clear the composer immediately — the optimistic thread now carries the text.
- props.onClearPendingAnchor();
- setComposerValue("");
props.onFocusThread(optimisticThread.id);
return { previous, optimisticId: optimisticThread.id };
},
- onError: (_error, _body, context) => {
+ onError: (error, _body, context) => {
if (context?.previous) {
queryClient.setQueryData(annotationsQueryKey, context.previous);
}
+ setMutationError(error instanceof Error && error.message
+ ? error.message
+ : "Failed to create comment.");
},
onSuccess: (thread, _body, context) => {
// Swap the optimistic placeholder for the real thread before refetch settles.
@@ -218,6 +220,9 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
entry.id === context?.optimisticId ? thread : entry,
),
);
+ props.onClearPendingAnchor();
+ setComposerValue("");
+ setMutationError(null);
props.onFocusThread(thread.id);
},
onSettled: () => invalidateAll(),
@@ -228,6 +233,7 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
documentAnnotationsApi.addCommentForTarget(annotationTarget, threadId, { body }),
// Optimistically append the reply so it stays on screen through the round-trip.
onMutate: async ({ threadId, body }) => {
+ setMutationError(null);
await queryClient.cancelQueries({ queryKey: annotationsQueryKey });
const previous = queryClient.getQueryData(annotationsQueryKey);
const optimisticComment = buildOptimisticComment({
@@ -244,13 +250,19 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
: thread,
),
);
- setReplyDrafts((current) => ({ ...current, [threadId]: "" }));
return { previous };
},
- onError: (_error, _variables, context) => {
+ onError: (error, _variables, context) => {
if (context?.previous) {
queryClient.setQueryData(annotationsQueryKey, context.previous);
}
+ setMutationError(error instanceof Error && error.message
+ ? error.message
+ : "Failed to add reply.");
+ },
+ onSuccess: (_comment, variables) => {
+ setReplyDrafts((current) => ({ ...current, [variables.threadId]: "" }));
+ setMutationError(null);
},
onSettled: () => invalidateAll(),
});
@@ -259,6 +271,7 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
mutationFn: ({ threadId, status }: { threadId: string; status: DocumentAnnotationThreadStatus }) =>
documentAnnotationsApi.updateStatusForTarget(annotationTarget, threadId, status),
onMutate: async ({ threadId, status }) => {
+ setMutationError(null);
await queryClient.cancelQueries({ queryKey: annotationsQueryKey });
const previous = queryClient.getQueryData(annotationsQueryKey);
queryClient.setQueryData(
@@ -269,11 +282,15 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
);
return { previous };
},
- onError: (_error, _variables, context) => {
+ onError: (error, _variables, context) => {
if (context?.previous) {
queryClient.setQueryData(annotationsQueryKey, context.previous);
}
+ setMutationError(error instanceof Error && error.message
+ ? error.message
+ : "Failed to update comment status.");
},
+ onSuccess: () => setMutationError(null),
onSettled: () => invalidateAll(),
});
@@ -336,6 +353,14 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
{props.newCommentDisabledReason}
) : null}
+ {mutationError ? (
+
+ {mutationError}
+
+ ) : null}
{visibleThreads.length === 0 ? null : (
@@ -409,11 +434,6 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
disabled={props.newCommentDisabled}
className="resize-y rounded-none text-sm"
/>
- {createThread.isError ? (
-
- {(createThread.error as Error).message || "Failed to create comment"}
-
- ) : null}