mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: comment creation in activity
This commit is contained in:
parent
373615f1e0
commit
6fc593bcb9
@ -6,10 +6,13 @@ import { useIssueDetail, useMention, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueCommentBlock } from "./comment-block";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
import { TActivityOperations } from "../root";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
@ -18,12 +21,14 @@ type TIssueCommentCard = {
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
commentId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
disabled: boolean;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
const { commentId, ends } = props;
|
||||
const { commentId, activityOperations, ends, showAccessSpecifier = true } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
@ -49,10 +54,10 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<TIssueComment>) => {
|
||||
if (isSubmitting) return;
|
||||
if (isSubmitting || !comment) return;
|
||||
setIsEditing(false);
|
||||
|
||||
// onSubmit(comment.id, formData);
|
||||
activityOperations.updateComment(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
@ -64,7 +69,52 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
|
||||
if (!comment) return <></>;
|
||||
return (
|
||||
<IssueCommentBlock commentId={commentId} quickActions={<></>} ends={ends}>
|
||||
<IssueCommentBlock
|
||||
commentId={commentId}
|
||||
quickActions={
|
||||
<>
|
||||
{currentUser?.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={() => activityOperations.updateComment(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={() => activityOperations.updateComment(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={() => {
|
||||
activityOperations.removeComment(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
@ -102,14 +152,11 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
</div>
|
||||
</form>
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{/* {showAccessSpecifier && (
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute right-2.5 top-2.5 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)} */}
|
||||
<div className="absolute right-2.5 top-2.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 ?? ""}
|
||||
|
@ -1,27 +1,26 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useRef } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { Button } from "@plane/ui";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// types
|
||||
import { TActivityOperations } from "../root";
|
||||
import { TIssueComment } from "@plane/types";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
type TIssueCommentCreateUpdate = {
|
||||
workspaceSlug: string;
|
||||
activityOperations: TActivityOperations;
|
||||
};
|
||||
|
||||
type TComment = {
|
||||
comment_html: string;
|
||||
};
|
||||
|
||||
const defaultValues: TComment = {
|
||||
comment_html: "",
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentCreateUpdate: FC<TIssueCommentCreateUpdate> = (props) => {
|
||||
const { activityOperations } = props;
|
||||
const { workspaceSlug, activityOperations, disabled } = props;
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
// react hook form
|
||||
const {
|
||||
handleSubmit,
|
||||
@ -29,30 +28,31 @@ export const IssueCommentCreateUpdate: FC<TIssueCommentCreateUpdate> = (props) =
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
} = useForm<TComment>({ defaultValues });
|
||||
} = useForm<Partial<TIssueComment>>({ defaultValues: { comment_html: "<p></p>" } });
|
||||
|
||||
const onSubmit = async (formData: Partial<TIssueComment>) => {
|
||||
await activityOperations.createComment(formData).finally(() => {
|
||||
reset({ comment_html: "" });
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <Controller
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={(e) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
});
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspace_slug as string)}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("comment_html")
|
||||
: value
|
||||
}
|
||||
value={!value ? "<p></p>" : value}
|
||||
customClassName="p-2"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -66,9 +66,7 @@ export const IssueCommentCreateUpdate: FC<TIssueCommentCreateUpdate> = (props) =
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
onClick={(e) => {
|
||||
userStore.requiredLogin(() => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
});
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
@ -76,7 +74,7 @@ export const IssueCommentCreateUpdate: FC<TIssueCommentCreateUpdate> = (props) =
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,16 +4,19 @@ import { observer } from "mobx-react-lite";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueCommentCard } from "./comment-card";
|
||||
// types
|
||||
import { TActivityOperations } from "../root";
|
||||
|
||||
type TIssueCommentRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled } = props;
|
||||
const { workspaceSlug, projectId, issueId, disabled, activityOperations } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentsByIssueId },
|
||||
@ -32,6 +35,7 @@ export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||
commentId={commentId}
|
||||
disabled={disabled}
|
||||
ends={index === 0 ? "top" : index === commentIds.length - 1 ? "bottom" : undefined}
|
||||
activityOperations={activityOperations}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -5,7 +5,9 @@ import { History, LucideIcon, MessageSquare, Network } from "lucide-react";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { IssueActivityCommentRoot, IssueActivityRoot, IssueCommentRoot, IssueCommentCreateUpdate } from ".";
|
||||
import { IssueActivityCommentRoot, IssueActivityRoot, IssueCommentRoot, IssueCommentCreateUpdate } from "./";
|
||||
// types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
|
||||
type TIssueActivity = {
|
||||
workspaceSlug: string;
|
||||
@ -35,8 +37,9 @@ const activityTabs: { key: TActivityTabs; title: string; icon: LucideIcon }[] =
|
||||
];
|
||||
|
||||
export type TActivityOperations = {
|
||||
createComment: (data: any) => Promise<void>;
|
||||
updateComment: (commentId: string, data: any) => Promise<void>;
|
||||
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>;
|
||||
};
|
||||
@ -44,14 +47,15 @@ export type TActivityOperations = {
|
||||
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled } = props;
|
||||
// hooks
|
||||
const { createComment, updateComment, createCommentReaction, removeCommentReaction } = useIssueDetail();
|
||||
const { createComment, updateComment, removeComment, createCommentReaction, removeCommentReaction } =
|
||||
useIssueDetail();
|
||||
const { setToastAlert } = useToast();
|
||||
// state
|
||||
const [activityTab, setActivityTab] = useState<TActivityTabs>("comments");
|
||||
const [activityTab, setActivityTab] = useState<TActivityTabs>("all");
|
||||
|
||||
const activityOperations: TActivityOperations = useMemo(
|
||||
() => ({
|
||||
createComment: async (data: any) => {
|
||||
createComment: async (data: Partial<TIssueComment>) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await createComment(workspaceSlug, projectId, issueId, data);
|
||||
@ -68,7 +72,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
updateComment: async (commentId: string, data: any) => {
|
||||
updateComment: async (commentId: string, data: Partial<TIssueComment>) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||
@ -85,6 +89,23 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
removeComment: async (commentId: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await removeComment(workspaceSlug, projectId, issueId, commentId);
|
||||
setToastAlert({
|
||||
title: "Comment removed successfully.",
|
||||
type: "success",
|
||||
message: "Comment removed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment remove failed.",
|
||||
type: "error",
|
||||
message: "Comment remove failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
createCommentReaction: async (commentId: string, reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
|
||||
@ -126,6 +147,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
issueId,
|
||||
createComment,
|
||||
updateComment,
|
||||
removeComment,
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
setToastAlert,
|
||||
@ -168,17 +190,28 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
|
||||
<div className="min-h-[200px]">
|
||||
{activityTab === "all" ? (
|
||||
<IssueActivityCommentRoot {...componentCommonProps} />
|
||||
<>
|
||||
<IssueActivityCommentRoot {...componentCommonProps} />
|
||||
<IssueCommentCreateUpdate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
) : activityTab === "activity" ? (
|
||||
<IssueActivityRoot {...componentCommonProps} />
|
||||
) : (
|
||||
<IssueCommentRoot {...componentCommonProps} />
|
||||
<>
|
||||
<IssueCommentRoot {...componentCommonProps} activityOperations={activityOperations} />
|
||||
<IssueCommentCreateUpdate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* rendering issue comment editor */}
|
||||
<IssueCommentCreateUpdate activityOperations={activityOperations} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user