From 409a3e84ab0493d9384842a44f17289955880c97 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:02:40 +0530 Subject: [PATCH] [WEB-5768]chore: updated comment UI #8402 --- .../ce/components/comments/comment-block.tsx | 52 ++----- apps/web/ce/components/comments/index.ts | 1 + .../core/components/comments/card/display.tsx | 132 +++++++++++++++--- .../core/components/comments/card/root.tsx | 94 ++++--------- .../web/core/components/comments/comments.tsx | 7 +- .../components/editor/lite-text/editor.tsx | 3 + .../components/editor/lite-text/toolbar.tsx | 4 +- .../issue-activity/activity-comment-root.tsx | 2 + .../issue-detail/issue-activity/helper.tsx | 2 +- .../issue-detail/issue-activity/root.tsx | 4 +- .../icons/properties/comment-reply-icon.tsx | 15 ++ packages/propel/src/icons/properties/index.ts | 1 + 12 files changed, 184 insertions(+), 133 deletions(-) create mode 100644 packages/propel/src/icons/properties/comment-reply-icon.tsx diff --git a/apps/web/ce/components/comments/comment-block.tsx b/apps/web/ce/components/comments/comment-block.tsx index cf433297f..47590e53f 100644 --- a/apps/web/ce/components/comments/comment-block.tsx +++ b/apps/web/ce/components/comments/comment-block.tsx @@ -2,77 +2,43 @@ import type { ReactNode } from "react"; import { useRef } from "react"; import { observer } from "mobx-react"; // plane imports -import { useTranslation } from "@plane/i18n"; +import { CommentReplyIcon } from "@plane/propel/icons"; import type { TIssueComment } from "@plane/types"; -import { Avatar, Tooltip } from "@plane/ui"; -import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils"; +import { cn } from "@plane/utils"; // hooks -import { useMember } from "@/hooks/store/use-member"; type TCommentBlock = { comment: TIssueComment; ends: "top" | "bottom" | undefined; - quickActions: ReactNode; children: ReactNode; }; export const CommentBlock = observer(function CommentBlock(props: TCommentBlock) { - const { comment, ends, quickActions, children } = props; - // refs + const { comment, ends, children } = props; const commentBlockRef = useRef(null); - // store hooks - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(comment?.actor); - // translation - const { t } = useTranslation(); - - const displayName = comment?.actor_detail?.is_bot - ? comment?.actor_detail?.first_name + ` ${t("bot")}` - : (userDetails?.display_name ?? comment?.actor_detail?.display_name); - - const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url; if (!comment) return null; - return (
- +
-
-
-
- {displayName} -
-
- commented{" "} - - - {calculateTimeAgo(comment.created_at)} - {comment.edited_at && ` (${t("edited")})`} - - -
-
-
{quickActions}
+
+ {children}
-
{children}
); diff --git a/apps/web/ce/components/comments/index.ts b/apps/web/ce/components/comments/index.ts index f0ef4e2b6..6144f5236 100644 --- a/apps/web/ce/components/comments/index.ts +++ b/apps/web/ce/components/comments/index.ts @@ -1 +1,2 @@ export * from "./comment-block"; +export { CommentCardDisplay } from "@/components/comments/card/display"; diff --git a/apps/web/core/components/comments/card/display.tsx b/apps/web/core/components/comments/card/display.tsx index 452942e9c..057c5dff0 100644 --- a/apps/web/core/components/comments/card/display.tsx +++ b/apps/web/core/components/comments/card/display.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; import { Globe2, Lock } from "lucide-react"; @@ -7,24 +8,33 @@ import type { EditorRefApi } from "@plane/editor"; import { useHashScroll } from "@plane/hooks"; import { EIssueCommentAccessSpecifier } from "@plane/types"; import type { TCommentsOperations, TIssueComment } from "@plane/types"; -import { cn } from "@plane/utils"; +import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils"; // components import { LiteTextEditor } from "@/components/editor/lite-text"; // local imports import { CommentReactions } from "../comment-reaction"; +import { CommentCardEditForm } from "./edit-form"; +import { EmojiReactionButton, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; +import { Avatar, Tooltip } from "@plane/ui"; +import { useMember } from "@/hooks/store/use-member"; -type Props = { +export type TCommentCardDisplayProps = { activityOperations: TCommentsOperations; comment: TIssueComment; disabled: boolean; + entityId: string; projectId?: string; readOnlyEditorRef: React.RefObject; showAccessSpecifier: boolean; workspaceId: string; workspaceSlug: string; + isEditing?: boolean; + setIsEditing?: (isEditing: boolean) => void; + renderFooter?: (ReactionsComponent: ReactNode | null) => ReactNode; + renderQuickActions?: () => ReactNode; }; -export const CommentCardDisplay = observer(function CommentCardDisplay(props: Props) { +export const CommentCardDisplay = observer(function CommentCardDisplay(props: TCommentCardDisplayProps) { const { activityOperations, comment, @@ -34,13 +44,34 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr showAccessSpecifier, workspaceId, workspaceSlug, + isEditing = false, + setIsEditing, + renderFooter, + renderQuickActions, } = props; // states const [highlightClassName, setHighlightClassName] = useState(""); + // state + const [isPickerOpen, setIsPickerOpen] = useState(false); + // store hooks + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(comment?.actor); + const displayName = comment?.actor_detail?.is_bot + ? comment?.actor_detail?.first_name + `Bot` + : (userDetails?.display_name ?? comment?.actor_detail?.display_name); + const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url; + + const userReactions = activityOperations.userReactions(comment.id); + // navigation const pathname = usePathname(); // derived values const commentBlockId = `comment-${comment?.id}`; + // Check if there are any reactions to determine if we should render the footer + const reactionIds = activityOperations.reactionIds(comment.id); + const hasReactions = reactionIds && Object.keys(reactionIds).some((key) => reactionIds[key]?.length > 0); + // scroll to comment const { isHashMatch } = useHashScroll({ elementId: commentBlockId, @@ -57,6 +88,17 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr return () => clearTimeout(timeout); }, [isHashMatch]); + const handleEmojiSelect = useCallback( + (emoji: string) => { + if (!userReactions) return; + // emoji is already in decimal string format from EmojiReactionPicker + void activityOperations.react(comment.id, emoji, userReactions); + }, + [activityOperations, comment.id, userReactions] + ); + + const shouldRenderReactions = hasReactions && !disabled; + return (
{showAccessSpecifier && ( @@ -68,20 +110,74 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr )}
)} - - +
+ +
+
{displayName}
+
+ commented{" "} + + + {calculateTimeAgo(comment.created_at)} + {comment.edited_at && " (edited)"} + + +
+
+ {!disabled && ( +
+ setIsPickerOpen(true)} />} + placement="bottom-start" + /> + {renderQuickActions ? renderQuickActions() : null} +
+ )} +
+ {isEditing && setIsEditing ? ( + + ) : ( + <> + + {shouldRenderReactions && + (renderFooter ? ( + renderFooter( + + ) + ) : ( + + ))} + + )}
); }); diff --git a/apps/web/core/components/comments/card/root.tsx b/apps/web/core/components/comments/card/root.tsx index 2f86df135..18666a42c 100644 --- a/apps/web/core/components/comments/card/root.tsx +++ b/apps/web/core/components/comments/card/root.tsx @@ -1,23 +1,22 @@ -import { useCallback, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { observer } from "mobx-react"; // plane imports -import { EmojiReactionButton, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; import type { EditorRefApi } from "@plane/editor"; import type { TIssueComment, TCommentsOperations } from "@plane/types"; // plane web imports -import { CommentBlock } from "@/plane-web/components/comments"; +import { CommentBlock, CommentCardDisplay } from "@/plane-web/components/comments"; // local imports import { CommentQuickActions } from "../quick-actions"; -import { CommentCardDisplay } from "./display"; -import { CommentCardEditForm } from "./edit-form"; type TCommentCard = { workspaceSlug: string; + entityId: string; comment: TIssueComment | undefined; activityOperations: TCommentsOperations; ends: "top" | "bottom" | undefined; showAccessSpecifier: boolean; showCopyLinkOption: boolean; + enableReplies: boolean; disabled?: boolean; projectId?: string; }; @@ -25,6 +24,7 @@ type TCommentCard = { export const CommentCard = observer(function CommentCard(props: TCommentCard) { const { workspaceSlug, + entityId, comment, activityOperations, ends, @@ -35,75 +35,37 @@ export const CommentCard = observer(function CommentCard(props: TCommentCard) { } = props; // states const [isEditing, setIsEditing] = useState(false); - const [isPickerOpen, setIsPickerOpen] = useState(false); // refs const readOnlyEditorRef = useRef(null); // derived values const workspaceId = comment?.workspace; - const userReactions = comment?.id ? activityOperations.userReactions(comment.id) : undefined; - - const handleEmojiSelect = useCallback( - (emoji: string) => { - if (!userReactions || !comment?.id) return; - // emoji is already in decimal string format from EmojiReactionPicker - void activityOperations.react(comment.id, emoji, userReactions); - }, - [activityOperations, comment?.id, userReactions] - ); - if (!comment || !workspaceId) return null; return ( - - setIsPickerOpen(true)} />} - placement="bottom-start" - /> - - setIsEditing(true)} - showAccessSpecifier={showAccessSpecifier} - showCopyLinkOption={showCopyLinkOption} - /> -
- ) - } - ends={ends} - > - {isEditing ? ( - - ) : ( - - )} + + ( + setIsEditing(true)} + showAccessSpecifier={showAccessSpecifier} + showCopyLinkOption={showCopyLinkOption} + /> + )} + /> ); }); diff --git a/apps/web/core/components/comments/comments.tsx b/apps/web/core/components/comments/comments.tsx index 62e028204..e370fcb1d 100644 --- a/apps/web/core/components/comments/comments.tsx +++ b/apps/web/core/components/comments/comments.tsx @@ -1,5 +1,4 @@ -import type { FC } from "react"; -import React, { useMemo } from "react"; +import { useMemo } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports @@ -19,6 +18,7 @@ type TCommentsWrapper = { getCommentById?: (activityId: string) => TIssueComment | undefined; showAccessSpecifier?: boolean; showCopyLinkOption?: boolean; + enableReplies?: boolean; }; export const CommentsWrapper = observer(function CommentsWrapper(props: TCommentsWrapper) { @@ -31,6 +31,7 @@ export const CommentsWrapper = observer(function CommentsWrapper(props: TComment projectId, showAccessSpecifier = false, showCopyLinkOption = false, + enableReplies = false, } = props; // router const { workspaceSlug: routerWorkspaceSlug } = useParams(); @@ -65,6 +66,7 @@ export const CommentsWrapper = observer(function CommentsWrapper(props: TComment ); })} diff --git a/apps/web/core/components/editor/lite-text/editor.tsx b/apps/web/core/components/editor/lite-text/editor.tsx index 1be9aadf1..6cae6fed8 100644 --- a/apps/web/core/components/editor/lite-text/editor.tsx +++ b/apps/web/core/components/editor/lite-text/editor.tsx @@ -38,6 +38,7 @@ type LiteTextEditorWrapperProps = MakeOptional< issue_id?: string; parentClassName?: string; editorClassName?: string; + submitButtonText?: string; } & ( | { editable: false; @@ -73,6 +74,7 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor( disabledExtensions: additionalDisabledExtensions = [], editorClassName = "", showPlaceholderOnEmpty = true, + submitButtonText = "common.comment", ...rest } = props; // states @@ -208,6 +210,7 @@ export const LiteTextEditor = React.forwardRef(function LiteTextEditor( showAccessSpecifier={showAccessSpecifier} editorRef={editorRef} showSubmitButton={showSubmitButton} + submitButtonText={submitButtonText} /> )} diff --git a/apps/web/core/components/editor/lite-text/toolbar.tsx b/apps/web/core/components/editor/lite-text/toolbar.tsx index bad4787af..6aa292da1 100644 --- a/apps/web/core/components/editor/lite-text/toolbar.tsx +++ b/apps/web/core/components/editor/lite-text/toolbar.tsx @@ -25,6 +25,7 @@ type Props = { showAccessSpecifier: boolean; showSubmitButton: boolean; editorRef: EditorRefApi | null; + submitButtonText?: string; }; type TCommentAccessType = { @@ -60,6 +61,7 @@ export function IssueCommentToolbar(props: Props) { showAccessSpecifier, showSubmitButton, editorRef, + submitButtonText = "common.comment", } = props; // State to manage active states of toolbar items const [activeStates, setActiveStates] = useState>({}); @@ -175,7 +177,7 @@ export function IssueCommentToolbar(props: Props) { disabled={isSubmitButtonDisabled} loading={isSubmitting} > - {t("common.comment")} + {t(submitButtonText)} )} diff --git a/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index f4a72f34b..844034189 100644 --- a/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -67,6 +67,7 @@ export const IssueActivityCommentRoot = observer(function IssueActivityCommentRo ) : BASE_ACTIVITY_FILTER_TYPES.includes(activityComment.activity_type as EActivityFilterType) ? ( + + + ); +} diff --git a/packages/propel/src/icons/properties/index.ts b/packages/propel/src/icons/properties/index.ts index dc2b4e50a..73e135208 100644 --- a/packages/propel/src/icons/properties/index.ts +++ b/packages/propel/src/icons/properties/index.ts @@ -1,4 +1,5 @@ export * from "./boolean-icon"; +export * from "./comment-reply-icon"; export * from "./dropdown-icon"; export * from "./due-date-icon"; export * from "./duplicate-icon";