mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 14:52:05 +10:00
Add resolve to comment mark in editor (EE)
This commit is contained in:
@ -2,14 +2,21 @@ import { ActionIcon, Tooltip } from "@mantine/core";
|
|||||||
import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
|
import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
|
||||||
import { useResolveCommentMutation } from "@/ee/comment/queries/comment-query";
|
import { useResolveCommentMutation } from "@/ee/comment/queries/comment-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Editor } from "@tiptap/react";
|
||||||
|
|
||||||
interface ResolveCommentProps {
|
interface ResolveCommentProps {
|
||||||
|
editor: Editor;
|
||||||
commentId: string;
|
commentId: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
resolvedAt?: Date;
|
resolvedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps) {
|
function ResolveComment({
|
||||||
|
editor,
|
||||||
|
commentId,
|
||||||
|
pageId,
|
||||||
|
resolvedAt,
|
||||||
|
}: ResolveCommentProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const resolveCommentMutation = useResolveCommentMutation();
|
const resolveCommentMutation = useResolveCommentMutation();
|
||||||
|
|
||||||
@ -23,13 +30,19 @@ function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps)
|
|||||||
pageId,
|
pageId,
|
||||||
resolved: !isResolved,
|
resolved: !isResolved,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.commands.setCommentResolved(commentId, !isResolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to toggle resolved state:", error);
|
console.error("Failed to toggle resolved state:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={isResolved ? t("Re-Open comment") : t("Resolve comment")}
|
label={isResolved ? t("Re-Open comment") : t("Resolve comment")}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
@ -51,4 +64,4 @@ function ResolveComment({ commentId, pageId, resolvedAt }: ResolveCommentProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResolveComment;
|
export default ResolveComment;
|
||||||
|
|||||||
@ -113,6 +113,7 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) {
|
|||||||
<div style={{ visibility: hovered ? "visible" : "hidden" }}>
|
<div style={{ visibility: hovered ? "visible" : "hidden" }}>
|
||||||
{!comment.parentCommentId && isCloudEE && (
|
{!comment.parentCommentId && isCloudEE && (
|
||||||
<ResolveComment
|
<ResolveComment
|
||||||
|
editor={editor}
|
||||||
commentId={comment.id}
|
commentId={comment.id}
|
||||||
pageId={comment.pageId}
|
pageId={comment.pageId}
|
||||||
resolvedAt={comment.resolvedAt}
|
resolvedAt={comment.resolvedAt}
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
import "@/features/editor/styles/index.css";
|
import "@/features/editor/styles/index.css";
|
||||||
import React, {
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { IndexeddbPersistence } from "y-indexeddb";
|
import { IndexeddbPersistence } from "y-indexeddb";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import {
|
import {
|
||||||
@ -79,7 +74,7 @@ export default function PageEditor({
|
|||||||
const [isLocalSynced, setLocalSynced] = useState(false);
|
const [isLocalSynced, setLocalSynced] = useState(false);
|
||||||
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
||||||
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
||||||
yjsConnectionStatusAtom,
|
yjsConnectionStatusAtom
|
||||||
);
|
);
|
||||||
const menuContainerRef = useRef(null);
|
const menuContainerRef = useRef(null);
|
||||||
const documentName = `page.${pageId}`;
|
const documentName = `page.${pageId}`;
|
||||||
@ -239,7 +234,7 @@ export default function PageEditor({
|
|||||||
debouncedUpdateContent(editorJson);
|
debouncedUpdateContent(editorJson);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[pageId, editable, remoteProvider],
|
[pageId, editable, remoteProvider]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
|
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
|
||||||
@ -255,7 +250,12 @@ export default function PageEditor({
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
const handleActiveCommentEvent = (event) => {
|
const handleActiveCommentEvent = (event) => {
|
||||||
const { commentId } = event.detail;
|
const { commentId, resolved } = event.detail;
|
||||||
|
|
||||||
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setActiveCommentId(commentId);
|
setActiveCommentId(commentId);
|
||||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||||
|
|
||||||
@ -272,7 +272,7 @@ export default function PageEditor({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
"ACTIVE_COMMENT_EVENT",
|
"ACTIVE_COMMENT_EVENT",
|
||||||
handleActiveCommentEvent,
|
handleActiveCommentEvent
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -142,6 +142,11 @@
|
|||||||
.comment-mark {
|
.comment-mark {
|
||||||
background: rgba(255, 215, 0, 0.14);
|
background: rgba(255, 215, 0, 0.14);
|
||||||
border-bottom: 2px solid rgb(166, 158, 12);
|
border-bottom: 2px solid rgb(166, 158, 12);
|
||||||
|
|
||||||
|
&.resolved {
|
||||||
|
background: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-highlight {
|
.comment-highlight {
|
||||||
@ -187,7 +192,7 @@
|
|||||||
mask-size: 100% 100%;
|
mask-size: 100% 100%;
|
||||||
background-color: currentColor;
|
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");
|
--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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ declare module "@tiptap/core" {
|
|||||||
unsetCommentDecoration: () => ReturnType;
|
unsetCommentDecoration: () => ReturnType;
|
||||||
setComment: (commentId: string) => ReturnType;
|
setComment: (commentId: string) => ReturnType;
|
||||||
unsetComment: (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 [
|
return [
|
||||||
{
|
{
|
||||||
tag: "span[data-comment-id]",
|
tag: "span[data-comment-id]",
|
||||||
getAttrs: (el) =>
|
getAttrs: (el) => {
|
||||||
!!(el as HTMLSpanElement).getAttribute("data-comment-id")?.trim() &&
|
const element = el as HTMLSpanElement;
|
||||||
null,
|
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) =>
|
(commentId) =>
|
||||||
({ commands }) => {
|
({ commands }) => {
|
||||||
if (!commentId) return false;
|
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:
|
unsetComment:
|
||||||
(commentId) =>
|
(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);
|
return dispatch?.(tr);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -116,13 +165,17 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
|||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
const commentId = HTMLAttributes?.["data-comment-id"] || null;
|
const commentId = HTMLAttributes?.["data-comment-id"] || null;
|
||||||
|
const resolved = HTMLAttributes?.["data-resolved"] || false;
|
||||||
|
|
||||||
|
console.log("firstResolved", resolved);
|
||||||
|
|
||||||
if (typeof window === "undefined" || typeof document === "undefined") {
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
||||||
return [
|
return [
|
||||||
"span",
|
"span",
|
||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
class: 'comment-mark',
|
class: resolved ? 'comment-mark resolved' : 'comment-mark',
|
||||||
"data-comment-id": commentId,
|
"data-comment-id": commentId,
|
||||||
|
...(resolved && { "data-resolved": "true" }),
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
];
|
];
|
||||||
@ -134,6 +187,11 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
|||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||||
).forEach(([attr, val]) => elem.setAttribute(attr, val));
|
).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) => {
|
elem.addEventListener("click", (e) => {
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
if (selection.type === "Range") return;
|
if (selection.type === "Range") return;
|
||||||
@ -141,7 +199,7 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
|||||||
this.storage.activeCommentId = commentId;
|
this.storage.activeCommentId = commentId;
|
||||||
const commentEventClick = new CustomEvent("ACTIVE_COMMENT_EVENT", {
|
const commentEventClick = new CustomEvent("ACTIVE_COMMENT_EVENT", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { commentId },
|
detail: { commentId, resolved },
|
||||||
});
|
});
|
||||||
|
|
||||||
elem.dispatchEvent(commentEventClick);
|
elem.dispatchEvent(commentEventClick);
|
||||||
|
|||||||
Reference in New Issue
Block a user