From ee00975fd41b93d5f0653ec6c2f1ddba9fdc4020 Mon Sep 17 00:00:00 2001 From: cuisailong Date: Mon, 11 Aug 2025 16:46:17 +0800 Subject: [PATCH] feat(UI):Comments support quote and copy --- .../web/components/Issues/IssueDetailPage.tsx | 2 +- .../components/MrView/CommentDropdownMenu.tsx | 105 +++++++++++++++++- moon/apps/web/components/MrView/MRComment.tsx | 5 +- .../web/components/MrView/TimelineItems.tsx | 5 +- .../SimpleNoteEditor/SimpleNoteContent.tsx | 2 +- moon/apps/web/pages/[org]/mr/[link]/index.tsx | 2 +- 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx index f8f1bdf69..3438f4666 100644 --- a/moon/apps/web/components/Issues/IssueDetailPage.tsx +++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx @@ -384,7 +384,7 @@ export default function IssueDetailPage({ link }: { link: string }) { ) : ( - + )} {info && info.status === 'open' && ( diff --git a/moon/apps/web/components/MrView/CommentDropdownMenu.tsx b/moon/apps/web/components/MrView/CommentDropdownMenu.tsx index 292038dff..7e6e55b43 100644 --- a/moon/apps/web/components/MrView/CommentDropdownMenu.tsx +++ b/moon/apps/web/components/MrView/CommentDropdownMenu.tsx @@ -18,17 +18,25 @@ import { useDeleteIssueComment } from '@/hooks/issues/useDeleteIssueComment' import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete' import { editIdAtom } from '../Issues/utils/store' +import { SimpleNoteContentRef } from '../SimpleNoteEditor/SimpleNoteContent'; +import { generateJSON } from '@tiptap/core'; +import toast from 'react-hot-toast'; interface CommentDropdownMenuProps { id: string Conversation: ConversationItem CommentType: 'mr' | 'issue' | (string & {}) + editorRef: React.RefObject } -export function CommentDropdownMenu({ Conversation, id, CommentType }: CommentDropdownMenuProps) { + +export function CommentDropdownMenu({ Conversation, id, CommentType, editorRef }: CommentDropdownMenuProps) { const { mutate: deleteComment } = useDeleteMrCommentDelete(id) const { mutate: deleteIssueComment } = useDeleteIssueComment(id) const [_editId, setEditId] = useAtom(editIdAtom) + const commentContent = typeof Conversation.comment === 'string' + ? Conversation.comment + : String(Conversation.comment) const handleDelete = () => { switch (CommentType) { @@ -43,16 +51,107 @@ export function CommentDropdownMenu({ Conversation, id, CommentType }: CommentDr } } + const handleQuote = () => { + if (!editorRef?.current?.editor || !Conversation.comment) return + + try { + const jsonContent = generateJSON(commentContent, editorRef.current.editor.extensionManager.extensions) + + editorRef.current.editor.chain().insertContent([ + { + type: 'blockquote', + content: jsonContent.content, + }, + { + type: 'paragraph', + content: [] + } + ]).run() + + setTimeout(() => { + const editorDom = editorRef.current?.editor?.view?.dom + + if (editorDom) { + editorDom.scrollIntoView({ behavior: 'smooth', block: 'center' }) + + setTimeout(() => { + editorRef.current?.editor?.commands.focus('end', { scrollIntoView: false }) + }, 150) + } + }, 300) + + } catch (error) { + toast.error('Quote failed, Please try again later.') + } + } + + + const handleCopy = () => { + let value = commentContent; + + if (value.includes('<') && value.includes('>')) { + const tempDiv = document.createElement('div'); + + tempDiv.innerHTML = value; + value = tempDiv.textContent || tempDiv.innerText || ''; + } + + value = value.trim(); + + if (!value) return; + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(value) + .catch(() => { + toast.error('Copy failed, Please try again later.') + }) + } else { + const textArea = document.createElement('textarea'); + + textArea.value = value; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + textArea.style.top = '-9999px'; + textArea.style.opacity = '0'; + textArea.style.pointerEvents = 'none'; + textArea.style.zIndex = '-1000'; + document.body.appendChild(textArea); + + setTimeout(() => { + try { + textArea.focus(); + + setTimeout(() => { + textArea.select(); + textArea.setSelectionRange(0, textArea.value.length); + + const successful = document.execCommand('copy'); + + if (!successful) { + toast.error('Copy failed, Please try again later.') + } + + document.body.removeChild(textArea); + }, 10); + } catch (err) { + document.body.removeChild(textArea); + } + }, 10); + } + }; + const items = buildMenuItems([ { type: 'item', label: 'Copy', - leftSlot: + leftSlot: , + onSelect: () => handleCopy(), }, { type: 'item', label: 'Quote', - leftSlot: + leftSlot: , + onSelect: () => handleQuote(), }, { type: 'item', diff --git a/moon/apps/web/components/MrView/MRComment.tsx b/moon/apps/web/components/MrView/MRComment.tsx index 17ecbee0f..234f73eb1 100644 --- a/moon/apps/web/components/MrView/MRComment.tsx +++ b/moon/apps/web/components/MrView/MRComment.tsx @@ -32,9 +32,10 @@ interface CommentProps { conv: ConversationItem id: string whoamI: string + editorRef: React.RefObject } -const Comment = ({ conv, id, whoamI }: CommentProps) => { +const Comment = ({ conv, id, whoamI, editorRef }: CommentProps) => { const { data: member } = useGetOrganizationMember({ username: conv.username }) const extensions = useMemo(() => getNoteExtensions({ linkUnfurl: {} }), []) @@ -93,7 +94,7 @@ const Comment = ({ conv, id, whoamI }: CommentProps) => { } onReactionSelect={handleReactionSelect} /> - + diff --git a/moon/apps/web/components/MrView/TimelineItems.tsx b/moon/apps/web/components/MrView/TimelineItems.tsx index 322a69ce2..5582e96e4 100644 --- a/moon/apps/web/components/MrView/TimelineItems.tsx +++ b/moon/apps/web/components/MrView/TimelineItems.tsx @@ -22,6 +22,7 @@ import MergedItem from './MergedItem' import ReopenItem from './ReopenItem' import LabelItem from '@/components/MrView/LabelItem' import ForcePushItem from './item/ForcePushItem' +import { SimpleNoteContentRef } from '../SimpleNoteEditor/SimpleNoteContent' interface TimelineItemProps { badge?: React.ReactNode @@ -68,7 +69,7 @@ const TimelineWrapper: React.FC = ({ convItems = [] }) => ) } -const TimelineItems: React.FC<{ detail: any; id: string; type: string }> = ({ detail, id, type }) => { +const TimelineItems: React.FC<{ detail: any; id: string; type: string; editorRef: React.RefObject }> = ({ detail, id, type, editorRef }) => { const convItems: ConvItem[] = detail.conversations.map((conv: ConversationItem) => { let icon let children @@ -77,7 +78,7 @@ const TimelineItems: React.FC<{ detail: any; id: string; type: string }> = ({ de switch (conv.conv_type) { case 'Comment': icon = - children = + children = break case 'Merged': icon = diff --git a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx index 89d56af1f..baafb81d0 100644 --- a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx +++ b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx @@ -160,7 +160,7 @@ export const SimpleNoteContent = memo( }, [editor, onChange]) return ( -
+
{ diff --git a/moon/apps/web/pages/[org]/mr/[link]/index.tsx b/moon/apps/web/pages/[org]/mr/[link]/index.tsx index c50c930f0..c2ad4f309 100644 --- a/moon/apps/web/pages/[org]/mr/[link]/index.tsx +++ b/moon/apps/web/pages/[org]/mr/[link]/index.tsx @@ -290,7 +290,7 @@ const MRDetailPage: PageWithLayout = () => {
) : ( - mrDetail && + mrDetail && )}