diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 5c64c3e5..143c78da 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -208,6 +208,9 @@ "Reply...": "Reply...", "Error loading comments.": "Error loading comments.", "No comments yet.": "No comments yet.", + "No open comments.": "No open comments.", + "No resolved comments.": "No resolved comments.", + "Add a comment...": "Add a comment...", "Edit comment": "Edit comment", "Delete comment": "Delete comment", "Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?", diff --git a/apps/client/src/components/common/avatar-uploader.tsx b/apps/client/src/components/common/avatar-uploader.tsx index 0c83411c..8d9552f6 100644 --- a/apps/client/src/components/common/avatar-uploader.tsx +++ b/apps/client/src/components/common/avatar-uploader.tsx @@ -130,7 +130,7 @@ export default function AvatarUploader({ top: "50%", left: "50%", transform: "translate(-50%, -50%)", - zIndex: 1000, + zIndex: 200, }} > diff --git a/apps/client/src/components/layouts/global/aside.tsx b/apps/client/src/components/layouts/global/aside.tsx index 1d6d3f41..324bf3bf 100644 --- a/apps/client/src/components/layouts/global/aside.tsx +++ b/apps/client/src/components/layouts/global/aside.tsx @@ -31,7 +31,7 @@ export default function Aside() { } return ( - + {component && ( <> diff --git a/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx b/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx index 6f7578c2..ab02d770 100644 --- a/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx +++ b/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx @@ -271,7 +271,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => { return createPortal(
{ + try { + setIsPageCommentLoading(true); + const createdComment = await createCommentMutation.mutateAsync({ + pageId: page?.id, + content: JSON.stringify(content), + }); + + emit({ + operation: "invalidateComment", + pageId: page?.id, + }); + + setTimeout(() => { + const selector = `div[data-comment-id="${createdComment.id}"]`; + const commentElement = document.querySelector(selector); + commentElement?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 400); + } catch (error) { + console.error("Failed to post comment:", error); + } finally { + setIsPageCommentLoading(false); + } + }, + [createCommentMutation, page?.id], + ); + const handleAddReply = useCallback( async (commentId: string, content: string) => { try { @@ -132,70 +163,76 @@ function CommentListWithTabs() { const totalComments = activeComments.length + resolvedComments.length; + const pageCommentInput = canComment ? ( + + ) : null; + // If not cloud/enterprise, show simple list without tabs if (!isCloudEE) { - if (totalComments === 0) { - return <>{t("No comments yet.")}; - } - return ( - -
- {comments?.items - .filter((comment: IComment) => comment.parentCommentId === null) - .map((comment) => ( - -
- - -
- - {canComment && ( - <> - - + +
+ {comments?.items + .filter((comment: IComment) => comment.parentCommentId === null) + .map((comment) => ( + +
+ - - )} - - ))} -
- + +
+ + {canComment && ( + <> + + + + )} +
+ ))} +
+
+ {pageCommentInput} +
); } return (
- + -
+
{activeComments.length === 0 ? ( - - {t("No open comments.")} - +
+ + + + {t("No open comments.")} + + +
) : ( activeComments.map(renderComments) )} @@ -237,16 +279,23 @@ function CommentListWithTabs() { {resolvedComments.length === 0 ? ( - - {t("No resolved comments.")} - +
+ + + + {t("No resolved comments.")} + + +
) : ( resolvedComments.map(renderComments) )}
+
+ {pageCommentInput}
); } @@ -298,7 +347,7 @@ const ChildComments = ({ const MemoizedChildComments = memo(ChildComments); -const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { +const CommentEditorWithActions = ({ commentId, onSave, isLoading, placeholder = undefined }) => { const [content, setContent] = useState(""); const { ref, focused } = useFocusWithin(); const commentEditorRef = useRef(null); @@ -316,10 +365,48 @@ const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { onUpdate={setContent} onSave={handleSave} editable={true} + placeholder={placeholder} /> {focused && }
); }; +const PageCommentInput = ({ onSave, isLoading }) => { + const { t } = useTranslation(); + const [content, setContent] = useState(""); + const { ref, focused } = useFocusWithin(); + const commentEditorRef = useRef(null); + + const handleSave = useCallback(() => { + onSave(null, content); + setContent(""); + commentEditorRef.current?.clearContent(); + }, [content, onSave]); + + return ( +
+ + {focused && ( + + + + )} +
+ ); +}; + export default CommentListWithTabs; diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index a5d3f8a5..e7bc89f3 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -164,7 +164,7 @@ export const EditorBubbleMenu: FC = (props) => { return (
{isGenerativeAiEnabled && ( diff --git a/apps/client/src/features/editor/components/mention/mention-suggestion.ts b/apps/client/src/features/editor/components/mention/mention-suggestion.ts index 01a4ffad..658fd182 100644 --- a/apps/client/src/features/editor/components/mention/mention-suggestion.ts +++ b/apps/client/src/features/editor/components/mention/mention-suggestion.ts @@ -106,7 +106,7 @@ const mentionRenderItems = () => { left: `${x}px`, top: `${y}px`, position: "absolute", - zIndex: "100", + zIndex: "190", }); }); }, diff --git a/apps/server/src/core/comment/comment.service.ts b/apps/server/src/core/comment/comment.service.ts index 91a96554..84824544 100644 --- a/apps/server/src/core/comment/comment.service.ts +++ b/apps/server/src/core/comment/comment.service.ts @@ -66,8 +66,8 @@ export class CommentService { const comment = await this.commentRepo.insertComment({ pageId: page.id, content: commentContent, - selection: createCommentDto?.selection?.substring(0, 250), - type: 'inline', + selection: createCommentDto?.selection?.substring(0, 250) ?? null, + type: createCommentDto.type ?? 'page', parentCommentId: createCommentDto?.parentCommentId, creatorId: userId, workspaceId: workspaceId, diff --git a/apps/server/src/core/comment/dto/create-comment.dto.ts b/apps/server/src/core/comment/dto/create-comment.dto.ts index 26bdbf26..ca21f47b 100644 --- a/apps/server/src/core/comment/dto/create-comment.dto.ts +++ b/apps/server/src/core/comment/dto/create-comment.dto.ts @@ -1,4 +1,4 @@ -import { IsJSON, IsOptional, IsString, IsUUID } from 'class-validator'; +import { IsIn, IsJSON, IsOptional, IsString, IsUUID } from 'class-validator'; export class CreateCommentDto { @IsString() @@ -11,6 +11,10 @@ export class CreateCommentDto { @IsString() selection: string; + @IsOptional() + @IsIn(['inline', 'page']) + type: string; + @IsOptional() @IsUUID() parentCommentId: string;