diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs index b9fb2efd..6f468538 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs @@ -78,6 +78,17 @@ public sealed record ResolvedAttribute /// public string? DataSourceReference { get; init; } + /// + /// The data-source reference as resolved from the template chain BEFORE any + /// per-instance InstanceConnectionBinding.DataSourceReferenceOverride was + /// applied — i.e. the template/composed default the Configure page shows as the + /// placeholder. Equals when no override is set; + /// null for attributes that are not data-sourced. + /// Central-side presentation data only: deliberately excluded from the revision + /// hash (RevisionHashService projects through HashableAttribute). + /// + public string? DefaultDataSourceReference { get; init; } + /// /// If data-sourced: the resolved connection ID from the instance binding. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs index bf4fca32..c01e5be7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs @@ -369,6 +369,15 @@ public class FlatteningService Dictionary attributes, IReadOnlyDictionary dataConnections) { + // Capture the pre-binding default for EVERY data-sourced attribute (bound or + // not) so the Configure page can show the template default as a placeholder. + foreach (var key in attributes.Keys.ToList()) + { + var attr = attributes[key]; + if (attr.DataSourceReference is not null && attr.DefaultDataSourceReference is null) + attributes[key] = attr with { DefaultDataSourceReference = attr.DataSourceReference }; + } + foreach (var binding in bindings) { if (!attributes.TryGetValue(binding.AttributeName, out var existing)) @@ -385,7 +394,8 @@ public class FlatteningService BoundDataConnectionId = connection.Id, BoundDataConnectionName = connection.Name, BoundDataConnectionProtocol = connection.Protocol, - DataSourceReference = binding.DataSourceReferenceOverride ?? existing.DataSourceReference + DataSourceReference = binding.DataSourceReferenceOverride ?? existing.DataSourceReference, + DefaultDataSourceReference = existing.DefaultDataSourceReference ?? existing.DataSourceReference }; } } diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DefaultDataSourceReferenceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DefaultDataSourceReferenceTests.cs new file mode 100644 index 00000000..1daf2b2d --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DefaultDataSourceReferenceTests.cs @@ -0,0 +1,81 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; +using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening; + +namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Flattening; + +/// +/// Design 2026-08-20 (grouped connection bindings): the flattened attribute +/// carries the data-source reference as it was BEFORE the instance binding +/// override, so the Configure page can show the template default next to the +/// effective reference. The field is deliberately NOT part of the revision hash. +/// +public class DefaultDataSourceReferenceTests +{ + [Fact] + public void RevisionHash_IgnoresDefaultDataSourceReference() + { + var svc = new RevisionHashService(); + var a = new ResolvedAttribute + { + CanonicalName = "LeftMESReceiver.MoveInFlag", + DataType = "Boolean", + DataSourceReference = "MESReceiver_022.MoveInFlag", + BoundDataConnectionId = 1 + }; + var without = new FlattenedConfiguration { InstanceUniqueName = "Z28061", Attributes = [a] }; + var with = without with { Attributes = [a with { DefaultDataSourceReference = "MoveInFlag" }] }; + + Assert.Equal(svc.ComputeHash(without), svc.ComputeHash(with)); + } + + [Fact] + public void ResolvedAttribute_DefaultDataSourceReference_IsNullByDefault() + { + var a = new ResolvedAttribute { CanonicalName = "X" }; + Assert.Null(a.DefaultDataSourceReference); + } + + [Fact] + public void Flatten_PopulatesDefault_ForBoundAndUnboundDataSourcedAttributes() + { + var template = new Template("Base") { Id = 1 }; + template.Attributes.Add(new TemplateAttribute("MoveInFlag") { DataType = DataType.Boolean, DataSourceReference = "MoveInFlag" }); + template.Attributes.Add(new TemplateAttribute("Unbound") { DataType = DataType.Boolean, DataSourceReference = "UnboundRef" }); + template.Attributes.Add(new TemplateAttribute("Static") { DataType = DataType.Boolean, Value = "false" }); + + var instance = new Instance("Z28061") { Id = 1, TemplateId = 1, SiteId = 1 }; + instance.ConnectionBindings.Add(new InstanceConnectionBinding("MoveInFlag") + { + DataConnectionId = 1, + DataSourceReferenceOverride = "MESReceiver_022.MoveInFlag" + }); + + var connections = new Dictionary + { + [1] = new("GW", "MxGateway", 1) { Id = 1, PrimaryConfiguration = "http://localhost" } + }; + + var result = new FlatteningService().Flatten( + instance, + [template], + new Dictionary>(), + new Dictionary>(), + connections); + + Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null); + var bound = result.Value.Attributes.Single(a => a.CanonicalName == "MoveInFlag"); + Assert.Equal("MESReceiver_022.MoveInFlag", bound.DataSourceReference); + Assert.Equal("MoveInFlag", bound.DefaultDataSourceReference); + + var unbound = result.Value.Attributes.Single(a => a.CanonicalName == "Unbound"); + Assert.Equal("UnboundRef", unbound.DataSourceReference); + Assert.Equal("UnboundRef", unbound.DefaultDataSourceReference); + + var stat = result.Value.Attributes.Single(a => a.CanonicalName == "Static"); + Assert.Null(stat.DefaultDataSourceReference); + } +}