fix(centralui): grouped bindings review fixes — Apply with the header at none/mixed keeps each member's connection (never unbinds; upsert Save could not persist it), object inference runs over effective references (bare-default members make an overridden group mixed, default-only prefixes infer), bindings to a deleted connection count as unbound, 'n changed · unsaved' diffs against last-saved state; bUnit behaviour tests for fallback banner, grouping and mixed-group Apply+Save
This commit is contained in:
@@ -44,8 +44,20 @@ public static class BindingGroupBuilder
|
||||
{
|
||||
public const string InstanceGroupKey = "(instance)";
|
||||
|
||||
public static IReadOnlyList<BindingGroup> Build(IEnumerable<BindingRow> rows)
|
||||
/// <param name="rows">The data-sourced flattened attributes with their current binding state.</param>
|
||||
/// <param name="knownConnectionIds">
|
||||
/// The site's data connection ids. When supplied, a row whose connection id is not
|
||||
/// in the set is treated as UNBOUND — a binding to a deleted connection renders as
|
||||
/// "— none —" and the deploy gate rejects it, so the pills must agree.
|
||||
/// </param>
|
||||
public static IReadOnlyList<BindingGroup> Build(IEnumerable<BindingRow> rows, IReadOnlyCollection<int>? knownConnectionIds = null)
|
||||
{
|
||||
if (knownConnectionIds is not null)
|
||||
{
|
||||
var known = knownConnectionIds as ISet<int> ?? new HashSet<int>(knownConnectionIds);
|
||||
rows = rows.Select(r => r.ConnectionId is int id && !known.Contains(id) ? r with { ConnectionId = null } : r);
|
||||
}
|
||||
|
||||
var byKey = new Dictionary<string, List<BindingGroupMember>>(StringComparer.Ordinal);
|
||||
var nested = new Dictionary<string, SortedSet<string>>(StringComparer.Ordinal);
|
||||
|
||||
@@ -93,26 +105,40 @@ public static class BindingGroupBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Longest object prefix shared by every member that has an override: each override
|
||||
/// must be <c>{prefix}.{memberTail}</c>. Returns (null,false) when no member has an
|
||||
/// override; (null,true) when members disagree or an override doesn't end in its tail.
|
||||
/// Object prefix shared by every member's EFFECTIVE reference (override if set, else
|
||||
/// the default): each must be <c>{prefix}.{memberTail}</c>.
|
||||
/// <list type="bullet">
|
||||
/// <item>No member has an override: infer from the defaults alone — (prefix,false) when
|
||||
/// they all agree, otherwise (null,false). Nothing has been edited yet, so a bare
|
||||
/// composed default is "unset", not "mixed".</item>
|
||||
/// <item>Some member has an override: (prefix,false) only when EVERY member's effective
|
||||
/// reference shares it; (null,true) when members disagree, an override doesn't end in
|
||||
/// its tail, or a member still sits on a bare default the gate would reject.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static (string? Object, bool Mixed) InferObject(IReadOnlyList<BindingGroupMember> members)
|
||||
{
|
||||
var anyOverride = members.Any(m => !string.IsNullOrWhiteSpace(m.Row.Override));
|
||||
string? candidate = null;
|
||||
var any = false;
|
||||
foreach (var m in members)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(m.Row.Override)) continue;
|
||||
any = true;
|
||||
var suffix = "." + m.MemberTail;
|
||||
if (!m.Row.Override.EndsWith(suffix, StringComparison.Ordinal)) return (null, true);
|
||||
var prefix = m.Row.Override[..^suffix.Length];
|
||||
if (prefix.Length == 0) return (null, true);
|
||||
if (candidate is null) candidate = prefix;
|
||||
else if (!string.Equals(candidate, prefix, StringComparison.Ordinal)) return (null, true);
|
||||
var effective = m.EffectiveReference;
|
||||
var prefix = PrefixOf(effective, m.MemberTail);
|
||||
if (prefix is null || (candidate is not null && !string.Equals(candidate, prefix, StringComparison.Ordinal)))
|
||||
return (null, anyOverride);
|
||||
candidate = prefix;
|
||||
}
|
||||
return any ? (candidate, false) : (null, false);
|
||||
return (candidate, false);
|
||||
}
|
||||
|
||||
/// <summary>Returns <c>prefix</c> when <paramref name="reference"/> is <c>{prefix}.{tail}</c>, else null.</summary>
|
||||
private static string? PrefixOf(string? reference, string tail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reference)) return null;
|
||||
var suffix = "." + tail;
|
||||
if (!reference.EndsWith(suffix, StringComparison.Ordinal)) return null;
|
||||
var prefix = reference[..^suffix.Length];
|
||||
return prefix.Length == 0 ? null : prefix;
|
||||
}
|
||||
|
||||
/// <summary>Result of a bulk apply for one member.</summary>
|
||||
@@ -121,14 +147,19 @@ public static class BindingGroupBuilder
|
||||
/// <summary>
|
||||
/// Rewrites every member of <paramref name="group"/> to
|
||||
/// <c>{objectPath}.{memberTail}</c> on <paramref name="connectionId"/>. A blank
|
||||
/// <paramref name="objectPath"/> clears the overrides (template default applies) and
|
||||
/// leaves the connection as given. Surrounding whitespace and a trailing '.' are tolerated.
|
||||
/// <paramref name="objectPath"/> clears the overrides (template default applies).
|
||||
/// A null <paramref name="connectionId"/> (header reading "— mixed —"/"— none —")
|
||||
/// KEEPS each member's current connection — it never unbinds a group, since the
|
||||
/// Save path is an upsert and a removal could not persist anyway. Surrounding
|
||||
/// whitespace and a trailing '.' are tolerated.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<BindingChange> ApplyObject(BindingGroup group, int? connectionId, string? objectPath)
|
||||
{
|
||||
var obj = (objectPath ?? string.Empty).Trim().TrimEnd('.');
|
||||
return group.Members
|
||||
.Select(m => new BindingChange(m.Row.CanonicalName, connectionId,
|
||||
.Select(m => new BindingChange(
|
||||
m.Row.CanonicalName,
|
||||
connectionId ?? (m.Row.ConnectionId is > 0 ? m.Row.ConnectionId : null),
|
||||
obj.Length == 0 ? null : obj + "." + m.MemberTail))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user