Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/superdoc/src/core/collaboration/collaboration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,60 @@ describe('collaboration helpers', () => {
expect(superdoc.provider.on).not.toHaveBeenCalled();
});

it('initCollaborationComments emits comments-update for new comments added via collaboration', () => {
// Mock useComment to return an object with getValues
useCommentMock.mockImplementation((comment) => ({
commentId: comment.commentId,
normalized: comment.commentId,
getValues: () => ({ commentId: comment.commentId, text: comment.text }),
}));

// Start with one existing comment
commentsArray.items = [new MockYMap(Object.entries({ commentId: 'existing-1', text: 'Existing' }))];
initCollaborationComments(superdoc);

// Verify existing comment loaded
expect(superdoc.commentsStore.commentsList).toHaveLength(1);
expect(superdoc.emit).not.toHaveBeenCalled();

// Simulate remote user adding a new comment
commentsArray.items = [
new MockYMap(Object.entries({ commentId: 'existing-1', text: 'Existing' })),
new MockYMap(Object.entries({ commentId: 'new-from-agent', text: 'Agent comment' })),
];

const event = {
transaction: { origin: { user: { name: 'Agent', email: 'agent@example.com' } } },
};
commentsArray.emit(event);

// Should emit comments-update for the new comment
expect(superdoc.emit).toHaveBeenCalledWith('comments-update', {
type: 'add',
comment: expect.objectContaining({ commentId: 'new-from-agent' }),
});
});

it('initCollaborationComments does not emit for comments that already exist', () => {
useCommentMock.mockImplementation((comment) => ({
commentId: comment.commentId,
normalized: comment.commentId,
getValues: () => ({ commentId: comment.commentId, text: comment.text }),
}));

commentsArray.items = [new MockYMap(Object.entries({ commentId: 'c1', text: 'Comment 1' }))];
initCollaborationComments(superdoc);

// Simulate update event that doesn't add new comments
const event = {
transaction: { origin: { user: { name: 'Other', email: 'other@example.com' } } },
};
commentsArray.emit(event);

// Should not emit since no new comments were added
expect(superdoc.emit).not.toHaveBeenCalled();
});

it('initSuperdocYdoc delegates to createProvider with derived document id', () => {
const mockProvider = { provider: 'p', ydoc: 'y' };
const spy = vi.spyOn(collaborationModule, 'createProvider').mockReturnValue(mockProvider);
Expand Down
21 changes: 21 additions & 0 deletions packages/superdoc/src/core/collaboration/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createProvider } from '../collaboration/collaboration';
import useComment from '../../components/CommentsLayer/use-comment';
import { comments_module_events } from '@superdoc/common';

import { addYComment, updateYComment, deleteYComment } from './collaboration-comments';

Expand Down Expand Up @@ -99,8 +100,28 @@ export const initCollaborationComments = (superdoc) => {

if (currentUser.name === user.name && currentUser.email === user.email) return;

// Capture existing comment IDs before loading new state
const existingIds = new Set(superdoc.commentsStore.commentsList?.map((c) => c.commentId || c.importedId) || []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the loader and this code disagree on which id wins when a comment has both. loader (line 19) prefers importedId, here commentId wins. so if an imported comment later gets a commentId, or two people import the same docx and pick different commentIds, this code treats it as new and fires add for a comment that's already there. fix: match the loader at lines 104 and 112.

Suggested change
const existingIds = new Set(superdoc.commentsStore.commentsList?.map((c) => c.commentId || c.importedId) || []);
const existingIds = new Set(superdoc.commentsStore.commentsList?.map((c) => c.importedId ?? c.commentId) || []);


// Update conversations
updateCommentsStore();

// Emit events for comments added via collaboration
// This allows consumers to react to remotely-added comments (e.g., scroll to them)
const newComments = superdoc.commentsStore.commentsList?.filter((c) => {
const id = c.commentId || c.importedId;
return id && !existingIds.has(id);
});

if (newComments?.length && superdoc.emit) {
newComments.forEach((comment) => {
const commentValues = typeof comment.getValues === 'function' ? comment.getValues() : comment;
superdoc.emit('comments-update', {
type: comments_module_events.ADD,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments-store.js emits 'add', CommentsLayer.vue emits 'new' for UI creation. does the listener that scrolls to remote comments use 'add'? if it keys off 'new', it won't fire.

comment: commentValues,
});
});
}
});
};

Expand Down
Loading