forked from github/plane
Merge pull request #2100 from makeplane/feat/comment_reactions
feat: plane space comment reactions
This commit is contained in:
commit
f11ae00201
@ -1,17 +1,22 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
|
||||||
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
// lib
|
// lib
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { TipTapEditor } from "components/tiptap";
|
||||||
|
import { CommentReactions } from "components/issues/peek-overview";
|
||||||
// icons
|
// icons
|
||||||
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { Comment } from "types/issue";
|
import { Comment } from "types/issue";
|
||||||
// components
|
|
||||||
import { TipTapEditor } from "components/tiptap";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -76,7 +81,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||||
<>Commented {timeAgo(comment.created_at)}</>
|
<>commented {timeAgo(comment.created_at)}</>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="issue-comments-section p-0">
|
<div className="issue-comments-section p-0">
|
||||||
@ -125,6 +130,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
editable={false}
|
editable={false}
|
||||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||||
/>
|
/>
|
||||||
|
<CommentReactions commentId={comment.id} projectId={comment.project} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { ReactionSelector, Tooltip } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { groupReactions, renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
commentId: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||||
|
const { commentId, projectId } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspace_slug } = router.query;
|
||||||
|
|
||||||
|
const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const peekId = issueDetailsStore.peekId;
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
|
||||||
|
const commentReactions = peekId
|
||||||
|
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
|
||||||
|
: [];
|
||||||
|
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||||
|
|
||||||
|
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||||
|
|
||||||
|
const handleAddReaction = (reactionHex: string) => {
|
||||||
|
if (!workspace_slug || !projectId || !peekId) return;
|
||||||
|
|
||||||
|
issueDetailsStore.addCommentReaction(
|
||||||
|
workspace_slug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
peekId,
|
||||||
|
commentId,
|
||||||
|
reactionHex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveReaction = (reactionHex: string) => {
|
||||||
|
if (!workspace_slug || !projectId || !peekId) return;
|
||||||
|
|
||||||
|
issueDetailsStore.removeCommentReaction(
|
||||||
|
workspace_slug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
peekId,
|
||||||
|
commentId,
|
||||||
|
reactionHex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReactionClick = (reactionHex: string) => {
|
||||||
|
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||||
|
|
||||||
|
if (userReaction) handleRemoveReaction(reactionHex);
|
||||||
|
else handleAddReaction(reactionHex);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1.5 items-center mt-2">
|
||||||
|
<ReactionSelector
|
||||||
|
onSelect={(value) => {
|
||||||
|
userStore.requiredLogin(() => {
|
||||||
|
handleReactionClick(value);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
position="top"
|
||||||
|
selected={userReactions?.map((r) => r.reaction)}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||||
|
const reactions = groupedReactions?.[reaction] ?? [];
|
||||||
|
const REACTIONS_LIMIT = 1000;
|
||||||
|
|
||||||
|
if (reactions.length > 0)
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={reaction}
|
||||||
|
tooltipContent={
|
||||||
|
<div>
|
||||||
|
{reactions
|
||||||
|
.map((r) => r.actor_detail.display_name)
|
||||||
|
.splice(0, REACTIONS_LIMIT)
|
||||||
|
.join(", ")}
|
||||||
|
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
userStore.requiredLogin(() => {
|
||||||
|
handleReactionClick(reaction);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||||
|
commentReactions?.some(
|
||||||
|
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||||
|
)
|
||||||
|
? "bg-custom-primary-100/10"
|
||||||
|
: "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{renderEmoji(reaction)}</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
commentReactions?.some(
|
||||||
|
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||||
|
)
|
||||||
|
? "text-custom-primary-100"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groupedReactions?.[reaction].length}{" "}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
3
space/components/issues/peek-overview/comment/index.ts
Normal file
3
space/components/issues/peek-overview/comment/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./add-comment";
|
||||||
|
export * from "./comment-detail-card";
|
||||||
|
export * from "./comment-reactions";
|
@ -1,5 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -41,7 +43,7 @@ const peekModes: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||||
const { handleClose, issueDetails } = props;
|
const { handleClose, issueDetails } = props;
|
||||||
|
|
||||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||||
@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./comment";
|
||||||
export * from "./full-screen-peek-view";
|
export * from "./full-screen-peek-view";
|
||||||
export * from "./header";
|
export * from "./header";
|
||||||
export * from "./issue-activity";
|
export * from "./issue-activity";
|
||||||
@ -8,5 +9,3 @@ export * from "./side-peek-view";
|
|||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./issue-vote-reactions";
|
export * from "./issue-vote-reactions";
|
||||||
export * from "./issue-emoji-reactions";
|
export * from "./issue-emoji-reactions";
|
||||||
export * from "./comment-detail-card";
|
|
||||||
export * from "./add-comment";
|
|
||||||
|
@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
|||||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
||||||
const groupedReactions = groupReactions(reactions, "reaction");
|
const groupedReactions = groupReactions(reactions, "reaction");
|
||||||
|
|
||||||
const handleReactionSelectClick = (reactionHex: string) => {
|
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||||
|
|
||||||
|
const handleAddReaction = (reactionHex: string) => {
|
||||||
if (!workspace_slug || !project_slug || !issueId) return;
|
if (!workspace_slug || !project_slug || !issueId) return;
|
||||||
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
|
||||||
if (userReaction) return;
|
|
||||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReactionClick = (reactionHex: string) => {
|
const handleRemoveReaction = (reactionHex: string) => {
|
||||||
if (!workspace_slug || !project_slug || !issueId) return;
|
if (!workspace_slug || !project_slug || !issueId) return;
|
||||||
|
|
||||||
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReactionClick = (reactionHex: string) => {
|
||||||
|
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||||
|
|
||||||
|
if (userReaction) handleRemoveReaction(reactionHex);
|
||||||
|
else handleAddReaction(reactionHex);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) return;
|
if (user) return;
|
||||||
userStore.fetchCurrentUser();
|
userStore.fetchCurrentUser();
|
||||||
@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
|||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
userStore.requiredLogin(() => {
|
userStore.requiredLogin(() => {
|
||||||
handleReactionSelectClick(value);
|
handleReactionClick(value);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
selected={userReactions?.map((r) => r.reaction)}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
@ -12,13 +12,14 @@ import { Icon } from "components/ui";
|
|||||||
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
position?: "top" | "bottom";
|
|
||||||
onSelect: (emoji: string) => void;
|
onSelect: (emoji: string) => void;
|
||||||
|
position?: "top" | "bottom";
|
||||||
|
selected?: string[];
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReactionSelector: React.FC<Props> = (props) => {
|
export const ReactionSelector: React.FC<Props> = (props) => {
|
||||||
const { onSelect, position, size } = props;
|
const { onSelect, position, selected = [], size } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@ -51,7 +52,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
position === "top" ? "-top-12" : "-bottom-12"
|
position === "top" ? "-top-12" : "-bottom-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
{reactionEmojis.map((emoji) => (
|
{reactionEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
@ -61,7 +62,9 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
onSelect(emoji);
|
onSelect(emoji);
|
||||||
closePopover();
|
closePopover();
|
||||||
}}
|
}}
|
||||||
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90"
|
className={`grid place-items-center select-none rounded-md text-sm p-1 ${
|
||||||
|
selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{renderEmoji(emoji)}
|
{renderEmoji(emoji)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -93,16 +93,6 @@ class IssueService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise<any> {
|
|
||||||
return this.get(
|
|
||||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`
|
|
||||||
)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
||||||
return this.post(
|
return this.post(
|
||||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
||||||
@ -140,6 +130,39 @@ class IssueService extends APIService {
|
|||||||
throw error?.response;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createCommentReaction(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
data: {
|
||||||
|
reaction: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
return this.post(
|
||||||
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCommentReaction(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
): Promise<any> {
|
||||||
|
return this.delete(
|
||||||
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IssueService;
|
export default IssueService;
|
||||||
|
@ -32,6 +32,20 @@ export interface IIssueDetailStore {
|
|||||||
data: any
|
data: any
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
||||||
|
addCommentReaction: (
|
||||||
|
workspaceId: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => void;
|
||||||
|
removeCommentReaction: (
|
||||||
|
workspaceId: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => void;
|
||||||
// issue reactions
|
// issue reactions
|
||||||
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||||
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||||
@ -61,8 +75,17 @@ class IssueDetailStore implements IssueDetailStore {
|
|||||||
details: observable.ref,
|
details: observable.ref,
|
||||||
// actions
|
// actions
|
||||||
setPeekId: action,
|
setPeekId: action,
|
||||||
fetchIssueDetails: action,
|
|
||||||
setPeekMode: action,
|
setPeekMode: action,
|
||||||
|
fetchIssueDetails: action,
|
||||||
|
addIssueComment: action,
|
||||||
|
updateIssueComment: action,
|
||||||
|
deleteIssueComment: action,
|
||||||
|
addCommentReaction: action,
|
||||||
|
removeCommentReaction: action,
|
||||||
|
addIssueReaction: action,
|
||||||
|
removeIssueReaction: action,
|
||||||
|
addIssueVote: action,
|
||||||
|
removeIssueVote: action,
|
||||||
});
|
});
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -175,6 +198,94 @@ class IssueDetailStore implements IssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => {
|
||||||
|
const newReaction = {
|
||||||
|
id: uuidv4(),
|
||||||
|
comment: commentId,
|
||||||
|
reaction: reactionHex,
|
||||||
|
actor_detail: this.rootStore.user.currentActor,
|
||||||
|
};
|
||||||
|
const newComments = this.details[issueId].comments.map((comment) => ({
|
||||||
|
...comment,
|
||||||
|
comment_reactions:
|
||||||
|
comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: [...newComments],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, {
|
||||||
|
reaction: reactionHex,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: issueComments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
removeCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const comment = this.details[issueId].comments.find((c) => c.id === commentId);
|
||||||
|
const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: this.details[issueId].comments.map((c) => ({
|
||||||
|
...c,
|
||||||
|
comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex);
|
||||||
|
} catch (error) {
|
||||||
|
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: issueComments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -68,25 +68,30 @@ export interface IVote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: string;
|
|
||||||
actor_detail: ActorDetail;
|
actor_detail: ActorDetail;
|
||||||
issue_detail: IssueDetail;
|
|
||||||
project_detail: ProjectDetail;
|
|
||||||
workspace_detail: WorkspaceDetail;
|
|
||||||
comment_reactions: any[];
|
|
||||||
is_member: boolean;
|
|
||||||
created_at: Date;
|
|
||||||
updated_at: Date;
|
|
||||||
comment_stripped: string;
|
|
||||||
comment_html: string;
|
|
||||||
attachments: any[];
|
|
||||||
access: string;
|
access: string;
|
||||||
created_by: string;
|
|
||||||
updated_by: string;
|
|
||||||
project: string;
|
|
||||||
workspace: string;
|
|
||||||
issue: string;
|
|
||||||
actor: string;
|
actor: string;
|
||||||
|
attachments: any[];
|
||||||
|
comment_html: string;
|
||||||
|
comment_reactions: {
|
||||||
|
actor_detail: ActorDetail;
|
||||||
|
comment: string;
|
||||||
|
id: string;
|
||||||
|
reaction: string;
|
||||||
|
}[];
|
||||||
|
comment_stripped: string;
|
||||||
|
created_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
id: string;
|
||||||
|
is_member: boolean;
|
||||||
|
issue: string;
|
||||||
|
issue_detail: IssueDetail;
|
||||||
|
project: string;
|
||||||
|
project_detail: ProjectDetail;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
workspace: string;
|
||||||
|
workspace_detail: WorkspaceDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueReaction {
|
export interface IIssueReaction {
|
||||||
|
@ -61,7 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
position === "top" ? "-top-12" : "-bottom-12"
|
position === "top" ? "-top-12" : "-bottom-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
{reactionEmojis.map((emoji) => (
|
{reactionEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
|
Loading…
Reference in New Issue
Block a user