fix(central-ui): Flow Debugger — remount ParameterValueForm per script so stale form state cannot block Run
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -235,7 +235,12 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div data-test="flow-script-params">
|
<div data-test="flow-script-params">
|
||||||
<ParameterValueForm ParameterDefinitions="@script.ParameterDefinitions"
|
@* Keyed on the script so a change of script REMOUNTS the form: its raw
|
||||||
|
text and parse errors are per-instance state that must not survive
|
||||||
|
(and two scripts may even declare byte-identical definitions, which
|
||||||
|
would otherwise skip the reparse and its ValidChanged entirely). *@
|
||||||
|
<ParameterValueForm @key="@script.CanonicalName"
|
||||||
|
ParameterDefinitions="@script.ParameterDefinitions"
|
||||||
Values="_scriptParams"
|
Values="_scriptParams"
|
||||||
ValuesChanged="@(v => _scriptParams = v)"
|
ValuesChanged="@(v => _scriptParams = v)"
|
||||||
ValidChanged="@(ok => _scriptParamsValid = ok)" />
|
ValidChanged="@(ok => _scriptParamsValid = ok)" />
|
||||||
|
|||||||
@@ -330,6 +330,11 @@ public partial class FlowDebugger : ComponentBase, IDisposable
|
|||||||
ResetResult();
|
ResetResult();
|
||||||
_predicted = null;
|
_predicted = null;
|
||||||
_state = FlowState.Idle;
|
_state = FlowState.Idle;
|
||||||
|
// Inputs are per-entry-point, as in OnScriptChangedAsync: never carry a script's value or
|
||||||
|
// parameter tree (or its validity) across a switch of entry kind.
|
||||||
|
_scriptParams = new();
|
||||||
|
_scriptParamsValid = false;
|
||||||
|
_valueChangeValue = string.Empty;
|
||||||
await PersistAsync(StorageEntry, entry.ToString());
|
await PersistAsync(StorageEntry, entry.ToString());
|
||||||
if (_entry == EntryKind.Method && _selectedMethodId > 0)
|
if (_entry == EntryKind.Method && _selectedMethodId > 0)
|
||||||
await OnMethodChangedAsync();
|
await OnMethodChangedAsync();
|
||||||
|
|||||||
@@ -691,4 +691,64 @@ public class FlowDebuggerPageTests : BunitContext
|
|||||||
cut.WaitForAssertion(() => Assert.Equal("Manual", cut.Find("select[data-test='flow-script-select']").GetAttribute("value")));
|
cut.WaitForAssertion(() => Assert.Equal("Manual", cut.Find("select[data-test='flow-script-select']").GetAttribute("value")));
|
||||||
_ = _flow.Received().PredictScriptAsync(7, "Manual", Arg.Any<CancellationToken>());
|
_ = _flow.Received().PredictScriptAsync(7, "Manual", Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Two call-kind scripts declaring BYTE-IDENTICAL parameter definitions — the case that makes
|
||||||
|
/// <c>ParameterValueForm</c> skip its reparse (and its <c>ValidChanged</c>) and keeps the previous
|
||||||
|
/// script's raw text and parse errors. The page must remount the form per script instead.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly FlowScriptOption Manual2 = new("Manual2", "Call", null,
|
||||||
|
"""{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}""", 20);
|
||||||
|
|
||||||
|
private void WireInstanceWithTwinScripts()
|
||||||
|
{
|
||||||
|
_templates.GetInstancesBySiteIdAsync(1, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<Instance>>([new Instance("Sim01") { Id = 7, SiteId = 1, TemplateId = 10 }]));
|
||||||
|
_flow.GetInstanceScriptsAsync(7, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<FlowScriptOption>>([Manual, Manual2, Tick]));
|
||||||
|
_flow.PredictScriptAsync(7, Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(ThreeNodePrediction());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IRenderedComponent<FlowDebuggerPage> ChooseScriptWithAnInvalidParameter()
|
||||||
|
{
|
||||||
|
var cut = Render<FlowDebuggerPage>();
|
||||||
|
cut.Find("button[data-test='flow-entry-script']").Click();
|
||||||
|
cut.Find("select[data-test='flow-site-select']").Change("1");
|
||||||
|
cut.Find("select[data-test='flow-instance-select']").Change("7");
|
||||||
|
cut.Find("select[data-test='flow-script-select']").Change("Manual");
|
||||||
|
cut.Find("#param-n").Input("abc"); // not an integer: the form parks a parse error
|
||||||
|
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||||
|
return cut;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptEntry_SwitchingBetweenCallKindScripts_ResetsTheFormAndValidity()
|
||||||
|
{
|
||||||
|
RegisterServices();
|
||||||
|
WireInstanceWithTwinScripts();
|
||||||
|
var cut = ChooseScriptWithAnInvalidParameter();
|
||||||
|
|
||||||
|
cut.Find("select[data-test='flow-script-select']").Change("Manual2");
|
||||||
|
|
||||||
|
// A fresh form: no echo of the previous script's text, and no stale parse error keeping
|
||||||
|
// Run disabled invisibly — Run is disabled only because the required field is unset.
|
||||||
|
Assert.Equal(string.Empty, cut.Find("#param-n").GetAttribute("value"));
|
||||||
|
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||||
|
|
||||||
|
cut.Find("#param-n").Input("5");
|
||||||
|
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptEntry_SwitchingFromAnInvalidFormToANoParameterScript_EnablesRun()
|
||||||
|
{
|
||||||
|
RegisterServices();
|
||||||
|
WireInstanceWithTwinScripts();
|
||||||
|
var cut = ChooseScriptWithAnInvalidParameter();
|
||||||
|
|
||||||
|
cut.Find("select[data-test='flow-script-select']").Change("Tick");
|
||||||
|
|
||||||
|
Assert.Contains("takes no parameters", cut.Find("[data-test='flow-inputs']").TextContent);
|
||||||
|
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user