[WIKI-509] feat: comment copy link option (#7385)

* feat: comment copy link option

* chore: add translations

* chore: update block position

* chore: rename use id scroll hook

* refactor: setTimeout function

* refactor: use-hash-scroll hook
This commit is contained in:
Aaryan Khandelwal 2025-07-14 17:07:44 +05:30 committed by GitHub
parent f90e553881
commit 2c70c1aaa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 694 additions and 250 deletions

View file

@ -0,0 +1,85 @@
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 { EditorReadOnlyRefApi } from "@plane/editor";
import { useHashScroll } from "@plane/hooks";
import { EIssueCommentAccessSpecifier, type TCommentsOperations, type TIssueComment } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { LiteTextReadOnlyEditor } from "@/components/editor";
// local imports
import { CommentReactions } from "../comment-reaction";
type Props = {
activityOperations: TCommentsOperations;
comment: TIssueComment;
disabled: boolean;
projectId?: string;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
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>
)}
<LiteTextReadOnlyEditor
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,130 @@
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 { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
import type { TCommentsOperations, TIssueComment } from "@plane/types";
import { isCommentEmpty } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor";
type Props = {
activityOperations: TCommentsOperations;
comment: TIssueComment;
isEditing: boolean;
projectId?: string;
readOnlyEditorRef: EditorReadOnlyRefApi | 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
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?.toString() ?? ""}
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,85 @@
"use client";
import { FC, useRef, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import type { EditorReadOnlyRefApi } 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;
const readOnlyEditorRef = useRef<EditorReadOnlyRefApi>(null);
// states
const [isEditing, setIsEditing] = useState(false);
// 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}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
/>
) : (
<CommentCardDisplay
activityOperations={activityOperations}
comment={comment}
disabled={disabled}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef}
showAccessSpecifier={showAccessSpecifier}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
/>
)}
</CommentBlock>
);
});

View file

