a506b19d17
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.
189 lines
7.4 KiB
Plaintext
189 lines
7.4 KiB
Plaintext
@using ZB.MOM.WW.ScadaBridge.Commons.Types
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
|
|
|
@*
|
|
M10 residual: body component for the template "Add / Edit Attribute" 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.
|
|
|
|
Mirrors MoveDataConnectionDialog: the save round-trip runs from inside the
|
|
body, a failure is shown INLINE and the dialog STAYS OPEN, and success closes
|
|
with Close(true) so the page toasts and reloads. The actual persistence stays
|
|
on the page (TemplateEdit owns TemplateService + the inherited-member rules)
|
|
and is reached through the OnSaveAsync delegate, which returns null on success
|
|
or the error message to display.
|
|
*@
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label class="form-label" for="attr-name">Name</label>
|
|
<input id="attr-name" type="text" class="form-control" @bind="_name" readonly="@Editing" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="attr-data-type">Data Type</label>
|
|
<select id="attr-data-type" class="form-select" value="@_dataType" @onchange="OnDataTypeChanged" disabled="@Editing">
|
|
@foreach (var dt in Enum.GetValues<DataType>())
|
|
{
|
|
<option value="@dt">@dt</option>
|
|
}
|
|
</select>
|
|
@if (Editing)
|
|
{
|
|
<div class="form-text">Data type is fixed once the attribute is created.</div>
|
|
}
|
|
</div>
|
|
@if (_dataType == DataType.List)
|
|
{
|
|
<div class="col-12">
|
|
@* List VALUE stays editable when editing; only DataType/ElementDataType are server-fixed (ShowElementType hides the type select on edit). *@
|
|
<AttributeListEditor @bind-ElementDataType="_elementDataType"
|
|
@bind-Rows="_listRows"
|
|
ShowElementType="@(!Editing)"
|
|
Disabled="false" />
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="col-12">
|
|
<label class="form-label" for="attr-value">Value</label>
|
|
<input id="attr-value" type="text" class="form-control" @bind="_value" />
|
|
</div>
|
|
}
|
|
<div class="col-12">
|
|
<label class="form-label" for="attr-data-source-ref">Data Source Ref</label>
|
|
<input id="attr-data-source-ref" type="text" class="form-control" @bind="_dataSourceRef" placeholder="Tag path" />
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" @bind="_isLocked" id="attrLocked" />
|
|
<label class="form-check-label" for="attrLocked">Locked</label>
|
|
</div>
|
|
</div>
|
|
@if (_error != null)
|
|
{
|
|
<div class="col-12"><div class="text-danger small" data-test="attr-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 attribute (name + data type become read-only).</summary>
|
|
[Parameter] public bool Editing { get; set; }
|
|
|
|
[Parameter] public string InitialName { get; set; } = string.Empty;
|
|
[Parameter] public string? InitialValue { get; set; }
|
|
[Parameter] public DataType InitialDataType { get; set; }
|
|
[Parameter] public DataType InitialElementDataType { get; set; } = DataType.String;
|
|
[Parameter] public IReadOnlyList<string> InitialListRows { get; set; } = Array.Empty<string>();
|
|
[Parameter] public bool InitialIsLocked { get; set; }
|
|
[Parameter] public string? InitialDataSourceRef { get; set; }
|
|
|
|
/// <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<AttributeDraft, Task<string?>> OnSaveAsync { get; set; } = default!;
|
|
|
|
private string _name = string.Empty;
|
|
private string? _value;
|
|
private DataType _dataType;
|
|
private DataType _elementDataType = DataType.String;
|
|
private List<string> _listRows = new();
|
|
private bool _isLocked;
|
|
private string? _dataSourceRef;
|
|
private string? _error;
|
|
private bool _busy;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_name = InitialName;
|
|
_value = InitialValue;
|
|
_dataType = InitialDataType;
|
|
_elementDataType = InitialElementDataType;
|
|
_listRows = InitialListRows.ToList();
|
|
_isLocked = InitialIsLocked;
|
|
_dataSourceRef = InitialDataSourceRef;
|
|
}
|
|
|
|
// Switching the data type clears stale list state so a List ⇄ scalar
|
|
// toggle never carries the other mode's value into the submit.
|
|
private void OnDataTypeChanged(ChangeEventArgs e)
|
|
{
|
|
if (!Enum.TryParse<DataType>((string?)e.Value, out var dt) || dt == _dataType) return;
|
|
_dataType = dt;
|
|
if (dt == DataType.List)
|
|
{
|
|
_value = null;
|
|
_listRows = new();
|
|
if (!AttributeValueCodec.IsValidElementType(_elementDataType))
|
|
_elementDataType = DataType.String;
|
|
}
|
|
else
|
|
{
|
|
_listRows = new();
|
|
}
|
|
}
|
|
|
|
private async Task Submit()
|
|
{
|
|
if (_busy) return;
|
|
_error = null;
|
|
if (string.IsNullOrWhiteSpace(_name)) { _error = "Name is required."; return; }
|
|
|
|
_busy = true;
|
|
try
|
|
{
|
|
var draft = new AttributeDraft(
|
|
_name,
|
|
_dataType,
|
|
_elementDataType,
|
|
_listRows.ToList(),
|
|
_value,
|
|
_dataSourceRef,
|
|
_isLocked);
|
|
|
|
var error = await OnSaveAsync(draft);
|
|
if (error == null)
|
|
{
|
|
Context.Close(true);
|
|
}
|
|
else
|
|
{
|
|
_error = error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_busy = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The authored attribute as submitted. The page resolves the stored value
|
|
/// (list encoding + validation for <see cref="DataType.List"/>) and performs
|
|
/// the add/update, so this record carries the raw form state only.
|
|
/// </summary>
|
|
/// <param name="Name">Attribute name (ignored on edit — the stored name is fixed).</param>
|
|
/// <param name="DataType">Selected data type (fixed on edit).</param>
|
|
/// <param name="ElementDataType">Element scalar type; meaningful only for a List attribute.</param>
|
|
/// <param name="ListRows">Per-element string rows; meaningful only for a List attribute.</param>
|
|
/// <param name="Value">Scalar value; meaningful only for a non-List attribute.</param>
|
|
/// <param name="DataSourceRef">Tag path binding, or null.</param>
|
|
/// <param name="IsLocked">Whether instances may override the attribute.</param>
|
|
public sealed record AttributeDraft(
|
|
string Name,
|
|
DataType DataType,
|
|
DataType ElementDataType,
|
|
List<string> ListRows,
|
|
string? Value,
|
|
string? DataSourceRef,
|
|
bool IsLocked);
|
|
}
|