fix(adminui): refresh script dropdown label after inline create

After inline "New script" creates an SC-… id, the entry is now added
to _scripts BEFORE _form.ScriptId is set so the <InputSelect> has a
matching <option> on first render and the displayed label is correct.
Extracts VirtualTagModalHelpers.ResolveScriptLabel as a testable pure
helper (5 new unit tests in VirtualTagScriptDropdownTests).
This commit is contained in:
Joseph Doherty
2026-06-19 02:06:51 -04:00
parent 2dd723e195
commit da57c307a7
3 changed files with 130 additions and 4 deletions
@@ -329,14 +329,16 @@
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.
// Add the new script to the options list BEFORE setting the selected value so the
// <InputSelect> can resolve the label on first render — if we set ScriptId first the
// dropdown has no matching <option> yet and shows a blank/stale label.
if (!_scripts.Any(s => s.Id == result.CreatedId))
{
_scripts.Add((result.CreatedId, $"{seedName} (CSharp)"));
}
// Bind the new script and load its (blank) source so the inline editor renders + expands.
_form.ScriptId = result.CreatedId;
await LoadScriptSourceAsync();
_scriptExpanded = true;
}
finally
@@ -0,0 +1,39 @@
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns;
/// <summary>
/// Pure helpers extracted from <see cref="VirtualTagModal"/> to support unit testing of state
/// logic that is otherwise embedded in Razor component lifecycle methods.
/// </summary>
internal static class VirtualTagModalHelpers
{
/// <summary>
/// Resolves the display label for the given <paramref name="selectedId"/> from the supplied
/// dropdown <paramref name="options"/> list.
/// </summary>
/// <param name="options">
/// The current in-memory options, each as an <c>(Id, Display)</c> pair. This list MUST include
/// the newly-created script entry BEFORE <c>selectedId</c> is set on the form; otherwise the
/// rendered <c>&lt;select&gt;</c> will have no matching <c>&lt;option&gt;</c> and show a blank
/// label.
/// </param>
/// <param name="selectedId">The id of the currently selected script (may be null or empty).</param>
/// <returns>
/// The display label for the selected id, or <c>null</c> when <paramref name="selectedId"/> is
/// empty or is not present in <paramref name="options"/>.
/// </returns>
internal static string? ResolveScriptLabel(
IEnumerable<(string Id, string Display)> options,
string? selectedId)
{
if (string.IsNullOrEmpty(selectedId))
return null;
foreach (var (id, display) in options)
{
if (id == selectedId)
return display;
}
return null;
}
}
@@ -0,0 +1,85 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// Unit tests for <see cref="VirtualTagModalHelpers.ResolveScriptLabel"/> — the pure helper that
/// resolves a script's display label from the in-memory options list. The bug scenario: after an
/// inline "New script" create, the dropdown label drifted because the new entry was appended to the
/// options list AFTER the selected value was set, so the rendered select had no matching option.
/// The fix is to add the entry first, then set the selected value; this helper captures that
/// invariant.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VirtualTagScriptDropdownTests
{
// --- ResolveScriptLabel ---
[Fact]
public void ResolveScriptLabel_ReturnsNull_WhenOptionsDoNotContainId()
{
// Represents the bug scenario: options list does NOT yet include the newly-created id.
var options = new List<(string Id, string Display)>
{
("SC-existing001", "OldScript (CSharp)"),
};
var label = VirtualTagModalHelpers.ResolveScriptLabel(options, "SC-newscript00");
label.ShouldBeNull();
}
[Fact]
public void ResolveScriptLabel_ReturnsDisplay_WhenOptionsContainId()
{
// Represents the fixed scenario: the new entry is in the options before label is resolved.
var options = new List<(string Id, string Display)>
{
("SC-existing001", "OldScript (CSharp)"),
("SC-newscript00", "MyTag script (CSharp)"),
};
var label = VirtualTagModalHelpers.ResolveScriptLabel(options, "SC-newscript00");
label.ShouldBe("MyTag script (CSharp)");
}
[Fact]
public void ResolveScriptLabel_ReturnsNull_WhenSelectedIdIsEmpty()
{
var options = new List<(string Id, string Display)>
{
("SC-existing001", "OldScript (CSharp)"),
};
var label = VirtualTagModalHelpers.ResolveScriptLabel(options, "");
label.ShouldBeNull();
}
[Fact]
public void ResolveScriptLabel_ReturnsNull_WhenOptionsIsEmpty()
{
var label = VirtualTagModalHelpers.ResolveScriptLabel(
Array.Empty<(string Id, string Display)>(), "SC-newscript00");
label.ShouldBeNull();
}
[Fact]
public void ResolveScriptLabel_ReturnsCorrectLabel_WhenMultipleOptionsExist()
{
var options = new List<(string Id, string Display)>
{
("SC-aaa", "Alpha (CSharp)"),
("SC-bbb", "Beta (CSharp)"),
("SC-ccc", "Gamma (CSharp)"),
};
VirtualTagModalHelpers.ResolveScriptLabel(options, "SC-aaa").ShouldBe("Alpha (CSharp)");
VirtualTagModalHelpers.ResolveScriptLabel(options, "SC-bbb").ShouldBe("Beta (CSharp)");
VirtualTagModalHelpers.ResolveScriptLabel(options, "SC-ccc").ShouldBe("Gamma (CSharp)");
}
}