From 8497d296b1dd2986d8ab7d8da4d1f5b3b7a9a017 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 18 Nov 2025 09:53:15 +0800 Subject: [PATCH] feat: can drag avatar to move the comment input --- .../workflow/comment/comment-input.tsx | 96 ++++++++++++++++++- web/app/components/workflow/index.tsx | 7 ++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/comment/comment-input.tsx b/web/app/components/workflow/comment/comment-input.tsx index f8c96a0595..8bb8b1af03 100644 --- a/web/app/components/workflow/comment/comment-input.tsx +++ b/web/app/components/workflow/comment/comment-input.tsx @@ -1,5 +1,5 @@ -import type { FC } from 'react' -import { memo, useCallback, useEffect, useState } from 'react' +import type { FC, PointerEvent as ReactPointerEvent } from 'react' +import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Avatar from '@/app/components/base/avatar' import { useAppContext } from '@/context/app-context' @@ -10,12 +10,36 @@ type CommentInputProps = { position: { x: number; y: number } onSubmit: (content: string, mentionedUserIds: string[]) => void onCancel: () => void + onPositionChange?: (position: { + pageX: number + pageY: number + elementX: number + elementY: number + }) => void } -export const CommentInput: FC = memo(({ position, onSubmit, onCancel }) => { +export const CommentInput: FC = memo(({ position, onSubmit, onCancel, onPositionChange }) => { const [content, setContent] = useState('') const { t } = useTranslation() const { userProfile } = useAppContext() + const dragStateRef = useRef<{ + pointerId: number | null + startPointerX: number + startPointerY: number + startX: number + startY: number + active: boolean + } & { + endHandler?: (event: PointerEvent) => void + }>({ + pointerId: null, + startPointerX: 0, + startPointerY: 0, + startX: 0, + startY: 0, + active: false, + endHandler: undefined, + }) useEffect(() => { const handleGlobalKeyDown = (e: KeyboardEvent) => { @@ -37,6 +61,67 @@ export const CommentInput: FC = memo(({ position, onSubmit, o setContent('') }, [onSubmit]) + const handleDragPointerMove = useCallback((event: PointerEvent) => { + const state = dragStateRef.current + if (!state.active || (state.pointerId !== null && event.pointerId !== state.pointerId)) + return + if (!onPositionChange) + return + event.preventDefault() + const deltaX = event.clientX - state.startPointerX + const deltaY = event.clientY - state.startPointerY + onPositionChange({ + pageX: event.clientX, + pageY: event.clientY, + elementX: state.startX + deltaX, + elementY: state.startY + deltaY, + }) + }, [onPositionChange]) + + const stopDragging = useCallback((event?: PointerEvent) => { + const state = dragStateRef.current + if (!state.active) + return + if (event && state.pointerId !== null && event.pointerId !== state.pointerId) + return + state.active = false + state.pointerId = null + window.removeEventListener('pointermove', handleDragPointerMove) + if (state.endHandler) { + window.removeEventListener('pointerup', state.endHandler) + window.removeEventListener('pointercancel', state.endHandler) + state.endHandler = undefined + } + }, [handleDragPointerMove]) + + const handleDragPointerDown = useCallback((event: ReactPointerEvent) => { + if (event.button !== 0) + return + event.stopPropagation() + event.preventDefault() + if (!onPositionChange) + return + const endHandler = (pointerEvent: PointerEvent) => { + stopDragging(pointerEvent) + } + dragStateRef.current = { + pointerId: event.pointerId, + startPointerX: event.clientX, + startPointerY: event.clientY, + startX: position.x, + startY: position.y, + active: true, + endHandler, + } + window.addEventListener('pointermove', handleDragPointerMove, { passive: false }) + window.addEventListener('pointerup', endHandler) + window.addEventListener('pointercancel', endHandler) + }, [handleDragPointerMove, onPositionChange, position.x, position.y, stopDragging]) + + useEffect(() => () => { + stopDragging() + }, [stopDragging]) + return (
= memo(({ position, onSubmit, o data-comment-input >
-
+
diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index ed1667440a..01b8edd640 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -81,6 +81,7 @@ import { useStore, useWorkflowStore, } from './store' +import type { WorkflowSliceShape } from './store/workflow/workflow-slice' import { CUSTOM_EDGE, CUSTOM_NODE, @@ -210,6 +211,7 @@ export const Workflow: FC = memo(({ const showUserComments = useStore(s => s.showUserComments) const showUserCursors = useStore(s => s.showUserCursors) const isCommentPreviewHovering = useStore(s => s.isCommentPreviewHovering) + const setPendingCommentState = useStore(s => s.setPendingComment) const isCommentInputActive = Boolean(pendingComment) const { t } = useTranslation() @@ -244,6 +246,10 @@ export const Workflow: FC = memo(({ } }, []) + const handlePendingCommentPositionChange = useCallback((position: NonNullable) => { + setPendingCommentState(position) + }, [setPendingCommentState]) + const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() const handleSyncWorkflowDraftWhenPageClose = useCallback(() => { if (document.visibilityState === 'hidden') @@ -483,6 +489,7 @@ export const Workflow: FC = memo(({ }} onSubmit={handleCommentSubmit} onCancel={handleCommentCancel} + onPositionChange={handlePendingCommentPositionChange} /> )} {comments.map((comment, index) => {