Added custom connectors for array and list
Some checks failed
Build / build (push) Has been cancelled
Some checks failed
Build / build (push) Has been cancelled
This commit is contained in:
24
Examples/Nodify.Calculator/AssertOperationViewModel.cs
Normal file
24
Examples/Nodify.Calculator/AssertOperationViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace Nodify.Calculator
|
||||
Circle,
|
||||
Triangle,
|
||||
Square,
|
||||
Grid,
|
||||
}
|
||||
|
||||
public class ConnectorViewModel : ObservableObject
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
10
Examples/Nodify.Calculator/ForEachOperationViewModel.cs
Normal file
10
Examples/Nodify.Calculator/ForEachOperationViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ForEachOperationViewModel : SystemOperationViewModel
|
||||
{
|
||||
public ForEachOperationViewModel()
|
||||
{
|
||||
SystemOperationType = SystemOperations.FOREACH;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -20,7 +20,9 @@ namespace Nodify.Calculator
|
||||
AUTH,
|
||||
FUNCTION,
|
||||
DEBUG,
|
||||
NEW_OBJECT
|
||||
NEW_OBJECT,
|
||||
FOREACH,
|
||||
ASSERT
|
||||
}
|
||||
|
||||
public class SystemOperationViewModel : OperationViewModel
|
||||
|
||||
Reference in New Issue
Block a user