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:
@@ -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 (~25–33% 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: ~25–33% (`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 25–33% 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.
|
||||
|
||||
@@ -64,10 +64,9 @@ The `NodeContent` fragment receives the `TItem` and is responsible for rendering
|
||||
|
||||
### R4 — Indentation and Visual Structure
|
||||
|
||||
- Each depth level is indented by a fixed amount (default 24px, configurable via `IndentPx` parameter).
|
||||
- Vertical guide lines connect parent to children at each depth level (thin left-border or CSS pseudo-element).
|
||||
- The toggle icon is inline with the node content, left-aligned at the current depth.
|
||||
- Leaf nodes align with sibling branch labels (the content starts at the same horizontal position, with empty space where the toggle would be).
|
||||
The component renders the structural chrome: indent gutters per depth, the toggle slot, and ancestor guide lines. Leaf nodes render an empty toggle placeholder so labels align across siblings.
|
||||
|
||||
The exact tokens (indent unit, toggle glyph, guide-line treatment) are specified in **V2** of the Visual Design Guide.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -132,12 +131,11 @@ This keeps filter logic in the page (domain-specific) while the component handle
|
||||
|
||||
### R9 — Styling
|
||||
|
||||
- Uses Bootstrap 5 utility classes only (no third-party frameworks).
|
||||
- No hardcoded colors — uses standard Bootstrap text/background utilities.
|
||||
- Toggle icons: Unicode characters (`+` / `−`) in a `<span>` with `cursor: pointer`, or a small SVG chevron. No icon library dependency.
|
||||
- Compact row height for dense data (matching `table-sm` density).
|
||||
- Hover effect on rows: subtle background highlight (`bg-light` or similar).
|
||||
- CSS scoped to the component via Blazor CSS isolation (`TreeView.razor.css`).
|
||||
- Uses Bootstrap 5 utility classes and CSS variables. No third-party Blazor component frameworks.
|
||||
- Adds one icon-library dependency: **Bootstrap Icons** (static files at `wwwroot/lib/bootstrap-icons/`). Distribution rules in **V4** of the Visual Design Guide.
|
||||
- Hardcoded colors are forbidden; use Bootstrap utility classes (`bg-primary bg-opacity-10`, `text-muted`) or CSS variables (`var(--bs-tertiary-bg)`, `var(--bs-border-color)`).
|
||||
- Component-local CSS lives in `TreeView.razor.css` (Blazor CSS isolation).
|
||||
- All visual tokens (row density, indent, state visuals, glyphs, labels, badges) are specified in the **Visual Design Guide** (V1–V7). This requirement is non-normative summary; the Guide is authoritative.
|
||||
|
||||
### R10 — No Internal Scrolling
|
||||
|
||||
@@ -296,7 +294,183 @@ Future enhancement. Single selection (R5) covers current needs. A future version
|
||||
- Shift+click for range select, Ctrl+click for toggle
|
||||
- Use case: bulk operations (select multiple instances → deploy/disable all)
|
||||
|
||||
## Component API Summary
|
||||
## Visual Design Guide
|
||||
|
||||
This section is the canonical visual specification for the TreeView. It is normative: any change to the chrome (row layout, indentation, glyphs, state visuals, badge styling) must update this section. Consumers' `NodeContent` fragments follow the label and badge recipes in V5–V6; `/design/templates` is the worked example in V7.
|
||||
|
||||
R4 and R9 above describe *that* the component renders structural chrome and uses Bootstrap utilities. This section says *exactly how*.
|
||||
|
||||
### V1 — Density & Row Anatomy
|
||||
|
||||
Each `<li role="treeitem">` renders one row. The row is a flexbox so trailing meta can right-align cleanly and the entire row width is a hover/selected/drop-target surface.
|
||||
|
||||
**Row container** (replaces today's `.tv-row` styling):
|
||||
|
||||
```html
|
||||
<div class="tv-row d-flex align-items-center"
|
||||
style="gap:.25rem; padding:.25rem .5rem; padding-left: calc(.5rem + var(--tv-indent, 0px));">
|
||||
<span class="tv-toggle">…chevron or placeholder…</span>
|
||||
<span class="tv-glyph">…Bootstrap Icon or placeholder…</span>
|
||||
<span class="tv-label">…primary + secondary…</span>
|
||||
<span class="tv-meta ms-auto">…badges…</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
| Token | Value | Notes |
|
||||
|---|---|---|
|
||||
| Row vertical padding | `py-1` (0.25rem top/bottom) | Yields ~32px row height at base font-size + line-height 1.5. |
|
||||
| Row horizontal padding | `px-2` (0.5rem left/right) | Selected/hover background spans full row including this padding. |
|
||||
| Inter-slot gap | `gap: .25rem` | Between toggle, glyph, label. The meta slot is offset by `margin-left: auto`. |
|
||||
| Font size | inherits (1rem base) | Compact pages may opt into `small` per-page, not at the component level. |
|
||||
| Line height | inherits (1.5) | Aligns the chevron, glyph, and label baselines correctly. |
|
||||
| Toggle slot width | 20px (`width: 1.25rem`) | Always present, even on leaves (which render an empty placeholder). |
|
||||
| Glyph slot width | 20px (`width: 1.25rem`) | Always present; consumer may render an empty span to preserve alignment. |
|
||||
| Label slot | `flex: 1 1 auto; min-width: 0;` | `min-width: 0` is required for ellipsis truncation to work in a flex child. |
|
||||
| Meta slot | `margin-left: auto;` | Pushes badges to the right edge of the row. |
|
||||
|
||||
**Hit semantics**:
|
||||
- The full row (`tv-row`) is the surface for hover, selected, focus-visible, and drop-target backgrounds.
|
||||
- Click-to-select fires only on the **label slot** (preserves R5: toggle clicks do not select).
|
||||
- The toggle slot's invisible tap target is enlarged by negative margins inside the 20px slot so it remains a comfortable 24×24px target.
|
||||
|
||||
### V2 — Depth, Indent & Guide Lines
|
||||
|
||||
| Token | Value |
|
||||
|---|---|
|
||||
| Indent per depth | 24px (`IndentPx` default, unchanged) |
|
||||
| Toggle glyph (collapsed) | `<i class="bi bi-chevron-right">` |
|
||||
| Toggle glyph (expanded) | `<i class="bi bi-chevron-down">` (or `bi-chevron-right` rotated 90° via CSS) |
|
||||
| Guide line color | `var(--bs-border-color)` |
|
||||
| Guide line width | 1px |
|
||||
| Guide line style | solid, vertical-only (no horizontal stubs) |
|
||||
| Guide line position | one line per ancestor depth, drawn down the indent column (left edge of each 24px indent slot) |
|
||||
| Guide lines enabled | `ShowGuideLines` parameter (default true) |
|
||||
| Leaf alignment | identical depth gutter as siblings; the toggle slot renders an empty placeholder so glyphs and labels align across leaves and branches |
|
||||
|
||||
Implementation note: guide lines are drawn by repeating a `linear-gradient` background or by stacking `border-left` on indent spacers — both are pure CSS, no extra DOM. The current `tv-guides` class is the hook.
|
||||
|
||||
### V3 — State Visuals
|
||||
|
||||
States compose: focus rings layer on top of hover/selected; drop-target overrides hover and selected. All states paint the full row width (V1).
|
||||
|
||||
| State | Visual | Implementation |
|
||||
|---|---|---|
|
||||
| Default | none | — |
|
||||
| Hover | full-row tint | `background: var(--bs-tertiary-bg);` on `:hover` of `.tv-row` |
|
||||
| Focus-visible | inset 2px primary ring | `box-shadow: inset 0 0 0 2px var(--bs-primary);` on `:focus-visible` |
|
||||
| Selected | full-row primary tint | `class="bg-primary bg-opacity-10"` (existing `SelectedCssClass` default, unchanged) |
|
||||
| Selected + hover | selected tint persists; hover does not deepen | hover background applies only when not selected (`:hover:not(.bg-primary)`) |
|
||||
| Selected + focus | tint + ring both visible | focus ring layers via box-shadow |
|
||||
| Drop-target (valid) | `bg-info bg-opacity-25` | overrides hover/selected backgrounds; opt-in per consumer |
|
||||
| Drop-target (invalid) | cursor `not-allowed`, no tint change | absence of valid-tint is the cue |
|
||||
| Dragging source | `opacity: 0.5` | applied to the row currently being dragged |
|
||||
| Dimmed (non-droppable while a drag is in progress) | `opacity: 0.5` | applied to nodes the consumer marks as unsuitable drop targets |
|
||||
|
||||
Drag-drop is **not** part of the TreeView component's intrinsic behavior — it is opt-in per consuming page. The drag-related state visuals (drop-target, dragging, dimmed) are documented here so consumers that *do* implement DnD share the same visual language. The `/design/templates` page (V7) explicitly does **not** use drag-drop; reorganization happens via the right-click context menu.
|
||||
|
||||
### V4 — Glyph & Icon System
|
||||
|
||||
**Distribution**: Bootstrap Icons ships as static files under `src/ScadaLink.CentralUI/wwwroot/lib/bootstrap-icons/` (`bootstrap-icons.css` + `fonts/*.woff2`). Referenced once from `MainLayout.razor`:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="~/lib/bootstrap-icons/bootstrap-icons.css" />
|
||||
```
|
||||
|
||||
No CDN dependency — works on air-gapped industrial deployments. Version pinned in the file path or filename.
|
||||
|
||||
**Rules**:
|
||||
- Glyphs are inline `<i class="bi bi-…"></i>` elements inside the 20px glyph slot.
|
||||
- Branches render an **open/closed pair**: a `closed` glyph when collapsed, an `open` glyph when expanded (consumer chooses both via `NodeContent`). The chevron toggle reinforces the same state.
|
||||
- Leaves render a single static glyph or no glyph (empty span preserves alignment).
|
||||
- **Color**: glyphs inherit `color` from their row. Default is body text; consumers may apply `text-muted` for de-emphasis. Kind is communicated by *shape*, not by color, to keep the palette available for status badges.
|
||||
- **Size**: glyphs render at `1em` (inherits row font-size). No fixed pixel size.
|
||||
|
||||
### V5 — Label Recipe & Typography
|
||||
|
||||
The label slot contains, in order: **[primary] [secondary modifiers]**. Trailing meta lives in the separate `.tv-meta` slot (V1).
|
||||
|
||||
| Element | Style |
|
||||
|---|---|
|
||||
| Primary label (branches) | `class="fw-semibold"` |
|
||||
| Primary label (leaves) | normal weight |
|
||||
| Secondary modifiers | `class="text-muted small ms-1"` |
|
||||
| Overflow handling | `.tv-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }` |
|
||||
| Tooltip | `title` attribute on the primary label span, set to the full name on every row (cheap, helps when the row is narrower than the name) |
|
||||
|
||||
**Rule of thumb**: font-weight tracks *has children*, not *kind*. A folder with no children renders regular weight; a leaf-template promoted to a branch by adding compositions becomes semibold automatically.
|
||||
|
||||
### V6 — Badge Taxonomy
|
||||
|
||||
Three semantic badge roles. The meta slot holds **at most two** badges per row. All badges live in `.tv-meta`, right-aligned (V1).
|
||||
|
||||
| Role | Purpose | Markup | Examples |
|
||||
|---|---|---|---|
|
||||
| Count | numeric child aggregation | `<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@N</span>` | folder child count; area instance count |
|
||||
| Status | semantic state | `<span class="badge bg-{success\|warning\|danger\|info}">@Label</span>` | Enabled / Disabled / Stale / Error |
|
||||
| Kind | category / type tag | same filled semantic style, used sparingly | Protocol (OPC UA), Source (Inherited) |
|
||||
|
||||
**Rules**:
|
||||
- Counts represent **direct children only**. Never transitive descendants.
|
||||
- A count of 0 **renders nothing** — no badge at all.
|
||||
- Status uses Bootstrap semantic colors; do not introduce custom palettes.
|
||||
- The component does not enforce the 2-badge cap; it is a documented convention. PR review should catch violations.
|
||||
|
||||
### V7 — Worked Example: `/design/templates`
|
||||
|
||||
**Page model**: the templates page is a **tree browser only**. Selecting a template in the tree navigates to a dedicated edit page (`/design/templates/{id}`); creating a template navigates to `/design/templates/create`. No split-pane editor. Reorganization (move folder, move template) happens exclusively through the **right-click context menu** with modal dialog pickers — there is no drag-and-drop on this page.
|
||||
|
||||
Three node kinds; concrete recipes following V1–V6.
|
||||
|
||||
| Kind | Glyph (collapsed) | Glyph (expanded) | Primary | Secondary | Badges |
|
||||
|---|---|---|---|---|---|
|
||||
| Folder | `bi-folder` | `bi-folder2-open` | folder name (semibold when has children, regular otherwise) | — | count of direct children (subtle pill), only if ≥ 1 |
|
||||
| Template | `bi-file-earmark-text` | same (templates with compositions still use the same glyph — chevron carries state) | `$Name` (semibold when has compositions, regular otherwise) | — | none |
|
||||
| Composition | `bi-arrow-return-right` | n/a (leaf, no expanded state) | composition instance name (regular weight) | `<span class="text-muted small ms-1">→ $TargetName</span>` | none |
|
||||
|
||||
**`NodeContent` fragment** for the templates page (replaces the current `RenderNodeLabel` in `Templates.razor`):
|
||||
|
||||
```razor
|
||||
@switch (node.Kind)
|
||||
{
|
||||
case TmplNodeKind.Folder:
|
||||
var folderOpen = _tree.IsExpanded(node.Key);
|
||||
<span class="tv-glyph"><i class="bi @(folderOpen ? "bi-folder2-open" : "bi-folder")"></i></span>
|
||||
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
|
||||
title="@node.Label">@node.Label</span>
|
||||
@if (node.Children.Count > 0)
|
||||
{
|
||||
<span class="tv-meta ms-auto">
|
||||
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@node.Children.Count</span>
|
||||
</span>
|
||||
}
|
||||
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>
|
||||
break;
|
||||
|
||||
case TmplNodeKind.Composition:
|
||||
var composedName = _templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name
|
||||
?? $"#{node.Composition!.ComposedTemplateId}";
|
||||
<span class="tv-glyph"><i class="bi bi-arrow-return-right"></i></span>
|
||||
<span class="tv-label" title="@node.Label">
|
||||
@node.Label
|
||||
<span class="text-muted small ms-1">→ @composedName</span>
|
||||
</span>
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
**Locked subtractions from the previous design**:
|
||||
- Template node "inherits $Parent" muted text — **removed**. Inheritance is shown in the right pane only.
|
||||
- Template node "X attr, Y alm, Z scr" compound badge — **removed**.
|
||||
- Template node "N comp" accent badge — **removed**.
|
||||
|
||||
These subtractions are deliberate: templates are leaves-from-the-tree's-perspective (their inner attributes/alarms/scripts are not tree-navigable), so the tree row should carry only what's needed to identify and pick the template. All counts and inheritance information live in the right detail pane.
|
||||
|
||||
|
||||
|
||||
```csharp
|
||||
@typeparam TItem
|
||||
|
||||
Reference in New Issue
Block a user