Fixed new object request node and allowed multiple classes with dot notation
All checks were successful
Build / build (push) Successful in 39s
All checks were successful
Build / build (push) Successful in 39s
This commit is contained in:
@@ -32,9 +32,6 @@ namespace Nodify.Calculator
|
|||||||
// Dynamic Split node: populate outputs when a model connects to the Square input
|
// Dynamic Split node: populate outputs when a model connects to the Square input
|
||||||
if (!IsLoading) HandleSplitNodeConnected(c);
|
if (!IsLoading) HandleSplitNodeConnected(c);
|
||||||
|
|
||||||
// Dynamic New Object node: populate inputs when a model connects
|
|
||||||
if (!IsLoading) HandleNewObjectNodeConnected(c);
|
|
||||||
|
|
||||||
// Dynamic Copy node: adapt connectors when something connects
|
// Dynamic Copy node: adapt connectors when something connects
|
||||||
if (!IsLoading) HandleCopyNodeConnected(c);
|
if (!IsLoading) HandleCopyNodeConnected(c);
|
||||||
|
|
||||||
@@ -73,9 +70,6 @@ namespace Nodify.Calculator
|
|||||||
// Dynamic Split node: clear outputs when model disconnects
|
// Dynamic Split node: clear outputs when model disconnects
|
||||||
HandleSplitNodeDisconnected(c);
|
HandleSplitNodeDisconnected(c);
|
||||||
|
|
||||||
// Dynamic New Object node: clear inputs when model disconnects
|
|
||||||
HandleNewObjectNodeDisconnected(c);
|
|
||||||
|
|
||||||
// Dynamic Copy node: reset on disconnect
|
// Dynamic Copy node: reset on disconnect
|
||||||
HandleCopyNodeDisconnected(c);
|
HandleCopyNodeDisconnected(c);
|
||||||
|
|
||||||
@@ -437,144 +431,6 @@ namespace Nodify.Calculator
|
|||||||
sysVm.Title = "Split";
|
sysVm.Title = "Split";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleNewObjectNodeConnected(ConnectionViewModel c)
|
|
||||||
{
|
|
||||||
// Find the New Object node's Square input connector
|
|
||||||
var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
|
||||||
if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
|
||||||
if (squareInput == null) return;
|
|
||||||
|
|
||||||
var newObjOp = squareInput.Operation;
|
|
||||||
if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// The other end is the source — find which model class it comes from
|
|
||||||
var sourceConnector = (c.Input == squareInput) ? c.Output : c.Input;
|
|
||||||
var sourceOp = sourceConnector.Operation;
|
|
||||||
|
|
||||||
string className = null;
|
|
||||||
if (sourceOp is SystemOperationViewModel srcSys)
|
|
||||||
{
|
|
||||||
var title = srcSys.Title ?? "";
|
|
||||||
if (title.StartsWith("GET ")) className = title.Substring(4).Trim();
|
|
||||||
else if (title.StartsWith("SET ")) className = title.Split(' ').ElementAtOrDefault(1);
|
|
||||||
}
|
|
||||||
else if (sourceOp is APIOperationViewModel apiVm)
|
|
||||||
{
|
|
||||||
className = apiVm.ResponseModelClassName;
|
|
||||||
if (!string.IsNullOrEmpty(className) && className.StartsWith("List<") && className.EndsWith(">"))
|
|
||||||
className = className.Substring(5, className.Length - 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(sourceConnector.DataType))
|
|
||||||
{
|
|
||||||
var dt = sourceConnector.DataType;
|
|
||||||
if (dt.StartsWith("List<") && dt.EndsWith(">"))
|
|
||||||
dt = dt.Substring(5, dt.Length - 6);
|
|
||||||
if (!new[] { "string", "int", "double", "bool", "object" }.Contains(dt.ToLower()))
|
|
||||||
className = dt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(className)) return;
|
|
||||||
|
|
||||||
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
|
||||||
var filePath = Path.Combine(customModelDir, $"{className}.cs");
|
|
||||||
if (!File.Exists(filePath)) return;
|
|
||||||
|
|
||||||
var fileContent = File.ReadAllText(filePath);
|
|
||||||
var properties = OperationFactory.GetPropertiesFromClassPublic(fileContent);
|
|
||||||
if (properties == null || properties.Count == 0) return;
|
|
||||||
|
|
||||||
// Remove existing dynamic inputs (non-triangle, non-square)
|
|
||||||
var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
|
||||||
toRemove.ForEach(i =>
|
|
||||||
{
|
|
||||||
DisconnectConnector(i);
|
|
||||||
newObjOp.Input.Remove(i);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add property inputs
|
|
||||||
foreach (var prop in properties)
|
|
||||||
{
|
|
||||||
var propType = prop.Type;
|
|
||||||
bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
|
|
||||||
var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
|
|
||||||
|
|
||||||
var primTypes = new[] { "string", "int", "double", "bool", "float", "decimal", "long", "object", "datetime" };
|
|
||||||
bool isModel = !primTypes.Contains(innerType.ToLower())
|
|
||||||
&& File.Exists(Path.Combine(customModelDir, $"{innerType}.cs"));
|
|
||||||
|
|
||||||
ConnectorShape shape;
|
|
||||||
System.Drawing.Color color;
|
|
||||||
|
|
||||||
if (isList)
|
|
||||||
{
|
|
||||||
shape = ConnectorShape.Grid;
|
|
||||||
color = System.Drawing.Color.MediumSpringGreen;
|
|
||||||
}
|
|
||||||
else if (isModel)
|
|
||||||
{
|
|
||||||
shape = ConnectorShape.Square;
|
|
||||||
color = System.Drawing.Color.MediumPurple;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
shape = ConnectorShape.Circle;
|
|
||||||
color = ConnectorViewModel.GetColorForType(propType);
|
|
||||||
}
|
|
||||||
|
|
||||||
newObjOp.Input.Add(new ConnectorViewModel
|
|
||||||
{
|
|
||||||
Title = $"{prop.Name} ({propType})",
|
|
||||||
Shape = shape,
|
|
||||||
ConnectorColor = color,
|
|
||||||
DataType = isModel || isList ? propType : propType.ToLower()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the output connector to reflect the model type
|
|
||||||
var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
|
||||||
if (modelOutput != null)
|
|
||||||
{
|
|
||||||
modelOutput.Title = className;
|
|
||||||
modelOutput.DataType = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
sysVm.Title = $"New {className}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleNewObjectNodeDisconnected(ConnectionViewModel c)
|
|
||||||
{
|
|
||||||
var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
|
||||||
if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
|
||||||
if (squareInput == null) return;
|
|
||||||
|
|
||||||
var newObjOp = squareInput.Operation;
|
|
||||||
if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var stillConnected = Connections.Any(con => con.Input == squareInput || con.Output == squareInput);
|
|
||||||
if (stillConnected) return;
|
|
||||||
|
|
||||||
// Remove dynamic property inputs (keep triangle and square connectors)
|
|
||||||
var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
|
||||||
toRemove.ForEach(i =>
|
|
||||||
{
|
|
||||||
DisconnectConnector(i);
|
|
||||||
newObjOp.Input.Remove(i);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset output
|
|
||||||
var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
|
||||||
if (modelOutput != null)
|
|
||||||
{
|
|
||||||
modelOutput.Title = "Object";
|
|
||||||
modelOutput.DataType = "object";
|
|
||||||
}
|
|
||||||
|
|
||||||
sysVm.Title = "New Object";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCopyNodeConnected(ConnectionViewModel c)
|
private void HandleCopyNodeConnected(ConnectionViewModel c)
|
||||||
{
|
{
|
||||||
// Find the COPY node's input connector
|
// Find the COPY node's input connector
|
||||||
|
|||||||
@@ -521,6 +521,25 @@
|
|||||||
</nodify:Node>
|
</nodify:Node>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate DataType="{x:Type local:NewObjectOperationViewModel}">
|
||||||
|
<nodify:Node Header="{Binding Title}"
|
||||||
|
Content="{Binding}"
|
||||||
|
Input="{Binding Input}"
|
||||||
|
Output="{Binding Output}">
|
||||||
|
<nodify:Node.ContentTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type local:NewObjectOperationViewModel}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Model" FontSize="10" Foreground="Gray" Margin="0,0,0,2" />
|
||||||
|
<ComboBox ItemsSource="{Binding AvailableModels}"
|
||||||
|
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
|
||||||
|
MinWidth="140"
|
||||||
|
Margin="0,0,0,2" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</nodify:Node.ContentTemplate>
|
||||||
|
</nodify:Node>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate DataType="{x:Type local:StringConcatOperationViewModel}">
|
<DataTemplate DataType="{x:Type local:StringConcatOperationViewModel}">
|
||||||
<nodify:Node Header="{Binding Title}"
|
<nodify:Node Header="{Binding Title}"
|
||||||
Content="{Binding}"
|
Content="{Binding}"
|
||||||
|
|||||||
@@ -75,41 +75,70 @@ namespace Nodify.Calculator.Execution.Handlers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class NewObjectHandler : INodeExecutionHandler
|
internal sealed class NewObjectExecHandler : INodeExecutionHandler
|
||||||
{
|
{
|
||||||
public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
|
public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
|
||||||
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.NEW_OBJECT;
|
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.NEW_OBJECT;
|
||||||
|
|
||||||
public void Execute(OperationViewModel node, ExecutionContext ctx)
|
public void Execute(OperationViewModel node, ExecutionContext ctx)
|
||||||
{
|
{
|
||||||
ctx.Log($"Constructing new object: {node.Title}");
|
ctx.Log($"[NEW OBJECT] Constructing: {node.Title}");
|
||||||
var jObj = new JObject();
|
var root = new JObject();
|
||||||
|
|
||||||
foreach (var inp in node.Input)
|
foreach (var inp in node.Input)
|
||||||
{
|
{
|
||||||
if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue;
|
if (inp.Shape == ConnectorShape.Triangle) continue;
|
||||||
|
|
||||||
var propName = inp.Title ?? "";
|
// Extract property path from title: "Address.Street (string)" → "Address.Street"
|
||||||
var parenIdx = propName.IndexOf(" (", StringComparison.Ordinal);
|
var title = inp.Title ?? "";
|
||||||
if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
|
var parenIdx = title.IndexOf(" (", StringComparison.Ordinal);
|
||||||
|
var propPath = parenIdx > 0 ? title.Substring(0, parenIdx).Trim() : title.Trim();
|
||||||
|
if (string.IsNullOrEmpty(propPath)) continue;
|
||||||
|
|
||||||
var val = ctx.ReadInput(inp);
|
var val = ctx.ReadInput(inp);
|
||||||
|
ctx.Log($"[NEW OBJECT] {propPath} = {(val?.Length > 80 ? val.Substring(0, 80) + "..." : val ?? "null")}");
|
||||||
|
|
||||||
|
SetNestedValue(root, propPath, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = root.ToString(Formatting.None);
|
||||||
|
ctx.Outputs[node.NodeId] = json;
|
||||||
|
ctx.Variables[node.NodeId] = json;
|
||||||
|
ctx.Log($"[NEW OBJECT] Result: {(json.Length > 200 ? json.Substring(0, 200) + "..." : json)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a value at a dot-notation path in a JObject, creating intermediate objects as needed.
|
||||||
|
/// e.g., "Address.Street" with value "Main St" → { "Address": { "Street": "Main St" } }
|
||||||
|
/// </summary>
|
||||||
|
private static void SetNestedValue(JObject root, string path, string val)
|
||||||
|
{
|
||||||
|
var parts = path.Split('.');
|
||||||
|
var current = root;
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.Length - 1; i++)
|
||||||
|
{
|
||||||
|
if (current[parts[i]] is JObject existing)
|
||||||
|
current = existing;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var child = new JObject();
|
||||||
|
current[parts[i]] = child;
|
||||||
|
current = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var leaf = parts[parts.Length - 1];
|
||||||
if (val != null)
|
if (val != null)
|
||||||
{
|
{
|
||||||
try { jObj[propName] = JToken.Parse(val); }
|
try { current[leaf] = JToken.Parse(val); }
|
||||||
catch { jObj[propName] = val; }
|
catch { current[leaf] = val; }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
jObj[propName] = null;
|
current[leaf] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = jObj.ToString(Formatting.None);
|
|
||||||
ctx.Outputs[node.NodeId] = json;
|
|
||||||
ctx.Variables[node.NodeId] = json;
|
|
||||||
ctx.Log($"Object constructed: {json}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class AssertHandler : INodeExecutionHandler
|
internal sealed class AssertHandler : INodeExecutionHandler
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace Nodify.Calculator
|
|||||||
new AuthHandler(),
|
new AuthHandler(),
|
||||||
new KnotHandler(),
|
new KnotHandler(),
|
||||||
new DebugHandler(),
|
new DebugHandler(),
|
||||||
new NewObjectHandler(),
|
new NewObjectExecHandler(),
|
||||||
new AssertHandler(),
|
new AssertHandler(),
|
||||||
new ForEachHandler(),
|
new ForEachHandler(),
|
||||||
new Execution.Handlers.DeserializeHandler(),
|
new Execution.Handlers.DeserializeHandler(),
|
||||||
|
|||||||
176
Examples/Nodify.Calculator/NewObjectOperationViewModel.cs
Normal file
176
Examples/Nodify.Calculator/NewObjectOperationViewModel.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public class NewObjectOperationViewModel : SystemOperationViewModel
|
||||||
|
{
|
||||||
|
private string _selectedModel = "";
|
||||||
|
|
||||||
|
public ObservableCollection<string> AvailableModels { get; } = new ObservableCollection<string>();
|
||||||
|
|
||||||
|
public string SelectedModel
|
||||||
|
{
|
||||||
|
get => _selectedModel;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedModel, value))
|
||||||
|
{
|
||||||
|
RebuildInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NewObjectOperationViewModel()
|
||||||
|
{
|
||||||
|
Title = "New Object";
|
||||||
|
SystemOperationType = SystemOperations.NEW_OBJECT;
|
||||||
|
RefreshAvailableModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshAvailableModels()
|
||||||
|
{
|
||||||
|
var current = _selectedModel;
|
||||||
|
AvailableModels.Clear();
|
||||||
|
AvailableModels.Add("");
|
||||||
|
|
||||||
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
|
if (Directory.Exists(customModelDir))
|
||||||
|
{
|
||||||
|
var modelFiles = Directory.GetFiles(customModelDir, "*.cs");
|
||||||
|
foreach (var file in modelFiles)
|
||||||
|
{
|
||||||
|
AvailableModels.Add(Path.GetFileNameWithoutExtension(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(current) && AvailableModels.Contains(current))
|
||||||
|
_selectedModel = current;
|
||||||
|
else
|
||||||
|
_selectedModel = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RebuildInputs()
|
||||||
|
{
|
||||||
|
// Keep only Triangle (flow) connectors
|
||||||
|
var toRemove = Input.Where(i => i.Shape != ConnectorShape.Triangle).ToList();
|
||||||
|
foreach (var c in toRemove) Input.Remove(c);
|
||||||
|
|
||||||
|
// Update output connector
|
||||||
|
var modelOutput = Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
||||||
|
if (modelOutput != null)
|
||||||
|
{
|
||||||
|
modelOutput.Title = string.IsNullOrEmpty(_selectedModel) ? "Object" : _selectedModel;
|
||||||
|
modelOutput.DataType = string.IsNullOrEmpty(_selectedModel) ? "object" : _selectedModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_selectedModel))
|
||||||
|
{
|
||||||
|
Title = "New Object";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title = $"New {_selectedModel}";
|
||||||
|
|
||||||
|
// Recursively flatten model properties with dot notation
|
||||||
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var flatProps = new List<System.ValueTuple<string, string, bool, bool>>();
|
||||||
|
FlattenModel(_selectedModel, "", customModelDir, visited, flatProps);
|
||||||
|
|
||||||
|
foreach (var entry in flatProps)
|
||||||
|
{
|
||||||
|
var path = entry.Item1;
|
||||||
|
var type = entry.Item2;
|
||||||
|
var isModel = entry.Item3;
|
||||||
|
var isList = entry.Item4;
|
||||||
|
ConnectorShape shape;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
if (isList)
|
||||||
|
{
|
||||||
|
shape = ConnectorShape.Grid;
|
||||||
|
color = NodeColors.List;
|
||||||
|
}
|
||||||
|
else if (isModel)
|
||||||
|
{
|
||||||
|
// For nested model types we still use Circle with a label since
|
||||||
|
// the user will type/connect primitives via dot notation
|
||||||
|
shape = ConnectorShape.Square;
|
||||||
|
color = NodeColors.Model;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shape = ConnectorShape.Circle;
|
||||||
|
color = ConnectorViewModel.GetColorForType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
Input.Add(new ConnectorViewModel
|
||||||
|
{
|
||||||
|
Title = $"{path} ({type})",
|
||||||
|
Shape = shape,
|
||||||
|
ConnectorColor = color,
|
||||||
|
DataType = isModel || isList ? type : type.ToLower()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively flattens a model's properties. Nested model properties are expanded
|
||||||
|
/// with dot notation (e.g., "Address.Street (string)"). Primitives and lists are leaves.
|
||||||
|
/// </summary>
|
||||||
|
private static void FlattenModel(string modelName, string prefix, string modelDir,
|
||||||
|
HashSet<string> visited, List<System.ValueTuple<string, string, bool, bool>> result)
|
||||||
|
{
|
||||||
|
if (visited.Contains(modelName)) return; // prevent circular references
|
||||||
|
visited.Add(modelName);
|
||||||
|
|
||||||
|
var filePath = Path.Combine(modelDir, $"{modelName}.cs");
|
||||||
|
if (!File.Exists(filePath)) return;
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath);
|
||||||
|
var properties = OperationFactory.GetPropertiesFromClassPublic(content);
|
||||||
|
if (properties == null) return;
|
||||||
|
|
||||||
|
var primTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"string", "int", "double", "bool", "float", "decimal", "long",
|
||||||
|
"object", "datetime", "byte", "short", "char", "guid"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var propType = prop.Type;
|
||||||
|
var fullPath = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
|
||||||
|
|
||||||
|
bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
|
||||||
|
var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
|
||||||
|
|
||||||
|
bool isNestedModel = !primTypes.Contains(innerType)
|
||||||
|
&& File.Exists(Path.Combine(modelDir, $"{innerType}.cs"));
|
||||||
|
|
||||||
|
if (isList)
|
||||||
|
{
|
||||||
|
// List properties are always leaf inputs (Grid connector)
|
||||||
|
result.Add(new System.ValueTuple<string, string, bool, bool>(fullPath, propType, false, true));
|
||||||
|
}
|
||||||
|
else if (isNestedModel)
|
||||||
|
{
|
||||||
|
// Recurse into nested model with dot notation prefix
|
||||||
|
FlattenModel(innerType, fullPath, modelDir, visited, result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Primitive leaf
|
||||||
|
result.Add(new System.ValueTuple<string, string, bool, bool>(fullPath, propType, false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.Remove(modelName); // allow same model in different branches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,7 @@ namespace Nodify.Calculator.NodeHandlers
|
|||||||
TakeOperationViewModel => _handlers.OfType<TakeHandler>().FirstOrDefault(),
|
TakeOperationViewModel => _handlers.OfType<TakeHandler>().FirstOrDefault(),
|
||||||
ForEachOperationViewModel => _handlers.OfType<ForEachHandler>().FirstOrDefault(),
|
ForEachOperationViewModel => _handlers.OfType<ForEachHandler>().FirstOrDefault(),
|
||||||
AssertOperationViewModel => _handlers.OfType<AssertHandler>().FirstOrDefault(),
|
AssertOperationViewModel => _handlers.OfType<AssertHandler>().FirstOrDefault(),
|
||||||
|
NewObjectOperationViewModel => _handlers.OfType<NewObjectHandler>().FirstOrDefault(),
|
||||||
APIOperationViewModel => _handlers.OfType<ApiHandler>().FirstOrDefault(),
|
APIOperationViewModel => _handlers.OfType<ApiHandler>().FirstOrDefault(),
|
||||||
ExpandoOperationViewModel => _handlers.OfType<ExpandoHandler>().FirstOrDefault(),
|
ExpandoOperationViewModel => _handlers.OfType<ExpandoHandler>().FirstOrDefault(),
|
||||||
CalculatorOperationViewModel => _handlers.OfType<NormalHandler>().FirstOrDefault(),
|
CalculatorOperationViewModel => _handlers.OfType<NormalHandler>().FirstOrDefault(),
|
||||||
|
|||||||
@@ -82,33 +82,33 @@ namespace Nodify.Calculator.NodeHandlers
|
|||||||
|
|
||||||
public OperationViewModel Create(OperationInfoViewModel info)
|
public OperationViewModel Create(OperationInfoViewModel info)
|
||||||
{
|
{
|
||||||
var op = new SystemOperationViewModel
|
var op = new NewObjectOperationViewModel();
|
||||||
{
|
|
||||||
Title = info.Title ?? "New Object",
|
|
||||||
SystemOperationType = SystemOperations.NEW_OBJECT
|
|
||||||
};
|
|
||||||
op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||||
op.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
op.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||||
op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple });
|
|
||||||
op.Output.Add(new ConnectorViewModel { Title = "Object", IsInput = false, Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple, DataType = "object" });
|
op.Output.Add(new ConnectorViewModel { Title = "Object", IsInput = false, Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple, DataType = "object" });
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OperationViewModel Restore(NodeData data)
|
public OperationViewModel Restore(NodeData data)
|
||||||
{
|
{
|
||||||
var op = (SystemOperationViewModel)Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.NEW_OBJECT });
|
var op = new NewObjectOperationViewModel();
|
||||||
|
op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||||
|
op.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||||
|
op.Output.Add(new ConnectorViewModel { Title = "Object", IsInput = false, Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple, DataType = "object" });
|
||||||
|
|
||||||
// Restore dynamic property inputs (keep triangle + square, replace the rest)
|
// Restore selected model (triggers RebuildInputs via property setter)
|
||||||
var dynamicInputs = op.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
if (!string.IsNullOrEmpty(data.ClassName))
|
||||||
foreach (var d in dynamicInputs) op.Input.Remove(d);
|
|
||||||
|
|
||||||
foreach (var ic in data.InputConnectors)
|
|
||||||
{
|
{
|
||||||
if (ic.Shape == "Triangle" || ic.Shape == "Square") continue;
|
op.RefreshAvailableModels();
|
||||||
op.Input.Add(NodeHandlerRegistry.DeserializeConnector(ic, true));
|
op.SelectedModel = data.ClassName;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Legacy: restore from saved input connectors
|
||||||
|
var dynamicInputs = data.InputConnectors.Where(ic => ic.Shape != "Triangle").ToList();
|
||||||
|
foreach (var ic in dynamicInputs)
|
||||||
|
op.Input.Add(NodeHandlerRegistry.DeserializeConnector(ic, true));
|
||||||
|
|
||||||
// Restore output connector metadata (Square model output)
|
|
||||||
var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square");
|
var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square");
|
||||||
var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
||||||
if (savedModelOutput != null && modelOutput != null)
|
if (savedModelOutput != null && modelOutput != null)
|
||||||
@@ -117,6 +117,7 @@ namespace Nodify.Calculator.NodeHandlers
|
|||||||
modelOutput.DataType = savedModelOutput.DataType;
|
modelOutput.DataType = savedModelOutput.DataType;
|
||||||
modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb);
|
modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
op.Title = data.Title;
|
op.Title = data.Title;
|
||||||
return op;
|
return op;
|
||||||
@@ -126,6 +127,10 @@ namespace Nodify.Calculator.NodeHandlers
|
|||||||
{
|
{
|
||||||
data.NodeType = "System";
|
data.NodeType = "System";
|
||||||
data.SystemOp = nameof(SystemOperations.NEW_OBJECT);
|
data.SystemOp = nameof(SystemOperations.NEW_OBJECT);
|
||||||
|
if (vm is NewObjectOperationViewModel newObjVm)
|
||||||
|
{
|
||||||
|
data.ClassName = newObjVm.SelectedModel ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user