mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 13:00:56 +03:00
Feature/agentflow v2 (#4298)
* agent flow v2 * chat message background * conditon agent flow * add sticky note * update human input dynamic prompt * add HTTP node * add default tool icon * fix export duplicate agentflow v2 * add agentflow v2 marketplaces * refractor memoization, add iteration nodes * add agentflow v2 templates * add agentflow generator * add migration scripts for mysql, mariadb, posrgres and fix date filters for executions * update agentflow chat history config * fix get all flows error after deletion and rename * add previous nodes from parent node * update generator prompt * update run time state when using iteration nodes * prevent looping connection, prevent duplication of start node, add executeflow node, add nodes agentflow, chat history variable * update embed * convert form input to string * bump openai version * add react rewards * add prompt generator to prediction queue * add array schema to overrideconfig * UI touchup * update embedded chat version * fix node info dialog * update start node and loop default iteration * update UI fixes for agentflow v2 * fix async drop down * add export import to agentflowsv2, executions, fix UI bugs * add default empty object to flowlisttable * add ability to share trace link publicly, allow MCP tool use for Agent and Assistant * add runtime message length to variable, display conditions on UI * fix array validation * add ability to add knowledge from vector store and embeddings for agent * add agent tool require human input * add ephemeral memory to start node * update agent flow node to show vs and embeddings icons * feat: add import chat data functionality for AgentFlowV2 * feat: set chatMessage.executionId to null if not found in import JSON file or database * fix: MariaDB execution migration script to utf8mb4_unicode_520_ci --------- Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com> Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
@@ -78,6 +78,7 @@ export const exportData = (exportAllData) => {
|
||||
try {
|
||||
return {
|
||||
AgentFlow: sanitizeChatflow(exportAllData.AgentFlow),
|
||||
AgentFlowV2: sanitizeChatflow(exportAllData.AgentFlowV2),
|
||||
AssistantFlow: sanitizeChatflow(exportAllData.AssistantFlow),
|
||||
AssistantCustom: sanitizeAssistant(exportAllData.AssistantCustom),
|
||||
AssistantOpenAI: sanitizeAssistant(exportAllData.AssistantOpenAI),
|
||||
@@ -88,6 +89,7 @@ export const exportData = (exportAllData) => {
|
||||
CustomTemplate: exportAllData.CustomTemplate,
|
||||
DocumentStore: exportAllData.DocumentStore,
|
||||
DocumentStoreFileChunk: exportAllData.DocumentStoreFileChunk,
|
||||
Execution: exportAllData.Execution,
|
||||
Tool: sanitizeTool(exportAllData.Tool),
|
||||
Variable: sanitizeVariable(exportAllData.Variable)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { uniq } from 'lodash'
|
||||
import { uniq, get, isEqual } from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
@@ -16,6 +16,94 @@ export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
return baseId
|
||||
}
|
||||
|
||||
export const getUniqueNodeLabel = (nodeData, nodes) => {
|
||||
if (nodeData.type === 'StickyNote') return nodeData.label
|
||||
if (nodeData.name === 'startAgentflow') return nodeData.label
|
||||
|
||||
let suffix = 0
|
||||
|
||||
// Construct base ID
|
||||
let baseId = `${nodeData.name}_${suffix}`
|
||||
|
||||
// Increment suffix until a unique ID is found
|
||||
while (nodes.some((node) => node.id === baseId)) {
|
||||
suffix += 1
|
||||
baseId = `${nodeData.name}_${suffix}`
|
||||
}
|
||||
|
||||
return `${nodeData.label} ${suffix}`
|
||||
}
|
||||
|
||||
const createAgentFlowOutputs = (nodeData, newNodeId) => {
|
||||
if (nodeData.hideOutput) return []
|
||||
|
||||
if (nodeData.outputs?.length) {
|
||||
return nodeData.outputs.map((_, index) => ({
|
||||
id: `${newNodeId}-output-${index}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}))
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${newNodeId}-output-${nodeData.name}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createOutputOption = (output, newNodeId) => {
|
||||
const outputBaseClasses = output.baseClasses ?? []
|
||||
const baseClasses = outputBaseClasses.length > 1 ? outputBaseClasses.join('|') : outputBaseClasses[0] || ''
|
||||
|
||||
const type = outputBaseClasses.length > 1 ? outputBaseClasses.join(' | ') : outputBaseClasses[0] || ''
|
||||
|
||||
return {
|
||||
id: `${newNodeId}-output-${output.name}-${baseClasses}`,
|
||||
name: output.name,
|
||||
label: output.label,
|
||||
description: output.description ?? '',
|
||||
type,
|
||||
isAnchor: output?.isAnchor,
|
||||
hidden: output?.hidden
|
||||
}
|
||||
}
|
||||
|
||||
const createStandardOutputs = (nodeData, newNodeId) => {
|
||||
if (nodeData.hideOutput) return []
|
||||
|
||||
if (nodeData.outputs?.length) {
|
||||
const outputOptions = nodeData.outputs.map((output) => createOutputOption(output, newNodeId))
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
description: nodeData.outputs[0].description ?? '',
|
||||
options: outputOptions,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
description: nodeData.description ?? '',
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const initializeOutputAnchors = (nodeData, newNodeId, isAgentflow) => {
|
||||
return isAgentflow ? createAgentFlowOutputs(nodeData, newNodeId) : createStandardOutputs(nodeData, newNodeId)
|
||||
}
|
||||
|
||||
export const initializeDefaultNodeData = (nodeParams) => {
|
||||
const initialValues = {}
|
||||
|
||||
@@ -27,17 +115,17 @@ export const initializeDefaultNodeData = (nodeParams) => {
|
||||
return initialValues
|
||||
}
|
||||
|
||||
export const initNode = (nodeData, newNodeId) => {
|
||||
export const initNode = (nodeData, newNodeId, isAgentflow) => {
|
||||
const inputAnchors = []
|
||||
const inputParams = []
|
||||
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
|
||||
const outgoing = 1
|
||||
|
||||
const whitelistTypes = [
|
||||
'asyncOptions',
|
||||
'asyncMultiOptions',
|
||||
'options',
|
||||
'multiOptions',
|
||||
'array',
|
||||
'datagrid',
|
||||
'string',
|
||||
'number',
|
||||
@@ -75,55 +163,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
}
|
||||
|
||||
// Outputs
|
||||
const outputAnchors = []
|
||||
for (let i = 0; i < outgoing; i += 1) {
|
||||
if (nodeData.hideOutput) continue
|
||||
if (nodeData.outputs && nodeData.outputs.length) {
|
||||
const options = []
|
||||
for (let j = 0; j < nodeData.outputs.length; j += 1) {
|
||||
let baseClasses = ''
|
||||
let type = ''
|
||||
|
||||
const outputBaseClasses = nodeData.outputs[j].baseClasses ?? []
|
||||
if (outputBaseClasses.length > 1) {
|
||||
baseClasses = outputBaseClasses.join('|')
|
||||
type = outputBaseClasses.join(' | ')
|
||||
} else if (outputBaseClasses.length === 1) {
|
||||
baseClasses = outputBaseClasses[0]
|
||||
type = outputBaseClasses[0]
|
||||
}
|
||||
|
||||
const newOutputOption = {
|
||||
id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`,
|
||||
name: nodeData.outputs[j].name,
|
||||
label: nodeData.outputs[j].label,
|
||||
description: nodeData.outputs[j].description ?? '',
|
||||
type,
|
||||
isAnchor: nodeData.outputs[j]?.isAnchor,
|
||||
hidden: nodeData.outputs[j]?.hidden
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
}
|
||||
const newOutput = {
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
description: nodeData.outputs[0].description ?? '',
|
||||
options,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
} else {
|
||||
const newOutput = {
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
description: nodeData.description ?? '',
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
}
|
||||
}
|
||||
let outputAnchors = initializeOutputAnchors(nodeData, newNodeId, isAgentflow)
|
||||
|
||||
/* Initial
|
||||
inputs = [
|
||||
@@ -160,9 +200,10 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
|
||||
// Inputs
|
||||
if (nodeData.inputs) {
|
||||
nodeData.inputAnchors = inputAnchors
|
||||
nodeData.inputParams = inputParams
|
||||
nodeData.inputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
const defaultInputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
nodeData.inputAnchors = showHideInputAnchors({ ...nodeData, inputAnchors, inputs: defaultInputs })
|
||||
nodeData.inputParams = showHideInputParams({ ...nodeData, inputParams, inputs: defaultInputs })
|
||||
nodeData.inputs = defaultInputs
|
||||
} else {
|
||||
nodeData.inputAnchors = []
|
||||
nodeData.inputParams = []
|
||||
@@ -185,8 +226,10 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
return nodeData
|
||||
}
|
||||
|
||||
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData) => {
|
||||
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id)
|
||||
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData, isAgentflow) => {
|
||||
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id, isAgentflow)
|
||||
|
||||
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows' || existingComponentNodeData.category === 'Agent Flows'
|
||||
|
||||
// Update credentials with existing credentials
|
||||
if (existingComponentNodeData.credential) {
|
||||
@@ -220,6 +263,11 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
|
||||
}
|
||||
}
|
||||
|
||||
if (isAgentFlowV2) {
|
||||
// persists the label from the existing node
|
||||
initNewComponentNodeData.label = existingComponentNodeData.label
|
||||
}
|
||||
|
||||
// Special case for Condition node to update outputAnchors
|
||||
if (initNewComponentNodeData.name.includes('seqCondition')) {
|
||||
const options = existingComponentNodeData.outputAnchors[0].options || []
|
||||
@@ -243,22 +291,34 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
|
||||
|
||||
export const updateOutdatedNodeEdge = (newComponentNodeData, edges) => {
|
||||
const removedEdges = []
|
||||
|
||||
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows'
|
||||
|
||||
for (const edge of edges) {
|
||||
const targetNodeId = edge.targetHandle.split('-')[0]
|
||||
const sourceNodeId = edge.sourceHandle.split('-')[0]
|
||||
|
||||
if (targetNodeId === newComponentNodeData.id) {
|
||||
// Check if targetHandle is in inputParams or inputAnchors
|
||||
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
|
||||
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
|
||||
if (isAgentFlowV2) {
|
||||
if (edge.targetHandle !== newComponentNodeData.id) {
|
||||
removedEdges.push(edge)
|
||||
}
|
||||
} else {
|
||||
// Check if targetHandle is in inputParams or inputAnchors
|
||||
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
|
||||
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
|
||||
|
||||
if (!inputParam && !inputAnchor) {
|
||||
removedEdges.push(edge)
|
||||
if (!inputParam && !inputAnchor) {
|
||||
removedEdges.push(edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceNodeId === newComponentNodeData.id) {
|
||||
if (newComponentNodeData.outputAnchors?.length) {
|
||||
if (isAgentFlowV2) {
|
||||
// AgentFlow v2 doesn't have specific output anchors, connections are directly from node
|
||||
// No need to remove edges for AgentFlow v2 outputs
|
||||
} else if (newComponentNodeData.outputAnchors?.length) {
|
||||
for (const outputAnchor of newComponentNodeData.outputAnchors) {
|
||||
const outputAnchorType = outputAnchor.type
|
||||
if (outputAnchorType === 'options') {
|
||||
@@ -315,6 +375,63 @@ export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
return false
|
||||
}
|
||||
|
||||
export const isValidConnectionAgentflowV2 = (connection, reactFlowInstance) => {
|
||||
const source = connection.source
|
||||
const target = connection.target
|
||||
|
||||
// Prevent self connections
|
||||
if (source === target) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if this connection would create a cycle in the graph
|
||||
if (wouldCreateCycle(source, target, reactFlowInstance)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Function to check if a new connection would create a cycle
|
||||
const wouldCreateCycle = (sourceId, targetId, reactFlowInstance) => {
|
||||
// The most direct cycle check: if target connects back to source
|
||||
if (sourceId === targetId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Build directed graph from existing edges
|
||||
const graph = {}
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
|
||||
// Initialize graph
|
||||
edges.forEach((edge) => {
|
||||
if (!graph[edge.source]) graph[edge.source] = []
|
||||
graph[edge.source].push(edge.target)
|
||||
})
|
||||
|
||||
// Check if there's a path from target to source (which would create a cycle when we add source → target)
|
||||
const visited = new Set()
|
||||
|
||||
function hasPath(current, destination) {
|
||||
if (current === destination) return true
|
||||
if (visited.has(current)) return false
|
||||
|
||||
visited.add(current)
|
||||
|
||||
const neighbors = graph[current] || []
|
||||
for (const neighbor of neighbors) {
|
||||
if (hasPath(neighbor, destination)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// If there's a path from target to source, adding an edge from source to target will create a cycle
|
||||
return hasPath(targetId, sourceId)
|
||||
}
|
||||
|
||||
export const convertDateStringToDateObject = (dateString) => {
|
||||
if (dateString === undefined || !dateString) return undefined
|
||||
|
||||
@@ -367,6 +484,21 @@ export const getFolderName = (base64ArrayStr) => {
|
||||
}
|
||||
}
|
||||
|
||||
const _removeCredentialId = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return obj
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => _removeCredentialId(item))
|
||||
}
|
||||
|
||||
const newObj = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key === 'FLOWISE_CREDENTIAL_ID') continue
|
||||
newObj[key] = _removeCredentialId(value)
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
export const generateExportFlowData = (flowData) => {
|
||||
const nodes = flowData.nodes
|
||||
const edges = flowData.edges
|
||||
@@ -381,6 +513,9 @@ export const generateExportFlowData = (flowData) => {
|
||||
version: node.data.version,
|
||||
name: node.data.name,
|
||||
type: node.data.type,
|
||||
color: node.data.color,
|
||||
hideOutput: node.data.hideOutput,
|
||||
hideInput: node.data.hideInput,
|
||||
baseClasses: node.data.baseClasses,
|
||||
tags: node.data.tags,
|
||||
category: node.data.category,
|
||||
@@ -406,7 +541,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
newNodeData.inputs = nodeDataInputs
|
||||
}
|
||||
|
||||
nodes[i].data = newNodeData
|
||||
nodes[i].data = _removeCredentialId(newNodeData)
|
||||
}
|
||||
const exportJson = {
|
||||
nodes,
|
||||
@@ -415,11 +550,13 @@ export const generateExportFlowData = (flowData) => {
|
||||
return exportJson
|
||||
}
|
||||
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) => {
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle, includesStart = false) => {
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string|json-llmChain_1-llmChain_1-input-promptValues-string"
|
||||
// {source} -{sourceHandle} -{target} -{targetHandle}
|
||||
const parentNodes = []
|
||||
|
||||
const isAgentFlowV2 = nodes.find((nd) => nd.id === target)?.data?.category === 'Agent Flows'
|
||||
|
||||
const isSeqAgent = nodes.find((nd) => nd.id === target)?.data?.category === 'Sequential Agents'
|
||||
|
||||
function collectParentNodes(targetNodeId, nodes, edges) {
|
||||
@@ -442,10 +579,35 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
|
||||
}
|
||||
})
|
||||
}
|
||||
function collectAgentFlowV2ParentNodes(targetNodeId, nodes, edges) {
|
||||
const inputEdges = edges.filter((edg) => edg.target === targetNodeId && edg.targetHandle === targetNodeId)
|
||||
|
||||
// Traverse each edge found
|
||||
inputEdges.forEach((edge) => {
|
||||
const parentNode = nodes.find((nd) => nd.id === edge.source)
|
||||
if (!parentNode) return
|
||||
|
||||
// Recursive call to explore further up the tree
|
||||
collectAgentFlowV2ParentNodes(parentNode.id, nodes, edges)
|
||||
|
||||
// Check and add the parent node to the list if it does not include specific names
|
||||
const excludeNodeNames = ['startAgentflow']
|
||||
if (!excludeNodeNames.includes(parentNode.data.name) || includesStart) {
|
||||
parentNodes.push(parentNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isSeqAgent) {
|
||||
collectParentNodes(target, nodes, edges)
|
||||
return uniq(parentNodes)
|
||||
} else if (isAgentFlowV2) {
|
||||
collectAgentFlowV2ParentNodes(target, nodes, edges)
|
||||
const parentNodeId = nodes.find((nd) => nd.id === target)?.parentNode
|
||||
if (parentNodeId) {
|
||||
collectAgentFlowV2ParentNodes(parentNodeId, nodes, edges)
|
||||
}
|
||||
return uniq(parentNodes)
|
||||
} else {
|
||||
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
|
||||
if (inputEdges && inputEdges.length) {
|
||||
@@ -931,3 +1093,84 @@ export const getCustomConditionOutputs = (value, nodeId, existingEdges, isDataGr
|
||||
|
||||
return { outputAnchors, toBeRemovedEdgeIds }
|
||||
}
|
||||
|
||||
const _showHideOperation = (nodeData, inputParam, displayType, index) => {
|
||||
const displayOptions = inputParam[displayType]
|
||||
/* For example:
|
||||
show: {
|
||||
enableMemory: true
|
||||
}
|
||||
*/
|
||||
Object.keys(displayOptions).forEach((path) => {
|
||||
const comparisonValue = displayOptions[path]
|
||||
if (path.includes('$index')) {
|
||||
path = path.replace('$index', index)
|
||||
}
|
||||
const groundValue = get(nodeData.inputs, path, '')
|
||||
|
||||
if (Array.isArray(comparisonValue)) {
|
||||
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'string') {
|
||||
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'boolean') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'object') {
|
||||
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'number') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const showHideInputs = (nodeData, inputType, overrideParams, arrayIndex) => {
|
||||
const params = overrideParams ?? nodeData[inputType] ?? []
|
||||
|
||||
for (let i = 0; i < params.length; i += 1) {
|
||||
const inputParam = params[i]
|
||||
|
||||
// Reset display flag to false for each inputParam
|
||||
inputParam.display = true
|
||||
|
||||
if (inputParam.show) {
|
||||
_showHideOperation(nodeData, inputParam, 'show', arrayIndex)
|
||||
}
|
||||
if (inputParam.hide) {
|
||||
_showHideOperation(nodeData, inputParam, 'hide', arrayIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export const showHideInputParams = (nodeData) => {
|
||||
return showHideInputs(nodeData, 'inputParams')
|
||||
}
|
||||
|
||||
export const showHideInputAnchors = (nodeData) => {
|
||||
return showHideInputs(nodeData, 'inputAnchors')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user