chore: peekoverview issue comments and comment reactions (#2507)

This commit is contained in:
guru_sainath 2023-10-20 17:55:20 +05:30 committed by GitHub
parent 4b03802d22
commit 914657334d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 791 additions and 34 deletions

View File

@ -0,0 +1,135 @@
import { FC } from "react";
import Link from "next/link";
import { History } from "lucide-react";
// packages
import { Loader, Tooltip } from "@plane/ui";
// components
import { ActivityIcon, ActivityMessage } from "components/core";
import { IssueCommentCard } from "./comment-card";
// helpers
import { render24HourFormatTime, renderLongDateFormat, timeAgo } from "helpers/date-time.helper";
interface IssueActivityCard {
workspaceSlug: string;
projectId: string;
user: any;
issueComments: any;
issueCommentUpdate: (comment: any) => void;
issueCommentRemove: (commentId: string) => void;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
}
export const IssueActivityCard: FC<IssueActivityCard> = (props) => {
const {
workspaceSlug,
projectId,
user,
issueComments,
issueCommentUpdate,
issueCommentRemove,
issueCommentReactionCreate,
issueCommentReactionRemove,
} = props;
console.log("issueComments", issueComments);
return (
<div className="flow-root">
<ul role="list" className="-mb-4">
{issueComments.map((activityItem: any, index: any) => {
// determines what type of action is performed
const message = activityItem.field ? <ActivityMessage activity={activityItem} /> : "created the issue.";
if ("field" in activityItem && activityItem.field !== "updated_by") {
return (
<li key={activityItem.id}>
<div className="relative pb-1">
{issueComments.length > 1 && index !== issueComments.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-[1.5px] h-full w-0.5 bg-custom-background-100"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-2">
<div>
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-100 text-custom-text-200 ring-white">
{activityItem.field ? (
activityItem.new_value === "restore" ? (
<History className="h-3.5 w-3.5 text-custom-text-200" />
) : (
<ActivityIcon activity={activityItem} />
)
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full h-full w-full object-cover"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name.charAt(0)
: activityItem.actor_detail.display_name.charAt(0)}
</div>
)}
</div>
</div>
</div>
</div>
<div className="min-w-0 flex-1 py-3">
<div className="text-xs text-custom-text-200 break-words">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : activityItem.actor_detail.is_bot ? (
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
) : (
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
<a className="text-gray font-medium">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name
: activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
{message}{" "}
<Tooltip
tooltipContent={`${renderLongDateFormat(activityItem.created_at)}, ${render24HourFormatTime(
activityItem.created_at
)}`}
>
<span className="whitespace-nowrap">{timeAgo(activityItem.created_at)}</span>
</Tooltip>
</div>
</div>
</div>
</div>
</li>
);
} else if ("comment_json" in activityItem)
return (
<div key={activityItem.id} className="mt-4">
<IssueCommentCard
workspaceSlug={workspaceSlug}
projectId={projectId}
user={user}
comment={activityItem}
onSubmit={issueCommentUpdate}
handleCommentDeletion={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
// showAccessSpecifier={showAccessSpecifier}
/>
</div>
);
})}
</ul>
</div>
);
};

View File

@ -0,0 +1,209 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
// services
import { FileService } from "services/file.service";
// ui
import { CustomMenu } from "@plane/ui";
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
// components
import { IssueCommentReaction } from "./comment-reaction";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
import type { IIssueComment } from "types";
// services
const fileService = new FileService();
type IIssueCommentCard = {
comment: IIssueComment;
handleCommentDeletion: (comment: string) => void;
onSubmit: (data: Partial<IIssueComment>) => void;
showAccessSpecifier?: boolean;
workspaceSlug: string;
projectId: string;
user: any;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
};
export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
const {
comment,
handleCommentDeletion,
onSubmit,
showAccessSpecifier = false,
workspaceSlug,
projectId,
user,
issueCommentReactionCreate,
issueCommentReactionRemove,
} = props;
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const [isEditing, setIsEditing] = useState(false);
const {
formState: { isSubmitting },
handleSubmit,
setFocus,
watch,
setValue,
} = useForm<IIssueComment>({
defaultValues: comment,
});
const formSubmit = (formData: Partial<IIssueComment>) => {
if (isSubmitting) return;
setIsEditing(false);
onSubmit({ id: comment.id, ...formData });
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
useEffect(() => {
isEditing && setFocus("comment");
}, [isEditing, setFocus]);
return (
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<img
src={comment.actor_detail.avatar}
alt={
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
/>
) : (
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
{comment.actor_detail.is_bot
? comment.actor_detail.first_name.charAt(0)
: comment.actor_detail.display_name.charAt(0)}
</div>
)}
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">commented {timeAgo(comment.created_at)}</p>
</div>
<div className="issue-comments-section p-0">
<div className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
<div>
<LiteTextEditorWithRef
onEnterKeyPress={handleSubmit(formSubmit)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.deleteImage}
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => {
setValue("comment_json", comment_json);
setValue("comment_html", comment_html);
}}
/>
</div>
<div className="flex gap-1 self-end">
<button
type="button"
disabled={isSubmitting}
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
onClick={handleSubmit(formSubmit)}
>
<Check className="h-3 w-3 text-green-500 duration-300 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"
onClick={() => setIsEditing(false)}
>
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
</button>
</div>
</div>
<div className={`relative ${isEditing ? "hidden" : ""}`}>
{showAccessSpecifier && (
<div className="absolute top-1 right-1.5 z-[1] text-custom-text-300">
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
</div>
)}
<LiteReadOnlyEditorWithRef
ref={showEditorRef}
value={comment.comment_html}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
<div className="mt-1">
<IssueCommentReaction
workspaceSlug={workspaceSlug}
projectId={projectId}
user={user}
comment={comment}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
</div>
</div>
</div>
{user?.id === comment.actor && (
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
<Pencil className="h-3 w-3" />
Edit comment
</CustomMenu.MenuItem>
{showAccessSpecifier && (
<>
{comment.access === "INTERNAL" ? (
<CustomMenu.MenuItem
onClick={() => onSubmit({ id: comment.id, access: "EXTERNAL" })}
className="flex items-center gap-1"
>
<Globe2 className="h-3 w-3" />
Switch to public comment
</CustomMenu.MenuItem>
) : (
<CustomMenu.MenuItem
onClick={() => onSubmit({ id: comment.id, access: "INTERNAL" })}
className="flex items-center gap-1"
>
<Lock className="h-3 w-3" />
Switch to private comment
</CustomMenu.MenuItem>
)}
</>
)}
<CustomMenu.MenuItem
onClick={() => {
handleCommentDeletion(comment.id);
}}
className="flex items-center gap-1"
>
<Trash2 className="h-3 w-3" />
Delete comment
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
);
};

View File

@ -0,0 +1,136 @@
import React from "react";
import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form";
// services
import { FileService } from "services/file.service";
// components
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
// ui
import { Button, Tooltip } from "@plane/ui";
import { Globe2, Lock } from "lucide-react";
// types
import type { IIssueComment } from "types";
const defaultValues: Partial<IIssueComment> = {
access: "INTERNAL",
comment_html: "",
};
type IIssueCommentEditor = {
disabled?: boolean;
onSubmit: (data: IIssueComment) => Promise<void>;
showAccessSpecifier?: boolean;
};
type commentAccessType = {
icon: any;
key: string;
label: "Private" | "Public";
};
const commentAccess: commentAccessType[] = [
{
icon: Lock,
key: "INTERNAL",
label: "Private",
},
{
icon: Globe2,
key: "EXTERNAL",
label: "Public",
},
];
// services
const fileService = new FileService();
export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
const { disabled = false, onSubmit, showAccessSpecifier = false } = props;
const editorRef = React.useRef<any>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
} = useForm<IIssueComment>({ defaultValues });
const handleAddComment = async (formData: IIssueComment) => {
if (!formData.comment_html || isSubmitting) return;
await onSubmit(formData).then(() => {
reset(defaultValues);
editorRef.current?.clearEditor();
});
};
return (
<form onSubmit={handleSubmit(handleAddComment)}>
<div className="space-y-2">
<div className="relative">
{showAccessSpecifier && (
<div className="absolute bottom-2 left-3 z-[1]">
<Controller
control={control}
name="access"
render={({ field: { onChange, value } }) => (
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
{commentAccess.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}>
<button
type="button"
onClick={() => onChange(access.key)}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${
value === access.key ? "bg-custom-background-80" : ""
}`}
>
<access.icon
className={`w-4 h-4 -mt-1 ${
value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
}`}
/>
</button>
</Tooltip>
))}
</div>
)}
/>
</div>
)}
<Controller
name="access"
control={control}
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
<Controller
name="comment_html"
control={control}
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
<LiteTextEditorWithRef
onEnterKeyPress={handleSubmit(handleAddComment)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.deleteImage}
ref={editorRef}
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
customClassName="p-3 min-h-[100px] shadow-sm"
debouncedUpdatesEnabled={false}
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }}
/>
)}
/>
)}
/>
</div>
<Button variant="neutral-primary" type="submit" disabled={isSubmitting || disabled}>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
</div>
</form>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// components
import { IssueReaction } from "../reactions";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// types
import { RootStore } from "store/root";
interface IIssueCommentReaction {
workspaceSlug: any;
projectId: any;
user: any;
comment: any;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
}
export const IssueCommentReaction: FC<IIssueCommentReaction> = observer((props) => {
const { workspaceSlug, projectId, user, comment, issueCommentReactionCreate, issueCommentReactionRemove } = props;
const { issueDetail: issueDetailStore }: RootStore = useMobxStore();
const handleCommentReactionCreate = (reaction: string) => {
if (issueCommentReactionCreate && comment?.id) issueCommentReactionCreate(comment?.id, reaction);
};
const handleCommentReactionRemove = (reaction: string) => {
if (issueCommentReactionRemove && comment?.id) issueCommentReactionRemove(comment?.id, reaction);
};
useSWR(
workspaceSlug && projectId && comment && comment?.id ? `ISSUE+PEEK_OVERVIEW_COMMENT_${comment?.id}` : null,
() => {
if (workspaceSlug && projectId && comment && comment.id) {
issueDetailStore.fetchIssueCommentReactions(workspaceSlug, projectId, comment?.id);
}
}
);
const issueReactions = issueDetailStore?.getIssueCommentReactionsByCommentId(comment.id) || [];
return (
<div>
<IssueReaction
issueReactions={issueReactions}
user={user}
issueReactionCreate={handleCommentReactionCreate}
issueReactionRemove={handleCommentReactionRemove}
position="top"
/>
</div>
);
});

View File

@ -0,0 +1,5 @@
export * from "./view";
export * from "./card";
export * from "./comment-editor";
export * from "./comment-reaction";

View File

@ -0,0 +1,59 @@
import { FC } from "react";
// components
import { IssueActivityCard } from "./card";
import { IssueCommentEditor } from "./comment-editor";
interface IIssueComment {
workspaceSlug: string;
projectId: string;
user: any;
issueComments: any;
issueCommentCreate: (comment: any) => void;
issueCommentUpdate: (comment: any) => void;
issueCommentRemove: (commentId: string) => void;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
}
export const IssueComment: FC<IIssueComment> = (props) => {
const {
workspaceSlug,
projectId,
user,
issueComments,
issueCommentCreate,
issueCommentUpdate,
issueCommentRemove,
issueCommentReactionCreate,
issueCommentReactionRemove,
} = props;
const handleAddComment = async (formData: any) => {
if (!formData.comment_html) return;
await issueCommentCreate(formData);
};
return (
<div className="space-y-4">
<div className="font-medium text-xl">Activity</div>
<div className="space-y-2">
<IssueCommentEditor
onSubmit={handleAddComment}
// showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/>
<IssueActivityCard
workspaceSlug={workspaceSlug}
projectId={projectId}
user={user}
issueComments={issueComments}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
</div>
);
};

View File

@ -30,7 +30,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
}, 1500);
return (
<div className="space-y-3">
<div className="space-y-4">
<div className="font-medium text-sm text-custom-text-200">
{issue?.project_detail?.identifier}-{issue?.sequence_id}
</div>
@ -46,6 +46,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
onChange={(description: Object, description_html: string) => {
debouncedIssueDescription(description_html);
}}
customClassName="p-3 min-h-[80px] shadow-sm"
/>
<IssueReaction

View File

@ -7,10 +7,11 @@ interface IIssueReaction {
user: any;
issueReactionCreate: (reaction: string) => void;
issueReactionRemove: (reaction: string) => void;
position?: "top" | "bottom";
}
export const IssueReaction: FC<IIssueReaction> = (props) => {
const { issueReactions, user, issueReactionCreate, issueReactionRemove } = props;
const { issueReactions, user, issueReactionCreate, issueReactionRemove, position = "bottom" } = props;
const handleReaction = (reaction: string) => {
const isReactionAvailable =
@ -22,7 +23,7 @@ export const IssueReaction: FC<IIssueReaction> = (props) => {
return (
<div className="relative flex items-center flex-wrap gap-2">
<IssueReactionSelector onSelect={handleReaction} position="bottom" />
<IssueReactionSelector onSelect={handleReaction} position={position} />
<IssueReactionPreview issueReactions={issueReactions} user={user} handleReaction={handleReaction} />
</div>
);

View File

@ -34,13 +34,26 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}
};
const issueReactionCreate = (data: string) => {
issueDetailStore.createIssueReaction(workspaceSlug, projectId, issueId, data);
};
const issueReactionCreate = (reaction: string) =>
issueDetailStore.createIssueReaction(workspaceSlug, projectId, issueId, reaction);
const issueReactionRemove = (data: string) => {
issueDetailStore.removeIssueReaction(workspaceSlug, projectId, issueId, data);
};
const issueReactionRemove = (reaction: string) =>
issueDetailStore.removeIssueReaction(workspaceSlug, projectId, issueId, reaction);
const issueCommentCreate = (comment: any) =>
issueDetailStore.createIssueComment(workspaceSlug, projectId, issueId, comment);
const issueCommentUpdate = (comment: any) =>
issueDetailStore.updateIssueComment(workspaceSlug, projectId, issueId, comment?.id, comment);
const issueCommentRemove = (commentId: string) =>
issueDetailStore.removeIssueComment(workspaceSlug, projectId, issueId, commentId);
const issueCommentReactionCreate = (commentId: string, reaction: string) =>
issueDetailStore.creationIssueCommentReaction(workspaceSlug, projectId, commentId, reaction);
const issueCommentReactionRemove = (commentId: string, reaction: string) =>
issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, commentId, reaction);
return (
<IssueView
@ -53,6 +66,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
>
{children}
</IssueView>

View File

@ -6,6 +6,7 @@ import useSWR from "swr";
// components
import { PeekOverviewIssueDetails } from "./issue-detail";
import { PeekOverviewProperties } from "./properties";
import { IssueComment } from "./activity";
// types
import { IIssue } from "types";
import { RootStore } from "store/root";
@ -19,6 +20,11 @@ interface IIssueView {
issueUpdate: (issue: Partial<IIssue>) => void;
issueReactionCreate: (reaction: string) => void;
issueReactionRemove: (reaction: string) => void;
issueCommentCreate: (comment: any) => void;
issueCommentUpdate: (comment: any) => void;
issueCommentRemove: (commentId: string) => void;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
states: any;
members: any;
priorities: any;
@ -53,6 +59,11 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueUpdate,
issueReactionCreate,
issueReactionRemove,
issueCommentCreate,
issueCommentUpdate,
issueCommentRemove,
issueCommentReactionCreate,
issueCommentReactionRemove,
states,
members,
priorities,
@ -108,6 +119,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const issue = issueDetailStore.getIssue;
const issueReactions = issueDetailStore.getIssueReactions;
const issueComments = issueDetailStore.getIssueComments;
const user = userStore?.currentUser;
return (
@ -178,7 +191,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issue && (
<>
{["side-peek", "modal"].includes(peekMode) ? (
<div className="space-y-8 p-4 py-5">
<div className="space-y-6 p-4 py-5">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
@ -197,11 +210,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
priorities={priorities}
/>
{/* <div className="border border-red-500">Activity</div> */}
<div className="border-t border-custom-border-400" />
<IssueComment
workspaceSlug={workspaceSlug}
projectId={projectId}
user={user}
issueComments={issueComments}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
) : (
<div className="w-full h-full flex">
<div className="w-full h-full space-y-8 p-4 py-5">
<div className="w-full h-full space-y-6 p-4 py-5">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
@ -212,7 +237,19 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueReactionRemove={issueReactionRemove}
/>
{/* <div className="border border-red-500">Activity</div> */}
<div className="border-t border-custom-border-400" />
<IssueComment
workspaceSlug={workspaceSlug}
projectId={projectId}
user={user}
issueComments={issueComments}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
<PeekOverviewProperties

View File

@ -1,6 +1,6 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
// services
import { IssueService, IssueReactionService } from "services/issue";
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
// types
import { RootStore } from "../root";
import { IIssue } from "types";
@ -31,7 +31,7 @@ export interface IIssueDetailStore {
getIssue: IIssue | null;
getIssueReactions: any | null;
getIssueComments: any | null;
getIssueCommentReactions: any | null;
getIssueCommentReactionsByCommentId: any | null;
// fetch issue details
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
@ -59,23 +59,16 @@ export interface IIssueDetailStore {
) => Promise<void>;
removeIssueComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<void>;
fetchIssueCommentReactions: (
fetchIssueCommentReactions: (workspaceSlug: string, projectId: string, commentId: string) => Promise<void>;
creationIssueCommentReaction: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string
) => Promise<void>;
addIssueCommentReaction: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => Promise<void>;
removeIssueCommentReaction: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => Promise<void>;
@ -104,6 +97,7 @@ export class IssueDetailStore implements IIssueDetailStore {
// service
issueService;
issueReactionService;
issueCommentService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@ -120,10 +114,11 @@ export class IssueDetailStore implements IIssueDetailStore {
getIssue: computed,
getIssueReactions: computed,
getIssueComments: computed,
getIssueCommentReactions: computed,
setPeekId: action,
getIssueCommentReactionsByCommentId: action,
fetchIssueDetails: action,
createIssue: action,
updateIssue: action,
@ -141,13 +136,14 @@ export class IssueDetailStore implements IIssueDetailStore {
removeIssueComment: action,
fetchIssueCommentReactions: action,
addIssueCommentReaction: action,
creationIssueCommentReaction: action,
removeIssueCommentReaction: action,
});
this.rootStore = _rootStore;
this.issueService = new IssueService();
this.issueReactionService = new IssueReactionService();
this.issueCommentService = new IssueCommentService();
}
get getIssue() {
@ -168,11 +164,11 @@ export class IssueDetailStore implements IIssueDetailStore {
return _comments || null;
}
get getIssueCommentReactions() {
if (!this.peekId) return null;
const _reactions = this.issue_comment_reactions[this.peekId];
getIssueCommentReactionsByCommentId = (commentId: string) => {
if (!commentId) return null;
const _reactions = this.issue_comment_reactions[commentId];
return _reactions || null;
}
};
setPeekId = (issueId: string | null) => (this.peekId = issueId);
@ -426,6 +422,16 @@ export class IssueDetailStore implements IIssueDetailStore {
// comments
fetchIssueComments = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _issueCommentResponse = await this.issueService.getIssueActivities(workspaceSlug, projectId, issueId);
const _issueComments = {
...this.issue_comments,
[issueId]: [..._issueCommentResponse],
};
runInAction(() => {
this.issue_comments = _issueComments;
});
} catch (error) {
console.warn("error creating the issue comment", error);
throw error;
@ -433,6 +439,22 @@ export class IssueDetailStore implements IIssueDetailStore {
};
createIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => {
try {
const _issueCommentResponse = await this.issueCommentService.createIssueComment(
workspaceSlug,
projectId,
issueId,
data,
undefined
);
const _issueComments = {
...this.issue_comments,
[issueId]: [...this.issue_comments[issueId], _issueCommentResponse],
};
runInAction(() => {
this.issue_comments = _issueComments;
});
} catch (error) {
console.warn("error creating the issue comment", error);
throw error;
@ -446,6 +468,25 @@ export class IssueDetailStore implements IIssueDetailStore {
data: any
) => {
try {
const _issueCommentResponse = await this.issueCommentService.patchIssueComment(
workspaceSlug,
projectId,
issueId,
commentId,
data,
undefined
);
const _issueComments = {
...this.issue_comments,
[issueId]: this.issue_comments[issueId].map((comment: any) =>
comment.id === commentId ? _issueCommentResponse : comment
),
};
runInAction(() => {
this.issue_comments = _issueComments;
});
} catch (error) {
console.warn("error updating the issue comment", error);
throw error;
@ -453,6 +494,16 @@ export class IssueDetailStore implements IIssueDetailStore {
};
removeIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
try {
const _issueComments = {
...this.issue_comments,
[issueId]: this.issue_comments[issueId].filter((comment: any) => comment.id != commentId),
};
await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId, undefined);
runInAction(() => {
this.issue_comments = _issueComments;
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
@ -460,21 +511,52 @@ export class IssueDetailStore implements IIssueDetailStore {
};
// comment reaction
fetchIssueCommentReactions = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
fetchIssueCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => {
try {
const _reactions = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
const _issue_comment_reactions = {
...this.issue_comment_reactions,
[commentId]: groupReactionEmojis(_reactions),
};
runInAction(() => {
this.issue_comment_reactions = _issue_comment_reactions;
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
}
};
addIssueCommentReaction = async (
creationIssueCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => {
let _currentReactions = this.getIssueCommentReactionsByCommentId(commentId);
try {
const _reaction = await this.issueReactionService.createIssueCommentReaction(
workspaceSlug,
projectId,
commentId,
{
reaction,
}
);
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions[reaction], { ..._reaction }],
};
runInAction(() => {
this.issue_comment_reactions = {
...this.issue_comment_reactions,
[commentId]: _currentReactions,
};
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
@ -483,11 +565,29 @@ export class IssueDetailStore implements IIssueDetailStore {
removeIssueCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => {
let _currentReactions = this.getIssueCommentReactionsByCommentId(commentId);
try {
const user = this.rootStore.user.currentUser;
if (user) {
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions[reaction].filter((r: any) => r.actor !== user.id)],
};
runInAction(() => {
this.issue_comment_reactions = {
...this.issue_comment_reactions,
[commentId]: _currentReactions,
};
});
await this.issueReactionService.deleteIssueCommentReaction(workspaceSlug, projectId, commentId, reaction);
}
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;