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:
Joseph Doherty
2026-05-13 00:33:00 -04:00
parent 57f477fd28
commit 783da8e21a
25 changed files with 3609 additions and 861 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}