Merge remote-tracking branch 'origin/main' into feat/model-plugins-implementing

This commit is contained in:
yyh
2026-03-09 17:03:54 +08:00
62 changed files with 4852 additions and 262 deletions

View File

@ -332,8 +332,7 @@ const Chat: FC<ChatProps> = ({
!noStopResponding && isResponding && (
<div data-testid="stop-responding-container" className="mb-2 flex justify-center">
<Button className="border-components-panel-border bg-components-panel-bg text-components-button-secondary-text" onClick={onStopResponding}>
{/* eslint-disable-next-line tailwindcss/no-unknown-classes */}
<div className="i-custom-vender-solid-mediaanddevices-stop-circle mr-[5px] h-3.5 w-3.5" />
<div className="i-custom-vender-solid-mediaAndDevices-stop-circle mr-[5px] h-3.5 w-3.5" />
<span className="text-xs font-normal">{t('operation.stopResponding', { ns: 'appDebug' })}</span>
</Button>
</div>

View File

@ -111,11 +111,11 @@ const ToolItem: FC<Props> = ({
})
}}
>
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
<div className={cn('truncate border-l-2 border-divider-subtle py-2 pl-4 text-text-secondary system-sm-medium')}>
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
</div>
{isAdded && (
<div className="system-xs-regular mr-4 text-text-tertiary">{t('addToolModal.added', { ns: 'tools' })}</div>
<div className="mr-4 text-text-tertiary system-xs-regular">{t('addToolModal.added', { ns: 'tools' })}</div>
)}
</div>
</Tooltip>

View File

@ -77,11 +77,11 @@ const TriggerPluginActionItem: FC<Props> = ({
})
}}
>
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
<div className={cn('truncate border-l-2 border-divider-subtle py-2 pl-4 text-text-secondary system-sm-medium')}>
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
</div>
{isAdded && (
<div className="system-xs-regular mr-4 text-text-tertiary">{t('addToolModal.added', { ns: 'tools' })}</div>
<div className="mr-4 text-text-tertiary system-xs-regular">{t('addToolModal.added', { ns: 'tools' })}</div>
)}
</div>
</Tooltip>

View File

