Skip to content

refactor(databases): move Import Data and Export CSV into the table's "..." menu - #1693

Merged
dawsontoth merged 5 commits into
stagefrom
refactor/browse-toolbar-move-import-export-to-menu
Sep 9, 2026
Merged

refactor(databases): move Import Data and Export CSV into the table's "..." menu#1693
dawsontoth merged 5 commits into
stagefrom
refactor/browse-toolbar-move-import-export-to-menu

Conversation

@cb1kenobi

@cb1kenobi cb1kenobi commented Sep 8, 2026

Copy link
Copy Markdown
Member

What

On the databases browse-data page, the toolbar led with three buttons in the same bright green: Add New Record(s), Import Data, and Export CSV — which read as three equally common actions when only the first is.

The problem turned out to be their prominence, not their presence (see the discovery discussion below). So:

  • Add New Record(s) keeps the green positiveOutline — it's the one call to action.
  • Import Data and Export CSV keep toolbar buttons, restyled to the muted purple defaultOutline.
  • Below the xl breakpoint, where the row runs out of room, those two buttons drop out and their "..." menu entries take over.

The buttons are hidden xl:inline-flex and the menu entries (plus their separator) are xl:hidden, so each action is reachable at every width and duplicated at none.

Menu grouping

Below xl, the menu carries the full set:

Separators split the menu into three groups, so the destructive items are visibly set apart from everything else:

Import Data          (gated on canImportData)
Export CSV           (disabled while a export is in flight)
─────────────────
Only If Cached
Cleanup Orphan Blobs (staff operators only)
─────────────────
Drop Table           (needs describe_all; see below)
Drop Database

The second separator is gated on canManageBrowseInstance, so a user who can't see the drop items doesn't get a trailing separator hanging off the bottom of the menu.

At xl and up, the first two entries and the separator under them drop out, so the menu opens on Only If Cached.

Discoverability (review feedback)

@dawsontoth pushed back on moving both into the menu unconditionally, and was right to: Import Data is how a new user seeds their first data, so burying it trades onboarding friction for a tidier row, and Export CSV is common enough to follow it for symmetry. The suggested fix — mute the color rather than hide the controls, and let them collapse only when space is genuinely short — is what's implemented above.

The accessKey="i" / "e" shortcuts stay dropped even though the buttons came back. Access keys only fire on rendered elements, so they'd work at xl and up and silently do nothing below it; a shortcut that depends on window width seemed worse than none.

The menu trigger's describe_all gate (review feedback)

The trigger carried disabled={!instanceDatabaseMap}, added back when the menu held nothing but Drop Table / Drop Database — the split between those two turns on how many tables the database has, which only describe_all can answer. Moving Export CSV in made it inherit a condition it has nothing to do with: export needs only the separately fetched table schema and the search operation, and an operation allowlist can grant describe_table + search without describe_all. Since both describes use retry: false, such a role got a table that read normally with its only export control permanently dead.

The gate now sits where the dependency actually is:

  • The trigger and its button lose disabled entirely.
  • Drop Table requires positive evidence that another table remains.
  • Drop Database keeps only the manage-permission gate — it acts on the database named by the route and never needed the map.

isLastTableInDatabase is gone with it. It answered false both when another table remains and when the map hadn't said anything, and a resolved map that doesn't list this database counted its zero tables as "not the last one" — so it's replaced by the decision it was feeding, canDropTable, which has no unsafe reading. This also matches TableContextMenuItems, which already hid Drop Table on an absent map without disabling anything else.

Notes

  • Both entries keep their existing behavior: the canImportData permission gate on Import, and the isExportingCSV disabled state on Export.
  • The accessKey="i" / "e" shortcuts are dropped along with the buttons. Access keys only fire on rendered elements, and the menu's content isn't in the DOM while it's closed — there's nothing left for the browser to bind to. n (Add New Record(s)) and f (filters) are unchanged.
  • The right-click context menu (TableContextMenuItems) already offered both actions and is untouched.

Verification

tsc -b, oxlint, and dprint are clean, and the full suite passes (2941 tests).

DatabaseTableView had no tests; it has seven now, in a new DatabaseTableView.test.tsx:

  1. Import Data and Export CSV render as defaultOutline toolbar buttons shown from xl up, and as menu entries hidden from xl up — the complementary pair that keeps each reachable everywhere and duplicated nowhere. (jsdom runs no Tailwind, so the classes are what's observable; visibility itself isn't.)
  2. With the map absent and describe_table seeded: the trigger isn't disabled, Export CSV is present and enabled, Import Data is present — and the grid's columns really did come from describe_table alone, so this pins a readable table whose export is dead rather than a half-loaded view.
  3. Drop Table withheld, Drop Database kept, while the map is absent.
  4. Drop Table withheld when the map resolved without this database.
  5. Both drop entries offered once the map resolves, for a role that can manage.
  6. Neither drop entry for a role that can't manage.
  7. Only Drop Database when it's the database's only table.

