Initial commit
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using RobotNet10.ScriptEditor.Clients
|
||||
@using RobotNet10.ScriptEditor.Services
|
||||
@using RobotNet10.ScriptEditor.Models
|
||||
@using RobotNet10.ScriptEditor.Components
|
||||
@using RobotNet10.ScriptEditor.Dialogs
|
||||
@using RobotNet10.ScriptEngine.Shared
|
||||
@inject FileManagerHubClient FileManagerClient
|
||||
@inject ScriptManagerHubClient ScriptManagerClient
|
||||
@inject ScriptWorkspace Workspace
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<SidebarAccordionItem TabName="workspace" Label="WORKSPACE">
|
||||
<HeaderActions>
|
||||
<IconButton Icon="folder-plus" Title="Add Folder" OnClick="HandleAddFolder" />
|
||||
<IconButton Icon="file-plus" Title="Add File" OnClick="HandleAddFile" />
|
||||
<IconButton Icon="content-save-all text-primary" Title="Save All" OnClick="HandleSaveAll" />
|
||||
<IconButton Icon="package-down" Title="Backup" OnClick="HandleBackup" />
|
||||
<IconButton Icon="package-up" Title="Restore" OnClick="HandleRestore" />
|
||||
</HeaderActions>
|
||||
<ChildContent>
|
||||
<CascadingValue Value="@RadioName" Name="FileExplorerRadioName">
|
||||
<div class="file-explorer-container" @onclick="HandleContainerClick" @oncontextmenu="HandleContainerRightClick" @oncontextmenu:preventDefault="true" @oncontextmenu:stopPropagation="true">
|
||||
<div class="file-explorer-tree" @ref="TreeContainerRef">
|
||||
@foreach (var folder in Workspace.Folders.OrderBy(f => f.Name))
|
||||
{
|
||||
<FolderExplorerItem Folder="@folder" @key="@folder.Path" />
|
||||
}
|
||||
@foreach (var file in Workspace.Files.OrderBy(f => f.Name))
|
||||
{
|
||||
<FileExplorerItem File="@file" @key="@file.Path" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<MudMenu Class="flex-grow-1 w-100" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft" Size="@Size.Small" PositionAtCursor Dense ActivationEvent="@MouseEvent.RightClick">
|
||||
<ActivatorContent>
|
||||
<div class="w-100 h-100" id="@_contextMenuActivatorId" @ref="ContextMenuActivator"></div>
|
||||
</ActivatorContent>
|
||||
<ChildContent>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.CreateNewFolder" OnClick="HandleCreateFolderFromContext">
|
||||
Create Folder
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.NoteAdd" OnClick="HandleCreateFileFromContext">
|
||||
Create File
|
||||
</MudMenuItem>
|
||||
</ChildContent>
|
||||
</MudMenu>
|
||||
</CascadingValue>
|
||||
</ChildContent>
|
||||
</SidebarAccordionItem>
|
||||
|
||||
@code {
|
||||
private ElementReference TreeContainerRef;
|
||||
private ElementReference ContextMenuActivator;
|
||||
private readonly string _contextMenuActivatorId = $"context-menu-activator-{Guid.NewGuid()}";
|
||||
private const string RadioName = "script-explorer-item";
|
||||
private bool ShowContextMenu { get; set; }
|
||||
private IJSObjectReference? _jsModule;
|
||||
private DotNetObjectReference<FileExplorer>? _dotNetRef;
|
||||
private double _contextMenuX;
|
||||
private double _contextMenuY;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Workspace.RootChanged += OnWorkspaceRootChanged;
|
||||
|
||||
// Subscribe to FileManagerHubClient events to update workspace
|
||||
FileManagerClient.FileCreated += OnFileCreated;
|
||||
FileManagerClient.FileDeleted += OnFileDeleted;
|
||||
FileManagerClient.FolderCreated += OnFolderCreated;
|
||||
FileManagerClient.FolderDeleted += OnFolderDeleted;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./_content/RobotNet10.ScriptEditor/fileExplorer.js");
|
||||
|
||||
if (_jsModule != null)
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("init", TreeContainerRef);
|
||||
|
||||
// Register Esc key handler at document level
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await _jsModule.InvokeVoidAsync("registerEscapeKeyHandler", _dotNetRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task HandleEscapeKey()
|
||||
{
|
||||
// Reset selection when Esc is pressed
|
||||
Workspace.SelectedFile = null;
|
||||
Workspace.SelectedFolder = null;
|
||||
|
||||
// Uncheck all radio buttons
|
||||
if (_jsModule != null)
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("UncheckRadioByName", RadioName);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnWorkspaceRootChanged()
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleContainerClick(MouseEventArgs e)
|
||||
{
|
||||
// Clear selection when clicking on empty area (container itself, not children)
|
||||
// Children will handle their own clicks and stop propagation
|
||||
Workspace.SelectedFile = null;
|
||||
Workspace.SelectedFolder = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleContainerRightClick(MouseEventArgs e)
|
||||
{
|
||||
// Right click on empty area - show context menu
|
||||
// Only show if clicking directly on container (not on file/folder items)
|
||||
// Clear selection first
|
||||
Workspace.SelectedFile = null;
|
||||
Workspace.SelectedFolder = null;
|
||||
|
||||
// Store click position for context menu (use ClientX/ClientY for viewport coordinates)
|
||||
_contextMenuX = e.ClientX;
|
||||
_contextMenuY = e.ClientY;
|
||||
|
||||
// Update activator position via JavaScript
|
||||
if (_jsModule != null)
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("setElementPosition", _contextMenuActivatorId, _contextMenuX, _contextMenuY);
|
||||
}
|
||||
|
||||
// Update activator position and show menu
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
ShowContextMenu = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleCreateFolderFromContext()
|
||||
{
|
||||
ShowContextMenu = false;
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateFolderDialog>("Create New Folder", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string folderName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create folder at root level (no parent)
|
||||
var newPath = folderName;
|
||||
|
||||
await FileManagerClient.CreateFolderAsync(newPath);
|
||||
|
||||
// Add folder to workspace at root level
|
||||
var level = 1;
|
||||
var folderDto = new ScriptFolderDto(folderName, level, [], []);
|
||||
Workspace.AddFolder(folderDto, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to create folder: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCreateFileFromContext()
|
||||
{
|
||||
ShowContextMenu = false;
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateFileDialog>("Create New File", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create file at root level (no parent)
|
||||
var newPath = fileName;
|
||||
|
||||
await FileManagerClient.CreateFileAsync(newPath, "");
|
||||
|
||||
// Add file to workspace at root level
|
||||
var level = 1;
|
||||
var fileDto = new ScriptFileDto(fileName, level, "");
|
||||
Workspace.AddFile(fileDto, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to create file: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task HandleAddFolder(MouseEventArgs e)
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateFolderDialog>("Create New Folder", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string folderName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parentFolder = Workspace.SelectedFolder;
|
||||
var parentPath = parentFolder?.Path ?? "";
|
||||
var newPath = string.IsNullOrEmpty(parentPath) ? folderName : System.IO.Path.Combine(parentPath, folderName);
|
||||
|
||||
await FileManagerClient.CreateFolderAsync(newPath);
|
||||
|
||||
// Add folder to workspace directly
|
||||
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
|
||||
var folderDto = new ScriptFolderDto(folderName, level, [], []);
|
||||
Workspace.AddFolder(folderDto, parentFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to create folder: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAddFile(MouseEventArgs e)
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateFileDialog>("Create New File", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parentFolder = Workspace.SelectedFolder;
|
||||
var parentPath = parentFolder?.Path ?? "";
|
||||
var newPath = string.IsNullOrEmpty(parentPath) ? fileName : System.IO.Path.Combine(parentPath, fileName);
|
||||
|
||||
await FileManagerClient.CreateFileAsync(newPath, "");
|
||||
|
||||
// Add file to workspace directly
|
||||
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
|
||||
var fileDto = new ScriptFileDto(fileName, level, "");
|
||||
Workspace.AddFile(fileDto, parentFolder);
|
||||
|
||||
// Expand parent folder if it exists (ChildrenChanged event will handle the expansion)
|
||||
// The expansion will be handled by FolderExplorerItem.OnFolderChildrenChanged
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to create file: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSaveAll(MouseEventArgs e)
|
||||
{
|
||||
// Save all modified files
|
||||
var modifiedFiles = GetAllModifiedFiles(Workspace.Folders).Concat(Workspace.Files.Where(f => f.IsModified));
|
||||
|
||||
foreach (var file in modifiedFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
await FileManagerClient.SaveFileAsync(file.Path, file.Code);
|
||||
file.Saved();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to save file '{file.Name}': {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private IEnumerable<ScriptFile> GetAllModifiedFiles(IEnumerable<ScriptFolder> folders)
|
||||
{
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
foreach (var file in folder.Files.Where(f => f.IsModified))
|
||||
{
|
||||
yield return file;
|
||||
}
|
||||
|
||||
foreach (var modifiedFile in GetAllModifiedFiles(folder.Folders))
|
||||
{
|
||||
yield return modifiedFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleBackup(MouseEventArgs e)
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<CreateBackupDialog>("Create Backup", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string backupName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var backupFileName = await FileManagerClient.CreateBackupAsync(backupName);
|
||||
Snackbar.Add($"Backup created successfully: {backupFileName}", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to create backup: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRestore(MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var backups = await FileManagerClient.ListBackupsAsync();
|
||||
|
||||
var parameters = new DialogParameters<RestoreBackupDialog>
|
||||
{
|
||||
{ x => x.Backups, backups }
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<RestoreBackupDialog>("Restore Backup", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is not null && !result.Canceled && result.Data is string backupFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await FileManagerClient.RestoreBackupAsync(backupFileName, replaceExisting: true);
|
||||
|
||||
// Request edit permission before reinitializing workspace
|
||||
try
|
||||
{
|
||||
await FileManagerClient.RequestEditPermissionAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore if edit permission cannot be requested (e.g., state is not Idle)
|
||||
}
|
||||
|
||||
// Reinitialize workspace with new root folder
|
||||
var rootFolder = await FileManagerClient.GetRootFolderAsync();
|
||||
await Workspace.ReinitializeAsync(rootFolder);
|
||||
|
||||
// Reset selections
|
||||
Workspace.SelectedFile = null;
|
||||
Workspace.SelectedFolder = null;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to restore backup: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to list backups: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnFileCreated(string path, string? userId)
|
||||
{
|
||||
// Skip if this is our own action (we already updated workspace)
|
||||
// Only handle actions from other clients
|
||||
try
|
||||
{
|
||||
var file = Workspace.FindFileByPath(path);
|
||||
if (file == null)
|
||||
{
|
||||
// File doesn't exist in workspace, need to add it
|
||||
// Parse path to get name and parent
|
||||
var pathParts = path.Split(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||
var fileName = pathParts[^1];
|
||||
var parentPath = pathParts.Length > 1
|
||||
? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), pathParts.Take(pathParts.Length - 1))
|
||||
: "";
|
||||
|
||||
var parentFolder = string.IsNullOrEmpty(parentPath) ? null : Workspace.FindFolderByPath(parentPath);
|
||||
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
|
||||
|
||||
// Get file content from server (we need to read it)
|
||||
// For now, create with empty content - it will be loaded when opened
|
||||
var fileDto = new ScriptFileDto(fileName, level, "");
|
||||
Workspace.AddFile(fileDto, parentFolder);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error updating workspace after file creation: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFolderCreated(string path, string? userId)
|
||||
{
|
||||
// Skip if this is our own action (we already updated workspace)
|
||||
// Only handle actions from other clients
|
||||
try
|
||||
{
|
||||
var folder = Workspace.FindFolderByPath(path);
|
||||
if (folder == null)
|
||||
{
|
||||
// Folder doesn't exist in workspace, need to add it
|
||||
// Parse path to get name and parent
|
||||
var pathParts = path.Split(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
|
||||
var folderName = pathParts[^1];
|
||||
var parentPath = pathParts.Length > 1
|
||||
? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), pathParts.Take(pathParts.Length - 1))
|
||||
: "";
|
||||
|
||||
var parentFolder = string.IsNullOrEmpty(parentPath) ? null : Workspace.FindFolderByPath(parentPath);
|
||||
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
|
||||
|
||||
var folderDto = new ScriptFolderDto(folderName, level, [], []);
|
||||
Workspace.AddFolder(folderDto, parentFolder);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error updating workspace after folder creation: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileDeleted(string path, string? userId)
|
||||
{
|
||||
// Remove file from workspace directly
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = Workspace.FindFileByPath(path);
|
||||
if (file != null)
|
||||
{
|
||||
Workspace.RemoveFile(file);
|
||||
// RootChanged event will trigger OnWorkspaceRootChanged which calls StateHasChanged
|
||||
// But we also call StateHasChanged here to ensure immediate UI update
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error updating workspace after file deletion: {ex.Message}", Severity.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFolderDeleted(string path, string? userId)
|
||||
{
|
||||
// Remove folder from workspace directly
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var folder = Workspace.FindFolderByPath(path);
|
||||
if (folder != null)
|
||||
{
|
||||
Workspace.RemoveFolder(folder);
|
||||
// RootChanged event will trigger OnWorkspaceRootChanged which calls StateHasChanged
|
||||
// But we also call StateHasChanged here to ensure immediate UI update
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error updating workspace after folder deletion: {ex.Message}", Severity.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Workspace.RootChanged -= OnWorkspaceRootChanged;
|
||||
|
||||
// Unsubscribe from FileManagerHubClient events
|
||||
FileManagerClient.FileCreated -= OnFileCreated;
|
||||
FileManagerClient.FolderCreated -= OnFolderCreated;
|
||||
FileManagerClient.FileDeleted -= OnFileDeleted;
|
||||
FileManagerClient.FolderDeleted -= OnFolderDeleted;
|
||||
|
||||
// Unregister Esc key handler
|
||||
if (_jsModule != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsModule.InvokeVoidAsync("unregisterEscapeKeyHandler");
|
||||
await _jsModule.DisposeAsync();
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// Ignore if JS context is disconnected
|
||||
}
|
||||
}
|
||||
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user