Added function in the panel for user to be added new function

This commit is contained in:
Ankitkumar Satapara
2026-04-18 17:24:55 +05:30
parent 21aaef6776
commit 5c838908eb
9 changed files with 454 additions and 8 deletions

View File

@@ -0,0 +1,97 @@
<Window x:Class="Nodify.Calculator.CreateFunctionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Create New Function"
Width="500"
Height="520"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Background="#2D2D30"
Foreground="White">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Function Name -->
<TextBlock Text="Function Name:" Grid.Row="0" Margin="0 0 0 4" />
<TextBox x:Name="FunctionNameBox" Grid.Row="1" Margin="0 0 0 10"
Background="#3E3E42" Foreground="White" Padding="4" />
<!-- Input Parameters -->
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0 0 0 4">
<TextBlock Text="Input Parameters:" FontWeight="Bold" />
<Button Content=" Add" Margin="10 0 0 0" Padding="8 2" Click="AddInput_Click"
Background="#3E3E42" Foreground="White" Cursor="Hand" />
</StackPanel>
<DataGrid x:Name="InputParamsGrid" Grid.Row="3" Margin="0 0 0 10"
AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="True"
Background="#3E3E42" Foreground="White" RowBackground="#3E3E42"
AlternatingRowBackground="#333337" GridLinesVisibility="None"
HeadersVisibility="Column" BorderBrush="#555">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
<DataGridComboBoxColumn Header="Type" SelectedItemBinding="{Binding Type}" Width="120">
<DataGridComboBoxColumn.ItemsSource>
<x:Array Type="sys:String" xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<sys:String>string</sys:String>
<sys:String>int</sys:String>
<sys:String>double</sys:String>
<sys:String>bool</sys:String>
<sys:String>float</sys:String>
<sys:String>decimal</sys:String>
<sys:String>long</sys:String>
<sys:String>DateTime</sys:String>
<sys:String>object</sys:String>
</x:Array>
</DataGridComboBoxColumn.ItemsSource>
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Output Parameters -->
<StackPanel Orientation="Horizontal" Grid.Row="4" Margin="0 0 0 4">
<TextBlock Text="Output Parameters:" FontWeight="Bold" />
<Button Content=" Add" Margin="10 0 0 0" Padding="8 2" Click="AddOutput_Click"
Background="#3E3E42" Foreground="White" Cursor="Hand" />
</StackPanel>
<DataGrid x:Name="OutputParamsGrid" Grid.Row="5" Margin="0 0 0 10"
AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="True"
Background="#3E3E42" Foreground="White" RowBackground="#3E3E42"
AlternatingRowBackground="#333337" GridLinesVisibility="None"
HeadersVisibility="Column" BorderBrush="#555">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
<DataGridComboBoxColumn Header="Type" SelectedItemBinding="{Binding Type}" Width="120">
<DataGridComboBoxColumn.ItemsSource>
<x:Array Type="sys:String" xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<sys:String>string</sys:String>
<sys:String>int</sys:String>
<sys:String>double</sys:String>
<sys:String>bool</sys:String>
<sys:String>float</sys:String>
<sys:String>decimal</sys:String>
<sys:String>long</sys:String>
<sys:String>DateTime</sys:String>
<sys:String>object</sys:String>
</x:Array>
</DataGridComboBoxColumn.ItemsSource>
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Buttons -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="6">
<Button Content="Create" Width="80" Margin="0 0 10 0" Padding="4"
Click="OnCreateClick" IsDefault="True" />
<Button Content="Cancel" Width="80" Padding="4"
Click="OnCancelClick" IsCancel="True" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
namespace Nodify.Calculator
{
public partial class CreateFunctionDialog : Window
{
public string FunctionName { get; private set; } = string.Empty;
public List<FunctionParameterInfo> InputParameters { get; } = new List<FunctionParameterInfo>();
public List<FunctionParameterInfo> OutputParameters { get; } = new List<FunctionParameterInfo>();
private readonly ObservableCollection<FunctionParameterInfo> _inputs = new ObservableCollection<FunctionParameterInfo>();
private readonly ObservableCollection<FunctionParameterInfo> _outputs = new ObservableCollection<FunctionParameterInfo>();
public CreateFunctionDialog()
{
InitializeComponent();
InputParamsGrid.ItemsSource = _inputs;
OutputParamsGrid.ItemsSource = _outputs;
}
private void AddInput_Click(object sender, RoutedEventArgs e)
{
_inputs.Add(new FunctionParameterInfo { Name = $"param{_inputs.Count + 1}", Type = "string" });
}
private void AddOutput_Click(object sender, RoutedEventArgs e)
{
_outputs.Add(new FunctionParameterInfo { Name = $"result{_outputs.Count + 1}", Type = "string" });
}
private void OnCreateClick(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(FunctionNameBox.Text))
{
MessageBox.Show("Please enter a function name.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
FunctionName = FunctionNameBox.Text.Trim();
InputParameters.AddRange(_inputs);
OutputParameters.AddRange(_outputs);
DialogResult = true;
}
private void OnCancelClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
}

View File

@@ -433,6 +433,21 @@
</nodify:Node> </nodify:Node>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type local:FunctionOperationViewModel}">
<nodify:Node Header="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}"
ToolTip="Double click to edit function"
BorderBrush="#FF9800"
BorderThickness="2">
<nodify:Node.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding DataContext.OpenCalculatorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding InnerCalculator}" />
</nodify:Node.InputBindings>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:CalculatorOperationViewModel}"> <DataTemplate DataType="{x:Type local:CalculatorOperationViewModel}">
<nodify:Node Header="{Binding Title}" <nodify:Node Header="{Binding Title}"
Input="{Binding Input}" Input="{Binding Input}"
@@ -691,6 +706,38 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<!-- Functions Section -->
<TextBlock Text="Functions" FontWeight="Bold" Margin="0 8 0 4" />
<Button Content=" Create New Function"
Command="{Binding Calculator.OperationsMenu.CreateFunctionCommand}"
Margin="0 0 0 6"
Padding="6 3"
Cursor="Hand"
Background="#FF9800"
Foreground="White" />
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableFunctions}"
Focusable="False">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="FrameworkElement.Margin" Value="3" />
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop to add this function to the flow" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
<Border Background="#3E3E42" CornerRadius="3" Padding="6 4"
MouseMove="OnNodeDrag" Cursor="Hand">
<StackPanel Orientation="Horizontal">
<TextBlock Text="ƒ " Foreground="#FF9800" FontWeight="Bold" />
<TextBlock Text="{Binding Title}" Foreground="Orange" FontWeight="SemiBold" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</DockPanel> </DockPanel>

