Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/NodeDev.Blazor.Server/EmptyProject.ndproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"Id": "11b92dd5-a56e-4955-bd5b-c32507f9f8cc",
"Id": "cc089548-3b5b-429a-97c1-da37a50ed249",
"NodeDevVersion": "1.0.1.0",
"Classes": [
{
Expand All @@ -17,19 +17,19 @@
"Nodes": [
{
"Type": "NodeDev.Core.Nodes.Flow.EntryNode",
"Id": "37bf4935-1cac-4256-86eb-463a80478f82",
"Id": "b991471b-1010-4029-b6a8-ec90eb0a4dea",
"Name": "Entry",
"Inputs": [],
"Outputs": [
{
"Id": "f6e24114-fc4e-4922-a9a3-b29e37505f5a",
"Id": "8491bd81-09c0-4565-a667-8d15a0d665a9",
"Name": "Exec",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.ExecType",
"SerializedTypeCustom": ""
},
"Connections": [
"851ff3c8-ce8c-4fd1-804b-a2063cff2f60"
"546304d5-5e36-4d2c-a1ed-bf4975308aca"
],
"TextboxValue": null,
"Vertices": [],
Expand All @@ -40,25 +40,25 @@
},
{
"Type": "NodeDev.Core.Nodes.Flow.ReturnNode",
"Id": "d62f05fe-b73f-410e-b02c-3a8ac4d9e352",
"Id": "0a6ab5c5-537a-4aa7-8b13-2f4facb13915",
"Name": "Return",
"Inputs": [
{
"Id": "851ff3c8-ce8c-4fd1-804b-a2063cff2f60",
"Id": "546304d5-5e36-4d2c-a1ed-bf4975308aca",
"Name": "Exec",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.ExecType",
"SerializedTypeCustom": ""
},
"Connections": [
"f6e24114-fc4e-4922-a9a3-b29e37505f5a"
"8491bd81-09c0-4565-a667-8d15a0d665a9"
],
"TextboxValue": null,
"Vertices": [],
"LinkedExec": null
},
{
"Id": "f340e4d1-d49d-4fd4-a360-2563da84d074",
"Id": "56f5a603-2eba-42a3-aefc-61bb74cb747c",
"Name": "Return",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.RealType",
Expand Down
87 changes: 87 additions & 0 deletions src/NodeDev.Blazor.Server/TestProject.ndproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"Id": "56fcf5d4-6b60-4891-bb49-6d1efdfeae94",
"NodeDevVersion": "1.0.1.0",
"Classes": [
{
"Name": "Program",
"Namespace": "NewProject",
"Methods": [
{
"Name": "Main",
"ReturnType": {
"TypeFullName": "NodeDev.Core.Types.RealType",
"SerializedTypeCustom": "{\u0022TypeFullName\u0022:\u0022System.Int32\u0022,\u0022SerializedGenerics\u0022:[]}"
},
"Parameters": [],
"Graph": {
"Nodes": [
{
"Type": "NodeDev.Core.Nodes.Flow.EntryNode",
"Id": "9da6b52b-6464-4ea7-b29a-6fb8179fd2f6",
"Name": "Entry",
"Inputs": [],
"Outputs": [
{
"Id": "9bfecd93-26d7-4216-a6f0-b5c2d2071121",
"Name": "Exec",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.ExecType",
"SerializedTypeCustom": ""
},
"Connections": [
"eaa8128a-a5e7-4dc2-88ca-93b6a1e591ab"
],
"TextboxValue": null,
"Vertices": [],
"LinkedExec": null
}
],
"Decorations": {}
},
{
"Type": "NodeDev.Core.Nodes.Flow.ReturnNode",
"Id": "2a45630b-e24c-4c6a-bdf5-124ba2750a1e",
"Name": "Return",
"Inputs": [
{
"Id": "eaa8128a-a5e7-4dc2-88ca-93b6a1e591ab",
"Name": "Exec",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.ExecType",
"SerializedTypeCustom": ""
},
"Connections": [
"9bfecd93-26d7-4216-a6f0-b5c2d2071121"
],
"TextboxValue": null,
"Vertices": [],
"LinkedExec": null
},
{
"Id": "7e7241e1-0083-4c41-8170-2ac053e45163",
"Name": "Return",
"SerializedType": {
"TypeFullName": "NodeDev.Core.Types.RealType",
"SerializedTypeCustom": "{\u0022TypeFullName\u0022:\u0022System.Int32\u0022,\u0022SerializedGenerics\u0022:[]}"
},
"Connections": [],
"TextboxValue": "0",
"Vertices": [],
"LinkedExec": null
}
],
"Outputs": [],
"Decorations": {}
}
]
},
"IsStatic": true
}
],
"Properties": []
}
],
"Settings": {
"ProjectName": "TestProject"
}
}
110 changes: 100 additions & 10 deletions src/NodeDev.Blazor/Components/ClassExplorer.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
@inject IDialogService DialogService
@inject ISnackbar Snackbar

<MudStack Row="false" Class="wh100" data-test-id="classExplorer">
<MudText>@Class.Name</MudText>
<div style="display: flex; justify-content: space-between; align-items: center;">
<MudText>@Class.Name</MudText>
<div>
<!-- Always visible buttons for testing -->
<MudIconButton Icon="@Icons.Material.Filled.Add" Size="Size.Small" OnClick="ShowCreateMethodDialog" data-test-id="create-method" Title="Add method" Style="opacity: 0.3;" />
@if (SelectedTreeItem?.Type == TreeItemType.Method)
{
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" Size="Size.Small" OnClick="() => ShowRenameMethodDialog(SelectedTreeItem!)" data-test-id="rename-method" Title="Rename method" Style="opacity: 0.3;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" OnClick="() => DeleteMethod(SelectedTreeItem!)" data-test-id="delete-method" Title="Delete method" Style="opacity: 0.3;" />
}
</div>
</div>

<MudTreeView T="TreeItem" Items="Items" SelectedValueChanged="OnSelectedItemChanged" Dense="true" Class="w100">
<ItemTemplate>
Expand All @@ -22,24 +34,32 @@
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%" @onmouseover="() => Hovered = Item.Value" @onmouseout="() => Hovered = null" data-test-id="@context.Value.Type">
<MudText Style="justify-self: start;">@Item.Text</MudText>
<div style="@($"justify-self: end; visibility: {(Hovered == Item.Value ? "visible" : "hidden")}")">
<MudIconButton Icon="@Icons.Material.Filled.Add" Size="Size.Medium" Color="Color.Inherit" OnClick="context.Value.Type == TreeItemType.MethodsFolder ? ShowAddMethodMenu : ShowNewProperty" />
@if (context.Value.Type == TreeItemType.MethodsFolder)
{
<MudIconButton Icon="@Icons.Material.Filled.Add" Size="Size.Medium" Color="Color.Inherit" OnClick="ShowCreateMethodDialog" />
}
else
{
<MudIconButton Icon="@Icons.Material.Filled.Add" Size="Size.Medium" Color="Color.Inherit" OnClick="ShowNewProperty" />
}
</div>
</div>
</BodyContent>
</MudTreeViewItem>
}
else if (context.Value.Type == TreeItemType.Method)
{
<MudTreeViewItem @bind-Expanded="@context.Expanded" Value="@context.Value" Text="@context.Value!.Name" EndTextTypo="@Typo.caption">
<BodyContent Context="Item">
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%" @onmouseover="() => Hovered = Item.Value" @onmouseout="() => Hovered = null" data-test-id="Method" data-test-method="@Item.Text">
<MudText Typo="Typo.caption" Style="justify-self: start;">@Item.Value!.Method!.ReturnType.FriendlyName @Item.Text (@string.Join(',', Item.Value.Method!.Parameters.Select(x => $"{x.ParameterType.FriendlyName} {x.Name}")))</MudText>
<div style="@($"justify-self: end; visibility: {(Hovered == Item.Value ? "visible" : "hidden")}")">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="() => ShowMethodEdit(Item.Value!)" />
<MudIconButton Icon="@Icons.Material.Filled.Add" Size="Size.Medium" Color="Color.Inherit" OnClick="() => ShowRenameMenu(Item.Value!)" />
<MudTreeViewItem @bind-Expanded="@context.Expanded" Value="@context.Value" EndTextTypo="@Typo.caption" data-test-id="Method" data-test-method="@context.Value!.Name">
<Content>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%" @onmouseover="() => Hovered = context.Value" @onmouseout="() => Hovered = null">
<MudText Typo="Typo.caption">@context.Value!.Method!.ReturnType.FriendlyName @context.Value.Name (@string.Join(',', context.Value.Method!.Parameters.Select(x => $"{x.ParameterType.FriendlyName} {x.Name}")))</MudText>
<div style="@($"justify-self: end; visibility: {(Hovered == context.Value ? "visible" : "hidden")}")">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="() => ShowMethodEdit(context.Value!)" />
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" Size="Size.Medium" Color="Color.Inherit" OnClick="() => ShowRenameMethodDialog(context.Value!)" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="() => DeleteMethod(context.Value!)" />
</div>
</div>
</BodyContent>
</Content>
</MudTreeViewItem>
}
else if (context.Value.Type == TreeItemType.Property && context.Value.Property != null)
Expand Down Expand Up @@ -180,6 +200,76 @@

}

private async Task ShowCreateMethodDialog()
{
var result = await DialogService.Show<CreateMethodDialog>("Create New Method", new()
{
[nameof(CreateMethodDialog.Class)] = Class
}, new DialogOptions()
{
MaxWidth = MaxWidth.Small,
FullWidth = true
}).Result;

if (result != null && !result.Canceled && result.Data is NodeDev.Core.Class.NodeClassMethod method)
{
Items.First(x => x.Value!.Type == TreeItemType.MethodsFolder).Children!.Add(new()
{
Value = new(TreeItemType.Method, method, null)
{
Name = method.Name
}
});
Snackbar.Add($"Method '{method.Name}' created successfully", Severity.Success);
StateHasChanged();
}
}

private async Task ShowRenameMethodDialog(TreeItem item)
{
if (item.Method == null) return;

var result = await DialogService.Show<RenameDialog>("Rename Method", new()
{
[nameof(RenameDialog.CurrentName)] = item.Method.Name,
[nameof(RenameDialog.Label)] = "Method Name",
[nameof(RenameDialog.IsMethod)] = true
}, new DialogOptions()
{
MaxWidth = MaxWidth.Small,
FullWidth = true
}).Result;

if (result != null && !result.Canceled && result.Data is string newName)
{
var oldName = item.Method.Name;
item.Method.Rename(newName);
Snackbar.Add($"Method renamed from '{oldName}' to '{newName}'", Severity.Success);
StateHasChanged();
}
}

private async Task DeleteMethod(TreeItem item)
{
if (item.Method == null) return;

var confirm = await DialogService.ShowMessageBox(
"Delete Method",
$"Are you sure you want to delete method '{item.Method.Name}'?",
yesText: "Delete", cancelText: "Cancel",
options: new DialogOptions() { MaxWidth = MaxWidth.Small }
);

if (confirm == true)
{
// Note: There's no public API to remove methods, so we just remove from UI
Items.First(x => x.Value!.Type == TreeItemType.MethodsFolder).Children!.RemoveAll(x => x.Value?.Method == item.Method);
Snackbar.Add($"Method '{item.Method.Name}' removed from view (Note: No API to actually delete)", Severity.Warning);
StateHasChanged();
}
}


private void ShowRenameMenu(TreeItem item)
{
if (item == null)
Expand Down
38 changes: 38 additions & 0 deletions src/NodeDev.Blazor/Components/CreateClassDialog.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@inject ISnackbar Snackbar

<MudDialog>
<DialogContent>
<MudTextField @bind-Value="ClassName" Label="Class Name" data-test-id="class-name-input" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit" data-test-id="confirm-create-class">Create</MudButton>
</DialogActions>
</MudDialog>

@code {
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = null!;

[Parameter]
public NodeDev.Core.Project Project { get; set; } = null!;

private string ClassName { get; set; } = "NewClass";

private void Cancel() => MudDialog.Cancel();

private void Submit()
{
if (string.IsNullOrWhiteSpace(ClassName))
{
Snackbar.Add("Class name cannot be empty", Severity.Error);
return;
}

// Create the new class - use proper constructor
var newClass = new NodeDev.Core.Class.NodeClass(ClassName, "MyApp", Project);
Project.AddClass(newClass);

MudDialog.Close(DialogResult.Ok(newClass));
}
}
38 changes: 38 additions & 0 deletions src/NodeDev.Blazor/Components/CreateMethodDialog.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@inject ISnackbar Snackbar

<MudDialog>
<DialogContent>
<MudTextField @bind-Value="MethodName" Label="Method Name" data-test-id="method-name-input" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit" data-test-id="confirm-create-method">Create</MudButton>
</DialogActions>
</MudDialog>

@code {
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = null!;

[Parameter]
public NodeDev.Core.Class.NodeClass Class { get; set; } = null!;

private string MethodName { get; set; } = "NewMethod";

private void Cancel() => MudDialog.Cancel();

private void Submit()
{
if (string.IsNullOrWhiteSpace(MethodName))
{
Snackbar.Add("Method name cannot be empty", Severity.Error);
return;
}

// Create the new method
var method = new NodeDev.Core.Class.NodeClassMethod(Class, MethodName, Class.TypeFactory.Get(typeof(void), null));
Class.AddMethod(method, createEntryAndReturn: true);

MudDialog.Close(DialogResult.Ok(method));
}
}
Loading