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:
@@ -200,6 +200,7 @@ public class ManagementActor : ReceiveActor
|
||||
or AddTemplateNativeAlarmSourceCommand or UpdateTemplateNativeAlarmSourceCommand or DeleteTemplateNativeAlarmSourceCommand
|
||||
or AddTemplateScriptCommand or UpdateTemplateScriptCommand or DeleteTemplateScriptCommand
|
||||
or AddTemplateCompositionCommand or DeleteTemplateCompositionCommand
|
||||
or ResyncInheritedMembersCommand
|
||||
or CreateSharedScriptCommand or UpdateSharedScriptCommand or DeleteSharedScriptCommand
|
||||
or CreateSharedSchemaCommand or UpdateSharedSchemaCommand or DeleteSharedSchemaCommand
|
||||
or CreateDatabaseConnectionDefCommand or UpdateDatabaseConnectionDefCommand or DeleteDatabaseConnectionDefCommand
|
||||
@@ -253,6 +254,7 @@ public class ManagementActor : ReceiveActor
|
||||
DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user.Username),
|
||||
ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd),
|
||||
GetResolvedTemplateMembersCommand cmd => await HandleGetResolvedTemplateMembers(sp, cmd),
|
||||
ResyncInheritedMembersCommand cmd => await HandleResyncInheritedMembers(sp, cmd, user.Username),
|
||||
|
||||
// Template members
|
||||
AddTemplateAttributeCommand cmd => await HandleAddAttribute(sp, cmd, user.Username),
|
||||
@@ -261,9 +263,9 @@ public class ManagementActor : ReceiveActor
|
||||
AddTemplateAlarmCommand cmd => await HandleAddAlarm(sp, cmd, user.Username),
|
||||
UpdateTemplateAlarmCommand cmd => await HandleUpdateAlarm(sp, cmd, user.Username),
|
||||
DeleteTemplateAlarmCommand cmd => await HandleDeleteAlarm(sp, cmd, user.Username),
|
||||
AddTemplateNativeAlarmSourceCommand cmd => await HandleAddNativeAlarmSource(sp, cmd),
|
||||
UpdateTemplateNativeAlarmSourceCommand cmd => await HandleUpdateNativeAlarmSource(sp, cmd),
|
||||
DeleteTemplateNativeAlarmSourceCommand cmd => await HandleDeleteNativeAlarmSource(sp, cmd),
|
||||
AddTemplateNativeAlarmSourceCommand cmd => await HandleAddNativeAlarmSource(sp, cmd, user.Username),
|
||||
UpdateTemplateNativeAlarmSourceCommand cmd => await HandleUpdateNativeAlarmSource(sp, cmd, user.Username),
|
||||
DeleteTemplateNativeAlarmSourceCommand cmd => await HandleDeleteNativeAlarmSource(sp, cmd, user.Username),
|
||||
ListTemplateNativeAlarmSourcesCommand cmd => await HandleListNativeAlarmSources(sp, cmd),
|
||||
AddTemplateScriptCommand cmd => await HandleAddScript(sp, cmd, user.Username),
|
||||
UpdateTemplateScriptCommand cmd => await HandleUpdateScript(sp, cmd, user.Username),
|
||||
@@ -645,6 +647,20 @@ public class ManagementActor : ReceiveActor
|
||||
return TemplateInheritanceResolver.Resolve(cmd.TemplateId, allTemplates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resync inherited members (follow-up #1/#2): reconciles a template's stored
|
||||
/// inherited rows (and its derived subtree's) with the resolved effective set —
|
||||
/// materializing missing placeholders, re-syncing drifted ones, and removing
|
||||
/// orphans — so the editor's editable tabs are complete and the staleness
|
||||
/// banner clears. Designer-gated; the service owns the audit row.
|
||||
/// </summary>
|
||||
private static async Task<object?> HandleResyncInheritedMembers(IServiceProvider sp, ResyncInheritedMembersCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.ResyncInheritedMembersAsync(cmd.TemplateId, user);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Template folder handlers
|
||||
// ========================================================================
|
||||
@@ -2226,7 +2242,7 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
// ── Native alarm source bindings (read-only mirror; repository-direct CRUD) ──
|
||||
|
||||
private static async Task<object?> HandleAddNativeAlarmSource(IServiceProvider sp, AddTemplateNativeAlarmSourceCommand cmd)
|
||||
private static async Task<object?> HandleAddNativeAlarmSource(IServiceProvider sp, AddTemplateNativeAlarmSourceCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var source = new TemplateNativeAlarmSource(cmd.Name)
|
||||
@@ -2240,10 +2256,13 @@ public class ManagementActor : ReceiveActor
|
||||
};
|
||||
await repo.AddTemplateNativeAlarmSourceAsync(source);
|
||||
await repo.SaveChangesAsync();
|
||||
// Propagate the new source to derived descendants (#1/#2). Native-source
|
||||
// CRUD lives here (not TemplateService), so call the propagation directly.
|
||||
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(cmd.TemplateId, user);
|
||||
return source;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateNativeAlarmSource(IServiceProvider sp, UpdateTemplateNativeAlarmSourceCommand cmd)
|
||||
private static async Task<object?> HandleUpdateNativeAlarmSource(IServiceProvider sp, UpdateTemplateNativeAlarmSourceCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var source = await repo.GetTemplateNativeAlarmSourceByIdAsync(cmd.NativeAlarmSourceId)
|
||||
@@ -2256,14 +2275,21 @@ public class ManagementActor : ReceiveActor
|
||||
source.IsLocked = cmd.IsLocked;
|
||||
await repo.UpdateTemplateNativeAlarmSourceAsync(source);
|
||||
await repo.SaveChangesAsync();
|
||||
// Propagate the changed source to derived descendants (#1/#2).
|
||||
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(source.TemplateId, user);
|
||||
return source;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteNativeAlarmSource(IServiceProvider sp, DeleteTemplateNativeAlarmSourceCommand cmd)
|
||||
private static async Task<object?> HandleDeleteNativeAlarmSource(IServiceProvider sp, DeleteTemplateNativeAlarmSourceCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
// Capture the owning template before delete so descendants can be reconciled.
|
||||
var source = await repo.GetTemplateNativeAlarmSourceByIdAsync(cmd.NativeAlarmSourceId);
|
||||
await repo.DeleteTemplateNativeAlarmSourceAsync(cmd.NativeAlarmSourceId);
|
||||
await repo.SaveChangesAsync();
|
||||
// Remove the now-orphaned inherited placeholder from derived descendants (#1/#2).
|
||||
if (source != null)
|
||||
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(source.TemplateId, user);
|
||||
return cmd.NativeAlarmSourceId;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user