Open a new host in the vault of the group it is being made in

+ NEW HOST decided two defaults separately and let them contradict each other.
The group came from the screen — the selected card, or failing that the group
whose contents are showing — and the vault came from the keychain screen's
standing "new items go to" preference. Inside a group belonging to any other
vault the two disagreed, and the group is what lost: GroupInEditingVault drops a
group the editor's vault has not got, on the sound reasoning that a host filed
under an id its readers cannot resolve looks unfiled to everybody but the person
who wrote it. So pressing the button while standing inside a team's PLATFORM
opened a form filed under nothing, bound for the personal vault, with no sentence
anywhere saying either thing had happened.

The vault now follows the group. A group lives in exactly one vault, so a host
that is to land in that group has to be sealed in that vault too — which is the
rule + NEW GROUP has followed for a parent since the cards became a tree, and the
comment there claiming this as a deliberate difference from the host's editor is
the one the code has now caught up with.

The filter stays, because there is one case left for it: the group's vault may be
one this session can read and not write, a team vault this account is a viewer of.
TargetVaults is the readable-and-writable set and is what decides here, so a
viewer keeps the standing preference and loses the group with it, rather than
opening an editor aimed at a save that cannot happen.

Both directions are tested, since one alone would not say which default wins:
standing in a shared vault's group, the editor opens on that vault with the group
selected and the host saves there; and with the preference pointed at the shared
vault while a personal-vault group is open, the group beats the picker somebody
set once.
This commit is contained in:
2026-08-06 12:08:15 +02:00
parent f1d6499bb5
commit 808a9a7fc1
2 changed files with 113 additions and 14 deletions
@@ -6174,10 +6174,10 @@ internal sealed partial class VaultViewModel(
editingEntityId = null;
// The keychain screen's picker is the default rather than the answer: the editor has a picker of its
// own from here on, and moving that one is what decides where this host lands. See
// EditorVaultChoices.
editingHostVaultId = TargetVaultId;
// Read before the vault is decided, because it is what decides it.
var inherited = GroupTarget;
editingHostVaultId = VaultForANewHostUnder(inherited);
EditorLabel = string.Empty;
EditorHostname = string.Empty;
@@ -6199,11 +6199,11 @@ internal sealed partial class VaultViewModel(
// 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. Only when that group is in the vault this host is going into,
// though — the grid draws the active vault's groups, and inheriting one into a shared vault would
// file the host under something nobody else in it can resolve. Before the authentication picker,
// because whether there is a group to inherit from decides whether that one offers to.
BuildGroupChoices(GroupInEditingVault(GroupTarget?.EntityId));
// 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.
BuildGroupChoices(GroupInEditingVault(inherited?.EntityId));
BuildAuthenticationChoices(
boundKeyId: null,
@@ -6214,6 +6214,32 @@ internal sealed partial class VaultViewModel(
Status = "Adding a host.";
}
/// <summary>Which vault a host started from the grid is sealed in.</summary>
/// <param name="inherited">The group the editor is about to open on, if there is one.</param>
/// <remarks>
/// <para>
/// The keychain screen's picker is the default rather than the answer: the editor has a picker of its own
/// from here on, and moving that one is what decides where the host lands. See
/// <see cref="EditorVaultChoices"/>.
/// </para>
/// <para>
/// A group to inherit beats it, because a group lives in exactly one vault and a host that is to land in
/// that group has to be sealed in that vault too. Deciding the two separately made them contradict each
/// other: the group was dropped by <see cref="GroupInEditingVault"/> and + NEW HOST inside PLATFORM
/// opened a form filed under nothing, bound for somewhere else. It is the rule <see cref="NewGroup"/>
/// already follows for a parent.
/// </para>
/// <para>
/// Only where this session can write to that vault, which is what <see cref="TargetVaults"/> is asked
/// here: a viewer of a shared vault keeps the standing preference and loses the group with it, rather
/// than opening an editor aimed at a save that cannot happen.
/// </para>
/// </remarks>
private Guid VaultForANewHostUnder(HostGroupRowViewModel? inherited) =>
inherited is { } group && TargetVaults.Any(choice => choice.VaultId == group.VaultId)
? group.VaultId
: TargetVaultId;
/// <summary>Opens the selected host for editing.</summary>
[RelayCommand]
private void EditSelectedHost()
@@ -6623,11 +6649,10 @@ internal sealed partial class VaultViewModel(
/// while the open group is the screen everybody can see they are on.
/// </para>
/// <para>
/// <b>And in the open group's vault, which is where this differs from <see cref="NewHost"/> a second
/// time.</b> A host opens on the standing "new items go to" preference and takes the open group only if
/// that group happens to be in the same vault; a group made inside another group is in that group's
/// vault by construction, because a parent in a second vault is a level half the readers cannot resolve.
/// Defaulting to the preference instead would answer "+ NEW GROUP inside PLATFORM" with a group
/// <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.
/// </para>
/// </remarks>