Skip to content

Commit

Permalink
Merge pull request #139 from PySpur-Dev/fix/for-loop-input
Browse files Browse the repository at this point in the history
Fix/for loop input
  • Loading branch information
JeanKaddour authored Feb 3, 2025
2 parents 412b70d + 938869f commit 28acc14
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 25 deletions.
48 changes: 28 additions & 20 deletions frontend/src/components/nodes/InputNode.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'
import { FlowWorkflowNode } from '@/types/api_types/nodeTypeSchemas'
import { convertToPythonVariableName } from '@/utils/variableNameUtils'
import { Alert, Button, Input } from '@heroui/react'
import { Icon } from '@iconify/react'
import { Handle, Position } from '@xyflow/react'
import { isEqual } from 'lodash'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import BaseNode from './BaseNode'
import {
setWorkflowInputVariable,
deleteWorkflowInputVariable,
setWorkflowInputVariable,
updateWorkflowInputVariableKey,
} from '../../store/flowSlice'
import { Input, Button, Alert } from '@heroui/react'
import { Icon } from '@iconify/react'
import styles from './InputNode.module.css'
import { RootState } from '../../store/store'
import { isEqual } from 'lodash'
import { FlowWorkflowNode } from '@/types/api_types/nodeTypeSchemas'
import BaseNode from './BaseNode'
import styles from './InputNode.module.css'
import NodeOutputDisplay from './NodeOutputDisplay'
import { convertToPythonVariableName } from '@/utils/variableNameUtils'

interface InputNodeProps {
id: string
Expand All @@ -35,6 +35,7 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
isEqual
)
const nodeConfig = useSelector((state: RootState) => state.flow.nodeConfigs[id])
const isFixedOutput = nodeConfig?.has_fixed_output || false

const outputSchema = nodeConfig?.output_schema || {}
const outputSchemaKeys = Object.keys(outputSchema)
Expand All @@ -57,7 +58,7 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
}, [nodeConfig, outputSchemaKeys])

const handleAddWorkflowInputVariable = useCallback(() => {
if (!newFieldValue.trim()) return
if (!newFieldValue.trim() || isFixedOutput) return
const newKey = convertToPythonVariableName(newFieldValue)

if (newKey !== newFieldValue) {
Expand All @@ -72,18 +73,20 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
})
)
setNewFieldValue('')
}, [dispatch, newFieldValue])
}, [dispatch, newFieldValue, isFixedOutput])

const handleDeleteWorkflowInputVariable = useCallback(
(keyToDelete: string) => {
dispatch(deleteWorkflowInputVariable({ key: keyToDelete }))
if (!isFixedOutput) {
dispatch(deleteWorkflowInputVariable({ key: keyToDelete }))
}
},
[dispatch]
[dispatch, isFixedOutput]
)

