From 8e388a89c55c7ce52a8229afd07cb42174c736b6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 11 May 2026 20:52:34 -0400 Subject: [PATCH] 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. --- ...05-11-templates-folder-hierarchy-design.md | 108 +- docs/requirements/Component-TreeView.md | 196 +- .../Pages/Design/TemplateCreate.razor | 7 +- .../Pages/Design/TemplateEdit.razor | 860 +++++++ .../Components/Pages/Design/Templates.razor | 1110 +-------- ...ateDialog.razor => MoveFolderDialog.razor} | 33 +- .../Components/Shared/TreeView.razor | 37 +- .../Components/Shared/TreeView.razor.css | 135 ++ src/ScadaLink.Host/Components/App.razor | 2 + .../lib/bootstrap-icons/bootstrap-icons.css | 2078 +++++++++++++++++ .../fonts/bootstrap-icons.woff | Bin 0 -> 176032 bytes .../fonts/bootstrap-icons.woff2 | Bin 0 -> 130396 bytes .../TemplatesPageTests.cs | 36 - .../TreeViewTests.cs | 40 +- 14 files changed, 3515 insertions(+), 1127 deletions(-) create mode 100644 src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor rename src/ScadaLink.CentralUI/Components/Shared/{NewTemplateDialog.razor => MoveFolderDialog.razor} (56%) create mode 100644 src/ScadaLink.CentralUI/Components/Shared/TreeView.razor.css create mode 100644 src/ScadaLink.Host/wwwroot/lib/bootstrap-icons/bootstrap-icons.css create mode 100644 src/ScadaLink.Host/wwwroot/lib/bootstrap-icons/fonts/bootstrap-icons.woff create mode 100644 src/ScadaLink.Host/wwwroot/lib/bootstrap-icons/fonts/bootstrap-icons.woff2 diff --git a/docs/plans/2026-05-11-templates-folder-hierarchy-design.md b/docs/plans/2026-05-11-templates-folder-hierarchy-design.md index d81a9b6..9656d31 100644 --- a/docs/plans/2026-05-11-templates-folder-hierarchy-design.md +++ b/docs/plans/2026-05-11-templates-folder-hierarchy-design.md @@ -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: `$Name` + optional "inherits $Parent" muted text + existing attr/alarm/script/comp count badges. -- Composition: `InstanceName` + muted `→ $ComposedTemplateName`. +**Inline node labels** (see `Component-TreeView.md` V7 for the canonical recipe): +- Folder: `` (closed) or `` (expanded) + name (semibold when has children) + count-pill badge of direct children. +- Template: `` + `$Name` (semibold when has compositions). **No** inheritance hint, **no** attr/alarm/script count, **no** composition count on the node. +- Composition: `` + 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. diff --git a/docs/requirements/Component-TreeView.md b/docs/requirements/Component-TreeView.md index 18addcf..6d327ba 100644 --- a/docs/requirements/Component-TreeView.md +++ b/docs/requirements/Component-TreeView.md @@ -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 `` 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 `
  • ` 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 +
    + …chevron or placeholder… + …Bootstrap Icon or placeholder… + …primary + secondary… + …badges… +
    +``` + +| 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) | `` | +| Toggle glyph (expanded) | `` (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 + +``` + +No CDN dependency — works on air-gapped industrial deployments. Version pinned in the file path or filename. + +**Rules**: +- Glyphs are inline `` 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 | `@N` | folder child count; area instance count | +| Status | semantic state | `@Label` | 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) | `→ $TargetName` | 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); + + @node.Label + @if (node.Children.Count > 0) + { + + @node.Children.Count + + } + break; + + case TmplNodeKind.Template: + + @node.Label + break; + + case TmplNodeKind.Composition: + var composedName = _templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name + ?? $"#{node.Composition!.ComposedTemplateId}"; + + + @node.Label + → @composedName + + 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 diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateCreate.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateCreate.razor index dd5ccac..d8551a2 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateCreate.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateCreate.razor @@ -56,6 +56,8 @@ @code { + [SupplyParameterFromQuery] public int? FolderId { get; set; } + private List