feat(templates/ui): phase 6-8 — derived template UX

Templates tree hides IsDerived templates by default. A "Show derived"
form-switch in the page header toggles them into the listing so users
can reach orphaned derived templates when they need to.

TemplateEdit:
- Banner on derived templates: links to the base + the composing owner /
  slot name pulled from OwnerCompositionId.
- Attributes/Scripts tables grew a context-aware column:
  * On derived templates: a Source badge (Inherited / Override / Local)
    plus a 🔒 Base-locked badge when the base marks LockedInDerived.
  * On base templates: a switch that flips LockedInDerived through
    UpdateAttribute/UpdateScript.
- Effective Value / Code now resolves from the base when an inherited row
  carries a stale snapshot — matches the flatten-time behavior so the UI
  doesn't lie.
- Override / Revert-to-base actions added to the row kebab; delete is
  hidden on inherited rows (the base owns those).
This commit is contained in:
Joseph Doherty
2026-05-12 08:55:20 -04:00
parent f599809486
commit f05b03f1cc
2 changed files with 310 additions and 14 deletions
@@ -62,6 +62,14 @@
</li>
</ul>
</div>
<div class="form-check form-switch d-flex align-items-center gap-2 mb-0"
title="Derived templates back individual composition slots and are normally hidden.">
<input class="form-check-input" type="checkbox" role="switch"
id="show-derived-toggle"
checked="@_showDerived"
@onchange="OnToggleShowDerived" />
<label class="form-check-label small text-muted" for="show-derived-toggle">Show derived</label>
</div>
<button class="btn btn-outline-secondary btn-sm"
title="New folder at root"
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
@@ -102,6 +110,7 @@
private List<Template> _templates = new();
private List<TemplateFolder> _folders = new();
private bool _showDerived;
private bool _loading = true;
private string? _errorMessage;
@@ -113,6 +122,12 @@
await LoadTemplatesAsync();
}
private void OnToggleShowDerived(ChangeEventArgs e)
{
_showDerived = e.Value is bool b && b;
BuildTemplateTree();
}
private async Task LoadTemplatesAsync()
{
_loading = true;
@@ -172,7 +187,10 @@
}
// 3. Template nodes with composition leaves
foreach (var t in _templates.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
var visibleTemplates = _showDerived
? _templates
: _templates.Where(t => !t.IsDerived);
foreach (var t in visibleTemplates.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
{
var compChildren = t.Compositions
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)