From f9d08b738c74ba9e30ea3926c6ea199ec2edd007 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Mon, 3 Aug 2026 15:49:58 +0200 Subject: [PATCH] Take the group headings out of the host grid, and drop onto a group card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A first group turned the wrap of host cards into an accordion: SidebarRows interleaves a full-width fold-away heading — chevron, name, count — between the cards, and in a grid that reads as a dropdown somebody left open. The desktop grid binds VisibleHosts now. The headings and the fold stay for the phone, whose list has no room for the row of group cards the desktop draws above the grid. What a heading said, the card says: HostRowViewModel.GroupLabel, resolved once per reload like the tag names, drawn as an accent chip and absent from a host in no group — or in one that has been deleted, which is the same thing to look at. What a heading also was is the drop target, and that moves to the group cards. Two things go with it. A host dropped onto another host card used to be filed beside it, which was legible while a heading named the band of cards it landed in and is guesswork now; it is refused. And UNGROUPED was how a host was dragged back out of a group; the way out is the picker in its own editor, which is the one place "no group" can be said in words. A drag held at either edge of the grid scrolls it. Without that the gesture only works for whoever can see both ends of it: the group cards are the first thing in the scroller, the host may be the fortieth card down, and a drag cannot use the wheel. A step per drag event rather than a timer, so it follows the pointer and stops when it stops. The two heading-shaped tests are replaced. TheHostsGridHoldsCardsAndNoGroupHeadings asserts the grid's contents rather than only measuring them, because a heading that came back would lay out perfectly cleanly. TheGroupCardsAreWhatAcceptsADroppedHost raises a real DragOver over both kinds of card and checks the effects and the mark — the nearest a headless test gets to a gesture no headless test can synthesise. manual-checks 3.1-3.2 and 7.6-7.9 follow. --- docs/manual-checks.md | 73 +++++---- src/DodoSSH.Client.App/App.axaml | 35 +++-- .../Views/HostsScreen.axaml | 108 ++++++++----- .../Views/HostsScreen.axaml.cs | 144 +++++++++++++----- .../ViewModels/VaultViewModel.cs | 54 ++++++- .../HostGridTests.cs | 104 +++++++++++-- .../ScreenLayoutTests.cs | 39 ++++- .../ShellFlowTests.cs | 22 ++- 8 files changed, 432 insertions(+), 147 deletions(-) diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 2c4467d..cc3a96a 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -269,33 +269,32 @@ single-process test can reach. Open the hosts screen without creating any group. -**Pass:** the grid is the flat wrap of host cards it always was — no GROUPS section above it, no headings -between the cards, no UNGROUPED, nothing saying the hosts are unfiled. +**Pass:** the grid is the flat wrap of host cards it always was — no GROUPS section above it, no chip on any +card, nothing saying the hosts are unfiled. -**Failure means:** the "invisible until used" property is gone, and every existing user gets a heading they -did not ask for. `RebuildSidebarRows` returns early when `Groups` is empty; that early return is the feature. +**Failure means:** the "invisible until used" property is gone, and every existing user gets a section they +did not ask for. `HasGroups` is what hides the row of group cards; `HostRowViewModel.HasGroup` hides the chip. -### 3.2 Filing hosts, and folding a heading +### 3.2 Filing hosts, and reading which group a card is in -Make two groups, file some hosts into each through the host editor, then click a heading. +Make two groups and file some hosts into each through the host editor. -**Pass:** the heading's chevron flips and its hosts disappear; the count on the heading does not change, -because it counts what is in the group rather than what is on screen. Clicking again brings them back. - -**Also check:** clicking a heading does not change which host is selected — the drawer stays open on the -same machine and its EDIT and DELETE go on acting on it. This is asserted in a test, but the test drives the -view model directly; what it cannot see is whether the `ListBox` writes something else back through the -binding first. +**Pass:** every filed card carries the group's name as an accent chip under its address; the cards stay one +flat wrap, in one order, with nothing between them. **There is no heading and no fold on the desktop** — the +headings, their chevrons and UNGROUPED are the phone's, whose list has no room for a row of group cards. **Then press a group card.** The grid narrows to that group's hosts, the card is marked as chosen, and SHOW ALL appears beside GROUPS. Pressing it brings the rest back and unmarks the card. +**Failure means:** a full-width bar with a chevron between the cards is the old grouping coming back through +`SidebarRows` — the desktop grid binds `VisibleHosts`. See `HostsScreen.axaml`. + ### 3.3 Deleting a group with hosts in it Select a group with hosts and press DELETE. **Pass:** the question names how many hosts are filed under it and says they stay. Agreeing removes the -group; the hosts reappear under UNGROUPED with everything else about them unchanged. +group; the hosts lose their chip and are otherwise unchanged. **Failure means:** if the hosts vanish, the delete is rewriting host payloads, which it must not — see `HostGroupRepository`. @@ -305,8 +304,8 @@ group; the hosts reappear under UNGROUPED with everything else about them unchan Make a group on machine A, file a host into it, sync. On machine B, sync, then delete the group and sync again. Back on A, sync. -**Pass:** A shows the host under UNGROUPED. Open that host's editor: the group picker shows "(a group that is -no longer here)" and *keeps it selected*. Change the port and save. +**Pass:** A shows the host with no chip on its card, as though it were in no group. Open that host's editor: +the group picker shows "(a group that is no longer here)" and *keeps it selected*. Change the port and save. **Failure means:** if the picker opened on "No group", saving has just unfiled the host — quietly, as a side effect of an unrelated edit. That is the case `BuildGroupChoices` adds the placeholder for. @@ -642,33 +641,45 @@ Start 7.1's slow connection and press GIVE UP (or the tab's cross) while it is s opened — it is a real shell, and one running with nothing naming it would be worse than one that comes back. -### 7.6 Dragging a host into a group · **least covered, like all drag and drop** +### 7.6 Dragging a host onto a group card · **least covered, like all drag and drop** -Make two groups and file a host into one. Drag a host card onto another group's heading; onto a host card -inside another group; and onto UNGROUPED. +Make two groups and file a host into one. Drag a host card up onto the other group's card. -**Pass:** whatever is under the pointer washes accent while the pointer is over it, the cursor shows a move -rather than a refusal, and the drop files the host — it moves under that heading and the counts on both -headings change. Dropping onto its own group's heading is refused while still in the air. +**Pass:** the group card under the pointer takes a two-pixel accent border while the pointer is over it, the +cursor shows a move rather than a refusal, and the drop files the host — the chip on its card changes to the +new group's name, the host counts under both group cards change, and the status line says where it went. -**Failure means:** headless Avalonia cannot synthesise a platform drag, so nothing about this gesture is -automated. The write it performs is: `MovingAHostToAGroup_FilesItAndLeavesItSelected`. +**Also check three refusals**, each of which must show the "no" cursor and mark nothing: over the card of the +group the host is *already* in; over another **host** card, which is deliberately not a target now that there +are no headings to say which group it would mean; and over the empty space around the cards. + +**And getting a host back out** is the host's own editor — pick "No group" in its picker. There is no +UNGROUPED target on the desktop any more, because there is no UNGROUPED heading for it to be. + +**And a drag held near the top or bottom edge of the grid scrolls it**, which is what makes this usable at +all with forty machines: the group cards are the first thing in the scroller, and a drag cannot use the +wheel. The pointer has to keep moving inside the band — a stationary pointer gets no drag events. + +**Failure means:** headless Avalonia cannot synthesise a platform drag, so the picking up, the cursor and +the drop are covered by nothing. What *is* automated is the decision each drag event takes — +`HostGridTests.TheGroupCardsAreWhatAcceptsADroppedHost` raises a real `DragOver` over both kinds of card — +and the write at the end, `ShellFlowTests.MovingAHostToAGroup_FilesItAndLeavesItSelected`. ### 7.7 A click still selects, and a double click still connects -Click host cards; drag one a few pixels without releasing; double-click one. Then double-click a group -heading. +Click host cards; drag one a few pixels without releasing; double-click one. Then double-click the space +around the cards. -**Pass:** a click selects, a small movement starts nothing, and a double click connects. Double-clicking a -heading folds it and unfolds it again and connects to nothing. +**Pass:** a click selects, a small movement starts nothing, and a double click connects. Double-clicking the +space around the cards connects to nothing. **Failure means:** the 5-pixel threshold in `HostsScreen.axaml.cs` is not doing its job — the same failure -as 2.16 on the other screen, and here it would make the grid unusable. A heading that connects means the +as 2.16 on the other screen, and here it would make the grid unusable. Empty space that connects means the double-tap handler has lost its check that the pointer was over a card. ### 7.8 The highlight clears after a drag that goes nowhere -Drag a host over a heading and release outside the grid, or press Escape mid-drag. +Drag a host over a group card and release outside the grid, or press Escape mid-drag. **Pass:** the wash goes away. @@ -676,7 +687,7 @@ Drag a host over a heading and release outside the grid, or press Escape mid-dra With host A selected, right-click host B and choose Delete. -**Pass:** the question names **B**. Then right-click a group heading. +**Pass:** the question names **B**. Then right-click the space around the cards. **Pass:** no menu opens at all, and the host selection has not moved. diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml index 8ad5b88..80ad3eb 100644 --- a/src/DodoSSH.Client.App/App.axaml +++ b/src/DodoSSH.Client.App/App.axaml @@ -276,9 +276,12 @@ - + - - + + @@ -198,10 +207,18 @@ SelectedItem is GroupFilter and not SelectedGroup. The two are nearly the same thing here — GroupFilter assigns SelectedGroup — but only one of them may narrow the grid; see the property for why the phone's own use of SelectedGroup must not. + + ◆ THESE CARDS ARE THE DROP TARGET. A host card dragged onto one is filed under that group, and + that is the whole of what a drag does on this screen. It used to be a heading inside the host + grid, and the headings are gone — see the note on the host grid below. A group is a thing that + is drawn once, at the top, where the eye already goes to narrow the grid; a target that is also + the label of what you are aiming at needs no explaining. See the code-behind for the gesture. --> + SelectedItem="{Binding GroupFilter}" + ToolTip.Tip="Press a group to show only its hosts. Drag a host card onto one to file it there."> @@ -257,15 +274,31 @@ focus off the terminal's child window and then calls Focus() on something that refuses it, leaving the window with nothing focused and the keystrokes going nowhere. - It binds SidebarRows rather than VisibleHosts, so the group headings are still in it. Two - reasons, and neither is decoration: a grid showing every group at once has to say which cards - belong to which, and a heading is the drop target that takes a host out of one group and into - another. See VaultViewModel.RebuildSidebarRows for why a heading is a row rather than a - container, and the code-behind for what a drag does with one. + ◆ CARDS ONLY, AND NO HEADINGS. It binds VisibleHosts rather than SidebarRows, which is the + collection with the group headings folded into it. Those headings were a full-width fold-away + bar between the cards — a chevron, a name and a count — and in a wrap of cards they read as a + collapsed dropdown that had somehow been dealt the hosts: the moment a first group existed the + grid stopped being a grid. Two controls saying "group" on one screen was one too many, and the + cards above are the better of the two. SidebarRows and the fold are still the phone's, whose + list has no room for a row of group cards; see Android's HostsScreen. + + What a heading also was is the drop target, and that moved to the group cards rather than + going away with it — see the note on them above, and the code-behind for the gesture. A host is + taken back out of a group through its own editor, which is the one place "no group" can be said + plainly. + + Which group a card is in is on the card, as a chip. That is what the heading above it used to + say, and one chip per card says it without an accordion. + + SelectedItem is still SelectedSidebarRow and deliberately not SelectedHost, though every item + here is now a host. That property swallows the null the ListBox writes back whenever the + collection is rebuilt — which is every filter keystroke and every background sync — where + binding the host selection directly would let that null through, and a null host selection + closes the drawer and disarms a delete somebody is in the middle of confirming. See the + property, and RebuildVisibleHosts on where the real clearing is done. --> @@ -279,7 +312,8 @@ one per card: a ContextMenu inside a DataTemplate would have the row for its data context, and the commands here are the vault's. The code-behind selects whatever was right-clicked before the menu opens, so the selection-based commands act on the card under the pointer, - and cancels the menu outright over a group heading, which has no host to act on. + and cancels the menu outright over the space around the cards, where there is no host to act + on. --> @@ -290,35 +324,9 @@ - + - - - - - - + @@ -389,6 +397,26 @@ VerticalAlignment="Top" Margin="6,3,0,0" /> + + + + +