From 21a91ae05e57b8b53e1170cc5acc3242ec0aa50d Mon Sep 17 00:00:00 2001 From: Ankitkumar Satapara Date: Mon, 20 Apr 2026 18:59:24 +0530 Subject: [PATCH] Added custom connectors for array and list --- .../AssertOperationViewModel.cs | 24 ++++ .../Nodify.Calculator/ConnectorViewModel.cs | 1 + Examples/Nodify.Calculator/EditorView.xaml | 78 +++++++++++ Examples/Nodify.Calculator/Executor.cs | 132 ++++++++++++++++++ .../ForEachOperationViewModel.cs | 10 ++ .../Operations/OperationFactory.cs | 100 ++++++++++++- .../SystemOperationViewModel.cs | 4 +- 7 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 Examples/Nodify.Calculator/AssertOperationViewModel.cs create mode 100644 Examples/Nodify.Calculator/ForEachOperationViewModel.cs diff --git a/Examples/Nodify.Calculator/AssertOperationViewModel.cs b/Examples/Nodify.Calculator/AssertOperationViewModel.cs new file mode 100644 index 0000000..64c2444 --- /dev/null +++ b/Examples/Nodify.Calculator/AssertOperationViewModel.cs @@ -0,0 +1,24 @@ +namespace Nodify.Calculator +{ + public class AssertOperationViewModel : SystemOperationViewModel + { + private string _assertType = "Equals"; + public string AssertType + { + get => _assertType; + set => SetProperty(ref _assertType, value); + } + + private bool? _lastResult; + public bool? LastResult + { + get => _lastResult; + set => SetProperty(ref _lastResult, value); + } + + public AssertOperationViewModel() + { + SystemOperationType = SystemOperations.ASSERT; + } + } +} diff --git a/Examples/Nodify.Calculator/ConnectorViewModel.cs b/Examples/Nodify.Calculator/ConnectorViewModel.cs index 8855b0b..c707d97 100644 --- a/Examples/Nodify.Calculator/ConnectorViewModel.cs +++ b/Examples/Nodify.Calculator/ConnectorViewModel.cs @@ -11,6 +11,7 @@ namespace Nodify.Calculator Circle, Triangle, Square, + Grid, } public class ConnectorViewModel : ObservableObject diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index 9e58ae3..558596f 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -129,6 +129,21 @@ + + + + + + + + + + + + + + + @@ -288,6 +312,11 @@ Value="{x:Static local:ConnectorShape.Circle}"> + + + + @@ -551,6 +580,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + c.Input == inp); + string val = ""; + if (inputCon != null) + { + var srcNodeId = inputCon.Output.Operation.NodeId; + if (outputs.TryGetValue(srcNodeId, out var srcVal)) + val = srcVal; + else if (inputCon.Output.Value != null) + val = inputCon.Output.Value.ToString(); + } + else + { + val = inp.Value.ToString(); + } + + if (title.StartsWith("Actual")) actualVal = val; + else if (title.StartsWith("Expected")) expectedVal = val; + } + + bool passed = string.Equals(actualVal?.Trim(), expectedVal?.Trim(), StringComparison.OrdinalIgnoreCase); + outputs[op.NodeId] = passed.ToString(); + + if (passed) + { + OnLogMe?.Invoke($"[ASSERT] ✅ PASSED — Actual: \"{actualVal}\" == Expected: \"{expectedVal}\""); + } + else + { + OnLogMe?.Invoke($"[ASSERT] ❌ FAILED — Actual: \"{actualVal}\" != Expected: \"{expectedVal}\"", logType.Error); + } + return; + } + + // Handle ForEach loop nodes + if (op is ForEachOperationViewModel forEachSysOp) + { + OnLogMe?.Invoke($"[FOREACH] Starting loop: {op.Title}"); + + // Get the list input value + string listJson = ""; + var listInput = op.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle); + if (listInput != null) + { + var listCon = connections.FirstOrDefault(c => c.Input == listInput); + if (listCon != null) + { + var srcNodeId = listCon.Output.Operation.NodeId; + if (outputs.TryGetValue(srcNodeId, out var srcVal)) + listJson = srcVal; + } + } + + if (string.IsNullOrEmpty(listJson)) + { + OnLogMe?.Invoke($"[FOREACH] No list data found. Skipping loop.", logType.Warning); + return; + } + + // Parse the list + Newtonsoft.Json.Linq.JArray array; + try + { + var token = Newtonsoft.Json.Linq.JToken.Parse(listJson); + if (token is Newtonsoft.Json.Linq.JArray arr) + array = arr; + else + { + OnLogMe?.Invoke($"[FOREACH] Input is not an array. Wrapping as single-element array.", logType.Warning); + array = new Newtonsoft.Json.Linq.JArray(token); + } + } + catch + { + OnLogMe?.Invoke($"[FOREACH] Failed to parse input as JSON array.", logType.Error); + return; + } + + // Find the "Loop Body" flow output connection and "Current Item"/"Index" data outputs + var loopBodyOutput = op.Output.FirstOrDefault(o => o.Title == "Loop Body"); + var currentItemOutput = op.Output.FirstOrDefault(o => o.Title == "Current Item"); + var indexOutput = op.Output.FirstOrDefault(o => o.Title == "Index"); + + var loopBodyConnection = loopBodyOutput != null + ? connections.FirstOrDefault(c => c.Output == loopBodyOutput) + : null; + + OnLogMe?.Invoke($"[FOREACH] Iterating over {array.Count} items..."); + + for (int i = 0; i < array.Count; i++) + { + var item = array[i]; + var itemStr = item.ToString(Newtonsoft.Json.Formatting.None); + + OnLogMe?.Invoke($"[FOREACH] Iteration {i}: {(itemStr.Length > 80 ? itemStr.Substring(0, 80) + "..." : itemStr)}"); + + // Set Current Item and Index output values + outputs[op.NodeId] = itemStr; + if (currentItemOutput != null) + currentItemOutput.Value = double.TryParse(itemStr, out var dv) ? dv : 0; + if (indexOutput != null) + indexOutput.Value = i; + + // Execute the loop body chain if connected + if (loopBodyConnection != null) + { + var bodyNode = loopBodyConnection.Input?.Operation; + if (bodyNode != null) + { + var visited = new HashSet(); + TraverseChain(bodyNode, "end", connections, visited, true); + } + } + } + + OnLogMe?.Invoke($"[FOREACH] Loop completed. {array.Count} iterations executed."); + return; + } + // Handle Function nodes — execute the inner flow if (op is FunctionOperationViewModel funcOp) { diff --git a/Examples/Nodify.Calculator/ForEachOperationViewModel.cs b/Examples/Nodify.Calculator/ForEachOperationViewModel.cs new file mode 100644 index 0000000..a229d9e --- /dev/null +++ b/Examples/Nodify.Calculator/ForEachOperationViewModel.cs @@ -0,0 +1,10 @@ +namespace Nodify.Calculator +{ + public class ForEachOperationViewModel : SystemOperationViewModel + { + public ForEachOperationViewModel() + { + SystemOperationType = SystemOperations.FOREACH; + } + } +} diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs index a0725bf..e135ede 100644 --- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs +++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs @@ -117,6 +117,25 @@ namespace Nodify.Calculator }; newObjectNode.Input.Add(""); + var forEachNode = new OperationInfoViewModel() + { + Title = "ForEach", + Type = OperationType.System, + sysOp = SystemOperations.FOREACH, + IsFlowNode = true + }; + forEachNode.Input.Add("List"); + + var assertNode = new OperationInfoViewModel() + { + Title = "Assert", + Type = OperationType.System, + sysOp = SystemOperations.ASSERT, + IsFlowNode = true + }; + assertNode.Input.Add("Actual"); + assertNode.Input.Add("Expected"); + systemNodes.Add(authNode); systemNodes.Add(copynode); systemNodes.Add(debugNode); @@ -127,6 +146,8 @@ namespace Nodify.Calculator systemNodes.Add(jsonParseNode); systemNodes.Add(splitNode); systemNodes.Add(takeNode); + systemNodes.Add(forEachNode); + systemNodes.Add(assertNode); return systemNodes; } @@ -292,13 +313,13 @@ namespace Nodify.Calculator bool isList = responseClassName.StartsWith("List<") && responseClassName.EndsWith(">"); var innerType = isList ? responseClassName.Substring(5, responseClassName.Length - 6) : responseClassName; - // Square connector for model output + // Use Grid shape for list/array types, Square for single model _o.Output.Add(new ConnectorViewModel { Title = responseClassName, IsInput = false, - Shape = ConnectorShape.Square, - ConnectorColor = Color.MediumPurple, + Shape = isList ? ConnectorShape.Grid : ConnectorShape.Square, + ConnectorColor = isList ? Color.MediumSpringGreen : Color.MediumPurple, DataType = responseClassName }); } @@ -370,6 +391,79 @@ namespace Nodify.Calculator return newObjOp; } + if (info.sysOp == SystemOperations.FOREACH) + { + var forEachOp = new ForEachOperationViewModel + { + Title = info.Title + }; + // Flow connectors + forEachOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle }); + forEachOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false }); + // List data input (Grid shape for array/list types) + forEachOp.Input.Add(new ConnectorViewModel + { + Title = "List", + Shape = ConnectorShape.Grid, + ConnectorColor = Color.MediumSpringGreen + }); + // "Loop Body" flow output — connects to nodes executed per iteration + forEachOp.Output.Add(new ConnectorViewModel + { + Title = "Loop Body", + IsInput = false, + Shape = ConnectorShape.Triangle, + ConnectorColor = Color.MediumSpringGreen + }); + // "Current Item" data output — the element of the current iteration + forEachOp.Output.Add(new ConnectorViewModel + { + Title = "Current Item", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.MediumSpringGreen, + DataType = "object" + }); + // "Index" data output — current iteration index + forEachOp.Output.Add(new ConnectorViewModel + { + Title = "Index", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LightSkyBlue, + DataType = "int" + }); + return forEachOp; + } + + if (info.sysOp == SystemOperations.ASSERT) + { + var assertOp = new AssertOperationViewModel + { + Title = info.Title + }; + // Flow connectors + assertOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle }); + assertOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false }); + // Data inputs + foreach (var inp in input) + { + inp.ConnectorColor = Color.Gold; + inp.Shape = ConnectorShape.Circle; + assertOp.Input.Add(inp); + } + // "Result" output — pass/fail boolean + assertOp.Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.Gold, + DataType = "bool" + }); + return assertOp; + } + if (info.sysOp == SystemOperations.DEBUG) { var debugOp = new SystemOperationViewModel diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs index 3c8bee7..fa75c5e 100644 --- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs +++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs @@ -20,7 +20,9 @@ namespace Nodify.Calculator AUTH, FUNCTION, DEBUG, - NEW_OBJECT + NEW_OBJECT, + FOREACH, + ASSERT } public class SystemOperationViewModel : OperationViewModel