Each guard was checked by reverting it and confirming the matching tests fail: restoring the trigger's disabled fails 2 and 3; dropping the databaseTables evidence requirement fails 3 and 4; putting the buttons back to green, or dropping xl:hidden from the menu entries, each fails 1.

Still not exercised in a running app. The xl breakpoint in particular is a judgment call I made from reading the toolbar's contents, not from watching the row wrap — worth resizing the browser across it before merge, along with a look at the purple buttons next to the green one.

🤖 Generated with Claude Code

… "..." menu

The browse-data toolbar led with three primary-looking buttons, but only Add New
Record(s) is a common action -- Import Data and Export CSV are occasional, and
they crowded out the row for no gain. Both move into the existing "..." menu
alongside the other per-table actions, keeping their permission gate
(`canImportData`) and the in-flight `isExportingCSV` disabled state.

Separators now group the menu: one after Export CSV to split the data-transfer
pair from Only If Cached / Cleanup Orphan Blobs, and one before Drop Table /
Drop Database so the destructive items are visibly set apart. The second is
gated on `canManageBrowseInstance` so a user without those items doesn't get a
trailing separator.

The `accessKey="i"` / `"e"` shortcuts are dropped with the buttons -- access
keys only fire on rendered elements, and the menu's content isn't in the DOM
while it's closed. `n` (Add New Record(s)) and `f` (filters) are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cb1kenobi
cb1kenobi requested a review from a team as a code owner September 8, 2026 23:26
@cb1kenobi
cb1kenobi requested a review from dawsontoth September 8, 2026 23:26

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the DatabaseTableView component by moving the 'Import Data' and 'Export CSV' action buttons from the main toolbar into the dropdown menu. It also adds separators within the dropdown menu to improve visual organization. There are no review comments, so I have no feedback to provide.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 61.45% 8394 / 13658
🔵 Statements 61.84% 8985 / 14528
🔵 Functions 53.84% 2106 / 3911
🔵 Branches 55% 6006 / 10920
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/databases/components/DatabaseTableView.tsx 65.24% 55.37% 30.3% 64.81% 183-196, 200-202, 227, 232-235, 239, 283-286, 376, 386-388, 393-406, 416-422, 436-452, 456-464, 473-481, 487-498, 505-510, 513-516, 521, 525, 536, 679-689
Generated in workflow #1879 for commit 69f94a9 by the Vitest Coverage Report Action

@kriszyp kriszyp 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.

Good move.
🤖 Reviewed with Codex

Comment thread src/features/instance/databases/components/DatabaseTableView.tsx Outdated
cb1kenobi added a commit that referenced this pull request Sep 9, 2026
cb1kenobi and others added 2 commits September 8, 2026 23:46
…e_all gate

The "..." trigger carried `disabled={!instanceDatabaseMap}`, added back when the
menu held nothing but Drop Table / Drop Database -- the split between those two
turns on how many tables the database has, which only `describe_all` can answer.
Moving Export CSV into the menu made it inherit that gate, though export needs
only the separately fetched table schema and the search operation. An operation
allowlist can grant `describe_table` and search without `describe_all`, and both
describes use `retry: false`, so such a role gets a table that reads normally
with its only export control permanently dead.

The gate moves to where the dependency actually is: the trigger and its button
lose `disabled`, and Drop Table alone waits on the map. Drop Database keeps just
the manage-permission gate -- it acts on the database named by the route.

DatabaseTableView had no tests; this adds its first, covering the reported case
(map absent, table schema present).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t gates on

Builds on the previous commit's fix. `isLastTableInDatabase` answered `false` both
when another table remains and when `describe_all` simply hasn't said -- so the
call site had to carry `!!databaseTables &&` alongside it to stay honest, and any
future reader of the flag alone would get the unsafe reading. Phrasing it as the
decision instead (`canDropTable`: manage permission plus positive evidence that
another table remains) leaves no unsafe reading to pick.

Also widens the new test file: Drop Database stays available with the map absent
(it acts on the database named by the route and never needed the map), Drop Table
is withheld both for an absent map and for a resolved map that doesn't list this
database, the single-table and cannot-manage cases are pinned, and the absent-map
case now asserts the grid really did come up from describe_table alone -- so it
tests a readable table whose export is dead, not a half-loaded view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cb1kenobi
cb1kenobi force-pushed the refactor/browse-toolbar-move-import-export-to-menu branch from c6c00b0 to 699a029 Compare September 9, 2026 04:47

@dawsontoth dawsontoth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I disagree that they should always be moved into the ... dropdown. They can move there when we don't have space for them, but I want them to stay visible for discovery. Especially for new users, having a mechanism to seed their data rapidly (import data) is really important to reducing their onboarding friction. Exporting data has also been something I've seen commonly done, but I don't have as strong of an argument towards that other than for symmetry with import.

Downgrading their visual important from the bright green to a more muted purple could accomplish some of what you're looking for, and then let them collapse into the ... menu when space is restricted.

cb1kenobi added a commit that referenced this pull request Sep 9, 2026
@dawsontoth

Copy link
Copy Markdown
Contributor

