feat(adminui): inline script-source editor in the virtual-tag modal
This commit is contained in:
@@ -47,7 +47,8 @@
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="vtag-script">Script</label>
|
||||
<InputSelect id="vtag-script" @bind-Value="_form.ScriptId" class="form-select form-select-sm">
|
||||
<InputSelect id="vtag-script" @bind-Value="_form.ScriptId"
|
||||
@bind-Value:after="OnScriptChangedAsync" class="form-select form-select-sm">
|
||||
<option value="">— pick script —</option>
|
||||
@foreach (var (id, display) in Scripts)
|
||||
{
|
||||
@@ -57,6 +58,48 @@
|
||||
<ValidationMessage For="@(() => _form.ScriptId)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Inline script-source editor. Shown only when a script is bound. This panel saves
|
||||
the SHARED Script row on its OWN concurrency-guarded button — it is deliberately
|
||||
separate from the virtual-tag Create/Save below and never touches _form or closes
|
||||
the modal. *@
|
||||
@if (!string.IsNullOrEmpty(_form.ScriptId) && _scriptLoaded)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Script source</span>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none"
|
||||
@onclick="ToggleScriptPanel">
|
||||
@(_scriptExpanded ? "Hide" : "Edit source")
|
||||
</button>
|
||||
</div>
|
||||
@if (_scriptExpanded)
|
||||
{
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning py-2 small mb-2">
|
||||
Editing shared script "<span class="mono">@_scriptName</span>" — used by
|
||||
@_scriptUsageCount virtual tag(s). Changes affect all of them.
|
||||
</div>
|
||||
<MonacoEditor @bind-Value="_scriptSource" Height="300px" />
|
||||
@if (!string.IsNullOrWhiteSpace(_scriptError))
|
||||
{
|
||||
<div class="text-danger small mt-2">@_scriptError</div>
|
||||
}
|
||||
else if (_scriptSaved)
|
||||
{
|
||||
<div class="text-success small mt-2">Script saved.</div>
|
||||
}
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
@onclick="SaveScriptAsync" disabled="@_scriptSaving">
|
||||
@if (_scriptSaving) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
Save script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Change-triggered</label>
|
||||
@@ -92,8 +135,8 @@
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@_busy">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" disabled="@_busy">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@(_busy || _scriptSaving)">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" disabled="@(_busy || _scriptSaving)">
|
||||
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
@(IsNew ? "Create" : "Save changes")
|
||||
</button>
|
||||
@@ -135,8 +178,37 @@
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
// Tracks which open this modal last loaded for, so unrelated Blazor Server re-renders don't
|
||||
// rebuild _form / reload the script source and clobber in-progress edits. Null while closed.
|
||||
private string? _loadedKey;
|
||||
|
||||
// Inline script-source editor state. Independent of _form and the virtual-tag save.
|
||||
private string _scriptSource = "";
|
||||
private bool _scriptLoaded;
|
||||
private byte[] _scriptRowVersion = Array.Empty<byte>();
|
||||
private string? _scriptName;
|
||||
private int _scriptUsageCount;
|
||||
private bool _scriptExpanded;
|
||||
private bool _scriptSaving;
|
||||
private bool _scriptSaved;
|
||||
private string? _scriptError;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!Visible)
|
||||
{
|
||||
_loadedKey = null; // closed → next open reloads fresh
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against unrelated re-renders. In Blazor Server any live-status push re-invokes
|
||||
// OnParametersSetAsync; without this the rebuild + script reload below would silently
|
||||
// discard whatever the operator has typed. Only rebuild when the modal OPENS or the
|
||||
// target entity CHANGES.
|
||||
var key = IsNew ? "<new>" : Existing?.VirtualTagId;
|
||||
if (key == _loadedKey) return; // same open, re-render → preserve in-progress form + script edits
|
||||
_loadedKey = key;
|
||||
|
||||
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
||||
if (IsNew)
|
||||
{
|
||||
@@ -157,6 +229,105 @@
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
_scriptExpanded = false;
|
||||
|
||||
// Load the bound script's source for the inline panel when editing an existing tag that
|
||||
// already has a script (the open-load path; selection changes go through OnScriptChangedAsync).
|
||||
await LoadScriptSourceAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the inline-editor state from the currently selected script. Clears it when no script is
|
||||
/// bound. This drives both the open-load and the post-selection refresh, and never touches _form.
|
||||
/// </summary>
|
||||
private async Task LoadScriptSourceAsync()
|
||||
{
|
||||
_scriptSaved = false;
|
||||
_scriptError = null;
|
||||
|
||||
if (string.IsNullOrEmpty(_form.ScriptId))
|
||||
{
|
||||
_scriptSource = "";
|
||||
_scriptLoaded = false;
|
||||
_scriptRowVersion = Array.Empty<byte>();
|
||||
_scriptName = null;
|
||||
_scriptUsageCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var loaded = await Svc.GetScriptSourceAsync(_form.ScriptId);
|
||||
if (loaded is null)
|
||||
{
|
||||
_scriptSource = "";
|
||||
_scriptLoaded = false;
|
||||
_scriptRowVersion = Array.Empty<byte>();
|
||||
_scriptName = null;
|
||||
_scriptUsageCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_scriptSource = loaded.Value.SourceCode;
|
||||
_scriptLoaded = true;
|
||||
_scriptRowVersion = loaded.Value.RowVersion;
|
||||
_scriptName = loaded.Value.Name;
|
||||
_scriptUsageCount = await Svc.CountVirtualTagsUsingScriptAsync(_form.ScriptId);
|
||||
}
|
||||
|
||||
/// <summary>Refreshes the inline script panel when the operator picks a different script.</summary>
|
||||
private Task OnScriptChangedAsync() => LoadScriptSourceAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the inline script-source panel. Clears the stale "Script saved." banner when
|
||||
/// (re)expanding so a collapse/expand cycle doesn't re-show a confirmation from an earlier save.
|
||||
/// </summary>
|
||||
private void ToggleScriptPanel()
|
||||
{
|
||||
_scriptExpanded = !_scriptExpanded;
|
||||
if (_scriptExpanded)
|
||||
{
|
||||
_scriptSaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the edited script body via its own RowVersion-guarded service call. This is wholly
|
||||
/// separate from the virtual-tag save: it does not touch _form, does not raise OnSaved, and does
|
||||
/// not close the modal. On success it re-fetches the source so the panel holds a fresh token.
|
||||
/// </summary>
|
||||
private async Task SaveScriptAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_form.ScriptId) || !_scriptLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scriptSaving = true;
|
||||
_scriptSaved = false;
|
||||
_scriptError = null;
|
||||
try
|
||||
{
|
||||
var result = await Svc.UpdateScriptSourceAsync(_form.ScriptId, _scriptSource, _scriptRowVersion);
|
||||
if (result.Ok)
|
||||
{
|
||||
// Re-fetch to refresh the concurrency token for any subsequent save.
|
||||
var reloaded = await Svc.GetScriptSourceAsync(_form.ScriptId);
|
||||
if (reloaded is not null)
|
||||
{
|
||||
_scriptSource = reloaded.Value.SourceCode;
|
||||
_scriptRowVersion = reloaded.Value.RowVersion;
|
||||
_scriptName = reloaded.Value.Name;
|
||||
}
|
||||
_scriptSaved = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_scriptError = result.Error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scriptSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
|
||||
Reference in New Issue
Block a user