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..577a613494 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 commentKey in editorCommentPositions.value; }); 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']); + }); }); }); 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');