docs(plans): Instance Picker implementation plan (8 tasks)
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -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 `<select>` on Flow Debugger and Debug View with a shared combobox whose dropdown is the site's collapsed area tree, searchable by typing.
|
||||||
|
|
||||||
|
**Architecture:** A pure `InstanceTreeBuilder` turns `Area`/`Instance` lists (+ filter) into `InstanceTreeNode` forests; a new Bootstrap-only `InstancePicker` component wraps a text field + the existing `TreeView` in an absolutely-positioned panel with a transparent backdrop (no JS). Both pages keep their selection state and only swap the control. Design: `docs/plans/2026-09-08-instance-picker-design.md`.
|
||||||
|
|
||||||
|
**Tech Stack:** Blazor Server, Bootstrap 5, bUnit + NSubstitute + xUnit, Playwright (rig).
|
||||||
|
|
||||||
|
**Branch:** `feat/instance-picker` (from `main` @ `859922ba`). Worktree agents branch from `main`, so every implementer starts with `git reset --hard <current feature sha>` (see memory `agent-worktree-isolation-base`).
|
||||||
|
|
||||||
|
**Conventions:** `docs/plans/...` rules in `CLAUDE.md`; tests run per project (`dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter ...`); commit trailer `Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29`. Never block on a Task from a Blazor lifecycle/event path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `InstanceTreeNode` + `InstanceTreeBuilder` (pure) with tests
|
||||||
|
|
||||||
|
**Classification:** standard
|
||||||
|
**Estimated implement time:** ~4 min
|
||||||
|
**Parallelizable with:** none (everything else depends on it)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstanceTreeNode.cs`
|
||||||
|
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstanceTreeBuilder.cs`
|
||||||
|
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/InstanceTreeBuilderTests.cs`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests** (`InstanceTreeBuilderTests`, namespace `ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared`; `Area` is `ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Area` — check the namespace with grep — `new Area("Name") { Id, SiteId, ParentAreaId }`; `Instance` is `Commons.Entities.Instances.Instance`, `new Instance("Name") { Id, SiteId, AreaId }`):
|
||||||
|
|
||||||
|
- `Build_NestsAreasByParent_AndPutsRootInstancesAfterRootAreas`: areas Line1 (1), Cell A (2, parent 1); instances P1 (area 2), Zulu (no area), Alpha (no area) → roots = [`a:1`, `i:<Alpha>`, `i:<Zulu>`]; `a:1` → [`a:2`] → [`i:P1`]. Labels = names; `IsInstance` true only on instance nodes.
|
||||||
|
- `Build_SortsAreasAndInstancesCaseInsensitively`: "beta", "Alpha" areas → Alpha first; same for instances.
|
||||||
|
- `Build_OrphanedArea_BecomesARoot`: area with `ParentAreaId = 99` (absent) appears at root, its instances under it.
|
||||||
|
- `Build_Filter_KeepsMatchingInstancesWithAncestors_DropsEmptyBranches`: filter "p1" (case-insensitive) keeps `a:1 → a:2 → i:P1`, drops Zulu/Alpha and any area with no match; `Build_Filter_WhitespaceMeansNoFilter`.
|
||||||
|
- `Build_NullOrEmptyInputs_ReturnEmpty` (null areas / null instances tolerated).
|
||||||
|
- `Build_AreaWithNoInstancesAndNoFilter_IsStillShown` (empty areas are shown when unfiltered).
|
||||||
|
|
||||||
|
**Step 2: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter FullyQualifiedName~InstanceTreeBuilderTests` → build errors (types missing).
|
||||||
|
|
||||||
|
**Step 3: Implement**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// InstanceTreeNode.cs
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||||
|
|
||||||
|
/// <summary>One row of the <see cref="InstancePicker"/> tree: an area branch or an instance leaf. Keys are a:{areaId} / i:{instanceId}.</summary>
|
||||||
|
public sealed record InstanceTreeNode(string Key, string Label, Area? Area, Instance? Instance)
|
||||||
|
{
|
||||||
|
public List<InstanceTreeNode> Children { get; } = new();
|
||||||
|
public bool IsInstance => Instance is not null;
|
||||||
|
public bool HasChildren => Children.Count > 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// InstanceTreeBuilder.cs — pure, no Blazor/DI.
|
||||||
|
public static class InstanceTreeBuilder
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<InstanceTreeNode> Build(IReadOnlyList<Area>? areas, IReadOnlyList<Instance>? instances, string? filter)
|
||||||
|
// 1. areas ??= []; instances ??= []; needle = filter?.Trim(); hasFilter = !string.IsNullOrEmpty(needle)
|
||||||
|
// 2. known = set of area ids; children-of lookup: parentId → areas (null parent OR parent not in known ⇒ root)
|
||||||
|
// 3. instancesByArea: AreaId (null ⇒ root) → instances, each sorted OrdinalIgnoreCase by UniqueName
|
||||||
|
// 4. BuildLevel(parentId): child areas sorted by Name OrdinalIgnoreCase, then instances of that level;
|
||||||
|
// with a filter an instance is kept iff UniqueName.Contains(needle, OrdinalIgnoreCase),
|
||||||
|
// an area is kept iff it has any kept child (area or instance); without a filter everything is kept.
|
||||||
|
// 5. return roots
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run the tests → PASS. Step 5: Commit** `feat(central-ui): InstanceTreeBuilder — area/instance forest for the instance picker`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `InstancePicker` component — field, panel, select, outside-click
|
||||||
|
|
||||||
|
**Classification:** standard
|
||||||
|
**Estimated implement time:** ~5 min
|
||||||
|
**Parallelizable with:** none
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor`
|
||||||
|
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor.cs`
|
||||||
|
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor.css`
|
||||||
|
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/InstancePickerTests.cs`
|
||||||
|
|
||||||
|
Read first: `TreeView.razor` (parameters at ~line 165–190, `ExpandAll`/`CollapseAll`/`RevealNode` at ~600–680, the context-menu overlay markup at ~line 41 for the backdrop pattern), `TemplatePickerDialog.razor` (search box + tree host + `NodeContent` with `tv-glyph`/`tv-label`), `TemplatePickerDialogTests.cs` (bUnit style; `JSInterop.Mode = JSRuntimeMode.Loose`).
|
||||||
|
|
||||||
|
**Step 1: Failing tests** (`InstancePickerTests : BunitContext`, Loose JS interop). Fixture: areas Line1 (1) → Cell A (2); instances P1 (id 7, area 2), Zulu (id 8, root). Render `InstancePicker` with `Areas`, `Instances`, `DataTestPrefix="t"`, `SelectedInstanceIdChanged` capturing into a local.
|
||||||
|
|
||||||
|
- `Closed_ShowsNothingButTheField`: no `[data-test='t-panel']`; input `[data-test='t-input']` has `role="combobox"`, `aria-expanded="false"`.
|
||||||
|
- `Toggle_OpensPanel_WithAreasCollapsed`: click `[data-test='t-toggle']` → panel present; rows `[data-test='t-row']` visible = Line1 (area) and Zulu (root instance) only — P1 is under a collapsed area, so no `[data-instance-id='7']`.
|
||||||
|
- `ClickingAnArea_ExpandsIt`: click the `tv-toggle` of Line1 then Cell A → `[data-instance-id='7']` appears.
|
||||||
|
- `ClickingAnInstance_Selects_Closes_AndRaisesTheCallback`: after expanding, click `[data-instance-id='7']` → callback got 7, panel gone, input value `"P1"`.
|
||||||
|
- `Disabled_NeverOpens`: `Disabled=true` → input disabled, toggle disabled, clicking does nothing.
|
||||||
|
- `Backdrop_ClickClosesThePanel_WithoutChangingSelection`.
|
||||||
|
- `ParentDrivenSelection_UpdatesTheFieldText`: re-render with `SelectedInstanceId=8` → input value "Zulu"; `SelectedInstanceId=0` → "".
|
||||||
|
- `ItemLabel_IsUsedForRowsAndField`: `ItemLabel = i => i.UniqueName + " (x)"`.
|
||||||
|
|
||||||
|
**Step 2: run → build errors. Step 3: Implement.**
|
||||||
|
|
||||||
|
`InstancePicker.razor` (sketch — Bootstrap only):
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
||||||
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
|
||||||
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
||||||
|
|
||||||
|
<div class="ip-wrap position-relative" data-test="@($"{DataTestPrefix}-picker")">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control" role="combobox" autocomplete="off"
|
||||||
|
id="@_inputId" aria-autocomplete="list" aria-haspopup="tree"
|
||||||
|
aria-expanded="@(_open ? "true" : "false")" aria-controls="@_panelId"
|
||||||
|
placeholder="@Placeholder" disabled="@Disabled"
|
||||||
|
data-test="@($"{DataTestPrefix}-input")"
|
||||||
|
value="@_text" @oninput="OnInput" @onfocus="OpenAsync" @onkeydown="OnInputKeyDown" />
|
||||||
|
<button type="button" class="btn btn-outline-secondary" tabindex="-1" aria-label="Browse instances"
|
||||||
|
disabled="@Disabled" data-test="@($"{DataTestPrefix}-toggle")" @onclick="ToggleAsync">
|
||||||
|
<i class="bi @(_open ? "bi-chevron-up" : "bi-chevron-down")"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (_open)
|
||||||
|
{
|
||||||
|
<div class="ip-backdrop" @onclick="CloseAsync"></div>
|
||||||
|
<div class="ip-panel border rounded bg-body shadow-sm" id="@_panelId" data-test="@($"{DataTestPrefix}-panel")">
|
||||||
|
<TreeView @ref="_tree" TItem="InstanceTreeNode" Items="_roots"
|
||||||
|
ChildrenSelector="n => n.Children" HasChildrenSelector="n => n.HasChildren"
|
||||||
|
KeySelector="n => n.Key" IndentPx="16">
|
||||||
|
<NodeContent Context="node">
|
||||||
|
<span class="tv-glyph"><i class="bi @(node.IsInstance ? "bi-box" : "bi-diagram-3")"></i></span>
|
||||||
|
<span class="tv-label @(node.IsInstance ? "ip-instance" : "ip-area")" title="@node.Label"
|
||||||
|
data-test="@($"{DataTestPrefix}-row")" data-instance-id="@(node.Instance?.Id)"
|
||||||
|
@onclick="() => OnRowClickAsync(node)">@node.Label</span>
|
||||||
|
</NodeContent>
|
||||||
|
<EmptyContent><span class="text-muted fst-italic small px-2">@(_hasFilter ? "No instances match." : "No instances at this site.")</span></EmptyContent>
|
||||||
|
</TreeView>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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<InstanceTreeNode>?`), `_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 `<select>`)
|
||||||
|
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs` (`_instances` ~114, `LoadInstancesAsync` ~412)
|
||||||
|
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs` (five `select[data-test='flow-instance-select']` call sites ~623/637/656/675/717; `WireInstance` ~608)
|
||||||
|
|
||||||
|
**Steps:** (1) Add a test helper `static void PickInstance(IRenderedComponent<FlowDebuggerPage> cut, int id)` = click `[data-test='flow-instance-toggle']`, click `[data-test='flow-instance-row'][data-instance-id='{id}']` (the fixture instance has no area, so it is a root row); replace the five `.Change("7")` lines; make `_templates.GetAreasBySiteIdAsync(1, …)` return `[]` in `WireInstance`. Add `ScriptEntry_InstancePicker_ShowsAreasCollapsed` (areas fixture with the instance under an area: only the area row is visible until expanded). (2) Run the class → fails. (3) Page: `private List<Area> _areas = [];` loaded in `LoadInstancesAsync` alongside instances (`GetAreasBySiteIdAsync`), cleared on site 0; markup:
|
||||||
|
|
||||||
|
```razor
|
||||||
|
<InstancePicker Areas="_areas" Instances="_instances"
|
||||||
|
SelectedInstanceId="_selectedInstanceId" SelectedInstanceIdChanged="OnInstancePickedAsync"
|
||||||
|
Disabled="@(_running || _selectedSiteId == 0)" DataTestPrefix="flow-instance" />
|
||||||
|
```
|
||||||
|
with `private async Task OnInstancePickedAsync(int id) { _selectedInstanceId = id; await OnInstanceChangedAsync(); }`. Everything else (deep link, localStorage, scope) unchanged. (4) `FlowDebuggerPageTests` all pass. (5) Commit `feat(central-ui): Flow Debugger picks the instance through InstancePicker`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Wire Debug View to the picker
|
||||||
|
|
||||||
|
**Classification:** standard
|
||||||
|
**Estimated implement time:** ~4 min
|
||||||
|
**Parallelizable with:** Task 4, Task 6
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor` (instance `<select>` ~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 `<select>` + `SelectOptionAsync` with: `FillAsync` the `[data-test='{prefix}-input']` with the instance name, then `Expect(row [data-test='{prefix}-row'][data-instance-id='{id}']).ToBeVisibleAsync()` and `ClickAsync` it (Flow Debugger knows the name, not the id → click `[data-test='flow-instance-row']:has-text("<name>")` exactly; `Skip.If` no row appears within 10 s, same skip messages). `DebugViewTests` line 68 asserts the picker container `[data-test='debug-instance-picker']` count 1. Not runnable without the rig — build the project (`dotnet build tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests`) and commit `test(playwright): drive the instance picker on Flow Debugger and Debug View`. The controller runs them on the rig in the gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Docs
|
||||||
|
|
||||||
|
**Classification:** trivial
|
||||||
|
**Estimated implement time:** ~2 min
|
||||||
|
**Parallelizable with:** Task 4, Task 5, Task 6
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/requirements/Component-CentralUI.md` (Flow Debugger v1.3 paragraph + the Debug View section — grep `debug-instance-select` / "Debug View")
|
||||||
|
- Modify: `CLAUDE.md` (Flow Debugger bullet, v1.3 sentence — append one sentence)
|
||||||
|
|
||||||
|
Text: "The instance is picked through the shared **`InstancePicker`** combobox (2026-09-08, `docs/plans/2026-09-08-instance-picker-design.md`): a text field whose dropdown is the site's area tree (`InstanceTreeBuilder`, all areas collapsed, un-areaed instances at the root), typed text filters by instance name and expands to the matches, Enter picks the first match; Debug View uses the same component with a `(State)` suffix." Commit `docs: instance picker on Flow Debugger + Debug View`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Gate (controller)
|
||||||
|
|
||||||
|
Build solution (0 warnings), run `CentralUI.Tests` + `Commons.Tests`, `bash docker/deploy.sh`, Playwright `FlowDebuggerTests` + `DebugViewTests` + `DebugViewTreeTests`, browser check of both pages (collapsed areas, typing filter, Enter, Escape, deep link `?instanceId=`), then append gate evidence to this plan and finish via `finishing-a-development-branch`.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"planPath": "docs/plans/2026-09-08-instance-picker.md",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"subject": "Task 1: InstanceTreeNode + InstanceTreeBuilder (pure) with tests",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"subject": "Task 2: InstancePicker component \u2014 field, panel, select, outside-click",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"subject": "Task 3: InstancePicker \u2014 search filter + keyboard",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"subject": "Task 4: Wire Flow Debugger to the picker",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"subject": "Task 5: Wire Debug View to the picker",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"subject": "Task 6: Playwright tests drive the picker",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"subject": "Task 7: Docs",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"subject": "Task 8: Gate (controller)",
|
||||||
|
"status": "pending",
|
||||||
|
"blockedBy": [
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastUpdated": "2026-09-08T13:18:02.265057Z"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user