Add resolve to comment mark in editor (EE)

This commit is contained in:
Philipinho
2025-07-03 17:54:19 -07:00
parent e190945da8
commit 129aaaa375
5 changed files with 97 additions and 20 deletions

View File

@ -2,14 +2,21 @@ import { ActionIcon, Tooltip } from "@mantine/core";
import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
import { useResolveCommentMutation } from "@/ee/comment/queries/comment-query";
import { useTranslation } from "react-i18next";
import { Editor } from "@tiptap/react";
interface ResolveCommentProps {
editor: Editor;
commentId: string;
pageId: string;
resolvedAt?: Date;
}
function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps) {
function ResolveComment({
editor,
commentId,
pageId,
resolvedAt,
}: ResolveCommentProps) {
const { t } = useTranslation();
const resolveCommentMutation = useResolveCommentMutation();
@ -23,13 +30,19 @@ function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps)
pageId,
resolved: !isResolved,
});
if (editor) {
editor.commands.setCommentResolved(commentId, !isResolved);
}
//
} catch (error) {
console.error("Failed to toggle resolved state:", error);
}
};
return (
<Tooltip
<Tooltip
label={isResolved ? t("Re-Open comment") : t("Resolve comment")}
position="top"
>
@ -51,4 +64,4 @@ function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps)
);
}
export default ResolveComment;
export default ResolveComment;

View File

@ -113,6 +113,7 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) {
<div style={{ visibility: hovered ? "visible" : "hidden" }}>
{!comment.parentCommentId && isCloudEE && (
<ResolveComment
editor={editor}
commentId={comment.id}
pageId={comment.pageId}
resolvedAt={comment.resolvedAt}

View File

@ -1,10 +1,5 @@
import "@/features/editor/styles/index.css";
import React, {
useEffect,
useMemo,
useRef,
useState,
} from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs";
import {
@ -79,7 +74,7 @@ export default function PageEditor({
const [isLocalSynced, setLocalSynced] = useState(false);
const [isRemoteSynced, setRemoteSynced] = useState(false);
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
yjsConnectionStatusAtom,
yjsConnectionStatusAtom
);
const menuContainerRef = useRef(null);
const documentName = `page.${pageId}`;
@ -239,7 +234,7 @@ export default function PageEditor({
debouncedUpdateContent(editorJson);
},
},
[pageId, editable, remoteProvider],
[pageId, editable, remoteProvider]
);
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
@ -255,7 +250,12 @@ export default function PageEditor({
}, 3000);
const handleActiveCommentEvent = (event) => {
const { commentId } = event.detail;
const { commentId, resolved } = event.detail;
if (resolved) {
return;
}
setActiveCommentId(commentId);
setAsideState({ tab: "comments", isAsideOpen: true });
@ -272,7 +272,7 @@ export default function PageEditor({
return () => {
document.removeEventListener(
"ACTIVE_COMMENT_EVENT",
handleActiveCommentEvent,
handleActiveCommentEvent
);
};
}, []);

View File

@ -142,6 +142,11 @@
.comment-mark {
background: rgba(255, 215, 0, 0.14);
border-bottom: 2px solid rgb(166, 158, 12);
&.resolved {
background: none;
border-bottom: none;
}
}
.comment-highlight {
@ -187,7 +192,7 @@
mask-size: 100% 100%;
background-color: currentColor;
& -open {
&-open {
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M10 3v2H5v14h14v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm7.586 2H13V3h8v8h-2V6.414l-7 7L10.586 12z'/%3E%3C/svg%3E");
}

View File

@ -19,6 +19,7 @@ declare module "@tiptap/core" {
unsetCommentDecoration: () => ReturnType;
setComment: (commentId: string) => ReturnType;
unsetComment: (commentId: string) => ReturnType;
setCommentResolved: (commentId: string, resolved: boolean) => ReturnType;
};
}
}
@ -53,6 +54,17 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
};
},
},
resolved: {
default: false,
parseHTML: (element) => element.hasAttribute("data-resolved"),
renderHTML: (attributes) => {
if (!attributes.resolved) return {};
return {
"data-resolved": "true",
};
},
},
};
},
@ -60,9 +72,18 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
return [
{
tag: "span[data-comment-id]",
getAttrs: (el) =>
!!(el as HTMLSpanElement).getAttribute("data-comment-id")?.trim() &&
null,
getAttrs: (el) => {
const element = el as HTMLSpanElement;
const commentId = element.getAttribute("data-comment-id")?.trim();
const resolved = element.hasAttribute("data-resolved");
if (!commentId) return false;
return {
commentId,
resolved,
};
},
},
];
},
@ -87,7 +108,8 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
(commentId) =>
({ commands }) => {
if (!commentId) return false;
return commands.setMark(this.name, { commentId });
// Just add the new mark, do not remove existing ones
return commands.setMark(this.name, { commentId, resolved: false });
},
unsetComment:
(commentId) =>
@ -109,6 +131,33 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
}
});
return dispatch?.(tr);
},
setCommentResolved:
(commentId, resolved) =>
({ tr, dispatch }) => {
if (!commentId) return false;
tr.doc.descendants((node, pos) => {
const from = pos;
const to = pos + node.nodeSize;
const commentMark = node.marks.find(
(mark) =>
mark.type.name === this.name &&
mark.attrs.commentId === commentId,
);
if (commentMark) {
// Remove the existing mark and add a new one with updated resolved state
tr = tr.removeMark(from, to, commentMark);
tr = tr.addMark(from, to, this.type.create({
commentId: commentMark.attrs.commentId,
resolved: resolved,
}));
}
});
return dispatch?.(tr);
},
};
@ -116,13 +165,17 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
renderHTML({ HTMLAttributes }) {
const commentId = HTMLAttributes?.["data-comment-id"] || null;
const resolved = HTMLAttributes?.["data-resolved"] || false;
console.log("firstResolved", resolved);
if (typeof window === "undefined" || typeof document === "undefined") {
return [
"span",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: 'comment-mark',
class: resolved ? 'comment-mark resolved' : 'comment-mark',
"data-comment-id": commentId,
...(resolved && { "data-resolved": "true" }),
}),
0,
];
@ -134,6 +187,11 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
).forEach(([attr, val]) => elem.setAttribute(attr, val));
// Add resolved class if the comment is resolved
if (resolved) {
elem.classList.add('resolved');
}
elem.addEventListener("click", (e) => {
const selection = document.getSelection();
if (selection.type === "Range") return;
@ -141,7 +199,7 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
this.storage.activeCommentId = commentId;
const commentEventClick = new CustomEvent("ACTIVE_COMMENT_EVENT", {
bubbles: true,
detail: { commentId },
detail: { commentId, resolved },
});
elem.dispatchEvent(commentEventClick);