LANCommander/LANCommander.Server/UI/Components/ScriptEditor.razor
Pat Hartl 885032547b Improve script editor with context-aware completions, validation, and UX
- Filter variable completions and hover by script type so only relevant variables appear (e.g. $AllocatedKey only in KeyChange scripts)
- Add inline validation warnings for package scripts missing New-Package and for variables used outside their valid script type
- Extend debug console to support tools and redistributables, add stop button and elapsed time display
- Disable minimap, enable bracket pair colorization and word wrap
- Add snippet insertion via Monaco snippet controller with selection replacement support
- Add script templates that auto-populate on new script creation
- Add Variables dropdown in editor toolbar based on current script type
- Change Add Script button to type-selection dropdown, auto-populate script name from type, remove Type field from editor dialog
- Move Requires Admin checkbox to toolbar row, move toolbar and editor outside Form to prevent dropdown clicks from closing the modal
- Set MaskClosable=false on script editor modals so Monaco autocomplete clicks don't dismiss the dialog
2026-05-10 19:33:48 -05:00

185 lines
5.6 KiB
Text

@using System.Linq.Expressions
@using LANCommander.SDK.Extensions
@using LANCommander.SDK.Enums
@inject ScriptService ScriptService
@inject ModalService ModalService
@inject IMessageService MessageService
@inject ILogger<ScriptEditor> Logger
<Flex Direction="FlexDirection.Vertical" Gap="FlexGap.Large">
<DataTable
@ref="Table"
TItem="Script"
HidePagination
Responsive
Size="TableSize.Small"
Query="GetQueryExpression()">
<BoundDataColumn Property="s => s.Name" />
<BoundDataColumn Property="s => s.Type">
@context.Type.GetDisplayName()
</BoundDataColumn>
<BoundDataColumn Title="Created By" Property="s => s.CreatedBy != null ? s.CreatedBy.UserName : String.Empty" Include="CreatedBy">
@context.CreatedBy?.UserName
</BoundDataColumn>
<BoundDataColumn Property="s => s.CreatedOn">
<LocalTime Value="context.CreatedOn" />
</BoundDataColumn>
<DataActions TData="string">
<Button OnClick="() => Edit(context.Id)" Icon="@IconType.Outline.Edit" Type="ButtonType.Text"/>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this script?">
<Button Icon="@IconType.Outline.Close" Type="ButtonType.Text" Danger/>
</Popconfirm>
</DataActions>
</DataTable>
<Flex Gap="FlexGap.Small" Justify="FlexJustify.End">
<Dropdown>
<Overlay>
<Menu>
@foreach (var type in GetAllowedTypes())
{
<MenuItem OnClick="() => Add(type)">
@type.GetDisplayName()
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="ButtonType.Primary">Add Script</Button>
</ChildContent>
</Dropdown>
</Flex>
</Flex>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
@code {
[CascadingParameter(Name = "GameId")] public Guid? GameId { get; set; }
[CascadingParameter(Name = "RedistributableId")] public Guid? RedistributableId { get; set; }
[CascadingParameter(Name = "ServerId")] public Guid? ServerId { get; set; }
[CascadingParameter(Name = "ToolId")] public Guid? ToolId { get; set; }
[Parameter] public IEnumerable<ScriptType> AllowedTypes { get; set; }
Expression<Func<Script, bool>> Query = s => s.GameId == Guid.Empty && s.RedistributableId == Guid.Empty && s.ServerId == Guid.Empty && s.ToolId == Guid.Empty;
DataTable<Script> Table;
Archive _archive;
IEnumerable<ScriptType> GetAllowedTypes()
{
if (AllowedTypes != null)
return AllowedTypes;
return Enum.GetValues<ScriptType>();
}
Expression<Func<Script, bool>> GetQueryExpression()
{
if (GameId.HasValue)
return s => s.GameId == GameId;
if (RedistributableId.HasValue)
return s => s.RedistributableId == RedistributableId;
if (ServerId.HasValue)
return s => s.ServerId == ServerId;
if (ToolId.HasValue)
return s => s.ToolId == ToolId;
return s =>
(s.GameId == null || s.GameId == Guid.Empty)
&&
(s.RedistributableId == null || s.RedistributableId == Guid.Empty)
&&
(s.ServerId == null || s.ServerId == Guid.Empty)
&&
(s.ToolId == null || s.ToolId == Guid.Empty);
}
async void Add(ScriptType scriptType)
{
var modalOptions = new ModalOptions()
{
Title = $"Add {scriptType.GetDisplayName()} Script",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
MaskClosable = false,
OkText = "Save"
};
var options = new ScriptEditorOptions()
{
ScriptType = scriptType,
AllowedTypes = AllowedTypes,
GameId = GameId,
RedistributableId = RedistributableId,
ServerId = ServerId,
ToolId = ToolId,
};
var modalRef = await ModalService.CreateModalAsync<ScriptEditorDialog, ScriptEditorOptions, Script>(modalOptions, options);
modalRef.OnOk = async (script) =>
{
await Table.ReloadAsync();
};
}
async void Edit(Guid scriptId)
{
var modalOptions = new ModalOptions()
{
Title = "Edit Script",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
MaskClosable = false,
OkText = "Save"
};
var options = new ScriptEditorOptions()
{
ScriptId = scriptId,
AllowedTypes = AllowedTypes,
GameId = GameId,
RedistributableId = RedistributableId,
ServerId = ServerId,
ToolId = ToolId,
};
var modalRef = await ModalService.CreateModalAsync<ScriptEditorDialog, ScriptEditorOptions, Script>(modalOptions, options);
modalRef.OnOk = async (script) =>
{
await Table.ReloadAsync();
};
}
async Task Delete(Script script = null)
{
try
{
if (script != null)
await ScriptService.DeleteAsync(script);
await Table.ReloadAsync();
MessageService.Success("Script deleted!");
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not delete script");
MessageService.Error("Could not delete script!");
}
}
}