diff --git a/apps/client/src/features/comment/components/comment-list-item.tsx b/apps/client/src/features/comment/components/comment-list-item.tsx index 41b2f659..b34dbd35 100644 --- a/apps/client/src/features/comment/components/comment-list-item.tsx +++ b/apps/client/src/features/comment/components/comment-list-item.tsx @@ -23,9 +23,14 @@ import { useTranslation } from "react-i18next"; interface CommentListItemProps { comment: IComment; pageId: string; + canComment: boolean; } -function CommentListItem({ comment, pageId }: CommentListItemProps) { +function CommentListItem({ + comment, + pageId, + canComment, +}: CommentListItemProps) { const { t } = useTranslation(); const { hovered, ref } = useHover(); const [isEditing, setIsEditing] = useState(false); @@ -39,7 +44,7 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) { const isCloudEE = useIsCloudEE(); useEffect(() => { - setContent(comment.content) + setContent(comment.content); }, [comment]); async function handleUpdateComment() { @@ -78,7 +83,9 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) { } function handleCommentClick(comment: IComment) { - const el = document.querySelector(`.comment-mark[data-comment-id="${comment.id}"]`); + const el = document.querySelector( + `.comment-mark[data-comment-id="${comment.id}"]`, + ); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("comment-highlight"); @@ -111,12 +118,12 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) {
- {!comment.parentCommentId && isCloudEE && ( - )} @@ -139,7 +146,10 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) {
{!comment.parentCommentId && comment?.selection && ( - handleCommentClick(comment)}> + handleCommentClick(comment)} + > {comment?.selection} )} diff --git a/apps/client/src/features/comment/components/comment-list-with-tabs.tsx b/apps/client/src/features/comment/components/comment-list-with-tabs.tsx index 08a27013..86fb1120 100644 --- a/apps/client/src/features/comment/components/comment-list-with-tabs.tsx +++ b/apps/client/src/features/comment/components/comment-list-with-tabs.tsx @@ -16,6 +16,12 @@ import { extractPageSlugId } from "@/lib"; import { useTranslation } from "react-i18next"; import { useQueryEmit } from "@/features/websocket/use-query-emit"; import { useIsCloudEE } from "@/hooks/use-is-cloud-ee"; +import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts"; +import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts"; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from "@/features/space/permissions/permissions.type.ts"; function CommentListWithTabs() { const { t } = useTranslation(); @@ -30,6 +36,15 @@ function CommentListWithTabs() { const [isLoading, setIsLoading] = useState(false); const emit = useQueryEmit(); const isCloudEE = useIsCloudEE(); + const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug); + + const spaceRules = space?.membership?.permissions; + const spaceAbility = useSpaceAbility(spaceRules); + + const canComment: boolean = spaceAbility.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Page, + ); // Separate active and resolved comments const { activeComments, resolvedComments } = useMemo(() => { @@ -38,14 +53,14 @@ function CommentListWithTabs() { } const parentComments = comments.items.filter( - (comment: IComment) => comment.parentCommentId === null + (comment: IComment) => comment.parentCommentId === null, ); const active = parentComments.filter( - (comment: IComment) => !comment.resolvedAt + (comment: IComment) => !comment.resolvedAt, ); const resolved = parentComments.filter( - (comment: IComment) => comment.resolvedAt + (comment: IComment) => comment.resolvedAt, ); return { activeComments: active, resolvedComments: resolved }; @@ -88,11 +103,20 @@ function CommentListWithTabs() { data-comment-id={comment.id} >
- - + +
- {!comment.resolvedAt && ( + {!comment.resolvedAt && canComment && ( <> {t("Error loading comments.")}
; } - const totalComments = (activeComments.length + resolvedComments.length); + const totalComments = activeComments.length + resolvedComments.length; if (totalComments === 0) { return <>{t("No comments yet.")}; @@ -135,8 +159,8 @@ function CommentListWithTabs() { return ( - {activeComments.length} @@ -145,8 +169,8 @@ function CommentListWithTabs() { > {t("Open")} - {resolvedComments.length} @@ -184,8 +208,14 @@ interface ChildCommentsProps { comments: IPagination; parentId: string; pageId: string; + canComment: boolean; } -const ChildComments = ({ comments, parentId, pageId }: ChildCommentsProps) => { +const ChildComments = ({ + comments, + parentId, + pageId, + canComment, +}: ChildCommentsProps) => { const getChildComments = useCallback( (parentId: string) => comments.items.filter( @@ -198,11 +228,16 @@ const ChildComments = ({ comments, parentId, pageId }: ChildCommentsProps) => {
{getChildComments(parentId).map((childComment) => (
- +
))} @@ -236,4 +271,4 @@ const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { ); }; -export default CommentListWithTabs; \ No newline at end of file +export default CommentListWithTabs; diff --git a/apps/client/src/features/comment/components/comment-list.tsx b/apps/client/src/features/comment/components/comment-list.tsx deleted file mode 100644 index 1aded495..00000000 --- a/apps/client/src/features/comment/components/comment-list.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState, useRef, useCallback, memo } from "react"; -import { useParams } from "react-router-dom"; -import { Divider, Paper } from "@mantine/core"; -import CommentListItem from "@/features/comment/components/comment-list-item"; -import { - useCommentsQuery, - useCreateCommentMutation, -} from "@/features/comment/queries/comment-query"; -import CommentEditor from "@/features/comment/components/comment-editor"; -import CommentActions from "@/features/comment/components/comment-actions"; -import { useFocusWithin } from "@mantine/hooks"; -import { IComment } from "@/features/comment/types/comment.types.ts"; -import { usePageQuery } from "@/features/page/queries/page-query.ts"; -import { IPagination } from "@/lib/types.ts"; -import { extractPageSlugId } from "@/lib"; -import { useTranslation } from "react-i18next"; -import { useQueryEmit } from "@/features/websocket/use-query-emit"; - -function CommentList() { - const { t } = useTranslation(); - const { pageSlug } = useParams(); - const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) }); - const { - data: comments, - isLoading: isCommentsLoading, - isError, - } = useCommentsQuery({ pageId: page?.id, limit: 100 }); - const createCommentMutation = useCreateCommentMutation(); - const [isLoading, setIsLoading] = useState(false); - const emit = useQueryEmit(); - - const handleAddReply = useCallback( - async (commentId: string, content: string) => { - try { - setIsLoading(true); - const commentData = { - pageId: page?.id, - parentCommentId: commentId, - content: JSON.stringify(content), - }; - - await createCommentMutation.mutateAsync(commentData); - - emit({ - operation: "invalidateComment", - pageId: page?.id, - }); - } catch (error) { - console.error("Failed to post comment:", error); - } finally { - setIsLoading(false); - } - }, - [createCommentMutation, page?.id], - ); - - const renderComments = useCallback( - (comment: IComment) => ( - -
- - -
- - - - -
- ), - [comments, handleAddReply, isLoading], - ); - - if (isCommentsLoading) { - return <>; - } - - if (isError) { - return
{t("Error loading comments.")}
; - } - - if (!comments || comments.items.length === 0) { - return <>{t("No comments yet.")}; - } - - return ( - <> - {comments.items - .filter((comment) => comment.parentCommentId === null) - .map(renderComments)} - - ); -} - -interface ChildCommentsProps { - comments: IPagination; - parentId: string; - pageId: string; -} -const ChildComments = ({ comments, parentId, pageId }: ChildCommentsProps) => { - const getChildComments = useCallback( - (parentId: string) => - comments.items.filter( - (comment: IComment) => comment.parentCommentId === parentId, - ), - [comments.items], - ); - - return ( -
- {getChildComments(parentId).map((childComment) => ( -
- - -
- ))} -
- ); -}; - -const MemoizedChildComments = memo(ChildComments); - -const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { - const [content, setContent] = useState(""); - const { ref, focused } = useFocusWithin(); - const commentEditorRef = useRef(null); - - const handleSave = useCallback(() => { - onSave(commentId, content); - setContent(""); - commentEditorRef.current?.clearContent(); - }, [commentId, content, onSave]); - - return ( -
- - {focused && } -
- ); -}; - -export default CommentList;