mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
feat: mouse right click can add new comment
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ type MentionInputProps = {
|
||||
onSubmit: (content: string, mentionedUserIds: string[]) => void
|
||||
placeholder?: string
|
||||
autoFocus?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -5,6 +5,7 @@ import { CommentCursor } from './cursor'
|
||||
|
||||
const mockState = {
|
||||
controlMode: ControlMode.Pointer,
|
||||
isCommentPlacing: false,
|
||||
mousePosition: {
|
||||
elementX: 10,
|
||||
elementY: 20,
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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={() => {
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user