Public Access
Choose more than one host card on the desktop, the way the phone already can
The chosen-hosts set has been in VaultViewModel since the phone's connect card became a contextual action bar: a set of entity ids, a tick on the row, and seven things that can be done to it. Only one head could fill it. The desktop's grid answered a press with one selection — the card the drawer, CONNECT and the context menu are about — so filing eleven imported machines under a heading was eleven drags, and clearing out a vault was eleven rounds of the deletion question. So the pointer gets three ways into the same set. Ctrl-clicks a card to tick it, Shift-clicks to tick the run between the anchor and the card, and drags a band out over the space between and below the cards to tick everything it touches. Esc, CLEAR, a plain click on a card and a click on the empty space each drop it, and Ctrl+A takes every card being drawn — VisibleHosts, so with something in the find box that is the ones on screen and not the ones it is hiding, which is the version of that shortcut whose result can be checked before Delete is pressed. TWO SELECTIONS ON ONE SCREEN, AND KEEPING THEM FROM DISAGREEING IS MOST OF THE CHANGE. Ctrl and Shift are answered on the tunnel and marked handled, so the ListBox never moves its own mark onto the card: a Ctrl-click that also selected would light the card it had just unticked and open the drawer on a machine somebody is removing from a set. A plain press drops the set unless it lands on a ticked card, and that case is deferred to the release, because the press may be the start of a drag of all of it. After any ordinary click exactly one card is in play, which is what makes every command on the screen unambiguous again. The context menu is where the seven live, and it is one markup with two halves gated on IsChoosingHosts. Connect, Browse files and Edit… are drawn only for a single ticked host, as the phone's sheet collapses them and for the same reason; the other four read better for a count. A right click on a card outside the set drops the set first, so a Delete… about the card under the pointer can never be offered while six sit ticked behind the menu — the same rule OnContextRequested has always enforced for the selection, reached from the other direction. No bar of buttons: the phone raises one because it has no other way to hold seven entries, and a strip repeating a menu that already exists would be a second home for the wording that matters most. What the desktop gains instead is a count beside the HOSTS heading, CLEAR, and a sentence saying where the actions are. A drag that starts on a ticked card carries every ticked card. The payload is a list rather than a row now, and a drop of more than one goes through FileChosenHostsUnder, which makes the refusals once — an open editor, and a group belonging to one keychain — and reports a count instead of forty status lines. Moving whichever card the pointer happened to be holding and leaving the other five where they are is a gesture that quietly does a fraction of what it looks like it does, and the five left behind look filed. The three panels the set's actions raise had never been drawn in a window: the vault picker with its key question, the group picker, and the deletion question. All three sit above the grid rather than over it, which is the arrangement the GROUPS section and the phone's list already use and for the reason written there — the ticked cards are the information the question exists to give, so the grid shortens instead. A DEFECT FOUND BEHIND IT, AND IT WAS ALREADY LIVE ON THE PHONE. The deletion question names a count and the run that answers it reads the set again, and nothing kept the two the same set: the panel is deliberately above a live list, so one more tick between "Delete these 6 hosts?" and pressing DELETE deleted seven, with the seventh named in nothing the user had read. It needed a deliberate act on a phone and a second's work with a band, which is what turned it up. VaultViewModel now remembers which hosts the question was asked about and drops the question when the set stops being them — the question rather than the set, because what somebody has just chosen is what they meant. It also covers the case nobody performs: a colleague's deletion arriving mid-question and shrinking the set under it. VERIFIED. 354 tests in App.Tests and 111 in App.Layout.Tests, build clean, no new warnings. Six gesture tests drive real pointer and key input through the headless window — the modifier click and what it must not do to the selection, the run and its re-measurement from the anchor, the band and the click that drops the set, Ctrl+A under a filter, and the menu's two halves — plus a DragOver carrying two hosts. Four layout tests measure the strip and the three panels at the window's minimum; the vault panel binds a key to its host first, or it would measure the short shape and certify the tall one. Two flow tests cover the multi-drop's write and its refusal, and the deletion question dropping itself. manual-checks gains 7.6a for dragging a set, which no test can see for the reason 7.6 gives, and 7.7a for the gestures — the rectangle actually being painted and the tick and the fill being legible together are the two things the harness cannot look at.
This commit is contained in:
@@ -5312,6 +5312,100 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.IsChoosingHosts.ShouldBeFalse("the run finishes by leaving selection mode");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A question about six hosts does not survive the set becoming seven.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The question names a count and the run that answers it reads the set again, and the panel is drawn
|
||||
/// above the list rather than over it — deliberately, so the ticked rows stay in view — which leaves
|
||||
/// every one of them still tickable while it is up. One more tick between the question and the answer
|
||||
/// used to delete a machine nobody had been asked about. The desktop is where this became easy: a
|
||||
/// Ctrl-click or a band is a second's work. See <c>VaultViewModel.deletionAskedAbout</c>.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TickingAnotherHost_DropsTheDeletionQuestionAlreadyOnScreen()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddHostAsync(vault, "prod-web");
|
||||
await AddHostAsync(vault, "staging");
|
||||
|
||||
vault.ChooseHostCommand.Execute(Host(vault, "prod-db"));
|
||||
vault.ToggleHostChoiceCommand.Execute(Host(vault, "prod-web"));
|
||||
|
||||
vault.DeleteChosenHostsCommand.Execute(null);
|
||||
|
||||
vault.PendingDeletion.ShouldNotBeNull().Question.ShouldBe("Delete these 2 hosts?");
|
||||
|
||||
vault.ToggleHostChoiceCommand.Execute(Host(vault, "staging"));
|
||||
|
||||
vault.IsConfirmingChosenHostDeletion
|
||||
.ShouldBeFalse("the question was about two of them and there are three now");
|
||||
vault.IsChoosingHosts.ShouldBeTrue("the set is what was just chosen, so it stays");
|
||||
|
||||
vault.DeleteChosenHostsCommand.Execute(null);
|
||||
|
||||
vault.PendingDeletion.ShouldNotBeNull().Question.ShouldBe("Delete these 3 hosts?");
|
||||
|
||||
// And unticking back to the set it was asked about does not bring a stale question back up.
|
||||
vault.ToggleHostChoiceCommand.Execute(Host(vault, "staging"));
|
||||
|
||||
vault.IsConfirmingChosenHostDeletion.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The desktop's drag, once more than one card is ticked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Dragging one host onto a group card has always been <c>MoveHostToGroup</c>; a set dragged onto one has
|
||||
/// to file all of it, because moving whichever card the pointer happened to be holding and leaving the
|
||||
/// other five where they are is a gesture that quietly does a fraction of what it looks like it does. It
|
||||
/// is the picker's write with the picker skipped — see <c>ConfirmRegroupChosenHosts</c>, which shares it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The refusal is the one <c>RefusesTheDrop</c> makes for a single card, made once for the set: a drop is
|
||||
/// a gesture on the grid, and rewriting a host under a half-typed edit of it is a save nobody asked for
|
||||
/// and could not then cancel.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task DroppingTheChosenHostsOnAGroupCard_FilesEveryOneOfThem()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddHostAsync(vault, "prod-web");
|
||||
await AddHostAsync(vault, "staging");
|
||||
await AddGroupAsync(vault, "production");
|
||||
|
||||
vault.ChooseHostCommand.Execute(Host(vault, "prod-db"));
|
||||
vault.ToggleHostChoiceCommand.Execute(Host(vault, "prod-web"));
|
||||
|
||||
var card = vault.Groups.Single(
|
||||
row => string.Equals(row.Label, "production", StringComparison.Ordinal));
|
||||
|
||||
vault.NewHostCommand.Execute(null);
|
||||
|
||||
await vault.FileChosenHostsUnderCommand.ExecuteAsync(card);
|
||||
|
||||
Host(vault, "prod-db").Host.GroupId.ShouldBeNull("nothing is written under an open editor");
|
||||
vault.Status.ShouldNotBeEmpty("and it says which editor is in the way");
|
||||
|
||||
vault.CancelEditCommand.Execute(null);
|
||||
|
||||
await vault.FileChosenHostsUnderCommand.ExecuteAsync(card);
|
||||
|
||||
Host(vault, "prod-db").Host.GroupId.ShouldBe(card.EntityId, vault.Status);
|
||||
Host(vault, "prod-web").Host.GroupId.ShouldBe(card.EntityId);
|
||||
Host(vault, "staging").Host.GroupId.ShouldBeNull("it was never ticked");
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeFalse("the run finishes by leaving selection mode");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Duplicating keeps the group and the tags, which is the whole difference between it and a copy into
|
||||
/// another vault: the copy stays in the same keychain, so everything it points at is still there.
|
||||
|
||||
Reference in New Issue
Block a user