Merge branch 'main' into the group's move and its deletion question

Main took the group's EDIT and DELETE off the GROUPS heading while this branch
was adding a MOVE beside them, so the conflict was about the same six pixels
from both directions. Main's answer wins outright, and it is the better one for
the reason its own message gives: a button beside a heading has no card under a
pointer to mean, and had to work its subject out from the selection or from the
trail. Moving a group had that problem worst of all — the thing it takes with it
is everything on the shelf, and "which shelf" is not a question a button there
could answer plainly.

So the MOVE button is gone and the menu entry it was drawn beside is the whole
of it. That entry was already in this branch, above the separator DELETE sits
below, and it needed no change: the card menu selects whatever was right-clicked
before it runs anything, which is exactly the aiming a group move wants.

Three things went with the button. ShowsGroupActions, which main deleted because
hiding buttons was all it did, and which this branch had extended to hide them
for the move panel as well. CanMoveGroupTarget, which existed to answer whether
that button was worth drawing — CanMoveSelectedHost stays, because the phone
really does leave the host's MOVE out rather than offer a refusal, and a menu
whose entries came and went would be a menu whose items move. And the two test
assertions that read them, which were describing the button rather than the
behaviour; what they were guarding is that the two panels never share the
moment, and IsConfirmingGroupDeletion says that directly.

The move panel and the deletion question both keep their place under the
heading, which is where the buttons were and is now simply where that section
puts things. They still exclude each other, by disarming rather than by a
visibility flag: MoveGroup clears a pending deletion and DeleteGroup folds the
move panel away.

Manual checks 3.3 was rewritten by main for the menu and by this branch for the
tick, and now says both; 3.3a is new and walks a two-level shelf across a vault
boundary, which is the half of this feature no headless test can watch land.
This commit is contained in:
2026-08-04 17:11:06 +02:00
38 changed files with 3416 additions and 165 deletions
@@ -912,13 +912,12 @@ public sealed class VaultSharingTests : IAsyncLifetime
production.VaultId.ShouldNotBe(sharedVaultId, "this test is meaningless with both in one vault");
production.Group.ParentId.ShouldNotBeNull("it was nested, which is what has to stay behind");
// As the card's menu does before it runs the command; see HostsScreen.OnGroupContextRequested.
vault.SelectedGroup = production;
vault.CanMoveGroupTarget.ShouldBeTrue("there is a second vault this session can write to");
vault.MoveGroupCommand.Execute(null);
vault.IsMovingGroup.ShouldBeTrue(vault.Status);
vault.ShowsGroupActions.ShouldBeFalse("the panel takes the row of buttons rather than sharing it");
vault.MoveGroupVaultChoices.ShouldNotContain(choice => choice.VaultId == production.VaultId);
vault.SelectedMoveGroupVault =
@@ -956,7 +955,9 @@ public sealed class VaultSharingTests : IAsyncLifetime
/// <remarks>
/// The move is refused where it would have nowhere to go, by the command rather than by an empty picker
/// — the same answer <c>MoveHostCommand</c> gives one level down.
/// — the same answer <c>MoveHostCommand</c> gives one level down, and the only place the question is
/// asked. The menu entry is drawn either way, because a menu whose items came and went would be a menu
/// whose items move.
/// </remarks>
[Fact]
public async Task MovingAGroupWithNowhereToMoveIt_SaysSoRatherThanOpeningAnEmptyPicker()
@@ -973,8 +974,6 @@ public sealed class VaultSharingTests : IAsyncLifetime
vault.SelectedGroup = vault.Groups.ShouldHaveSingleItem();
vault.CanMoveGroupTarget.ShouldBeFalse("the personal vault is the only one there is");
vault.MoveGroupCommand.Execute(null);
vault.IsMovingGroup.ShouldBeFalse();