@ -1422,21 +1422,136 @@ export const useNodesInteractions = () => {
extent: currentNode.extent,
zIndex: currentNode.zIndex,
})
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
connectedEdges.map(edge => ({ type: 'remove', edge })),
nodes,
)
const newNodes = produce(nodes, (draft) => {
const parentNode = nodes.find(node => node.id === currentNode.parentId)
const newNodeIsInIteration
= !!parentNode && parentNode.data.type === BlockEnum.Iteration
const newNodeIsInLoop
= !!parentNode && parentNode.data.type === BlockEnum.Loop
const outgoingEdges = connectedEdges.filter(
edge => edge.source === currentNodeId,
)
const normalizedSourceHandle = sourceHandle || 'source'
const outgoingHandles = new Set(
outgoingEdges.map(edge => edge.sourceHandle || 'source'),
)
const branchSourceHandle = currentNode.data._targetBranches?.[0]?.id
let outgoingHandleToPreserve = normalizedSourceHandle
if (!outgoingHandles.has(outgoingHandleToPreserve)) {
if (branchSourceHandle && outgoingHandles.has(branchSourceHandle))
outgoingHandleToPreserve = branchSourceHandle
else if (outgoingHandles.has('source'))
outgoingHandleToPreserve = 'source'
else
outgoingHandleToPreserve = outgoingEdges[0]?.sourceHandle || 'source'
}
const outgoingEdgesToPreserve = outgoingEdges.filter(
edge => (edge.sourceHandle || 'source') === outgoingHandleToPreserve,
)
const outgoingEdgeIds = new Set(
outgoingEdgesToPreserve.map(edge => edge.id),
)
const newNodeSourceHandle = newCurrentNode.data._targetBranches?.[0]?.id || 'source'
const reconnectedEdges = connectedEdges.reduce<Edge[]>(
(acc, edge) => {
if (outgoingEdgeIds.has(edge.id)) {
const originalTargetNode = nodes.find(
node => node.id === edge.target,
)
const targetNodeForEdge
= originalTargetNode && originalTargetNode.id !== currentNodeId
? originalTargetNode
: newCurrentNode
if (!targetNodeForEdge)
return acc
const targetHandle = edge.targetHandle || 'target'
const targetParentNode
= targetNodeForEdge.id === newCurrentNode.id
? parentNode || null
: nodes.find(node => node.id === targetNodeForEdge.parentId)
|| null
const isInIteration
= !!targetParentNode
&& targetParentNode.data.type === BlockEnum.Iteration
const isInLoop
= !!targetParentNode
&& targetParentNode.data.type === BlockEnum.Loop
acc.push({
...edge,
id: `${newCurrentNode.id}-${newNodeSourceHandle}-${targetNodeForEdge.id}-${targetHandle}`,
source: newCurrentNode.id,
sourceHandle: newNodeSourceHandle,
target: targetNodeForEdge.id,
targetHandle,
type: CUSTOM_EDGE,
data: {
...(edge.data || {}),
sourceType: newCurrentNode.data.type,
targetType: targetNodeForEdge.data.type,
isInIteration,
iteration_id: isInIteration
? targetNodeForEdge.parentId
: undefined,
isInLoop,
loop_id: isInLoop ? targetNodeForEdge.parentId : undefined,
_connectedNodeIsSelected: false,
},
zIndex: targetNodeForEdge.parentId
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
})
}
if (
edge.target === currentNodeId
&& edge.source !== currentNodeId
&& !outgoingEdgeIds.has(edge.id)
) {
const sourceNode = nodes.find(node => node.id === edge.source)
if (!sourceNode)
return acc
const targetHandle = edge.targetHandle || 'target'
const sourceHandle = edge.sourceHandle || 'source'
acc.push({
...edge,
id: `${sourceNode.id}-${sourceHandle}-${newCurrentNode.id}-${targetHandle}`,
source: sourceNode.id,
sourceHandle,
target: newCurrentNode.id,
targetHandle,
type: CUSTOM_EDGE,
data: {
...(edge.data || {}),
sourceType: sourceNode.data.type,
targetType: newCurrentNode.data.type,
isInIteration: newNodeIsInIteration,
iteration_id: newNodeIsInIteration
? newCurrentNode.parentId
: undefined,
isInLoop: newNodeIsInLoop,
loop_id: newNodeIsInLoop ? newCurrentNode.parentId : undefined,
_connectedNodeIsSelected: false,
},
zIndex: newCurrentNode.parentId
? newNodeIsInIteration
? ITERATION_CHILDREN_Z_INDEX
: LOOP_CHILDREN_Z_INDEX
: 0,
})
}
return acc
},
[],
)
const nodesWithNewNode = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data.selected = false
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
const index = draft.findIndex(node => node.id === currentNodeId)
@ -1446,18 +1561,32 @@ export const useNodesInteractions = () => {
if (newLoopStartNode)
draft.push(newLoopStartNode)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const filtered = draft.filter(
edge =>
!connectedEdges.find(
connectedEdge => connectedEdge.id === edge.id,
),
const nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
[
...connectedEdges.map(edge => ({ type: 'remove', edge })),
...reconnectedEdges.map(edge => ({ type: 'add', edge })),
],
nodesWithNewNode,
)
return filtered
const newNodes = produce(nodesWithNewNode, (draft) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setEdges(newEdges)
setNodes(newNodes)
const remainingEdges = edges.filter(
edge =>
!connectedEdges.find(
connectedEdge => connectedEdge.id === edge.id,
),
)
setEdges([...remainingEdges, ...reconnectedEdges])
if (nodeType === BlockEnum.TriggerWebhook) {
handleSyncWorkflowDraft(true, true, {
onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
@ -1606,6 +1735,7 @@ export const useNodesInteractions = () => {
const offsetX = currentPosition.x - x
const offsetY = currentPosition.y - y
let idMapping: Record<string, string> = {}
const pastedNodesMap: Record<string, Node> = {}
const parentChildrenToAppend: { parentId: string, childId: string, childType: BlockEnum }[] = []
clipboardElements.forEach((nodeToPaste, index) => {
const nodeType = nodeToPaste.data.type
@ -1665,7 +1795,21 @@ export const useNodesInteractions = () => {
newLoopStartNode!.parentId = newNode.id;
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id
newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
const oldLoopStartNode = nodes.find(
n =>
n.parentId === nodeToPaste.id
&& n.type === CUSTOM_LOOP_START_NODE,
)
idMapping[oldLoopStartNode!.id] = newLoopStartNode!.id
const { copyChildren, newIdMapping }
= handleNodeLoopChildrenCopy(
nodeToPaste.id,
newNode.id,
idMapping,
)
newChildren = copyChildren
idMapping = newIdMapping
newChildren.forEach((child) => {
newNode.data._children?.push({
nodeId: child.id,
@ -1710,18 +1854,31 @@ export const useNodesInteractions = () => {
}
}
idMapping[nodeToPaste.id] = newNode.id
nodesToPaste.push(newNode)
pastedNodesMap[newNode.id] = newNode
if (newChildren.length)
if (newChildren.length) {
newChildren.forEach((child) => {
pastedNodesMap[child.id] = child
})
nodesToPaste.push(...newChildren)
}
})
// only handle edge when paste nested block
// Rebuild edges where both endpoints are part of the pasted set.
edges.forEach((edge) => {
const sourceId = idMapping[edge.source]
const targetId = idMapping[edge.target]
if (sourceId && targetId) {
const sourceNode = pastedNodesMap[sourceId]
const targetNode = pastedNodesMap[targetId]
const parentNode = sourceNode?.parentId && sourceNode.parentId === targetNode?.parentId
? pastedNodesMap[sourceNode.parentId] ?? nodes.find(n => n.id === sourceNode.parentId)
: null
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
const isInLoop = parentNode?.data.type === BlockEnum.Loop
const newEdge: Edge = {
...edge,
id: `${sourceId}-${edge.sourceHandle}-${targetId}-${edge.targetHandle}`,
@ -1729,8 +1886,19 @@ export const useNodesInteractions = () => {
target: targetId,
data: {
...edge.data,
isInIteration,
iteration_id: isInIteration ? parentNode?.id : undefined,
isInLoop,
loop_id: isInLoop ? parentNode?.id : undefined,
_connectedNodeIsSelected: false,
},
zIndex: parentNode
? isInIteration
? ITERATION_CHILDREN_Z_INDEX
: isInLoop
? LOOP_CHILDREN_Z_INDEX
: 0
: 0,
}
edgesToPaste.push(newEdge)
}

View File

@ -108,12 +108,13 @@ export const useNodeLoopInteractions = () => {
handleNodeLoopRerender(parentId)
}, [store, handleNodeLoopRerender])
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string) => {
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string, idMapping: Record<string, string>) => {
const { getNodes } = store.getState()
const nodes = getNodes()
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE)
const newIdMapping = { ...idMapping }
return childrenNodes.map((child, index) => {
const copyChildren = childrenNodes.map((child, index) => {
const childNodeType = child.data.type as BlockEnum
const { defaultValue } = nodesMetaDataMap![childNodeType]
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
@ -139,8 +140,14 @@ export const useNodeLoopInteractions = () => {
zIndex: LOOP_CHILDREN_Z_INDEX,
})
newNode.id = `${newNodeId}${newNode.id + index}`
newIdMapping[child.id] = newNode.id
return newNode
})
return {
copyChildren,
newIdMapping,
}
}, [store, nodesMetaDataMap])
return {

View File

@ -6319,9 +6319,6 @@
"app/components/workflow/block-selector/tool/action-item.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
@ -6341,9 +6338,6 @@
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}

View File

@ -228,7 +228,7 @@
"eslint-plugin-sonarjs": "4.0.0",
"eslint-plugin-storybook": "10.2.13",
"husky": "9.1.7",
"iconify-import-svg": "0.1.1",
"iconify-import-svg": "0.1.2",
"jsdom": "27.3.0",
"jsdom-testing-mocks": "1.16.0",
"knip": "5.78.0",

10
web/pnpm-lock.yaml generated
View File

@ -552,8 +552,8 @@ importers:
specifier: 9.1.7
version: 9.1.7
iconify-import-svg:
specifier: 0.1.1
version: 0.1.1
specifier: 0.1.2
version: 0.1.2
jsdom:
specifier: 27.3.0
version: 27.3.0(canvas@3.2.1)
@ -5263,8 +5263,8 @@ packages:
typescript:
optional: true
iconify-import-svg@0.1.1:
resolution: {integrity: sha512-8HwZIe3ZqCfZ68NZUCnHN264fwHWhE+O5hWDfBtOEY7u1V97yOogHaoXGRLOx17M0c8+z65xYqJXA16ieCYIwA==}
iconify-import-svg@0.1.2:
resolution: {integrity: sha512-8dwxdGK1a7oPDQhLQOPTbx51tpkxYB6HZvf4fxWz2QVYqEtgop0FWE7OXQ+4zqnrTVUpMIGnOsvqIHtPBK9Isw==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
@ -13145,7 +13145,7 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
iconify-import-svg@0.1.1:
iconify-import-svg@0.1.2:
dependencies:
'@iconify/tools': 4.2.0
'@iconify/types': 2.0.0

View File

@ -14,11 +14,16 @@ const _dirname = typeof __dirname !== 'undefined'
: path.dirname(fileURLToPath(import.meta.url))
const disableSVGOptimize = process.env.TAILWIND_MODE === 'ESLINT'
const parseColorOptions = {
fallback: () => 'currentColor',
}
const svgOptimizeConfig = {
cleanupSVG: !disableSVGOptimize,
deOptimisePaths: !disableSVGOptimize,
runSVGO: !disableSVGOptimize,
parseColors: !disableSVGOptimize,
parseColors: !disableSVGOptimize
? parseColorOptions
: false,
}
const config = {