refactor(centralui): migrate TemplateEdit's four page-embedded modals to the DialogService host (M10 residual)

TemplateEdit was the last page still hand-rolling its own modal chrome after
M10 — T34c only tokenized its backdrops. All four member-authoring forms
(Attribute, Alarm, Native Alarm Source, Script) now open through
IDialogService.ShowAsync, so the single DialogHost in MainLayout owns the
backdrop, focus trap, Escape and focus restoration.

Pattern copied from the already-migrated pages (MoveDataConnectionDialog +
Templates' Move/Rename dialogs): each body is its own component beside the page
taking a DialogContext<bool>, owning its form state, rendering validation and
server errors INLINE while staying open, and closing with Close(true) only once
the save succeeded — at which point the page reloads. An inline RenderFragment
would NOT have worked: DialogHost renders the captured fragment in its own tree,
so the page's StateHasChanged could never refresh it.

Persistence deliberately stayed on the page (it owns TemplateService, the
inherited-member rules, and the repository-direct native-source path) and is
reached through an OnSaveAsync delegate returning null on success or the message
to display. Behaviour preserved verbatim, including the List-attribute encode +
Decode round-trip check, the name/trigger-type read-only-on-edit rules, the
duplicate-native-source-name guard, and NormalizeExecutionTimeout.

TemplateScriptDialog keeps all four tab panels mounted (Monaco and the JSONJoy
island must not tear down on tab switch) and hosts the Test Run panel, which
needs the live unsaved editor buffer, so it injects ScriptAnalysisService
directly and cancels an in-flight run on dispose.

Extraction moved markup that three structural source-scanning tests pinned;
all three were repointed at the new files rather than weakened:
  - TemplateNativeAlarmSourceEditorTests (+ a new test asserting the form is
    host-mounted and the body renders no chrome of its own)
  - AttributeListEditorTests (list-editor reveal now in the dialog body)
  - TestRunWarningTests (Real I/O warning travelled with the script panel)

Build 0/0; CentralUI.Tests 973/973 green.
This commit is contained in:
Joseph Doherty
2026-08-01 11:26:42 -04:00
parent fdfd5e1b27
commit a506b19d17
9 changed files with 1344 additions and 930 deletions
@@ -13,18 +13,28 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design;
/// </summary>
public class TemplateNativeAlarmSourceEditorTests
{
private static string TemplateEditMarkup
private static string DesignComponentsRoot
{
get
{
var dir = AppContext.BaseDirectory;
for (var i = 0; i < 6 && dir is not null; i++)
dir = Directory.GetParent(dir)?.FullName;
return File.ReadAllText(Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI",
"Components", "Pages", "Design", "TemplateEdit.razor"));
return Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI",
"Components", "Pages", "Design");
}
}
private static string TemplateEditMarkup
=> File.ReadAllText(Path.Combine(DesignComponentsRoot, "TemplateEdit.razor"));
// M10 residual: the authoring form moved out of the page into the dialog body
// component hosted by IDialogService.ShowAsync. The field-level assertions
// below follow it there; the tab, the connection filtering, and the CRUD
// wiring all still live on the page.
private static string NativeSourceDialogMarkup
=> File.ReadAllText(Path.Combine(DesignComponentsRoot, "TemplateNativeAlarmSourceDialog.razor"));
[Fact]
public void TemplateEditor_HasNativeAlarmsTab()
{
@@ -38,19 +48,39 @@ public class TemplateNativeAlarmSourceEditorTests
[Fact]
public void NativeAlarmsForm_HasConnectionSourceFilterAndLockFields()
{
var markup = TemplateEditMarkup;
var pageMarkup = TemplateEditMarkup;
// Connection dropdown filtered to alarm-capable protocols via the
// single-source-of-truth Commons helper. The OpcUa/MxGateway literal set
// now lives in AlarmCapableProtocols (Commons) — pinned by
// AlarmCapableProtocolsTests — so this page only needs to delegate to it.
Assert.Contains("_alarmCapableConnections", markup);
Assert.Contains("AlarmCapableProtocols.IsAlarmCapable", markup);
// The authoring form fields.
Assert.Contains("@bind=\"_nasName\"", markup);
Assert.Contains("@bind=\"_nasConnection\"", markup);
Assert.Contains("@bind=\"_nasSourceRef\"", markup);
Assert.Contains("@bind=\"_nasFilter\"", markup);
Assert.Contains("@bind=\"_nasIsLocked\"", markup);
Assert.Contains("_alarmCapableConnections", pageMarkup);
Assert.Contains("AlarmCapableProtocols.IsAlarmCapable", pageMarkup);
// …and hands the filtered list to the dialog body.
Assert.Contains("AlarmCapableConnections=", pageMarkup);
// The authoring form fields now live in the dialog body component.
var dialogMarkup = NativeSourceDialogMarkup;
Assert.Contains("@bind=\"_nasName\"", dialogMarkup);
Assert.Contains("@bind=\"_nasConnection\"", dialogMarkup);
Assert.Contains("@bind=\"_nasSourceRef\"", dialogMarkup);
Assert.Contains("@bind=\"_nasFilter\"", dialogMarkup);
Assert.Contains("@bind=\"_nasIsLocked\"", dialogMarkup);
}
[Fact]
public void NativeAlarmsForm_IsHostedByTheDialogService()
{
// M10 residual: the page-embedded modal is gone — the form opens through
// IDialogService.ShowAsync so the host owns the backdrop, focus trap,
// Escape, and focus restoration.
var pageMarkup = TemplateEditMarkup;
Assert.Contains("<TemplateNativeAlarmSourceDialog", pageMarkup);
Assert.DoesNotContain("_showNativeSourceForm", pageMarkup);
// The body renders no chrome of its own (no backdrop / modal shell).
var dialogMarkup = NativeSourceDialogMarkup;
Assert.DoesNotContain("sb-modal-backdrop", dialogMarkup);
Assert.Contains("DialogContext<bool> Context", dialogMarkup);
}
[Fact]
@@ -63,9 +93,9 @@ public class TemplateNativeAlarmSourceEditorTests
Assert.Contains("GetNativeAlarmSourcesByTemplateIdAsync", markup);
Assert.Contains("SaveChangesAsync", markup);
// Add/edit/delete handlers are wired to the UI.
Assert.Contains("BeginAddNativeSource", markup);
Assert.Contains("BeginEditNativeSource", markup);
Assert.Contains("SaveNativeSource", markup);
Assert.Contains("OpenAddNativeSourceDialog", markup);
Assert.Contains("OpenEditNativeSourceDialog", markup);
Assert.Contains("SaveNativeSourceDraftAsync", markup);
Assert.Contains("DeleteNativeSource", markup);
}
}