feat(ui): structured editors for script schemas and alarm triggers
Replace raw-JSON text inputs with rich UI: script parameter/return types use a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration to convert existing definitions); alarm trigger config uses a type-aware editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor gains optional direction (rising/falling/either) on RateOfChange triggers.
This commit is contained in:
@@ -30,11 +30,15 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Parameters</label>
|
||||
<ParameterListEditor Json="@_params" JsonChanged="@(v => _params = v)" />
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_params"
|
||||
ValueChanged="@(v => _params = v)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Return value</label>
|
||||
<ReturnTypeEditor Json="@_returns" JsonChanged="@(v => _returns = v)" />
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_returns"
|
||||
ValueChanged="@(v => _returns = v)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Script</label>
|
||||
|
||||
@@ -32,11 +32,15 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Parameters</label>
|
||||
<ParameterListEditor Json="@_formParameters" JsonChanged="@(v => _formParameters = v)" />
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_formParameters"
|
||||
ValueChanged="@(v => _formParameters = v)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Return value</label>
|
||||
<ReturnTypeEditor Json="@_formReturn" JsonChanged="@(v => _formReturn = v)" />
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_formReturn"
|
||||
ValueChanged="@(v => _formReturn = v)" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Code</label>
|
||||
|
||||
@@ -70,8 +70,9 @@
|
||||
private bool _validating;
|
||||
private Commons.Types.Flattening.ValidationResult? _validationResult;
|
||||
|
||||
// Member add forms
|
||||
// Member add/edit forms. _edit*Id null = adding; non-null = editing that row.
|
||||
private bool _showAttrForm;
|
||||
private int? _editAttrId;
|
||||
private string _attrName = string.Empty;
|
||||
private string? _attrValue;
|
||||
private DataType _attrDataType;
|
||||
@@ -80,6 +81,7 @@
|
||||
private string? _attrFormError;
|
||||
|
||||
private bool _showAlarmForm;
|
||||
private int? _editAlarmId;
|
||||
private string _alarmName = string.Empty;
|
||||
private int _alarmPriority;
|
||||
private AlarmTriggerType _alarmTriggerType;
|
||||
@@ -88,6 +90,7 @@
|
||||
private string? _alarmFormError;
|
||||
|
||||
private bool _showScriptForm;
|
||||
private int? _editScriptId;
|
||||
private string _scriptName = string.Empty;
|
||||
private string _scriptCode = string.Empty;
|
||||
private string? _scriptTriggerType;
|
||||
@@ -96,6 +99,7 @@
|
||||
private string? _scriptReturn;
|
||||
private bool _scriptIsLocked;
|
||||
private string? _scriptFormError;
|
||||
private string _scriptModalTab = "code"; // "code" | "parameters" | "return"
|
||||
private MonacoEditor? _scriptEditor;
|
||||
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker> _scriptMarkers
|
||||
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker>();
|
||||
@@ -470,49 +474,57 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Attributes</h5>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showAttrForm = true; _attrFormError = null; _attrName = string.Empty; _attrValue = null; _attrIsLocked = false; _attrDataSourceRef = null; }">Add Attribute</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick="BeginAddAttribute">Add Attribute</button>
|
||||
</div>
|
||||
|
||||
@if (_showAttrForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Add Attribute</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_attrName" />
|
||||
var editing = _editAttrId.HasValue;
|
||||
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">@(editing ? "Edit Attribute" : "Add Attribute")</h6>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAttributeForm"></button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Data Type</label>
|
||||
<select class="form-select" @bind="_attrDataType">
|
||||
@foreach (var dt in Enum.GetValues<DataType>())
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_attrName" readonly="@editing" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Data Type</label>
|
||||
<select class="form-select" @bind="_attrDataType">
|
||||
@foreach (var dt in Enum.GetValues<DataType>())
|
||||
{
|
||||
<option value="@dt">@dt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" @bind="_attrValue" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Data Source Ref</label>
|
||||
<input type="text" class="form-control" @bind="_attrDataSourceRef" placeholder="Tag path" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_attrIsLocked" id="attrLocked" />
|
||||
<label class="form-check-label" for="attrLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_attrFormError != null)
|
||||
{
|
||||
<option value="@dt">@dt</option>
|
||||
<div class="col-12"><div class="text-danger small">@_attrFormError</div></div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" @bind="_attrValue" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Data Source Ref</label>
|
||||
<input type="text" class="form-control" @bind="_attrDataSourceRef" placeholder="Tag path" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_attrIsLocked" id="attrLocked" />
|
||||
<label class="form-check-label" for="attrLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_attrFormError != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small">@_attrFormError</div></div>
|
||||
}
|
||||
<div class="col-12 text-end">
|
||||
<button class="btn btn-outline-secondary me-1" @onclick="() => _showAttrForm = false">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="AddAttribute">Add</button>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelAttributeForm">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="SaveAttribute">@(editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,6 +628,10 @@
|
||||
}
|
||||
@if (!(derived && baseAttr != null))
|
||||
{
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => BeginEditAttribute(attr)">Edit…</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteAttribute(attr)">Delete</button>
|
||||
@@ -701,49 +717,60 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Alarms</h5>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showAlarmForm = true; _alarmFormError = null; _alarmName = string.Empty; _alarmPriority = 500; _alarmTriggerConfig = null; _alarmIsLocked = false; }">Add Alarm</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick="BeginAddAlarm">Add Alarm</button>
|
||||
</div>
|
||||
|
||||
@if (_showAlarmForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Add Alarm</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_alarmName" />
|
||||
var editing = _editAlarmId.HasValue;
|
||||
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">@(editing ? "Edit Alarm" : "Add Alarm")</h6>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAlarmForm"></button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Type</label>
|
||||
<select class="form-select" @bind="_alarmTriggerType">
|
||||
@foreach (var tt in Enum.GetValues<AlarmTriggerType>())
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_alarmName" readonly="@editing" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Type</label>
|
||||
<select class="form-select" @bind="_alarmTriggerType" disabled="@editing">
|
||||
@foreach (var tt in Enum.GetValues<AlarmTriggerType>())
|
||||
{
|
||||
<option value="@tt">@tt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Priority</label>
|
||||
<input type="number" class="form-control" @bind="_alarmPriority" min="0" max="1000" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Configuration</label>
|
||||
<AlarmTriggerEditor TriggerType="@_alarmTriggerType"
|
||||
Value="@_alarmTriggerConfig"
|
||||
ValueChanged="@(v => _alarmTriggerConfig = v)"
|
||||
AvailableAttributes="@BuildAlarmAttributeChoices()" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_alarmIsLocked" id="alarmLocked" />
|
||||
<label class="form-check-label" for="alarmLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_alarmFormError != null)
|
||||
{
|
||||
<option value="@tt">@tt</option>
|
||||
<div class="col-12"><div class="text-danger small">@_alarmFormError</div></div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Priority</label>
|
||||
<input type="number" class="form-control" @bind="_alarmPriority" min="0" max="1000" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Config (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_alarmTriggerConfig" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_alarmIsLocked" id="alarmLocked" />
|
||||
<label class="form-check-label" for="alarmLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (_alarmFormError != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small">@_alarmFormError</div></div>
|
||||
}
|
||||
<div class="col-12 text-end">
|
||||
<button class="btn btn-outline-secondary me-1" @onclick="() => _showAlarmForm = false">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="AddAlarm">Add</button>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelAlarmForm">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="SaveAlarm">@(editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,6 +813,10 @@
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {alarm.Name}")">⋮</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => BeginEditAlarm(alarm)">Edit…</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteAlarm(alarm)">Delete</button>
|
||||
@@ -804,61 +835,100 @@
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Scripts</h5>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showScriptForm = true; _scriptFormError = null; _scriptName = string.Empty; _scriptCode = string.Empty; _scriptTriggerType = null; _scriptTriggerConfig = null; _scriptParameters = null; _scriptReturn = null; _scriptIsLocked = false; }">Add Script</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick="BeginAddScript">Add Script</button>
|
||||
</div>
|
||||
|
||||
@if (_showScriptForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Add Script</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_scriptName" />
|
||||
var editingScript = _editScriptId.HasValue;
|
||||
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">@(editingScript ? "Edit Script" : "Add Script")</h6>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelScriptForm"></button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Type</label>
|
||||
<input type="text" class="form-control" @bind="_scriptTriggerType" placeholder="e.g. ValueChange" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Trigger Config (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_scriptTriggerConfig" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Parameters</label>
|
||||
<ParameterListEditor Json="@_scriptParameters" JsonChanged="@(v => _scriptParameters = v)" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Return value</label>
|
||||
<ReturnTypeEditor Json="@_scriptReturn" JsonChanged="@(v => _scriptReturn = v)" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_scriptIsLocked" id="scriptLocked" />
|
||||
<label class="form-check-label" for="scriptLocked">Locked</label>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_scriptName" readonly="@editingScript" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Trigger Type</label>
|
||||
<input type="text" class="form-control" @bind="_scriptTriggerType" placeholder="e.g. ValueChange" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Trigger Config (JSON)</label>
|
||||
<input type="text" class="form-control" @bind="_scriptTriggerConfig" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" @bind="_scriptIsLocked" id="scriptLocked" />
|
||||
<label class="form-check-label" for="scriptLocked">Locked</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Tabs: Code, Parameters, Return. Both editor panels stay
|
||||
mounted (toggled via display:none) so Monaco and the
|
||||
JSONJoy React island don't tear down on tab switch. *@
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_scriptModalTab == "code" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_scriptModalTab == "code" ? "true" : "false")"
|
||||
@onclick='() => _scriptModalTab = "code"'>Code</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_scriptModalTab == "parameters" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_scriptModalTab == "parameters" ? "true" : "false")"
|
||||
@onclick='() => _scriptModalTab = "parameters"'>Parameters</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_scriptModalTab == "return" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_scriptModalTab == "return" ? "true" : "false")"
|
||||
@onclick='() => _scriptModalTab = "return"'>Return type</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="border border-top-0 rounded-bottom p-3">
|
||||
<div style="display: @(_scriptModalTab == "code" ? "block" : "none")">
|
||||
<MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
|
||||
Language="csharp" Height="360px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
|
||||
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_scriptParameters)"
|
||||
SiblingScripts="@(_scripts.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
|
||||
SelfAttributes="@(_attributes.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
|
||||
Children="@_editorChildren"
|
||||
Parent="@ActiveEditorParent"
|
||||
MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
|
||||
<ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||
</div>
|
||||
<div style="display: @(_scriptModalTab == "parameters" ? "block" : "none")">
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_scriptParameters"
|
||||
ValueChanged="@(v => _scriptParameters = v)" />
|
||||
</div>
|
||||
<div style="display: @(_scriptModalTab == "return" ? "block" : "none")">
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_scriptReturn"
|
||||
ValueChanged="@(v => _scriptReturn = v)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_scriptFormError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_scriptFormError</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Code</label>
|
||||
<MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
|
||||
Language="csharp" Height="320px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
|
||||
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_scriptParameters)"
|
||||
SiblingScripts="@(_scripts.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
|
||||
SelfAttributes="@(_attributes.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
|
||||
Children="@_editorChildren"
|
||||
Parent="@ActiveEditorParent"
|
||||
MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
|
||||
<ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||
</div>
|
||||
@if (_scriptFormError != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small">@_scriptFormError</div></div>
|
||||
}
|
||||
<div class="col-12 text-end">
|
||||
<button class="btn btn-outline-secondary me-1" @onclick="() => _showScriptForm = false">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="AddScript">Add</button>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelScriptForm">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="SaveScript">@(editingScript ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -952,6 +1022,10 @@
|
||||
}
|
||||
@if (!(derivedScripts && baseScript != null))
|
||||
{
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="() => BeginEditScript(script)">Edit…</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteScript(script)">Delete</button>
|
||||
@@ -1013,12 +1087,74 @@
|
||||
|
||||
// ---- CRUD handlers ----
|
||||
|
||||
private async Task AddAttribute()
|
||||
private void BeginAddAttribute()
|
||||
{
|
||||
_showAttrForm = true;
|
||||
_editAttrId = null;
|
||||
_attrFormError = null;
|
||||
_attrName = string.Empty;
|
||||
_attrValue = null;
|
||||
_attrDataType = default;
|
||||
_attrIsLocked = false;
|
||||
_attrDataSourceRef = null;
|
||||
}
|
||||
|
||||
private void BeginEditAttribute(TemplateAttribute attr)
|
||||
{
|
||||
_showAttrForm = true;
|
||||
_editAttrId = attr.Id;
|
||||
_attrFormError = null;
|
||||
_attrName = attr.Name;
|
||||
_attrValue = attr.Value;
|
||||
_attrDataType = attr.DataType;
|
||||
_attrIsLocked = attr.IsLocked;
|
||||
_attrDataSourceRef = attr.DataSourceReference;
|
||||
}
|
||||
|
||||
private void CancelAttributeForm()
|
||||
{
|
||||
_showAttrForm = false;
|
||||
_editAttrId = null;
|
||||
_attrFormError = null;
|
||||
}
|
||||
|
||||
private async Task SaveAttribute()
|
||||
{
|
||||
if (_selectedTemplate == null) return;
|
||||
_attrFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_attrName)) { _attrFormError = "Name is required."; return; }
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
if (_editAttrId is int id)
|
||||
{
|
||||
var existing = _attributes.FirstOrDefault(a => a.Id == id);
|
||||
if (existing == null) { _attrFormError = "Attribute no longer exists."; return; }
|
||||
var proposed = new TemplateAttribute(existing.Name)
|
||||
{
|
||||
DataType = _attrDataType,
|
||||
Value = _attrValue?.Trim(),
|
||||
IsLocked = _attrIsLocked,
|
||||
DataSourceReference = _attrDataSourceRef?.Trim(),
|
||||
Description = existing.Description,
|
||||
IsInherited = existing.IsInherited,
|
||||
LockedInDerived = existing.LockedInDerived,
|
||||
};
|
||||
var result = await TemplateService.UpdateAttributeAsync(id, proposed, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showAttrForm = false;
|
||||
_editAttrId = null;
|
||||
_toast.ShowSuccess($"Attribute '{existing.Name}' updated.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_attrFormError = result.Error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var attr = new TemplateAttribute(_attrName.Trim())
|
||||
{
|
||||
DataType = _attrDataType,
|
||||
@@ -1027,9 +1163,8 @@
|
||||
DataSourceReference = _attrDataSourceRef?.Trim()
|
||||
};
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.AddAttributeAsync(_selectedTemplate.Id, attr, user);
|
||||
if (result.IsSuccess)
|
||||
var addResult = await TemplateService.AddAttributeAsync(_selectedTemplate.Id, attr, user);
|
||||
if (addResult.IsSuccess)
|
||||
{
|
||||
_showAttrForm = false;
|
||||
_toast.ShowSuccess($"Attribute '{_attrName}' added.");
|
||||
@@ -1037,7 +1172,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
_attrFormError = result.Error;
|
||||
_attrFormError = addResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,12 +1193,105 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddAlarm()
|
||||
private void BeginAddAlarm()
|
||||
{
|
||||
_showAlarmForm = true;
|
||||
_editAlarmId = null;
|
||||
_alarmFormError = null;
|
||||
_alarmName = string.Empty;
|
||||
_alarmPriority = 500;
|
||||
_alarmTriggerType = default;
|
||||
_alarmTriggerConfig = null;
|
||||
_alarmIsLocked = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the attribute choice list shown in the alarm trigger editor's
|
||||
/// picker. Combines direct + inherited attributes (from <c>_attributes</c>)
|
||||
/// with composed children's attributes (from <c>_editorChildren</c>,
|
||||
/// path-qualified as <c>[ChildInstance].[AttributeName]</c>).
|
||||
/// </summary>
|
||||
private IReadOnlyList<AlarmAttributeChoice> BuildAlarmAttributeChoices()
|
||||
{
|
||||
var list = new List<AlarmAttributeChoice>(capacity: _attributes.Count + 8);
|
||||
|
||||
foreach (var a in _attributes)
|
||||
{
|
||||
list.Add(new AlarmAttributeChoice(
|
||||
a.Name,
|
||||
MapDataType(a.DataType),
|
||||
a.IsInherited ? "Inherited" : "Direct"));
|
||||
}
|
||||
|
||||
foreach (var child in _editorChildren)
|
||||
{
|
||||
foreach (var shape in child.Attributes)
|
||||
{
|
||||
list.Add(new AlarmAttributeChoice(
|
||||
$"{child.Name}.{shape.Name}",
|
||||
shape.Type,
|
||||
"Composed"));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void BeginEditAlarm(TemplateAlarm alarm)
|
||||
{
|
||||
_showAlarmForm = true;
|
||||
_editAlarmId = alarm.Id;
|
||||
_alarmFormError = null;
|
||||
_alarmName = alarm.Name;
|
||||
_alarmPriority = alarm.PriorityLevel;
|
||||
_alarmTriggerType = alarm.TriggerType;
|
||||
_alarmTriggerConfig = alarm.TriggerConfiguration;
|
||||
_alarmIsLocked = alarm.IsLocked;
|
||||
}
|
||||
|
||||
private void CancelAlarmForm()
|
||||
{
|
||||
_showAlarmForm = false;
|
||||
_editAlarmId = null;
|
||||
_alarmFormError = null;
|
||||
}
|
||||
|
||||
private async Task SaveAlarm()
|
||||
{
|
||||
if (_selectedTemplate == null) return;
|
||||
_alarmFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_alarmName)) { _alarmFormError = "Name is required."; return; }
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
if (_editAlarmId is int id)
|
||||
{
|
||||
var existing = _alarms.FirstOrDefault(a => a.Id == id);
|
||||
if (existing == null) { _alarmFormError = "Alarm no longer exists."; return; }
|
||||
var proposed = new TemplateAlarm(existing.Name)
|
||||
{
|
||||
TriggerType = existing.TriggerType, // fixed
|
||||
PriorityLevel = _alarmPriority,
|
||||
TriggerConfiguration = _alarmTriggerConfig?.Trim(),
|
||||
IsLocked = _alarmIsLocked,
|
||||
Description = existing.Description,
|
||||
OnTriggerScriptId = existing.OnTriggerScriptId,
|
||||
};
|
||||
var result = await TemplateService.UpdateAlarmAsync(id, proposed, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showAlarmForm = false;
|
||||
_editAlarmId = null;
|
||||
_toast.ShowSuccess($"Alarm '{existing.Name}' updated.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_alarmFormError = result.Error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var alarm = new TemplateAlarm(_alarmName.Trim())
|
||||
{
|
||||
TriggerType = _alarmTriggerType,
|
||||
@@ -1072,9 +1300,8 @@
|
||||
IsLocked = _alarmIsLocked
|
||||
};
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.AddAlarmAsync(_selectedTemplate.Id, alarm, user);
|
||||
if (result.IsSuccess)
|
||||
var addResult = await TemplateService.AddAlarmAsync(_selectedTemplate.Id, alarm, user);
|
||||
if (addResult.IsSuccess)
|
||||
{
|
||||
_showAlarmForm = false;
|
||||
_toast.ShowSuccess($"Alarm '{_alarmName}' added.");
|
||||
@@ -1082,7 +1309,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
_alarmFormError = result.Error;
|
||||
_alarmFormError = addResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,13 +1327,82 @@
|
||||
else { _toast.ShowError(result.Error); }
|
||||
}
|
||||
|
||||
private async Task AddScript()
|
||||
private void BeginAddScript()
|
||||
{
|
||||
_showScriptForm = true;
|
||||
_editScriptId = null;
|
||||
_scriptFormError = null;
|
||||
_scriptName = string.Empty;
|
||||
_scriptCode = string.Empty;
|
||||
_scriptTriggerType = null;
|
||||
_scriptTriggerConfig = null;
|
||||
_scriptParameters = null;
|
||||
_scriptReturn = null;
|
||||
_scriptIsLocked = false;
|
||||
_scriptModalTab = "code";
|
||||
}
|
||||
|
||||
private void BeginEditScript(TemplateScript script)
|
||||
{
|
||||
_showScriptForm = true;
|
||||
_editScriptId = script.Id;
|
||||
_scriptFormError = null;
|
||||
_scriptName = script.Name;
|
||||
_scriptCode = script.Code;
|
||||
_scriptTriggerType = script.TriggerType;
|
||||
_scriptTriggerConfig = script.TriggerConfiguration;
|
||||
_scriptParameters = script.ParameterDefinitions;
|
||||
_scriptReturn = script.ReturnDefinition;
|
||||
_scriptIsLocked = script.IsLocked;
|
||||
_scriptModalTab = "code";
|
||||
}
|
||||
|
||||
private void CancelScriptForm()
|
||||
{
|
||||
_showScriptForm = false;
|
||||
_editScriptId = null;
|
||||
_scriptFormError = null;
|
||||
}
|
||||
|
||||
private async Task SaveScript()
|
||||
{
|
||||
if (_selectedTemplate == null) return;
|
||||
_scriptFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_scriptName)) { _scriptFormError = "Name is required."; return; }
|
||||
if (string.IsNullOrWhiteSpace(_scriptCode)) { _scriptFormError = "Code is required."; return; }
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
if (_editScriptId is int id)
|
||||
{
|
||||
var existing = _scripts.FirstOrDefault(s => s.Id == id);
|
||||
if (existing == null) { _scriptFormError = "Script no longer exists."; return; }
|
||||
var proposed = new TemplateScript(existing.Name, _scriptCode)
|
||||
{
|
||||
TriggerType = _scriptTriggerType?.Trim(),
|
||||
TriggerConfiguration = _scriptTriggerConfig?.Trim(),
|
||||
ParameterDefinitions = _scriptParameters,
|
||||
ReturnDefinition = _scriptReturn,
|
||||
IsLocked = _scriptIsLocked,
|
||||
MinTimeBetweenRuns = existing.MinTimeBetweenRuns,
|
||||
IsInherited = existing.IsInherited,
|
||||
LockedInDerived = existing.LockedInDerived,
|
||||
};
|
||||
var result = await TemplateService.UpdateScriptAsync(id, proposed, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showScriptForm = false;
|
||||
_editScriptId = null;
|
||||
_toast.ShowSuccess($"Script '{existing.Name}' updated.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_scriptFormError = result.Error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var script = new TemplateScript(_scriptName.Trim(), _scriptCode)
|
||||
{
|
||||
TriggerType = _scriptTriggerType?.Trim(),
|
||||
@@ -1116,9 +1412,8 @@
|
||||
IsLocked = _scriptIsLocked
|
||||
};
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.AddScriptAsync(_selectedTemplate.Id, script, user);
|
||||
if (result.IsSuccess)
|
||||
var addResult = await TemplateService.AddScriptAsync(_selectedTemplate.Id, script, user);
|
||||
if (addResult.IsSuccess)
|
||||
{
|
||||
_showScriptForm = false;
|
||||
_toast.ShowSuccess($"Script '{_scriptName}' added.");
|
||||
@@ -1126,7 +1421,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
_scriptFormError = result.Error;
|
||||
_scriptFormError = addResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user