mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 06:39:06 +08:00
feat: Variable click jumps to source node (#13623)
This commit is contained in:
parent
6a9e0b1005
commit
276c02f341
@ -74,9 +74,11 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleSelect = useCallback((e: MouseEvent) => {
|
const handleSelect = useCallback((e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
if (!e.metaKey && !e.ctrlKey) {
|
||||||
clearSelection()
|
e.stopPropagation()
|
||||||
setSelected(true)
|
clearSelection()
|
||||||
|
setSelected(true)
|
||||||
|
}
|
||||||
}, [setSelected, clearSelection])
|
}, [setSelected, clearSelection])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
RiErrorWarningFill,
|
RiErrorWarningFill,
|
||||||
RiMoreLine,
|
RiMoreLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||||
import { useSelectOrDelete } from '../../hooks'
|
import { useSelectOrDelete } from '../../hooks'
|
||||||
import type { WorkflowNodesMap } from './node'
|
import type { WorkflowNodesMap } from './node'
|
||||||
import { WorkflowVariableBlockNode } from './node'
|
import { WorkflowVariableBlockNode } from './node'
|
||||||
@ -66,6 +68,9 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
const isChatVar = isConversationVar(variables)
|
const isChatVar = isConversationVar(variables)
|
||||||
const isException = isExceptionVariable(varName, node?.type)
|
const isException = isExceptionVariable(varName, node?.type)
|
||||||
|
|
||||||
|
const reactflow = useReactFlow()
|
||||||
|
const store = useStoreApi()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||||
@ -83,6 +88,26 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
)
|
)
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
|
const handleVariableJump = useCallback(() => {
|
||||||
|
const workflowContainer = document.getElementById('workflow-container')
|
||||||
|
const {
|
||||||
|
clientWidth,
|
||||||
|
clientHeight,
|
||||||
|
} = workflowContainer!
|
||||||
|
|
||||||
|
const {
|
||||||
|
setViewport,
|
||||||
|
} = reactflow
|
||||||
|
const { transform } = store.getState()
|
||||||
|
const zoom = transform[2]
|
||||||
|
const position = node.position
|
||||||
|
setViewport({
|
||||||
|
x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom,
|
||||||
|
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom,
|
||||||
|
zoom: transform[2],
|
||||||
|
})
|
||||||
|
}, [node, reactflow, store])
|
||||||
|
|
||||||
const Item = (
|
const Item = (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -90,6 +115,10 @@ const WorkflowVariableBlockComponent = ({
|
|||||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||||
!node && !isEnv && !isChatVar && '!border-state-destructive-solid !bg-state-destructive-hover',
|
!node && !isEnv && !isChatVar && '!border-state-destructive-solid !bg-state-destructive-hover',
|
||||||
)}
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleVariableJump()
|
||||||
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{!isEnv && !isChatVar && (
|
{!isEnv && !isChatVar && (
|
||||||
|
@ -64,7 +64,7 @@ export type GetVarType = (payload: {
|
|||||||
export type WorkflowVariableBlockType = {
|
export type WorkflowVariableBlockType = {
|
||||||
show?: boolean
|
show?: boolean
|
||||||
variables?: NodeOutPutVar[]
|
variables?: NodeOutPutVar[]
|
||||||
workflowNodesMap?: Record<string, Pick<Node['data'], 'title' | 'type'>>
|
workflowNodesMap?: Record<string, Pick<Node['data'], 'title' | 'type' | 'height' | 'width' | 'position'>>
|
||||||
onInsert?: () => void
|
onInsert?: () => void
|
||||||
onDelete?: () => void
|
onDelete?: () => void
|
||||||
getVarType?: GetVarType
|
getVarType?: GetVarType
|
||||||
|
@ -91,6 +91,9 @@ const Editor: FC<Props> = ({
|
|||||||
acc[node.id] = {
|
acc[node.id] = {
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
type: node.data.type,
|
type: node.data.type,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
position: node.position,
|
||||||
}
|
}
|
||||||
if (node.data.type === BlockEnum.Start) {
|
if (node.data.type === BlockEnum.Start) {
|
||||||
acc.sys = {
|
acc.sys = {
|
||||||
|
@ -259,6 +259,9 @@ const Editor: FC<Props> = ({
|
|||||||
acc[node.id] = {
|
acc[node.id] = {
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
type: node.data.type,
|
type: node.data.type,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
position: node.position,
|
||||||
}
|
}
|
||||||
if (node.data.type === BlockEnum.Start) {
|
if (node.data.type === BlockEnum.Start) {
|
||||||
acc.sys = {
|
acc.sys = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useNodes } from 'reactflow'
|
import { useNodes, useReactFlow, useStoreApi } from 'reactflow'
|
||||||
import { capitalize } from 'lodash-es'
|
import { capitalize } from 'lodash-es'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiErrorWarningFill } from '@remixicon/react'
|
import { RiErrorWarningFill } from '@remixicon/react'
|
||||||
@ -48,12 +48,42 @@ const VariableTag = ({
|
|||||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||||
const isException = isExceptionVariable(variableName, node?.data.type)
|
const isException = isExceptionVariable(variableName, node?.data.type)
|
||||||
|
|
||||||
|
const reactflow = useReactFlow()
|
||||||
|
const store = useStoreApi()
|
||||||
|
|
||||||
|
const handleVariableJump = useCallback(() => {
|
||||||
|
const workflowContainer = document.getElementById('workflow-container')
|
||||||
|
const {
|
||||||
|
clientWidth,
|
||||||
|
clientHeight,
|
||||||
|
} = workflowContainer!
|
||||||
|
|
||||||
|
const {
|
||||||
|
setViewport,
|
||||||
|
} = reactflow
|
||||||
|
const { transform } = store.getState()
|
||||||
|
const zoom = transform[2]
|
||||||
|
const position = node.position
|
||||||
|
setViewport({
|
||||||
|
x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom,
|
||||||
|
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom,
|
||||||
|
zoom: transform[2],
|
||||||
|
})
|
||||||
|
}, [node, reactflow, store])
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Tooltip popupContent={!isValid && t('workflow.errorMsg.invalidVariable')}>
|
<Tooltip popupContent={!isValid && t('workflow.errorMsg.invalidVariable')}>
|
||||||
<div className={cn('border-[rgba(16, 2440,0.08)] inline-flex h-6 max-w-full items-center rounded-md border-[0.5px] border-divider-subtle bg-components-badge-white-to-dark px-1.5 text-xs shadow-xs',
|
<div className={cn('border-[rgba(16, 2440,0.08)] inline-flex h-6 max-w-full items-center rounded-md border-[0.5px] border-divider-subtle bg-components-badge-white-to-dark px-1.5 text-xs shadow-xs',
|
||||||
!isValid && 'border-red-400 !bg-[#FEF3F2]',
|
!isValid && 'border-red-400 !bg-[#FEF3F2]',
|
||||||
)}>
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleVariableJump()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{(!isEnv && !isChatVar && <>
|
{(!isEnv && !isChatVar && <>
|
||||||
{node && (
|
{node && (
|
||||||
<>
|
<>
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
RiMoreLine,
|
RiMoreLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { useStoreApi } from 'reactflow'
|
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||||
import RemoveButton from '../remove-button'
|
import RemoveButton from '../remove-button'
|
||||||
import useAvailableVarList from '../../hooks/use-available-var-list'
|
import useAvailableVarList from '../../hooks/use-available-var-list'
|
||||||
import VarReferencePopup from './var-reference-popup'
|
import VarReferencePopup from './var-reference-popup'
|
||||||
@ -111,6 +111,9 @@ const VarReferencePicker: FC<Props> = ({
|
|||||||
passedInAvailableNodes,
|
passedInAvailableNodes,
|
||||||
filterVar,
|
filterVar,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const reactflow = useReactFlow()
|
||||||
|
|
||||||
const startNode = availableNodes.find((node: any) => {
|
const startNode = availableNodes.find((node: any) => {
|
||||||
return node.data.type === BlockEnum.Start
|
return node.data.type === BlockEnum.Start
|
||||||
})
|
})
|
||||||
@ -172,7 +175,11 @@ const VarReferencePicker: FC<Props> = ({
|
|||||||
if (isSystemVar(value as ValueSelector))
|
if (isSystemVar(value as ValueSelector))
|
||||||
return startNode?.data
|
return startNode?.data
|
||||||
|
|
||||||
return getNodeInfoById(availableNodes, outputVarNodeId)?.data
|
const node = getNodeInfoById(availableNodes, outputVarNodeId)?.data
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
id: outputVarNodeId,
|
||||||
|
}
|
||||||
}, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode])
|
}, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode])
|
||||||
|
|
||||||
const isShowAPart = (value as ValueSelector).length > 2
|
const isShowAPart = (value as ValueSelector).length > 2
|
||||||
@ -237,6 +244,28 @@ const VarReferencePicker: FC<Props> = ({
|
|||||||
onChange([], varKindType)
|
onChange([], varKindType)
|
||||||
}, [onChange, varKindType])
|
}, [onChange, varKindType])
|
||||||
|
|
||||||
|
const handleVariableJump = useCallback((nodeId: string) => {
|
||||||
|
const currentNodeIndex = availableNodes.findIndex(node => node.id === nodeId)
|
||||||
|
const currentNode = availableNodes[currentNodeIndex]
|
||||||
|
|
||||||
|
const workflowContainer = document.getElementById('workflow-container')
|
||||||
|
const {
|
||||||
|
clientWidth,
|
||||||
|
clientHeight,
|
||||||
|
} = workflowContainer!
|
||||||
|
const {
|
||||||
|
setViewport,
|
||||||
|
} = reactflow
|
||||||
|
const { transform } = store.getState()
|
||||||
|
const zoom = transform[2]
|
||||||
|
const position = currentNode.position
|
||||||
|
setViewport({
|
||||||
|
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
|
||||||
|
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
|
||||||
|
zoom: transform[2],
|
||||||
|
})
|
||||||
|
}, [availableNodes, reactflow, store])
|
||||||
|
|
||||||
const type = getCurrentVariableType({
|
const type = getCurrentVariableType({
|
||||||
parentNode: isInIteration ? iterationNode : loopNode,
|
parentNode: isInIteration ? iterationNode : loopNode,
|
||||||
valueSelector: value as ValueSelector,
|
valueSelector: value as ValueSelector,
|
||||||
@ -357,7 +386,12 @@ const VarReferencePicker: FC<Props> = ({
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
{isShowNodeName && !isEnv && !isChatVar && (
|
{isShowNodeName && !isEnv && !isChatVar && (
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center' onClick={(e) => {
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleVariableJump(outputVarNode?.id)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<div className='h-3 px-[1px]'>
|
<div className='h-3 px-[1px]'>
|
||||||
{outputVarNode?.type && <VarBlockIcon
|
{outputVarNode?.type && <VarBlockIcon
|
||||||
className='!text-text-primary'
|
className='!text-text-primary'
|
||||||
|
@ -37,6 +37,9 @@ const ConditionInput = ({
|
|||||||
acc[node.id] = {
|
acc[node.id] = {
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
type: node.data.type,
|
type: node.data.type,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
position: node.position,
|
||||||
}
|
}
|
||||||
if (node.data.type === BlockEnum.Start) {
|
if (node.data.type === BlockEnum.Start) {
|
||||||
acc.sys = {
|
acc.sys = {
|
||||||
|
@ -2,6 +2,7 @@ import type {
|
|||||||
Edge as ReactFlowEdge,
|
Edge as ReactFlowEdge,
|
||||||
Node as ReactFlowNode,
|
Node as ReactFlowNode,
|
||||||
Viewport,
|
Viewport,
|
||||||
|
XYPosition,
|
||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import type { Resolution, TransferMethod } from '@/types/app'
|
import type { Resolution, TransferMethod } from '@/types/app'
|
||||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||||
@ -83,6 +84,7 @@ export type CommonNodeType<T = {}> = {
|
|||||||
type: BlockEnum
|
type: BlockEnum
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
|
position?: XYPosition
|
||||||
_loopLength?: number
|
_loopLength?: number
|
||||||
_loopIndex?: number
|
_loopIndex?: number
|
||||||
isInLoop?: boolean
|
isInLoop?: boolean
|
||||||
|
Loading…
x
Reference in New Issue
Block a user