From 404f445d19f986395d4a0dc290340baee8b02861 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 2 Mar 2026 08:22:51 -0300 Subject: [PATCH 1/3] fix(comments): emit empty comment positions so undo clears orphan bubbles PresentationEditor skipped emission when positions were empty, so undoing the last tracked-change mark never signaled the store to clear stale data. Also fix getFloatingComments to use isEditorBackedComment() so tracked-change comments (which lack selection.source) require live positions like editor comments. --- .../presentation-editor/PresentationEditor.ts | 8 +++----- .../superdoc/src/stores/comments-store.js | 9 +++++---- .../src/stores/comments-store.test.js | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts index 22157c3a1b..3baaf3d5f4 100644 --- a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts @@ -3524,14 +3524,12 @@ export class PresentationEditor extends EventEmitter { this.emit('paginationUpdate', payload); // Emit fresh comment positions after layout completes. - // This ensures positions are always in sync with the current document and layout. + // Always emit — even when empty — so the store can clear stale positions + // (e.g. when undo removes the last tracked-change mark). const allowViewingCommentPositions = this.#layoutOptions.emitCommentPositionsInViewing === true; if (this.#documentMode !== 'viewing' || allowViewingCommentPositions) { const commentPositions = this.#collectCommentPositions(); - const positionKeys = Object.keys(commentPositions); - if (positionKeys.length > 0) { - this.emit('commentPositions', { positions: commentPositions }); - } + this.emit('commentPositions', { positions: commentPositions }); } this.#selectionSync.requestRender({ immediate: true }); diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index f05d048287..e2e5ab6954 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -876,11 +876,12 @@ export const useCommentsStore = defineStore('comments', () => { const comments = getGroupedComments.value?.parentComments .filter((c) => !c.resolvedTime) .filter((c) => { - const keys = Object.keys(editorCommentPositions.value); - const isPdfComment = c.selection?.source !== 'super-editor'; - if (isPdfComment) return true; + // Non-editor comments (e.g. PDF) are always shown. + // Editor-backed comments (including tracked changes, which have no + // selection.source) must have a live position in the document. + if (!isEditorBackedComment(c)) return true; const commentKey = c.commentId || c.importedId; - return keys.includes(commentKey); + return Object.keys(editorCommentPositions.value).includes(commentKey); }); return comments; }); diff --git a/packages/superdoc/src/stores/comments-store.test.js b/packages/superdoc/src/stores/comments-store.test.js index d3de3f69e1..c7e8b23425 100644 --- a/packages/superdoc/src/stores/comments-store.test.js +++ b/packages/superdoc/src/stores/comments-store.test.js @@ -946,5 +946,24 @@ describe('comments-store', () => { const floating = store.getFloatingComments; expect(floating).toEqual([]); }); + + it('excludes unresolved tracked change when positions are cleared (regression: SD-2071)', () => { + store.commentsList = [ + { commentId: 'tc-1', trackedChange: true, resolvedTime: null, createdTime: 1, selection: {} }, + ]; + // Undo removed the mark — positions are now empty + store.editorCommentPositions = {}; + + const floating = store.getFloatingComments; + expect(floating).toEqual([]); + }); + + it('keeps PDF comments visible when editor positions are empty (SD-2071)', () => { + store.commentsList = [{ commentId: 'pdf-1', createdTime: 1, selection: { source: 'pdf', selectionBounds: {} } }]; + store.editorCommentPositions = {}; + + const floating = store.getFloatingComments; + expect(floating.map((c) => c.commentId)).toEqual(['pdf-1']); + }); }); }); From 3975f6edc4445c154fb49501737ca6d0b5bf8045 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 2 Mar 2026 08:58:49 -0300 Subject: [PATCH 2/3] fix(comments): use `in` operator for position key lookup Avoids rebuilding Object.keys() array per comment in getFloatingComments filter. --- packages/superdoc/src/stores/comments-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index e2e5ab6954..577a613494 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -881,7 +881,7 @@ export const useCommentsStore = defineStore('comments', () => { // selection.source) must have a live position in the document. if (!isEditorBackedComment(c)) return true; const commentKey = c.commentId || c.importedId; - return Object.keys(editorCommentPositions.value).includes(commentKey); + return commentKey in editorCommentPositions.value; }); return comments; }); From bdb3a2fa961c8ea52b62e17ac0a62cb1e34f97b4 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 2 Mar 2026 09:59:12 -0300 Subject: [PATCH 3/3] fix(tests): wait for dialog deactivation before re-activating in collapse test Firefox needs the active dialog to fully unmount before re-activation, otherwise the thread renders expanded instead of collapsed. --- .../behavior/tests/comments/comment-thread-collapse.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/behavior/tests/comments/comment-thread-collapse.spec.ts b/tests/behavior/tests/comments/comment-thread-collapse.spec.ts index 168291d957..5dd3082c6f 100644 --- a/tests/behavior/tests/comments/comment-thread-collapse.spec.ts +++ b/tests/behavior/tests/comments/comment-thread-collapse.spec.ts @@ -42,6 +42,11 @@ test('thread with 2+ replies collapses and expands on click', async ({ superdoc }); await superdoc.waitForStable(); + // Wait for dialog to lose active state before re-activating — Firefox needs + // this gap so the component fully unmounts its expanded state. + const activeDialog = superdoc.page.locator('.comment-placeholder .comments-dialog.is-active'); + await expect(activeDialog).toHaveCount(0, { timeout: 5_000 }); + // Activate the comment dialog const dialog = await activateCommentDialog(superdoc, 'collapse');