bb-plane-fork/apps/space/components/issues/peek-overview/comment/comment-detail-card.tsx
sriram veeraghanta 7fb6696c67
chore: space folders (#8707)
* chore: change the space folders structure

* fix: format
2026-03-05 14:03:54 +05:30

228 lines
8.8 KiB
TypeScript

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useRef, useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { MessageSquare, MoreVertical } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import { CheckIcon, CloseIcon } from "@plane/propel/icons";
import type { TIssuePublicComment } from "@plane/types";
import { getFileURL } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
import { CommentReactions } from "@/components/issues/peek-overview/comment/comment-reactions";
// helpers
import { timeAgo } from "@/helpers/date-time.helper";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useUser } from "@/hooks/store/use-user";
import useIsInIframe from "@/hooks/use-is-in-iframe";
type Props = {
anchor: string;
comment: TIssuePublicComment;
};
export const CommentCard = observer(function CommentCard(props: Props) {
const { anchor, comment } = props;
// store hooks
const { peekId, deleteIssueComment, updateIssueComment, uploadCommentAsset } = useIssueDetails();
const { data: currentUser } = useUser();
const { workspace: workspaceID } = usePublish(anchor);
const isInIframe = useIsInIframe();
// states
const [isEditing, setIsEditing] = useState(false);
// refs
const editorRef = useRef<EditorRefApi>(null);
const showEditorRef = useRef<EditorRefApi>(null);
// form info
const {
control,
formState: { isSubmitting },
handleSubmit,
} = useForm<TIssuePublicComment>({
defaultValues: { comment_html: comment.comment_html },
});
const handleDelete = () => {
if (!anchor || !peekId) return;
deleteIssueComment(anchor, peekId, comment.id);
};
const handleCommentUpdate = async (formData: TIssuePublicComment) => {
if (!anchor || !peekId) return;
updateIssueComment(anchor, peekId, comment.id, formData);
setIsEditing(false);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
return (
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{comment.actor_detail.avatar_url && comment.actor_detail.avatar_url !== "" ? (
<img
src={getFileURL(comment.actor_detail.avatar_url)}
alt={
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
}
height={30}
width={30}
className="grid size-7 place-items-center rounded-full border-2 border-strong-1"
/>
) : (
<div
className={`bg-gray-500 grid size-7 place-items-center rounded-full border-2 border-strong-1 text-on-color`}
>
{comment.actor_detail.is_bot
? comment?.actor_detail?.first_name?.charAt(0)
: comment?.actor_detail?.display_name?.charAt(0)}
</div>
)}
<span className="absolute -right-1 -bottom-0.5 rounded-tl-sm bg-layer-1 px-0.5 py-px">
<MessageSquare className="size-3 text-secondary" aria-hidden="true" strokeWidth={2} />
</span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-11">
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-11 text-secondary">
<>commented {timeAgo(comment.created_at)}</>
</p>
</div>
<div className="issue-comments-section p-0">
<form
onSubmit={handleSubmit(handleCommentUpdate)}
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
>
<div>
<Controller
control={control}
name="comment_html"
render={({ field: { onChange, value } }) => (
<LiteTextEditor
editable
anchor={anchor}
workspaceId={workspaceID?.toString() ?? ""}
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
ref={editorRef}
id={comment.id}
initialValue={value}
value={null}
onChange={(comment_json, comment_html) => onChange(comment_html)}
isSubmitting={isSubmitting}
showSubmitButton={false}
uploadFile={async (blockId, file) => {
const { asset_id } = await uploadCommentAsset(file, anchor, comment.id);
return asset_id;
}}
displayConfig={{
fontSize: "small-font",
}}
/>
)}
/>
</div>
<div className="flex gap-1 self-end">
<button
type="submit"
disabled={isSubmitting}
className="group shadow-md rounded-sm border border-success-strong bg-success-primary p-2 duration-300 hover:bg-success-primary"
>
<CheckIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
</button>
<button
type="button"
className="group shadow-md rounded-sm border border-danger-strong bg-danger-primary p-2 duration-300 hover:bg-danger-primary-hover"
onClick={() => setIsEditing(false)}
>
<CloseIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
</button>
</div>
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<LiteTextEditor
editable={false}
anchor={anchor}
workspaceId={workspaceID?.toString() ?? ""}
ref={showEditorRef}
id={comment.id}
initialValue={comment.comment_html}
displayConfig={{
fontSize: "small-font",
}}
/>
<CommentReactions anchor={anchor} commentId={comment.id} />
</div>
</div>
</div>
{!isInIframe && currentUser?.id === comment?.actor_detail?.id && (
<Menu as="div" className="relative w-min text-left">
<Menu.Button
type="button"
onClick={() => {}}
className="relative grid cursor-pointer place-items-center rounded-sm p-1 text-tertiary outline-none hover:bg-layer-transparent-hover"
>
<MoreVertical className="size-4" strokeWidth={2} />
</Menu.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="shadow-lg absolute right-0 z-10 mt-1 max-h-36 min-w-[8rem] origin-top-right overflow-auto overflow-y-scroll rounded-md border border-strong bg-surface-1 p-1 text-11 whitespace-nowrap focus:outline-none">
<Menu.Item>
{({ active }) => (
<div className="py-1">
<button
type="button"
onClick={() => {
setIsEditing(true);
}}
className={`w-full truncate rounded-sm px-1 py-1.5 text-left text-secondary select-none hover:bg-layer-transparent-hover ${
active ? "bg-layer-transparent-hover" : ""
}`}
>
Edit
</button>
</div>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<div className="py-1">
<button
type="button"
onClick={handleDelete}
className={`w-full truncate rounded-sm px-1 py-1.5 text-left text-secondary select-none hover:bg-layer-transparent-hover ${
active ? "bg-layer-transparent-hover" : ""
}`}
>
Delete
</button>
</div>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
);
});