@ -1,224 +0,0 @@
"use client";
import { FC, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useForm } from "react-hook-form";
import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react";
// PLane
import { EIssueCommentAccessSpecifier } from "@plane/constants";
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { TIssueComment, TCommentsOperations } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// components
import { isCommentEmpty } from "@plane/utils";
import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor";
// helpers
// hooks
import { useUser } from "@/hooks/store";
//
import { CommentBlock } from "@/plane-web/components/comments";
import { CommentReactions } from "./comment-reaction";
type TCommentCard = {
workspaceSlug: string;
comment: TIssueComment | undefined;
activityOperations: TCommentsOperations;
ends: "top" | "bottom" | undefined;
showAccessSpecifier?: boolean;
disabled?: boolean;
projectId?: string;
};
export const CommentCard: FC<TCommentCard> = observer((props) => {
const {
workspaceSlug,
comment,
activityOperations,
ends,
showAccessSpecifier = false,
disabled = false,
projectId,
} = props;
const { t } = useTranslation();
// refs
const editorRef = useRef<EditorRefApi>(null);
const showEditorRef = useRef<EditorReadOnlyRefApi>(null);
// state
const [isEditing, setIsEditing] = useState(false);
// store hooks
const { data: currentUser } = useUser();
// form info
const {
formState: { isSubmitting },
handleSubmit,
setFocus,
watch,
setValue,
} = useForm<Partial<TIssueComment>>({
defaultValues: { comment_html: comment?.comment_html },
});
// derived values
const workspaceId = comment?.workspace;
const commentHTML = watch("comment_html");
const isEmpty = isCommentEmpty(commentHTML ?? undefined);
const isEditorReadyToDiscard = editorRef.current?.isEditorReadyToDiscard();
const isSubmitButtonDisabled = isSubmitting || !isEditorReadyToDiscard;
const isDisabled = isSubmitting || isEmpty || isSubmitButtonDisabled;
// helpers
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>");
showEditorRef.current?.setEditorValue(formData?.comment_html ?? "<p></p>");
};
useEffect(() => {
if (isEditing) {
setFocus("comment_html");
}
}, [isEditing, setFocus]);
if (!comment || !currentUser || !workspaceId) return <></>;
return (
<CommentBlock
comment={comment}
quickActions={
<>
{!disabled && currentUser?.id === comment.actor && (
<CustomMenu ellipsis closeOnSelect>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
<Pencil className="flex-shrink-0 size-3" />
{t("common.actions.edit")}
</CustomMenu.MenuItem>
{showAccessSpecifier && (
<>
{comment.access === "INTERNAL" ? (
<CustomMenu.MenuItem
onClick={() =>
activityOperations.updateComment(comment.id, { access: EIssueCommentAccessSpecifier.EXTERNAL })
}
className="flex items-center gap-1"
>
<Globe2 className="flex-shrink-0 size-3" />
{t("issue.comments.switch.public")}
</CustomMenu.MenuItem>
) : (
<CustomMenu.MenuItem
onClick={() =>
activityOperations.updateComment(comment.id, { access: EIssueCommentAccessSpecifier.INTERNAL })
}
className="flex items-center gap-1"
>
<Lock className="flex-shrink-0 size-3" />
{t("issue.comments.switch.private")}
</CustomMenu.MenuItem>
)}
</>
)}
<CustomMenu.MenuItem
onClick={() => activityOperations.removeComment(comment.id)}
className="flex items-center gap-1"
>
<Trash2 className="flex-shrink-0 size-3" />
{t("common.actions.delete")}
</CustomMenu.MenuItem>
</CustomMenu>
)}
</>
}
ends={ends}
>
<>
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
<div
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty) handleSubmit(onEnter)(e);
}}
>
<LiteTextEditor
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?.toString() ?? ""}
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>
<div className={`relative flex flex-col gap-2 ${isEditing ? "hidden" : ""}`}>
{showAccessSpecifier && (
<div className="absolute right-2.5 top-2.5 z-[1] text-custom-text-300">
{comment.access === EIssueCommentAccessSpecifier.INTERNAL ? (
<Lock className="h-3 w-3" />
) : (
<Globe2 className="h-3 w-3" />
)}
</div>
)}
<LiteTextReadOnlyEditor
ref={showEditorRef}
id={comment.id}
initialValue={comment.comment_html ?? ""}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
containerClassName="!py-1"
projectId={(projectId as string) ?? ""}
displayConfig={{
fontSize: "small-font",
}}
/>
<CommentReactions comment={comment} disabled={disabled} activityOperations={activityOperations} />
</div>
</>
</CommentBlock>
);
});

View file

