mirror of https://github.com/langgenius/dify.git
add top operation buttons of comment detail
This commit is contained in:
parent
3eac26929a
commit
e776accaf3
|
|
@ -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<CommentThreadProps> = memo(({ comment, loading = false, onClose }) => {
|
||||
export const CommentThread: FC<CommentThreadProps> = 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<CommentThreadProps> = memo(({ comment, loading =
|
|||
<div className='relative rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>
|
||||
<div className='flex items-center justify-between rounded-t-2xl px-4 py-3'>
|
||||
<div className='system-2xs-semibold uppercase tracking-[0.08em] text-text-tertiary'>Comment</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={onClose}
|
||||
aria-label='Close comment'
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</button>
|
||||
<div className='flex items-center gap-1'>
|
||||
<button
|
||||
type='button'
|
||||
disabled={loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onDelete}
|
||||
aria-label='Delete comment'
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
disabled={comment.resolved || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onResolve}
|
||||
aria-label='Resolve comment'
|
||||
>
|
||||
{comment.resolved ? <RiCheckboxCircleFill className='h-4 w-4' /> : <RiCheckboxCircleLine className='h-4 w-4' />}
|
||||
</button>
|
||||
<div className='bg-components-panel-border/80 mx-1 h-4 w-px' />
|
||||
<button
|
||||
type='button'
|
||||
disabled={!canGoPrev || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onPrev}
|
||||
aria-label='Previous comment'
|
||||
>
|
||||
<RiArrowUpSLine className='h-4 w-4' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
disabled={!canGoNext || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onNext}
|
||||
aria-label='Next comment'
|
||||
>
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-full text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={onClose}
|
||||
aria-label='Close comment'
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative px-4 pb-4'>
|
||||
<ThreadMessage
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useReactFlow } from 'reactflow'
|
|||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
|
||||
import { createWorkflowComment, fetchWorkflowComment, fetchWorkflowComments } from '@/service/workflow-comment'
|
||||
import { createWorkflowComment, deleteWorkflowComment, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment } from '@/service/workflow-comment'
|
||||
|
||||
export const useWorkflowComment = () => {
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const [isMouseOverCanvas, setIsMouseOverCanvas] = useState(false)
|
||||
const [pendingDeleteCommentId, setPendingDeleteCommentId] = useState<string | null>(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<WorkflowProps> = memo(({
|
|||
handleCommentCancel,
|
||||
handleCommentIconClick,
|
||||
handleActiveCommentClose,
|
||||
handleCommentResolve,
|
||||
handleCommentDelete,
|
||||
handleCommentNavigate,
|
||||
} = useWorkflowComment()
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
|
||||
|
|
@ -332,17 +336,27 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
<PanelContextmenu />
|
||||
<NodeContextmenu />
|
||||
<HelpLine />
|
||||
{
|
||||
!!showConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
onCancel={() => setShowConfirm(undefined)}
|
||||
onConfirm={showConfirm.onConfirm}
|
||||
title={showConfirm.title}
|
||||
content={showConfirm.desc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
onCancel={() => setShowConfirm(undefined)}
|
||||
onConfirm={showConfirm.onConfirm}
|
||||
title={showConfirm.title}
|
||||
content={showConfirm.desc}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteCommentId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Delete this thread?'
|
||||
content='This action will permanently delete the thread and all its replies. This cannot be undone.'
|
||||
onCancel={() => setPendingDeleteCommentId(null)}
|
||||
onConfirm={async () => {
|
||||
await handleCommentDelete(pendingDeleteCommentId)
|
||||
setPendingDeleteCommentId(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LimitTips />
|
||||
{controlMode === ControlMode.Comment && isMouseOverCanvas && (
|
||||
<CommentCursor mousePosition={mousePosition} />
|
||||
|
|
@ -354,16 +368,24 @@ export const Workflow: FC<WorkflowProps> = 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 (
|
||||
<CommentThread
|
||||
key={comment.id}
|
||||
comment={activeComment}
|
||||
loading={activeCommentLoading}
|
||||
onClose={handleActiveCommentClose}
|
||||
onResolve={() => handleCommentResolve(comment.id)}
|
||||
onDelete={() => setPendingDeleteCommentId(comment.id)}
|
||||
onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined}
|
||||
onNext={canGoNext ? () => handleCommentNavigate('next') : undefined}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue