From 6d635d867e522591af47967de8bd3bbf1f9bb77b Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 17 Mar 2025 13:59:11 +0530 Subject: [PATCH 01/11] fix: comments refactor --- .../src/issues/activity/issue_comment.d.ts | 26 +++ web/ce/components/comments/comment-block.tsx | 74 ++++++++ web/ce/components/comments/index.ts | 1 + .../comments/comment-card.tsx | 95 ++++------- .../comments/comment-create.tsx | 53 +++--- .../components/comments/comment-reaction.tsx | 67 ++++++++ web/core/components/comments/comments.tsx | 56 +++++++ web/core/components/comments/index.ts | 1 + .../lite-text-editor/lite-text-editor.tsx | 60 ++++--- .../lite-text-read-only-editor.tsx | 2 +- .../issue-activity/activity-comment-root.tsx | 15 +- .../actions/helpers/activity-block.tsx | 2 +- .../issue-activity/comments/comment-block.tsx | 70 -------- .../issue-activity/comments/index.ts | 4 - .../issue-activity/comments/root.tsx | 63 ------- .../issue-detail/issue-activity/helper.tsx | 158 ++++++++++++++++++ .../issue-detail/issue-activity/index.ts | 3 - .../issue-detail/issue-activity/root.tsx | 99 ++--------- .../issue-details/comment_reaction.store.ts | 4 +- web/ee/components/comments/index.ts | 1 + 20 files changed, 494 insertions(+), 360 deletions(-) create mode 100644 web/ce/components/comments/comment-block.tsx create mode 100644 web/ce/components/comments/index.ts rename web/core/components/{issues/issue-detail/issue-activity => }/comments/comment-card.tsx (74%) rename web/core/components/{issues/issue-detail/issue-activity => }/comments/comment-create.tsx (74%) create mode 100644 web/core/components/comments/comment-reaction.tsx create mode 100644 web/core/components/comments/comments.tsx create mode 100644 web/core/components/comments/index.ts delete mode 100644 web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx delete mode 100644 web/core/components/issues/issue-detail/issue-activity/comments/index.ts delete mode 100644 web/core/components/issues/issue-detail/issue-activity/comments/root.tsx create mode 100644 web/core/components/issues/issue-detail/issue-activity/helper.tsx create mode 100644 web/ee/components/comments/index.ts diff --git a/packages/types/src/issues/activity/issue_comment.d.ts b/packages/types/src/issues/activity/issue_comment.d.ts index aef5134c674..95be9a7d449 100644 --- a/packages/types/src/issues/activity/issue_comment.d.ts +++ b/packages/types/src/issues/activity/issue_comment.d.ts @@ -5,7 +5,15 @@ import { TIssueActivityUserDetail, } from "./base"; import { EIssueCommentAccessSpecifier } from "../../enums"; +import { TFileSignedURLResponse } from "../../file"; +import { IUserLite } from "../../users"; +export type TCommentReaction = { + id: string; + reaction: string; + actor: string; + actor_detail: IUserLite; +}; export type TIssueComment = { id: string; workspace: string; @@ -17,6 +25,7 @@ export type TIssueComment = { actor: string; actor_detail: TIssueActivityUserDetail; created_at: string; + edited_at?: string | undefined; updated_at: string; created_by: string | undefined; updated_by: string | undefined; @@ -30,6 +39,23 @@ export type TIssueComment = { access: EIssueCommentAccessSpecifier; }; +export type TCommentsOperations = { + createComment: (data: Partial) => Promise; + updateComment: (commentId: string, data: Partial) => Promise; + removeComment: (commentId: string) => Promise; + uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise; + addCommentReaction: (commentId: string, reactionEmoji: string) => Promise; + deleteCommentReaction: (commentId: string, reactionEmoji: string, userReactions: TCommentReaction[]) => Promise; + react: (commentId: string, reactionEmoji: string, userReactions: string[]) => Promise; + reactionIds: (commentId: string) => + | { + [reaction: string]: string[]; + } + | undefined; + userReactions: (commentId: string) => string[] | undefined; + getReactionUsers: (reaction: string, reactionIds: Record) => string; +}; + export type TIssueCommentMap = { [issue_id: string]: TIssueComment; }; diff --git a/web/ce/components/comments/comment-block.tsx b/web/ce/components/comments/comment-block.tsx new file mode 100644 index 00000000000..c9357d2f7cf --- /dev/null +++ b/web/ce/components/comments/comment-block.tsx @@ -0,0 +1,74 @@ +import { FC, ReactNode, useRef } from "react"; +import { observer } from "mobx-react"; +// plane imports +import { TIssueComment } from "@plane/types"; +import { Avatar, Tooltip } from "@plane/ui"; +import { calculateTimeAgo, cn, getFileURL, renderFormattedDate } from "@plane/utils"; +// hooks +import { renderFormattedTime } from "@/helpers/date-time.helper"; +import { useMember } from "@/hooks/store"; + +type TCommentBlock = { + comment: TIssueComment; + ends: "top" | "bottom" | undefined; + quickActions: ReactNode; + children: ReactNode; +}; + +export const CommentBlock: FC = observer((props) => { + const { comment, ends, quickActions, children } = props; + const commentBlockRef = useRef(null); + // store hooks + const { getUserDetails } = useMember(); + const userDetails = getUserDetails(comment?.actor); + + if (!comment || !userDetails) return <>; + return ( +
+
+
+ +
+
+
+
+
+ {comment?.actor_detail?.is_bot + ? comment?.actor_detail?.first_name + " Bot" + : comment?.actor_detail?.display_name || userDetails.display_name} +
+
+ commented{" "} + + + {calculateTimeAgo(comment.updated_at)} + {comment.edited_at && " (edited)"} + + +
+
+
{quickActions}
+
+
{children}
+
+
+ ); +}); diff --git a/web/ce/components/comments/index.ts b/web/ce/components/comments/index.ts new file mode 100644 index 00000000000..f0ef4e2b691 --- /dev/null +++ b/web/ce/components/comments/index.ts @@ -0,0 +1 @@ +export * from "./comment-block"; diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx similarity index 74% rename from web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx rename to web/core/components/comments/comment-card.tsx index 96f0960b605..2e0a07f037e 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -4,66 +4,41 @@ 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 constants +// PLane import { EIssueCommentAccessSpecifier } from "@plane/constants"; -// plane editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; -// plane i18n import { useTranslation } from "@plane/i18n"; -// plane types -import { TIssueComment } from "@plane/types"; -// plane ui +import { TIssueComment, TCommentsOperations } from "@plane/types"; import { CustomMenu } from "@plane/ui"; -// plane utils -import { cn } from "@plane/utils"; // components import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor"; // helpers import { isCommentEmpty } from "@/helpers/string.helper"; // hooks -import { useIssueDetail, useUser, useWorkspace } from "@/hooks/store"; -// components -import { IssueCommentReaction } from "../../reactions/issue-comment"; -import { TActivityOperations } from "../root"; -import { IssueCommentBlock } from "./comment-block"; +import { useUser } from "@/hooks/store"; +// +import { CommentBlock } from "@/plane-web/components/comments"; +import { CommentReactions } from "./comment-reaction"; -type TIssueCommentCard = { - projectId: string; - issueId: string; +type TCommentCard = { workspaceSlug: string; - commentId: string; - activityOperations: TActivityOperations; + comment: TIssueComment | undefined; + activityOperations: TCommentsOperations; ends: "top" | "bottom" | undefined; showAccessSpecifier?: boolean; disabled?: boolean; }; -export const IssueCommentCard: FC = observer((props) => { - const { - workspaceSlug, - projectId, - issueId, - commentId, - activityOperations, - ends, - showAccessSpecifier = false, - disabled = false, - } = props; +export const CommentCard: FC = observer((props) => { + const { workspaceSlug, comment, activityOperations, ends, showAccessSpecifier = false, disabled = false } = props; const { t } = useTranslation(); - // states - const [isEditing, setIsEditing] = useState(false); // refs const editorRef = useRef(null); const showEditorRef = useRef(null); + // state + const [isEditing, setIsEditing] = useState(false); // store hooks - const { - comment: { getCommentById }, - } = useIssueDetail(); const { data: currentUser } = useUser(); - // derived values - const comment = getCommentById(commentId); - const workspaceStore = useWorkspace(); - const workspaceId = workspaceStore.getWorkspaceBySlug(comment?.workspace_detail?.slug as string)?.id as string; // form info const { formState: { isSubmitting }, @@ -75,13 +50,16 @@ export const IssueCommentCard: FC = observer((props) => { defaultValues: { comment_html: comment?.comment_html }, }); // derived values + const workspaceId = comment?.workspace; const commentHTML = watch("comment_html"); - const isEmpty = isCommentEmpty(commentHTML); + const isEmpty = isCommentEmpty(commentHTML ?? undefined); const isEditorReadyToDiscard = editorRef.current?.isEditorReadyToDiscard(); const isSubmitButtonDisabled = isSubmitting || !isEditorReadyToDiscard; + // helpers const onEnter = async (formData: Partial) => { if (isSubmitting || !comment) return; + setIsEditing(false); await activityOperations.updateComment(comment.id, formData); @@ -96,11 +74,11 @@ export const IssueCommentCard: FC = observer((props) => { } }, [isEditing, setFocus]); - if (!comment || !currentUser) return <>; + if (!comment || !currentUser || !workspaceId) return <>; return ( - {!disabled && currentUser?.id === comment.actor && ( @@ -156,8 +134,6 @@ export const IssueCommentCard: FC = observer((props) => { > = observer((props) => { )}
-
+
{showAccessSpecifier && (
{comment.access === EIssueCommentAccessSpecifier.INTERNAL ? ( @@ -217,18 +192,12 @@ export const IssueCommentCard: FC = observer((props) => { initialValue={comment.comment_html ?? ""} workspaceId={workspaceId} workspaceSlug={workspaceSlug} - projectId={projectId} - /> - - +
- + ); }); diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx similarity index 74% rename from web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx rename to web/core/components/comments/comment-create.tsx index 6abd2d6e4e9..281ebc4a3a1 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -1,40 +1,38 @@ import { FC, useRef, useState } from "react"; +import { observer } from "mobx-react"; import { useForm, Controller } from "react-hook-form"; +// plane constants import { EIssueCommentAccessSpecifier } from "@plane/constants"; // plane editor import { EditorRefApi } from "@plane/editor"; -// types -import { TIssueComment } from "@plane/types"; // components +import { TIssueComment, TCommentsOperations } from "@plane/types"; import { LiteTextEditor } from "@/components/editor"; +// constants // helpers import { cn } from "@/helpers/common.helper"; import { isCommentEmpty } from "@/helpers/string.helper"; // hooks -import { useIssueDetail, useWorkspace } from "@/hooks/store"; -// services +import { useWorkspace } from "@/hooks/store"; import { FileService } from "@/services/file.service"; -const fileService = new FileService(); -// editor -import { TActivityOperations } from "../root"; -type TIssueCommentCreate = { - projectId: string; +type TCommentCreate = { + entityId: string; workspaceSlug: string; - activityOperations: TActivityOperations; - showAccessSpecifier?: boolean; - issueId: string; + activityOperations: TCommentsOperations; + showToolbarInitially?: boolean; }; -export const IssueCommentCreate: FC = (props) => { - const { workspaceSlug, projectId, issueId, activityOperations, showAccessSpecifier = false } = props; +// services +const fileService = new FileService(); +export const CommentCreate: FC = observer((props) => { + const { workspaceSlug, entityId, activityOperations, showToolbarInitially = false } = props; // states const [uploadedAssetIds, setUploadedAssetIds] = useState([]); // refs const editorRef = useRef(null); // store hooks const workspaceStore = useWorkspace(); - const { peekIssue } = useIssueDetail(); // derived values const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; // form info @@ -53,11 +51,12 @@ export const IssueCommentCreate: FC = (props) => { const onSubmit = async (formData: Partial) => { await activityOperations .createComment(formData) - .then(async (res) => { + .then(async () => { if (uploadedAssetIds.length > 0) { - await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId, res.id, { - asset_ids: uploadedAssetIds, - }); + // TODO: WHat is this + // await fileService.updateBulkCommentAssetsUploadStatus(workspaceSlug, entityId, res?.id, { + // asset_ids: uploadedAssetIds, + // }); setUploadedAssetIds([]); } }) @@ -70,13 +69,11 @@ export const IssueCommentCreate: FC = (props) => { }; const commentHTML = watch("comment_html"); - const isEmpty = isCommentEmpty(commentHTML); + const isEmpty = isCommentEmpty(commentHTML ?? undefined); return (
{ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty && !isSubmitting) handleSubmit(onSubmit)(e); @@ -92,10 +89,8 @@ export const IssueCommentCreate: FC = (props) => { render={({ field: { value, onChange } }) => (

"} - projectId={projectId} - issue_id={issueId} workspaceSlug={workspaceSlug} onEnterKeyPress={(e) => { if (!isEmpty && !isSubmitting) { @@ -104,17 +99,17 @@ export const IssueCommentCreate: FC = (props) => { }} ref={editorRef} initialValue={value ?? "

"} - containerClassName="min-h-[35px]" + containerClassName="min-h-min" onChange={(comment_json, comment_html) => onChange(comment_html)} accessSpecifier={accessValue ?? EIssueCommentAccessSpecifier.INTERNAL} handleAccessChange={onAccessChange} - showAccessSpecifier={showAccessSpecifier} isSubmitting={isSubmitting} uploadFile={async (blockId, file) => { const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file); setUploadedAssetIds((prev) => [...prev, asset_id]); return asset_id; }} + showToolbarInitially={showToolbarInitially} /> )} /> @@ -122,4 +117,4 @@ export const IssueCommentCreate: FC = (props) => { />
); -}; +}); diff --git a/web/core/components/comments/comment-reaction.tsx b/web/core/components/comments/comment-reaction.tsx new file mode 100644 index 00000000000..b101c9402dd --- /dev/null +++ b/web/core/components/comments/comment-reaction.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// Plane +import { TCommentsOperations, TIssueComment } from "@plane/types"; +import { Tooltip } from "@plane/ui"; +// components +import { ReactionSelector } from "@/components/issues"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { renderEmoji } from "@/helpers/emoji.helper"; + +export type TProps = { + comment: TIssueComment; + disabled?: boolean; + activityOperations: TCommentsOperations; +}; + +export const CommentReactions: FC = observer((props) => { + const { comment, activityOperations, disabled = false } = props; + + const userReactions = activityOperations.userReactions(comment.id); + const reactionIds = activityOperations.reactionIds(comment.id); + + if (!userReactions) return null; + return ( +
+ {!disabled && ( + activityOperations.react(comment.id, reactionEmoji, userReactions)} + /> + )} + + {reactionIds && + Object.keys(reactionIds || {}).map( + (reaction: string) => + reactionIds[reaction]?.length > 0 && ( + <> + + + + + ) + )} +
+ ); +}); diff --git a/web/core/components/comments/comments.tsx b/web/core/components/comments/comments.tsx new file mode 100644 index 00000000000..7c95831d794 --- /dev/null +++ b/web/core/components/comments/comments.tsx @@ -0,0 +1,56 @@ +"use client"; + +import React, { FC } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// plane imports +import { TCommentsOperations, TIssueComment } from "@plane/types"; +// local components +import { CommentCard } from "./comment-card"; +import { CommentCreate } from "./comment-create"; + +type TCommentsWrapper = { + entityId: string; + isEditingAllowed?: boolean; + activityOperations: TCommentsOperations; + comments: TIssueComment[] | string[]; + getCommentById?: (activityId: string) => TIssueComment | undefined; +}; + +export const CommentsWrapper: FC = observer((props) => { + const { entityId, activityOperations, comments, getCommentById, isEditingAllowed = true } = props; + // router + const { workspaceSlug: routerWorkspaceSlug } = useParams(); + const workspaceSlug = routerWorkspaceSlug?.toString(); + + return ( +
+ {isEditingAllowed && ( + + )} + +
+ {comments?.map((r, index) => { + let comment; + if (typeof r === "string") { + comment = getCommentById?.(r); + } else { + comment = r; + } + + if (!comment) return null; + return ( + + ); + })} +
+
+ ); +}); diff --git a/web/core/components/comments/index.ts b/web/core/components/comments/index.ts new file mode 100644 index 00000000000..222a6656c00 --- /dev/null +++ b/web/core/components/comments/index.ts @@ -0,0 +1 @@ +export * from "./comments"; diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index b06d39c8550..107822e7947 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -23,15 +23,17 @@ interface LiteTextEditorWrapperProps extends MakeOptional, "disabledExtensions"> { workspaceSlug: string; workspaceId: string; - projectId: string; + projectId?: string; accessSpecifier?: EIssueCommentAccessSpecifier; handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void; showAccessSpecifier?: boolean; showSubmitButton?: boolean; isSubmitting?: boolean; showToolbarInitially?: boolean; + showToolbar?: boolean; uploadFile: TFileHandler["upload"]; issue_id?: string; + parentClassName?: string; } export const LiteTextEditor = React.forwardRef((props, ref) => { @@ -48,6 +50,8 @@ export const LiteTextEditor = React.forwardRef !showToolbarInitially && setIsFocused(true)} onBlur={() => !showToolbarInitially && setIsFocused(false)} > @@ -102,31 +106,33 @@ export const LiteTextEditor = React.forwardRef -
- { - // TODO: update this while toolbar homogenization - // @ts-expect-error type mismatch here - editorRef?.executeMenuItemCommand({ - itemKey: item.itemKey, - ...item.extraProps, - }); - }} - handleAccessChange={handleAccessChange} - handleSubmit={(e) => rest.onEnterKeyPress?.(e)} - isCommentEmpty={isEmpty} - isSubmitting={isSubmitting} - showAccessSpecifier={showAccessSpecifier} - editorRef={editorRef} - showSubmitButton={showSubmitButton} - /> -
+ {showToolbar && ( +
+ { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }} + handleAccessChange={handleAccessChange} + handleSubmit={(e) => rest.onEnterKeyPress?.(e)} + isCommentEmpty={isEmpty} + isSubmitting={isSubmitting} + showAccessSpecifier={showAccessSpecifier} + editorRef={editorRef} + showSubmitButton={showSubmitButton} + /> +
+ )}
); }); diff --git a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx index 0763a49f968..1662211dd26 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx @@ -17,7 +17,7 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional< > & { workspaceId: string; workspaceSlug: string; - projectId: string; + projectId?: string; }; export const LiteTextReadOnlyEditor = React.forwardRef( diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index daf0115b18b..7e4f817b61b 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -3,22 +3,21 @@ import { observer } from "mobx-react"; // constants import { E_SORT_ORDER, TActivityFilters, filterActivityOnSelectedFilters } from "@plane/constants"; // hooks +import { TCommentsOperations } from "@plane/types"; +import { CommentCard } from "@/components/comments/comment-card"; import { useIssueDetail } from "@/hooks/store"; // plane web components import { IssueAdditionalPropertiesActivity } from "@/plane-web/components/issues"; import { IssueActivityWorklog } from "@/plane-web/components/issues/worklog/activity/root"; // components import { IssueActivityItem } from "./activity/activity-list"; -import { IssueCommentCard } from "./comments/comment-card"; -// types -import { TActivityOperations } from "./root"; type TIssueActivityCommentRoot = { workspaceSlug: string; projectId: string; issueId: string; selectedFilters: TActivityFilters[]; - activityOperations: TActivityOperations; + activityOperations: TCommentsOperations; showAccessSpecifier?: boolean; disabled?: boolean; sortOrder: E_SORT_ORDER; @@ -38,7 +37,7 @@ export const IssueActivityCommentRoot: FC = observer( // hooks const { activity: { getActivityCommentByIssueId }, - comment: {}, + comment: { getCommentById }, } = useIssueDetail(); const activityComments = getActivityCommentByIssueId(issueId, sortOrder); @@ -51,12 +50,10 @@ export const IssueActivityCommentRoot: FC = observer(
{filteredActivityComments.map((activityComment, index) => activityComment.activity_type === "COMMENT" ? ( - = (pr isMobile={isMobile} tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`} > - {calculateTimeAgo(activity.created_at)} + {calculateTimeAgo(activity.created_at)}
diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx deleted file mode 100644 index 9a35ef1da4f..00000000000 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { FC, ReactNode } from "react"; -import { observer } from "mobx-react"; -import { MessageCircle } from "lucide-react"; -// helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useIssueDetail } from "@/hooks/store"; - -type TIssueCommentBlock = { - commentId: string; - ends: "top" | "bottom" | undefined; - quickActions: ReactNode; - children: ReactNode; -}; - -export const IssueCommentBlock: FC = observer((props) => { - const { commentId, ends, quickActions, children } = props; - // hooks - const { - comment: { getCommentById }, - } = useIssueDetail(); - - const comment = getCommentById(commentId); - - if (!comment) return <>; - return ( -
-
-
- {comment.actor_detail?.avatar_url && comment.actor_detail?.avatar_url !== "" ? ( - { - ) : ( - <> - {comment.actor_detail?.is_bot - ? comment.actor_detail?.first_name.charAt(0) - : comment.actor_detail?.display_name.charAt(0)} - - )} -
- -
-
-
-
-
-
- {comment.actor_detail?.is_bot - ? comment.actor_detail?.first_name + " Bot" - : comment.actor_detail?.display_name} -
-
commented {calculateTimeAgo(comment.created_at)}
-
-
{children}
-
-
{quickActions}
-
-
- ); -}); diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/index.ts b/web/core/components/issues/issue-detail/issue-activity/comments/index.ts deleted file mode 100644 index 8077dd06d22..00000000000 --- a/web/core/components/issues/issue-detail/issue-activity/comments/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./comment-block"; -export * from "./comment-card"; -export * from "./comment-create"; -export * from "./root"; diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx deleted file mode 100644 index 8a7e9210845..00000000000 --- a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { SimpleEmptyState } from "@/components/empty-state"; -// hooks -import { useIssueDetail } from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -// local components -import { TActivityOperations } from "../root"; -import { IssueCommentCard } from "./comment-card"; - -type TIssueCommentRoot = { - projectId: string; - workspaceSlug: string; - issueId: string; - activityOperations: TActivityOperations; - showAccessSpecifier?: boolean; - disabled?: boolean; -}; - -export const IssueCommentRoot: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, activityOperations, showAccessSpecifier, disabled } = props; - // hooks - const { - comment: { getCommentsByIssueId }, - } = useIssueDetail(); - const { t } = useTranslation(); - // derived values - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/comments" }); - - const commentIds = getCommentsByIssueId(issueId); - if (!commentIds) return <>; - - return ( -
- {commentIds.length > 0 ? ( - commentIds.map((commentId, index) => ( - - )) - ) : ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/issues/issue-detail/issue-activity/helper.tsx b/web/core/components/issues/issue-detail/issue-activity/helper.tsx new file mode 100644 index 00000000000..3ac270daf45 --- /dev/null +++ b/web/core/components/issues/issue-detail/issue-activity/helper.tsx @@ -0,0 +1,158 @@ +import { useMemo } from "react"; +import { useTranslation } from "@plane/i18n"; +import { TCommentsOperations, TIssueComment } from "@plane/types"; +import { EFileAssetType } from "@plane/types/src/enums"; +import { setToast, TOAST_TYPE } from "@plane/ui"; +import { formatTextList } from "@/helpers/issue.helper"; +import { useEditorAsset, useIssueDetail, useMember, useUser } from "@/hooks/store"; + +export const useCommentOperations = ( + workspaceSlug: string | undefined, + projectId: string | undefined, + issueId: string | undefined +): TCommentsOperations => { + // store hooks + const { + commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById }, + createComment, + updateComment, + removeComment, + createCommentReaction, + removeCommentReaction, + } = useIssueDetail(); + const { getUserDetails } = useMember(); + const { uploadEditorAsset } = useEditorAsset(); + const { data: currentUser } = useUser(); + const { t } = useTranslation(); + + const operations = useMemo(() => { + // Define operations object with all methods + const ops = { + createComment: async (data: Partial) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + const comment = await createComment(workspaceSlug, projectId, issueId, data); + setToast({ + title: t("common.success"), + type: TOAST_TYPE.SUCCESS, + message: t("issue.comments.create.success"), + }); + return comment; + } catch { + setToast({ + title: t("common.error.label"), + type: TOAST_TYPE.ERROR, + message: t("issue.comments.create.error"), + }); + } + }, + updateComment: async (commentId: string, data: Partial) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + await updateComment(workspaceSlug, projectId, issueId, commentId, data); + setToast({ + title: t("common.success"), + type: TOAST_TYPE.SUCCESS, + message: t("issue.comments.update.success"), + }); + } catch { + setToast({ + title: t("common.error.label"), + type: TOAST_TYPE.ERROR, + message: t("issue.comments.update.error"), + }); + } + }, + removeComment: async (commentId: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + await removeComment(workspaceSlug, projectId, issueId, commentId); + setToast({ + title: t("common.success"), + type: TOAST_TYPE.SUCCESS, + message: t("issue.comments.remove.success"), + }); + } catch { + setToast({ + title: t("common.error.label"), + type: TOAST_TYPE.ERROR, + message: t("issue.comments.remove.error"), + }); + } + }, + uploadCommentAsset: async (blockId: string, file: File, commentId?: string) => { + try { + if (!workspaceSlug || !projectId) throw new Error("Missing fields"); + const res = await uploadEditorAsset({ + blockId, + data: { + entity_identifier: commentId ?? "", + entity_type: EFileAssetType.COMMENT_DESCRIPTION, + }, + file, + projectId, + workspaceSlug, + }); + return res; + } catch (error) { + console.log("Error in uploading comment asset:", error); + throw new Error(t("issue.comments.upload.error")); + } + }, + addCommentReaction: async (commentId: string, reaction: string) => { + try { + if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields"); + await createCommentReaction(workspaceSlug, projectId, commentId, reaction); + setToast({ + title: "Success!", + type: TOAST_TYPE.SUCCESS, + message: "Reaction created successfully", + }); + } catch (error) { + setToast({ + title: "Error!", + type: TOAST_TYPE.ERROR, + message: "Reaction creation failed", + }); + } + }, + deleteCommentReaction: async (commentId: string, reaction: string) => { + try { + if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields"); + removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id); + setToast({ + title: "Success!", + type: TOAST_TYPE.SUCCESS, + message: "Reaction removed successfully", + }); + } catch (error) { + setToast({ + title: "Error!", + type: TOAST_TYPE.ERROR, + message: "Reaction remove failed", + }); + } + }, + react: async (commentId: string, reactionEmoji: string, userReactions: string[]) => { + if (userReactions.includes(reactionEmoji)) await ops.deleteCommentReaction(commentId, reactionEmoji); + else await ops.addCommentReaction(commentId, reactionEmoji); + }, + reactionIds: (commentId: string) => getCommentReactionsByCommentId(commentId), + userReactions: (commentId: string) => + currentUser ? commentReactionsByUser(commentId, currentUser?.id).map((r) => r.reaction) : [], + getReactionUsers: (reaction: string, reactionIds: Record): string => { + const reactionUsers = (reactionIds?.[reaction] || []) + .map((reactionId) => { + const reactionDetails = getCommentReactionById(reactionId); + return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null; + }) + .filter((displayName): displayName is string => !!displayName); + const formattedUsers = formatTextList(reactionUsers); + return formattedUsers; + }, + }; + return ops; + }, [workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment]); + + return operations; +}; diff --git a/web/core/components/issues/issue-detail/issue-activity/index.ts b/web/core/components/issues/issue-detail/issue-activity/index.ts index e6d5ce077ca..5eb4ac7d30c 100644 --- a/web/core/components/issues/issue-detail/issue-activity/index.ts +++ b/web/core/components/issues/issue-detail/issue-activity/index.ts @@ -6,8 +6,5 @@ export * from "./activity-comment-root"; export * from "./activity/activity-list"; export * from "./activity-filter"; -// issue comment -export * from "./comments"; - // sort export * from "./sort-root"; diff --git a/web/core/components/issues/issue-detail/issue-activity/root.tsx b/web/core/components/issues/issue-detail/issue-activity/root.tsx index 0133b5e6154..f96c51a6948 100644 --- a/web/core/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/root.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react"; // plane package imports import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters, EUserPermissions } from "@plane/constants"; @@ -9,16 +9,15 @@ import { useLocalStorage } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; //types import { TFileSignedURLResponse, TIssueComment } from "@plane/types"; -import { EFileAssetType } from "@plane/types/src/enums"; -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { IssueCommentCreate } from "@/components/issues"; +import { CommentCreate } from "@/components/comments/comment-create"; import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail"; // constants // hooks -import { useEditorAsset, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store"; // plane web components import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog"; +import { useCommentOperations } from "./helper"; type TIssueActivity = { workspaceSlug: string; @@ -48,14 +47,11 @@ export const IssueActivity: FC = observer((props) => { // store hooks const { issue: { getIssueById }, - createComment, - updateComment, - removeComment, } = useIssueDetail(); + const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions(); const { getProjectById } = useProject(); const { data: currentUser } = useUser(); - const { uploadEditorAsset } = useEditorAsset(); // derived values const issue = issueId ? getIssueById(issueId) : undefined; const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId); @@ -81,82 +77,8 @@ export const IssueActivity: FC = observer((props) => { setSortOrder(sortOrder === E_SORT_ORDER.ASC ? E_SORT_ORDER.DESC : E_SORT_ORDER.ASC); }; - const activityOperations: TActivityOperations = useMemo( - () => ({ - createComment: async (data) => { - try { - if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); - const comment = await createComment(workspaceSlug, projectId, issueId, data); - setToast({ - title: t("common.success"), - type: TOAST_TYPE.SUCCESS, - message: t("issue.comments.create.success"), - }); - return comment; - } catch { - setToast({ - title: t("common.error.label"), - type: TOAST_TYPE.ERROR, - message: t("issue.comments.create.error"), - }); - } - }, - updateComment: async (commentId, data) => { - try { - if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); - await updateComment(workspaceSlug, projectId, issueId, commentId, data); - setToast({ - title: t("common.success"), - type: TOAST_TYPE.SUCCESS, - message: t("issue.comments.update.success"), - }); - } catch { - setToast({ - title: t("common.error.label"), - type: TOAST_TYPE.ERROR, - message: t("issue.comments.update.error"), - }); - } - }, - removeComment: async (commentId) => { - try { - if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); - await removeComment(workspaceSlug, projectId, issueId, commentId); - setToast({ - title: t("common.success"), - type: TOAST_TYPE.SUCCESS, - message: t("issue.comments.remove.success"), - }); - } catch { - setToast({ - title: t("common.error.label"), - type: TOAST_TYPE.ERROR, - message: t("issue.comments.remove.error"), - }); - } - }, - uploadCommentAsset: async (blockId, file, commentId) => { - try { - if (!workspaceSlug || !projectId) throw new Error("Missing fields"); - const res = await uploadEditorAsset({ - blockId, - data: { - entity_identifier: commentId ?? "", - entity_type: EFileAssetType.COMMENT_DESCRIPTION, - }, - file, - projectId, - workspaceSlug, - }); - return res; - } catch (error) { - console.log("Error in uploading comment asset:", error); - throw new Error(t("issue.comments.upload.error")); - } - }, - }), - [workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment] - ); + // helper hooks + const activityOperations = useCommentOperations(workspaceSlug, projectId, issueId); const project = getProjectById(projectId); if (!project) return <>; @@ -200,12 +122,11 @@ export const IssueActivity: FC = observer((props) => { sortOrder={sortOrder || E_SORT_ORDER.ASC} /> {!disabled && ( - )}
diff --git a/web/core/store/issue/issue-details/comment_reaction.store.ts b/web/core/store/issue/issue-details/comment_reaction.store.ts index e965ba97eed..5ab54f061b6 100644 --- a/web/core/store/issue/issue-details/comment_reaction.store.ts +++ b/web/core/store/issue/issue-details/comment_reaction.store.ts @@ -144,13 +144,15 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore { }; createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => { + // eslint-disable-next-line no-useless-catch try { const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, { reaction, }); + if (!this.commentReactions[commentId]) this.commentReactions[commentId] = {}; runInAction(() => { - update(this.commentReactions, [commentId, reaction], (reactionId) => { + update(this.commentReactions, `${commentId}.${reaction}`, (reactionId) => { if (!reactionId) return [response.id]; return concat(reactionId, response.id); }); diff --git a/web/ee/components/comments/index.ts b/web/ee/components/comments/index.ts new file mode 100644 index 00000000000..6cff36e715e --- /dev/null +++ b/web/ee/components/comments/index.ts @@ -0,0 +1 @@ +export * from "ce/components/comments"; From 275aa54d98a9fef16242b03be268edcd7f45af7a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 17 Mar 2025 14:06:15 +0530 Subject: [PATCH 02/11] fix: add edited at --- apiserver/plane/bgtasks/issue_activities_task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index efc7ce8da65..d470e6348c8 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -710,6 +710,11 @@ def update_comment_activity( ) if current_instance.get("comment_html") != requested_data.get("comment_html"): + # Get the issue comment and update the edited_at + issue_comment = IssueComment.objects.get(id=current_instance.get("id")) + issue_comment.edited_at = timezone.now() + issue_comment.save(update_fields=["edited_at"]) + issue_activities.append( IssueActivity( issue_id=issue_id, From ebfc6478b61c6eed34ba747dbcee01b61c365aa1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 17 Mar 2025 14:22:40 +0530 Subject: [PATCH 03/11] chore: add edited_at validation at issue comment update --- apiserver/plane/app/views/issue/comment.py | 8 +++++++- apiserver/plane/bgtasks/issue_activities_task.py | 5 ----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 91d27bff29e..f257683feb1 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -105,7 +105,13 @@ def partial_update(self, request, slug, project_id, issue_id, pk): issue_comment, data=request.data, partial=True ) if serializer.is_valid(): - serializer.save() + if ( + "comment_html" in request.data + and request.data["comment_html"] != issue_comment.comment_html + ): + serializer.save(edited_at=timezone.now()) + else: + serializer.save() issue_activity.delay( type="comment.activity.updated", requested_data=requested_data, diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index d470e6348c8..efc7ce8da65 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -710,11 +710,6 @@ def update_comment_activity( ) if current_instance.get("comment_html") != requested_data.get("comment_html"): - # Get the issue comment and update the edited_at - issue_comment = IssueComment.objects.get(id=current_instance.get("id")) - issue_comment.edited_at = timezone.now() - issue_comment.save(update_fields=["edited_at"]) - issue_activities.append( IssueActivity( issue_id=issue_id, From 5c31fcddcc18b515c0900f66756ea150462d7368 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 17 Mar 2025 14:33:23 +0530 Subject: [PATCH 04/11] fix: comment mentions --- web/core/components/comments/comment-card.tsx | 4 ++++ web/core/components/comments/comment-create.tsx | 3 +++ 2 files changed, 7 insertions(+) diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index 2e0a07f037e..4d625e5de21 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -2,6 +2,7 @@ import { FC, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { useForm } from "react-hook-form"; import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react"; // PLane @@ -32,6 +33,7 @@ type TCommentCard = { export const CommentCard: FC = observer((props) => { const { workspaceSlug, comment, activityOperations, ends, showAccessSpecifier = false, disabled = false } = props; const { t } = useTranslation(); + const { projectId } = useParams(); // refs const editorRef = useRef(null); const showEditorRef = useRef(null); @@ -150,6 +152,7 @@ export const CommentCard: FC = observer((props) => { const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id); return asset_id; }} + projectId={(projectId as string) ?? ""} />
@@ -194,6 +197,7 @@ export const CommentCard: FC = observer((props) => { workspaceSlug={workspaceSlug} editorClassName="[&>*]:!py-0 [&>*]:!text-sm" containerClassName="!py-1" + projectId={(projectId as string) ?? ""} />
diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index 281ebc4a3a1..5d340b164b1 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -1,5 +1,6 @@ import { FC, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { useForm, Controller } from "react-hook-form"; // plane constants import { EIssueCommentAccessSpecifier } from "@plane/constants"; @@ -27,6 +28,7 @@ type TCommentCreate = { const fileService = new FileService(); export const CommentCreate: FC = observer((props) => { const { workspaceSlug, entityId, activityOperations, showToolbarInitially = false } = props; + const { projectId } = useParams(); // states const [uploadedAssetIds, setUploadedAssetIds] = useState([]); // refs @@ -88,6 +90,7 @@ export const CommentCreate: FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => (

"} From d82debebb507c26ab1ac077399b3aab60e84c476 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 17 Mar 2025 15:07:03 +0530 Subject: [PATCH 05/11] fix: edited at --- web/core/services/issue/issue_comment.service.ts | 2 +- web/core/store/issue/issue-details/comment.store.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/core/services/issue/issue_comment.service.ts b/web/core/services/issue/issue_comment.service.ts index ccfce8a86c1..8a55f49a1d5 100644 --- a/web/core/services/issue/issue_comment.service.ts +++ b/web/core/services/issue/issue_comment.service.ts @@ -63,7 +63,7 @@ export class IssueCommentService extends APIService { issueId: string, commentId: string, data: Partial - ): Promise { + ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/comments/${commentId}/`, data diff --git a/web/core/store/issue/issue-details/comment.store.ts b/web/core/store/issue/issue-details/comment.store.ts index b0fba6d3c05..9bb3a7d1937 100644 --- a/web/core/store/issue/issue-details/comment.store.ts +++ b/web/core/store/issue/issue-details/comment.store.ts @@ -155,6 +155,11 @@ export class IssueCommentStore implements IIssueCommentStore { data ); + runInAction(() => { + set(this.commentMap, [commentId, "updated_at"], response.updated_at); + set(this.commentMap, [commentId, "edited_at"], response.edited_at); + }); + return response; } catch (error) { this.rootIssueDetail.activity.fetchActivities(workspaceSlug, projectId, issueId); From 806b96e676b13139cb320472a883f6fdfec8f435 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 18 Mar 2025 18:07:19 +0530 Subject: [PATCH 06/11] fix: css --- web/core/components/comments/comments.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/comments/comments.tsx b/web/core/components/comments/comments.tsx index 7c95831d794..2dfb6c2abe5 100644 --- a/web/core/components/comments/comments.tsx +++ b/web/core/components/comments/comments.tsx @@ -29,7 +29,7 @@ export const CommentsWrapper: FC = observer((props) => { )} -
+
{comments?.map((r, index) => { let comment; if (typeof r === "string") { From 39d2c25dab096754e0c1a604c2f6599bcbae9804 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 21 Mar 2025 18:32:33 +0530 Subject: [PATCH 07/11] fix: added bulk asset upload api --- web/core/components/comments/comment-create.tsx | 17 +++++++++++------ web/core/services/file.service.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index 5d340b164b1..f4fd42e98a0 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -55,10 +55,15 @@ export const CommentCreate: FC = observer((props) => { .createComment(formData) .then(async () => { if (uploadedAssetIds.length > 0) { - // TODO: WHat is this - // await fileService.updateBulkCommentAssetsUploadStatus(workspaceSlug, entityId, res?.id, { - // asset_ids: uploadedAssetIds, - // }); + if (projectId) { + await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId.toString(), entityId, { + asset_ids: uploadedAssetIds, + }); + } else { + await fileService.updateBulkWorkspaceAssetsUploadStatus(workspaceSlug, entityId, { + asset_ids: uploadedAssetIds, + }); + } setUploadedAssetIds([]); } }) @@ -90,7 +95,6 @@ export const CommentCreate: FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => (

"} @@ -102,7 +106,7 @@ export const CommentCreate: FC = observer((props) => { }} ref={editorRef} initialValue={value ?? "

"} - containerClassName="min-h-min" + containerClassName="min-h-min [&_p]:!p-0 [&_p]:!text-base" onChange={(comment_json, comment_html) => onChange(comment_html)} accessSpecifier={accessValue ?? EIssueCommentAccessSpecifier.INTERNAL} handleAccessChange={onAccessChange} @@ -113,6 +117,7 @@ export const CommentCreate: FC = observer((props) => { return asset_id; }} showToolbarInitially={showToolbarInitially} + parentClassName="p-2" /> )} /> diff --git a/web/core/services/file.service.ts b/web/core/services/file.service.ts index dba3027c552..2c9c82a6daa 100644 --- a/web/core/services/file.service.ts +++ b/web/core/services/file.service.ts @@ -109,6 +109,20 @@ export class FileService extends APIService { }); } + async updateBulkWorkspaceAssetsUploadStatus( + workspaceSlug: string, + entityId: string, + data: { + asset_ids: string[]; + } + ): Promise { + return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/${entityId}/bulk/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async updateBulkProjectAssetsUploadStatus( workspaceSlug: string, projectId: string, From b4cd1e0c0c7e4cfa958d7530db0e8c6e04eec4af Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 25 Mar 2025 13:30:54 +0530 Subject: [PATCH 08/11] fix: projectId prop fixed --- web/core/components/comments/comment-card.tsx | 13 ++++++++++--- web/core/components/comments/comment-create.tsx | 4 ++-- web/core/components/comments/comments.tsx | 11 +++++++++-- .../issue-activity/activity-comment-root.tsx | 1 + .../issues/issue-detail/issue-activity/root.tsx | 1 + 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index 4d625e5de21..915f7d0b6a4 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -2,7 +2,6 @@ import { FC, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import { useForm } from "react-hook-form"; import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react"; // PLane @@ -28,12 +27,20 @@ type TCommentCard = { ends: "top" | "bottom" | undefined; showAccessSpecifier?: boolean; disabled?: boolean; + projectId?: string; }; export const CommentCard: FC = observer((props) => { - const { workspaceSlug, comment, activityOperations, ends, showAccessSpecifier = false, disabled = false } = props; + const { + workspaceSlug, + comment, + activityOperations, + ends, + showAccessSpecifier = false, + disabled = false, + projectId, + } = props; const { t } = useTranslation(); - const { projectId } = useParams(); // refs const editorRef = useRef(null); const showEditorRef = useRef(null); diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index f4fd42e98a0..3d1b80a6c86 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -22,13 +22,13 @@ type TCommentCreate = { workspaceSlug: string; activityOperations: TCommentsOperations; showToolbarInitially?: boolean; + projectId?: string; }; // services const fileService = new FileService(); export const CommentCreate: FC = observer((props) => { - const { workspaceSlug, entityId, activityOperations, showToolbarInitially = false } = props; - const { projectId } = useParams(); + const { workspaceSlug, entityId, activityOperations, showToolbarInitially = false, projectId } = props; // states const [uploadedAssetIds, setUploadedAssetIds] = useState([]); // refs diff --git a/web/core/components/comments/comments.tsx b/web/core/components/comments/comments.tsx index 2dfb6c2abe5..67aa1b6c741 100644 --- a/web/core/components/comments/comments.tsx +++ b/web/core/components/comments/comments.tsx @@ -10,6 +10,7 @@ import { CommentCard } from "./comment-card"; import { CommentCreate } from "./comment-create"; type TCommentsWrapper = { + projectId?: string; entityId: string; isEditingAllowed?: boolean; activityOperations: TCommentsOperations; @@ -18,7 +19,7 @@ type TCommentsWrapper = { }; export const CommentsWrapper: FC = observer((props) => { - const { entityId, activityOperations, comments, getCommentById, isEditingAllowed = true } = props; + const { entityId, activityOperations, comments, getCommentById, isEditingAllowed = true, projectId } = props; // router const { workspaceSlug: routerWorkspaceSlug } = useParams(); const workspaceSlug = routerWorkspaceSlug?.toString(); @@ -26,7 +27,12 @@ export const CommentsWrapper: FC = observer((props) => { return (
{isEditingAllowed && ( - + )}
@@ -47,6 +53,7 @@ export const CommentsWrapper: FC = observer((props) => { activityOperations={activityOperations} disabled={!isEditingAllowed} ends={index === 0 ? "top" : index === comments.length - 1 ? "bottom" : undefined} + projectId={projectId} /> ); })} diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 7e4f817b61b..17117189ffe 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -58,6 +58,7 @@ export const IssueActivityCommentRoot: FC = observer( ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined} showAccessSpecifier={showAccessSpecifier} disabled={disabled} + projectId={projectId} /> ) : activityComment.activity_type === "ACTIVITY" ? ( = observer((props) => { entityId={issueId} activityOperations={activityOperations} showToolbarInitially + projectId={projectId} /> )}
From 430a22080ac2bcf1f8ccc2c226e8c391be7839b3 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 25 Mar 2025 15:08:40 +0530 Subject: [PATCH 09/11] fix: css --- web/ce/components/comments/comment-block.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ce/components/comments/comment-block.tsx b/web/ce/components/comments/comment-block.tsx index c9357d2f7cf..3544ebca945 100644 --- a/web/ce/components/comments/comment-block.tsx +++ b/web/ce/components/comments/comment-block.tsx @@ -34,7 +34,7 @@ export const CommentBlock: FC = observer((props) => { />
Date: Thu, 27 Mar 2025 14:15:24 +0530 Subject: [PATCH 10/11] fix: refactor --- packages/i18n/src/locales/en/translations.json | 4 +++- packages/i18n/src/locales/es/translations.json | 2 ++ packages/i18n/src/locales/fr/translations.json | 2 ++ packages/i18n/src/locales/it/translations.json | 2 ++ packages/i18n/src/locales/ja/translations.json | 2 ++ packages/i18n/src/locales/ko/translations.json | 2 ++ packages/i18n/src/locales/pl/translations.json | 3 +++ packages/i18n/src/locales/ru/translations.json | 2 ++ packages/i18n/src/locales/sk/translations.json | 2 ++ packages/i18n/src/locales/ua/translations.json | 3 +++ packages/i18n/src/locales/zh-CN/translations.json | 2 ++ packages/i18n/src/locales/zh-TW/translations.json | 2 ++ web/ce/components/comments/comment-block.tsx | 6 ++++-- web/core/components/comments/comment-card.tsx | 5 +++-- web/core/components/comments/comment-create.tsx | 3 +-- web/core/components/comments/comments.tsx | 8 ++++---- .../issue-activity/activity-comment-root.tsx | 11 ++++++----- .../issue/issue-details/comment_reaction.store.ts | 3 ++- 18 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 6beb2917b5f..fa4480b4ab0 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -331,7 +331,9 @@ "re_generate_key": "Re-generate key", "export": "Export", "member": "{count, plural, one{# member} other{# members}}", - + "edited": "Modifié", + "bot": "Bot", + "project_view": { "sort_by": { "created_at": "Created at", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index c349364d4bf..72f88fdf282 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -503,6 +503,8 @@ "re_generate_key": "Regenerar clave", "export": "Exportar", "member": "{count, plural, one{# miembro} other{# miembros}}", + "edited": "Modificado", + "bot": "Bot", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 651622b9714..03be496340e 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -501,6 +501,8 @@ "re_generate_key": "Régénérer la clé", "export": "Exporter", "member": "{count, plural, one{# membre} other{# membres}}", + "edited": "Modifié", + "bot": "Bot", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index ba40f5b9bd1..00994c5a331 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -500,6 +500,8 @@ "re_generate_key": "Rigenera chiave", "export": "Esporta", "member": "{count, plural, one {# membro} other {# membri}}", + "edited": "Modificato", + "bot": "Bot", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 8f3c82e0596..ad3d25fabc5 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -501,6 +501,8 @@ "re_generate_key": "キーを再生成", "export": "エクスポート", "member": "{count, plural, other{# メンバー}}", + "edited": "編集済み", + "bot": "ボット", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index b3b0a29d405..e8622338383 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -501,6 +501,8 @@ "re_generate_key": "키 다시 생성", "export": "내보내기", "member": "{count, plural, one{# 멤버} other{# 멤버}}", + "edited": "수정됨", + "bot": "봇", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 3cee6585357..b861d03d52b 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -499,6 +499,9 @@ "re_generate_key": "Wygeneruj klucz ponownie", "export": "Eksportuj", "member": "{count, plural, one{# członek} few{# członkowie} other{# członków}}", + "edited": "Edytowano", + "bot": "Bot", + "project_view": { "sort_by": { "created_at": "Utworzono dnia", diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index d753b30bac1..664c9c42043 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -499,6 +499,8 @@ "re_generate_key": "Перегенерировать ключ", "export": "Экспорт", "member": "{count, plural, one{# участник} few{# участника} other{# участников}}", + "edited": "Редактировано", + "bot": "Бот", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 30a1a4db835..ff86734bd00 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -499,6 +499,8 @@ "re_generate_key": "Znova generovať kľúč", "export": "Exportovať", "member": "{count, plural, one{# člen} few{# členovia} other{# členov}}", + "edited": "Upravené", + "bot": "Bot", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index ace8349fff3..389fe3d660d 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -499,6 +499,9 @@ "re_generate_key": "Повторно згенерувати ключ", "export": "Експортувати", "member": "{count, plural, one{# учасник} few{# учасники} other{# учасників}}", + "edited": "Редагувано", + "bot": "Бот", + "project_view": { "sort_by": { "created_at": "Створено", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 5e2725127b5..48774ad038a 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -501,6 +501,8 @@ "re_generate_key": "重新生成密钥", "export": "导出", "member": "{count, plural, other{# 成员}}", + "edited": "已编辑", + "bot": "机器人", "project_view": { "sort_by": { diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index a59d78e4dd1..0deb5f4a884 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -501,6 +501,8 @@ "re_generate_key": "重新產生金鑰", "export": "匯出", "member": "{count, plural, one{# 位成員} other{# 位成員}}", + "edited": "已編輯", + "bot": "機器人", "project_view": { "sort_by": { diff --git a/web/ce/components/comments/comment-block.tsx b/web/ce/components/comments/comment-block.tsx index 3544ebca945..72b5b7bf58d 100644 --- a/web/ce/components/comments/comment-block.tsx +++ b/web/ce/components/comments/comment-block.tsx @@ -1,6 +1,7 @@ import { FC, ReactNode, useRef } from "react"; import { observer } from "mobx-react"; // plane imports +import { useTranslation } from "@plane/i18n"; import { TIssueComment } from "@plane/types"; import { Avatar, Tooltip } from "@plane/ui"; import { calculateTimeAgo, cn, getFileURL, renderFormattedDate } from "@plane/utils"; @@ -20,6 +21,7 @@ export const CommentBlock: FC = observer((props) => { const commentBlockRef = useRef(null); // store hooks const { getUserDetails } = useMember(); + const { t } = useTranslation(); const userDetails = getUserDetails(comment?.actor); if (!comment || !userDetails) return <>; @@ -49,7 +51,7 @@ export const CommentBlock: FC = observer((props) => {
{comment?.actor_detail?.is_bot - ? comment?.actor_detail?.first_name + " Bot" + ? comment?.actor_detail?.first_name + ` ${t("bot")}` : comment?.actor_detail?.display_name || userDetails.display_name}
@@ -60,7 +62,7 @@ export const CommentBlock: FC = observer((props) => { > {calculateTimeAgo(comment.updated_at)} - {comment.edited_at && " (edited)"} + {comment.edited_at && ` (${t("edited")})`}
diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index 915f7d0b6a4..0f52cd66bf4 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -64,6 +64,7 @@ export const CommentCard: FC = observer((props) => { 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) => { @@ -159,7 +160,7 @@ export const CommentCard: FC = observer((props) => { const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id); return asset_id; }} - projectId={(projectId as string) ?? ""} + projectId={projectId?.toString() ?? ""} />
@@ -167,7 +168,7 @@ export const CommentCard: FC = observer((props) => {