Flatten the hosts screen into one board of sections

This commit is contained in:
2026-08-07 18:12:22 +02:00
parent 369109dd4e
commit c3ef4bd8b4
4 changed files with 1133 additions and 1284 deletions
@@ -287,7 +287,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
await OnTheHostsScreenAsync((screen, window) =>
{
var card = screen.HostGrid.GetVisualDescendants()
var card = screen.GetVisualDescendants()
.OfType<ListBoxItem>()
.First(item => item.DataContext is HostRowViewModel);
@@ -332,22 +332,22 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
{
await OnTheHostsScreenAsync((screen, _) =>
{
screen.KeyboardTarget.ShouldBeSameAs(screen.HostGrid);
screen.KeyboardTarget.Focus().ShouldBeTrue("the grid has cards in it");
screen.KeyboardTarget.ShouldBeSameAs(screen.Board);
screen.KeyboardTarget.Focus().ShouldBeTrue("the board has cards on it");
});
vault.HostFilter = "nothing matches this";
vault.HasVisibleHosts.ShouldBeFalse();
vault.HasHostBoardEntries.ShouldBeFalse();
await OnTheHostsScreenAsync((screen, _) =>
{
screen.KeyboardTarget.ShouldBeSameAs(screen.HostFilter);
screen.KeyboardTarget.Focus().ShouldBeTrue("the grid is empty, so the find box takes it");
screen.KeyboardTarget.Focus().ShouldBeTrue("the board is empty, so the find box takes it");
});
}
/// <remarks>
/// The editor open with the grid still on screen beside it, which is the state a user is most likely to
/// The editor open with the board still on screen beside it, which is the state a user is most likely to
/// leave this screen in — so it is the state the keyboard answer most has to hold in.
/// </remarks>
[Fact]
@@ -357,56 +357,61 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
await OnTheHostsScreenAsync((screen, _) =>
{
screen.HostGrid.IsEffectivelyVisible.ShouldBeTrue();
screen.Board.IsEffectivelyVisible.ShouldBeTrue();
screen.KeyboardTarget.Focus().ShouldBeTrue();
});
}
/// <summary>
/// The grid is cards and nothing else, whatever the vault has been filed into.
/// v5: each section's own card list holds cards and nothing else — the heading is a sibling, not a row.
/// </summary>
/// <remarks>
/// <para>
/// It used to hold the group headings too — a full-width fold-away bar between the cards for each group
/// — and in a wrap of cards that read as a dropdown somebody had left open. They are the phone's now.
/// This asserts the grid's contents rather than only measuring them, because a heading that came back
/// would lay out perfectly cleanly: the harness asks whether a control is inside the window, and a bar
/// that spans the width is as inside it as a card is.
/// This used to hold for one flat grid: no group headings were mixed into the host rows, because the
/// group cards above it were where a group's name was drawn. The board replaces that grid with one
/// section per group — see <c>VaultViewModel.HostSections</c> — and the invariant moves down a level
/// with it: it is each section's own <c>ListBox</c> (<c>Classes="sectioncards"</c>) that must hold only
/// <see cref="HostRowViewModel"/>s, because <c>HostsScreen.axaml.cs</c> finds every card by walking every
/// list wearing that class and would misfire on a heading it found mixed in.
/// </para>
/// <para>
/// Measured with every host filed and that group open, which is the shape that used to produce the most
/// headings. Open, because the grid holds one level of the tree — a host inside a group is drawn inside
/// that group and nowhere else — so measuring at the outermost level would be measuring an empty grid.
/// See <c>VaultViewModel.Matches</c>.
/// Measured with every host filed under one group, which is the shape that puts the most rows behind one
/// heading.
/// </para>
/// </remarks>
[Fact]
public async Task TheHostsGridHoldsCardsAndNoGroupHeadings()
public async Task EachSectionsCardListHoldsCardsAndNothingElse()
{
await SeedGroupsAsync(3);
// Filed through the host editor, same as SeedGroupsAsync itself does — the drag this used to exercise
// left with the group cards in v5; see VaultViewModel.EditGroup's own remarks.
foreach (var host in vault.Hosts.ToArray())
{
await vault.MoveHostToGroupCommand.ExecuteAsync(
new HostGroupMove(host, vault.Groups[0].EntityId));
}
vault.SelectedHost = host;
vault.EditSelectedHostCommand.Execute(null);
vault.OpenGroupCommand.Execute(vault.Groups[0]);
vault.EditorSelectedGroup = vault.EditorGroupChoices
.First(choice => choice.EntityId == vault.Groups[0].EntityId);
await vault.SaveHostCommand.ExecuteAsync(null);
}
await OnTheHostsScreenAsync((screen, _) =>
{
var rows = screen.HostGrid
.GetVisualDescendants()
.OfType<ListBoxItem>()
var rows = screen.GetVisualDescendants()
.OfType<ListBox>()
.Where(list => list.Classes.Contains("sectioncards"))
.SelectMany(list => list.GetVisualDescendants().OfType<ListBoxItem>())
.Select(item => item.DataContext)
.ToList();
rows.ShouldNotBeEmpty("the seed has to put hosts in the grid");
rows.ShouldNotBeEmpty("the seed has to put hosts in one of the sections");
rows.ShouldAllBe(row => row is HostRowViewModel);
});
await MeasureHostsAsync(
faults => faults.ShouldBeEmpty("with every host filed and the group holding them open"));
faults => faults.ShouldBeEmpty("with every host filed under one group and four sections drawn"));
}
/// <remarks>
@@ -447,15 +452,14 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
await OnTheHostsScreenAsync((screen, window) =>
{
var cards = screen.HostGrid
.GetVisualDescendants()
var cards = screen.GetVisualDescendants()
.OfType<ListBoxItem>()
.Where(item => item.DataContext is HostRowViewModel)
.Select(item => item.TranslatePoint(default, window)
?? throw new InvalidOperationException("a card is not in this window's tree"))
.ToList();
cards.Count.ShouldBeGreaterThan(1, "the seed has to put more than one host in the grid");
cards.Count.ShouldBeGreaterThan(1, "the seed has to put more than one host on the board");
cards.GroupBy(point => Math.Round(point.Y))
.Max(row => row.Count())
@@ -589,8 +593,9 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
{
await SeedGroupsAsync(3);
vault.SelectedGroup = vault.Groups[0];
vault.DeleteGroupCommand.Execute(null);
// Passed directly, the way DeleteGroupFromHeading resolves and hands in a row now that there is no
// group-card selection to fall back on.
vault.DeleteGroupCommand.Execute(vault.Groups[0]);
vault.IsConfirmingGroupDeletion.ShouldBeTrue("the question has to be up for this to measure it");
vault.PendingDeletion.ShouldNotBeNull().HasChoice
@@ -617,8 +622,6 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
{
await SeedGroupsAsync(3);
vault.SelectedGroup = vault.Groups[0];
vault.MoveGroupVaultChoices.Add(
new VaultChoiceViewModel(Guid.CreateVersion7(), "Platform Engineering secrets", false));