mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: issue comment reaction
This commit is contained in:
parent
8f8b106a89
commit
8c63a9d7cd
@ -1,20 +1,20 @@
|
||||
export type TIssueCommentReaction = {
|
||||
id: string;
|
||||
comment: string;
|
||||
actor: string;
|
||||
reaction: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
reaction: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
actor: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionMap = {
|
||||
[issue_id: string]: TIssueCommentReaction;
|
||||
[reaction_id: string]: TIssueCommentReaction;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
[comment_id: string]: { [reaction: string]: string[] };
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ export type TIssueReaction = {
|
||||
};
|
||||
|
||||
export type TIssueReactionMap = {
|
||||
[issue_id: string]: TIssueReaction;
|
||||
[reaction_id: string]: TIssueReaction;
|
||||
};
|
||||
|
||||
export type TIssueReactionIdMap = {
|
||||
|
@ -6,6 +6,7 @@ import { useIssueDetail, useMention, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueCommentBlock } from "./comment-block";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { IssueCommentReaction } from "../../reactions/issue-comment";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// services
|
||||
@ -28,7 +29,7 @@ type TIssueCommentCard = {
|
||||
};
|
||||
|
||||
export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
const { commentId, activityOperations, ends, showAccessSpecifier = true } = props;
|
||||
const { workspaceSlug, commentId, activityOperations, ends, showAccessSpecifier = true } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
@ -67,7 +68,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
isEditing && setFocus("comment_html");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
if (!comment) return <></>;
|
||||
if (!comment || !currentUser) return <></>;
|
||||
return (
|
||||
<IssueCommentBlock
|
||||
commentId={commentId}
|
||||
@ -161,7 +162,13 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
{/* <CommentReaction projectId={comment.project} commentId={comment.id} /> */}
|
||||
|
||||
<IssueCommentReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={comment?.project_detail?.id}
|
||||
commentId={comment.id}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</IssueCommentBlock>
|
||||
|
@ -40,8 +40,6 @@ export type TActivityOperations = {
|
||||
createComment: (data: Partial<TIssueComment>) => Promise<void>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
removeComment: (commentId: string) => Promise<void>;
|
||||
createCommentReaction: (commentId: string, reaction: string) => Promise<void>;
|
||||
removeCommentReaction: (commentId: string, reaction: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
@ -106,52 +104,8 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
createCommentReaction: async (commentId: string, reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
|
||||
await createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
setToastAlert({
|
||||
title: "Comment reaction added successfully.",
|
||||
type: "success",
|
||||
message: "Comment reaction added successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment reaction addition failed.",
|
||||
type: "error",
|
||||
message: "Comment reaction addition failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
removeCommentReaction: async (commentId: string, reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
|
||||
await removeCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
setToastAlert({
|
||||
title: "Comment reaction removed successfully.",
|
||||
type: "success",
|
||||
message: "Comment reaction removed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment reaction removal failed.",
|
||||
type: "error",
|
||||
message: "Comment reaction removal failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
createComment,
|
||||
updateComment,
|
||||
removeComment,
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
setToastAlert,
|
||||
]
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, removeComment, setToastAlert]
|
||||
);
|
||||
|
||||
const componentCommonProps = {
|
||||
@ -167,7 +121,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
<div className="text-lg text-custom-text-100">Comments/Activity</div>
|
||||
|
||||
{/* rendering activity */}
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="relative flex items-center gap-1">
|
||||
{activityTabs.map((tab) => (
|
||||
<div
|
||||
@ -190,25 +144,25 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
|
||||
<div className="min-h-[200px]">
|
||||
{activityTab === "all" ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<IssueActivityCommentRoot {...componentCommonProps} activityOperations={activityOperations} />
|
||||
<IssueCommentCreateUpdate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
) : activityTab === "activity" ? (
|
||||
<IssueActivityRoot {...componentCommonProps} />
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<IssueCommentRoot {...componentCommonProps} activityOperations={activityOperations} />
|
||||
<IssueCommentCreateUpdate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
118
web/components/issues/issue-detail/reactions/issue-comment.tsx
Normal file
118
web/components/issues/issue-detail/reactions/issue-comment.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { ReactionSelector } from "./reaction-selector";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
export type TIssueCommentReaction = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
commentId: string;
|
||||
currentUser: IUser;
|
||||
};
|
||||
|
||||
export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, commentId, currentUser } = props;
|
||||
|
||||
// hooks
|
||||
const {
|
||||
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser },
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
} = useIssueDetail();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const reactionIds = getCommentReactionsByCommentId(commentId);
|
||||
const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction);
|
||||
|
||||
const issueCommentReactionOperations = useMemo(
|
||||
() => ({
|
||||
create: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields");
|
||||
await createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
setToastAlert({
|
||||
title: "Reaction created successfully",
|
||||
type: "success",
|
||||
message: "Reaction created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Reaction creation failed",
|
||||
type: "error",
|
||||
message: "Reaction creation failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields");
|
||||
removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id);
|
||||
setToastAlert({
|
||||
title: "Reaction removed successfully",
|
||||
type: "success",
|
||||
message: "Reaction removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Reaction remove failed",
|
||||
type: "error",
|
||||
message: "Reaction remove failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
react: async (reaction: string) => {
|
||||
if (userReactions.includes(reaction)) await issueCommentReactionOperations.remove(reaction);
|
||||
else await issueCommentReactionOperations.create(reaction);
|
||||
},
|
||||
}),
|
||||
[
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
commentId,
|
||||
currentUser,
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
setToastAlert,
|
||||
userReactions,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 relative flex items-center gap-1.5">
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={userReactions}
|
||||
onSelect={issueCommentReactionOperations.react}
|
||||
/>
|
||||
|
||||
{reactionIds &&
|
||||
Object.keys(reactionIds || {}).map(
|
||||
(reaction) =>
|
||||
reactionIds[reaction]?.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => issueCommentReactionOperations.react(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||
{(reactionIds || {})[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -50,7 +50,7 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
},
|
||||
remove: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields");
|
||||
await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id);
|
||||
setToastAlert({
|
||||
title: "Reaction removed successfully",
|
||||
|
@ -101,6 +101,7 @@ export class IssueCommentStore implements IIssueCommentStore {
|
||||
return uniq(concat(_commentIds, commentIds));
|
||||
});
|
||||
comments.forEach((comment) => {
|
||||
this.rootIssueDetail.commentReaction.applyCommentReactions(comment.id, comment?.comment_reactions || []);
|
||||
set(this.commentMap, comment.id, comment);
|
||||
});
|
||||
this.loader = undefined;
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import update from "lodash/update";
|
||||
import concat from "lodash/concat";
|
||||
import find from "lodash/find";
|
||||
import pull from "lodash/pull";
|
||||
// services
|
||||
import { IssueReactionService } from "services/issue";
|
||||
// types
|
||||
import { IIssueDetail } from "./root.store";
|
||||
import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types";
|
||||
// helpers
|
||||
import { groupReactions } from "helpers/emoji.helper";
|
||||
|
||||
export interface IIssueCommentReactionStoreActions {
|
||||
// actions
|
||||
@ -13,6 +19,7 @@ export interface IIssueCommentReactionStoreActions {
|
||||
projectId: string,
|
||||
commentId: string
|
||||
) => Promise<TIssueCommentReaction[]>;
|
||||
applyCommentReactions: (commentId: string, commentReactions: TIssueCommentReaction[]) => void;
|
||||
createCommentReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -23,7 +30,8 @@ export interface IIssueCommentReactionStoreActions {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => Promise<any>;
|
||||
}
|
||||
|
||||
@ -32,8 +40,9 @@ export interface IIssueCommentReactionStore extends IIssueCommentReactionStoreAc
|
||||
commentReactions: TIssueCommentReactionIdMap;
|
||||
commentReactionMap: TIssueCommentReactionMap;
|
||||
// helper methods
|
||||
getCommentReactionsByCommentId: (commentId: string) => string[] | undefined;
|
||||
getCommentReactionsByCommentId: (commentId: string) => { [reaction_id: string]: string[] } | undefined;
|
||||
getCommentReactionById: (reactionId: string) => TIssueCommentReaction | undefined;
|
||||
commentReactionsByUser: (commentId: string, userId: string) => TIssueCommentReaction[];
|
||||
}
|
||||
|
||||
export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
@ -52,6 +61,7 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
commentReactionMap: observable,
|
||||
// actions
|
||||
fetchCommentReactions: action,
|
||||
applyCommentReactions: action,
|
||||
createCommentReaction: action,
|
||||
removeCommentReaction: action,
|
||||
});
|
||||
@ -72,25 +82,66 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
return this.commentReactionMap[reactionId] ?? undefined;
|
||||
};
|
||||
|
||||
commentReactionsByUser = (commentId: string, userId: string) => {
|
||||
if (!commentId || !userId) return [];
|
||||
|
||||
const reactions = this.getCommentReactionsByCommentId(commentId);
|
||||
if (!reactions) return [];
|
||||
|
||||
const _userReactions: TIssueCommentReaction[] = [];
|
||||
Object.keys(reactions).forEach((reaction) => {
|
||||
reactions[reaction].map((reactionId) => {
|
||||
const currentReaction = this.getCommentReactionById(reactionId);
|
||||
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
||||
});
|
||||
});
|
||||
|
||||
return _userReactions;
|
||||
};
|
||||
|
||||
// actions
|
||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => {
|
||||
try {
|
||||
const reactions = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
|
||||
const response = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
|
||||
|
||||
const groupedReactions = groupReactions(response || [], "reaction");
|
||||
|
||||
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||
|
||||
Object.keys(groupedReactions).map((reactionId) => {
|
||||
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||
commentReactionIdsMap[reactionId] = reactionIds;
|
||||
});
|
||||
|
||||
const reactionIds = reactions.map((reaction) => reaction.id);
|
||||
runInAction(() => {
|
||||
set(this.commentReactions, commentId, reactionIds);
|
||||
reactions.forEach((reaction) => {
|
||||
set(this.commentReactionMap, reaction.id, reaction);
|
||||
});
|
||||
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||
response.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||
});
|
||||
|
||||
return reactions;
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
applyCommentReactions = (commentId: string, commentReactions: TIssueCommentReaction[]) => {
|
||||
const groupedReactions = groupReactions(commentReactions || [], "reaction");
|
||||
|
||||
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||
|
||||
Object.keys(groupedReactions).map((reactionId) => {
|
||||
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||
commentReactionIdsMap[reactionId] = reactionIds;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||
commentReactions.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
||||
try {
|
||||
const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, {
|
||||
@ -98,7 +149,10 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.commentReactions[commentId].push(response.id);
|
||||
update(this.commentReactions, [commentId, reaction], (reactionId) => {
|
||||
if (!reactionId) return [response.id];
|
||||
return concat(reactionId, response.id);
|
||||
});
|
||||
set(this.commentReactionMap, response.id, response);
|
||||
});
|
||||
|
||||
@ -108,14 +162,23 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||
}
|
||||
};
|
||||
|
||||
removeCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
||||
removeCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => {
|
||||
try {
|
||||
const reactionIndex = this.commentReactions[commentId].findIndex((_reaction) => _reaction === reaction);
|
||||
if (reactionIndex >= 0)
|
||||
const userReactions = this.commentReactionsByUser(commentId, userId);
|
||||
const currentReaction = find(userReactions, { actor: userId, reaction: reaction });
|
||||
|
||||
if (currentReaction && currentReaction.id) {
|
||||
runInAction(() => {
|
||||
this.commentReactions[commentId].splice(reactionIndex, 1);
|
||||
pull(this.commentReactions[commentId][reaction], currentReaction.id);
|
||||
delete this.commentReactionMap[reaction];
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.issueReactionService.deleteIssueCommentReaction(
|
||||
workspaceSlug,
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
IIssueCommentReactionStoreActions,
|
||||
} from "./comment_reaction.store";
|
||||
|
||||
import { TIssue, TIssueComment, TIssueLink, TIssueRelationTypes } from "@plane/types";
|
||||
import { TIssue, TIssueComment, TIssueCommentReaction, TIssueLink, TIssueRelationTypes } from "@plane/types";
|
||||
|
||||
export type TPeekIssue = {
|
||||
workspaceSlug: string;
|
||||
@ -238,8 +238,15 @@ export class IssueDetail implements IIssueDetail {
|
||||
// comment reaction
|
||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) =>
|
||||
this.commentReaction.fetchCommentReactions(workspaceSlug, projectId, commentId);
|
||||
applyCommentReactions = async (commentId: string, commentReactions: TIssueCommentReaction[]) =>
|
||||
this.commentReaction.applyCommentReactions(commentId, commentReactions);
|
||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
||||
this.commentReaction.createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
removeCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
||||
this.commentReaction.removeCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
removeCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
userId: string
|
||||
) => this.commentReaction.removeCommentReaction(workspaceSlug, projectId, commentId, reaction, userId);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user