# Templates Page — Folder & Hierarchy Reorganization **Date:** 2026-05-11 **Status:** Design approved, ready for implementation planning **Scope:** `/design/templates` page in Central UI, plus supporting data model, services, message contracts, and migration. ## Goal 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 The reference image (Wonderware Template Toolbox) shows three distinct concepts that this design carries over: - **Folders** (yellow folder glyphs) — purely organizational, can be nested arbitrarily deep. - **Templates** (`$Name`) — placed inside folders or at the tree root. - **Composition children** — rendered inline under their owning template (e.g., `$TestMachine` shows `DelmiaReceiver` and `MESReceiver`). Inheritance is **not** rendered as tree nesting in the image, and it is not rendered as tree nesting in this design. Inheritance remains metadata on the template node label ("inherits $Parent"). ## Locked decisions | Decision | Choice | |---|---| | 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 | **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 | **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; composition rows also name-only — the glyph signals the kind), folder child-count pill. | ## Data model **New entity** in `src/ScadaLink.Commons/Entities/Templates/TemplateFolder.cs`: ```csharp public class TemplateFolder { public int Id { get; set; } public string Name { get; set; } // unique among siblings of the same parent (case-insensitive) public int? ParentFolderId { get; set; } // null = root public int SortOrder { get; set; } // reserved for future manual ordering; defaults to 0 // Audit fields follow existing entity conventions. public TemplateFolder(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } } ``` **Modification to `Template`:** ```csharp public int? FolderId { get; set; } // null = root ``` **Invariants (server-enforced):** - Folder name unique among siblings of the same parent (case-insensitive). - `ParentFolderId` graph is acyclic. - A folder cannot be deleted if it has any child folders or child templates. - Moving a template into a folder is a single FK update; folders carry no semantic meaning to the template engine. **Repository surface** (in `ITemplateEngineRepository` or a new `ITemplateFolderRepository`): - `GetAllFoldersAsync()` - `GetFolderAsync(int id)` - `AddFolderAsync(TemplateFolder)` - `UpdateFolderAsync(TemplateFolder)` - `DeleteFolderAsync(int id)` - `MoveFolderAsync(int folderId, int? newParentId)` - `MoveTemplateAsync(int templateId, int? newFolderId)` **Migration:** EF Core migration adds a `TemplateFolders` table and a nullable `FolderId` column on `Templates`. Existing templates retain `FolderId = null` (root). No data movement. **Audit:** All folder mutations and template-folder moves go through `IAuditService` with the same conventions as existing template operations. ## Server-side service **`TemplateFolderService`** (new, in `src/ScadaLink.TemplateEngine/`), mirroring `TemplateService`: - `CreateFolderAsync(name, parentFolderId?, user) → Result` - `RenameFolderAsync(id, newName, user) → Result` - `MoveFolderAsync(id, newParentId?, user) → Result` — cycle check: walk parent chain from `newParentId` upward, reject if `id` appears. - `DeleteFolderAsync(id, user) → Result` — structured failure with `(childFolderCount, childTemplateCount)` when non-empty. - `MoveTemplateAsync(templateId, newFolderId?, user) → Result