View File

@@ -428,6 +428,14 @@ namespace Nodify.Calculator
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}"); OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
return; return;
} }
// Handle Function nodes — execute the inner flow
if (op is FunctionOperationViewModel funcOp)
{
OnLogMe?.Invoke($"Executing function: {funcOp.FunctionName}");
ExecuteFunction(funcOp, connections);
return;
}
OnLogMe?.Invoke($"Starting Execution : {url}"); OnLogMe?.Invoke($"Starting Execution : {url}");
var res = GetResponse(url, "get"); var res = GetResponse(url, "get");
if (!string.IsNullOrEmpty(res)) if (!string.IsNullOrEmpty(res))
@@ -509,16 +517,83 @@ namespace Nodify.Calculator
private static string GetConnectorTextValue(ConnectorViewModel connector) private static string GetConnectorTextValue(ConnectorViewModel connector)
{ {
// When the connector is not connected, the user enters text in the Value text box.
// Value is a double, so we try to use it as a string representation.
// However, for string inputs (URL, token, etc.) the value won't be meaningful as a double.
// The user-typed string is actually stored in the Title field for unconnected inputs in some cases,
// but here we rely on the ConnectorViewModel.Value being 0 (default) and the actual string
// is not captured as double. So we return empty — the AuthOperationViewModel properties are the
// primary source set via the node's text boxes.
return string.Empty; return string.Empty;
} }
private void ExecuteFunction(FunctionOperationViewModel funcOp, ICollection<ConnectionViewModel> connections)
{
// Propagate outer input values into the function
for (int i = 0; i < funcOp.InputParameters.Count; i++)
{
var outerInputIndex = i + 1; // skip flow triangle
if (outerInputIndex < funcOp.Input.Count)
{
var outerConnector = funcOp.Input[outerInputIndex];
var inputCon = connections.FirstOrDefault(c => c.Input == outerConnector);
if (inputCon != null)
{
var sourceNodeId = inputCon.Output.Operation.NodeId;
if (outputs.TryGetValue(sourceNodeId, out var val))
{
outerConnector.Value = double.TryParse(val, out var d) ? d : 0;
}
}
}
}
// Trigger propagation of input values to inner Begin node
funcOp.PropagateInputsToInner();
// Execute the inner calculator's flow (Begin -> End)
var innerOps = funcOp.InnerCalculator.Operations;
var innerConns = funcOp.InnerCalculator.Connections;
OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Resolving inner GET variables...");
ResolveGetVariableNodes(innerOps, innerConns);
OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Executing inner chain...");
var innerStart = innerOps.FirstOrDefault(n => n.Title?.Equals("Begin", StringComparison.OrdinalIgnoreCase) == true);
if (innerStart == null)
{
OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] No Begin node found inside function!", logType.Error);
return;
}
var visited = new HashSet<string>();
TraverseChain(innerStart, "end", innerConns, visited, true);
// Collect outputs from the inner End node
var innerEnd = innerOps.FirstOrDefault(n => n.Title?.Equals("End", StringComparison.OrdinalIgnoreCase) == true);
if (innerEnd != null)
{
for (int i = 0; i < funcOp.OutputParameters.Count; i++)
{
var endInputIndex = i + 1; // skip flow triangle
if (endInputIndex < innerEnd.Input.Count)
{
var endConnector = innerEnd.Input[endInputIndex];
var endCon = innerConns.FirstOrDefault(c => c.Input == endConnector);
if (endCon != null)
{
var sourceId = endCon.Output.Operation.NodeId;
if (outputs.TryGetValue(sourceId, out var resultVal))
{
var outerOutputIndex = i + 1; // skip flow triangle
if (outerOutputIndex < funcOp.Output.Count)
{
funcOp.Output[outerOutputIndex].Value = double.TryParse(resultVal, out var dv) ? dv : 0;
}
outputs[funcOp.NodeId] = resultVal;
OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Output '{funcOp.OutputParameters[i].Name}' = {resultVal}");
}
}
}
}
}
OnLogMe?.Invoke($"Function '{funcOp.FunctionName}' execution completed.");
}
private string GetResponse(string url, string type) private string GetResponse(string url, string type)
{ {
string baseURL = !string.IsNullOrWhiteSpace(_authBaseUrl) ? _authBaseUrl : "https://localhost:7107"; string baseURL = !string.IsNullOrWhiteSpace(_authBaseUrl) ? _authBaseUrl : "https://localhost:7107";

View File

@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Calculator
{
public class FunctionOperationViewModel : CalculatorOperationViewModel
{
private string _functionName = "Function";
public string FunctionName
{
get => _functionName;
set
{
if (SetProperty(ref _functionName, value))
{
Title = value;
}
}
}
public List<FunctionParameterInfo> InputParameters { get; } = new List<FunctionParameterInfo>();
public List<FunctionParameterInfo> OutputParameters { get; } = new List<FunctionParameterInfo>();
private OperationViewModel InnerBegin { get; }
private OperationViewModel InnerEnd { get; }
public FunctionOperationViewModel()
{
// Add Begin and End nodes inside the inner calculator for flow-based editing
InnerBegin = new SystemOperationViewModel
{
Title = "Begin",
SystemOperationType = SystemOperations.BEGIN,
Location = new Point(50, 150)
};
InnerBegin.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
InnerEnd = new SystemOperationViewModel
{
Title = "End",
SystemOperationType = SystemOperations.END,
Location = new Point(600, 150)
};
InnerEnd.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
InnerCalculator.Operations.Add(InnerBegin);
InnerCalculator.Operations.Add(InnerEnd);
}
public void ConfigureParameters(List<FunctionParameterInfo> inputs, List<FunctionParameterInfo> outputs)
{
InputParameters.Clear();
InputParameters.AddRange(inputs);
OutputParameters.Clear();
OutputParameters.AddRange(outputs);
// Add data output connectors to InnerBegin (these feed the inner flow with input values)
foreach (var inp in inputs)
{
InnerBegin.Output.Add(new ConnectorViewModel
{
Title = $"{inp.Name} ({inp.Type})",
Shape = ConnectorShape.Circle,
IsInput = false
});
// Also add an outer input connector on the function node
Input.Add(new ConnectorViewModel
{
Title = $"{inp.Name} ({inp.Type})"
});
}
// Add data input connectors to InnerEnd (these collect inner flow results)
foreach (var outp in outputs)
{
InnerEnd.Input.Add(new ConnectorViewModel
{
Title = $"{outp.Name} ({outp.Type})",
Shape = ConnectorShape.Circle
});
// Also add an outer output connector on the function node
Output.Add(new ConnectorViewModel
{
Title = $"{outp.Name} ({outp.Type})",
IsInput = false
});
}
}
protected override void OnInputValueChanged()
{
PropagateInputsToInner();
}
public void PropagateInputsToInner()
{
// Propagate outer input values into the InnerBegin's data output connectors
// The first output of InnerBegin is the flow triangle, data starts at index 1
for (var i = 0; i < InputParameters.Count; i++)
{
var outerIndex = i; // outer Input: first is flow triangle (index 0), data starts at 1
var innerIndex = i + 1; // InnerBegin Output: first is flow triangle, data starts at 1
if (outerIndex + 1 < Input.Count && innerIndex < InnerBegin.Output.Count)
{
InnerBegin.Output[innerIndex].Value = Input[outerIndex + 1].Value;
}
}
}
}
public class FunctionParameterInfo
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = "string";
}
}

View File

@@ -32,5 +32,8 @@ namespace Nodify.Calculator
public string VariableType { get; set; } = string.Empty; public string VariableType { get; set; } = string.Empty;
public string DefaultValue { get; set; } = string.Empty; public string DefaultValue { get; set; } = string.Empty;
public bool IsSimpleVariable { get; set; } public bool IsSimpleVariable { get; set; }
public bool IsFunction { get; set; }
public List<FunctionParameterInfo> FunctionInputs { get; set; } = new List<FunctionParameterInfo>();
public List<FunctionParameterInfo> FunctionOutputs { get; set; } = new List<FunctionParameterInfo>();
} }
} }

