fix(scripts): gate the source dialog's existence on the held id, not the resolve

The modal's existence was a function of _rows (FindSourceScript() as the
visibility gate), so any render where the row is momentarily unresolvable —
a future reload nulling or replacing the list — unmounts the subtree and
disposes the Close handler's binding id mid-click (GetRequiredEventBindingEntry).
Latent here (_rows is written once, in OnInitializedAsync) but the identical
shape bit ScadaBridge live the moment a page refresh landed, and it presents
as an unattributable intermittent once one does.

ScadaBridge's shape, ported by the family sweep: intent (_sourceScriptId)
gates existence, the resolve drives only content; an unresolvable script
renders a notice with the frame and Close still mounted, and closing remains
the operator's action rather than a side effect of list state.
This commit is contained in:
Joseph Doherty
2026-08-11 06:06:49 -04:00
parent 29a202f5ee
commit 8ab520ef57
@@ -60,21 +60,42 @@ else
</section>
}
@if (FindSourceScript() is { } src)
@* The dialog's EXISTENCE is gated on the held id (operator intent); the resolve drives only its
CONTENT. Gating existence on FindSourceScript() would make the modal a function of _rows, and any
render where the row is momentarily unresolvable (a future reload/poll setting _rows null or
replacing the list) would unmount the subtree and dispose the Close handler's binding id — a
click already in flight then throws GetRequiredEventBindingEntry. Latent today (_rows is written
once, in OnInitializedAsync) but the shape bit ScadaBridge live the moment a refresh landed. *@
@if (_sourceScriptId is not null)
{
var src = FindSourceScript();
<div class="modal-backdrop fade show" style="display:block"></div>
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@(string.IsNullOrWhiteSpace(src.Name) ? src.ScriptId : src.Name)
<span class="mono small text-muted ms-2">@src.ScriptId</span>
@if (src is not null)
{
@(string.IsNullOrWhiteSpace(src.Name) ? src.ScriptId : src.Name)
<span class="mono small text-muted ms-2">@src.ScriptId</span>
}
else
{
<span class="mono">@_sourceScriptId</span>
}
</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseSource"></button>
</div>
<div class="modal-body">
<pre class="mono small mb-0" style="background:var(--zebra-bg);padding:1rem;border-radius:4px;overflow:auto">@src.SourceCode</pre>
@if (src is not null)
{
<pre class="mono small mb-0" style="background:var(--zebra-bg);padding:1rem;border-radius:4px;overflow:auto">@src.SourceCode</pre>
}
else
{
<p class="text-muted mb-0">This script is no longer loaded.</p>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseSource">Close</button>
@@ -99,8 +120,9 @@ else
.ToListAsync();
}
/// <summary>Re-resolves the open dialog's script from the current list on every render; a script
/// that is no longer loaded renders nothing, so the dialog self-closes.</summary>
/// <summary>Re-resolves the open dialog's script from the current list on every render. Drives
/// the dialog's CONTENT only — never its existence (see the comment at the markup): a script
/// that is no longer loaded renders a notice with the frame and Close still mounted.</summary>
private Script? FindSourceScript()
=> _sourceScriptId is null
? null