diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 239ffd6..1635664 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -49,6 +49,9 @@ namespace Nodify.Calculator // Dynamic Parse Json node: adapt input to match source if (!IsLoading) HandleParseJsonNodeConnected(c); + + // Dynamic List nodes: adapt connectors based on connected list type + if (!IsLoading) HandleListNodeConnected(c); }) .WhenRemoved(c => { @@ -87,6 +90,9 @@ namespace Nodify.Calculator // Dynamic Parse Json node: reset input on disconnect HandleParseJsonNodeDisconnected(c); + + // Dynamic List nodes: reset on disconnect + HandleListNodeDisconnected(c); }); Operations.WhenAdded(x => @@ -211,7 +217,16 @@ namespace Nodify.Calculator // If either side has no DataType set, allow connection (generic/untyped connector) if (string.IsNullOrEmpty(sourceType) || string.IsNullOrEmpty(targetType)) return true; - return string.Equals(sourceType, targetType, System.StringComparison.OrdinalIgnoreCase); + if (string.Equals(sourceType, targetType, System.StringComparison.OrdinalIgnoreCase)) + return true; + // Generic "list" is compatible with any List or array type + var s = sourceType.ToLower(); + var t = targetType.ToLower(); + if (s == "list" && (t.StartsWith("list<") || t.EndsWith("[]"))) + return true; + if (t == "list" && (s.StartsWith("list<") || s.EndsWith("[]"))) + return true; + return false; } private static bool IsTakeListBlockedConnection(ConnectorViewModel source, ConnectorViewModel target) @@ -957,6 +972,178 @@ namespace Nodify.Calculator parseInput.ConnectorColor = System.Drawing.Color.White; parseInput.DataType = string.Empty; } + + private static readonly SystemOperations[] _listOps = new[] + { + SystemOperations.LIST_ADD, SystemOperations.LIST_REMOVE, SystemOperations.LIST_UPDATE, + SystemOperations.LIST_GET, SystemOperations.LIST_COUNT, SystemOperations.LIST_CONTAINS, + SystemOperations.LIST_CLEAR + }; + + private static bool IsListNode(OperationViewModel op, out SystemOperationViewModel sysVm) + { + sysVm = op as SystemOperationViewModel; + return sysVm != null && _listOps.Contains(sysVm.SystemOperationType); + } + + private void HandleListNodeConnected(ConnectionViewModel c) + { + // Find the list node's Grid (List) input that was just connected + ConnectorViewModel listInput = null; + ConnectorViewModel sourceConnector = null; + + if (c.Input.Shape == ConnectorShape.Grid && c.Input.IsInput && IsListNode(c.Input.Operation, out _)) + { + listInput = c.Input; + sourceConnector = c.Output; + } + else if (c.Output.Shape == ConnectorShape.Grid && c.Output.IsInput && IsListNode(c.Output.Operation, out _)) + { + listInput = c.Output; + sourceConnector = c.Input; + } + + if (listInput == null || sourceConnector == null) return; + + if (!IsListNode(listInput.Operation, out var sysVm)) return; + + // Determine the full list type and element type from the source + var sourceDataType = sourceConnector.DataType ?? ""; + string elementType; + string listType; + if (sourceDataType.StartsWith("List<") && sourceDataType.EndsWith(">")) + { + elementType = sourceDataType.Substring(5, sourceDataType.Length - 6); + listType = sourceDataType; + } + else + { + elementType = "object"; + listType = sourceDataType; + } + + // Adapt the List input connector + listInput.Title = $"List ({listType})"; + listInput.ConnectorColor = sourceConnector.RawColor; + listInput.DataType = sourceDataType; + + // Adapt all List output connectors (Grid shape) + foreach (var outConn in listInput.Operation.Output.Where(o => o.Shape == ConnectorShape.Grid)) + { + outConn.Title = listType; + outConn.ConnectorColor = sourceConnector.RawColor; + outConn.DataType = sourceDataType; + } + + // Determine element connector properties + var primitiveTypes = new[] { "string", "int", "double", "bool", "float", "decimal", "long", "object", "datetime" }; + bool isModel = !primitiveTypes.Contains(elementType.ToLower()) + && File.Exists(Path.Combine(ProjectManager.ProjectDirectory, "CustomModels", $"{elementType}.cs")); + + var elemShape = isModel ? ConnectorShape.Square : ConnectorShape.Circle; + var elemColor = isModel ? System.Drawing.Color.FromArgb(255, 150, 80, 220) : ConnectorViewModel.GetColorForType(elementType); + + // Adapt Item input connectors (IsCopyConnector ones) for Add/Update/Contains + foreach (var itemConn in listInput.Operation.Input.Where(i => i.IsCopyConnector)) + { + itemConn.Title = $"Item ({elementType})"; + itemConn.Shape = elemShape; + itemConn.ConnectorColor = elemColor; + itemConn.DataType = elementType; + } + + // Adapt Item output connector for List Get + if (sysVm.SystemOperationType == SystemOperations.LIST_GET) + { + var itemOut = listInput.Operation.Output.FirstOrDefault(o => o.Shape != ConnectorShape.Triangle && o.Shape != ConnectorShape.Grid); + if (itemOut != null) + { + itemOut.Title = elementType; + itemOut.Shape = elemShape; + itemOut.ConnectorColor = elemColor; + itemOut.DataType = elementType; + } + } + + // Update node title + var baseTitle = sysVm.SystemOperationType switch + { + SystemOperations.LIST_ADD => "List Add", + SystemOperations.LIST_REMOVE => "List Remove", + SystemOperations.LIST_UPDATE => "List Update", + SystemOperations.LIST_GET => "List Get", + SystemOperations.LIST_COUNT => "List Count", + SystemOperations.LIST_CONTAINS => "List Contains", + SystemOperations.LIST_CLEAR => "List Clear", + _ => "List" + }; + sysVm.Title = $"{baseTitle} <{elementType}>"; + } + + private void HandleListNodeDisconnected(ConnectionViewModel c) + { + ConnectorViewModel listInput = null; + + if (c.Input.Shape == ConnectorShape.Grid && c.Input.IsInput && IsListNode(c.Input.Operation, out _)) + listInput = c.Input; + else if (c.Output.Shape == ConnectorShape.Grid && c.Output.IsInput && IsListNode(c.Output.Operation, out _)) + listInput = c.Output; + + if (listInput == null) return; + + var stillConnected = Connections.Any(con => con.Input == listInput || con.Output == listInput); + if (stillConnected) return; + + if (!IsListNode(listInput.Operation, out var sysVm)) return; + + // Reset List input + listInput.Title = "List"; + listInput.ConnectorColor = NodeColors.List; + listInput.DataType = "list"; + + // Reset List outputs + foreach (var outConn in listInput.Operation.Output.Where(o => o.Shape == ConnectorShape.Grid)) + { + outConn.Title = "List"; + outConn.ConnectorColor = NodeColors.List; + outConn.DataType = "list"; + } + + // Reset Item input connectors + foreach (var itemConn in listInput.Operation.Input.Where(i => i.IsCopyConnector)) + { + itemConn.Title = "Item"; + itemConn.Shape = ConnectorShape.Circle; + itemConn.ConnectorColor = NodeColors.Universal; + itemConn.DataType = string.Empty; + } + + // Reset Item output for List Get + if (sysVm.SystemOperationType == SystemOperations.LIST_GET) + { + var itemOut = listInput.Operation.Output.FirstOrDefault(o => o.Shape != ConnectorShape.Triangle && o.Shape != ConnectorShape.Grid); + if (itemOut != null) + { + itemOut.Title = "Item"; + itemOut.Shape = ConnectorShape.Circle; + itemOut.ConnectorColor = NodeColors.Object; + itemOut.DataType = "object"; + } + } + + // Reset title + sysVm.Title = sysVm.SystemOperationType switch + { + SystemOperations.LIST_ADD => "List Add", + SystemOperations.LIST_REMOVE => "List Remove", + SystemOperations.LIST_UPDATE => "List Update", + SystemOperations.LIST_GET => "List Get", + SystemOperations.LIST_COUNT => "List Count", + SystemOperations.LIST_CONTAINS => "List Contains", + SystemOperations.LIST_CLEAR => "List Clear", + _ => "List" + }; + } } } diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index da80708..d1a833c 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -839,6 +839,34 @@ + + + + + + + + + + + + + + diff --git a/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs b/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs index b480708..0aae73a 100644 --- a/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs +++ b/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs @@ -340,6 +340,108 @@ namespace Nodify.Calculator.Execution.Handlers } } + // ───────────────────────────────────────────────────────────────────────────── + // List operation execution handlers + // ───────────────────────────────────────────────────────────────────────────── + + internal sealed class ListAddExecutionHandler : INodeExecutionHandler + { + public bool CanHandle(OperationViewModel node, ExecutionContext ctx) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_ADD; + + public void Execute(OperationViewModel node, ExecutionContext ctx) + { + var listJson = ListExecHelper.ReadInput(node, ctx, "List"); + var itemJson = ListExecHelper.ReadInput(node, ctx, "Item"); + var list = ListExecHelper.ParseList(listJson); + list.Add(itemJson != null ? JToken.Parse(itemJson) : JValue.CreateNull()); + var result = list.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Variables[node.NodeId] = result; + ctx.Log($"[List Add] Added item. Count={list.Count}"); + } + } + + internal sealed class ListRemoveExecutionHandler : INodeExecutionHandler + { + public bool CanHandle(OperationViewModel node, ExecutionContext ctx) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_REMOVE; + + public void Execute(OperationViewModel node, ExecutionContext ctx) + { + var listJson = ListExecHelper.ReadInput(node, ctx, "List"); + var indexStr = ListExecHelper.ReadInput(node, ctx, "Index"); + var list = ListExecHelper.ParseList(listJson); + int index = int.TryParse(indexStr, out var i) ? i : -1; + if (index >= 0 && index < list.Count) + list.RemoveAt(index); + else + ctx.Log($"[List Remove] Index {index} out of range (Count={list.Count})", logType.Warning); + var result = list.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Variables[node.NodeId] = result; + } + } + + internal sealed class ListUpdateExecutionHandler : INodeExecutionHandler + { + public bool CanHandle(OperationViewModel node, ExecutionContext ctx) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_UPDATE; + + public void Execute(OperationViewModel node, ExecutionContext ctx) + { + var listJson = ListExecHelper.ReadInput(node, ctx, "List"); + var indexStr = ListExecHelper.ReadInput(node, ctx, "Index"); + var itemJson = ListExecHelper.ReadInput(node, ctx, "Item"); + var list = ListExecHelper.ParseList(listJson); + int index = int.TryParse(indexStr, out var i) ? i : -1; + if (index >= 0 && index < list.Count) + list[index] = itemJson != null ? JToken.Parse(itemJson) : JValue.CreateNull(); + else + ctx.Log($"[List Update] Index {index} out of range (Count={list.Count})", logType.Warning); + var result = list.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Variables[node.NodeId] = result; + } + } + + internal sealed class ListClearExecutionHandler : INodeExecutionHandler + { + public bool CanHandle(OperationViewModel node, ExecutionContext ctx) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_CLEAR; + + public void Execute(OperationViewModel node, ExecutionContext ctx) + { + var result = "[]"; + ctx.Outputs[node.NodeId] = result; + ctx.Variables[node.NodeId] = result; + ctx.Log("[List Clear] Cleared list"); + } + } + + /// Helper for reading list node inputs and parsing JSON arrays. + internal static class ListExecHelper + { + public static string ReadInput(OperationViewModel node, ExecutionContext ctx, string title) + { + var conn = ctx.Connections.FirstOrDefault(c => + c.Input.Operation == node + && (c.Input.Title ?? "").StartsWith(title, StringComparison.OrdinalIgnoreCase) + && c.Input.Shape != ConnectorShape.Triangle); + if (conn == null) return null; + if (ctx.Outputs.TryGetValue(conn.OutputNodeId, out var val)) + return val; + return null; + } + + public static JArray ParseList(string json) + { + if (string.IsNullOrEmpty(json)) return new JArray(); + try { return JArray.Parse(json); } + catch { return new JArray(); } + } + } + // ───────────────────────────────────────────────────────────────────────────── // Default / fallback: API call // ───────────────────────────────────────────────────────────────────────────── diff --git a/Examples/Nodify.Calculator/Execution/Resolvers/NodeValueResolvers.cs b/Examples/Nodify.Calculator/Execution/Resolvers/NodeValueResolvers.cs index 733e0a6..b757b85 100644 --- a/Examples/Nodify.Calculator/Execution/Resolvers/NodeValueResolvers.cs +++ b/Examples/Nodify.Calculator/Execution/Resolvers/NodeValueResolvers.cs @@ -205,4 +205,88 @@ namespace Nodify.Calculator.Execution.Resolvers return null; } } + + // ───────────────────────────────────────────────────────────────────────────── + // LIST GET: pick an element by index from a JSON array (non-flow value node). + // ───────────────────────────────────────────────────────────────────────────── + internal sealed class ListGetValueResolver : INodeValueResolver + { + public bool CanResolve(OperationViewModel node, ConnectorViewModel c) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_GET; + + public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx) + { + var listInput = node.Input.FirstOrDefault(i => i.Shape == ConnectorShape.Grid); + var indexInput = node.Input.FirstOrDefault(i => (i.Title ?? "").StartsWith("Index", StringComparison.OrdinalIgnoreCase)); + if (listInput == null) return null; + + var json = ctx.Read(listInput); + var indexStr = indexInput != null ? ctx.Read(indexInput) : "0"; + int index = int.TryParse(indexStr, out var i) ? i : 0; + + try + { + var arr = JArray.Parse(json ?? "[]"); + if (index >= 0 && index < arr.Count) + { + var item = arr[index]; + return item.Type == JTokenType.String ? item.ToString() : item.ToString(Formatting.None); + } + } + catch { } + return null; + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // LIST COUNT: returns the length of a JSON array as a string integer. + // ───────────────────────────────────────────────────────────────────────────── + internal sealed class ListCountValueResolver : INodeValueResolver + { + public bool CanResolve(OperationViewModel node, ConnectorViewModel c) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_COUNT; + + public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx) + { + var listInput = node.Input.FirstOrDefault(i => i.Shape == ConnectorShape.Grid); + if (listInput == null) return "0"; + + var json = ctx.Read(listInput); + try + { + var arr = JArray.Parse(json ?? "[]"); + return arr.Count.ToString(); + } + catch { return "0"; } + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // LIST CONTAINS: checks if item exists in array, returns "true"/"false". + // ───────────────────────────────────────────────────────────────────────────── + internal sealed class ListContainsValueResolver : INodeValueResolver + { + public bool CanResolve(OperationViewModel node, ConnectorViewModel c) + => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.LIST_CONTAINS; + + public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx) + { + var listInput = node.Input.FirstOrDefault(i => i.Shape == ConnectorShape.Grid); + var itemInput = node.Input.FirstOrDefault(i => i.IsCopyConnector); + if (listInput == null) return "false"; + + var json = ctx.Read(listInput); + var itemJson = itemInput != null ? ctx.Read(itemInput) : null; + + try + { + var arr = JArray.Parse(json ?? "[]"); + if (string.IsNullOrEmpty(itemJson)) return "false"; + var searchToken = JToken.Parse(itemJson); + bool found = arr.Any(t => JToken.DeepEquals(t, searchToken)); + return found.ToString().ToLower(); + } + catch { return "false"; } + } + } } diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs index 0402bdf..1385f24 100644 --- a/Examples/Nodify.Calculator/Executor.cs +++ b/Examples/Nodify.Calculator/Executor.cs @@ -59,6 +59,10 @@ namespace Nodify.Calculator new AssertHandler(), new ForEachHandler(), new FunctionHandler(), + new ListAddExecutionHandler(), + new ListRemoveExecutionHandler(), + new ListUpdateExecutionHandler(), + new ListClearExecutionHandler(), new ApiRequestHandler(), // default / fallback }; @@ -71,6 +75,9 @@ namespace Nodify.Calculator new KnotNodeValueResolver(), new TakeNodeValueResolver(), new ParseJsonNodeValueResolver(), + new ListGetValueResolver(), + new ListCountValueResolver(), + new ListContainsValueResolver(), new GetVariableNodeValueResolver(), }; diff --git a/Examples/Nodify.Calculator/NodeHandlers/ListNodeHandlers.cs b/Examples/Nodify.Calculator/NodeHandlers/ListNodeHandlers.cs new file mode 100644 index 0000000..d21f061 --- /dev/null +++ b/Examples/Nodify.Calculator/NodeHandlers/ListNodeHandlers.cs @@ -0,0 +1,251 @@ +using Nodify.Calculator.Models; +using System.Drawing; +using System.Linq; + +namespace Nodify.Calculator.NodeHandlers +{ + // ─── LIST ADD ─── + public class ListAddHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_ADD; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_ADD); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Add", + SystemOperationType = SystemOperations.LIST_ADD + }; + 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 = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Input.Add(new ConnectorViewModel { Title = "Item", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Universal, IsCopyConnector = true }); + op.Output.Add(new ConnectorViewModel { Title = "List", IsInput = false, Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_ADD }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_ADD); + } + } + + // ─── LIST REMOVE ─── + public class ListRemoveHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_REMOVE; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_REMOVE); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Remove", + SystemOperationType = SystemOperations.LIST_REMOVE + }; + 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 = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Input.Add(new ConnectorViewModel { Title = "Index", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Integer, DataType = "int" }); + op.Output.Add(new ConnectorViewModel { Title = "List", IsInput = false, Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_REMOVE }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_REMOVE); + } + } + + // ─── LIST UPDATE ─── + public class ListUpdateHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_UPDATE; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_UPDATE); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Update", + SystemOperationType = SystemOperations.LIST_UPDATE + }; + 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 = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Input.Add(new ConnectorViewModel { Title = "Index", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Integer, DataType = "int" }); + op.Input.Add(new ConnectorViewModel { Title = "Item", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Universal, IsCopyConnector = true }); + op.Output.Add(new ConnectorViewModel { Title = "List", IsInput = false, Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_UPDATE }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_UPDATE); + } + } + + // ─── LIST GET (non-flow, value node) ─── + public class ListGetHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_GET; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_GET); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Get", + SystemOperationType = SystemOperations.LIST_GET + }; + op.Input.Add(new ConnectorViewModel { Title = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Input.Add(new ConnectorViewModel { Title = "Index", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Integer, DataType = "int" }); + op.Output.Add(new ConnectorViewModel { Title = "Item", IsInput = false, Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Object, DataType = "object" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_GET }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_GET); + } + } + + // ─── LIST COUNT (non-flow, value node) ─── + public class ListCountHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_COUNT; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_COUNT); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Count", + SystemOperationType = SystemOperations.LIST_COUNT + }; + op.Input.Add(new ConnectorViewModel { Title = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Output.Add(new ConnectorViewModel { Title = "Count", IsInput = false, Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Integer, DataType = "int" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_COUNT }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_COUNT); + } + } + + // ─── LIST CONTAINS (non-flow, value node) ─── + public class ListContainsHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_CONTAINS; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_CONTAINS); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Contains", + SystemOperationType = SystemOperations.LIST_CONTAINS + }; + op.Input.Add(new ConnectorViewModel { Title = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Input.Add(new ConnectorViewModel { Title = "Item", Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Universal, IsCopyConnector = true }); + op.Output.Add(new ConnectorViewModel { Title = "Result", IsInput = false, Shape = ConnectorShape.Circle, ConnectorColor = NodeColors.Boolean, DataType = "bool" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_CONTAINS }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_CONTAINS); + } + } + + // ─── LIST CLEAR ─── + public class ListClearHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.LIST_CLEAR; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.LIST_CLEAR); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new SystemOperationViewModel + { + Title = info.Title ?? "List Clear", + SystemOperationType = SystemOperations.LIST_CLEAR + }; + 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 = "List", Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + op.Output.Add(new ConnectorViewModel { Title = "List", IsInput = false, Shape = ConnectorShape.Grid, ConnectorColor = NodeColors.List, DataType = "list" }); + return op; + } + + public OperationViewModel Restore(NodeData data) + => Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.LIST_CLEAR }); + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.LIST_CLEAR); + } + } +} diff --git a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs index ea74eb8..7279242 100644 --- a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs +++ b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs @@ -43,6 +43,13 @@ namespace Nodify.Calculator.NodeHandlers _handlers.Add(new CopyHandler()); _handlers.Add(new SplitHandler()); _handlers.Add(new ParseJsonHandler()); + _handlers.Add(new ListAddHandler()); + _handlers.Add(new ListRemoveHandler()); + _handlers.Add(new ListUpdateHandler()); + _handlers.Add(new ListGetHandler()); + _handlers.Add(new ListCountHandler()); + _handlers.Add(new ListContainsHandler()); + _handlers.Add(new ListClearHandler()); _handlers.Add(new GetSetVariableHandler()); _handlers.Add(new GetSetModelHandler()); _handlers.Add(new BeginEndHandler()); @@ -108,6 +115,13 @@ namespace Nodify.Calculator.NodeHandlers SystemOperations.END => _handlers.OfType().FirstOrDefault(), SystemOperations.GET_SET when sys.IsSimpleVariable => _handlers.OfType().FirstOrDefault(), SystemOperations.GET_SET => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_ADD => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_REMOVE => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_UPDATE => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_GET => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_COUNT => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_CONTAINS => _handlers.OfType().FirstOrDefault(), + SystemOperations.LIST_CLEAR => _handlers.OfType().FirstOrDefault(), _ => _handlers.OfType().FirstOrDefault() }; } diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs index 8dafb6c..e65fae1 100644 --- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs +++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs @@ -153,13 +153,116 @@ namespace Nodify.Calculator systemNodes.Add(debugAndCreateModels); systemNodes.Add(jsonParseNode); systemNodes.Add(splitNode); - systemNodes.Add(takeNode); - systemNodes.Add(forEachNode); systemNodes.Add(assertNode); systemNodes.Add(stringConcatNode); return systemNodes; } + public static List GetListNodes() + { + var listNodes = new List(); + + var listAdd = new OperationInfoViewModel + { + Title = "List Add", + Type = OperationType.System, + sysOp = SystemOperations.LIST_ADD, + IsFlowNode = true + }; + listAdd.Input.Add("List"); + listAdd.Input.Add("Item"); + + var listRemove = new OperationInfoViewModel + { + Title = "List Remove", + Type = OperationType.System, + sysOp = SystemOperations.LIST_REMOVE, + IsFlowNode = true + }; + listRemove.Input.Add("List"); + listRemove.Input.Add("Index"); + + var listUpdate = new OperationInfoViewModel + { + Title = "List Update", + Type = OperationType.System, + sysOp = SystemOperations.LIST_UPDATE, + IsFlowNode = true + }; + listUpdate.Input.Add("List"); + listUpdate.Input.Add("Index"); + listUpdate.Input.Add("Item"); + + var listGet = new OperationInfoViewModel + { + Title = "List Get", + Type = OperationType.System, + sysOp = SystemOperations.LIST_GET, + IsFlowNode = false + }; + listGet.Input.Add("List"); + listGet.Input.Add("Index"); + + var listCount = new OperationInfoViewModel + { + Title = "List Count", + Type = OperationType.System, + sysOp = SystemOperations.LIST_COUNT, + IsFlowNode = false + }; + listCount.Input.Add("List"); + + var listContains = new OperationInfoViewModel + { + Title = "List Contains", + Type = OperationType.System, + sysOp = SystemOperations.LIST_CONTAINS, + IsFlowNode = false + }; + listContains.Input.Add("List"); + listContains.Input.Add("Item"); + + var listClear = new OperationInfoViewModel + { + Title = "List Clear", + Type = OperationType.System, + sysOp = SystemOperations.LIST_CLEAR, + IsFlowNode = true + }; + listClear.Input.Add("List"); + + // Also include existing list-related nodes + var takeNode = new OperationInfoViewModel + { + Title = "TAKE", + Type = OperationType.System, + sysOp = SystemOperations.TAKE, + IsFlowNode = false + }; + takeNode.Input.Add("List"); + + var forEachNode = new OperationInfoViewModel + { + Title = "ForEach", + Type = OperationType.System, + sysOp = SystemOperations.FOREACH, + IsFlowNode = true + }; + forEachNode.Input.Add("List"); + + listNodes.Add(takeNode); + listNodes.Add(forEachNode); + listNodes.Add(listAdd); + listNodes.Add(listRemove); + listNodes.Add(listUpdate); + listNodes.Add(listGet); + listNodes.Add(listCount); + listNodes.Add(listContains); + listNodes.Add(listClear); + + return listNodes; + } + public static List GetOperationsInfo(Type container) { List result = new List(); diff --git a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs index 8f11679..e27c74f 100644 --- a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs +++ b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs @@ -120,6 +120,9 @@ namespace Nodify.Calculator /// Non-API system nodes for the right-side panel. public NodifyObservableCollection SystemNodes { get; } = new NodifyObservableCollection(); + + /// List/Array operation nodes for the right-side panel. + public NodifyObservableCollection ListNodes { get; } = new NodifyObservableCollection(); public INodifyCommand CreateOperationCommand { get; } [Newtonsoft.Json.JsonIgnore] @@ -160,6 +163,12 @@ namespace Nodify.Calculator foreach (var sysNode in operations) SystemNodes.Add(sysNode); + // Populate list/array nodes for the right panel + var listOps = OperationFactory.GetListNodes(); + foreach (var listNode in listOps) + ListNodes.Add(listNode); + operations.AddRange(listOps); + SwaggerOperations = new NodifyObservableCollection(); LoadSwaggerNodesFromDb(); RebuildGroupedSwaggerOperations(); diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs index 8bc0a5e..0d66824 100644 --- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs +++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs @@ -24,7 +24,14 @@ namespace Nodify.Calculator FOREACH, ASSERT, KNOT, - STRING_CONCAT + STRING_CONCAT, + LIST_ADD, + LIST_REMOVE, + LIST_UPDATE, + LIST_COUNT, + LIST_CONTAINS, + LIST_CLEAR, + LIST_GET } public class SystemOperationViewModel : OperationViewModel