diff --git a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerEditor.razor b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerEditor.razor index bb79bf5..6f098b4 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerEditor.razor +++ b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerEditor.razor @@ -12,6 +12,10 @@
@* ── Monitored attribute ───────────────────────────────────────────── *@ + @* Expression triggers reference attributes inside the C# expression itself, + so they do not use the single-attribute picker. *@ + @if (TriggerType != AlarmTriggerType.Expression) + {
}
+ } @* ── Type-specific block ───────────────────────────────────────────── *@ @switch (TriggerType) @@ -83,6 +88,9 @@ case AlarmTriggerType.HiLo: @RenderHiLo(); break; + case AlarmTriggerType.Expression: + @RenderExpression(); + break; } @* ── Hint ──────────────────────────────────────────────────────────── *@ @@ -559,6 +567,30 @@ await Emit(); } + // ── Expression ───────────────────────────────────────────────────────── + + private RenderFragment RenderExpression() => __builder => + { + + +
+ A boolean C# expression — e.g. Attributes["Temperature"] > 80. +
+ }; + + private async Task OnExpressionChanged(string value) + { + _model.Expression = value; + await Emit(); + } + // ── Hint text ────────────────────────────────────────────────────────── private string BuildHint() @@ -582,6 +614,9 @@ AlarmTriggerType.HiLo => BuildHiLoHint(attr), + AlarmTriggerType.Expression => + "Alarm is active while this expression is true.", + _ => string.Empty }; } diff --git a/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor b/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor index 0f5a3ae..07f0dad 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor +++ b/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor @@ -23,6 +23,7 @@ + @if (_kind == ScriptTriggerKind.Unknown) { @@ -45,6 +46,9 @@ case ScriptTriggerKind.Conditional: @RenderConditional(); break; + case ScriptTriggerKind.Expression: + @RenderExpression(); + break; case ScriptTriggerKind.Call:
No automatic trigger — this script runs only when another script @@ -62,7 +66,8 @@ } @* ── Hint ──────────────────────────────────────────────────────────── *@ - @if (_kind is ScriptTriggerKind.Interval or ScriptTriggerKind.ValueChange or ScriptTriggerKind.Conditional) + @if (_kind is ScriptTriggerKind.Interval or ScriptTriggerKind.ValueChange + or ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression) {
@BuildHint()
} @@ -244,6 +249,30 @@ await Emit(); } + // ── Expression ───────────────────────────────────────────────────────── + + private RenderFragment RenderExpression() => __builder => + { + + +
+ A boolean C# expression — e.g. Attributes["Temperature"] > 80. +
+ }; + + private async Task OnExpressionChanged(string value) + { + _model.Expression = value; + await Emit(); + } + // ── Attribute picker (ValueChange + Conditional) ─────────────────────── private RenderFragment RenderAttributePicker(string label) => __builder => @@ -315,6 +344,9 @@ ? $"Runs when {attr} changes, if {attr} {_model.Operator} {t.ToString("0.###", CultureInfo.InvariantCulture)}." : $"Runs when {attr} changes and meets the configured condition — set a threshold above.", + ScriptTriggerKind.Expression => + "Runs once each time this expression becomes true.", + _ => string.Empty }; } diff --git a/src/ScadaLink.CentralUI/Components/Shared/TriggerAttributeMapper.cs b/src/ScadaLink.CentralUI/Components/Shared/TriggerAttributeMapper.cs new file mode 100644 index 0000000..71874f6 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Shared/TriggerAttributeMapper.cs @@ -0,0 +1,49 @@ +using ScadaLink.CentralUI.ScriptAnalysis; + +namespace ScadaLink.CentralUI.Components.Shared; + +/// +/// Maps the trigger editors' flattened list +/// into the metadata the uses to drive C# completion +/// inside an expression trigger: +/// +/// Direct + Inherited choices become s, +/// surfaced under Attributes["..."]. +/// Composed choices — whose canonical name is dotted, e.g. +/// CoolingTank.Temp — are grouped by their composition-instance prefix +/// into s, surfaced under +/// Children["..."].Attributes["..."]. +/// +/// +public static class TriggerAttributeMapper +{ + /// Direct and inherited attributes, exposed as Attributes["..."]. + public static IReadOnlyList SelfAttributes( + IReadOnlyList choices) => + choices + .Where(c => c.Source is "Direct" or "Inherited") + .Select(c => new AttributeShape(c.CanonicalName, c.DataType)) + .ToList(); + + /// + /// Composed attributes grouped by composition-instance name, exposed as + /// Children["X"].Attributes["Y"]. Entries without a dotted prefix + /// are skipped (no child scope to attach them to). + /// + public static IReadOnlyList Children( + IReadOnlyList choices) => + choices + .Where(c => c.Source == "Composed" && c.CanonicalName.Contains('.')) + .Select(c => new + { + Child = c.CanonicalName[..c.CanonicalName.IndexOf('.')], + Member = c.CanonicalName[(c.CanonicalName.IndexOf('.') + 1)..], + c.DataType + }) + .GroupBy(x => x.Child, StringComparer.Ordinal) + .Select(g => new CompositionContext( + g.Key, + g.Select(x => new AttributeShape(x.Member, x.DataType)).ToList(), + Array.Empty())) + .ToList(); +}