Skip to content

fix(folders): keep empty folders visible and in JSON exports (#338) - #342

Merged
kipavy merged 5 commits into
devfrom
fix/empty-folders-338
Sep 23, 2026
Merged

kipavy merged 5 commits into
devfrom
fix/empty-folders-338

Conversation

@kipavy

@kipavy kipavy commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #338.

Problem

Folders exist on four object pages: Hosts, Keychain, Port Forwarding and Snippets. Two things went wrong with empty ones.

  • Page view. On Hosts and Snippets, a page with zero items showed the empty-state screen (EmptyState) in place of the folder view. Empty folders vanished, creating a folder with no items looked like it did nothing, and the folder came back once an item existed.
    • Port Forwarding already counted folders before showing its empty message.
    • Keychain draws its folders outside the key and identity lists, which each show their own per-section empty message.
    • Neither of those two was affected.
  • JSON export. buildBundle only emitted folders that an exported item referenced, so empty folders of every type were dropped ("folders": []). Import likewise only recreated folders that an imported item needed.

Fix

  • Pages. HostsPage and SnippetsPage show the empty state only when there are no items and no scoped folders.

  • Export. selectionMethods now takes the folder object_type (connection, keychain, port_forwarding or snippet, typed as that union) and exposes accumulateVaultFolderIds.

    • A full export includes every live folder of each enabled type in the exported vaults, so empty folders are kept on all four pages.
    • Selection exports are unchanged.
  • Import. Bundle folders are created unless skipDupes is set. Only the snippet catalogue install sets it.

    • Every bundle folder is reused when the target vault already has a live folder with the same name, type and parent, and imported items go into it. Re-importing a backup, where every duplicate row defaults to "skip", no longer rebuilds the folder tree as empty copies, and a new host lands in its existing folder instead of a second copy. Root folders come back from the backend with parent_folder_id: null, so the match treats null and a missing parent the same.
    • The import screen passes only the folders that still matter (importableFolders): ancestors of a kept item, plus empty leaf folders and their ancestors. Unticking every item of a folder no longer recreates that folder.
    • newImportCtx builds the eid maps, so the three runImport callers stop spelling them out.
  • Terminal snippets panel. During a search, the panel listed every snippet folder, including ones with zero matches. Folders with no matching snippet are now hidden. Without a search, the panel still hides empty folders as before; it can't create folders.

Tests

  • registry.spec.ts:
    • A full export includes empty folders, including nested and snippet folders. A selection export leaves them out.
    • Tombstoned, other-vault and other-type folders are left out.
    • Import recreates unreferenced folders, and still skips them when deduplicating.
    • Import reuses an existing matching folder (root parent sent as null, like the backend), puts an imported item into it, and does not reuse one with a different parent or type.
    • importableFolders drops folders whose items were all skipped, and keeps empty ones, their ancestors, and the folders of kept items.
  • HostsPage.folders.test.tsx and SnippetsPage.folders.test.tsx: an empty folder is listed when there are zero items. Both tests fail without the fix.
  • SnippetsPanel.search.test.tsx: a search hides a folder with no matching snippet. It fails without the fix.
  • tsc is clean. The import-export, plugin-domain, hosts, snippets, catalogue-install and right-panel suites pass (426/426).

Live verification (headless build, fresh profile, 0 hosts)

  • Right-click → New Folder shows the FOLDERS section with the folder. Renaming it shows "Saved".
  • After adding and then deleting a host, the empty folder stays listed.
  • Export → delete → import of an empty tree ("Prod servers" › "EU", plus an empty snippet folder) exports all 3 folders and re-imports all 3 with the nesting intact, 0 errors. This drove buildBundle/runImport directly with live stores; the save-file dialog can't be driven headless.
  • The Snippets page lists an empty snippet folder instead of its empty state.
  • Import screen, pasted JSON: a first import of Prod › EU, Empty and Dev with 2 hosts created 4 folders and 2 hosts. Re-importing the same file plus one new host in Dev (duplicates skipped by default) imported 1 host into the existing Dev and created no folders.
    • This run caught the null parent bug: before that fix, the re-import duplicated all 4 folders.
  • Terminal snippets panel: with snippets in "OpsFolder" and "DatabaseFolder", searching "deploy" shows only OpsFolder.

The hosts and snippets pages swapped the whole view for the empty state
whenever they had no items, hiding any empty folders. The empty state now
requires no folders too.

A full JSON export only carried folders an exported item lived in, and
import only recreated those. Handlers now report every folder of their
type in the exported vaults, and import creates every folder in the bundle
unless it is deduplicating (the snippet catalog install).
#338)

Creating every bundle folder on import meant a re-import, where each
duplicate row defaults to "skip", rebuilt the whole folder tree empty.

- A folder no imported item needs now maps onto a live folder of the
  same name, type and parent in the target vault instead of being saved.
- The import screen drops folders whose items were all skipped, keeping
  empty leaf folders and their ancestors (importableFolders).
- newImportCtx builds the eid maps so the three callers stop spelling
  them out.
- selectionMethods' folderType is the four-value union.
The backend returns root folders with parent_folder_id: null, so the
strict comparison against undefined never matched one and a re-import
still duplicated the whole tree.
… panel

During a search the panel listed every snippet folder, including ones
with zero matches. byFolder is already built from the filtered list, so
both branches collapse into one and an empty folder is hidden either way.
…338)

A folder an imported item needed was always created, so a re-import
that brought one new host put it in a second copy of its folder. Every
bundle folder now reuses a live folder of the same name, type and parent
in the target vault.
@kipavy
kipavy merged commit 63a59cb into dev Sep 23, 2026
4 checks passed
@kipavy
kipavy deleted the fix/empty-folders-338 branch October 2, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant