mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 04:26:30 +08:00
can update comment position
This commit is contained in:
parent
659cbc05a9
commit
33d4c95470
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
|
||||||
import { memo, useMemo, useState } from 'react'
|
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useReactFlow, useViewport } from 'reactflow'
|
import { useReactFlow, useViewport } from 'reactflow'
|
||||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||||
import CommentPreview from './comment-preview'
|
import CommentPreview from './comment-preview'
|
||||||
@ -11,17 +11,22 @@ type CommentIconProps = {
|
|||||||
comment: WorkflowCommentList
|
comment: WorkflowCommentList
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
onPositionUpdate?: (position: { x: number; y: number }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false }) => {
|
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false, onPositionUpdate }) => {
|
||||||
const { flowToScreenPosition } = useReactFlow()
|
const { flowToScreenPosition, screenToFlowPosition } = useReactFlow()
|
||||||
const viewport = useViewport()
|
const viewport = useViewport()
|
||||||
const [showPreview, setShowPreview] = useState(false)
|
const [showPreview, setShowPreview] = useState(false)
|
||||||
|
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null)
|
||||||
const handlePreviewClick = () => {
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
setShowPreview(false)
|
const dragStateRef = useRef<{
|
||||||
onClick()
|
offsetX: number
|
||||||
}
|
offsetY: number
|
||||||
|
startX: number
|
||||||
|
startY: number
|
||||||
|
hasMoved: boolean
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const screenPosition = useMemo(() => {
|
const screenPosition = useMemo(() => {
|
||||||
return flowToScreenPosition({
|
return flowToScreenPosition({
|
||||||
@ -30,6 +35,108 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isAct
|
|||||||
})
|
})
|
||||||
}, [comment.position_x, comment.position_y, viewport.x, viewport.y, viewport.zoom, flowToScreenPosition])
|
}, [comment.position_x, comment.position_y, viewport.x, viewport.y, viewport.zoom, flowToScreenPosition])
|
||||||
|
|
||||||
|
const effectivePosition = dragPosition ?? screenPosition
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
if (event.button !== 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
dragStateRef.current = {
|
||||||
|
offsetX: event.clientX - screenPosition.x,
|
||||||
|
offsetY: event.clientY - screenPosition.y,
|
||||||
|
startX: event.clientX,
|
||||||
|
startY: event.clientY,
|
||||||
|
hasMoved: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragPosition(screenPosition)
|
||||||
|
setIsDragging(false)
|
||||||
|
|
||||||
|
if (event.currentTarget.dataset.role !== 'comment-preview')
|
||||||
|
setShowPreview(false)
|
||||||
|
|
||||||
|
if (event.currentTarget.setPointerCapture)
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
|
}, [screenPosition])
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = dragStateRef.current
|
||||||
|
if (!dragState)
|
||||||
|
return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const nextX = event.clientX - dragState.offsetX
|
||||||
|
const nextY = event.clientY - dragState.offsetY
|
||||||
|
|
||||||
|
if (!dragState.hasMoved) {
|
||||||
|
const distance = Math.hypot(event.clientX - dragState.startX, event.clientY - dragState.startY)
|
||||||
|
if (distance > 4) {
|
||||||
|
dragState.hasMoved = true
|
||||||
|
setIsDragging(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragPosition({ x: nextX, y: nextY })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const finishDrag = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = dragStateRef.current
|
||||||
|
if (!dragState)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (event.currentTarget.hasPointerCapture?.(event.pointerId))
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||||
|
|
||||||
|
dragStateRef.current = null
|
||||||
|
setDragPosition(null)
|
||||||
|
setIsDragging(false)
|
||||||
|
return dragState.hasMoved
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePointerUp = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const finalScreenPosition = dragPosition ?? screenPosition
|
||||||
|
const didDrag = finishDrag(event)
|
||||||
|
|
||||||
|
setShowPreview(false)
|
||||||
|
|
||||||
|
if (didDrag) {
|
||||||
|
if (onPositionUpdate) {
|
||||||
|
const flowPosition = screenToFlowPosition({
|
||||||
|
x: finalScreenPosition.x,
|
||||||
|
y: finalScreenPosition.y,
|
||||||
|
})
|
||||||
|
onPositionUpdate(flowPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!isActive) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}, [dragPosition, finishDrag, isActive, onClick, onPositionUpdate, screenPosition, screenToFlowPosition])
|
||||||
|
|
||||||
|
const handlePointerCancel = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
finishDrag(event)
|
||||||
|
}, [finishDrag])
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
if (isActive || isDragging)
|
||||||
|
return
|
||||||
|
setShowPreview(true)
|
||||||
|
}, [isActive, isDragging])
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setShowPreview(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Calculate dynamic width based on number of participants
|
// Calculate dynamic width based on number of participants
|
||||||
const participantCount = comment.participants?.length || 0
|
const participantCount = comment.participants?.length || 0
|
||||||
const maxVisible = Math.min(3, participantCount)
|
const maxVisible = Math.min(3, participantCount)
|
||||||
@ -42,21 +149,29 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isAct
|
|||||||
8 + avatarSize + Math.max(0, (showCount ? 2 : maxVisible - 1)) * (avatarSize - avatarSpacing) + 8,
|
8 + avatarSize + Math.max(0, (showCount ? 2 : maxVisible - 1)) * (avatarSize - avatarSpacing) + 8,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pointerEventHandlers = useMemo(() => ({
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onPointerMove: handlePointerMove,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
onPointerCancel: handlePointerCancel,
|
||||||
|
}), [handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute z-10"
|
className="absolute z-10"
|
||||||
style={{
|
style={{
|
||||||
left: screenPosition.x,
|
left: effectivePosition.x,
|
||||||
top: screenPosition.y,
|
top: effectivePosition.y,
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
|
data-role='comment-marker'
|
||||||
|
{...pointerEventHandlers}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={isActive ? '' : 'cursor-pointer'}
|
className={isActive ? (isDragging ? 'cursor-grabbing' : '') : isDragging ? 'cursor-grabbing' : 'cursor-pointer'}
|
||||||
onClick={isActive ? undefined : onClick}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseEnter={isActive ? undefined : () => setShowPreview(true)}
|
onMouseLeave={handleMouseLeave}
|
||||||
onMouseLeave={isActive ? undefined : () => setShowPreview(false)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
|
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
|
||||||
@ -84,14 +199,19 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isAct
|
|||||||
<div
|
<div
|
||||||
className="absolute z-20"
|
className="absolute z-20"
|
||||||
style={{
|
style={{
|
||||||
left: screenPosition.x - dynamicWidth / 2,
|
left: (dragPosition ?? screenPosition).x - dynamicWidth / 2,
|
||||||
top: screenPosition.y + 20,
|
top: (dragPosition ?? screenPosition).y + 20,
|
||||||
transform: 'translateY(-100%)',
|
transform: 'translateY(-100%)',
|
||||||
}}
|
}}
|
||||||
|
data-role='comment-preview'
|
||||||
|
{...pointerEventHandlers}
|
||||||
onMouseEnter={() => setShowPreview(true)}
|
onMouseEnter={() => setShowPreview(true)}
|
||||||
onMouseLeave={() => setShowPreview(false)}
|
onMouseLeave={() => setShowPreview(false)}
|
||||||
>
|
>
|
||||||
<CommentPreview comment={comment} onClick={handlePreviewClick} />
|
<CommentPreview comment={comment} onClick={() => {
|
||||||
|
setShowPreview(false)
|
||||||
|
onClick()
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -103,6 +223,7 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isAct
|
|||||||
&& prevProps.comment.position_y === nextProps.comment.position_y
|
&& prevProps.comment.position_y === nextProps.comment.position_y
|
||||||
&& prevProps.onClick === nextProps.onClick
|
&& prevProps.onClick === nextProps.onClick
|
||||||
&& prevProps.isActive === nextProps.isActive
|
&& prevProps.isActive === nextProps.isActive
|
||||||
|
&& prevProps.onPositionUpdate === nextProps.onPositionUpdate
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useReactFlow } from 'reactflow'
|
|||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import { ControlMode } from '../types'
|
import { ControlMode } from '../types'
|
||||||
import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
|
import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
|
||||||
import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment'
|
import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment'
|
||||||
import { collaborationManager } from '@/app/components/workflow/collaboration'
|
import { collaborationManager } from '@/app/components/workflow/collaboration'
|
||||||
|
|
||||||
export const useWorkflowComment = () => {
|
export const useWorkflowComment = () => {
|
||||||
@ -229,6 +229,69 @@ export const useWorkflowComment = () => {
|
|||||||
}
|
}
|
||||||
}, [appId, comments, handleCommentIconClick, loadComments, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache])
|
}, [appId, comments, handleCommentIconClick, loadComments, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache])
|
||||||
|
|
||||||
|
const handleCommentPositionUpdate = useCallback(async (commentId: string, position: { x: number; y: number }) => {
|
||||||
|
if (!appId) return
|
||||||
|
|
||||||
|
const targetComment = comments.find(c => c.id === commentId)
|
||||||
|
if (!targetComment) return
|
||||||
|
|
||||||
|
const nextPosition = {
|
||||||
|
position_x: position.x,
|
||||||
|
position_y: position.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousComments = comments
|
||||||
|
const updatedComments = comments.map(c =>
|
||||||
|
c.id === commentId
|
||||||
|
? { ...c, ...nextPosition }
|
||||||
|
: c,
|
||||||
|
)
|
||||||
|
setComments(updatedComments)
|
||||||
|
|
||||||
|
const cachedDetail = commentDetailCacheRef.current[commentId]
|
||||||
|
const updatedDetail = cachedDetail ? { ...cachedDetail, ...nextPosition } : null
|
||||||
|
if (updatedDetail) {
|
||||||
|
commentDetailCacheRef.current = {
|
||||||
|
...commentDetailCacheRef.current,
|
||||||
|
[commentId]: updatedDetail,
|
||||||
|
}
|
||||||
|
setCommentDetailCache(commentDetailCacheRef.current)
|
||||||
|
|
||||||
|
if (activeCommentIdRef.current === commentId)
|
||||||
|
setActiveComment(updatedDetail)
|
||||||
|
}
|
||||||
|
else if (activeComment?.id === commentId) {
|
||||||
|
setActiveComment({ ...activeComment, ...nextPosition })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateWorkflowComment(appId, commentId, {
|
||||||
|
content: targetComment.content,
|
||||||
|
position_x: nextPosition.position_x,
|
||||||
|
position_y: nextPosition.position_y,
|
||||||
|
})
|
||||||
|
collaborationManager.emitCommentsUpdate(appId)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to update comment position:', error)
|
||||||
|
setComments(previousComments)
|
||||||
|
|
||||||
|
if (cachedDetail) {
|
||||||
|
commentDetailCacheRef.current = {
|
||||||
|
...commentDetailCacheRef.current,
|
||||||
|
[commentId]: cachedDetail,
|
||||||
|
}
|
||||||
|
setCommentDetailCache(commentDetailCacheRef.current)
|
||||||
|
|
||||||
|
if (activeCommentIdRef.current === commentId)
|
||||||
|
setActiveComment(cachedDetail)
|
||||||
|
}
|
||||||
|
else if (activeComment?.id === commentId) {
|
||||||
|
setActiveComment(activeComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeComment, appId, comments, setComments, setCommentDetailCache, setActiveComment])
|
||||||
|
|
||||||
const handleCommentReply = useCallback(async (commentId: string, content: string, mentionedUserIds: string[] = []) => {
|
const handleCommentReply = useCallback(async (commentId: string, content: string, mentionedUserIds: string[] = []) => {
|
||||||
if (!appId) return
|
if (!appId) return
|
||||||
const trimmed = content.trim()
|
const trimmed = content.trim()
|
||||||
@ -336,6 +399,7 @@ export const useWorkflowComment = () => {
|
|||||||
handleCommentReply,
|
handleCommentReply,
|
||||||
handleCommentReplyUpdate,
|
handleCommentReplyUpdate,
|
||||||
handleCommentReplyDelete,
|
handleCommentReplyDelete,
|
||||||
|
handleCommentPositionUpdate,
|
||||||
refreshActiveComment,
|
refreshActiveComment,
|
||||||
handleCreateComment,
|
handleCreateComment,
|
||||||
loadComments,
|
loadComments,
|
||||||
|
|||||||
@ -198,6 +198,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
handleCommentReply,
|
handleCommentReply,
|
||||||
handleCommentReplyUpdate,
|
handleCommentReplyUpdate,
|
||||||
handleCommentReplyDelete,
|
handleCommentReplyDelete,
|
||||||
|
handleCommentPositionUpdate,
|
||||||
} = useWorkflowComment()
|
} = useWorkflowComment()
|
||||||
const showUserComments = useStore(s => s.showUserComments)
|
const showUserComments = useStore(s => s.showUserComments)
|
||||||
const showUserCursors = useStore(s => s.showUserCursors)
|
const showUserCursors = useStore(s => s.showUserCursors)
|
||||||
@ -461,6 +462,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
comment={comment}
|
comment={comment}
|
||||||
onClick={() => handleCommentIconClick(comment)}
|
onClick={() => handleCommentIconClick(comment)}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
|
onPositionUpdate={position => handleCommentPositionUpdate(comment.id, position)}
|
||||||
/>
|
/>
|
||||||
<CommentThread
|
<CommentThread
|
||||||
key={`${comment.id}-thread`}
|
key={`${comment.id}-thread`}
|
||||||
@ -486,6 +488,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
key={comment.id}
|
key={comment.id}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
onClick={() => handleCommentIconClick(comment)}
|
onClick={() => handleCommentIconClick(comment)}
|
||||||
|
onPositionUpdate={position => handleCommentPositionUpdate(comment.id, position)}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user