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
@@ -1,18 +1,26 @@
import { ActionIcon, Menu } from "@mantine/core";
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
import { IconDots, IconEdit, IconTrash, IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useTranslation } from "react-i18next";
type CommentMenuProps = {
onEditComment: () => void;
onDeleteComment: () => void;
onResolveComment?: () => void;
canEdit?: boolean;
canComment?: boolean;
isResolved?: boolean;
isParentComment?: boolean;
};
function CommentMenu({
onEditComment,
onDeleteComment,
onResolveComment,
canEdit = true,
canComment = true,
isResolved = false,
isParentComment = false,
}: CommentMenuProps) {
const { t } = useTranslation();
@@ -47,6 +55,20 @@ function CommentMenu({
{t("Edit comment")}
</Menu.Item>
)}
{isParentComment && canComment && (
<Menu.Item
onClick={onResolveComment}
leftSection={
isResolved ? (
<IconCircleCheckFilled size={14} />
) : (
<IconCircleCheck size={14} />
)
}
>
{isResolved ? t("Re-open comment") : t("Resolve comment")}
</Menu.Item>
)}
<Menu.Item
leftSection={<IconTrash size={14} />}
onClick={openDeleteModal}