View File

@@ -275,6 +275,24 @@ namespace Nodify.Calculator
//_o.Input.AddRange(input); //_o.Input.AddRange(input);
return _o; return _o;
case OperationType.System: case OperationType.System:
if (info.sysOp == SystemOperations.FUNCTION)
{
var funcOp = new FunctionOperationViewModel
{
Title = info.Title,
FunctionName = info.Title
};
// Add flow connectors (triangle)
var funcFlowIn = new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle };
var funcFlowOut = new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false };
funcOp.Input.Add(funcFlowIn);
funcOp.Output.Add(funcFlowOut);
// Configure typed input/output parameters
funcOp.ConfigureParameters(info.FunctionInputs, info.FunctionOutputs);
return funcOp;
}
if (info.sysOp == SystemOperations.AUTH) if (info.sysOp == SystemOperations.AUTH)
{ {
var authOp = new AuthOperationViewModel var authOp = new AuthOperationViewModel

View File

@@ -113,6 +113,7 @@ namespace Nodify.Calculator
public NodifyObservableCollection<OperationInfoViewModel> SwaggerOperations { get; } public NodifyObservableCollection<OperationInfoViewModel> SwaggerOperations { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; } public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; } public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableFunctions { get; }
public INodifyCommand CreateOperationCommand { get; } public INodifyCommand CreateOperationCommand { get; }
[Newtonsoft.Json.JsonIgnore] [Newtonsoft.Json.JsonIgnore]
@@ -125,6 +126,7 @@ namespace Nodify.Calculator
List<OperationInfoViewModel> operations = new List<OperationInfoViewModel>(); List<OperationInfoViewModel> operations = new List<OperationInfoViewModel>();
AvailableModels = new NodifyObservableCollection<OperationInfoViewModel>(); AvailableModels = new NodifyObservableCollection<OperationInfoViewModel>();
AvailableVariables = new NodifyObservableCollection<OperationInfoViewModel>(); AvailableVariables = new NodifyObservableCollection<OperationInfoViewModel>();
AvailableFunctions = new NodifyObservableCollection<OperationInfoViewModel>();
LoadVariablesFromDb(); LoadVariablesFromDb();
var customModelDir = "CustomModels"; var customModelDir = "CustomModels";
@@ -158,6 +160,7 @@ namespace Nodify.Calculator
CreateOperationCommand = new DelegateCommand<OperationInfoViewModel>(CreateOperation); CreateOperationCommand = new DelegateCommand<OperationInfoViewModel>(CreateOperation);
ImportSwaggerCommand = new DelegateCommand(ImportSwagger); ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
AddVariableCommand = new DelegateCommand(AddVariable); AddVariableCommand = new DelegateCommand(AddVariable);
CreateFunctionCommand = new DelegateCommand(CreateFunction);
} }
@@ -180,6 +183,37 @@ namespace Nodify.Calculator
public INodifyCommand ImportSwaggerCommand { get; } public INodifyCommand ImportSwaggerCommand { get; }
public INodifyCommand AddVariableCommand { get; } public INodifyCommand AddVariableCommand { get; }
public INodifyCommand CreateFunctionCommand { get; }
private void CreateFunction()
{
var dialog = new CreateFunctionDialog();
dialog.Owner = System.Windows.Application.Current.MainWindow;
if (dialog.ShowDialog() != true)
return;
var funcName = dialog.FunctionName;
if (AvailableFunctions.Any(f => f.Title == funcName))
{
MessageBox.Show($"A function named '{funcName}' already exists.", "Duplicate Function", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var funcInfo = new OperationInfoViewModel
{
Title = funcName,
Type = OperationType.System,
sysOp = SystemOperations.FUNCTION,
IsFlowNode = true,
IsFunction = true,
FunctionInputs = new System.Collections.Generic.List<FunctionParameterInfo>(dialog.InputParameters),
FunctionOutputs = new System.Collections.Generic.List<FunctionParameterInfo>(dialog.OutputParameters)
};
AvailableFunctions.Add(funcInfo);
AvailableOperations.Add(funcInfo);
}
private void AddVariable() private void AddVariable()
{ {

View File

@@ -17,7 +17,8 @@ namespace Nodify.Calculator
GET_SET, GET_SET,
PARSEJSON, PARSEJSON,
SPLIT, SPLIT,
AUTH AUTH,
FUNCTION
} }
public class SystemOperationViewModel : OperationViewModel public class SystemOperationViewModel : OperationViewModel