From 356b7620f8feb60167fea52588ec77f7d4a14cab Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 8 Sep 2026 09:18:02 -0400 Subject: [PATCH] docs(plans): Instance Picker implementation plan (8 tasks) Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- docs/plans/2026-09-08-instance-picker.md | 252 ++++++++++++++++++ .../2026-09-08-instance-picker.md.tasks.json | 70 +++++ 2 files changed, 322 insertions(+) create mode 100644 docs/plans/2026-09-08-instance-picker.md create mode 100644 docs/plans/2026-09-08-instance-picker.md.tasks.json diff --git a/docs/plans/2026-09-08-instance-picker.md b/docs/plans/2026-09-08-instance-picker.md new file mode 100644 index 00000000..1ad92970 --- /dev/null +++ b/docs/plans/2026-09-08-instance-picker.md @@ -0,0 +1,252 @@ +# Instance Picker Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the flat instance ` + + + @if (_open) + { +
+
+ + + + @node.Label + + @(_hasFilter ? "No instances match." : "No instances at this site.") + +
+ } + +``` + +Code-behind essentials: +- Parameters per design §2 table (`Areas`, `Instances`, `SelectedInstanceId`, `SelectedInstanceIdChanged`, `Disabled`, `ItemLabel`, `DataTestPrefix` (default `"instance"`), `Placeholder` (default `"Search or browse instances…"`)). +- State: `_open`, `_text`, `_filter`, `_roots`, `_tree` (`TreeView?`), `_inputId`/`_panelId` from a `Guid` suffix. +- `OnParametersSet`: if `Areas`/`Instances` references changed → rebuild `_roots` with the current filter and (if the selection is no longer in `Instances`) clear text; if `SelectedInstanceId` changed and the panel is closed → `_text = LabelOf(selected)`. +- `Rebuild()` → `InstanceTreeBuilder.Build(Areas, Instances, _filter)` — the label used for matching is `UniqueName` (the builder's contract), rows display `ItemLabel`. +- `OnRowClickAsync(node)`: area → `_tree.ToggleExpand`? (`TreeView` has no public toggle — expose one: add `public void ToggleExpanded(object key)` to `TreeView.razor` next to `IsExpanded`, a one-line wrapper around the existing private `ToggleExpand`); instance → `SelectAsync(node.Instance)`. +- `SelectAsync(inst)`: `_text = LabelOf(inst)`, `_filter = ""`, close, `await SelectedInstanceIdChanged.InvokeAsync(inst.Id)`. +- `CloseAsync`: `_open = false`, `_filter = ""`, `_text = LabelOf(current)`. +- Guard: when `Disabled`, `OpenAsync`/`ToggleAsync` no-op. + +CSS (`InstancePicker.razor.css`): `.ip-backdrop { position: fixed; inset: 0; z-index: 1040; background: transparent; }` `.ip-panel { position: absolute; left: 0; right: 0; top: 100%; z-index: 1041; max-height: 40vh; overflow-y: auto; min-width: 18rem; padding: .25rem; }` `.ip-instance { cursor: pointer; }` `.ip-area { cursor: pointer; }`. Rows render inside `TreeView`, whose `tv-*` classes are already global to that component — check `TreeView.razor.css` whether `tv-label`/`tv-glyph` are `::deep`-reachable from a consumer (TemplatePickerDialog uses them the same way, so follow that). + +**Step 4: tests → PASS. Step 5: Commit** `feat(central-ui): InstancePicker — combobox over the site's area tree`. + +--- + +### Task 3: `InstancePicker` — search filter + keyboard + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** none + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor` / `.razor.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/InstancePickerTests.cs` + +**Step 1: Failing tests** + +- `Typing_Filters_AndExpandsToTheMatches`: open, `Input("p1")` on the input → `[data-instance-id='7']` is rendered (ancestors expanded), Zulu row absent, Line1 row present. +- `Clearing_RestoresTheFullTree_Collapsed`: after the above, `Input("")` → Zulu back, P1 hidden again (Line1 collapsed). +- `Typing_OpensTheClosedPanel`. +- `Enter_PicksTheFirstVisibleMatch`: type "u" (matches Zulu only) + `KeyDown(Key="Enter")` → callback 8, closed, text "Zulu"; with no match Enter does nothing (still open). +- `Escape_ClosesAndRestoresTheSelectedText`: select P1, open, type "zzz", Escape → closed, text "P1". +- `ArrowDown_MovesFocusIntoTheTree`: after ArrowDown the first visible `[role='treeitem']` has `tabindex="0"` (the tree's roving tabindex target) — assert via the `TreeView` markup; focus itself is JS (Loose mode), so assert the call happened through `JSInterop` if `TreeView` uses `FocusAsync` (an `ElementReference.FocusAsync` shows up as a `Blazor._internal.domWrapper.focus` invocation in bUnit — assert `JSInterop.Invocations` contains it, or expose a `public Task FocusFirstVisibleAsync()` on `TreeView` and assert it doesn't throw). + +**Step 2: run → fail. Step 3: Implement** + +- `OnInput(ChangeEventArgs e)`: `_text = e.Value?.ToString() ?? ""`, `_filter = _text`, `_open = true`, `Rebuild()`; after render, if `_hasFilter` → `_tree?.ExpandAll()` else `_tree?.CollapseAll()` (do it in `OnAfterRender` guarded by a `_pendingExpand` flag so the tree instance exists). +- `OnInputKeyDown(KeyboardEventArgs e)`: `Enter` → first instance node in document order of the current (filtered) `_roots` → `SelectAsync`; `Escape` → `CloseAsync`; `ArrowDown` → `_open = true` and focus the tree's first visible item (`TreeView`: add `public async Task FocusFirstVisibleAsync()` that sets `_focusedKey` to the first visible node key and applies focus via the existing `_focusNeedsApply` mechanism). +- Typing on a closed picker opens it. + +**Step 4: PASS. Step 5: Commit** `feat(central-ui): InstancePicker search filter + keyboard (Enter/Escape/ArrowDown)`. + +--- + +### Task 4: Wire Flow Debugger to the picker + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 5, Task 6 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor` (~lines 62–71, the instance `` ~lines 83–92; `_siteInstances` ~259; `LoadInstancesForSite` ~430–452) +- Test: create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugViewInstancePickerTests.cs` (copy the service registration from `DebugViewDisposalTests.cs`) + +**Steps:** (1) Test: substitute repo returns one site, instances [Enabled "Sim01" id 7 in area 2] + areas [Line1 (1) → Cell A (2)]; render `DebugView`, `Change` the site select to the site, open `[data-test='debug-instance-toggle']` → only the Line1 row; expand twice → row `[data-instance-id='7']` text `"Sim01 (Enabled)"`; click it → *Connect* button enabled. (2) fails. (3) Page: `_siteAreas` list loaded in `LoadInstancesForSite` (same try/catch), picker markup with `ItemLabel="@(i => $"{i.UniqueName} ({i.State})")"`, `SelectedInstanceId="_selectedInstanceId" SelectedInstanceIdChanged="id => { _selectedInstanceId = id; OnInstanceSelectionChanged(); }"`, `Disabled="@_connected"`, `DataTestPrefix="debug-instance"`. (4) `DebugView*` tests pass. (5) Commit `feat(central-ui): Debug View picks the instance through InstancePicker`. + +--- + +### Task 6: Playwright tests drive the picker + +**Classification:** small +**Estimated implement time:** ~3 min +**Parallelizable with:** Task 4, Task 5 + +**Files:** +- Modify: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/FlowDebuggerTests.cs` (~lines 209–213) +- Modify: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs` (~68, ~93–97) +- Modify: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs` (~79–83) + +Replace `