feat(ui/templates): adopt TreeView design guide; split editor to /design/templates/{id}

Templates page is now a tree-only browser; editing happens on a dedicated
TemplateEdit page. Drag-drop is replaced by context-menu Move-to-Folder.
TreeView gains Bootstrap Icons (chevron + per-kind glyphs), ancestor guide
lines, defined hover/selected/focus tokens, and Escape-dismisses-menu per
the new Visual Design Guide (V1-V7) in Component-TreeView.md.
This commit is contained in:
Joseph Doherty
2026-05-11 20:52:34 -04:00
parent f3b33e7e1d
commit 8e388a89c5
14 changed files with 3515 additions and 1127 deletions

View File

@@ -6,7 +6,7 @@
## Goal
Replace the current single-list view at `/design/templates` with a tree-organized authoring surface modeled on the Wonderware ArchestrA Template Toolbox. Users organize templates into nested folders, see composition children inline under their owning template, and edit templates in a persistent split-pane layout.
Replace the current single-list view at `/design/templates` with a tree-organized browser modeled on the Wonderware ArchestrA Template Toolbox. Users organize templates into nested folders, see composition children inline under their owning template, and navigate to a dedicated edit page (`/design/templates/{id}`) when authoring a specific template. The tree page itself does not host the editor.
## Reference
@@ -22,13 +22,14 @@ Inheritance is **not** rendered as tree nesting in the image, and it is not rend
| Decision | Choice |
|---|---|
| Inheritance in tree | Not shown as nesting; shown as text on the template node label. |
| Inheritance in tree | Not shown as nesting; **not shown on the node label either** (label is name only). Inheritance is visible in the TemplateEdit page when a template is selected. |
| Folder model | New `TemplateFolder` entity with self-referencing `ParentFolderId`. `Template.FolderId` nullable. |
| Reorganization UX | Context menus + native HTML5 drag-drop. |
| Reorganization UX | **Right-click context menus only** (no drag-drop). Modal dialog pickers for move targets. |
| Composition rendering | Read-only leaves with navigation; right-click → Open composed template / Remove composition. |
| Root-level templates | Allowed (`FolderId` nullable). Existing templates migrate with `FolderId = null`. |
| Folder delete with contents | Blocked; structured error lists child counts. |
| Page layout | Persistent split pane: tree on left (~2533% width), template detail/editor on right. |
| Page layout | **Tree browser only** — no split-pane editor. Selecting a template navigates to `/design/templates/{id}` (TemplateEdit page); creating navigates to `/design/templates/create`. |
| Tree node visuals | Per `Component-TreeView.md` Visual Design Guide V7: Bootstrap Icons (`bi-folder` / `bi-folder2-open` / `bi-file-earmark-text` / `bi-arrow-return-right`), name-only labels (no count/inherit badges on template nodes), folder child-count pill, composition `→ $Target` muted secondary text. |
## Data model
@@ -138,72 +139,65 @@ private record TmplNode(
| `KeySelector` | `n => (object)n.Key` |
| `StorageKey` | `"templates-tree"` (preserved from current usage) |
| `Selectable` | `true` |
| `SelectedKeyChanged` | dispatch on key prefix: `t:`load template into right pane; `f:` → no-op; `c:`reveal + select composed template |
| `SelectedKeyChanged` | dispatch on key prefix: `t:``NavigationManager.NavigateTo($"/design/templates/{id}")` (TemplateEdit page); `f:` → no-op; `c:``NavigateTo` the composed template's edit page |
**Inline node labels:**
- Folder: glyph + name + child-count badge.
- Template: `<strong>$Name</strong>` + optional "inherits $Parent" muted text + existing attr/alarm/script/comp count badges.
- Composition: `<span>InstanceName</span>` + muted `→ $ComposedTemplateName`.
**Inline node labels** (see `Component-TreeView.md` V7 for the canonical recipe):
- Folder: `<i class="bi bi-folder">` (closed) or `<i class="bi bi-folder2-open">` (expanded) + name (semibold when has children) + count-pill badge of direct children.
- Template: `<i class="bi bi-file-earmark-text">` + `$Name` (semibold when has compositions). **No** inheritance hint, **no** attr/alarm/script count, **no** composition count on the node.
- Composition: `<i class="bi bi-arrow-return-right">` + composition instance name + muted `→ $ComposedTemplateName` secondary text.
**Search/filter:** out of scope for v1; the underlying component supports external filtering (per `Component-TreeView.md` R8) so it can be added later without component changes.
## Page layout
Two-column split inside the existing page container:
`/design/templates` is a **single-column tree browser** — no inline editor, no split pane.
```
+-------------------------------+----------------------------------------+
| Templates | $TestMachine |
| [+Folder][+Template][Expand] | inherits $gMachine |
| | [Properties] [Validate] |
| ▶ 📁 _Default Templates | |
| ▼ 📁 Dev | Tabs: Attributes | Alarms | Scripts |
| $TestMachine | | Compositions |
| DelmiaReceiver → ... | ... |
| MESReceiver → ... | |
| $TestObject | |
| ▶ 📁 System | |
| $UnfiledTemplate | |
+-------------------------------+----------------------------------------+
+--------------------------------------------+
| Templates |
| [+Folder] [+Template] [Expand] [Collapse] |
| |
| ▶ 📁 _Default Templates |
| ▼ 📂 Dev |
| 📄 $TestMachine |
| DelmiaReceiver → $DelmiaSvc |
| MESReceiver → $MesSvc |
| 📄 $TestObject |
| ▶ 📁 System |
| 📄 $UnfiledTemplate |
+--------------------------------------------+
```
- Left column: ~2533% (`col-md-4 col-lg-3`), scrollable (`max-height: calc(100vh - 160px); overflow-y: auto`).
- Right column: existing template properties card, validation block, four-tab editor (Attributes / Alarms / Scripts / Compositions) lifted unchanged into `RenderTemplateDetail()`.
- "Back to List" button is removed — the tree is always visible.
- Empty state in the right column when nothing is selected.
- URL contract preserved: `/design/templates/{id}` selects + reveals the template on load via `TreeView.RevealNode("t:" + id, select: true)`.
- Tree scrollable region: `max-height: calc(100vh - 160px); overflow-y: auto`. The 2533% sidebar width constraint is removed; the tree uses the page's main container width.
- Selecting a template node navigates to `/design/templates/{id}` (TemplateEdit page).
- Selecting a composition node navigates to the composed template's edit page.
- Selecting a folder node is a no-op (still allowed; expansion and context-menu still work).
- Creating a template: toolbar "+ Template" button (or folder context-menu "New Template") navigates to `/design/templates/create?folderId={id}`. After successful create, the create page navigates to `/design/templates/{newId}`.
- URL contract for deep links: `/design/templates/{id}` resolves to the TemplateEdit page directly — the browser doesn't need to be on the tree page first.
## Context menus
Per-node-kind `ContextMenu` fragment driven by `node.Kind`:
The context menu is the **only** reorganization mechanism. Per-node-kind `ContextMenu` fragment driven by `node.Kind`:
**Folder:** New Folder · New Template · Rename · Delete
**Template:** Edit · Move to Folder… (modal with folder-only mini-tree, "(Root)" option) · Delete
**Folder:** New Folder · New Template · Rename · Move to Folder… · Delete
**Template:** Edit · Move to Folder… · Delete
**Composition:** Open composed template · Remove composition
The "New Template" modal collects name + description and creates with `FolderId = thisFolder.Id`. Root-level "+Folder" and "+Template" buttons live in the tree-sidebar toolbar above the tree.
- **Move to Folder…** opens a modal (`MoveFolderDialog` / `MoveTemplateDialog`) with a flat folder picker. The list includes "(Root)" as the first entry. For folder-move, the dialog client-side prunes the folder being moved and its descendants from the candidate list to prevent obvious cycles; the server still validates (authoritative). For template-move, all folders are valid targets.
- **Edit** on a template navigates to `/design/templates/{id}` (TemplateEdit page) — equivalent to clicking the node, kept in the menu for discoverability.
- Root-level "+ Folder" and "+ Template" buttons live in the toolbar above the tree.
## Drag-drop
Native HTML5 drag-drop, no library.
**Draggability:** folders and templates are `draggable="true"`. Composition nodes are not draggable.
**Drop targets:** folder nodes and the root sidebar wrapper. Template/composition nodes are not drop targets in v1.
**Payload:** `_dragPayload = (kind, id)` held in component state on `@ondragstart`.
**Visual feedback:** CSS `drag-over` class toggled via `@ondragenter` / `@ondragleave`; compositions dimmed to 0.5 opacity while drag in progress.
**Server-side validation (authoritative):**
**Server-side validation (authoritative)**:
- Folder onto descendant → reject (cycle).
- Folder onto itself → no-op.
- Drop on a composition → ignored.
- Template-onto-template → **ignored** (no sibling reordering, no surprising "drop into parent's folder").
- Folder onto itself → no-op (client prunes).
- Template-onto-template → not a valid target (templates aren't shown in the folder picker).
## Edge cases
- Deep-link route reveals ancestors via `RevealNode`.
- Deep-link route `/design/templates/{id}` resolves directly to the TemplateEdit page; the tree page is not involved. If the user navigates back, the tree's sessionStorage-persisted expansion state is restored.
- Stale `f:{id}` keys in `sessionStorage` after folder delete are harmless (ignored on next render).
- Selected template moved to another folder → tree rebuilds; selection preserved by stable key.
- Selected template deleted → right pane clears to empty state.
- Template deleted from the TemplateEdit page → page navigates back to `/design/templates`; the tree rebuilds without the deleted node.
- Last-write-wins on concurrent folder edits, matching existing template policy.
- Tree fully rebuilt on every CRUD; expected scale (dozens to low hundreds) makes this trivially cheap.
@@ -226,14 +220,16 @@ Native HTML5 drag-drop, no library.
**bUnit (`tests/ScadaLink.CentralUI.Tests/`):**
- Tree renders folders / templates / compositions in correct nesting.
- Empty state when no selection.
- Selecting a template node loads the detail pane.
- Selecting a composition reveals + selects the composed template.
- Right-click menus differ by node kind.
- Folder-delete-non-empty surfaces structured error toast.
- Deep link selects + reveals.
- Empty state when no roots exist (no folders, no root templates).
- Selecting a template node invokes `NavigationManager.NavigateTo($"/design/templates/{id}")`.
- Selecting a composition node invokes `NavigateTo` for the composed template's edit page.
- Selecting a folder node is a no-op (no navigation).
- Right-click menus differ by node kind (Folder / Template / Composition each have distinct items).
- Folder context menu includes "Move to Folder…"; the dialog excludes the folder being moved and its descendants from candidates.
- Folder-delete-non-empty surfaces a structured error toast.
- Bootstrap Icons render in the glyph slot for each node kind (`bi-folder` / `bi-folder2-open` / `bi-file-earmark-text` / `bi-arrow-return-right`).
**Manual smoke (per `CLAUDE.md`):** nested folder creation, drag-drop reorg, cycle rejection, refresh persistence, composition navigation.
**Manual smoke (per `CLAUDE.md`):** nested folder creation, context-menu reorg (folder + template Move-to-Folder dialogs), cycle rejection, refresh persistence, composition navigation, navigation from tree to TemplateEdit and back.
## Documentation updates
@@ -247,6 +243,6 @@ Native HTML5 drag-drop, no library.
- Tree search / filter input (component already supports it; add when needed).
- CLI commands for folder operations (message contracts make this trivial later).
- Sibling reorder via drag-drop (sort stays alphabetical).
- Sibling reorder (sort stays alphabetical).
- Root context menu (right-click in empty tree area).
- Bootstrap Icons CDN — current pages don't use it, so this design uses Unicode glyphs.
- (Removed from out-of-scope.) Bootstrap Icons are now adopted (static files at `wwwroot/lib/bootstrap-icons/`) — see `Component-TreeView.md` V4.