From 3d9ed03b090970afba1d80637aae2aab81ef86a9 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Thu, 6 Aug 2026 07:39:03 +0200 Subject: [PATCH] Let a snippet be shared to a vault, the way a host already can MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A snippet was a first-class vault item everywhere except where it mattered: the crypto, the sync, the server table and every registry already treated it exactly as they treat a host, and the screen read it out of the active vault alone. So the one command a team most obviously wants to hold in common — the incantation somebody worked out once and everybody else retypes — was the only item kind that could not leave the machine that wrote it. The read is the half that had to come first, and it is why this is not simply a MoveAsync. ReloadSnippetsAsync now lists every readable vault rather than the active one, in the shape ReloadHostsAsync and ReloadKeysAsync already use: the vault new items go into first, then by vault name, then by label, with a badge on the row only where there is more than one vault to tell apart. Without that, a snippet moved into a team vault would have disappeared from the very screen that moved it, and one a colleague wrote there would never have arrived at all — sharing would have looked like losing. Three writes were pinned to the active vault and each one broke differently once the list spanned several. The delete tombstoned in the wrong vault, which tombstones nothing and leaves the snippet on screen. The save is the bad one: an update sent to the active vault creates a second snippet there and leaves the team original untouched, so the person editing sees their fix and nobody else ever does. That is a fork with no symptom, which is why the vault is now a parameter and the screen latches it when the editor opens — the chosen vault for a new snippet, the row own vault for an existing one — rather than reading it back off a selection that can move under a half-typed form. VaultViewModel has carried editingHostVaultId for the same reason since hosts crossed vaults. Two controls rather than one, and that is the same line the host pane draws. The editor asks which vault a new snippet is filed into; MOVE re-seals an existing one under another key and tombstones the first. Putting the second inside the first would let somebody correcting a typo hand a command to a team by leaving a picker where they found it, so the picker is not drawn for an existing snippet at all. Both live on SnippetsViewModel rather than VaultViewModel because this screen owns its editor, unlike the host drawer; the writing they ask for is still the vault. A snippet crosses whole, which is the one way this is simpler than the host it copies. A host leaves its group and its tags behind because both are items of the vault it came from and would dangle for everybody in the destination. A snippet is a label, a command and a note, and none of them points at anything — so there is nothing to strip, nothing to report as left behind, and what the copy says instead is the thing that is actually at stake: who can read the command afterwards. For a command that may carry a hostname or a path, that is the whole decision. Two judgement calls worth finding later. A hidden vault now hides its snippets, filtered in the screen projection rather than in VaultViewModel.Snippets, which is the rule keys and passwords already follow: the list stays whole so nothing that resolves against it breaks, and the projection is what a preference about reading gets to change. And the nav rail count is left spanning vaults unfiltered, because Vault.Hosts.Count beside it is unfiltered too — filtering one of the four would make the rail disagree with itself. Four flow tests in VaultSharingTests, beside the host ones they mirror: the move re-seals with a new id and carries the runs-on-insert flag across, the move with nowhere to go refuses rather than opening an empty picker, the editor files into the vault chosen on it, and the edit of a shared snippet goes back to its own vault instead of forking. That last one is the regression the latch exists for and the only one whose absence has no visible symptom. Plus a layout test with the move panel open, since that paragraph wraps in a 300-pixel column and the desktop pane it lands in is measured. The whole suite passes: 1660 tests, none failing. --- .../Views/SnippetsScreen.axaml | 99 +++++- .../Views/SnippetsScreen.axaml | 102 +++++- .../ViewModels/SnippetsViewModel.cs | 300 +++++++++++++++++- .../ViewModels/VaultViewModel.cs | 159 +++++++++- src/DodoSSH.Client.Sync/SnippetRepository.cs | 14 + .../ScreenLayoutTests.cs | 34 ++ .../VaultSharingTests.cs | 204 ++++++++++++ 7 files changed, 876 insertions(+), 36 deletions(-) diff --git a/src/DodoSSH.Client.Android/Views/SnippetsScreen.axaml b/src/DodoSSH.Client.Android/Views/SnippetsScreen.axaml index 1e87883..29e1c39 100644 --- a/src/DodoSSH.Client.Android/Views/SnippetsScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/SnippetsScreen.axaml @@ -64,6 +64,26 @@ + + + + + + + + + + + + + @@ -125,6 +145,15 @@ IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"> + + + + + @@ -151,20 +180,66 @@ IsEnabled="{Binding !IsEditing}"> -