feat: mouse right click can add new comment

This commit is contained in:
hjlarry
2026-01-29 09:13:12 +08:00
parent 0495dc5085
commit 26dd6c128c
31 changed files with 160 additions and 15 deletions

View File

@ -7,14 +7,25 @@ const CommentManager = () => {
const { handleCreateComment, handleCommentCancel } = useWorkflowComment()
useEventListener('click', (e) => {
const { controlMode, mousePosition, pendingComment } = workflowStore.getState()
const { controlMode, mousePosition, pendingComment, isCommentPlacing } = workflowStore.getState()
const target = e.target as HTMLElement
const isInDropdown = target.closest('[data-mention-dropdown]')
const isInCommentInput = target.closest('[data-comment-input]')
const isOnCanvasPane = target.closest('.react-flow__pane')
if (isCommentPlacing) {
if (!isInDropdown && !isInCommentInput && isOnCanvasPane) {
e.preventDefault()
e.stopPropagation()
workflowStore.setState({
pendingComment: mousePosition,
isCommentPlacing: false,
})
}
return
}
if (controlMode === 'comment') {
const target = e.target as HTMLElement
const isInDropdown = target.closest('[data-mention-dropdown]')
const isInCommentInput = target.closest('[data-comment-input]')
const isOnCanvasPane = target.closest('.react-flow__pane')
// Only when clicking on the React Flow canvas pane (background),
// and not inside comment input or its dropdown
if (!isInDropdown && !isInCommentInput && isOnCanvasPane) {
@ -28,6 +39,16 @@ const CommentManager = () => {
}
})
useEventListener('contextmenu', () => {
const { isCommentPlacing } = workflowStore.getState()
if (!isCommentPlacing)
return
workflowStore.setState({
isCommentPlacing: false,
isCommentQuickAdd: false,
})
})
return null
}

View File

@ -9,6 +9,7 @@ type MentionInputProps = {
onSubmit: (content: string, mentionedUserIds: string[]) => void
placeholder?: string
autoFocus?: boolean
disabled?: boolean
className?: string
}

View File

@ -10,6 +10,8 @@ type CommentInputProps = {
position: { x: number, y: number }
onSubmit: (content: string, mentionedUserIds: string[]) => void
onCancel: () => void
autoFocus?: boolean
disabled?: boolean
onPositionChange?: (position: {
pageX: number
pageY: number
@ -18,7 +20,14 @@ type CommentInputProps = {
}) => void
}
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel, onPositionChange }) => {
export const CommentInput: FC<CommentInputProps> = memo(({
position,
onSubmit,
onCancel,
autoFocus = true,
disabled = false,
onPositionChange,
}) => {
const [content, setContent] = useState('')
const { t } = useTranslation()
const { userProfile } = useAppContext()
@ -124,7 +133,10 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
return (
<div
className="absolute z-[60] w-96"
className={cn(
'absolute z-[60] w-96',
disabled && 'pointer-events-none opacity-80',
)}
style={{
left: position.x,
top: position.y,
@ -162,7 +174,8 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
onChange={setContent}
onSubmit={handleMentionSubmit}
placeholder={t('comments.placeholder.add', { ns: 'workflow' })}
autoFocus
autoFocus={autoFocus}
disabled={disabled}
className="relative"
/>
</div>

View File

@ -5,6 +5,7 @@ import { CommentCursor } from './cursor'
const mockState = {
controlMode: ControlMode.Pointer,
isCommentPlacing: false,
mousePosition: {
elementX: 10,
elementY: 20,

View File

@ -7,8 +7,9 @@ import { ControlMode } from '../types'
export const CommentCursor: FC = memo(() => {
const controlMode = useStore(s => s.controlMode)
const mousePosition = useStore(s => s.mousePosition)
const isCommentPlacing = useStore(s => s.isCommentPlacing)
if (controlMode !== ControlMode.Comment)
if (controlMode !== ControlMode.Comment || isCommentPlacing)
return null
return (

View File

@ -25,6 +25,9 @@ export const useWorkflowComment = () => {
const controlMode = useStore(s => s.controlMode)
const pendingComment = useStore(s => s.pendingComment)
const setPendingComment = useStore(s => s.setPendingComment)
const isCommentQuickAdd = useStore(s => s.isCommentQuickAdd)
const setCommentQuickAdd = useStore(s => s.setCommentQuickAdd)
const isCommentPlacing = useStore(s => s.isCommentPlacing)
const setActiveCommentId = useStore(s => s.setActiveCommentId)
const activeCommentId = useStore(s => s.activeCommentId)
const comments = useStore(s => s.comments)
@ -204,21 +207,29 @@ export const useWorkflowComment = () => {
collaborationManager.emitCommentsUpdate(appId)
setPendingComment(null)
setCommentQuickAdd(false)
}
catch (error) {
console.error('Failed to create comment:', error)
setPendingComment(null)
setCommentQuickAdd(false)
}
}, [appId, pendingComment, setPendingComment, reactflow, comments, setComments, userProfile, setCommentDetailCache, mentionableUsers])
}, [appId, pendingComment, setPendingComment, setCommentQuickAdd, reactflow, comments, setComments, userProfile, setCommentDetailCache, mentionableUsers])
const handleCommentCancel = useCallback(() => {
setPendingComment(null)
}, [setPendingComment])
setCommentQuickAdd(false)
}, [setPendingComment, setCommentQuickAdd])
useEffect(() => {
if (controlMode !== ControlMode.Comment)
if (controlMode !== ControlMode.Comment && !isCommentQuickAdd)
setPendingComment(null)
}, [controlMode, setPendingComment])
}, [controlMode, isCommentQuickAdd, setPendingComment])
useEffect(() => {
if (!pendingComment && !isCommentPlacing && isCommentQuickAdd)
setCommentQuickAdd(false)
}, [isCommentPlacing, isCommentQuickAdd, pendingComment, setCommentQuickAdd])
const handleCommentIconClick = useCallback(async (comment: WorkflowCommentList) => {
setPendingComment(null)

View File

@ -174,6 +174,37 @@ export type WorkflowProps = {
myUserId?: string | null
onlineUsers?: OnlineUser[]
}
const CommentPlacementPreview = memo(({
onSubmit,
onCancel,
}: {
onSubmit: (content: string, mentionedUserIds: string[]) => void
onCancel: () => void
}) => {
const isCommentPlacing = useStore(s => s.isCommentPlacing)
const pendingComment = useStore(s => s.pendingComment)
const mousePosition = useStore(s => s.mousePosition)
if (!isCommentPlacing || pendingComment)
return null
return (
<CommentInput
position={{
x: mousePosition.elementX,
y: mousePosition.elementY,
}}
onSubmit={onSubmit}
onCancel={onCancel}
autoFocus={false}
disabled
/>
)
})
CommentPlacementPreview.displayName = 'CommentPlacementPreview'
export const Workflow: FC<WorkflowProps> = memo(({
nodes: originalNodes,
edges: originalEdges,
@ -288,8 +319,11 @@ export const Workflow: FC<WorkflowProps> = memo(({
const showUserCursors = useStore(s => s.showUserCursors)
const showResolvedComments = useStore(s => s.showResolvedComments)
const isCommentPreviewHovering = useStore(s => s.isCommentPreviewHovering)
const isCommentPlacing = useStore(s => s.isCommentPlacing)
const setCommentPlacing = useStore(s => s.setCommentPlacing)
const setCommentQuickAdd = useStore(s => s.setCommentQuickAdd)
const setPendingCommentState = useStore(s => s.setPendingComment)
const isCommentInputActive = Boolean(pendingComment)
const isCommentInputActive = Boolean(pendingComment) || isCommentPlacing
const { t } = useTranslation()
const visibleComments = useMemo(() => {
if (showResolvedComments)
@ -345,6 +379,12 @@ export const Workflow: FC<WorkflowProps> = memo(({
setPendingCommentState(position)
}, [setPendingCommentState])
const handleCommentPlacementCancel = useCallback(() => {
setPendingCommentState(null)
setCommentPlacing(false)
setCommentQuickAdd(false)
}, [setCommentPlacing, setCommentQuickAdd, setPendingCommentState])
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
if (document.visibilityState === 'hidden') {
@ -611,6 +651,10 @@ export const Workflow: FC<WorkflowProps> = memo(({
{controlMode === ControlMode.Comment && isMouseOverCanvas && (
<CommentCursor />
)}
<CommentPlacementPreview
onSubmit={handleCommentSubmit}
onCancel={handleCommentPlacementCancel}
/>
{pendingComment && (
<CommentInput
position={{

View File

@ -11,6 +11,7 @@ import {
useDSL,
useNodesInteractions,
usePanelInteractions,
useWorkflowMoveMode,
useWorkflowStartRun,
} from './hooks'
import AddBlock from './operator/add-block'
@ -24,10 +25,14 @@ const PanelContextmenu = () => {
const panelMenu = useStore(s => s.panelMenu)
const clipboardElements = useStore(s => s.clipboardElements)
const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal)
const pendingComment = useStore(s => s.pendingComment)
const setCommentPlacing = useStore(s => s.setCommentPlacing)
const setCommentQuickAdd = useStore(s => s.setCommentQuickAdd)
const { handleNodesPaste } = useNodesInteractions()
const { handlePaneContextmenuCancel, handleNodeContextmenuCancel } = usePanelInteractions()
const { handleStartWorkflowRun } = useWorkflowStartRun()
const { handleAddNote } = useOperator()
const { isCommentModeAvailable } = useWorkflowMoveMode()
const { exportCheck } = useDSL()
useEffect(() => {
@ -79,6 +84,24 @@ const PanelContextmenu = () => {
>
{t('nodes.note.addNote', { ns: 'workflow' })}
</div>
{isCommentModeAvailable && (
<div
className={cn(
'flex h-8 items-center justify-between rounded-lg px-3 text-sm text-text-secondary',
pendingComment ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-state-base-hover',
)}
onClick={(e) => {
e.stopPropagation()
if (pendingComment)
return
setCommentQuickAdd(true)
setCommentPlacing(true)
handlePaneContextmenuCancel()
}}
>
{t('comments.actions.addComment', { ns: 'workflow' })}
</div>
)}
<div
className="flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover"
onClick={() => {

View File

@ -43,6 +43,10 @@ export type WorkflowSliceShape = {
setControlMode: (controlMode: WorkflowSliceShape['controlMode']) => void
pendingComment: MousePosition | null
setPendingComment: (pendingComment: WorkflowSliceShape['pendingComment']) => void
isCommentPlacing: boolean
setCommentPlacing: (isCommentPlacing: boolean) => void
isCommentQuickAdd: boolean
setCommentQuickAdd: (isCommentQuickAdd: boolean) => void
isCommentPreviewHovering: boolean
setCommentPreviewHovering: (hovering: boolean) => void
mousePosition: { pageX: number, pageY: number, elementX: number, elementY: number }
@ -89,6 +93,10 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
},
pendingComment: null,
setPendingComment: pendingComment => set(() => ({ pendingComment })),
isCommentPlacing: false,
setCommentPlacing: isCommentPlacing => set(() => ({ isCommentPlacing })),
isCommentQuickAdd: false,
setCommentQuickAdd: isCommentQuickAdd => set(() => ({ isCommentQuickAdd })),
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
setMousePosition: mousePosition => set(() => ({ mousePosition })),
isCommentPreviewHovering: false,