const handleWorkflowInputVariableKeyEdit = useCallback(
(oldKey: string, newKey: string) => {
if (oldKey === newKey || !newKey.trim()) {
if (isFixedOutput || oldKey === newKey || !newKey.trim()) {
setEditingField(null)
return
}
Expand All @@ -97,7 +100,7 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
dispatch(updateWorkflowInputVariableKey({ oldKey, newKey: validKey }))
setEditingField(null)
},
[dispatch]
[dispatch, isFixedOutput]
)

const InputHandleRow: React.FC<{ keyName: string }> = ({ keyName }) => {
Expand Down Expand Up @@ -168,7 +171,7 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
<td className={styles.handleLabelCell}>
{!isCollapsed && (
<div className="flex items-center gap-2">
{editingField === key && !readOnly ? (
{editingField === key && !readOnly && !isFixedOutput ? (
<Input
autoFocus
defaultValue={key}
Expand Down Expand Up @@ -210,12 +213,16 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr
<div className="flex flex-col w-full gap-1">
<div className="flex items-center justify-between">
<span
className={`${styles.handleLabel} text-sm font-medium ${!readOnly ? 'cursor-pointer hover:text-primary' : ''}`}
onClick={() => !readOnly && setEditingField(key)}
className={`${styles.handleLabel} text-sm font-medium ${!readOnly && !isFixedOutput ? 'cursor-pointer hover:text-primary' : ''}`}
onClick={() =>
!readOnly &&
!isFixedOutput &&
setEditingField(key)
}
>
{key}
</span>
{!readOnly && (
{!readOnly && !isFixedOutput && (
<Button
isIconOnly
size="sm"
Expand Down Expand Up @@ -259,7 +266,8 @@ const InputNode: React.FC<InputNodeProps> = ({ id, data, readOnly = false, ...pr

const renderAddField = () =>
!isCollapsed &&
!readOnly && (
!readOnly &&
!isFixedOutput && (
<div className="flex items-center gap-2 px-4 py-2">
<Input
placeholder="Enter new field name"
Expand Down
47 changes: 46 additions & 1 deletion frontend/src/components/nodes/loops/DynamicGroupNode.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { updateNodeConfigOnly } from '@/store/flowSlice'
import { RootState } from '@/store/store'
import { Divider } from '@heroui/react'
import {
Expand All @@ -11,7 +12,7 @@ import {
} from '@xyflow/react'
import isEqual from 'lodash/isEqual'
import React, { memo, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import BaseNode from '../BaseNode'
import styles from '../DynamicNode.module.css'
import { getRelativeNodesBounds } from './groupNodeUtils'
Expand All @@ -22,12 +23,14 @@ export interface DynamicGroupNodeProps {

const DynamicGroupNode: React.FC<DynamicGroupNodeProps> = ({ id }) => {
const [isCollapsed, setIsCollapsed] = useState(false)
const dispatch = useDispatch()

// Select node data and associated config (if any)
const node = useSelector((state: RootState) => state.flow.nodes.find((n) => n.id === id))
const nodeConfig = useSelector((state: RootState) => state.flow.nodeConfigs[id])
const nodes = useSelector((state: RootState) => state.flow.nodes)
const edges = useSelector((state: RootState) => state.flow.edges, isEqual)
const nodeConfigs = useSelector((state: RootState) => state.flow.nodeConfigs)

const updateNodeInternals = useUpdateNodeInternals()

Expand Down Expand Up @@ -97,6 +100,48 @@ const DynamicGroupNode: React.FC<DynamicGroupNodeProps> = ({ id }) => {
}
}, [finalPredecessors, predecessorNodes, id, updateNodeInternals])

// Keep input node's output schema in sync with parent's input_map
useEffect(() => {
// Find the input node by type and parent relationship
const inputNode = nodes.find((n) => n.type === 'InputNode' && n.parentId === id)
if (inputNode && nodeConfig?.input_map) {
// Build output schema by looking up the actual types from source nodes
const derivedSchema: Record<string, string> = {}

Object.entries(nodeConfig.input_map).forEach(([key, sourceField]) => {
// sourceField should be in format "node-title.field-name"
const [sourceNodeTitle, fieldName] = String(sourceField).split('.')
if (sourceNodeTitle && fieldName) {
// Find the source node by its title
const sourceNode = nodes.find((n) => n.data?.title === sourceNodeTitle)
if (sourceNode) {
// Get the source node's output schema
const sourceNodeConfig = nodeConfigs[sourceNode.id]
const sourceSchema = sourceNodeConfig?.output_schema
if (sourceSchema && fieldName in sourceSchema) {
// Use the type from the source node's output schema
derivedSchema[key] = sourceSchema[fieldName]
}
}
}
})

// Only update if the schema has actually changed
const inputNodeConfig = nodeConfigs[inputNode.id]
if (!isEqual(inputNodeConfig?.output_schema, derivedSchema)) {
dispatch(
updateNodeConfigOnly({
id: inputNode.id,
data: {
output_schema: derivedSchema,
has_fixed_output: true,
},
})
)
}
}
}, [id, nodeConfig?.input_map, nodes, nodeConfigs, dispatch])

// Handlers for Input and Output handle rows
interface HandleRowProps {
id: string
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/components/nodes/loops/groupNodeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { type Node, type NodeOrigin, type Rect, Box, Edge } from '@xyflow/react'
// @todo import from @xyflow/react when fixed
import { boxToRect, getNodePositionWithOrigin, rectToBox } from '@xyflow/system'
import { Dispatch } from '@reduxjs/toolkit'
import { boxToRect, getNodePositionWithOrigin, rectToBox } from '@xyflow/system'
import { updateNodeParentAndCoordinates } from '../../../store/flowSlice'
// Add MouseEvent from React
import { MouseEvent as ReactMouseEvent } from 'react'
import { addNodeWithConfig } from '@/store/flowSlice'
import { FlowWorkflowNodeTypesByCategory } from '@/store/nodeTypesSlice'
import { createNode } from '@/utils/nodeFactory'
import { AppDispatch } from '@/store/store'
import { addNodeWithConfig } from '@/store/flowSlice'
import { createNode } from '@/utils/nodeFactory'
import { MouseEvent as ReactMouseEvent } from 'react'

export const GROUP_NODE_TYPES = ['ForLoopNode']

Expand Down Expand Up @@ -237,6 +237,16 @@ export const createDynamicGroupNodeWithChildren = (
loopNodeAndConfig.node.id
)

// Set input node's output schema to be fixed but empty initially
// It will be populated reactively based on the parent's input_map
if (inputNodeAndConfig) {
inputNodeAndConfig.config = {
...inputNodeAndConfig.config,
has_fixed_output: true, // Make output schema non-editable
output_schema: {}, // Empty initially, will be populated reactively
}
}

// Dispatch all nodes
dispatch(addNodeWithConfig(loopNodeAndConfig))
dispatch(addNodeWithConfig(inputNodeAndConfig))
Expand Down

0 comments on commit 28acc14

Please sign in to comment.