From 912e519954b9c10873b944ec6d94c6cdc948853e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 8 Sep 2026 01:15:40 -0400 Subject: [PATCH] =?UTF-8?q?fix(central-ui):=20ParameterValueForm=20?= =?UTF-8?q?=E2=80=94=20raise=20ValidChanged=20on=20schema=20set=20so=20req?= =?UTF-8?q?uired-field=20validity=20is=20never=20stale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- .../Shared/ParameterValueForm.razor | 38 +++++++++++++++++++ .../Design/ParameterValueFormTests.cs | 28 ++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor index 5117bc64..0d1ab5f3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor @@ -77,6 +77,44 @@ else _shapes = ScriptParameterNames.ParseShapes(ParameterDefinitions); _rootSchema = await ParseRootAsync(); _topLevelFields = _rootSchema?.Type == "object" ? _rootSchema.Fields : Array.Empty(); + + // Report validity for the freshly-(re)parsed schema so a caller gating a Run + // button on ValidChanged never starts (or carries over from a prior script) + // a stale "valid" reading when required fields are unset. This deliberately + // does NOT call Validate()/populate _parseErrors: the component has no + // "untouched field" concept -- RenderError paints whatever is in + // _parseErrors unconditionally -- so doing that here would immediately + // paint every required field red before the user has touched anything. The + // silent check below computes the same answer without that side effect; + // _parseErrors (and the visible red text) still only appears once editing + // drives it through Emit()/Validate() as before. + await ValidChanged.InvokeAsync(ComputeIsValid()); + } + + // Mirrors Validate()'s schema pass (plus any raw-text parse markers already + // parked in _parseErrors) WITHOUT writing to _parseErrors, so it can run on + // every schema (re)parse without painting untouched required fields red. + private bool ComputeIsValid() + { + if (_parseErrors.Count > 0) + { + return false; + } + if (_rootSchema is null) + { + return true; + } + try + { + var element = JsonSerializer.SerializeToElement(Values); + var errors = new List(); + _rootSchema.Validate(element, string.Empty, errors); + return errors.Count == 0; + } + catch (JsonException) + { + return false; + } } /// diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ParameterValueFormTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ParameterValueFormTests.cs index 023e2f0e..c7b1f419 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ParameterValueFormTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ParameterValueFormTests.cs @@ -297,4 +297,32 @@ public class ParameterValueFormTests : BunitContext input.Input("5"); Assert.True(valid.Last()); } + + [Fact] + public void ValidChanged_ReportsFalseOnFirstRender_WhenARequiredFieldIsEmpty() + { + var valid = new List(); + var cut = Render(p => p + .Add(x => x.ParameterDefinitions, """{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}""") + .Add(x => x.ValidChanged, v => valid.Add(v))); + + // Reported without any input from the user... + Assert.False(valid.Last()); + + // ...and no red error text is painted for the untouched required field (the + // component has no "untouched" concept, so the validity check for + // ValidChanged deliberately does not populate _parseErrors on first render). + Assert.Empty(cut.FindAll(".text-danger")); + } + + [Fact] + public void ValidChanged_ReportsTrueOnFirstRender_WhenNothingIsRequired() + { + var valid = new List(); + var cut = Render(p => p + .Add(x => x.ParameterDefinitions, """{"type":"object","properties":{"n":{"type":"integer"}}}""") + .Add(x => x.ValidChanged, v => valid.Add(v))); + + Assert.True(valid.Last()); + } }