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:
+132
@@ -0,0 +1,132 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
|
||||
@*
|
||||
M10 residual: body component for the template "Add / Edit Alarm" dialog hosted
|
||||
by IDialogService.ShowAsync. Renders ONLY the form fields + action buttons
|
||||
inside the host's .modal-body; the host owns the backdrop, header, focus trap,
|
||||
Escape, and focus restoration.
|
||||
|
||||
Same contract as TemplateAttributeDialog: OnSaveAsync returns null on success
|
||||
(dialog closes with true) or the error to render inline (dialog stays open).
|
||||
*@
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="alarm-name">Name</label>
|
||||
<input id="alarm-name" type="text" class="form-control" @bind="_name" readonly="@Editing" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="alarm-trigger-type">Trigger Type</label>
|
||||
<select id="alarm-trigger-type" class="form-select" @bind="_triggerType" disabled="@Editing">
|
||||
@foreach (var tt in Enum.GetValues<AlarmTriggerType>())
|
||||
{
|
||||
<option value="@tt">@tt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="alarm-priority">Priority</label>
|
||||
<input id="alarm-priority" type="number" class="form-control" @bind="_priority" min="0" max="1000" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Configuration</label>
|
||||
<AlarmTriggerEditor TriggerType="@_triggerType"
|
||||
Value="@_triggerConfig"
|
||||
ValueChanged="@(v => _triggerConfig = v)"
|
||||
AvailableAttributes="@AvailableAttributes"
|
||||
FallbackPriority="@_priority" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_isLocked" id="alarmLocked" />
|
||||
<label class="form-check-label" for="alarmLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_error != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small" data-test="alarm-form-error">@_error</div></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer px-0 pb-0 mt-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>Host-supplied context: Close(true) on a successful save, Cancel on dismiss.</summary>
|
||||
[Parameter, EditorRequired] public DialogContext<bool> Context { get; set; } = default!;
|
||||
|
||||
/// <summary>True when editing an existing alarm (name + trigger type become read-only).</summary>
|
||||
[Parameter] public bool Editing { get; set; }
|
||||
|
||||
[Parameter] public string InitialName { get; set; } = string.Empty;
|
||||
[Parameter] public int InitialPriority { get; set; }
|
||||
[Parameter] public AlarmTriggerType InitialTriggerType { get; set; }
|
||||
[Parameter] public string? InitialTriggerConfig { get; set; }
|
||||
[Parameter] public bool InitialIsLocked { get; set; }
|
||||
|
||||
/// <summary>Attribute choices offered by the structured trigger editor's picker.</summary>
|
||||
[Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; }
|
||||
= Array.Empty<AlarmAttributeChoice>();
|
||||
|
||||
/// <summary>
|
||||
/// Persists the draft. Returns <c>null</c> on success (the dialog closes with
|
||||
/// <c>true</c>) or the message to render inline, leaving the dialog open.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired] public Func<AlarmDraft, Task<string?>> OnSaveAsync { get; set; } = default!;
|
||||
|
||||
private string _name = string.Empty;
|
||||
private int _priority;
|
||||
private AlarmTriggerType _triggerType;
|
||||
private string? _triggerConfig;
|
||||
private bool _isLocked;
|
||||
private string? _error;
|
||||
private bool _busy;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_name = InitialName;
|
||||
_priority = InitialPriority;
|
||||
_triggerType = InitialTriggerType;
|
||||
_triggerConfig = InitialTriggerConfig;
|
||||
_isLocked = InitialIsLocked;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_busy) return;
|
||||
_error = null;
|
||||
if (string.IsNullOrWhiteSpace(_name)) { _error = "Name is required."; return; }
|
||||
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
var error = await OnSaveAsync(new AlarmDraft(_name, _triggerType, _priority, _triggerConfig, _isLocked));
|
||||
if (error == null)
|
||||
{
|
||||
Context.Close(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The authored alarm as submitted; the page performs the add/update.</summary>
|
||||
/// <param name="Name">Alarm name (ignored on edit — the stored name is fixed).</param>
|
||||
/// <param name="TriggerType">Selected trigger type (ignored on edit — server-fixed).</param>
|
||||
/// <param name="Priority">Priority level.</param>
|
||||
/// <param name="TriggerConfig">Structured trigger configuration payload.</param>
|
||||
/// <param name="IsLocked">Whether instances may override the alarm.</param>
|
||||
public sealed record AlarmDraft(
|
||||
string Name,
|
||||
AlarmTriggerType TriggerType,
|
||||
int Priority,
|
||||
string? TriggerConfig,
|
||||
bool IsLocked);
|
||||
}
|
||||
Reference in New Issue
Block a user