feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Globe2, Lock } from "lucide-react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import { useHashScroll } from "@plane/hooks";
import { EIssueCommentAccessSpecifier } from "@plane/types";
import type { TCommentsOperations, TIssueComment } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text";
// local imports
import { CommentReactions } from "../comment-reaction";
type Props = {
activityOperations: TCommentsOperations;
comment: TIssueComment;
disabled: boolean;
projectId?: string;
readOnlyEditorRef: React.RefObject<EditorRefApi>;
showAccessSpecifier: boolean;
workspaceId: string;
workspaceSlug: string;
};
export const CommentCardDisplay: React.FC<Props> = observer((props) => {
const {
activityOperations,
comment,
disabled,
projectId,
readOnlyEditorRef,
showAccessSpecifier,
workspaceId,
workspaceSlug,
} = props;
// states
const [highlightClassName, setHighlightClassName] = useState("");
// navigation
const pathname = usePathname();
// derived values
const commentBlockId = `comment-${comment?.id}`;
// scroll to comment
const { isHashMatch } = useHashScroll({
elementId: commentBlockId,
pathname,
});
useEffect(() => {
if (!isHashMatch) return;
setHighlightClassName("border-custom-primary-100");
const timeout = setTimeout(() => {
setHighlightClassName("");
}, 8000);
return () => clearTimeout(timeout);
}, [isHashMatch]);
return (
<div id={commentBlockId} className="relative flex flex-col gap-2">
{showAccessSpecifier && (
<div className="absolute right-2.5 top-2.5 z-[1] text-custom-text-300">
{comment.access === EIssueCommentAccessSpecifier.INTERNAL ? (
<Lock className="size-3" />
) : (
<Globe2 className="size-3" />
)}
</div>
)}
<LiteTextEditor
editable={false}
ref={readOnlyEditorRef}
id={comment.id}
initialValue={comment.comment_html ?? ""}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
containerClassName={cn("!py-1 transition-[border-color] duration-500", highlightClassName)}
projectId={projectId?.toString()}
displayConfig={{
fontSize: "small-font",
}}
/>
<CommentReactions comment={comment} disabled={disabled} activityOperations={activityOperations} />
</div>
);
});

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { useForm } from "react-hook-form";
import { Check, X } from "lucide-react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import type { TCommentsOperations, TIssueComment } from "@plane/types";
import { isCommentEmpty } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text";
type Props = {
activityOperations: TCommentsOperations;
comment: TIssueComment;
isEditing: boolean;
projectId?: string;
readOnlyEditorRef: EditorRefApi | null;
setIsEditing: (isEditing: boolean) => void;
workspaceId: string;
workspaceSlug: string;
};
export const CommentCardEditForm: React.FC<Props> = observer((props) => {
const {
activityOperations,
comment,
isEditing,
projectId,
readOnlyEditorRef,
setIsEditing,
workspaceId,
workspaceSlug,
} = props;
// refs
const editorRef = useRef<EditorRefApi>(null);
// form info
const {
formState: { isSubmitting },
handleSubmit,
setFocus,
watch,
setValue,
} = useForm<Partial<TIssueComment>>({
defaultValues: { comment_html: comment?.comment_html },
});
const commentHTML = watch("comment_html");
const isEmpty = isCommentEmpty(commentHTML ?? undefined);
const isEditorReadyToDiscard = editorRef.current?.isEditorReadyToDiscard();
const isSubmitButtonDisabled = isSubmitting || !isEditorReadyToDiscard;
const isDisabled = isSubmitting || isEmpty || isSubmitButtonDisabled;
const onEnter = async (formData: Partial<TIssueComment>) => {
if (isSubmitting || !comment) return;
setIsEditing(false);
await activityOperations.updateComment(comment.id, formData);
editorRef.current?.setEditorValue(formData?.comment_html ?? "<p></p>");
readOnlyEditorRef?.setEditorValue(formData?.comment_html ?? "<p></p>");
};
useEffect(() => {
if (isEditing) {
setFocus("comment_html");
}
}, [isEditing, setFocus]);
return (
<form className="flex flex-col gap-2">
<div
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty) handleSubmit(onEnter)(e);
}}
>
<LiteTextEditor
editable
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
ref={editorRef}
id={comment.id}
initialValue={commentHTML ?? ""}
value={null}
onChange={(_comment_json, comment_html) => setValue("comment_html", comment_html)}
onEnterKeyPress={(e) => {
if (!isEmpty && !isSubmitting) {
handleSubmit(onEnter)(e);
}
}}
showSubmitButton={false}
uploadFile={async (blockId, file) => {
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id);
return asset_id;
}}
projectId={projectId}
parentClassName="p-2"
displayConfig={{
fontSize: "small-font",
}}
/>
</div>
<div className="flex gap-1 self-end">
{!isEmpty && (
<button
type="button"
onClick={handleSubmit(onEnter)}
disabled={isDisabled}
className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${
isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500"
}`}
>
<Check
className={`h-3 w-3 text-green-500 duration-300 ${isEmpty ? "text-black" : "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);
editorRef.current?.setEditorValue(comment.comment_html ?? "<p></p>");
}}
>
<X className="size-3 text-red-500 duration-300 group-hover:text-white" />
</button>
</div>
</form>
);
});

View File

@@ -0,0 +1,88 @@
"use client";
import type { FC } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import type { TIssueComment, TCommentsOperations } from "@plane/types";
// plane web imports
import { CommentBlock } from "@/plane-web/components/comments";
// local imports
import { CommentQuickActions } from "../quick-actions";
import { CommentCardDisplay } from "./display";
import { CommentCardEditForm } from "./edit-form";
type TCommentCard = {
workspaceSlug: string;
comment: TIssueComment | undefined;
activityOperations: TCommentsOperations;
ends: "top" | "bottom" | undefined;
showAccessSpecifier: boolean;
showCopyLinkOption: boolean;
disabled?: boolean;
projectId?: string;
};
export const CommentCard: FC<TCommentCard> = observer((props) => {
const {
workspaceSlug,
comment,
activityOperations,
ends,
showAccessSpecifier,
showCopyLinkOption,
disabled = false,
projectId,
} = props;
// states
const [isEditing, setIsEditing] = useState(false);
// refs
const readOnlyEditorRef = useRef<EditorRefApi>(null);
// derived values
const workspaceId = comment?.workspace;
if (!comment || !workspaceId) return null;
return (
<CommentBlock
comment={comment}
quickActions={
!disabled && (
<CommentQuickActions
activityOperations={activityOperations}
comment={comment}
setEditMode={() => setIsEditing(true)}
showAccessSpecifier={showAccessSpecifier}
showCopyLinkOption={showCopyLinkOption}
/>
)
}
ends={ends}
>
{isEditing ? (
<CommentCardEditForm
activityOperations={activityOperations}
comment={comment}
isEditing
readOnlyEditorRef={readOnlyEditorRef.current}
setIsEditing={setIsEditing}
projectId={projectId}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
/>
) : (
<CommentCardDisplay
activityOperations={activityOperations}
comment={comment}
disabled={disabled}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef}
showAccessSpecifier={showAccessSpecifier}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
/>
)}
</CommentBlock>
);
});