plane/apps/web/core/components/comments/comment-create.tsx
2025-09-30 15:27:41 +05:30

148 lines
4.6 KiB
TypeScript

import { FC, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useForm, Controller } from "react-hook-form";
// plane imports
import { EIssueCommentAccessSpecifier } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import type { TIssueComment, TCommentsOperations } from "@plane/types";
import { cn, isCommentEmpty } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
// services
import { FileService } from "@/services/file.service";
type TCommentCreate = {
entityId: string;
workspaceSlug: string;
activityOperations: TCommentsOperations;
showToolbarInitially?: boolean;
projectId?: string;
onSubmitCallback?: (elementId: string) => void;
};
// services
const fileService = new FileService();
export const CommentCreate: FC<TCommentCreate> = observer((props) => {
const {
workspaceSlug,
entityId,
activityOperations,
showToolbarInitially = false,
projectId,
onSubmitCallback,
} = props;
// states
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
// refs
const editorRef = useRef<EditorRefApi>(null);
// store hooks
const workspaceStore = useWorkspace();
// derived values
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
// form info
const {
handleSubmit,
control,
watch,
formState: { isSubmitting },
reset,
} = useForm<Partial<TIssueComment>>({
defaultValues: {
comment_html: "<p></p>",
},
});
const onSubmit = async (formData: Partial<TIssueComment>) => {
try {
const comment = await activityOperations.createComment(formData);
if (comment?.id) onSubmitCallback?.(comment.id);
if (uploadedAssetIds.length > 0) {
if (projectId) {
await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId.toString(), entityId, {
asset_ids: uploadedAssetIds,
});
} else {
await fileService.updateBulkWorkspaceAssetsUploadStatus(workspaceSlug, entityId, {
asset_ids: uploadedAssetIds,
});
}
setUploadedAssetIds([]);
}
} catch (error) {
console.error(error);
} finally {
reset({
comment_html: "<p></p>",
});
editorRef.current?.clearEditor();
}
};
const commentHTML = watch("comment_html");
const isEmpty = isCommentEmpty(commentHTML ?? undefined);
return (
<div
className={cn("sticky bottom-0 z-[4] bg-custom-background-100 sm:static")}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.ctrlKey &&
!e.metaKey &&
!isEmpty &&
!isSubmitting &&
editorRef.current?.isEditorReadyToDiscard()
)
handleSubmit(onSubmit)(e);
}}
>
<Controller
name="access"
control={control}
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
<Controller
name="comment_html"
control={control}
render={({ field: { value, onChange } }) => (
<LiteTextEditor
editable
workspaceId={workspaceId}
id={"add_comment_" + entityId}
value={"<p></p>"}
workspaceSlug={workspaceSlug}
projectId={projectId}
onEnterKeyPress={(e) => {
if (!isEmpty && !isSubmitting) {
handleSubmit(onSubmit)(e);
}
}}
ref={editorRef}
initialValue={value ?? "<p></p>"}
containerClassName="min-h-min"
onChange={(comment_json, comment_html) => onChange(comment_html)}
accessSpecifier={accessValue ?? EIssueCommentAccessSpecifier.INTERNAL}
handleAccessChange={onAccessChange}
isSubmitting={isSubmitting}
uploadFile={async (blockId, file) => {
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file);
setUploadedAssetIds((prev) => [...prev, asset_id]);
return asset_id;
}}
showToolbarInitially={showToolbarInitially}
parentClassName="p-2"
displayConfig={{
fontSize: "small-font",
}}
/>
)}
/>
)}
/>
</div>
);
});