forked from github/plane
chore: peekoverview issue comments and comment reactions (#2507)
This commit is contained in:
parent
4b03802d22
commit
914657334d
135
web/components/issues/issue-peek-overview/activity/card.tsx
Normal file
135
web/components/issues/issue-peek-overview/activity/card.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
export * from "./view";
|
||||
|
||||
export * from "./card";
|
||||
export * from "./comment-editor";
|
||||
export * from "./comment-reaction";
|
59
web/components/issues/issue-peek-overview/activity/view.tsx
Normal file
59
web/components/issues/issue-peek-overview/activity/view.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user