feat(templateengine+centralui): resolve follow-ups #1/#2 — inherited-member propagation & resync
Derived templates store IsInherited placeholder rows mirroring inherited members, but a base member added/changed/removed AFTER a child was derived never reached the child — leaving the editor's editable tabs incomplete (#1) and stored rows drifted from the resolved set (#2). Fix (one order-independent reconcile, two entry points): - Auto-propagation: every attribute/alarm/script add/update/delete now reconciles the template's derived subtree (TemplateService.ReconcileDescendantsAsync), hooked into all member-mutating paths incl. native-alarm-source CRUD in the ManagementActor. - Resync: ResyncInheritedMembersAsync repairs a template + its subtree on demand — materialize missing placeholders, re-sync drifted ones, remove orphans, across attributes/alarms/scripts/native sources. Exposed as management ResyncInheritedMembersCommand (Designer-gated, audited) → CLI `template resync-members` → a Resync button on the editor's staleness banner. Reconcile drives off TemplateInheritanceResolver (same precedence + HiLo merge as deploy), only ever touches IsInherited placeholders (never an authored override), and matches the staleness comparison keys so the banner clears. BuildDerivedTemplate now also materializes native-source placeholders at compose time (previously omitted → any inherited native source was perpetually stale). Tests: +8 TemplateServiceTests (materialize / drift-update / orphan-remove / override-untouched / base-cascade / multi-type / direct-propagate / end-to-end add) + 1 ManagementService test fix (native-source add resolves TemplateService). Affected suites green: TemplateEngine 446, ManagementService 230, CentralUI 866, CLI 333, Transport 127, ConfigurationDatabase 307; full solution builds 0/0. Docs: Component-TemplateEngine.md "Inherited-Member Propagation & Resync"; CLI README `template resync-members`; known-issues tracker #1/#2 resolved.
This commit is contained in:
@@ -288,18 +288,29 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* M9-T26b: read-only base-changed banner. Informational only — surfaced
|
||||
when the freshly-resolved inherited set differs from this template's
|
||||
stored copy (a multi-level inherited member, or a base member added
|
||||
after this template was created). No action button: a deploy already
|
||||
resolves fresh, so this is purely an authoring heads-up. *@
|
||||
@* Base-changed banner (follow-up #1/#2). Surfaced when the freshly-resolved
|
||||
inherited set differs from this template's stored copy (a base member
|
||||
added/changed/removed after this template was created, or a multi-level
|
||||
inherited member). The Resync action materializes the missing inherited
|
||||
rows, re-syncs drifted ones, and removes orphans on this template and its
|
||||
derived subtree — so the editable tabs below become complete and the
|
||||
banner clears. (A deploy already resolves fresh regardless.) *@
|
||||
@if (_resolved?.Staleness.IsStale == true)
|
||||
{
|
||||
<div class="alert alert-info py-2 mb-3" role="status">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Base template changed</strong> —
|
||||
@_resolved.Staleness.DifferingMemberCount inherited member(s) differ from this template's stored copy.
|
||||
The effective set above reflects the live base; a deploy resolves fresh.
|
||||
<div class="alert alert-info d-flex align-items-center justify-content-between py-2 mb-3" role="status">
|
||||
<div>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Base template changed</strong> —
|
||||
@_resolved.Staleness.DifferingMemberCount inherited member(s) differ from this template's stored copy.
|
||||
Resync to bring this template's stored members in line with the base.
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary ms-3 text-nowrap" @onclick="ResyncInheritedMembers" disabled="@_resyncing">
|
||||
@if (_resyncing)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
|
||||
}
|
||||
Resync inherited members
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -944,6 +955,38 @@
|
||||
else _toast.ShowError(result.Error);
|
||||
}
|
||||
|
||||
private bool _resyncing;
|
||||
|
||||
// Follow-up #1/#2: materialize/refresh this template's (and its subtree's)
|
||||
// inherited member rows so the editable tabs are complete and the
|
||||
// base-changed banner clears. In-process TemplateService call (the page is
|
||||
// already RequireDesign-gated, matching the management command's role gate).
|
||||
private async Task ResyncInheritedMembers()
|
||||
{
|
||||
if (_resyncing) return;
|
||||
_resyncing = true;
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.ResyncInheritedMembersAsync(Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var r = result.Value;
|
||||
_toast.ShowSuccess(
|
||||
$"Resynced inherited members — {r.MembersAdded} added, {r.MembersUpdated} updated, {r.MembersRemoved} removed across {r.TemplatesChanged} template(s).");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_toast.ShowError(result.Error);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Alarms Tab ----
|
||||
private RenderFragment RenderAlarmsTab() => __builder =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user