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}