feat(adminui): create-new-script from the inline virtual-tag panel
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
<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)
|
||||
@foreach (var (id, display) in _scripts)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
}
|
||||
@@ -59,6 +59,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* No script bound yet → offer a one-click "New script" action that creates a
|
||||
fresh (blank) Script row, binds this virtual tag to it, and expands the inline
|
||||
editor below so the operator can author the body without leaving the modal. *@
|
||||
@if (string.IsNullOrEmpty(_form.ScriptId))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
@onclick="CreateNewScriptAsync" disabled="@_scriptCreating">
|
||||
@if (_scriptCreating) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
New script
|
||||
</button>
|
||||
<span class="form-text ms-2">Creates a blank script bound to this virtual tag.</span>
|
||||
@if (!string.IsNullOrWhiteSpace(_scriptCreateError))
|
||||
{
|
||||
<div class="text-danger small mt-2">@_scriptCreateError</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
|
||||
@@ -178,6 +197,10 @@
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
// Working copy of the selectable scripts: the host-supplied list plus any script created inline via
|
||||
// "New script", so the freshly-created (and now-bound) script is a real option in the dropdown.
|
||||
private List<(string Id, string Display)> _scripts = new();
|
||||
|
||||
// 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;
|
||||
@@ -193,6 +216,10 @@
|
||||
private bool _scriptSaved;
|
||||
private string? _scriptError;
|
||||
|
||||
// "New script" action state (shown only when no script is bound yet).
|
||||
private bool _scriptCreating;
|
||||
private string? _scriptCreateError;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!Visible)
|
||||
@@ -209,6 +236,10 @@
|
||||
if (key == _loadedKey) return; // same open, re-render → preserve in-progress form + script edits
|
||||
_loadedKey = key;
|
||||
|
||||
// Snapshot the host-supplied script options; "New script" appends to this working copy so the
|
||||
// newly-created script is selectable without mutating the parent's parameter.
|
||||
_scripts = Scripts.Select(s => (s.Id, s.Display)).ToList();
|
||||
|
||||
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
||||
if (IsNew)
|
||||
{
|
||||
@@ -276,6 +307,43 @@
|
||||
/// <summary>Refreshes the inline script panel when the operator picks a different script.</summary>
|
||||
private Task OnScriptChangedAsync() => LoadScriptSourceAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brand-new (blank) script, binds this virtual tag to it, and expands the inline editor
|
||||
/// so the operator can author the body without leaving the modal. The new script name seeds from the
|
||||
/// virtual tag's name when set. This does NOT save the virtual tag — the binding lives in _form until
|
||||
/// the operator clicks Create/Save below; only the new Script row is persisted here. On failure the
|
||||
/// inline error is surfaced and the binding is left untouched.
|
||||
/// </summary>
|
||||
private async Task CreateNewScriptAsync()
|
||||
{
|
||||
_scriptCreating = true;
|
||||
_scriptCreateError = null;
|
||||
try
|
||||
{
|
||||
var seedName = string.IsNullOrWhiteSpace(_form.Name) ? "New script" : $"{_form.Name} script";
|
||||
var result = await Svc.CreateScriptAsync(seedName);
|
||||
if (!result.Ok || string.IsNullOrEmpty(result.CreatedId))
|
||||
{
|
||||
_scriptCreateError = result.Error ?? "Could not create the script.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind the new script and load its (blank) source so the inline editor renders + expands.
|
||||
_form.ScriptId = result.CreatedId;
|
||||
await LoadScriptSourceAsync();
|
||||
// Make the freshly-created script a real option in the dropdown so the select stays coherent.
|
||||
if (!_scripts.Any(s => s.Id == result.CreatedId))
|
||||
{
|
||||
_scripts.Add((result.CreatedId, $"{seedName} (CSharp)"));
|
||||
}
|
||||
_scriptExpanded = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scriptCreating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
|
||||
@@ -418,6 +418,19 @@ public interface IUnsTreeService
|
||||
/// <returns>The scripts projected to <c>(ScriptId, Display)</c> pairs.</returns>
|
||||
Task<IReadOnlyList<(string ScriptId, string Display)>> LoadScriptsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new, empty C# script row so the inline virtual-tag panel can bind a brand-new script
|
||||
/// without leaving the modal. The <c>ScriptId</c> is system-generated (<c>SC-</c> + the first 12 hex
|
||||
/// chars of a fresh <see cref="Guid"/>, mirroring the Script-edit page's convention); the source is
|
||||
/// blank, the <c>SourceHash</c> is the SHA-256 of that blank source (so it stays consistent with the
|
||||
/// inline save), and the language is <c>CSharp</c>. A whitespace-only name falls back to a default.
|
||||
/// The new id is returned via <see cref="UnsMutationResult.CreatedId"/> so the caller can bind it.
|
||||
/// </summary>
|
||||
/// <param name="name">The operator-friendly script name; whitespace falls back to a default.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success carrying the generated <c>SC-…</c> id in <see cref="UnsMutationResult.CreatedId"/>.</returns>
|
||||
Task<UnsMutationResult> CreateScriptAsync(string name, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new equipment-bound virtual tag (plan decision #2 — virtual tags are always scoped
|
||||
/// to an equipment). Fails if the equipment does not exist, if no script is chosen, if neither a
|
||||
|
||||
@@ -1011,6 +1011,30 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> CreateScriptAsync(string name, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// System-generated id, mirroring the Script-edit page's "SC-{12 hex}" convention.
|
||||
var scriptId = $"SC-{Guid.NewGuid().ToString("N")[..12]}";
|
||||
var resolvedName = string.IsNullOrWhiteSpace(name) ? "New script" : name.Trim();
|
||||
|
||||
// Blank source; SourceHash is the SHA-256 of that blank body so it stays consistent with the
|
||||
// inline save (UpdateScriptSourceAsync recomputes it the same way).
|
||||
db.Scripts.Add(new Script
|
||||
{
|
||||
ScriptId = scriptId,
|
||||
Name = resolvedName,
|
||||
Language = "CSharp",
|
||||
SourceCode = "",
|
||||
SourceHash = HashSource(""),
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null, scriptId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the bound script uses the reserved <c>{{equip}}</c> token, the owning equipment must have
|
||||
/// a derivable tag base (≥1 driver tag, all sharing one object prefix). Returns a rejection result
|
||||
|
||||
Reference in New Issue
Block a user