Remember to use conventional commits, Claude!

cb1kenobi and others added 2 commits September 9, 2026 11:09
…olbar runs out of room

Moving both into the "..." menu unconditionally cost more than it bought. Import
Data especially is how a new user seeds their first data, and burying it adds
onboarding friction for the sake of a tidier row; Export CSV follows it for
symmetry. What actually needed fixing was their prominence, not their presence --
they led the toolbar in the same bright green as Add New Record(s), which read as
three equally common actions.

So both keep a toolbar button at `xl` and up, in `defaultOutline` (muted purple)
rather than `positiveOutline` (green), leaving Add New Record(s) as the only green
call to action. Below `xl` the buttons drop out and the menu entries take over, so
the pair stays reachable at every width without ever appearing twice: the buttons
are `hidden xl:inline-flex`, and the menu entries plus their separator are
`xl:hidden`.

The `accessKey="i"` / `"e"` shortcuts stay dropped. They only fire on rendered
elements, so they would work at `xl` and up and silently do nothing below it --
a shortcut that depends on window width is worse than no shortcut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y substring

The new breakpoint test matched `className` against the literal
`'hidden xl:inline-flex'`, which only holds while `cn()` happens to emit those
two classes adjacently in that order. A tailwind-merge bump or a reordered
variant would break it with a failure that points at the string rather than at
the behavior. `classList.contains` asks the question directly, per class.

Also says why these assertions are the ones available: jsdom runs no Tailwind, so
visibility itself can't be observed -- only the complementary pair that produces
it, which is what actually keeps each action reachable at every width and
duplicated at none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cb1kenobi
cb1kenobi force-pushed the refactor/browse-toolbar-move-import-export-to-menu branch from 0b02047 to 69f94a9 Compare September 9, 2026 16:12
@cb1kenobi

Copy link
Copy Markdown
Member Author

You're right, and thanks for the push — the discovery argument for Import Data is the one that settles it. Seeding data fast is the whole first-five-minutes experience, and a control you have to go looking for isn't part of it. I'd been optimizing the toolbar for someone who already knows the page, which is the wrong user to design that row around.

Went with your suggestion:

  • Add New Record(s) stays green (positiveOutline) — the one call to action in the row.
  • Import Data and Export CSV keep their toolbar buttons, restyled to the muted purple defaultOutline.
  • Below the xl breakpoint they collapse into the "..." menu, which is where they'd earned a place anyway.

The buttons are hidden xl:inline-flex and the menu entries (plus the separator under them) are xl:hidden, so each action is reachable at every width and never shows up twice at the same one.

One deliberate omission: I left the accessKey="i" / "e" shortcuts off, even though the buttons are back. Access keys only fire on rendered elements, so they'd work at xl and up and silently do nothing below — a shortcut that depends on window width felt worse than no shortcut. Easy to add back if you disagree.

Two things worth your eye, since I can't settle them from tests:

  1. The xl breakpoint is a guess. I picked it from reading what's in the row, not from watching it wrap. If the buttons crowd the filter/refresh/columns group before 1280px — or survive comfortably below it — that number should move.
  2. The purple-next-to-green pairing is your call visually. jsdom gives me "the class is border-primary," not "this looks right."

Generated by Barber AI (Claude Opus 5)

@cb1kenobi

Copy link
Copy Markdown
Member Author

Fair hit — twice on the same PR, which is worse. History is rewritten and Verify Commits is green:

test(databases): assert the responsive toolbar classes by name, not by substring
feat(databases): keep Import Data and Export CSV visible until the toolbar runs out of room
refactor(databases): make Drop Table's map dependency the condition it gates on
fix(databases): stop the table menu's Export CSV inheriting a describe_all gate
refactor(databases): move Import Data and Export CSV into the table's "..." menu

Only the messages changed — the resulting tree hash is identical to the pre-rewrite one both times, so the reviewed content is untouched.

Worth naming the actual mechanism, because it's a trap for anyone working in a git worktree here and AGENTS.md already flags it:

core.hooksPath is set to .husky/_, which is generated by pnpm install's prepare: husky script and self-gitignored. A worktree created with git worktree add inherits the config pointing at that directory but not the directory itself — so git finds no hooks, .husky/commit-msg never runs commitlint, and a bare message commits silently. Nothing warns you; CI is the first objection. That's what happened both times.

The cheap habit that would have caught it locally, before the push:

npx commitlint --from origin/stage --to HEAD

AGENTS.md documents the trap but doesn't give that remedy — happy to add it there in a separate PR if you think it's worth having written down.

Generated by Barber AI (Claude Opus 5)

@dawsontoth dawsontoth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works great! Pushed to https://dev.studio.harperfabric.com/ to verify.

@dawsontoth
dawsontoth added this pull request to the merge queue Sep 9, 2026
Merged via the queue into stage with commit 17cb5a7 Sep 9, 2026
5 checks passed
@dawsontoth
dawsontoth deleted the refactor/browse-toolbar-move-import-export-to-menu branch September 9, 2026 16:52
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.

3 participants