[WEB-5515]fix: comment replies #4930
This commit is contained in:
@@ -41,7 +41,7 @@ export const CommentBlock = observer(function CommentBlock(props: TCommentBlock)
|
||||
ref={commentBlockRef}
|
||||
>
|
||||
<div
|
||||
className="absolute left-[13px] top-0 bottom-0 w-0.5 transition-border duration-1000 bg-custom-background-80"
|
||||
className="absolute left-[13px] top-0 bottom-0 w-px transition-border duration-1000 bg-custom-background-80"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -26,6 +27,7 @@ export type TCommentCardDisplayProps = {
|
||||
workspaceSlug: string;
|
||||
isEditing?: boolean;
|
||||
setIsEditing?: (isEditing: boolean) => void;
|
||||
renderFooter?: (ReactionsComponent: ReactNode) => ReactNode;
|
||||
};
|
||||
|
||||
export const CommentCardDisplay = observer(function CommentCardDisplay(props: TCommentCardDisplayProps) {
|
||||
@@ -40,6 +42,7 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: TC
|
||||
workspaceSlug,
|
||||
isEditing = false,
|
||||
setIsEditing,
|
||||
renderFooter,
|
||||
} = props;
|
||||
// states
|
||||
const [highlightClassName, setHighlightClassName] = useState("");
|
||||
@@ -99,11 +102,17 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: TC
|
||||
displayConfig={{
|
||||
fontSize: "small-font",
|
||||
}}
|
||||
parentClassName="border-none"
|
||||
parentClassName="border-none pl-1"
|
||||
/>
|
||||
<div className="pl-2">
|
||||
<CommentReactions comment={comment} disabled={disabled} activityOperations={activityOperations} />
|
||||
</div>
|
||||
{renderFooter ? (
|
||||
renderFooter(
|
||||
<CommentReactions comment={comment} disabled={disabled} activityOperations={activityOperations} />
|
||||
)
|
||||
) : (
|
||||
<div className="pl-2">
|
||||
<CommentReactions comment={comment} disabled={disabled} activityOperations={activityOperations} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { EditorRefApi } from "@plane/editor";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
import type { TCommentsOperations, TIssueComment } from "@plane/types";
|
||||
import { isCommentEmpty } from "@plane/utils";
|
||||
import { cn, isCommentEmpty } from "@plane/utils";
|
||||
// components
|
||||
import { LiteTextEditor } from "@/components/editor/lite-text";
|
||||
|
||||
@@ -99,7 +99,7 @@ export const CommentCardEditForm = observer(function CommentCardEditForm(props:
|
||||
return asset_id;
|
||||
}}
|
||||
projectId={projectId}
|
||||
parentClassName="p-2"
|
||||
parentClassName="p-2 bg-custom-background-100"
|
||||
displayConfig={{
|
||||
fontSize: "small-font",
|
||||
}}
|
||||
@@ -111,24 +111,41 @@ export const CommentCardEditForm = observer(function CommentCardEditForm(props:
|
||||
type="button"
|
||||
onClick={handleSubmit(onEnter)}
|
||||
disabled={isDisabled}
|
||||
className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${
|
||||
isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500"
|
||||
}`}
|
||||
className={cn(
|
||||
"group rounded border p-2 shadow-md duration-300",
|
||||
isDisabled
|
||||
? "cursor-not-allowed border-green-500/50 bg-green-500/10"
|
||||
: "border-green-500 bg-green-500/20 hover:bg-green-500"
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
className={`h-3 w-3 text-green-500 duration-300 ${isEmpty ? "text-black" : "group-hover:text-white"}`}
|
||||
className={cn(
|
||||
"h-3 w-3 duration-300",
|
||||
isDisabled ? "text-green-500/50" : "text-green-500 group-hover:text-white"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
"group rounded border p-2 shadow-md duration-300",
|
||||
isSubmitting
|
||||
? "cursor-not-allowed border-red-500/50 bg-red-500/10"
|
||||
: "border-red-500 bg-red-500/20 hover:bg-red-500"
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
editorRef.current?.setEditorValue(comment.comment_html ?? "<p></p>");
|
||||
}}
|
||||
>
|
||||
<CloseIcon className="size-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
<CloseIcon
|
||||
className={cn(
|
||||
"size-3 duration-300",
|
||||
isSubmitting ? "text-red-500/50" : "text-red-500 group-hover:text-white"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -57,15 +57,13 @@ export const CommentCard = observer(function CommentCard(props: TCommentCard) {
|
||||
workspaceSlug={workspaceSlug}
|
||||
isEditing={isEditing}
|
||||
setIsEditing={setIsEditing}
|
||||
renderQuickActions={(handleReply) => (
|
||||
renderQuickActions={() => (
|
||||
<CommentQuickActions
|
||||
activityOperations={activityOperations}
|
||||
comment={comment}
|
||||
setEditMode={() => setIsEditing(true)}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
showCopyLinkOption={showCopyLinkOption}
|
||||
showReplyOption={enableReplies}
|
||||
handleReply={handleReply}
|
||||
/>
|
||||
)}
|
||||
enableReplies={enableReplies}
|
||||
|
||||
@@ -16,20 +16,10 @@ type TCommentCard = {
|
||||
setEditMode: () => void;
|
||||
showAccessSpecifier: boolean;
|
||||
showCopyLinkOption: boolean;
|
||||
showReplyOption: boolean;
|
||||
handleReply?: () => void;
|
||||
};
|
||||
|
||||
export const CommentQuickActions: FC<TCommentCard> = observer((props) => {
|
||||
const {
|
||||
activityOperations,
|
||||
comment,
|
||||
setEditMode,
|
||||
showAccessSpecifier,
|
||||
showCopyLinkOption,
|
||||
showReplyOption,
|
||||
handleReply,
|
||||
} = props;
|
||||
const { activityOperations, comment, setEditMode, showAccessSpecifier, showCopyLinkOption } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
@@ -54,10 +44,10 @@ export const CommentQuickActions: FC<TCommentCard> = observer((props) => {
|
||||
: EIssueCommentAccessSpecifier.INTERNAL,
|
||||
}),
|
||||
handleDelete: () => activityOperations.removeComment(comment.id),
|
||||
handleReply,
|
||||
showReplyOption,
|
||||
});
|
||||
|
||||
if(MENU_ITEMS.length === 0) return null;
|
||||
|
||||
return (
|
||||
<CustomMenu ellipsis closeOnSelect>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
|
||||
@@ -239,12 +239,10 @@ interface UseCommentMenuItemsProps {
|
||||
isAuthor: boolean;
|
||||
showAccessSpecifier: boolean;
|
||||
showCopyLinkOption: boolean;
|
||||
showReplyOption: boolean;
|
||||
handleEdit: () => void;
|
||||
handleCopyLink: () => void;
|
||||
handleToggleAccess: () => void;
|
||||
handleDelete: () => void;
|
||||
handleReply?: () => void;
|
||||
}
|
||||
|
||||
export const useCommentMenuItems = (props: UseCommentMenuItemsProps): TContextMenuItem[] => {
|
||||
@@ -254,27 +252,18 @@ export const useCommentMenuItems = (props: UseCommentMenuItemsProps): TContextMe
|
||||
isAuthor,
|
||||
showAccessSpecifier,
|
||||
showCopyLinkOption,
|
||||
showReplyOption,
|
||||
handleEdit,
|
||||
handleCopyLink,
|
||||
handleToggleAccess,
|
||||
handleDelete,
|
||||
handleReply,
|
||||
} = props;
|
||||
|
||||
const replyFeature = factory.useCommentReplyFeature?.({
|
||||
commentId: comment.id,
|
||||
handleReply,
|
||||
shouldRender: showReplyOption,
|
||||
});
|
||||
|
||||
// Check if access is INTERNAL (0 or "INTERNAL")
|
||||
const isInternal = comment.access === 0 || comment.access === "INTERNAL" || comment.access === "0";
|
||||
|
||||
// Assemble final menu items - order defined here
|
||||
const items = [
|
||||
factory.createCommentEditMenuItem(handleEdit, isAuthor),
|
||||
...(replyFeature?.items ?? []),
|
||||
factory.createCommentCopyLinkMenuItem(handleCopyLink, showCopyLinkOption),
|
||||
factory.createCommentAccessSpecifierMenuItem(handleToggleAccess, isInternal, showAccessSpecifier),
|
||||
factory.createCommentDeleteMenuItem(handleDelete, isAuthor),
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export const IssueActivityBlockComponent = observer(function IssueActivityBlockC
|
||||
}`}
|
||||
ref={activityBlockRef}
|
||||
>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-px bg-custom-background-80" aria-hidden />
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-[4] bg-custom-background-80 transition-border duration-1000 border-2 border-transparent",
|
||||
|
||||
@@ -3,9 +3,12 @@ import { useCallback, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Separator } from "@plane/propel/separator";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { calculateTimeAgo, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
|
||||
import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
|
||||
// components
|
||||
import { CommentCardDisplay as BaseCommentCardDisplay } from "@/components/comments/card/display";
|
||||
import type { TCommentCardDisplayProps } from "@/components/comments/card/display";
|
||||
@@ -16,7 +19,7 @@ import type { CommentRepliesRootHandle } from "../replies/root";
|
||||
import { CommentRepliesRoot } from "../replies/root";
|
||||
|
||||
type Props = TCommentCardDisplayProps & {
|
||||
renderQuickActions: (handleReply: () => void) => ReactNode;
|
||||
renderQuickActions: () => ReactNode;
|
||||
enableReplies: boolean;
|
||||
isReply?: boolean;
|
||||
};
|
||||
@@ -38,6 +41,8 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
|
||||
// refs
|
||||
const repliesRootRef = useRef<CommentRepliesRootHandle>(null);
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
@@ -48,27 +53,27 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
|
||||
const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url;
|
||||
|
||||
const handleReply = useCallback(() => {
|
||||
editorRef.current?.setFocusAtPosition(0);
|
||||
repliesRootRef.current?.showReplyEditor();
|
||||
}, []);
|
||||
const areRepliesAvailable = comment.reply_count !== undefined && comment.reply_count > 0;
|
||||
const shouldShowIndicator = isReply || areRepliesAvailable;
|
||||
const shouldShowReplyButton = enableReplies && !isReply && !disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<div className={cn("relative", isReply && "pt-2")}>
|
||||
{shouldShowIndicator && (
|
||||
<div
|
||||
className="absolute left-[7px] top-1 -bottom-1 w-px transition-border duration-1000 bg-custom-background-80"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<div className="flex relative w-full gap-2 items-center z-10">
|
||||
<div className="flex relative w-full gap-2 items-center">
|
||||
<Avatar size="sm" name={displayName} src={getFileURL(avatarUrl)} className="shrink-0" />
|
||||
<div className="flex-1 flex flex-wrap items-center gap-1">
|
||||
<div className="text-xs font-medium">{displayName}</div>
|
||||
<div className="text-xs text-custom-text-300">
|
||||
commented{" "}
|
||||
{isReply ? "replied " : "commented "}
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(comment.created_at)} at ${renderFormattedTime(comment.created_at)}`}
|
||||
position="bottom"
|
||||
@@ -80,10 +85,10 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{!disabled && <div className="shrink-0">{renderQuickActions(handleReply)}</div>}
|
||||
{!disabled && <div className="shrink-0">{renderQuickActions()}</div>}
|
||||
</div>
|
||||
{/* Core: Comment content */}
|
||||
<div className="ml-4 pb-4">
|
||||
<div className="ml-4">
|
||||
<BaseCommentCardDisplay
|
||||
{...restProps}
|
||||
entityId={entityId}
|
||||
@@ -92,6 +97,24 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
renderFooter={(ReactionsComponent) => (
|
||||
<div className="flex items-center gap-1">
|
||||
{shouldShowReplyButton && (
|
||||
<>
|
||||
<Button
|
||||
variant="link-neutral"
|
||||
size="sm"
|
||||
onClick={handleReply}
|
||||
className="hover:bg-custom-background-80 px-2.5"
|
||||
>
|
||||
{t("common.actions.reply")}
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
</>
|
||||
)}
|
||||
<div className="px-2">{ReactionsComponent}</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,12 +125,12 @@ export const CommentCardDisplay = observer(function CommentCardDisplay(props: Pr
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
entityId={entityId}
|
||||
activityOperations={activityOperations}
|
||||
commentId={comment.id}
|
||||
repliesCount={comment.reply_count || 0}
|
||||
repliedUserIds={comment.replied_user_ids || []}
|
||||
lastReplyAt={comment.last_reply_at || null}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
activityOperations={activityOperations}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const CommentBlock: FC<TCommentBlock> = observer((props) => {
|
||||
ref={commentBlockRef}
|
||||
>
|
||||
<div
|
||||
className="absolute left-[13px] top-0 bottom-0 w-0.5 transition-border duration-1000 bg-custom-background-80"
|
||||
className="absolute left-[13px] top-0 bottom-0 w-px transition-border duration-1000 bg-custom-background-80"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -58,6 +58,24 @@ export const useCommentRepliesOperations = (
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
updateReply: async (replyId, data) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId || !replyId) throw new Error("Missing fields");
|
||||
await repliesStore.updateReply(workspaceSlug, projectId, issueId, replyId, data);
|
||||
setToast({
|
||||
title: t("common.success"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("issue.comments.replies.toast.update.success.message"),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating reply:", error);
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: t("issue.comments.replies.toast.update.error.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteReply: async (replyId) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId || !replyId) throw new Error("Missing fields");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
@@ -30,11 +30,25 @@ export const ReplyCard = observer(function ReplyCard(props: Props) {
|
||||
// derived values
|
||||
const reply = getReply();
|
||||
|
||||
// Create wrapper operations that route updateComment to updateReply for replies
|
||||
const replyActivityOperations: TCommentsOperations = useMemo(
|
||||
() => ({
|
||||
...activityOperations,
|
||||
updateComment: async (commentId: string, data: Partial<TIssueComment>) => {
|
||||
// Route to reply update operation
|
||||
if (activityOperations.replyOperations?.updateReply) {
|
||||
await activityOperations.replyOperations.updateReply(commentId, data);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[activityOperations]
|
||||
);
|
||||
|
||||
if (!reply) return null;
|
||||
|
||||
return (
|
||||
<CommentCardDisplay
|
||||
activityOperations={activityOperations}
|
||||
activityOperations={replyActivityOperations}
|
||||
entityId={entityId}
|
||||
comment={reply}
|
||||
disabled={false}
|
||||
|
||||
@@ -112,6 +112,7 @@ export const ReplyCreate = observer(function ReplyCreate(props: Props) {
|
||||
}
|
||||
}}
|
||||
ref={editorRef}
|
||||
value={null}
|
||||
initialValue={value ?? "<p></p>"}
|
||||
containerClassName="min-h-min"
|
||||
onChange={(comment_json, comment_html) => onChange(comment_html)}
|
||||
@@ -130,7 +131,7 @@ export const ReplyCreate = observer(function ReplyCreate(props: Props) {
|
||||
return asset_id;
|
||||
}}
|
||||
showToolbarInitially={false}
|
||||
parentClassName="p-2"
|
||||
parentClassName="p-2 bg-custom-background-100"
|
||||
placeholder={t("issue.comments.replies.create.placeholder")}
|
||||
displayConfig={{
|
||||
fontSize: "small-font",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react";
|
||||
import { useEffect, useState, useImperativeHandle, forwardRef } from "react";
|
||||
import { uniq } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
@@ -46,29 +46,17 @@ export const CommentRepliesRoot = observer(
|
||||
} = props;
|
||||
// states
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// refs
|
||||
const previousRepliesCountRef = useRef(repliesCount);
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// Expose method to show editor - expands and shows editor
|
||||
useImperativeHandle(ref, () => ({
|
||||
showReplyEditor: () => {
|
||||
setIsExpanded(true);
|
||||
if (!isExpanded) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
editorRef.current?.focus("end");
|
||||
},
|
||||
}));
|
||||
// Auto-expand only when a new reply is added (repliesCount increases)
|
||||
useEffect(() => {
|
||||
const previousCount = previousRepliesCountRef.current;
|
||||
const currentCount = repliesCount;
|
||||
|
||||
// Only auto-expand if replies count increased (new reply added) and not already expanded
|
||||
if (currentCount > previousCount && currentCount > 0 && !isExpanded) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
|
||||
// Update ref for next render
|
||||
previousRepliesCountRef.current = currentCount;
|
||||
}, [repliesCount, isExpanded]);
|
||||
// Fetch replies when expanded and has replies
|
||||
useEffect(() => {
|
||||
if (isExpanded && repliesCount > 0) {
|
||||
@@ -79,53 +67,63 @@ export const CommentRepliesRoot = observer(
|
||||
}, [isExpanded, repliesCount, commentId, activityOperations.replyOperations]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{repliesCount > 0 && !isExpanded && (
|
||||
<Button
|
||||
variant="link-neutral"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 text-xs text-custom-text-300 hover:text-custom-text-200 w-fit px-0 z-[1] ml-2 pl-3 relative"
|
||||
>
|
||||
{repliedUserIds.length > 0 && (
|
||||
<AvatarGroup size="sm" max={2} showTooltip={false}>
|
||||
{uniq(repliedUserIds).map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
if (!userDetails) return null;
|
||||
return (
|
||||
<Avatar
|
||||
key={userId}
|
||||
name={userDetails.display_name}
|
||||
src={userDetails.avatar_url ? getFileURL(userDetails.avatar_url) : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<div className="flex flex-col">
|
||||
{repliesCount > 0 && (
|
||||
<>
|
||||
{!isExpanded && (
|
||||
<Button
|
||||
variant="link-neutral"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="flex items-center gap-2 text-xs text-custom-text-300 hover:text-custom-text-200 w-fit px-0 ml-2 pl-3 relative"
|
||||
>
|
||||
{repliedUserIds.length > 0 && (
|
||||
<AvatarGroup size="sm" max={2} showTooltip={false}>
|
||||
{uniq(repliedUserIds).map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
if (!userDetails) return null;
|
||||
return (
|
||||
<Avatar
|
||||
key={userId}
|
||||
name={userDetails.display_name}
|
||||
src={userDetails.avatar_url ? getFileURL(userDetails.avatar_url) : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
)}
|
||||
<span>
|
||||
{repliesCount} {repliesCount === 1 ? "reply" : "replies"}
|
||||
</span>
|
||||
<div className="size-1 rounded-full bg-custom-background-80" />
|
||||
{lastReplyAt && (
|
||||
<div className="flex gap-1">
|
||||
<span className="text-custom-text-400">Last reply</span>
|
||||
<span className="text-custom-text-400">{calculateTimeAgo(lastReplyAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute left-0 top-0 h-4 w-2 border-l border-b rounded-bl-full border-custom-background-80" />
|
||||
</Button>
|
||||
)}
|
||||
<span>
|
||||
{repliesCount} {repliesCount === 1 ? "reply" : "replies"}
|
||||
</span>
|
||||
<div className="size-1 rounded-full bg-custom-background-80" />
|
||||
{lastReplyAt && (
|
||||
<>
|
||||
<span className="text-custom-text-400">Last reply</span>
|
||||
<span className="text-custom-text-400">{calculateTimeAgo(lastReplyAt)}</span>
|
||||
</>
|
||||
)}
|
||||
<div className="absolute left-0 top-0 h-4 w-2 border-l border-b rounded-bl-full border-custom-background-80" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isExpanded && repliesCount > 0 && (
|
||||
<RepliesList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
entityId={entityId}
|
||||
commentId={commentId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
setIsExpanded={setIsExpanded}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"grid transition-[grid-template-rows] duration-200 ease-out",
|
||||
isExpanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<RepliesList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
entityId={entityId}
|
||||
commentId={commentId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
setIsExpanded={setIsExpanded}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
<div
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
||||
}`}
|
||||
ref={activityBlockRef}
|
||||
>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-px bg-custom-background-80" aria-hidden />
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden border-2 border-transparent flex justify-center items-center z-[4] bg-custom-background-80 text-custom-text-200",
|
||||
|
||||
@@ -71,7 +71,7 @@ export const IssueActivityWorklog: FC<TIssueActivityWorklog> = observer((props)
|
||||
<div
|
||||
className={`relative flex gap-3 ${ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`} ${!worklog?.description && "items-center"}`}
|
||||
>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-px bg-custom-background-80" aria-hidden />
|
||||
<div className="flex-shrink-0 relative w-7 h-7 rounded-full flex justify-center items-center z-10 bg-gray-500 text-white border border-white uppercase font-medium">
|
||||
{currentUser?.member?.avatar_url && currentUser?.member?.avatar_url !== "" ? (
|
||||
<img
|
||||
|
||||
@@ -25,6 +25,13 @@ export interface IRepliesStore {
|
||||
commentId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => Promise<TIssueComment>;
|
||||
updateReply: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
replyId: string,
|
||||
data: Partial<TIssueComment>
|
||||
) => Promise<TIssueComment>;
|
||||
deleteReply: (workspaceSlug: string, projectId: string, issueId: string, replyId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -42,10 +49,10 @@ export class RepliesStore implements IRepliesStore {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
repliesMap: observable,
|
||||
|
||||
// actions
|
||||
fetchReplies: action,
|
||||
createReply: action,
|
||||
updateReply: action,
|
||||
deleteReply: action,
|
||||
});
|
||||
|
||||
@@ -59,10 +66,17 @@ export class RepliesStore implements IRepliesStore {
|
||||
const commentMap = this.repliesMap.get(commentId);
|
||||
if (!commentMap) return undefined;
|
||||
|
||||
return Array.from(commentMap.keys());
|
||||
// order ids by created_at
|
||||
const orderedIds = Array.from(commentMap.keys()).sort((a, b) => {
|
||||
const aCreatedAt = commentMap.get(a)?.created_at;
|
||||
const bCreatedAt = commentMap.get(b)?.created_at;
|
||||
if (!aCreatedAt || !bCreatedAt) return 0;
|
||||
return new Date(aCreatedAt).getTime() - new Date(bCreatedAt).getTime();
|
||||
});
|
||||
|
||||
return orderedIds;
|
||||
});
|
||||
|
||||
// actions
|
||||
fetchReplies = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -151,6 +165,7 @@ export class RepliesStore implements IRepliesStore {
|
||||
|
||||
const oldValues = instance.asJSON;
|
||||
|
||||
// Optimistic update with the data being sent
|
||||
runInAction(() => {
|
||||
instance.update(data);
|
||||
});
|
||||
@@ -164,6 +179,13 @@ export class RepliesStore implements IRepliesStore {
|
||||
data
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
instance.update({
|
||||
updated_at: updatedReply.updated_at,
|
||||
edited_at: updatedReply.edited_at,
|
||||
});
|
||||
});
|
||||
|
||||
return updatedReply;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"./popover": "./dist/popover/index.js",
|
||||
"./portal": "./dist/portal/index.js",
|
||||
"./scrollarea": "./dist/scrollarea/index.js",
|
||||
"./separator": "./dist/separator/index.js",
|
||||
"./skeleton": "./dist/skeleton/index.js",
|
||||
"./switch": "./dist/switch/index.js",
|
||||
"./tab-navigation": "./dist/tab-navigation/index.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {FC} from "react";
|
||||
import type { FC } from "react";
|
||||
import { AudioFileIcon, CodeFileIcon, DocumentFileIcon, ImageFileIcon, VideoFileIcon } from "../../icons/attachments";
|
||||
|
||||
const extensionToIconMap: Record<string, FC<{ className?: string }>> = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ISvgIcons } from "../type";
|
||||
export const CommentReplyIcon: React.FC<ISvgIcons> = ({ color = "currentColor", ...rest }) => (
|
||||
<IconWrapper color={color} {...rest}>
|
||||
<path
|
||||
d="M12 7.42481V3.8252C12 3.25494 11.9993 2.86462 11.9747 2.5625C11.9506 2.26796 11.9072 2.11207 11.8506 2.00098C11.7188 1.74225 11.5078 1.53124 11.2491 1.39941C11.138 1.34288 10.9821 1.29946 10.6875 1.27539C10.3854 1.25072 9.9951 1.25 9.42485 1.25H3.82524C3.25499 1.25 2.86467 1.25072 2.56254 1.27539C2.268 1.29946 2.11212 1.34288 2.00102 1.39941C1.7423 1.53124 1.53128 1.74225 1.39946 2.00098C1.34293 2.11207 1.2995 2.26796 1.27544 2.5625C1.25076 2.86462 1.25004 3.25495 1.25004 3.8252V7.95801C1.25004 8.6177 1.25553 8.82569 1.29692 8.98047C1.42406 9.45497 1.79507 9.82598 2.26958 9.95312C2.42436 9.99452 2.63235 10 3.29204 10C3.63707 10.0002 3.91704 10.2799 3.91704 10.625V11.9902L5.35844 10.8379C5.66347 10.5939 5.88163 10.415 6.13188 10.2871C6.3425 10.1795 6.56719 10.1013 6.79887 10.0537C7.07428 9.99719 7.35634 10 7.74712 10H9.42485C9.9951 10 10.3854 9.99928 10.6875 9.97461C10.9821 9.95054 11.138 9.90712 11.2491 9.85059C11.5078 9.71876 11.7188 9.50775 11.8506 9.24902C11.9072 9.13793 11.9506 8.98204 11.9747 8.6875C11.9993 8.38538 12 7.99506 12 7.42481ZM13.25 7.42481C13.25 7.97433 13.2505 8.42428 13.2207 8.78906C13.1903 9.16112 13.1252 9.49977 12.9639 9.81641C12.7122 10.3103 12.3104 10.7122 11.8165 10.9639C11.4998 11.1251 11.1612 11.1903 10.7891 11.2207C10.4243 11.2505 9.97437 11.25 9.42485 11.25H7.74712C7.30639 11.25 7.17224 11.2532 7.04985 11.2783C6.92869 11.3032 6.81138 11.3441 6.70122 11.4004C6.58992 11.4572 6.48314 11.5389 6.13872 11.8145L4.54887 13.0869C4.41993 13.1901 4.2891 13.2943 4.17583 13.3691C4.07215 13.4377 3.87768 13.5564 3.62602 13.5566C3.33426 13.5569 3.05808 13.4243 2.87602 13.1963C2.71896 12.9995 2.68999 12.7731 2.67876 12.6494C2.66648 12.5142 2.66704 12.3467 2.66704 12.1816V11.2441C2.39009 11.2365 2.15706 11.2169 1.94536 11.1602C1.03979 10.9174 0.332675 10.2103 0.0898885 9.30469C-0.00492448 8.95084 4.47304e-05 8.53775 4.47304e-05 7.95801V3.8252C4.47304e-05 3.27567 -0.000433179 2.82572 0.0293416 2.46094C0.0597401 2.08888 0.124899 1.75023 0.286178 1.43359C0.537845 0.939669 0.939713 0.537801 1.43364 0.286133C1.75027 0.124855 2.08892 0.0596958 2.46098 0.0292973C2.82576 -0.000477467 3.27572 4.62387e-07 3.82524 4.62387e-07H9.42485C9.97437 4.62387e-07 10.4243 -0.000477435 10.7891 0.0292973C11.1612 0.0596958 11.4998 0.124855 11.8165 0.286133C12.3104 0.537801 12.7122 0.939669 12.9639 1.43359C13.1252 1.75023 13.1903 2.08888 13.2207 2.46094C13.2505 2.82572 13.25 3.27567 13.25 3.8252V7.42481Z"
|
||||
d="M13.375 8.79981V5.2002C13.375 4.62994 13.3743 4.23962 13.3497 3.9375C13.3256 3.64296 13.2822 3.48707 13.2256 3.37598C13.0938 3.11725 12.8828 2.90624 12.6241 2.77441C12.513 2.71788 12.3571 2.67446 12.0625 2.65039C11.7604 2.62572 11.3701 2.625 10.7998 2.625H5.20024C4.62999 2.625 4.23967 2.62572 3.93754 2.65039C3.643 2.67446 3.48712 2.71788 3.37602 2.77441C3.1173 2.90624 2.90628 3.11725 2.77446 3.37598C2.71793 3.48707 2.6745 3.64296 2.65044 3.9375C2.62576 4.23962 2.62504 4.62995 2.62504 5.2002V9.33301C2.62504 9.9927 2.63053 10.2007 2.67192 10.3555C2.79906 10.83 3.17007 11.201 3.64458 11.3281C3.79936 11.3695 4.00735 11.375 4.66704 11.375C5.01207 11.3752 5.29204 11.6549 5.29204 12V13.3652L6.73344 12.2129C7.03847 11.9689 7.25663 11.79 7.50688 11.6621C7.7175 11.5545 7.94219 11.4763 8.17387 11.4287C8.44928 11.3722 8.73134 11.375 9.12212 11.375H10.7998C11.3701 11.375 11.7604 11.3743 12.0625 11.3496C12.3571 11.3255 12.513 11.2821 12.6241 11.2256C12.8828 11.0938 13.0938 10.8827 13.2256 10.624C13.2822 10.5129 13.3256 10.357 13.3497 10.0625C13.3743 9.76038 13.375 9.37006 13.375 8.79981ZM14.625 8.79981C14.625 9.34933 14.6255 9.79928 14.5957 10.1641C14.5653 10.5361 14.5002 10.8748 14.3389 11.1914C14.0872 11.6853 13.6854 12.0872 13.1915 12.3389C12.8748 12.5001 12.5362 12.5653 12.1641 12.5957C11.7993 12.6255 11.3494 12.625 10.7998 12.625H9.12212C8.68139 12.625 8.54724 12.6282 8.42485 12.6533C8.30369 12.6782 8.18638 12.7191 8.07622 12.7754C7.96492 12.8322 7.85814 12.9139 7.51372 13.1895L5.92387 14.4619C5.79493 14.5651 5.6641 14.6693 5.55083 14.7441C5.44715 14.8127 5.25268 14.9314 5.00102 14.9316C4.70926 14.9319 4.43308 14.7993 4.25102 14.5713C4.09396 14.3745 4.06499 14.1481 4.05376 14.0244C4.04148 13.8892 4.04204 13.7217 4.04204 13.5566V12.6191C3.76509 12.6115 3.53206 12.5919 3.32036 12.5352C2.41479 12.2924 1.70767 11.5853 1.46489 10.6797C1.37008 10.3258 1.37504 9.91275 1.37504 9.33301V5.2002C1.37504 4.65067 1.37457 4.20072 1.40434 3.83594C1.43474 3.46388 1.4999 3.12523 1.66118 2.80859C1.91285 2.31467 2.31471 1.9128 2.80864 1.66113C3.12527 1.49985 3.46392 1.4347 3.83598 1.4043C4.20076 1.37452 4.65072 1.375 5.20024 1.375H10.7998C11.3494 1.375 11.7993 1.37452 12.1641 1.4043C12.5362 1.4347 12.8748 1.49986 13.1915 1.66113C13.6854 1.9128 14.0872 2.31467 14.3389 2.80859C14.5002 3.12523 14.5653 3.46388 14.5957 3.83594C14.6255 4.20072 14.625 4.65067 14.625 5.2002V8.79981Z"
|
||||
fill={color}
|
||||
/>
|
||||
</IconWrapper>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./separator";
|
||||
@@ -11,7 +11,7 @@ interface SeparatorProps extends React.ComponentProps<typeof SeparatorPrimitive>
|
||||
}
|
||||
|
||||
const Separator = React.forwardRef(function Separator(
|
||||
{ orientation = "horizontal", ...props }: SeparatorProps,
|
||||
{ orientation = "horizontal", className, ...props }: SeparatorProps,
|
||||
ref: React.ForwardedRef<React.ElementRef<typeof SeparatorPrimitive>>
|
||||
) {
|
||||
return (
|
||||
@@ -21,7 +21,12 @@ const Separator = React.forwardRef(function Separator(
|
||||
data-slot="separator"
|
||||
data-orientation={orientation}
|
||||
{...props}
|
||||
className={cn("bg-custom-border-200", "shrink-0", orientation === "horizontal" ? "h-px w-full" : "h-full w-px")}
|
||||
className={cn(
|
||||
"bg-custom-border-200",
|
||||
"shrink-0",
|
||||
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineConfig({
|
||||
"src/popover/index.ts",
|
||||
"src/portal/index.ts",
|
||||
"src/scrollarea/index.ts",
|
||||
"src/separator/index.ts",
|
||||
"src/skeleton/index.ts",
|
||||
"src/switch/index.ts",
|
||||
"src/table/index.ts",
|
||||
|
||||
@@ -4,5 +4,6 @@ import type { TIssueComment } from "./issue_comment";
|
||||
export type TCommentReplyOperations = {
|
||||
fetchReplies: (commentId: string) => Promise<void>;
|
||||
createReply: (commentId: string, data: Partial<TIssueComment>) => Promise<TIssueComment | undefined>;
|
||||
updateReply: (replyId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
deleteReply: (replyId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user