mirror of
https://github.com/langgenius/dify.git
synced 2026-06-16 14:01:10 +08:00
fix(web): snippet add
This commit is contained in:
parent
77afc805e1
commit
c55105bff3
@ -10,6 +10,7 @@ import type {
|
||||
import type {
|
||||
CommonNodeType,
|
||||
NodeDefault,
|
||||
OnNodeAdd,
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
@ -65,6 +66,7 @@ export type NodeSelectorProps = {
|
||||
ignoreNodeIds?: string[]
|
||||
forceEnableStartTab?: boolean // Force enabling Start tab regardless of existing trigger/user input nodes (e.g., when changing Start node type).
|
||||
allowUserInputSelection?: boolean // Override user-input availability; default logic blocks it when triggers exist.
|
||||
snippetInsertPayload?: Parameters<OnNodeAdd>[1]
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
@ -90,6 +92,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
ignoreNodeIds = [],
|
||||
forceEnableStartTab = false,
|
||||
allowUserInputSelection,
|
||||
snippetInsertPayload,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
@ -335,7 +338,14 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
noTools={noTools}
|
||||
onTagsChange={setTags}
|
||||
forceShowStartContent={forceShowStartContent}
|
||||
snippetsElem={<Snippets loading={snippetsLoading} searchText={searchText} />}
|
||||
snippetsElem={(
|
||||
<Snippets
|
||||
loading={snippetsLoading}
|
||||
searchText={searchText}
|
||||
insertPayload={snippetInsertPayload}
|
||||
onInserted={() => handleOpenChange(false)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@ -1,6 +1,27 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useInsertSnippet } from '../use-insert-snippet'
|
||||
|
||||
type TestNode = {
|
||||
id: string
|
||||
position: { x: number, y: number }
|
||||
selected?: boolean
|
||||
parentId?: string
|
||||
data: {
|
||||
selected?: boolean
|
||||
_children?: { nodeId: string, nodeType: string }[]
|
||||
_connectedSourceHandleIds?: string[]
|
||||
_connectedTargetHandleIds?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type TestEdge = {
|
||||
id: string
|
||||
source: string
|
||||
sourceHandle?: string
|
||||
target: string
|
||||
targetHandle?: string
|
||||
}
|
||||
|
||||
const mockFetchQuery = vi.fn()
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
const mockSaveStateToHistory = vi.fn()
|
||||
@ -8,6 +29,7 @@ const mockToastError = vi.fn()
|
||||
const mockGetNodes = vi.fn()
|
||||
const mockSetNodes = vi.fn()
|
||||
const mockSetEdges = vi.fn()
|
||||
let mockEdges: unknown[] = [{ id: 'existing-edge', source: 'old', target: 'old-2' }]
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: () => ({
|
||||
@ -20,7 +42,7 @@ vi.mock('reactflow', () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
setNodes: mockSetNodes,
|
||||
edges: [{ id: 'existing-edge', source: 'old', target: 'old-2' }],
|
||||
edges: mockEdges,
|
||||
setEdges: mockSetEdges,
|
||||
}),
|
||||
}),
|
||||
@ -47,6 +69,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
describe('useInsertSnippet', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEdges = [{ id: 'existing-edge', source: 'old', target: 'old-2' }]
|
||||
mockGetNodes.mockReturnValue([
|
||||
{
|
||||
id: 'existing-node',
|
||||
@ -96,25 +119,127 @@ describe('useInsertSnippet', () => {
|
||||
expect(mockSetNodes).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetEdges).toHaveBeenCalledTimes(1)
|
||||
|
||||
const nextNodes = mockSetNodes.mock.calls[0][0]
|
||||
expect(nextNodes[0].selected).toBe(false)
|
||||
expect(nextNodes[0].data.selected).toBe(false)
|
||||
const nextNodes = mockSetNodes.mock.calls[0]![0] as TestNode[]
|
||||
expect(nextNodes[0]!.selected).toBe(false)
|
||||
expect(nextNodes[0]!.data.selected).toBe(false)
|
||||
expect(nextNodes).toHaveLength(3)
|
||||
expect(nextNodes[1].id).not.toBe('snippet-node-1')
|
||||
expect(nextNodes[2].parentId).toBe(nextNodes[1].id)
|
||||
expect(nextNodes[1].data._children[0].nodeId).toBe(nextNodes[2].id)
|
||||
expect(nextNodes[1]!.id).not.toBe('snippet-node-1')
|
||||
expect(nextNodes[2]!.parentId).toBe(nextNodes[1]!.id)
|
||||
expect(nextNodes[1]!.data._children![0]!.nodeId).toBe(nextNodes[2]!.id)
|
||||
|
||||
const nextEdges = mockSetEdges.mock.calls[0][0]
|
||||
const nextEdges = mockSetEdges.mock.calls[0]![0] as TestEdge[]
|
||||
expect(nextEdges).toHaveLength(2)
|
||||
expect(nextEdges[1].source).toBe(nextNodes[1].id)
|
||||
expect(nextEdges[1].target).toBe(nextNodes[2].id)
|
||||
expect(nextEdges[1]!.source).toBe(nextNodes[1]!.id)
|
||||
expect(nextEdges[1]!.target).toBe(nextNodes[2]!.id)
|
||||
|
||||
expect(mockSaveStateToHistory).toHaveBeenCalledWith('NodePaste', {
|
||||
nodeId: nextNodes[1].id,
|
||||
nodeId: nextNodes[1]!.id,
|
||||
})
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should connect inserted snippet nodes to the requested edge position', async () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
{
|
||||
id: 'prev-node',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 240,
|
||||
data: { type: 'start', selected: true, _connectedSourceHandleIds: ['source'] },
|
||||
},
|
||||
{
|
||||
id: 'next-node',
|
||||
position: { x: 300, y: 0 },
|
||||
data: { type: 'answer', selected: false, _connectedTargetHandleIds: ['target'] },
|
||||
},
|
||||
])
|
||||
mockEdges = [
|
||||
{
|
||||
id: 'prev-node-source-next-node-target',
|
||||
source: 'prev-node',
|
||||
sourceHandle: 'source',
|
||||
target: 'next-node',
|
||||
targetHandle: 'target',
|
||||
data: {
|
||||
sourceType: 'start',
|
||||
targetType: 'answer',
|
||||
},
|
||||
},
|
||||
]
|
||||
mockFetchQuery.mockResolvedValue({
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'snippet-entry',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: 'llm', selected: false },
|
||||
},
|
||||
{
|
||||
id: 'snippet-exit',
|
||||
position: { x: 300, y: 0 },
|
||||
data: { type: 'code', selected: false },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'snippet-entry-source-snippet-exit-target',
|
||||
source: 'snippet-entry',
|
||||
sourceHandle: 'source',
|
||||
target: 'snippet-exit',
|
||||
targetHandle: 'target',
|
||||
data: {
|
||||
sourceType: 'llm',
|
||||
targetType: 'code',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useInsertSnippet())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleInsertSnippet('snippet-1', {
|
||||
prevNodeId: 'prev-node',
|
||||
prevNodeSourceHandle: 'source',
|
||||
nextNodeId: 'next-node',
|
||||
nextNodeTargetHandle: 'target',
|
||||
})
|
||||
})
|
||||
|
||||
const nextNodes = mockSetNodes.mock.calls[0]![0] as TestNode[]
|
||||
const insertedEntry = nextNodes.find(node => node.id !== 'prev-node' && node.id !== 'next-node' && node.id.includes('snippet-entry'))!
|
||||
const insertedExit = nextNodes.find(node => node.id !== 'prev-node' && node.id !== 'next-node' && node.id.includes('snippet-exit'))!
|
||||
const shiftedNextNode = nextNodes.find(node => node.id === 'next-node')!
|
||||
expect(insertedEntry.position).toEqual({ x: 300, y: 0 })
|
||||
expect(shiftedNextNode.position.x).toBe(600)
|
||||
expect(nextNodes.find(node => node.id === 'prev-node')!.data._connectedSourceHandleIds).toEqual(['source'])
|
||||
expect(insertedEntry.data._connectedTargetHandleIds).toEqual(['target'])
|
||||
expect(insertedExit.data._connectedSourceHandleIds).toEqual(['source'])
|
||||
expect(shiftedNextNode.data._connectedTargetHandleIds).toEqual(['target'])
|
||||
|
||||
const nextEdges = mockSetEdges.mock.calls[0]![0] as TestEdge[]
|
||||
expect(nextEdges).toHaveLength(3)
|
||||
expect(nextEdges.some(edge => edge.id === 'prev-node-source-next-node-target')).toBe(false)
|
||||
expect(nextEdges).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source: 'prev-node',
|
||||
sourceHandle: 'source',
|
||||
target: insertedEntry.id,
|
||||
targetHandle: 'target',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: insertedEntry.id,
|
||||
target: insertedExit.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: insertedExit.id,
|
||||
sourceHandle: 'source',
|
||||
target: 'next-node',
|
||||
targetHandle: 'target',
|
||||
}),
|
||||
]))
|
||||
})
|
||||
|
||||
it('should show error toast when fetching snippet workflow fails', async () => {
|
||||
mockFetchQuery.mockRejectedValue(new Error('insert failed'))
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { OnNodeAdd } from '../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollAreaContent,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
import { useInfiniteScroll } from 'ahooks'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
useRef,
|
||||
@ -31,6 +33,8 @@ import { useInsertSnippet } from './use-insert-snippet'
|
||||
type SnippetsProps = {
|
||||
loading?: boolean
|
||||
searchText: string
|
||||
insertPayload?: Parameters<OnNodeAdd>[1]
|
||||
onInserted?: () => void
|
||||
}
|
||||
|
||||
const LoadingSkeleton = () => {
|
||||
@ -60,6 +64,8 @@ const LoadingSkeleton = () => {
|
||||
const Snippets = ({
|
||||
loading = false,
|
||||
searchText,
|
||||
insertPayload,
|
||||
onInserted,
|
||||
}: SnippetsProps) => {
|
||||
const {
|
||||
createSnippetMutation,
|
||||
@ -95,6 +101,11 @@ const Snippets = ({
|
||||
}, [data?.pages])
|
||||
|
||||
const isNoMore = hasNextPage === false
|
||||
const handleSnippetClick = useCallback(async (snippetId: string) => {
|
||||
const inserted = await handleInsertSnippet(snippetId, insertPayload)
|
||||
if (inserted)
|
||||
onInserted?.()
|
||||
}, [handleInsertSnippet, insertPayload, onInserted])
|
||||
|
||||
useInfiniteScroll(
|
||||
async () => {
|
||||
@ -129,7 +140,7 @@ const Snippets = ({
|
||||
<SnippetListItem
|
||||
snippet={item}
|
||||
isHovered={hoveredSnippetId === item.id}
|
||||
onClick={() => handleInsertSnippet(item.id)}
|
||||
onClick={() => handleSnippetClick(item.id)}
|
||||
onMouseEnter={() => setHoveredSnippetId(item.id)}
|
||||
onMouseLeave={() => setHoveredSnippetId(current => current === item.id ? null : current)}
|
||||
/>
|
||||
@ -146,7 +157,6 @@ const Snippets = ({
|
||||
/>
|
||||
<TooltipContent
|
||||
placement="left-start"
|
||||
variant="plain"
|
||||
className="bg-transparent! p-0!"
|
||||
>
|
||||
<SnippetDetailCard snippet={item} />
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import type { Edge, Node } from '../../types'
|
||||
import type { Edge, Node, OnNodeAdd } from '../../types'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { CUSTOM_EDGE, ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX, NODE_WIDTH_X_OFFSET, X_OFFSET } from '../../constants'
|
||||
import { useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from '../../hooks'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../../utils'
|
||||
|
||||
type SnippetInsertPayload = Parameters<OnNodeAdd>[1]
|
||||
|
||||
const getSnippetGraph = (graph: Record<string, unknown> | undefined) => {
|
||||
if (!graph)
|
||||
@ -17,7 +22,74 @@ const getSnippetGraph = (graph: Record<string, unknown> | undefined) => {
|
||||
}
|
||||
}
|
||||
|
||||
const remapSnippetGraph = (currentNodes: Node[], snippetNodes: Node[], snippetEdges: Edge[]) => {
|
||||
const getRootNodes = (nodes: Node[]) => {
|
||||
const rootNodes = nodes.filter(node => !node.parentId)
|
||||
return rootNodes.length ? rootNodes : nodes
|
||||
}
|
||||
|
||||
const getSnippetBoundaryNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
const rootNodes = getRootNodes(nodes)
|
||||
const rootNodeIds = new Set(rootNodes.map(node => node.id))
|
||||
const incomingNodeIds = new Set<string>()
|
||||
const outgoingNodeIds = new Set<string>()
|
||||
|
||||
edges.forEach((edge) => {
|
||||
if (!rootNodeIds.has(edge.source) || !rootNodeIds.has(edge.target))
|
||||
return
|
||||
|
||||
outgoingNodeIds.add(edge.source)
|
||||
incomingNodeIds.add(edge.target)
|
||||
})
|
||||
|
||||
return {
|
||||
entryNodes: rootNodes.filter(node => !incomingNodeIds.has(node.id)),
|
||||
exitNodes: rootNodes.filter(node => !outgoingNodeIds.has(node.id)),
|
||||
}
|
||||
}
|
||||
|
||||
const canConnectToTarget = (node: Node) => {
|
||||
return node.data.type !== BlockEnum.DataSource
|
||||
}
|
||||
|
||||
const canConnectFromSource = (node: Node) => {
|
||||
return node.data.type !== BlockEnum.IfElse
|
||||
&& node.data.type !== BlockEnum.QuestionClassifier
|
||||
&& node.data.type !== BlockEnum.HumanInput
|
||||
&& node.data.type !== BlockEnum.LoopEnd
|
||||
}
|
||||
|
||||
const getInsertAnchor = (
|
||||
currentNodes: Node[],
|
||||
insertPayload?: SnippetInsertPayload,
|
||||
) => {
|
||||
const prevNode = insertPayload?.prevNodeId
|
||||
? currentNodes.find(node => node.id === insertPayload.prevNodeId)
|
||||
: undefined
|
||||
const nextNode = insertPayload?.nextNodeId
|
||||
? currentNodes.find(node => node.id === insertPayload.nextNodeId)
|
||||
: undefined
|
||||
|
||||
if (nextNode) {
|
||||
return {
|
||||
x: nextNode.position.x,
|
||||
y: nextNode.position.y,
|
||||
}
|
||||
}
|
||||
|
||||
if (prevNode) {
|
||||
return {
|
||||
x: prevNode.position.x + (prevNode.width ?? 0) + X_OFFSET,
|
||||
y: prevNode.position.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const remapSnippetGraph = (
|
||||
currentNodes: Node[],
|
||||
snippetNodes: Node[],
|
||||
snippetEdges: Edge[],
|
||||
insertPayload?: SnippetInsertPayload,
|
||||
) => {
|
||||
const existingIds = new Set(currentNodes.map(node => node.id))
|
||||
const idMapping = new Map<string, string>()
|
||||
const rootNodes = snippetNodes.filter(node => !node.parentId)
|
||||
@ -32,8 +104,9 @@ const remapSnippetGraph = (currentNodes: Node[], snippetNodes: Node[], snippetEd
|
||||
const currentMinY = currentNodes.length
|
||||
? Math.min(...currentNodes.map(node => node.positionAbsolute?.y ?? node.position.y))
|
||||
: 0
|
||||
const offsetX = (currentNodes.length ? currentMaxX + 80 : 80) - minRootX
|
||||
const offsetY = (currentNodes.length ? currentMinY : 80) - minRootY
|
||||
const insertAnchor = getInsertAnchor(currentNodes, insertPayload)
|
||||
const offsetX = (insertAnchor?.x ?? (currentNodes.length ? currentMaxX + 80 : 80)) - minRootX
|
||||
const offsetY = (insertAnchor?.y ?? (currentNodes.length ? currentMinY : 80)) - minRootY
|
||||
|
||||
snippetNodes.forEach((node, index) => {
|
||||
let nextId = `${node.id}-${Date.now()}-${index}`
|
||||
@ -94,6 +167,111 @@ const remapSnippetGraph = (currentNodes: Node[], snippetNodes: Node[], snippetEd
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
const getCurrentEdge = (edges: Edge[], insertPayload?: SnippetInsertPayload) => {
|
||||
if (!insertPayload?.prevNodeId || !insertPayload.nextNodeId)
|
||||
return undefined
|
||||
|
||||
return edges.find(edge =>
|
||||
edge.source === insertPayload.prevNodeId
|
||||
&& edge.target === insertPayload.nextNodeId
|
||||
&& (edge.sourceHandle || 'source') === (insertPayload.prevNodeSourceHandle || 'source')
|
||||
&& (edge.targetHandle || 'target') === (insertPayload.nextNodeTargetHandle || 'target'),
|
||||
)
|
||||
}
|
||||
|
||||
const getParentNode = (nodes: Node[], insertPayload?: SnippetInsertPayload) => {
|
||||
const prevNode = insertPayload?.prevNodeId
|
||||
? nodes.find(node => node.id === insertPayload.prevNodeId)
|
||||
: undefined
|
||||
const nextNode = insertPayload?.nextNodeId
|
||||
? nodes.find(node => node.id === insertPayload.nextNodeId)
|
||||
: undefined
|
||||
const parentId = prevNode?.parentId ?? nextNode?.parentId
|
||||
|
||||
return parentId ? nodes.find(node => node.id === parentId) : undefined
|
||||
}
|
||||
|
||||
const createBoundaryEdges = ({
|
||||
currentNodes,
|
||||
insertPayload,
|
||||
entryNodes,
|
||||
exitNodes,
|
||||
}: {
|
||||
currentNodes: Node[]
|
||||
insertPayload?: SnippetInsertPayload
|
||||
entryNodes: Node[]
|
||||
exitNodes: Node[]
|
||||
}) => {
|
||||
const prevNode = insertPayload?.prevNodeId
|
||||
? currentNodes.find(node => node.id === insertPayload.prevNodeId)
|
||||
: undefined
|
||||
const nextNode = insertPayload?.nextNodeId
|
||||
? currentNodes.find(node => node.id === insertPayload.nextNodeId)
|
||||
: undefined
|
||||
const parentNode = getParentNode(currentNodes, insertPayload)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = parentNode?.data.type === BlockEnum.Loop
|
||||
const zIndex = parentNode
|
||||
? isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
: 0
|
||||
const incomingEdges: Edge[] = []
|
||||
const outgoingEdges: Edge[] = []
|
||||
|
||||
if (prevNode) {
|
||||
incomingEdges.push(...entryNodes.filter(canConnectToTarget).map((entryNode) => {
|
||||
const sourceHandle = insertPayload?.prevNodeSourceHandle || 'source'
|
||||
const targetHandle = 'target'
|
||||
|
||||
return {
|
||||
id: `${prevNode.id}-${sourceHandle}-${entryNode.id}-${targetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
source: prevNode.id,
|
||||
sourceHandle,
|
||||
target: entryNode.id,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: prevNode.data.type,
|
||||
targetType: entryNode.data.type,
|
||||
isInIteration,
|
||||
isInLoop,
|
||||
iteration_id: isInIteration ? parentNode?.id : undefined,
|
||||
loop_id: isInLoop ? parentNode?.id : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex,
|
||||
} as Edge
|
||||
}))
|
||||
}
|
||||
|
||||
if (nextNode) {
|
||||
outgoingEdges.push(...exitNodes.filter(canConnectFromSource).map((exitNode) => {
|
||||
const sourceHandle = 'source'
|
||||
const targetHandle = insertPayload?.nextNodeTargetHandle || 'target'
|
||||
|
||||
return {
|
||||
id: `${exitNode.id}-${sourceHandle}-${nextNode.id}-${targetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
source: exitNode.id,
|
||||
sourceHandle,
|
||||
target: nextNode.id,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: exitNode.data.type,
|
||||
targetType: nextNode.data.type,
|
||||
isInIteration,
|
||||
isInLoop,
|
||||
iteration_id: isInIteration ? parentNode?.id : undefined,
|
||||
loop_id: isInLoop ? parentNode?.id : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex,
|
||||
} as Edge
|
||||
}))
|
||||
}
|
||||
|
||||
return [...incomingEdges, ...outgoingEdges]
|
||||
}
|
||||
|
||||
export const useInsertSnippet = () => {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
@ -101,7 +279,7 @@ export const useInsertSnippet = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const handleInsertSnippet = useCallback(async (snippetId: string) => {
|
||||
const handleInsertSnippet = useCallback(async (snippetId: string, insertPayload?: SnippetInsertPayload) => {
|
||||
try {
|
||||
const workflow = await queryClient.fetchQuery(consoleQuery.snippets.publishedWorkflow.queryOptions({
|
||||
input: {
|
||||
@ -115,25 +293,107 @@ export const useInsertSnippet = () => {
|
||||
|
||||
const { getNodes, setNodes, edges, setEdges } = store.getState()
|
||||
const currentNodes = getNodes()
|
||||
const remappedGraph = remapSnippetGraph(currentNodes, snippetNodes, snippetEdges)
|
||||
const remappedGraph = remapSnippetGraph(currentNodes, snippetNodes, snippetEdges, insertPayload)
|
||||
const parentNode = getParentNode(currentNodes, insertPayload)
|
||||
const rootNodeIds = new Set(getRootNodes(remappedGraph.nodes).map(node => node.id))
|
||||
const rootSnippetNodes = remappedGraph.nodes.filter(node => rootNodeIds.has(node.id))
|
||||
const currentEdge = getCurrentEdge(edges, insertPayload)
|
||||
const { entryNodes, exitNodes } = getSnippetBoundaryNodes(remappedGraph.nodes, remappedGraph.edges)
|
||||
const boundaryEdges = createBoundaryEdges({
|
||||
currentNodes,
|
||||
insertPayload,
|
||||
entryNodes,
|
||||
exitNodes,
|
||||
})
|
||||
const changes = [
|
||||
...(currentEdge ? [{ type: 'remove', edge: currentEdge }] : []),
|
||||
...boundaryEdges.map(edge => ({ type: 'add', edge })),
|
||||
]
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
changes,
|
||||
[...currentNodes, ...remappedGraph.nodes],
|
||||
)
|
||||
const firstEntryNode = entryNodes.find(canConnectToTarget) ?? entryNodes[0]
|
||||
const clearedNodes = currentNodes.map(node => ({
|
||||
...node,
|
||||
selected: false,
|
||||
position: insertPayload?.nextNodeId && node.id === insertPayload.nextNodeId
|
||||
? {
|
||||
...node.position,
|
||||
x: node.position.x + NODE_WIDTH_X_OFFSET,
|
||||
}
|
||||
: node.position,
|
||||
data: {
|
||||
...node.data,
|
||||
selected: false,
|
||||
...(nodesConnectedSourceOrTargetHandleIdsMap[node.id] ?? {}),
|
||||
_children: parentNode?.id === node.id
|
||||
? [
|
||||
...(node.data._children ?? []),
|
||||
...rootSnippetNodes.map(rootNode => ({
|
||||
nodeId: rootNode.id,
|
||||
nodeType: rootNode.data.type,
|
||||
})),
|
||||
]
|
||||
: node.data._children,
|
||||
start_node_id: node.id === parentNode?.id
|
||||
&& node.data.start_node_id === insertPayload?.nextNodeId
|
||||
&& firstEntryNode
|
||||
? firstEntryNode.id
|
||||
: node.data.start_node_id,
|
||||
startNodeType: node.id === parentNode?.id
|
||||
&& node.data.start_node_id === insertPayload?.nextNodeId
|
||||
&& firstEntryNode
|
||||
? firstEntryNode.data.type
|
||||
: node.data.startNodeType,
|
||||
},
|
||||
}))
|
||||
const insertedNodes = remappedGraph.nodes.map((node) => {
|
||||
const shouldMoveIntoParent = !!parentNode && rootNodeIds.has(node.id)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = parentNode?.data.type === BlockEnum.Loop
|
||||
|
||||
setNodes([...clearedNodes, ...remappedGraph.nodes])
|
||||
setEdges([...edges, ...remappedGraph.edges])
|
||||
return {
|
||||
...node,
|
||||
parentId: shouldMoveIntoParent ? parentNode.id : node.parentId,
|
||||
extent: shouldMoveIntoParent ? parentNode.extent : node.extent,
|
||||
zIndex: shouldMoveIntoParent
|
||||
? isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
: node.zIndex,
|
||||
data: {
|
||||
...node.data,
|
||||
...(nodesConnectedSourceOrTargetHandleIdsMap[node.id] ?? {}),
|
||||
isInIteration: shouldMoveIntoParent ? isInIteration : node.data.isInIteration,
|
||||
isInLoop: shouldMoveIntoParent ? isInLoop : node.data.isInLoop,
|
||||
iteration_id: shouldMoveIntoParent && isInIteration ? parentNode.id : node.data.iteration_id,
|
||||
loop_id: shouldMoveIntoParent && isInLoop ? parentNode.id : node.data.loop_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
setNodes([...clearedNodes, ...insertedNodes])
|
||||
setEdges([
|
||||
...edges
|
||||
.filter(edge => edge.id !== currentEdge?.id)
|
||||
.map(edge => ({
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
_connectedNodeIsSelected: false,
|
||||
},
|
||||
})),
|
||||
...remappedGraph.edges,
|
||||
...boundaryEdges,
|
||||
])
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodePaste, {
|
||||
nodeId: remappedGraph.nodes[0]?.id,
|
||||
})
|
||||
handleSyncWorkflowDraft()
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('createFailed', { ns: 'snippet' }))
|
||||
return false
|
||||
}
|
||||
}, [handleSyncWorkflowDraft, queryClient, saveStateToHistory, store, t])
|
||||
|
||||
|
||||
@ -163,6 +163,12 @@ const CustomEdge = ({
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
snippetInsertPayload={{
|
||||
prevNodeId: source,
|
||||
prevNodeSourceHandle: sourceHandleId || 'source',
|
||||
nextNodeId: target,
|
||||
nextNodeTargetHandle: targetHandleId || 'target',
|
||||
}}
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks)}
|
||||
triggerClassName={() => 'hover:scale-150 transition-all'}
|
||||
/>
|
||||
|
||||
@ -91,6 +91,10 @@ const Add = ({
|
||||
onOpenChange={handleOpenChange}
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
snippetInsertPayload={{
|
||||
prevNodeId: nodeId,
|
||||
prevNodeSourceHandle: sourceHandle,
|
||||
}}
|
||||
placement="top"
|
||||
offset={0}
|
||||
trigger={renderTrigger}
|
||||
|
||||
@ -110,6 +110,10 @@ export const NodeTargetHandle = memo(({
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
snippetInsertPayload={{
|
||||
nextNodeId: id,
|
||||
nextNodeTargetHandle: handleId,
|
||||
}}
|
||||
asChild
|
||||
placement="left"
|
||||
triggerClassName={open => `
|
||||
@ -229,6 +233,10 @@ export const NodeSourceHandle = memo(({
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
snippetInsertPayload={{
|
||||
prevNodeId: id,
|
||||
prevNodeSourceHandle: handleId,
|
||||
}}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
absolute top-0 left-0 opacity-0 pointer-events-none transition-opacity duration-150
|
||||
|
||||
@ -68,6 +68,10 @@ const AddBlock = ({
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
snippetInsertPayload={{
|
||||
prevNodeId: iterationNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
}}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName="inline-flex"
|
||||
popupClassName="min-w-[256px]!"
|
||||
|
||||
@ -69,6 +69,10 @@ const AddBlock = ({
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
snippetInsertPayload={{
|
||||
prevNodeId: loopNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
}}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName="inline-flex"
|
||||
popupClassName="min-w-[256px]!"
|
||||
|
||||
@ -51,6 +51,10 @@ const InsertBlock = ({
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
snippetInsertPayload={{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
}}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user