Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions apps/web/core/components/comments/card/root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useRef, useState } from "react";
import { useCallback, 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
Expand Down Expand Up @@ -35,25 +35,48 @@ export const CommentCard = observer(function CommentCard(props: TCommentCard) {
} = props;
// states
const [isEditing, setIsEditing] = useState(false);
const [isPickerOpen, setIsPickerOpen] = useState(false);
// refs
const readOnlyEditorRef = useRef<EditorRefApi>(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 (
<CommentBlock
comment={comment}
quickActions={
!disabled && (
<CommentQuickActions
activityOperations={activityOperations}
comment={comment}
setEditMode={() => setIsEditing(true)}
showAccessSpecifier={showAccessSpecifier}
showCopyLinkOption={showCopyLinkOption}
/>
<div className="flex items-center gap-1">
<EmojiReactionPicker
isOpen={isPickerOpen}
handleToggle={setIsPickerOpen}
onChange={handleEmojiSelect}
disabled={disabled}
label={<EmojiReactionButton onAddReaction={() => setIsPickerOpen(true)} />}
placement="bottom-start"
/>

<CommentQuickActions
activityOperations={activityOperations}
comment={comment}
setEditMode={() => setIsEditing(true)}
showAccessSpecifier={showAccessSpecifier}
showCopyLinkOption={showCopyLinkOption}
/>
</div>
)
}
ends={ends}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/core/components/comments/comment-reaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export const CommentReactions = observer(function CommentReactions(props: TProps

if (!userReactions) return null;

// Don't render anything if there are no reactions and it's disabled
if (reactions.length === 0 && disabled) return null;

// Don't show the add button if there are no reactions
const showAddButton = !disabled && reactions.length > 0;
Comment on lines +64 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential UX issue: Users cannot add the first reaction.

The logic prevents showing the add button when reactions.length === 0, but this also means users cannot add the very first reaction to a comment since EmojiReactionGroup will render nothing to trigger the picker.

If the intent is to allow adding reactions through a separate UI element (like the quick actions menu), this is fine. Otherwise, consider allowing the add button when not disabled, regardless of existing reactions:

🔎 Proposed fix if add button should be shown for first reaction
-  // Don't show the add button if there are no reactions
-  const showAddButton = !disabled && reactions.length > 0;
+  // Show the add button when not disabled
+  const showAddButton = !disabled;
🤖 Prompt for AI Agents
apps/web/core/components/comments/comment-reaction.tsx around lines 64 to 68:
the current logic hides the add-button when there are zero reactions
(showAddButton = !disabled && reactions.length > 0), preventing users from
adding the first reaction; change showAddButton to depend only on !disabled
(e.g., showAddButton = !disabled) so the add button is shown even when
reactions.length === 0, and keep the early return (if (reactions.length === 0 &&
disabled) return null) so the component still renders when not disabled.


return (
<div className="relative">
<EmojiReactionPicker
Expand All @@ -72,7 +78,7 @@ export const CommentReactions = observer(function CommentReactions(props: TProps
<EmojiReactionGroup
reactions={reactions}
onReactionClick={handleReactionClick}
showAddButton={!disabled}
showAddButton={showAddButton}
onAddReaction={() => setIsPickerOpen(true)}
/>
}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/core/components/comments/quick-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useMemo } from "react";
import { observer } from "mobx-react";
import { Globe2, Link, Lock, Pencil, Trash2 } from "lucide-react";
import { Globe2, Link, Lock, Pencil, Trash2, MoreHorizontal } from "lucide-react";
// plane imports
import { EIssueCommentAccessSpecifier } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IconButton } from "@plane/propel/icon-button";
import type { TIssueComment, TCommentsOperations } from "@plane/types";
import type { TContextMenuItem } from "@plane/ui";
import { CustomMenu } from "@plane/ui";
Expand Down Expand Up @@ -76,7 +77,7 @@ export const CommentQuickActions = observer(function CommentQuickActions(props:
);

return (
<CustomMenu ellipsis closeOnSelect>
<CustomMenu customButton={<IconButton icon={MoreHorizontal} variant="ghost" size="sm" />} closeOnSelect>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function SidebarPropertyListItem(props: TSidebarPropertyListItemProps) {
const { icon: Icon, label, children, appendElement, childrenClassName } = props;

return (
<div className="flex items-center gap-2">
<div className="flex items-start gap-2">
<div className="flex shrink-0 items-center gap-1.5 w-30 text-body-xs-regular text-tertiary h-7.5">
<Icon className="size-4 shrink-0" />
<span>{label}</span>
Expand Down
28 changes: 12 additions & 16 deletions packages/propel/src/emoji-reaction/emoji-reaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { stringToEmoji } from "../emoji-icon-picker";
import { AddReactionIcon } from "../icons";
import { Tooltip } from "../tooltip";
import { cn } from "../utils";
import { IconButton } from "../icon-button";

export interface EmojiReactionType {
emoji: string;
Expand Down Expand Up @@ -100,22 +101,17 @@ const EmojiReactionButton = React.forwardRef(function EmojiReactionButton(
ref: React.ForwardedRef<HTMLButtonElement>
) {
return (
<button
ref={ref}
onClick={onAddReaction}
className={cn(
"inline-flex items-center justify-center rounded-full border border-dashed border-strong",
"bg-surface-1 text-placeholder transition-all duration-200",
"hover:border-accent-strong hover:text-accent-primary hover:bg-accent-primary/5",
"focus:outline-none focus:ring-2 focus:ring-accent-strong/20 focus:ring-offset-1",
"h-6 w-6",
className
)}
title="Add reaction"
{...props}
>
<AddReactionIcon className="h-3 w-3" />
</button>
<Tooltip tooltipContent="Add reaction">
<IconButton
ref={ref}
icon={AddReactionIcon}
variant="ghost"
size="sm"
onClick={onAddReaction}
className={className}
{...props}
/>
</Tooltip>
);
});

Expand Down
1 change: 1 addition & 0 deletions packages/propel/src/icons/arrows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./chevron-down";
export * from "./chevron-left";
export * from "./chevron-right";
export * from "./chevron-up";
export * from "./reply-icon";
13 changes: 13 additions & 0 deletions packages/propel/src/icons/arrows/reply-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";

export function ReplyIcon({ color = "currentColor", ...rest }: ISvgIcons) {
return (
<IconWrapper color={color} clipPathId="clip0_2890_23" {...rest}>
<path
d="M5.52865 4.19526C5.789 3.93491 6.211 3.93491 6.47135 4.19526C6.73168 4.45561 6.7317 4.87763 6.47135 5.13797L4.27604 7.33328H10.6667C11.5507 7.33328 12.3983 7.68472 13.0234 8.30985C13.6485 8.93496 14 9.78257 14 10.6666V11.9999C14 12.3681 13.7015 12.6666 13.3333 12.6666C12.9652 12.6666 12.6667 12.3681 12.6667 11.9999V10.6666C12.6667 10.1362 12.4558 9.62762 12.0807 9.25255C11.7526 8.92442 11.3223 8.72189 10.8646 8.67638L10.6667 8.66662H4.27604L6.47135 10.8619C6.73168 11.1223 6.7317 11.5443 6.47135 11.8046C6.21101 12.065 5.78899 12.065 5.52865 11.8046L2.19531 8.4713C2.1642 8.4402 2.13648 8.40583 2.11198 8.36909C2.09464 8.3431 2.07971 8.31603 2.06641 8.28836C2.05178 8.25798 2.03859 8.22669 2.02865 8.19396C2.02526 8.18281 2.02297 8.17139 2.02018 8.16011C2.0075 8.10875 2 8.05523 2 7.99995C2 7.94683 2.00649 7.89518 2.01823 7.84565C2.02148 7.83195 2.02452 7.81815 2.02865 7.80464C2.03858 7.77214 2.05185 7.74107 2.06641 7.71089C2.07918 7.68436 2.09353 7.65841 2.11003 7.63341C2.13495 7.59564 2.16344 7.56047 2.19531 7.5286L5.52865 4.19526Z"
fill={color}
/>
</IconWrapper>
);
}
1 change: 1 addition & 0 deletions packages/propel/src/icons/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const ArrowsIconsMap = [
{ icon: <Icon name="arrow.chevron-left" />, title: "ChevronLeftIcon" },
{ icon: <Icon name="arrow.chevron-right" />, title: "ChevronRightIcon" },
{ icon: <Icon name="arrow.chevron-up" />, title: "ChevronUpIcon" },
{ icon: <Icon name="arrow.reply" />, title: "ReplyIcon" },
];

export const WorkspaceIconsMap = [
Expand Down
2 changes: 2 additions & 0 deletions packages/propel/src/icons/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChevronDownIcon } from "./arrows/chevron-down";
import { ChevronLeftIcon } from "./arrows/chevron-left";
import { ChevronRightIcon } from "./arrows/chevron-right";
import { ChevronUpIcon } from "./arrows/chevron-up";
import { ReplyIcon } from "./arrows/reply-icon";
import { DefaultIcon } from "./default-icon";
import { BoardLayoutIcon } from "./layouts/board-icon";
import { CalendarLayoutIcon } from "./layouts/calendar-icon";
Expand Down Expand Up @@ -139,6 +140,7 @@ export const ICON_REGISTRY = {
"arrow.chevron-left": ChevronLeftIcon,
"arrow.chevron-right": ChevronRightIcon,
"arrow.chevron-up": ChevronUpIcon,
"arrow.reply": ReplyIcon,

// Default fallback
default: DefaultIcon,
Expand Down
Loading