@ -4,11 +4,10 @@ import React, { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import smoothScrollIntoView from "smooth-scroll-into-view-if-needed";
import { E_SORT_ORDER } from "@plane/constants";
import { TCommentsOperations, TIssueComment } from "@plane/types";
// local components
import { CommentCard } from "./comment-card";
import { CommentCard } from "./card/root";
import { CommentCreate } from "./comment-create";
type TCommentsWrapper = {
@ -19,10 +18,21 @@ type TCommentsWrapper = {
comments: TIssueComment[] | string[];
sortOrder?: E_SORT_ORDER;
getCommentById?: (activityId: string) => TIssueComment | undefined;
showAccessSpecifier?: boolean;
showCopyLinkOption?: boolean;
};
export const CommentsWrapper: FC<TCommentsWrapper> = observer((props) => {
const { entityId, activityOperations, comments, getCommentById, isEditingAllowed = true, projectId } = props;
const {
entityId,
activityOperations,
comments,
getCommentById,
isEditingAllowed = true,
projectId,
showAccessSpecifier = false,
showCopyLinkOption = false,
} = props;
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
@ -61,6 +71,8 @@ export const CommentsWrapper: FC<TCommentsWrapper> = observer((props) => {
disabled={!isEditingAllowed}
ends={index === 0 ? "top" : index === comments.length - 1 ? "bottom" : undefined}
projectId={projectId}
showAccessSpecifier={showAccessSpecifier}
showCopyLinkOption={showCopyLinkOption}
/>
);
})}

View file

@ -0,0 +1,113 @@
"use client";
import { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { Globe2, Link, Lock, Pencil, Trash2 } from "lucide-react";
// plane imports
import { EIssueCommentAccessSpecifier } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { TIssueComment, TCommentsOperations } from "@plane/types";
import { CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useUser } from "@/hooks/store";
type TCommentCard = {
activityOperations: TCommentsOperations;
comment: TIssueComment;
setEditMode: () => void;
showAccessSpecifier: boolean;
showCopyLinkOption: boolean;
};
export const CommentQuickActions: FC<TCommentCard> = observer((props) => {
const { activityOperations, comment, setEditMode, showAccessSpecifier, showCopyLinkOption } = props;
// store hooks
const { data: currentUser } = useUser();
// derived values
const isAuthor = currentUser?.id === comment.actor;
const canEdit = isAuthor;
const canDelete = isAuthor;
// translation
const { t } = useTranslation();
const MENU_ITEMS: TContextMenuItem[] = useMemo(
() => [
{
key: "edit",
action: setEditMode,
title: t("common.actions.edit"),
icon: Pencil,
shouldRender: canEdit,
},
{
key: "copy_link",
action: () => activityOperations.copyCommentLink(comment.id),
title: t("common.actions.copy_link"),
icon: Link,
shouldRender: showCopyLinkOption,
},
{
key: "access_specifier",
action: () =>
activityOperations.updateComment(comment.id, {
access:
comment.access === EIssueCommentAccessSpecifier.INTERNAL
? EIssueCommentAccessSpecifier.EXTERNAL
: EIssueCommentAccessSpecifier.INTERNAL,
}),
title:
comment.access === EIssueCommentAccessSpecifier.INTERNAL
? t("issue.comments.switch.public")
: t("issue.comments.switch.private"),
icon: comment.access === EIssueCommentAccessSpecifier.INTERNAL ? Globe2 : Lock,
shouldRender: showAccessSpecifier,
},
{
key: "delete",
action: () => activityOperations.removeComment(comment.id),
title: t("common.actions.delete"),
icon: Trash2,
shouldRender: canDelete,
},
],
[activityOperations, canDelete, canEdit, comment, setEditMode, showAccessSpecifier, showCopyLinkOption]
);
return (
<CustomMenu ellipsis closeOnSelect>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={() => item.action()}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("shrink-0 size-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
);
});

View file

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { E_SORT_ORDER, TActivityFilters, filterActivityOnSelectedFilters } from "@plane/constants";
// hooks
import { TCommentsOperations } from "@plane/types";
import { CommentCard } from "@/components/comments/comment-card";
import { CommentCard } from "@/components/comments/card/root";
import { useIssueDetail } from "@/hooks/store";
// plane web components
import { IssueAdditionalPropertiesActivity } from "@/plane-web/components/issues";
@ -57,7 +57,8 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
comment={comment}
activityOperations={activityOperations}
ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined}
showAccessSpecifier={showAccessSpecifier}
showAccessSpecifier={!!showAccessSpecifier}
showCopyLinkOption
disabled={disabled}
projectId={projectId}
/>

View file

@ -1,9 +1,9 @@
import { useMemo } from "react";
import { useTranslation } from "@plane/i18n";
import { EFileAssetType, TCommentsOperations, TIssueComment } from "@plane/types";
import { EFileAssetType, type TCommentsOperations } from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
import { formatTextList } from "@plane/utils";
import { useEditorAsset, useIssueDetail, useMember, useUser } from "@/hooks/store";
import { copyUrlToClipboard, formatTextList, generateWorkItemLink } from "@plane/utils";
import { useEditorAsset, useIssueDetail, useMember, useProject, useUser } from "@/hooks/store";
export const useCommentOperations = (
workspaceSlug: string | undefined,
@ -18,16 +18,49 @@ export const useCommentOperations = (
removeComment,
createCommentReaction,
removeCommentReaction,
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
const { getUserDetails } = useMember();
const { uploadEditorAsset } = useEditorAsset();
const { data: currentUser } = useUser();
// derived values
const issueDetails = issueId ? getIssueById(issueId) : undefined;
const projectDetails = projectId ? getProjectById(projectId) : undefined;
// translation
const { t } = useTranslation();
const operations = useMemo(() => {
const operations: TCommentsOperations = useMemo(() => {
// Define operations object with all methods
const ops = {
createComment: async (data: Partial<TIssueComment>) => {
const ops: TCommentsOperations = {
copyCommentLink: (id) => {
if (!workspaceSlug || !issueDetails) return;
try {
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issueDetails.project_id,
issueId,
projectIdentifier: projectDetails?.identifier,
sequenceId: issueDetails.sequence_id,
});
const commentLink = `${workItemLink}#comment-${id}`;
copyUrlToClipboard(commentLink).then(() => {
setToast({
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: t("issue.comments.copy_link.success"),
});
});
} catch (error) {
console.error("Error in copying comment link:", error);
setToast({
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: t("issue.comments.copy_link.error"),
});
}
},
createComment: async (data) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
const comment = await createComment(workspaceSlug, projectId, issueId, data);
@ -45,7 +78,7 @@ export const useCommentOperations = (
});
}
},
updateComment: async (commentId: string, data: Partial<TIssueComment>) => {
updateComment: async (commentId, data) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
await updateComment(workspaceSlug, projectId, issueId, commentId, data);
@ -62,7 +95,7 @@ export const useCommentOperations = (
});
}
},
removeComment: async (commentId: string) => {
removeComment: async (commentId) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
await removeComment(workspaceSlug, projectId, issueId, commentId);
@ -79,7 +112,7 @@ export const useCommentOperations = (
});
}
},
uploadCommentAsset: async (blockId: string, file: File, commentId?: string) => {
uploadCommentAsset: async (blockId, file, commentId) => {
try {
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
const res = await uploadEditorAsset({
@ -98,7 +131,7 @@ export const useCommentOperations = (
throw new Error(t("issue.comments.upload.error"));
}
},
addCommentReaction: async (commentId: string, reaction: string) => {
addCommentReaction: async (commentId, reaction) => {
try {
if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields");
await createCommentReaction(workspaceSlug, projectId, commentId, reaction);
@ -107,7 +140,7 @@ export const useCommentOperations = (
type: TOAST_TYPE.SUCCESS,
message: "Reaction created successfully",
});
} catch (error) {
} catch {
setToast({
title: "Error!",
type: TOAST_TYPE.ERROR,
@ -115,7 +148,7 @@ export const useCommentOperations = (
});
}
},
deleteCommentReaction: async (commentId: string, reaction: string) => {
deleteCommentReaction: async (commentId, reaction) => {
try {
if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields");
removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id);
@ -124,7 +157,7 @@ export const useCommentOperations = (
type: TOAST_TYPE.SUCCESS,
message: "Reaction removed successfully",
});
} catch (error) {
} catch {
setToast({
title: "Error!",
type: TOAST_TYPE.ERROR,
@ -132,14 +165,14 @@ export const useCommentOperations = (
});
}
},
react: async (commentId: string, reactionEmoji: string, userReactions: string[]) => {
react: async (commentId, reactionEmoji, userReactions) => {
if (userReactions.includes(reactionEmoji)) await ops.deleteCommentReaction(commentId, reactionEmoji);
else await ops.addCommentReaction(commentId, reactionEmoji);
},
reactionIds: (commentId: string) => getCommentReactionsByCommentId(commentId),
userReactions: (commentId: string) =>
reactionIds: (commentId) => getCommentReactionsByCommentId(commentId),
userReactions: (commentId) =>
currentUser ? commentReactionsByUser(commentId, currentUser?.id).map((r) => r.reaction) : [],
getReactionUsers: (reaction: string, reactionIds: Record<string, string[]>): string => {
getReactionUsers: (reaction, reactionIds) => {
const reactionUsers = (reactionIds?.[reaction] || [])
.map((reactionId) => {
const reactionDetails = getCommentReactionById(reactionId);