Files
gitmost/apps/client/src/features/comment/components/comment-menu.tsx
vvzvlad c758a36dd2 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>
2026-06-16 23:38:15 +03:00

84 lines
2.1 KiB
TypeScript

import { ActionIcon, Menu } from "@mantine/core";
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();
//@ts-ignore
const openDeleteModal = () =>
modals.openConfirmModal({
title: t("Are you sure you want to delete this comment?"),
centered: true,
labels: { confirm: t("Delete"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: onDeleteComment,
});
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon
variant="default"
style={{ border: "none" }}
aria-label={t("Comment menu")}
>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{canEdit && (
<Menu.Item
onClick={onEditComment}
leftSection={<IconEdit size={14} />}
>
{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}
>
{t("Delete comment")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}
export default CommentMenu;