refactor(databases): move Import Data and Export CSV into the table's "..." menu - #1693
Conversation
… "..." 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>
There was a problem hiding this comment.
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.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||
kriszyp
left a comment
There was a problem hiding this comment.
Good move.
🤖 Reviewed with Codex
…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>
c6c00b0 to
699a029
Compare
dawsontoth
left a comment
There was a problem hiding this comment.
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.
|
Remember to use conventional commits, Claude! |
…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>
0b02047 to
69f94a9
Compare
|
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:
The buttons are One deliberate omission: I left the Two things worth your eye, since I can't settle them from tests:
Generated by Barber AI (Claude Opus 5) |
|
Fair hit — twice on the same PR, which is worse. History is rewritten and Verify Commits is green: 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
The cheap habit that would have caught it locally, before the push: npx commitlint --from origin/stage --to HEAD
Generated by Barber AI (Claude Opus 5) |
dawsontoth
left a comment
There was a problem hiding this comment.
Works great! Pushed to https://dev.studio.harperfabric.com/ to verify.
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:
positiveOutline— it's the one call to action.defaultOutline.xlbreakpoint, where the row runs out of room, those two buttons drop out and their "..." menu entries take over.The buttons are
hidden xl:inline-flexand the menu entries (plus their separator) arexl: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:
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
xland 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 atxland up and silently do nothing below it; a shortcut that depends on window width seemed worse than none.The menu trigger's
describe_allgate (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 onlydescribe_allcan 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 grantdescribe_table+ search withoutdescribe_all. Since both describes useretry: 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:
disabledentirely.isLastTableInDatabaseis gone with it. It answeredfalseboth 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 matchesTableContextMenuItems, which already hid Drop Table on an absent map without disabling anything else.Notes
canImportDatapermission gate on Import, and theisExportingCSVdisabled state on Export.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)) andf(filters) are unchanged.TableContextMenuItems) already offered both actions and is untouched.Verification
tsc -b,oxlint, anddprintare clean, and the full suite passes (2941 tests).DatabaseTableViewhad no tests; it has seven now, in a newDatabaseTableView.test.tsx:defaultOutlinetoolbar buttons shown fromxlup, and as menu entries hidden fromxlup — 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.)describe_tableseeded: the trigger isn't disabled, Export CSV is present and enabled, Import Data is present — and the grid's columns really did come fromdescribe_tablealone, so this pins a readable table whose export is dead rather than a half-loaded view.Each guard was checked by reverting it and confirming the matching tests fail: restoring the trigger's
disabledfails 2 and 3; dropping thedatabaseTablesevidence requirement fails 3 and 4; putting the buttons back to green, or droppingxl:hiddenfrom the menu entries, each fails 1.Still not exercised in a running app. The
xlbreakpoint 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