diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index f8d627b308..857a2d2116 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import { memo, useMemo } from 'react' import { useReactFlow, useViewport } from 'reactflow' -import { RiCloseLine } from '@remixicon/react' +import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine } from '@remixicon/react' import Avatar from '@/app/components/base/avatar' import cn from '@/utils/classnames' import { useFormatTimeFromNow } from '@/app/components/workflow/hooks' @@ -13,6 +13,12 @@ type CommentThreadProps = { comment: WorkflowCommentDetail loading?: boolean onClose: () => void + onDelete?: () => void + onResolve?: () => void + onPrev?: () => void + onNext?: () => void + canGoPrev?: boolean + canGoNext?: boolean } const ThreadMessage: FC<{ @@ -58,7 +64,17 @@ const renderReply = (reply: WorkflowCommentDetailReply) => ( /> ) -export const CommentThread: FC = memo(({ comment, loading = false, onClose }) => { +export const CommentThread: FC = memo(({ + comment, + loading = false, + onClose, + onDelete, + onResolve, + onPrev, + onNext, + canGoPrev, + canGoNext, +}) => { const { flowToScreenPosition } = useReactFlow() const viewport = useViewport() @@ -81,14 +97,53 @@ export const CommentThread: FC = memo(({ comment, loading =
Comment
- +
+ + +
+ + + +
{ const params = useParams() @@ -100,8 +100,7 @@ export const useWorkflowComment = () => { setActiveCommentId(comment.id) const cachedDetail = commentDetailCacheRef.current[comment.id] - const fallbackDetail = cachedDetail ?? comment - setActiveComment(fallbackDetail) + setActiveComment(cachedDetail || comment) reactflow.setCenter(comment.position_x, comment.position_y, { zoom: 1, duration: 600 }) @@ -130,13 +129,85 @@ export const useWorkflowComment = () => { } }, [appId, reactflow, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache, setControlMode, setPendingComment]) + const handleCommentResolve = useCallback(async (commentId: string) => { + if (!appId) return + + setActiveCommentLoading(true) + try { + await resolveWorkflowComment(appId, commentId) + const detailResponse = await fetchWorkflowComment(appId, commentId) + const detail = (detailResponse as any)?.data ?? detailResponse + + commentDetailCacheRef.current = { + ...commentDetailCacheRef.current, + [commentId]: detail, + } + setCommentDetailCache(commentDetailCacheRef.current) + setActiveComment(detail) + await loadComments() + } + catch (error) { + console.error('Failed to resolve comment:', error) + } + finally { + setActiveCommentLoading(false) + } + }, [appId, loadComments, setActiveComment, setActiveCommentLoading, setCommentDetailCache]) + + const handleCommentDelete = useCallback(async (commentId: string) => { + if (!appId) return + + setActiveCommentLoading(true) + try { + await deleteWorkflowComment(appId, commentId) + const updatedCache = { ...commentDetailCacheRef.current } + delete updatedCache[commentId] + commentDetailCacheRef.current = updatedCache + setCommentDetailCache(updatedCache) + + const currentComments = comments.filter(c => c.id !== commentId) + const commentIndex = comments.findIndex(c => c.id === commentId) + const fallbackTarget = commentIndex >= 0 ? comments[commentIndex + 1] ?? comments[commentIndex - 1] : undefined + + await loadComments() + + if (fallbackTarget) { + handleCommentIconClick(fallbackTarget) + } + else if (currentComments.length > 0) { + const nextComment = currentComments[0] + handleCommentIconClick(nextComment) + } + else { + setActiveComment(null) + setActiveCommentId(null) + activeCommentIdRef.current = null + } + } + catch (error) { + console.error('Failed to delete comment:', error) + } + finally { + setActiveCommentLoading(false) + } + }, [appId, comments, handleCommentIconClick, loadComments, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache]) + + const handleCommentNavigate = useCallback((direction: 'prev' | 'next') => { + const currentId = activeCommentIdRef.current + if (!currentId) return + const idx = comments.findIndex(c => c.id === currentId) + if (idx === -1) return + const target = direction === 'prev' ? comments[idx - 1] : comments[idx + 1] + if (target) + handleCommentIconClick(target) + }, [comments, handleCommentIconClick]) + const handleActiveCommentClose = useCallback(() => { setActiveComment(null) setActiveCommentLoading(false) setActiveCommentId(null) - setControlMode(ControlMode.Pointer) activeCommentIdRef.current = null - }, [setActiveComment, setActiveCommentId, setActiveCommentLoading, setControlMode]) + }, [setActiveComment, setActiveCommentId, setActiveCommentLoading]) const handleCreateComment = useCallback((mousePosition: { pageX: number; pageY: number }) => { if (controlMode === ControlMode.Comment) { @@ -161,6 +232,9 @@ export const useWorkflowComment = () => { handleCommentCancel, handleCommentIconClick, handleActiveCommentClose, + handleCommentResolve, + handleCommentDelete, + handleCommentNavigate, handleCreateComment, loadComments, } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 0f21a6290b..bd04a51412 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -117,6 +117,7 @@ export const Workflow: FC = memo(({ const workflowStore = useWorkflowStore() const reactflow = useReactFlow() const [isMouseOverCanvas, setIsMouseOverCanvas] = useState(false) + const [pendingDeleteCommentId, setPendingDeleteCommentId] = useState(null) const [nodes, setNodes] = useNodesState(originalNodes) const [edges, setEdges] = useEdgesState(originalEdges) const controlMode = useStore(s => s.controlMode) @@ -170,6 +171,9 @@ export const Workflow: FC = memo(({ handleCommentCancel, handleCommentIconClick, handleActiveCommentClose, + handleCommentResolve, + handleCommentDelete, + handleCommentNavigate, } = useWorkflowComment() const mousePosition = useStore(s => s.mousePosition) @@ -332,17 +336,27 @@ export const Workflow: FC = memo(({ - { - !!showConfirm && ( - setShowConfirm(undefined)} - onConfirm={showConfirm.onConfirm} - title={showConfirm.title} - content={showConfirm.desc} - /> - ) - } + {!!showConfirm && ( + setShowConfirm(undefined)} + onConfirm={showConfirm.onConfirm} + title={showConfirm.title} + content={showConfirm.desc} + /> + )} + {pendingDeleteCommentId && ( + setPendingDeleteCommentId(null)} + onConfirm={async () => { + await handleCommentDelete(pendingDeleteCommentId) + setPendingDeleteCommentId(null) + }} + /> + )} {controlMode === ControlMode.Comment && isMouseOverCanvas && ( @@ -354,16 +368,24 @@ export const Workflow: FC = memo(({ onCancel={handleCommentCancel} /> )} - {comments.map((comment) => { + {comments.map((comment, index) => { const isActive = activeComment?.id === comment.id if (isActive && activeComment) { + const canGoPrev = index > 0 + const canGoNext = index < comments.length - 1 return ( handleCommentResolve(comment.id)} + onDelete={() => setPendingDeleteCommentId(comment.id)} + onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined} + onNext={canGoNext ? () => handleCommentNavigate('next') : undefined} + canGoPrev={canGoPrev} + canGoNext={canGoNext} /> ) }