Skip to content

feat(sidebar): add a favorite/pin toggle for connections - #812

Open
Asgabani wants to merge 4 commits into
libredb:mainfrom
Asgabani:feat/favorite-connections
Open

Asgabani wants to merge 4 commits into
libredb:mainfrom
Asgabani:feat/favorite-connections

Conversation

@Asgabani

Copy link
Copy Markdown
Contributor

Closes #694.

A star toggle on each connection row in the sidebar, persisted to a new favorite_connections storage collection. Favorited connections render together in their own group above the rest of the list, which keeps exactly the order and behaviour it always had — toggling a favorite only ever pulls entries out of the regular list, never reorders or filters what remains.

Why a separate id set, not a field on DatabaseConnection: a managed:true connection is always taken fresh from the server on every load, so a field on the connection object itself would be silently discarded on reload for exactly the connections a user is most likely to want to favorite. A separate id list favorites correctly regardless of who owns the connection, and a duplicated connection (new id) doesn't inherit the original's favorite status for free.

Implementation: useFavoriteConnections is built on useSyncExternalStore rather than an effect that reads storage and calls setStatefavorite_connections is exactly that: state living outside React, mutated by the storage facade — so a favorite pulled down from the server, or toggled from another mounted instance of the hook, is reflected without a synchronous setState-in-effect render cascade (caught by react-hooks/set-state-in-effect on my first draft). The new collection is wired into every place that treats STORAGE_COLLECTIONS as the full set (server-sync pull path, the collection-data switch), alongside the existing dismissed_seeds entry.

Testing

  • bun run format, lint, typecheck, knip, chart:check, channels:showcase:check, readme:check, security:check, and build all pass.
  • bun run test:coverage + coverage:check: 100.00% (47133/47133 lines) on the merged report. Every file this PR touches is individually at 100%, including the SSR-only getServerSnapshot branch (exercised via ReactDOMServer.renderToString, matching the existing pattern in use-line-numbers-preference.test.ts).
  • bun run test itself still fails locally — 195 pre-existing failures, all either Helm chart tests (no helm binary in my environment) or DB-provider integration tests needing a live database. Verified identical on a clean, unmodified checkout via git stash before starting. None are in a file this PR touches.

A star toggle on each connection row in the sidebar, persisted to the
storage facade's new favorite_connections collection. Favorited
connections render together in their own group above the rest of the
list, which keeps exactly the order and behaviour it always had - this
only ever pulls entries out of it, never reorders or filters what
remains.

Deliberately a separate id set rather than a field on
DatabaseConnection: a managed:true connection is always taken fresh
from the server on every load (mergeManagedConnections in
use-connection-manager.ts), so a field on the connection itself would
be silently discarded on reload for exactly the connections a user is
most likely to want to favorite. A separate id list favorites
correctly regardless of who owns the connection, and a duplicated
connection (new id) does not inherit the original's favorite status
for free.

useFavoriteConnections is built on useSyncExternalStore rather than an
effect that reads storage and calls setState - favorite_connections is
exactly that: state that lives outside React, mutated by the storage
facade - so a favorite pulled down from the server, or toggled from
another mounted instance of the hook, is reflected without a
synchronous setState-in-effect render cascade (caught by
react-hooks/set-state-in-effect on the first draft).

The new collection is wired into every place that treats
STORAGE_COLLECTIONS as the full set: the server-sync pull path and the
collection-data switch in use-storage-sync.ts, alongside the existing
dismissed_seeds entry.

Testing: bun run format, lint, typecheck, knip, chart:check,
channels:showcase:check, readme:check, security:check, and build all
pass. bun run test:coverage + coverage:check: 100.00% (47133/47133
lines) on the merged report; every file this PR touches is
individually at 100%, including the SSR-only getServerSnapshot branch
in use-favorite-connections.ts (exercised via ReactDOMServer.renderToString,
matching the existing pattern in use-line-numbers-preference.test.ts).

bun run test itself still fails locally - 195 pre-existing failures,
all either Helm chart tests (no `helm` binary in this environment) or
DB-provider integration tests that need a live database connection;
verified identical on a clean, unmodified checkout via git stash
before starting this change. None are in a file this PR touches.
@cevheri
cevheri self-requested a review September 12, 2026 10:37
@cevheri

cevheri commented Sep 12, 2026

Copy link
Copy Markdown
Member

