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>
84 lines
2.1 KiB
TypeScript
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;
|