diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 05936ed..63c9a46 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -46,6 +46,9 @@ namespace Nodify.Calculator // Dynamic Knot node: adapt shape/color to match connected wire if (!IsLoading) HandleKnotNodeConnected(c); + + // Dynamic Parse Json node: adapt input to match source + if (!IsLoading) HandleParseJsonNodeConnected(c); }) .WhenRemoved(c => { @@ -81,6 +84,9 @@ namespace Nodify.Calculator // Dynamic Knot node: reset on disconnect HandleKnotNodeDisconnected(c); + + // Dynamic Parse Json node: reset input on disconnect + HandleParseJsonNodeDisconnected(c); }); Operations.WhenAdded(x => @@ -854,6 +860,50 @@ namespace Nodify.Calculator DataType = "object" }); } + + private void HandleParseJsonNodeConnected(ConnectionViewModel c) + { + ConnectorViewModel parseInput = null; + ConnectorViewModel sourceConnector = null; + + if (c.Input.IsCopyConnector && c.Input.IsInput && c.Input.Operation is ParseJsonOperationViewModel) + { + parseInput = c.Input; + sourceConnector = c.Output; + } + else if (c.Output.IsCopyConnector && c.Output.IsInput && c.Output.Operation is ParseJsonOperationViewModel) + { + parseInput = c.Output; + sourceConnector = c.Input; + } + + if (parseInput == null || sourceConnector == null) return; + + // Adapt input connector to match the source + parseInput.Shape = sourceConnector.Shape; + parseInput.ConnectorColor = sourceConnector.RawColor; + parseInput.DataType = sourceConnector.DataType; + } + + private void HandleParseJsonNodeDisconnected(ConnectionViewModel c) + { + ConnectorViewModel parseInput = null; + + if (c.Input.IsCopyConnector && c.Input.IsInput && c.Input.Operation is ParseJsonOperationViewModel) + parseInput = c.Input; + else if (c.Output.IsCopyConnector && c.Output.IsInput && c.Output.Operation is ParseJsonOperationViewModel) + parseInput = c.Output; + + if (parseInput == null) return; + + var stillConnected = Connections.Any(con => con.Input == parseInput || con.Output == parseInput); + if (stillConnected) return; + + // Reset to universal + parseInput.Shape = ConnectorShape.Circle; + parseInput.ConnectorColor = System.Drawing.Color.White; + parseInput.DataType = string.Empty; + } } } diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index 9afd566..da80708 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -483,6 +483,25 @@ + + + + + + + + + + + + + node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.PARSEJSON; + => node is ParseJsonOperationViewModel + || (node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.PARSEJSON); public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx) { var input = ResolverUtils.FirstDataInput(node); - return input == null ? null : ctx.Read(input); + if (input == null) return null; + + var raw = ctx.Read(input); + if (string.IsNullOrEmpty(raw)) return null; + + // Ensure we have valid JSON — if not, serialize the raw value + var trimmed = raw.Trim(); + bool isJson = (trimmed.StartsWith("{") && trimmed.EndsWith("}")) + || (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + || (trimmed.StartsWith("\"") && trimmed.EndsWith("\"")); + if (!isJson) + { + try + { + raw = Newtonsoft.Json.JsonConvert.SerializeObject(raw); + } + catch { /* keep raw as-is */ } + } + + // Parse and re-serialize to ensure clean JSON output + try + { + var parsed = Newtonsoft.Json.JsonConvert.DeserializeObject(raw); + return Newtonsoft.Json.JsonConvert.SerializeObject(parsed); + } + catch + { + return raw; + } } } diff --git a/Examples/Nodify.Calculator/Models/SaveGraphModel.cs b/Examples/Nodify.Calculator/Models/SaveGraphModel.cs index 68f1de0..e4a01bd 100644 --- a/Examples/Nodify.Calculator/Models/SaveGraphModel.cs +++ b/Examples/Nodify.Calculator/Models/SaveGraphModel.cs @@ -38,6 +38,9 @@ namespace Nodify.Calculator.Models public string AuthBaseUrl { get; set; } = string.Empty; public string AuthType { get; set; } = string.Empty; + // Parse Json node properties + public string ParseJsonTargetType { get; set; } = string.Empty; + // Take node properties public int NthIndex { get; set; } public bool IsRandom { get; set; } diff --git a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs index 86943f7..ea74eb8 100644 --- a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs +++ b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs @@ -42,6 +42,7 @@ namespace Nodify.Calculator.NodeHandlers _handlers.Add(new NewObjectHandler()); _handlers.Add(new CopyHandler()); _handlers.Add(new SplitHandler()); + _handlers.Add(new ParseJsonHandler()); _handlers.Add(new GetSetVariableHandler()); _handlers.Add(new GetSetModelHandler()); _handlers.Add(new BeginEndHandler()); @@ -81,6 +82,7 @@ namespace Nodify.Calculator.NodeHandlers { KnotOperationViewModel => _handlers.OfType().FirstOrDefault(), StringConcatOperationViewModel => _handlers.OfType().FirstOrDefault(), + ParseJsonOperationViewModel => _handlers.OfType().FirstOrDefault(), FunctionOperationViewModel => _handlers.OfType().FirstOrDefault(), AuthOperationViewModel => _handlers.OfType().FirstOrDefault(), TakeOperationViewModel => _handlers.OfType().FirstOrDefault(), diff --git a/Examples/Nodify.Calculator/NodeHandlers/ParseJsonHandler.cs b/Examples/Nodify.Calculator/NodeHandlers/ParseJsonHandler.cs new file mode 100644 index 0000000..631b2c2 --- /dev/null +++ b/Examples/Nodify.Calculator/NodeHandlers/ParseJsonHandler.cs @@ -0,0 +1,97 @@ +using Nodify.Calculator.Models; +using System.Drawing; + +namespace Nodify.Calculator.NodeHandlers +{ + public class ParseJsonHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.PARSEJSON; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.PARSEJSON); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new ParseJsonOperationViewModel(); + + // Input: universal connector that accepts any shape (Circle, Square, Grid) + op.Input.Add(new ConnectorViewModel + { + Title = "Input", + Shape = ConnectorShape.Circle, + ConnectorColor = Color.White, + IsCopyConnector = true + }); + + // Default output (will be updated when user selects target type) + op.Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + + return op; + } + + public OperationViewModel Restore(NodeData data) + { + var op = new ParseJsonOperationViewModel(); + + // Input: universal connector + op.Input.Add(new ConnectorViewModel + { + Title = "Input", + Shape = ConnectorShape.Circle, + ConnectorColor = Color.White, + IsCopyConnector = true + }); + + // Restore adapted input connector from saved data + if (data.InputConnectors.Count > 0 && data.InputConnectors[0].Shape != "Circle") + { + var saved = data.InputConnectors[0]; + var inp = op.Input[0]; + inp.Shape = NodeHandlerRegistry.ParseShape(saved.Shape); + inp.ConnectorColor = Color.FromArgb(saved.ColorArgb); + inp.DataType = saved.DataType; + } + + // Restore selected target type (this triggers UpdateOutputConnector via property setter) + if (!string.IsNullOrEmpty(data.ParseJsonTargetType)) + { + op.RefreshAvailableTypes(); + op.SelectedTargetType = data.ParseJsonTargetType; + } + else + { + // Default output + op.Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + } + + return op; + } + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.PARSEJSON); + if (vm is ParseJsonOperationViewModel parseVm) + { + data.ParseJsonTargetType = parseVm.SelectedTargetType ?? "object"; + } + } + } +} diff --git a/Examples/Nodify.Calculator/ParseJsonOperationViewModel.cs b/Examples/Nodify.Calculator/ParseJsonOperationViewModel.cs new file mode 100644 index 0000000..9e0cbc3 --- /dev/null +++ b/Examples/Nodify.Calculator/ParseJsonOperationViewModel.cs @@ -0,0 +1,103 @@ +using System.Collections.ObjectModel; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace Nodify.Calculator +{ + public class ParseJsonOperationViewModel : SystemOperationViewModel + { + private string _selectedTargetType = "object"; + + public ObservableCollection AvailableTargetTypes { get; } = new ObservableCollection(); + + public string SelectedTargetType + { + get => _selectedTargetType; + set + { + if (SetProperty(ref _selectedTargetType, value)) + { + UpdateOutputConnector(); + } + } + } + + public ParseJsonOperationViewModel() + { + Title = "Parse Json"; + SystemOperationType = SystemOperations.PARSEJSON; + RefreshAvailableTypes(); + } + + public void RefreshAvailableTypes() + { + var current = _selectedTargetType; + AvailableTargetTypes.Clear(); + AvailableTargetTypes.Add("object"); + + var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels"); + if (Directory.Exists(customModelDir)) + { + var modelFiles = Directory.GetFiles(customModelDir, "*.cs"); + foreach (var file in modelFiles) + { + var name = Path.GetFileNameWithoutExtension(file); + AvailableTargetTypes.Add(name); + AvailableTargetTypes.Add($"List<{name}>"); + } + } + + // Restore selection if it still exists + if (!string.IsNullOrEmpty(current) && AvailableTargetTypes.Contains(current)) + _selectedTargetType = current; + else + _selectedTargetType = "object"; + } + + private void UpdateOutputConnector() + { + // Remove existing non-flow outputs + var toRemove = Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList(); + foreach (var o in toRemove) + Output.Remove(o); + + var targetType = _selectedTargetType ?? "object"; + + if (targetType == "object") + { + Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + } + else if (targetType.StartsWith("List<") && targetType.EndsWith(">")) + { + Output.Add(new ConnectorViewModel + { + Title = targetType, + IsInput = false, + Shape = ConnectorShape.Grid, + ConnectorColor = Color.MediumSpringGreen, + DataType = targetType + }); + } + else + { + // Model type + Output.Add(new ConnectorViewModel + { + Title = targetType, + IsInput = false, + Shape = ConnectorShape.Square, + ConnectorColor = Color.MediumPurple, + DataType = targetType + }); + } + } + } +}