thanks and welcome @Asgabani
I’m working on PR #811 right now, which includes some big changes to the schema/objects explorer(nosql databases test remaining)

After that, I’ll do a detailed local test your PR and give feedback

@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ctions

# Conflicts:
#	src/components/Studio.tsx
#	src/components/sidebar/Sidebar.tsx
@cevheri cevheri added the enhancement New feature or request label Sep 14, 2026

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, @Asgabani. I ran it in the app: starring, the Favorites group, persistence across a reload, unstarring, and the star not selecting the row all behave. Favoriting a managed seed connection survives a reload too, so the reasoning you wrote in types.ts for a separate id list rather than a field on DatabaseConnection is right, and it is wired at all five sites dismissed_seeds occupies. Three things before I merge.

  1. storage.deleteConnection does not prune favorite_connections. I deleted a favorited connection and its id was still in the list afterwards, and it keeps getting pushed to server storage. deleteConnection is already where a sibling id list is maintained on delete: dismissed_seeds is recorded there. Favorites are the inverse, so they should be removed there, with a test.

  2. Star every connection and the "Connections" header renders above nothing. rest is empty while connections is not, and the empty state is correctly suppressed, so the section itself needs to go when rest is empty. Note the guard can be just connections.length === 0, since rest is a subset of connections.

  3. docs/STORAGE.md is the collection inventory and it still describes ten. The collection list in the schema table, the collection reference table, the localStorage key map, the facade API row, and two places that spell the count. Two code comments count them too: encrypting-provider.ts says "the remaining six hold metadata", which is the written justification for encrypting only connections, and connection-secrets.ts says "all ten collections".

Two small ones while you are in there. The comment on useFavoriteConnections says a favorite pulled from the server arrives through the subscription, but the pull writes localStorage directly and dispatches no change event; it works because storageReady flips after the pull and that re-render re-reads the snapshot. And the star is the first focusable control a managed row has ever had, so focus-visible:opacity-100 would keep it reachable for a keyboard user.

- deleteConnection now prunes the deleted id out of
  favorite_connections too, mirroring the existing dismissed_seeds
  handling.
- ConnectionsList hides the "Connections" section entirely when
  every connection is favorited, instead of rendering its header
  above nothing.
- docs/STORAGE.md's collection inventory (schema table, collection
  reference table, localStorage key map, facade API row, and both
  "10 collections" call-outs) now accounts for favorite_connections,
  along with the two code comments in encrypting-provider.ts and
  connection-secrets.ts that counted collections.
- Corrected the useFavoriteConnections comment describing how a
  server-pulled favorite reaches the hook: the pull writes
  localStorage directly with no dispatched event: useSyncExternalStore
  simply re-reads getSnapshot on the re-render storageReady's own
  flip already causes.
- Added focus-visible:opacity-100 to the star toggle so it stays
  reachable for keyboard users.
- Updated tests/components/studio/source-tab.test.tsx's storage mock,
  which predates this branch's favorites work and was missing the
  two methods useFavoriteConnections calls.
@Asgabani

Copy link
Copy Markdown
Contributor Author

Addressed all three:

  1. deleteConnection now prunes the deleted id out of favorite_connections, mirroring the existing dismissed_seeds handling, with tests.
  2. ConnectionsList hides the "Connections" section entirely when rest is empty and connections isn't (all favorited), rather than rendering the header above nothing. Simplified the empty-state guard to connections.length === 0 as suggested.
  3. docs/STORAGE.md's collection inventory is updated everywhere it counted collections (schema table, collection reference table, localStorage key map, facade API row, both "10 collections" call-outs), plus the two code comments in encrypting-provider.ts and connection-secrets.ts.

Also fixed the two smaller ones: corrected the useFavoriteConnections comment (the server pull writes localStorage directly with no dispatched event — it works because useSyncExternalStore re-reads getSnapshot on the render storageReady's own flip causes), and added focus-visible:opacity-100 to the star toggle.

Rebased onto latest main — that pulled in a new tests/components/studio/source-tab.test.tsx whose storage mock predates this branch and was missing the two methods useFavoriteConnections calls, so I added those too (unrelated to your feedback, just needed to keep tests green post-merge).

Ran the full local suite again: format/lint/typecheck/knip clean, all four drift guards pass, test:components 46/46 groups, core suite at the same pre-existing 13 environment-gap failures (Helm/standalone-zip) as a clean checkout, 100.00% coverage, build green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to mark a connection as a favorite

2 participants