From daa01261f36d9b161f82dd9bab9fb5171b156a47 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 11 May 2026 10:20:50 -0400 Subject: [PATCH] design(templates-page): folder hierarchy and split-pane tree layout Replaces the current /design/templates list view with a Wonderware-style template toolbox: nested TemplateFolder entity, FolderId on Template, composition children as inline tree leaves, persistent split-pane with editor on the right, context menus + drag-drop reorg. --- ...05-11-templates-folder-hierarchy-design.md | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 docs/plans/2026-05-11-templates-folder-hierarchy-design.md diff --git a/docs/plans/2026-05-11-templates-folder-hierarchy-design.md b/docs/plans/2026-05-11-templates-folder-hierarchy-design.md new file mode 100644 index 0000000..d81a9b6 --- /dev/null +++ b/docs/plans/2026-05-11-templates-folder-hierarchy-design.md @@ -0,0 +1,252 @@ +# 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 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. + +## 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; shown as text on the template node label. | +| Folder model | New `TemplateFolder` entity with self-referencing `ParentFolderId`. `Template.FolderId` nullable. | +| Reorganization UX | Context menus + native HTML5 drag-drop. | +| 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. | + +## 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