Skip to content

feat(tabs): undo for closing a query tab (#747) - #818

Merged
cevheri merged 3 commits into
libredb:mainfrom
Asgabani:feat/undo-close-tab
Sep 14, 2026
Merged

cevheri merged 3 commits into
libredb:mainfrom
Asgabani:feat/undo-close-tab

Conversation

@Asgabani

Copy link
Copy Markdown
Contributor

Closes #747.

What changed

closeTab (src/hooks/use-tab-manager.ts) dropped the closed tab's state immediately, so a misclick on the close icon lost an unsaved query with no recovery. It now:

  • Captures the closed tab (its full QueryTab — query text, name, type — and its original index) in a ref before removing it.
  • Shows a toast (sonner, matching the existing showStarPromptToast pattern) with an "Undo" action.
  • reopenLastClosedTab() splices the tab back into its original position and makes it active again.

Deliberately only the single most-recently-closed tab is recoverable — closing a second tab before undoing the first drops the first. That matches the issue's framing (recovering from an immediate misclick, not a multi-level history) and keeps the implementation to one ref rather than a stack.

No confirmation step was added to the close action itself, per the issue's explicit constraint ("closing a tab is a frequent, usually intentional action").

Testing

Ran locally (bun run format && bun run lint && bun run typecheck && bun run knip && bun run chart:check && bun run channels:showcase:check && bun run readme:check && bun run security:check && bun run test && bun run build, plus bun run build:lib && bun run attw since useTabManager is reachable from the embeddable workspace export surface):

  • format / lint / typecheck / knip: clean. Lint warning count unchanged from a clean checkout (189).
  • All four drift guards (chart, channels showcase, readme, security): pass.
  • bun run test:components: 39/39 groups pass.
  • bun run test: same 13 pre-existing failures as a clean checkout (Helm binary not installed, missing built standalone zip). No live Postgres/MySQL in this sandbox.
  • bun run test:coverage && bun run coverage:check: 100.00% line coverage on the merged lcov.
  • bun run build, bun run build:lib, bun run attw: all succeed.

If CI surfaces something this sandbox couldn't (Helm chart tests, live DB integration tests), happy to fix it up.

closeTab dropped a tab's query and name with no way back — a misclick
on the close icon lost unsaved work outright. closeTab now keeps the
just-closed tab (query, name, position) and offers a toast with an
Undo action that restores it and makes it active again. Only the most
recent close is recoverable; the close action itself stays
confirmation-free per the issue's explicit constraint.
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri
cevheri self-requested a review September 14, 2026 18:17
@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.

Thanks, and the tests are real: reverting the hook to main turns five of your six cases red, and mutating the restore fails exactly the cases that name that behaviour.

Two things need changing first, both about where the undo keeps its memory.

The ref is one slot, but the UI offers one Undo per toast. sonner keeps three toasts for four seconds each, so closing two tabs in quick succession puts two Undo buttons on screen naming different tabs, and both call reopenLastClosedTab(). I closed Query 1, then Query 2, then clicked Undo in the toast reading Closed "Query 1": Query 2 came back and Query 1 was gone. Letting each toast close over the tab it names fixes that and makes the label honest.

The memory also survives a connection switch. Close a tab in connection A, switch to B while the toast is up, click Undo: A's tab lands in B's workspace, both tabs carry id: "default", React logs a duplicate-key error, and the save effect persists the duplicate into libredb_workspace_tabs_v1:connB, so editing one tab then edits both. Scope the memory to the workspace key and drop it when the workspace reloads.

Smaller: prev.length === 1 used to be checked inside the setTabs updater. Read from the closure, two batched closes both pass the guard and leave zero tabs, so keep the removal and its guard in one updater.

…redb#747)

One ref held the last closed tab, but sonner keeps several toasts on screen,
so the Undo under an older toast restored the newer tab and the older one was
lost. Each toast now closes over the tab it names.

A connection switch dismisses outstanding Undo toasts, and a late click is
checked against the workspace key. Undo after a switch put the old
connection's tab into the new one with a duplicate "default" id, which the
save effect persisted.

The last-tab guard is back inside the setTabs updater, a restored tab goes
back before its right-hand neighbour so out-of-order undos keep the order,
and a restore never duplicates an id that is open again.
@cevheri

cevheri commented Sep 14, 2026

Copy link
Copy Markdown
Member

@Asgabani, I needed this merged for other work of mine, so I went ahead and made the changes from my review myself instead of waiting for another round. I hope you don't mind; the commit is on top of yours, and I am merging once CI is green.

It follows the review point by point: each toast now closes over the tab it names, a connection switch dismisses outstanding Undo toasts so a late click cannot cross workspaces, and the last-tab guard is back inside the setTabs updater. Testing it turned up two small extras: a restored tab now goes back before its right-hand neighbour, so undoing out of order keeps the order, and reopenLastClosedTab left the hook's API since nothing called it. Each case has a test, and the diff may be useful as a reference.

The feature itself is a good one to have. Please keep going, and I look forward to your next PR.

@cevheri
cevheri merged commit 812ac31 into libredb:main Sep 14, 2026
22 checks passed
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.

Closing a tab loses its query with no way to undo it

2 participants