refactor(ui/design): card grid, SMTP split, TemplateEdit vertical-stack

Templates: <h4> in flex header, Expand/Collapse moved into a Bulk
actions dropdown, hover-visible kebab on tree nodes with aria-labels.
TreeView CSS gets a .tv-kebab opacity-on-hover utility.

TemplateCreate: form-control (not -sm) for primary inputs; accessible
Back button.

TemplateEdit: Properties card vertical-stacked with Save at the
bottom-right and Parent rendered as readonly plaintext. Add-member
forms (Attributes, Alarms, Scripts, Compositions) reflowed from
horizontal row g-2 align-items-end into cards with stacked col-12
inputs (Scripts gets rows=10). Lock/Unlock badges show full words.
Per-row Delete moved into a kebab dropdown. Tab nav gains
role="tablist" / role="tab" / aria-selected / aria-controls and panels
get role="tabpanel". Validation entries get consistent strong-and-
muted styling.

SharedScripts: migrated from table to card grid (col-lg-6) matching
Sites; cards show code preview + param/return badges + Edit + kebab.
Search filter, empty state CTA, @key.

SharedScriptForm: small ?-icon tooltips next to Parameters and Return
Definition labels.

ExternalSystems: SMTP split out to its own page; remaining tabs (
External Systems, DB Connections, Notification Lists, API Methods,
API Keys) unified as card grids with per-tab search + empty-state CTA.
Tab nav gets full ARIA instrumentation. Header gains a link to the
new SMTP page.

New page SmtpConfiguration.razor at /design/smtp: vertical-stacked
form using the existing Credentials field on the entity.

ExternalSystemForm: AuthConfig placeholder updates based on the
selected AuthType (None / ApiKey / BasicAuth).

DbConnectionForm: form-text below Connection String noting that the
value is stored in plain text and is admin-only.

ApiMethodForm: Script textarea rows=10; JSON example placeholders
for Params and Returns.

NotificationListForm: form-control sizing on Name/email inputs;
thead.table-dark -> table-light on the recipients table.
This commit is contained in:
Joseph Doherty
2026-05-12 03:32:39 -04:00
parent da2c0d714e
commit b6e2ec8a50
12 changed files with 1039 additions and 408 deletions
@@ -13,7 +13,7 @@
<div class="container-fluid mt-3">
<ToastNotification @ref="_toast" />
<ConfirmDialog @ref="_confirmDialog" />
<ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" />
<RenameFolderDialog @bind-IsVisible="_showRenameFolderDialog"
FolderId="_renameFolderId"
@@ -50,14 +50,30 @@
}
else
{
<h6 class="mb-2">Templates</h6>
<div class="btn-group btn-group-sm mb-2">
<button class="btn btn-outline-secondary" title="New folder at root"
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
<button class="btn btn-outline-secondary" title="New template at root"
@onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>+ Template</button>
<button class="btn btn-outline-secondary" @onclick="() => _tree.ExpandAll()">Expand</button>
<button class="btn btn-outline-secondary" @onclick="() => _tree.CollapseAll()">Collapse</button>
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Templates</h4>
<div class="d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
Bulk actions
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item" @onclick="() => _tree.ExpandAll()">Expand all folders</button>
</li>
<li>
<button class="dropdown-item" @onclick="() => _tree.CollapseAll()">Collapse all folders</button>
</li>
</ul>
</div>
<button class="btn btn-outline-secondary btn-sm"
title="New folder at root"
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
<button class="btn btn-primary btn-sm"
title="New template at root"
@onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>+ Template</button>
</div>
</div>
<div style="max-height: calc(100vh - 160px); overflow-y: auto; padding: 4px;">
@@ -230,21 +246,61 @@
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@node.Children.Count</span>
</span>
}
@RenderNodeKebab(node)
break;
case TmplNodeKind.Template:
<span class="tv-glyph"><i class="bi bi-file-earmark-text"></i></span>
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
title="@node.Label">@node.Label</span>
@RenderNodeKebab(node)
break;
case TmplNodeKind.Composition:
<span class="tv-glyph"><i class="bi bi-arrow-return-right"></i></span>
<span class="tv-label" title="@node.Label">@node.Label</span>
@RenderNodeKebab(node)
break;
}
};
private RenderFragment RenderNodeKebab(TmplNode node) => __builder =>
{
<span class="tv-kebab dropdown ms-auto" @onclick:stopPropagation="true">
<button type="button"
class="btn btn-link btn-sm p-0 px-1 text-secondary tv-kebab-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
aria-label="@($"More actions for {node.Label}")">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
@switch (node.Kind)
{
case TmplNodeKind.Folder:
<li><button class="dropdown-item" @onclick="() => OpenNewFolderDialog(node.EntityId)">New Folder</button></li>
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?folderId={node.EntityId}")'>New Template</button></li>
<li><button class="dropdown-item" @onclick="() => OpenRenameFolderDialog(node.EntityId, node.Label)">Rename</button></li>
<li><button class="dropdown-item" @onclick="() => OpenMoveFolderDialog(node.EntityId, node.Label)">Move to Folder…</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteFolder(node.EntityId, node.Label)">Delete</button></li>
break;
case TmplNodeKind.Template:
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Derived Template</button></li>
<li><button class="dropdown-item" @onclick="() => OpenMoveTemplateDialog(node.EntityId, node.Label)">Move to Folder…</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteTemplate(node.Template!)">Delete</button></li>
break;
case TmplNodeKind.Composition:
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/{node.Composition!.ComposedTemplateId}")'>Open composed template</button></li>
break;
}
</ul>
</span>
};
private void OnTreeNodeSelected(object? key)
{
if (key is not string s) return;