Added custom connectors for array and list
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Ankitkumar Satapara
2026-04-20 18:59:24 +05:30
parent 62e1b5b0f4
commit 21a91ae05e
7 changed files with 345 additions and 4 deletions

View File

@@ -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;
}
}
}

View File

@@ -11,6 +11,7 @@ namespace Nodify.Calculator
Circle,
Triangle,
Square,
Grid,
}
public class ConnectorViewModel : ObservableObject

View File

@@ -129,6 +129,21 @@
</Style>
<SolidColorBrush x:Key="SquareConnectorColor" Color="MediumSlateBlue"></SolidColorBrush>
<SolidColorBrush x:Key="TriangleConnectorColor" Color="White"></SolidColorBrush>
<SolidColorBrush x:Key="GridConnectorColor" Color="#4CAF50"></SolidColorBrush>
<ControlTemplate x:Key="GridConnector" TargetType="Control">
<Grid Width="14" Height="14">
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="1,1,0,0" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,1,0,0" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,1,1,0" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,0,0,0" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,1,0" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="1,0,0,1" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,1" />
<Ellipse Width="3" Height="3" Fill="{TemplateBinding BorderBrush}" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,1,1" />
</Grid>
</ControlTemplate>
<Style x:Key="ConnectionStyle" TargetType="{x:Type nodify:BaseConnection}">
<Style.Triggers>
<DataTrigger Binding="{Binding Input.Shape}"
@@ -139,6 +154,10 @@
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="Stroke" Value="{StaticResource TriangleConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Input.Shape}"
Value="{x:Static local:ConnectorShape.Grid}">
<Setter Property="Stroke" Value="{StaticResource GridConnectorColor}"></Setter>
</DataTrigger>
</Style.Triggers>
<Setter Property="Stroke" Value="{DynamicResource Connection.StrokeBrush}"></Setter>
<Setter Property="Cursor" Value="Hand"></Setter>
@@ -240,6 +259,11 @@
Value="{x:Static local:ConnectorShape.Circle}">
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Grid}">
<Setter Property="ConnectorTemplate" Value="{StaticResource GridConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource GridConnectorColor}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
@@ -288,6 +312,11 @@
Value="{x:Static local:ConnectorShape.Circle}">
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Grid}">
<Setter Property="ConnectorTemplate" Value="{StaticResource GridConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource GridConnectorColor}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
@@ -551,6 +580,55 @@
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ForEachOperationViewModel}">
<nodify:Node Input="{Binding Input}"
Output="{Binding Output}"
BorderBrush="#4CAF50"
BorderThickness="2">
<nodify:Node.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="🔁"
FontSize="14"
VerticalAlignment="Center"
Margin="0 0 6 0" />
<TextBlock Text="{Binding Title}"
VerticalAlignment="Center"
FontWeight="SemiBold" />
</StackPanel>
</nodify:Node.Header>
<TextBlock Text="Iterates over each item in a list"
FontSize="10"
Foreground="#4CAF50"
Opacity="0.8"
Margin="4 2" />
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:AssertOperationViewModel}">
<nodify:Node Input="{Binding Input}"
Output="{Binding Output}"
BorderBrush="#FF5722"
BorderThickness="2">
<nodify:Node.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="✅"
FontSize="14"
VerticalAlignment="Center"
Margin="0 0 6 0" />
<TextBlock Text="{Binding Title}"
VerticalAlignment="Center"
FontWeight="SemiBold" />
</StackPanel>
</nodify:Node.Header>
<StackPanel Margin="4 2">
<TextBlock Text="Compares Actual vs Expected"
FontSize="10"
Foreground="#FF5722"
Opacity="0.8" />
</StackPanel>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:TakeOperationViewModel}">
<nodify:Node Header="{Binding Title}"
Content="{Binding}"

View File

@@ -502,6 +502,138 @@ namespace Nodify.Calculator
return;
}
// Handle Assert nodes
if (op is AssertOperationViewModel assertSysOp)
{
OnLogMe?.Invoke($"[ASSERT] Evaluating assertion: {op.Title}");
string actualVal = "";
string expectedVal = "";
// Find "Actual" and "Expected" input values
foreach (var inp in op.Input)
{
if (inp.Shape == ConnectorShape.Triangle) continue;
var title = inp.Title?.Trim() ?? "";
// Check if there's a connection feeding this input
var inputCon = connections.FirstOrDefault(c => 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<string>();
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)
{

View File

@@ -0,0 +1,10 @@
namespace Nodify.Calculator
{
public class ForEachOperationViewModel : SystemOperationViewModel
{
public ForEachOperationViewModel()
{
SystemOperationType = SystemOperations.FOREACH;
}
}
}

View File

@@ -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

View File

@@ -20,7 +20,9 @@ namespace Nodify.Calculator
AUTH,
FUNCTION,
DEBUG,
NEW_OBJECT
NEW_OBJECT,
FOREACH,
ASSERT
}
public class SystemOperationViewModel : OperationViewModel