From a235558db1ff1bf48fd42a435905b8c75b39efb3 Mon Sep 17 00:00:00 2001 From: Ankitkumar Satapara Date: Wed, 29 Apr 2026 11:50:20 +0530 Subject: [PATCH] Fixed new object request node and allowed multiple classes with dot notation --- .../Nodify.Calculator/CalculatorViewModel.cs | 144 -------------- Examples/Nodify.Calculator/EditorView.xaml | 19 ++ .../Execution/Handlers/NodeHandlers.cs | 63 +++++-- Examples/Nodify.Calculator/Executor.cs | 2 +- .../NewObjectOperationViewModel.cs | 176 ++++++++++++++++++ .../NodeHandlers/NodeHandlerRegistry.cs | 1 + .../NodeHandlers/SpecializedNodeHandlers.cs | 49 ++--- 7 files changed, 270 insertions(+), 184 deletions(-) create mode 100644 Examples/Nodify.Calculator/NewObjectOperationViewModel.cs diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 1635664..b429c83 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -32,9 +32,6 @@ namespace Nodify.Calculator // Dynamic Split node: populate outputs when a model connects to the Square input 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 if (!IsLoading) HandleCopyNodeConnected(c); @@ -73,9 +70,6 @@ namespace Nodify.Calculator // Dynamic Split node: clear outputs when model disconnects HandleSplitNodeDisconnected(c); - // Dynamic New Object node: clear inputs when model disconnects - HandleNewObjectNodeDisconnected(c); - // Dynamic Copy node: reset on disconnect HandleCopyNodeDisconnected(c); @@ -437,144 +431,6 @@ namespace Nodify.Calculator 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) { // Find the COPY node's input connector diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index c40acde..e4b68d8 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -521,6 +521,25 @@ + + + + + + + + + + + + + node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.NEW_OBJECT; public void Execute(OperationViewModel node, ExecutionContext ctx) { - ctx.Log($"Constructing new object: {node.Title}"); - var jObj = new JObject(); + ctx.Log($"[NEW OBJECT] Constructing: {node.Title}"); + var root = new JObject(); 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 ?? ""; - var parenIdx = propName.IndexOf(" (", StringComparison.Ordinal); - if (parenIdx > 0) propName = propName.Substring(0, parenIdx); + // Extract property path from title: "Address.Street (string)" → "Address.Street" + var title = inp.Title ?? ""; + 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); - if (val != null) - { - try { jObj[propName] = JToken.Parse(val); } - catch { jObj[propName] = val; } - } + 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)}"); + } + + /// + /// 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" } } + /// + 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 { - jObj[propName] = null; + var child = new JObject(); + current[parts[i]] = child; + current = child; } } - var json = jObj.ToString(Formatting.None); - ctx.Outputs[node.NodeId] = json; - ctx.Variables[node.NodeId] = json; - ctx.Log($"Object constructed: {json}"); + var leaf = parts[parts.Length - 1]; + if (val != null) + { + try { current[leaf] = JToken.Parse(val); } + catch { current[leaf] = val; } + } + else + { + current[leaf] = null; + } } } diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs index 4111544..2efd72a 100644 --- a/Examples/Nodify.Calculator/Executor.cs +++ b/Examples/Nodify.Calculator/Executor.cs @@ -55,7 +55,7 @@ namespace Nodify.Calculator new AuthHandler(), new KnotHandler(), new DebugHandler(), - new NewObjectHandler(), + new NewObjectExecHandler(), new AssertHandler(), new ForEachHandler(), new Execution.Handlers.DeserializeHandler(), diff --git a/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs b/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs new file mode 100644 index 0000000..47bbce8 --- /dev/null +++ b/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs @@ -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 AvailableModels { get; } = new ObservableCollection(); + + 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(StringComparer.OrdinalIgnoreCase); + var flatProps = new List>(); + 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() + }); + } + } + + /// + /// Recursively flattens a model's properties. Nested model properties are expanded + /// with dot notation (e.g., "Address.Street (string)"). Primitives and lists are leaves. + /// + private static void FlattenModel(string modelName, string prefix, string modelDir, + HashSet visited, List> 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(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(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(fullPath, propType, false, false)); + } + } + + visited.Remove(modelName); // allow same model in different branches + } + } +} diff --git a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs index 129c7c8..763fd9d 100644 --- a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs +++ b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs @@ -97,6 +97,7 @@ namespace Nodify.Calculator.NodeHandlers TakeOperationViewModel => _handlers.OfType().FirstOrDefault(), ForEachOperationViewModel => _handlers.OfType().FirstOrDefault(), AssertOperationViewModel => _handlers.OfType().FirstOrDefault(), + NewObjectOperationViewModel => _handlers.OfType().FirstOrDefault(), APIOperationViewModel => _handlers.OfType().FirstOrDefault(), ExpandoOperationViewModel => _handlers.OfType().FirstOrDefault(), CalculatorOperationViewModel => _handlers.OfType().FirstOrDefault(), diff --git a/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs b/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs index 4e1d71d..f252ada 100644 --- a/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs +++ b/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs @@ -82,40 +82,41 @@ namespace Nodify.Calculator.NodeHandlers public OperationViewModel Create(OperationInfoViewModel info) { - var op = new SystemOperationViewModel - { - Title = info.Title ?? "New Object", - SystemOperationType = 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.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" }); return op; } 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) - var dynamicInputs = op.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList(); - foreach (var d in dynamicInputs) op.Input.Remove(d); - - foreach (var ic in data.InputConnectors) + // Restore selected model (triggers RebuildInputs via property setter) + if (!string.IsNullOrEmpty(data.ClassName)) { - if (ic.Shape == "Triangle" || ic.Shape == "Square") continue; - op.Input.Add(NodeHandlerRegistry.DeserializeConnector(ic, true)); + op.RefreshAvailableModels(); + op.SelectedModel = data.ClassName; } - - // Restore output connector metadata (Square model output) - var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square"); - var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square); - if (savedModelOutput != null && modelOutput != null) + else { - modelOutput.Title = savedModelOutput.Title; - modelOutput.DataType = savedModelOutput.DataType; - modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb); + // 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)); + + var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square"); + var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square); + if (savedModelOutput != null && modelOutput != null) + { + modelOutput.Title = savedModelOutput.Title; + modelOutput.DataType = savedModelOutput.DataType; + modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb); + } } op.Title = data.Title; @@ -126,6 +127,10 @@ namespace Nodify.Calculator.NodeHandlers { data.NodeType = "System"; data.SystemOp = nameof(SystemOperations.NEW_OBJECT); + if (vm is NewObjectOperationViewModel newObjVm) + { + data.ClassName = newObjVm.SelectedModel ?? ""; + } } }