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