feat(comments): implement comment resolution for the community build

Add comment resolve/re-open as a community feature, written from scratch on top
of the infrastructure already present in the community codebase: the
resolved_at/resolved_by_id columns, the COMMENT_RESOLVED notification job, the
resolveCommentMark collaboration handler, the commentResolved websocket event,
the comment service/types and the Open/Resolved tabs. No Enterprise-Edition code
is reused and there is no EE feature gating — resolving is available to anyone
who can comment.

Backend:
- add POST /comments/resolve (ResolveCommentDto) guarded by validateCanComment;
  reject resolving replies
- add CommentService.resolveComment: set/clear resolvedAt/resolvedById, sync the
  inline comment mark via collaboration handleYjsEvent, queue
  COMMENT_RESOLVED_NOTIFICATION (only when another user resolves), emit the
  commentResolved websocket event and write a resolve/reopen audit log

Frontend:
- add useResolveCommentMutation with optimistic update + rollback
- add ResolveComment toggle button
- wire the resolve button and menu item into comment-list-item / comment-menu,
  gated on canComment for parent comments

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
vvzvlad
2026-06-16 23:31:03 +03:00
parent 0e069ddba0
commit c758a36dd2
7 changed files with 326 additions and 2 deletions
@@ -17,7 +17,10 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
import { QueueJob, QueueName } from '../../integrations/queue/constants';
import { extractUserMentionIdsFromJson } from '../../common/helpers/prosemirror/utils';
import { ICommentNotificationJob } from '../../integrations/queue/constants/queue.interface';
import {
ICommentNotificationJob,
ICommentResolvedNotificationJob,
} from '../../integrations/queue/constants/queue.interface';
import { WsService } from '../../ws/ws.service';
@Injectable()
@@ -206,6 +209,65 @@ export class CommentService {
return comment;
}
async resolveComment(
comment: Comment,
resolved: boolean,
authUser: User,
): Promise<Comment> {
const resolvedAt = resolved ? new Date() : null;
const resolvedById = resolved ? authUser.id : null;
await this.commentRepo.updateComment(
{ resolvedAt, resolvedById },
comment.id,
);
// Reflect the resolved state on the inline comment mark in the
// collaborative document so all connected clients stay in sync.
const documentName = `page.${comment.pageId}`;
try {
await this.collaborationGateway.handleYjsEvent(
'resolveCommentMark',
documentName,
{ commentId: comment.id, resolved, user: authUser },
);
} catch (error) {
this.logger.warn(
`Failed to update comment mark for comment ${comment.id}`,
error,
);
}
// Notify the comment author when someone else resolves their comment.
if (resolved && comment.creatorId !== authUser.id) {
const jobData: ICommentResolvedNotificationJob = {
commentId: comment.id,
commentCreatorId: comment.creatorId,
pageId: comment.pageId,
spaceId: comment.spaceId,
workspaceId: comment.workspaceId,
actorId: authUser.id,
};
await this.notificationQueue.add(
QueueJob.COMMENT_RESOLVED_NOTIFICATION,
jobData,
);
}
const updatedComment = await this.commentRepo.findById(comment.id, {
includeCreator: true,
includeResolvedBy: true,
});
this.wsService.emitCommentEvent(comment.spaceId, comment.pageId, {
operation: 'commentResolved',
pageId: comment.pageId,
comment: updatedComment,
});
return updatedComment;
}
private async queueCommentNotification(
content: any,
oldMentionIds: string[],