Public Access
Sweep out the group-card navigation nothing reaches any more
This commit is contained in:
@@ -713,7 +713,7 @@
|
||||
chosen card that also drew an accent outline would be indistinguishable from the selected one, and the
|
||||
question "which of these is the drawer showing" would have no answer. Declared after that rule anyway,
|
||||
because it sets the same Background and Avalonia settles two matching rules by declaration order —
|
||||
the trap this file records for Button.tab, Border.rowmark and the drop target below.
|
||||
the trap this file records for Button.tab and Border.rowmark.
|
||||
-->
|
||||
<Style Selector="Border.tile.chosen">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
@@ -990,34 +990,6 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The group card a host is about to be dropped on. A wash rather than the accent strip a selection
|
||||
carries, because it is not a selection: it says "let go here", it lasts as long as the pointer is over
|
||||
the card, and it has to be legible on top of whatever that card already looks like — including the
|
||||
selected one, which is the group the grid is currently narrowed to and so often the one being dragged
|
||||
onto.
|
||||
|
||||
Painted on the tile rather than on the item's own presenter, which is where this used to go when the
|
||||
target was a full-width heading. A wash behind a 10-pixel-rounded tile is a square of colour with the
|
||||
card floating on it — the same four grey corners the tiles list clears the theme's own hover and
|
||||
selection brushes to avoid. Declared after that rule and after the selected-tile rule, because
|
||||
Avalonia has no specificity and settles two matching rules by declaration order.
|
||||
|
||||
◆ THE DOUBLED BORDER IS THE MARK, not the fill, and that is what the move to the tile forced. The fill
|
||||
a selected tile already carries is the strongest one this palette has, so a target that only filled
|
||||
would be invisible on the card the grid is narrowed to — which is the card a host most often has to be
|
||||
dragged off, into another group. Two pixels of accent is legible on both.
|
||||
|
||||
Set from the code-behind rather than by a binding, for the reason the whole gesture is code: the rows
|
||||
are rebuilt from scratch on every filter keystroke and every sync pass, so a flag on the view model
|
||||
would be state the list throws away halfway through the drag. See HostsScreen.axaml.cs.
|
||||
-->
|
||||
<Style Selector="ListBoxItem.droptarget Border.tile">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
A context menu, in this window's palette rather than the theme's. The Fluent default is a lighter grey
|
||||
than anything else here, which on a near-black chrome reads as a dialog from another application.
|
||||
|
||||
@@ -125,17 +125,6 @@ internal sealed class HostGroupRowViewModel(VaultGroupItem group, int hostCount)
|
||||
internal string Description => hostCount == 1 ? "1 host" : $"{hostCount} hosts";
|
||||
}
|
||||
|
||||
/// <summary>One step of the path into the groups, as a button in a breadcrumb trail.</summary>
|
||||
/// <param name="Name">What the step is called.</param>
|
||||
/// <param name="Group">The group it opens, or null for the step that shows every host again.</param>
|
||||
/// <remarks>
|
||||
/// The same shape the transfers screen's trail uses — see <c>CrumbViewModel</c> — and drawn the same way,
|
||||
/// because it answers the same question: a directory pane and a grid of groups both have to say where the
|
||||
/// thing on screen came from and offer a way back out. The row rather than its id, because
|
||||
/// <see cref="VaultViewModel.GroupFilter"/> holds a row, and an id would only be looked up again.
|
||||
/// </remarks>
|
||||
internal sealed record GroupCrumbViewModel(string Name, HostGroupRowViewModel? Group);
|
||||
|
||||
/// <summary>An entry in the host editor's group picker.</summary>
|
||||
/// <param name="EntityId">The group, or null for "no group".</param>
|
||||
/// <param name="Label">What to show.</param>
|
||||
@@ -194,16 +183,6 @@ internal sealed record GroupFilterChoice(Guid GroupId, string Label, bool IsChec
|
||||
/// <remarks>A host passes the filter by wearing <em>any</em> checked tag, not all of them.</remarks>
|
||||
internal sealed record TagFilterChoice(Guid TagId, string Label, bool IsChecked);
|
||||
|
||||
/// <summary>One host, and the group it is being filed under.</summary>
|
||||
/// <param name="Host">The host to move.</param>
|
||||
/// <param name="GroupId">The group it should end up in, or null for none.</param>
|
||||
/// <remarks>
|
||||
/// A pair rather than two command parameters, because a command takes one — and a pair rather than the two
|
||||
/// ids, because the host row is what the caller is holding: it is the thing that was dragged, and it already
|
||||
/// carries the vault the edit has to return to.
|
||||
/// </remarks>
|
||||
internal sealed record HostGroupMove(HostRowViewModel Host, Guid? GroupId);
|
||||
|
||||
/// <summary>One snippet, as a row in the list.</summary>
|
||||
/// <remarks>
|
||||
/// Carries the decrypted <see cref="SnippetSecret"/> so opening the editor needs no second decryption, in
|
||||
@@ -1764,50 +1743,12 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
/// <summary>The groups in this vault, with the number of hosts filed under each.</summary>
|
||||
/// <remarks>
|
||||
/// Every one of them, flat. This is what a group is looked up in and what the phone's headings are built
|
||||
/// from; <see cref="VisibleGroups"/> is the desktop's one level of it.
|
||||
/// Every one of them, flat. This is what a group is looked up in and what both heads' headings are built
|
||||
/// from — v5 retired the desktop's own grid of group cards in favour of the flat board the phone always
|
||||
/// drew; see <see cref="HostSections"/>.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<HostGroupRowViewModel> Groups { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The group cards the desktop is drawing: what is inside the group that is open, or the outermost
|
||||
/// groups when none is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="Groups"/> is to this what <see cref="Hosts"/> is to <see cref="VisibleHosts"/>: the whole
|
||||
/// collection beside the part of it on screen. The grid used to draw every group at once, which was the
|
||||
/// only honest thing to do while pressing a card meant nothing but "narrow the list" — a card was a
|
||||
/// filter, and every filter has to be reachable. Opening one is navigation, so the cards became the
|
||||
/// contents of wherever the trail says you are.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A group whose parent this vault has not got is drawn at the outermost level rather than nowhere, and
|
||||
/// so is one caught in a parent cycle. Both are states two offline edits can produce and neither can be
|
||||
/// repaired from a screen that will not draw the group — see <see cref="HostGroupSecret.ParentId"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal ObservableCollection<HostGroupRowViewModel> VisibleGroups { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The path down to the group that is open: every host, then each group above it, then it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Always at least one crumb, and the first one is the way back to every host — which is what it is for.
|
||||
/// A grid whose cards are one level of a tree needs somewhere to say which level, and the same control
|
||||
/// is the way out of it; without that the only way back would be a button that says so, which is what
|
||||
/// SHOW ALL was and what this replaces.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<GroupCrumbViewModel> GroupTrail { get; } = [];
|
||||
|
||||
/// <summary>Whether there are any group cards to draw at this level.</summary>
|
||||
/// <remarks>
|
||||
/// Separate from <see cref="HasGroups"/>, which is about the vault. A group with nothing inside it is an
|
||||
/// ordinary thing to open, and the trail and the group's own buttons have to stay on screen when it is —
|
||||
/// so it is the card grid alone that folds away, not the panel around it.
|
||||
/// </remarks>
|
||||
internal bool HasVisibleGroups => VisibleGroups.Count > 0;
|
||||
|
||||
/// <summary>The saved commands in this vault, unpushed local state included.</summary>
|
||||
/// <remarks>
|
||||
/// Held here rather than on the screen that shows them, for the reason every other list is: this is where
|
||||
@@ -1893,8 +1834,9 @@ internal sealed partial class VaultViewModel(
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The application's selection, which everything that acts on a host reads — connecting, editing,
|
||||
/// deleting. <b>It shares one selection with <see cref="SelectedGroup"/>:</b> selecting a host takes the
|
||||
/// mark off a group card and the other way about. See <see cref="OnSelectedHostChanged"/>.
|
||||
/// deleting. Through v4 this was one of two things a click could light, sharing itself with a group
|
||||
/// card's own selection; v5's flat board draws no group cards to share it with, so a host is the only
|
||||
/// thing this screen ever marks. See <see cref="OnSelectedHostChanged"/>.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private HostRowViewModel? selectedHost;
|
||||
@@ -1920,66 +1862,25 @@ internal sealed partial class VaultViewModel(
|
||||
private ISidebarRow? selectedSidebarRow;
|
||||
|
||||
/// <summary>
|
||||
/// The group card that is selected, or null when none is.
|
||||
/// The group <see cref="GroupTarget"/> falls back to when a command is not handed one directly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// One click, and nothing more than a highlight: it is what the group's own EDIT and DELETE act on. What
|
||||
/// it deliberately no longer does is narrow the grid — see <see cref="OpenGroup"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It shares one selection with <see cref="SelectedHost"/>.</b> Setting either clears the other, so
|
||||
/// exactly one card on the screen is ever lit. See <see cref="OnSelectedHostChanged"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private HostGroupRowViewModel? selectedGroup;
|
||||
|
||||
/// <summary>
|
||||
/// The group that is open: the one whose contents the screen is showing, or null for every host.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Set by <see cref="OpenGroup"/> and by nothing else. It decides three things at once — which hosts the
|
||||
/// grid holds, which groups the cards hold, and what the trail says — which is what makes it "where you
|
||||
/// are" rather than a filter that happens to be on.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Separate from <see cref="SelectedGroup"/>, and no longer sets it.</b> The two answer different
|
||||
/// questions — "what is on screen" and "which card is chosen" — and while one click meant
|
||||
/// both there was no way to name a group without also narrowing the grid to it. Two gestures, two
|
||||
/// properties; <see cref="GroupTarget"/> is where the two meet.
|
||||
/// ◆ <b>v3 named this "the group that is open" and set it from a double-click on a card, the way a
|
||||
/// directory pane's own breadcrumb worked.</b> v5 retired that grid, its cards and its trail in favour of
|
||||
/// the flat board both heads now draw — see <see cref="HostSections"/> — and nothing on either head
|
||||
/// opens a group any more, so nothing sets this to a value other than null. It stays rather than being
|
||||
/// removed with the card grid because <see cref="RebuildGroups"/> still re-resolves it against every
|
||||
/// reload, and <see cref="GroupTarget"/>, <see cref="EditGroup"/>, <see cref="DeleteGroup"/> and
|
||||
/// <see cref="MoveGroup"/> still read it as their fallback when handed no group directly — a caller none
|
||||
/// of them currently has, since <see cref="EditGroupFromHeading"/> and its two siblings always resolve
|
||||
/// and pass a row of their own. Removing the fallback itself would mean re-proving the same thing about
|
||||
/// three more commands than removing the trail did about one property.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private HostGroupRowViewModel? groupFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a group, or every host when handed null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A double-click on a card, or a press on a crumb of the trail. Deliberately not a single click, which
|
||||
/// is what it was: a card is the only place a group can be selected, and a gesture that both selected a
|
||||
/// group and threw the rest of the grid away left no way to rename one without first losing sight of
|
||||
/// everything else. Double-clicking to go inside something is what the transfers screen's directories do
|
||||
/// and what the host cards beneath these do to open a shell, so the grid now has one vocabulary rather
|
||||
/// than one per list.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The selection is dropped first, and it has to be: the cards are about to be redrawn one level along,
|
||||
/// and a selection pointing at a card that is no longer on screen would aim EDIT and DELETE at something
|
||||
/// nobody can see. Null is a real argument here rather than a missing one — it is the trail's first
|
||||
/// crumb, and it is the way back out.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void OpenGroup(HostGroupRowViewModel? group)
|
||||
{
|
||||
SelectedGroup = null;
|
||||
GroupFilter = group;
|
||||
}
|
||||
|
||||
/// <summary>What the group name box holds, for both creating and renaming.</summary>
|
||||
[ObservableProperty]
|
||||
private string groupEditorLabel = string.Empty;
|
||||
@@ -2986,10 +2887,10 @@ internal sealed partial class VaultViewModel(
|
||||
/// Whether the panel asking which group the chosen hosts should be filed under is up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The one action in the bar that has always existed and had no phone control: filing is
|
||||
/// <see cref="MoveHostToGroupAsync"/>, which is what dragging a card onto a group does on the desktop, and
|
||||
/// there is no dragging here. It is the reason the set is worth having at all — thirty imported machines
|
||||
/// under one heading is one gesture rather than thirty rounds of open, pick, save.
|
||||
/// The one action in the bar that has always had no phone control and, since v5 retired the desktop's own
|
||||
/// drag, no desktop gesture either: filing a set is now only ever this panel, on both heads, through
|
||||
/// <see cref="ConfirmRegroupChosenHostsAsync"/>. It is the reason the set is worth having at all — thirty
|
||||
/// imported machines under one heading is one gesture rather than thirty rounds of open, pick, save.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(AChosenHostPanelIsOpen))]
|
||||
@@ -3678,23 +3579,15 @@ internal sealed partial class VaultViewModel(
|
||||
internal bool AChosenHostPanelIsOpen =>
|
||||
IsSendingChosenHostsToAVault || IsRegroupingChosenHosts || IsConfirmingChosenHostDeletion;
|
||||
|
||||
/// <summary>
|
||||
/// What a group command with no argument acts on: the card that is selected, or the group that is open.
|
||||
/// </summary>
|
||||
/// <summary>What a group command with no argument acts on.</summary>
|
||||
/// <remarks>
|
||||
/// Two answers, because a card is no longer where the user is. Selecting one aims at it, which is what a
|
||||
/// click has always done; with nothing selected the answer is the group whose contents are on screen —
|
||||
/// the one the trail ends with. That fallback is what makes + NEW HOST open on the group somebody is
|
||||
/// standing in rather than on none, and it is what a file manager does: act on the selection, and on the
|
||||
/// current folder when there is none.
|
||||
/// <para>
|
||||
/// The desktop's Edit and Delete reach this through the card menu, which selects whatever was
|
||||
/// right-clicked first, so the fallback is not what they read — see <c>HostsScreen.OnGroupContextRequested</c>.
|
||||
/// They used to be a pair of buttons beside the GROUPS heading, which had no card under a pointer to
|
||||
/// mean and so leaned on it.
|
||||
/// </para>
|
||||
/// Equal to <see cref="GroupFilter"/> today, and <see cref="GroupFilter"/>'s own remarks say why nothing
|
||||
/// currently makes that non-null: the card-and-trail navigation this was a fallback for is gone from both
|
||||
/// heads. <see cref="EditGroupFromHeading"/>, <see cref="MoveGroupFromHeading"/> and
|
||||
/// <see cref="DeleteGroupFromHeading"/> resolve a row from the heading they were pressed on and pass it
|
||||
/// directly — every current caller hands a group in, so this is read but never leaned on.
|
||||
/// </remarks>
|
||||
internal HostGroupRowViewModel? GroupTarget => SelectedGroup ?? GroupFilter;
|
||||
internal HostGroupRowViewModel? GroupTarget => GroupFilter;
|
||||
|
||||
/// <summary>Whether this vault has any groups, which is what makes the sidebar draw headings.</summary>
|
||||
internal bool HasGroups => Groups.Count > 0;
|
||||
@@ -4271,21 +4164,14 @@ internal sealed partial class VaultViewModel(
|
||||
/// <summary>Which host a freshly filled <see cref="Hosts"/> leaves selected.</summary>
|
||||
/// <param name="selectedId">Whatever was selected before the list was refilled.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The selection survives a reload, because losing it on every sync would move the terminal's target out
|
||||
/// from under the user. The first host is the fallback rather than nothing, so that a fresh unlock has
|
||||
/// something under CONNECT.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// That fallback is skipped while a group card holds the selection, and it has to be: the two grids
|
||||
/// share one mark — see <see cref="OnSelectedHostChanged"/> — so a sync that invented a host would
|
||||
/// quietly unselect a group nobody had touched, once a minute. Read before <see cref="RebuildGroups"/>
|
||||
/// runs, which is where <see cref="SelectedGroup"/> is re-resolved against the rows this pass makes.
|
||||
/// </para>
|
||||
/// something under CONNECT. Through v4 that fallback was skipped while a group card held the selection
|
||||
/// instead of a host; v5's board draws no group cards to hold it, so there is no longer a second case to
|
||||
/// skip the fallback for.
|
||||
/// </remarks>
|
||||
private HostRowViewModel? SelectionAfterReload(Guid? selectedId) =>
|
||||
Hosts.FirstOrDefault(row => row.EntityId == selectedId)
|
||||
?? (SelectedGroup is null ? Hosts.FirstOrDefault() : null);
|
||||
Hosts.FirstOrDefault(row => row.EntityId == selectedId) ?? Hosts.FirstOrDefault();
|
||||
|
||||
/// <returns>How many buckets would not decrypt.</returns>
|
||||
/// <remarks>
|
||||
@@ -4725,7 +4611,6 @@ internal sealed partial class VaultViewModel(
|
||||
/// </remarks>
|
||||
private void RebuildGroups()
|
||||
{
|
||||
var selectedId = SelectedGroup?.EntityId;
|
||||
var filteredId = GroupFilter?.EntityId;
|
||||
|
||||
// The same test the host rows are badged by, and it counts the vaults this session can read rather
|
||||
@@ -4752,70 +4637,20 @@ internal sealed partial class VaultViewModel(
|
||||
});
|
||||
}
|
||||
|
||||
// Re-resolved by id rather than kept: every row object here is replaced on every reload, so an open
|
||||
// group holding the old one would go on showing a group that is no longer in the list — and the
|
||||
// crumb the user could press to leave it would be a different object that never matched. A group
|
||||
// deleted by a sync closes itself, which is the honest answer: the screen comes back to every host
|
||||
// rather than to none.
|
||||
// Re-resolved by id rather than kept: every row object here is replaced on every reload, so
|
||||
// GroupFilter holding the old one would go on naming a group that is no longer in the list. A group
|
||||
// deleted by a sync clears it, which is the honest answer.
|
||||
//
|
||||
// This assignment is a new row object whenever a group is open at all, so it always fires
|
||||
// This assignment is a new row object whenever GroupFilter names one at all, so it always fires
|
||||
// OnGroupFilterChanged and therefore an extra RebuildVisibleHosts before the caller's own. That is
|
||||
// wasted work rather than a bug — Hosts is already filled by the time this runs, so both passes see
|
||||
// the same thing — and it is left rather than dodged by writing the backing field, because writing
|
||||
// the field would skip the cards, the trail and GroupTarget with it.
|
||||
// the field would skip GroupTarget's own change notification with it.
|
||||
GroupFilter = Groups.FirstOrDefault(row => row.EntityId == filteredId);
|
||||
|
||||
// Unconditionally, because the assignment above is a no-op — and fires nothing — whenever no group
|
||||
// was open, and the cards still have to be rebuilt out of the row objects this pass just made.
|
||||
RebuildGroupLevel();
|
||||
|
||||
// Out of the cards on screen rather than out of every group, and after the level has been rebuilt:
|
||||
// this is the card ListBox's own selection, and a row it is not showing is one the control would
|
||||
// null straight back out again.
|
||||
//
|
||||
// Never defaulted to the first row, as the key and credential lists are not: this selection is what
|
||||
// EDIT and DELETE aim at, and a background sync that picked a group would point them at one nobody
|
||||
// chose.
|
||||
SelectedGroup = VisibleGroups.FirstOrDefault(row => row.EntityId == selectedId);
|
||||
|
||||
OnPropertyChanged(nameof(HasGroups));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refills the group cards and the trail from whichever group is open.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both together because they are two halves of one answer: the cards are what is inside the open group
|
||||
/// and the trail is how it was reached, and a pass that rebuilt one without the other would draw a level
|
||||
/// under a path that does not lead to it.
|
||||
/// </remarks>
|
||||
private void RebuildGroupLevel()
|
||||
{
|
||||
var parents = EffectiveParents();
|
||||
var open = GroupFilter?.EntityId;
|
||||
|
||||
VisibleGroups.Clear();
|
||||
|
||||
foreach (var row in Groups.Where(row => parents.GetValueOrDefault(row.EntityId) == open))
|
||||
{
|
||||
VisibleGroups.Add(row);
|
||||
}
|
||||
|
||||
GroupTrail.Clear();
|
||||
|
||||
// Always first, always there, and it is the way out — see GroupTrail. The name is what the grid
|
||||
// below shows when nothing is open, rather than the vault's, because that is the choice being
|
||||
// offered: this crumb widens the screen back to every machine in it.
|
||||
GroupTrail.Add(new GroupCrumbViewModel("ALL HOSTS", null));
|
||||
|
||||
foreach (var row in Ancestry(open, parents))
|
||||
{
|
||||
GroupTrail.Add(new GroupCrumbViewModel(row.Label, row));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasVisibleGroups));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which group each one sits under, with anything a walk upwards cannot get out of promoted to the
|
||||
/// outermost level.
|
||||
@@ -4871,29 +4706,6 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The groups from the outermost down to the one that is open, or nothing when none is.</summary>
|
||||
/// <remarks>
|
||||
/// Walked against <see cref="EffectiveParents"/> rather than against the stated parents, so the trail
|
||||
/// cannot lead through a group the cards will not draw — and so that it terminates, which is what the
|
||||
/// promotion above buys: a cycle has no parent left to follow.
|
||||
/// </remarks>
|
||||
private List<HostGroupRowViewModel> Ancestry(Guid? open, Dictionary<Guid, Guid?> parents)
|
||||
{
|
||||
var trail = new List<HostGroupRowViewModel>();
|
||||
var current = open;
|
||||
|
||||
while (current is { } id
|
||||
&& Groups.FirstOrDefault(row => row.EntityId == id) is { } row)
|
||||
{
|
||||
trail.Add(row);
|
||||
current = parents.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
trail.Reverse();
|
||||
|
||||
return trail;
|
||||
}
|
||||
|
||||
/// <summary>Refills the sidebar's list from <see cref="Hosts"/> and the filter.</summary>
|
||||
/// <remarks>
|
||||
/// The selection is captured and restored around the rebuild, and that is not tidiness — it is what
|
||||
@@ -5351,126 +5163,6 @@ internal sealed partial class VaultViewModel(
|
||||
OnPropertyChanged(nameof(CollapseAllLabel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Files one host under one group, or under none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// What dragging a host card onto a group card does, and the only thing in this application that changes
|
||||
/// a host without opening the editor. That is the justification for it existing at all: filing thirty
|
||||
/// imported machines meant thirty rounds of open, pick, save, and the field being changed is the one
|
||||
/// field of a host that is about arrangement rather than about the machine.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It writes the saved host rather than the editor's contents, and refuses while the editor is open. A
|
||||
/// drop is a gesture on the list, not on the form: rewriting the item under a half-typed edit of the same
|
||||
/// host would be a save the user never asked for, and one they would then be unable to cancel.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A group id that is in no readable vault is not refused — it is treated as no group at all, which is
|
||||
/// what the list already does with a dangling reference. See <see cref="RebuildSidebarRows"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>A group in a different vault to the host is refused, and said so.</b> The cards are every readable
|
||||
/// vault's since a group became a thing that can be shared, so this gesture can now be aimed across a
|
||||
/// boundary that a save cannot cross: the host would keep an id only the other vault's holders can
|
||||
/// resolve, and everybody in this one would see it filed under nothing. Refusing beats the two
|
||||
/// alternatives — filing it anyway is the quiet wrong, and treating it as "no group" would unfile a host
|
||||
/// somebody was plainly trying to file.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// No cancellation token, for the reason <see cref="ConnectAsync"/> has none: a command generated over a
|
||||
/// method that takes one cancels the previous execution's token on every invocation, and two drops in
|
||||
/// quick succession are two writes rather than one superseding the other. This is one row's one field
|
||||
/// and it is over in a moment.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="request">The host to move, and where to.</param>
|
||||
[RelayCommand]
|
||||
private async Task MoveHostToGroupAsync(HostGroupMove? request)
|
||||
{
|
||||
if (request is not { Host: { } row })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var card = request.GroupId is { } wanted
|
||||
? Groups.FirstOrDefault(group => group.EntityId == wanted)
|
||||
: null;
|
||||
|
||||
if (RefusesTheDrop(row, card))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Guid? target = card?.EntityId;
|
||||
|
||||
if (row.Host.GroupId == target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var moved = row.Host with { GroupId = target };
|
||||
var name = card?.Label ?? "no group";
|
||||
|
||||
await RunAsync(
|
||||
$"Filing {row.Label} under {name}…",
|
||||
async () =>
|
||||
{
|
||||
await session.Hosts
|
||||
.UpdateAsync(row.VaultId, row.EntityId, moved, CancellationToken.None)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
await ReloadAsync(CancellationToken.None).ConfigureAwait(true);
|
||||
|
||||
// Re-found rather than kept: the reload replaces every row, so the object that was dragged is
|
||||
// no longer the one in the list, and leaving the selection pointing at it would light nothing.
|
||||
SelectedHost = Hosts.FirstOrDefault(candidate => candidate.EntityId == row.EntityId);
|
||||
|
||||
Status = target is null
|
||||
? $"'{row.Label}' is no longer in a group."
|
||||
: $"Filed '{row.Label}' under '{name}'.";
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
// Pushed straight away, as a save from the editor is: this is a save from the editor, minus the
|
||||
// editor.
|
||||
await AutoSyncAsync(CancellationToken.None).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>Whether a drop has to be turned down, saying why on the status line when it does.</summary>
|
||||
/// <param name="row">The host that was dragged.</param>
|
||||
/// <param name="card">The group card it was dropped on, or null for the drop that unfiles a host.</param>
|
||||
/// <remarks>
|
||||
/// Three refusals rather than one, and separated from the write so that the reason reaches the status
|
||||
/// line before anything is encrypted. Every one of them is a thing the layer below would either refuse
|
||||
/// or, worse, accept: a newer client's item re-encoded loses fields, a write under an open editor is a
|
||||
/// save nobody asked for, and a group in another vault is an id half the readers cannot resolve.
|
||||
/// </remarks>
|
||||
private bool RefusesTheDrop(HostRowViewModel row, HostGroupRowViewModel? card)
|
||||
{
|
||||
if (row.IsReadOnly)
|
||||
{
|
||||
Status = "This host was written by a newer version of DodoSSH. Update before filing it.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsEditing)
|
||||
{
|
||||
Status = "Finish or cancel the host you are editing first.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (card is not null && card.VaultId != row.VaultId)
|
||||
{
|
||||
Status =
|
||||
$"'{card.Label}' is in {card.VaultName} and '{row.Label}' is in {row.VaultName}. "
|
||||
+ "A host can only be filed under a group in its own vault.";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Whether one host belongs on the grid at the level it is currently showing.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -6668,8 +6360,8 @@ internal sealed partial class VaultViewModel(
|
||||
/// <para>
|
||||
/// <b>One keychain's groups, so a selection spanning two is refused rather than half-filed.</b> A group is
|
||||
/// an item of one vault: filing a host from another under it would leave everybody but the person who did
|
||||
/// it seeing a machine filed under nothing. That is the same refusal <see cref="RefusesTheDrop"/> makes
|
||||
/// for a card dragged across the boundary on the desktop, made once here instead of per host.
|
||||
/// it seeing a machine filed under nothing — checked once here, over the whole set, rather than inside
|
||||
/// <see cref="ConfirmRegroupChosenHostsAsync"/> once per host.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "No group" is an entry rather than an omission, because unfiling a run of machines is exactly as
|
||||
@@ -6718,9 +6410,11 @@ internal sealed partial class VaultViewModel(
|
||||
/// Files every chosen host under the group that was picked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same write <see cref="MoveHostToGroupAsync"/> makes for one dragged card, and deliberately the same
|
||||
/// one: the group is the single field of a host that is about arrangement rather than about the machine,
|
||||
/// which is why it is the only one anything changes without opening the editor.
|
||||
/// The only way a set is filed since v5: dragging a ticked card onto a group card went with the group
|
||||
/// grid it was dragged onto, and nothing replaced the gesture — the picker this confirms was always the
|
||||
/// phone's only route and is now both heads'. The group is the single field of a host that is about
|
||||
/// arrangement rather than about the machine, which is why it is the only one anything changes without
|
||||
/// opening the editor.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private async Task ConfirmRegroupChosenHostsAsync(CancellationToken cancellationToken)
|
||||
@@ -6741,58 +6435,16 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Files the ticked hosts under the group card a drag of them was let go of.
|
||||
/// </summary>
|
||||
/// <param name="card">The group card the drop landed on.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>The desktop's drag, once more than one card is ticked.</b> Dragging one host onto a group is
|
||||
/// <see cref="MoveHostToGroupAsync"/> and always has been; a selection dragged onto one has to file all of
|
||||
/// it, because the alternative — 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The two refusals are the ones <see cref="RefusesTheDrop"/> makes for a single card, made once for the
|
||||
/// set: a write under an open editor is a save nobody asked for, and a group belongs to one vault, so a
|
||||
/// selection spanning two cannot be filed under it — the hosts from the other keychain would carry an id
|
||||
/// only its holders can resolve. A read-only host inside the set is skipped rather than refusing the
|
||||
/// whole drop; that is counted and said, in the run below.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// No cancellation token, for the reason <see cref="MoveHostToGroupAsync"/> has none: two drops in quick
|
||||
/// succession are two writes rather than one superseding the other.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private Task FileChosenHostsUnderAsync(HostGroupRowViewModel? card)
|
||||
{
|
||||
if (card is null || !IsChoosingHosts || AHostEditorIsInTheWay())
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (ChosenHosts.Any(row => row.VaultId != card.VaultId))
|
||||
{
|
||||
Status = $"'{card.Label}' is in another keychain to some of these hosts, and a group belongs to "
|
||||
+ "one. Move them to that keychain first, or file the ones already in it.";
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return FileTheChosenHostsUnderAsync(card.EntityId, card.Label, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The write behind both ways a set of ticked hosts is filed: the group picker, and a drag of them onto a
|
||||
/// group card.
|
||||
/// The write behind filing a set of ticked hosts under a group, from the group picker.
|
||||
/// </summary>
|
||||
/// <param name="groupId">The group they end up in, or null for none.</param>
|
||||
/// <param name="name">What to call it on the status line.</param>
|
||||
/// <param name="cancellationToken">The caller's lifetime.</param>
|
||||
/// <remarks>
|
||||
/// Shared rather than written twice, because what "filing a set" means — the hosts a newer version wrote
|
||||
/// are skipped, the ones already there are counted as done, and the selection is dropped once the reload
|
||||
/// has replaced every row — is the part that would drift between two copies of it.
|
||||
/// <see cref="ConfirmRegroupChosenHostsAsync"/>'s one caller today; kept as a method of its own rather
|
||||
/// than folded back into it because what "filing a set" means — the hosts a newer version wrote are
|
||||
/// skipped, the ones already there are counted as done, and the selection is dropped once the reload has
|
||||
/// replaced every row — reads better named than inlined, regardless of how many callers it has.
|
||||
/// </remarks>
|
||||
private async Task FileTheChosenHostsUnderAsync(
|
||||
Guid? groupId, string name, CancellationToken cancellationToken)
|
||||
@@ -7264,14 +6916,12 @@ internal sealed partial class VaultViewModel(
|
||||
// Before the group picker, because a group belongs to one vault and the picker is that vault's.
|
||||
BuildEditorVaultChoices(editingHostVaultId);
|
||||
|
||||
// A new host opens in the group the screen is already about — the card that is selected, or failing
|
||||
// that the group whose contents are showing. Adding three machines to the group somebody has just
|
||||
// made is the ordinary case, and since the grid holds one level at a time the alternative is worse
|
||||
// than a default nobody chose: a host created inside a group and filed under none would vanish from
|
||||
// the screen it was created on. Still filtered by the vault being written to, which is that group's
|
||||
// own wherever this session can write there: what the filter is left holding is the case where it
|
||||
// cannot. Before the authentication picker, because whether there is a group to inherit from decides
|
||||
// whether that one offers to.
|
||||
// A new host opens in the group GroupTarget names, on the terms that property's own remarks give —
|
||||
// nothing sets it to a value today, so this is the fallback path rather than the ordinary one it was
|
||||
// through v4. Still filtered by the vault being written to, which is that group's own wherever this
|
||||
// session can write there: what the filter is left holding is the case where it cannot. Before the
|
||||
// authentication picker, because whether there is a group to inherit from decides whether that one
|
||||
// offers to.
|
||||
BuildGroupChoices(GroupInEditingVault(inherited?.EntityId));
|
||||
|
||||
BuildAuthenticationChoices(
|
||||
@@ -7514,16 +7164,14 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
/// <summary>Loads the group being acted on into the box, so saving renames it.</summary>
|
||||
/// <param name="group">
|
||||
/// The group to edit, or null for whatever the screen is aimed at — the selected card, or the open group
|
||||
/// when no card is selected. See <see cref="GroupTarget"/>. The desktop's card menu passes nothing and
|
||||
/// means the card that was right-clicked, which opening the menu has already selected; the phone has no
|
||||
/// card to select and passes the group its heading names.
|
||||
/// The group to edit, or null to fall back to <see cref="GroupTarget"/> — see that property's own
|
||||
/// remarks for why nothing today actually relies on the fallback.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Taking it as an argument is what keeps the phone from having to select a group in order to edit one.
|
||||
/// A selection is shared with the host grid now — see <see cref="OnSelectedHostChanged"/> — so a command
|
||||
/// reachable only through <see cref="SelectedGroup"/> would deselect the machine somebody was about to
|
||||
/// connect to, on a screen that draws no group cards at all.
|
||||
/// Taking it as an argument rather than reading a selection is what lets both heads reach this the same
|
||||
/// way: <see cref="EditGroupFromHeading"/> resolves a row from the heading that was pressed and hands it
|
||||
/// straight in, on the desktop and the phone alike, so neither has to put a group on a selection a host
|
||||
/// card might be holding — see <see cref="SelectedHost"/>.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void EditGroup(HostGroupRowViewModel? group)
|
||||
@@ -7700,31 +7348,29 @@ internal sealed partial class VaultViewModel(
|
||||
/// <summary>Starts a new group, inside whichever one the screen is showing.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The desktop never needed this command: its group editor is a bar that is always on screen, so
|
||||
/// "adding" is what happens when nothing has been loaded into it. A phone has to be told, because its
|
||||
/// editor is a card that has to be raised — and raising it from a stale state would offer the last
|
||||
/// group's default key to the new one without anybody choosing it, which is what
|
||||
/// <see cref="ClearGroupEditor"/> prevents.
|
||||
/// ◆ <b>Both heads need this command now, and both reach it the same way: a menu entry rather than a
|
||||
/// button that stands on its own.</b> That has been true since v4's drawer replaced the desktop's own bar
|
||||
/// with an editor asked for on demand; v5 changes only where the entry lives — "New group…" at the foot
|
||||
/// of the Group ▾ filter flyout, since the flyout is what the toolbar now has where GROUPS used to be a
|
||||
/// heading of its own. The phone reaches the same command from its add sheet. Raising a stale editor
|
||||
/// would offer the last group's default key to the new one without anybody choosing it, which is what
|
||||
/// <see cref="ClearGroupEditor"/> prevents, on both heads alike.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The parent is defaulted after that clearing rather than inside it, and only here. This is the one
|
||||
/// path that means "make one", and a group made inside the group that is open is what + NEW GROUP has to
|
||||
/// mean now that the cards are one level of a tree — filed at the outermost level it would disappear
|
||||
/// from the screen it was made on. The other two callers are a cancel and a save, and neither is asking
|
||||
/// for a group anywhere.
|
||||
/// path that means "make one"; the other two callers are a cancel and a save, and neither is asking for a
|
||||
/// group anywhere.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The group that is open, and deliberately not the card that is selected</b> — which is where this
|
||||
/// differs from <see cref="NewHost"/>. A selected card is what EDIT and DELETE are aimed at; reading it
|
||||
/// as "and the next group goes inside it" would nest one because somebody had highlighted something,
|
||||
/// while the open group is the screen everybody can see they are on.
|
||||
/// <b>The group <see cref="GroupTarget"/> names, and deliberately not the card that is selected</b> —
|
||||
/// which is where this differs from <see cref="NewHost"/> only in which selection it would have read.
|
||||
/// Nothing today sets <see cref="GroupFilter"/> to a value worth reading, per that property's own
|
||||
/// remarks, so in practice this always falls through to the standing "new items go to" preference below.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>And in the open group's vault</b>, which <see cref="NewHost"/> now does too and for the same
|
||||
/// reason: a parent — or a group — in a second vault is a level half the readers cannot resolve, so the
|
||||
/// thing being made goes where the thing it is going inside already is. Defaulting to the standing
|
||||
/// "new items go to" preference instead would answer "+ NEW GROUP inside PLATFORM" with a group
|
||||
/// somewhere else and no parent — a form that silently dropped the one thing the button said.
|
||||
/// <b>And in that group's vault</b>, which <see cref="NewHost"/> does too and for the same reason: a
|
||||
/// parent — or a group — in a second vault is a level half the readers cannot resolve, so the thing being
|
||||
/// made goes where the thing it is going inside already is, on the rare occasion there is one.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
@@ -7990,18 +7636,16 @@ internal sealed partial class VaultViewModel(
|
||||
/// <para>
|
||||
/// <b>Refused with a host editor open</b>, which the deletion of a single host is not. The difference is
|
||||
/// that this one writes to hosts: unfiling rewrites every machine under the heading, and doing that
|
||||
/// beneath a half-typed edit of one of them is the save nobody asked for that
|
||||
/// <see cref="MoveHostToGroupAsync"/> refuses for the same reason.
|
||||
/// beneath a half-typed edit of one of them is the save nobody asked for.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Aims where Edit does: at the group it is handed, and failing that at the selected card, which on the
|
||||
/// desktop is the one the menu opened on. See <see cref="GroupTarget"/>.
|
||||
/// Aims where Edit does: at the group it is handed, and failing that at <see cref="GroupTarget"/> — see
|
||||
/// that property's own remarks for why nothing today actually reaches this through the fallback.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The argument is the phone's and the fallback is the desktop's.</b> The desktop's menu passes
|
||||
/// nothing, because the code-behind has already selected whatever was right-clicked; the phone has no
|
||||
/// group selection to make — a heading is not a thing its list can select — so its sheet passes the row
|
||||
/// it was opened on. See <see cref="DeleteGroupFromHeading"/>.
|
||||
/// <b>Both heads pass the row directly rather than leaning on a selection.</b> <see cref="DeleteGroupFromHeading"/>
|
||||
/// resolves it from the heading that was pressed, on the desktop's menu and the phone's sheet alike —
|
||||
/// see that command's own remarks.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="group">The group to ask about, or null to use <see cref="GroupTarget"/>.</param>
|
||||
@@ -8823,8 +8467,8 @@ internal sealed partial class VaultViewModel(
|
||||
/// <b>The parent is left behind and the tags are dropped</b>, and both are the same rule the host's move
|
||||
/// follows: a parent group and a tag are items of the vault being left, so a reference carried across
|
||||
/// would resolve on this machine — groups and tags resolve over every readable vault — and dangle for
|
||||
/// everybody else in the destination. The moved group becomes a root, which is what the trail will show,
|
||||
/// and it is said in the sentence afterwards rather than discovered.
|
||||
/// everybody else in the destination. The moved group becomes a root, and it is said in the sentence
|
||||
/// afterwards rather than discovered.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Keys and passwords are kept</b>, on the host and on the group's own defaults, because those do
|
||||
@@ -8834,11 +8478,10 @@ internal sealed partial class VaultViewModel(
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The group is resolved from the panel's own id rather than from <see cref="GroupTarget"/>.</b> That
|
||||
/// property is the desktop's selection and is null on the phone, whose sheet aims the move by handing
|
||||
/// the row in — so reading it here would leave the phone's MOVE button doing nothing at all. It also
|
||||
/// says the honest thing on both heads: what this moves is the shelf the panel was opened on, and a
|
||||
/// selection that has since gone elsewhere has already folded the panel away. See
|
||||
/// <see cref="CloseTheGroupMovePanelIfAimedElsewhere"/>.
|
||||
/// property's own remarks say why nothing sets it to a value worth reading here — resolving from the
|
||||
/// panel is what lets both heads' MOVE button work regardless, and it says the honest thing besides: what
|
||||
/// this moves is the shelf the panel was opened on, and a selection that has since gone elsewhere has
|
||||
/// already folded the panel away. See <see cref="CloseTheGroupMovePanelIfAimedElsewhere"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
@@ -8879,15 +8522,11 @@ internal sealed partial class VaultViewModel(
|
||||
"Moving…",
|
||||
async () =>
|
||||
{
|
||||
var landed = await ReSealTheSubtreeAsync(subtree, filed, target.VaultId, cancellationToken)
|
||||
await ReSealTheSubtreeAsync(subtree, filed, target.VaultId, cancellationToken)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
await ReloadAsync(cancellationToken).ConfigureAwait(true);
|
||||
|
||||
// By its new id, as a moved host's pane is: leaving the buttons aimed at a card that no
|
||||
// longer exists would read as the shelf having been deleted rather than moved.
|
||||
SelectedGroup = VisibleGroups.FirstOrDefault(card => card.EntityId == landed);
|
||||
|
||||
Status = $"Moved '{row.Label}' to {name}.{WhatCameAlong(subtree.Count, filed.Count)}"
|
||||
+ $"{WhatStayedBehind(uprooted, tagged)}{stranded}";
|
||||
}).ConfigureAwait(true);
|
||||
@@ -12188,16 +11827,6 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
partial void OnSelectedHostChanged(HostRowViewModel? value)
|
||||
{
|
||||
// One selection, across both grids. The two lists are drawn one above the other and they are marked
|
||||
// the same way, so two lit cards read as two things chosen — and the buttons underneath them are two
|
||||
// pairs, only one of which would act. Losing a selection leaves the other alone: a null here is what
|
||||
// a filter matching nothing writes, and taking the mark off a group card because a search box
|
||||
// emptied the grid beneath it would be this rule firing at something that is not a choice.
|
||||
if (value is not null)
|
||||
{
|
||||
SelectedGroup = null;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(SelectedHostAsksForAPassword));
|
||||
OnPropertyChanged(nameof(SelectedHostAuthenticationNote));
|
||||
|
||||
@@ -12276,35 +11905,12 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The other half of the shared selection; see <see cref="OnSelectedHostChanged"/>. Clearing the host
|
||||
/// takes the drawer with it, and that is the point rather than a side effect: a pane about one machine
|
||||
/// cannot go on standing beside a marked group, since nothing on it would be about what is selected.
|
||||
/// </remarks>
|
||||
partial void OnSelectedGroupChanged(HostGroupRowViewModel? value)
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
SelectedHost = null;
|
||||
}
|
||||
|
||||
DisarmIfAimedElsewhere(DeletionTarget.Group, GroupTarget?.EntityId);
|
||||
CloseTheGroupMovePanelIfAimedElsewhere();
|
||||
|
||||
OnPropertyChanged(nameof(GroupTarget));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Leaves the selection alone, which is the change that split one click into two gestures: this fires
|
||||
/// from <see cref="OpenGroup"/>, and that command has already dropped the selection before assigning
|
||||
/// here — see the property.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The cards are rebuilt before the hosts because both are the same move, and the deletion is disarmed
|
||||
/// against <see cref="GroupTarget"/> rather than against the group itself: a reload hands this a new row
|
||||
/// object for the group already open, and taking a question away from under somebody because the row
|
||||
/// behind it was replaced is exactly what <see cref="DisarmIfAimedElsewhere"/> compares ids to avoid.
|
||||
/// </para>
|
||||
/// The deletion is disarmed against <see cref="GroupTarget"/> rather than against the group itself: a
|
||||
/// reload hands this a new row object for whatever <see cref="GroupFilter"/> names, and taking a question
|
||||
/// away from under somebody because the row behind it was replaced is exactly what
|
||||
/// <see cref="DisarmIfAimedElsewhere"/> compares ids to avoid. <see cref="RebuildVisibleHosts"/> runs
|
||||
/// after, since a filter narrowing the board is still live even though nothing sets it any more — see
|
||||
/// <see cref="GroupFilter"/>'s own remarks.
|
||||
/// </remarks>
|
||||
partial void OnGroupFilterChanged(HostGroupRowViewModel? value)
|
||||
{
|
||||
@@ -12313,7 +11919,6 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
OnPropertyChanged(nameof(GroupTarget));
|
||||
|
||||
RebuildGroupLevel();
|
||||
RebuildVisibleHosts();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user