From 327eadb45cc65cc85be4643d68ae19403ac7e7a9 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 9 Apr 2026 06:12:43 -0500 Subject: [PATCH] fix(ui): harden issue comment editor sync --- ui/src/components/MarkdownEditor.tsx | 15 ++++++++- ui/src/lib/optimistic-issue-comments.test.ts | 35 ++++++++++++++++++++ ui/src/lib/optimistic-issue-comments.ts | 8 +++++ ui/src/pages/IssueDetail.tsx | 3 +- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 0151400f..2ed825b5 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -364,6 +364,19 @@ export const MarkdownEditor = forwardRef return map; }, [mentions]); + const setEditorRef = useCallback((instance: MDXEditorMethods | null) => { + ref.current = instance; + if (!instance) { + return; + } + if (valueRef.current !== latestValueRef.current) { + // Re-apply the latest controlled value once MDXEditor exposes its imperative API. + echoIgnoreMarkdownRef.current = valueRef.current; + instance.setMarkdown(valueRef.current); + latestValueRef.current = valueRef.current; + } + }, []); + const filteredMentions = useMemo(() => { if (!mentionState) return []; const q = mentionState.query.trim().toLowerCase(); @@ -798,7 +811,7 @@ export const MarkdownEditor = forwardRef onPasteCapture={handlePasteCapture} > { diff --git a/ui/src/lib/optimistic-issue-comments.test.ts b/ui/src/lib/optimistic-issue-comments.test.ts index 2499ce9b..bf0cdda5 100644 --- a/ui/src/lib/optimistic-issue-comments.test.ts +++ b/ui/src/lib/optimistic-issue-comments.test.ts @@ -6,6 +6,7 @@ import { applyOptimisticIssueCommentUpdate, createOptimisticIssueComment, flattenIssueCommentPages, + getNextIssueCommentPageParam, isQueuedIssueComment, matchesIssueRef, mergeIssueComments, @@ -171,6 +172,40 @@ describe("optimistic issue comments", () => { expect(flattened.map((comment) => comment.id)).toEqual(["comment-1", "comment-2", "comment-3"]); }); + it("returns no next page param when the last page is missing", () => { + expect(getNextIssueCommentPageParam(undefined, 50)).toBeUndefined(); + }); + + it("returns the oldest id when the last page is full", () => { + expect( + getNextIssueCommentPageParam( + [ + { + id: "comment-2", + companyId: "company-1", + issueId: "issue-1", + authorAgentId: null, + authorUserId: "board-1", + body: "Second", + createdAt: new Date("2026-03-28T14:00:02.000Z"), + updatedAt: new Date("2026-03-28T14:00:02.000Z"), + }, + { + id: "comment-1", + companyId: "company-1", + issueId: "issue-1", + authorAgentId: null, + authorUserId: "board-1", + body: "First", + createdAt: new Date("2026-03-28T14:00:01.000Z"), + updatedAt: new Date("2026-03-28T14:00:01.000Z"), + }, + ], + 2, + ), + ).toBe("comment-1"); + }); + it("upserts paged comments without dropping older pages", () => { const nextPages = upsertIssueCommentInPages( [ diff --git a/ui/src/lib/optimistic-issue-comments.ts b/ui/src/lib/optimistic-issue-comments.ts index c7237bf8..73d9aea8 100644 --- a/ui/src/lib/optimistic-issue-comments.ts +++ b/ui/src/lib/optimistic-issue-comments.ts @@ -102,6 +102,14 @@ export function flattenIssueCommentPages( return sortIssueComments((pages ?? []).flatMap((page) => page)); } +export function getNextIssueCommentPageParam( + lastPage: ReadonlyArray | undefined, + pageSize: number, +): string | undefined { + if (!lastPage || lastPage.length < pageSize) return undefined; + return lastPage[lastPage.length - 1]?.id; +} + export function upsertIssueComment( comments: IssueComment[] | undefined, nextComment: IssueComment, diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 5741b40b..73407936 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -36,6 +36,7 @@ import { applyOptimisticIssueCommentUpdate, createOptimisticIssueComment, flattenIssueCommentPages, + getNextIssueCommentPageParam, isQueuedIssueComment, matchesIssueRef, mergeIssueComments, @@ -416,7 +417,7 @@ export function IssueDetail() { enabled: !!issueId, initialPageParam: null as string | null, getNextPageParam: (lastPage) => - lastPage.length === ISSUE_COMMENT_PAGE_SIZE ? lastPage[lastPage.length - 1]?.id : undefined, + getNextIssueCommentPageParam(lastPage, ISSUE_COMMENT_PAGE_SIZE), placeholderData: keepPreviousData, }); const comments = useMemo(