refactor(export): UI/UX, native and HIG audit of the import and export surface - #2631
Merged
Conversation
…across the transfer sheets Fixes the audit's confirmed findings in the import/export surface. Export object tree - Every row refused selection, so the tree had no keyboard navigation at all: no arrow keys, no Space, no Left/Right, and nothing for VoiceOver to move through. Rows are selectable now and an NSOutlineView subclass routes Space to the same toggle a click performs. - Recycled cells swapped their hosting view's root without changing its identity, so a row inherited the previous object's @State: the row-scope popover offered the last table's columns and wrote them into this table's SELECT. Each row now carries .id(node.identity). - A checkbox toggle reloaded every row in the database. Only the rows a change can redraw are reloaded now: the object's own row and the containers above it. - rowSizeStyle was .default, shorter than the content each row draws. Matches the 24pt the app's two other hosted outline views name. - The loader computed isExpanded per container and nothing read it, so every database on the server opened at once. - A mixed object checkbox turned its row off while the identical control on the group above it turned everything on. - Checkboxes were unnamed to VoiceOver, and TristateCheckbox already took a label. Transfer - The destination Database picker was bound to a value nothing read: rows landed in whichever database the destination connection was last parked on. Both the column read and the write go through a scope built from the picker now. - A finished transfer closed its sheet and reported nothing, warnings included. - The confirmation was hand-built, so Return sat on the destroying button and Escape was inferred from a button title that stops matching once localized. Routed through AlertHelper, which exists for exactly that. - A Stop pressed during the pre-transfer column read was cleared by the service's own reset before it could be seen. Server-side export - Oracle Data Pump interpolated the directory, table and schema names into PL/SQL literals with no escaping, while the Snowflake and BigQuery arms escaped theirs. - The run could not be cancelled or escaped. - The destination field was labelled with its own example value. Elsewhere - Restore Dump replayed into a database with no confirmation. - Stopping an import applied at once while stopping an export asked first. - A streaming query export drew a determinate bar at zero and "N/0 rows". - Parquet was in neither the format order nor the description table, so it sorted arbitrarily and showed no description. Both now live in ExportFormatCatalog, which falls back to a plugin's own description for a format it does not know. - Fake plurals ("%d table(s)") replaced with explicit keys or SwiftUI inflection. - Cancel moved beside the action button, through the repo's own DialogFooter. - Export, transfer and server-side export sheets resize. - The backup flow's save panel and password warning followed NSApp.keyWindow. - sheetContent(for:) split: the eight transfer sheets moved to their own file. Claude-Session: https://claude.ai/code/session_011EqgjCjCAU6tiiVmnMpF86
…nd name every option control The rest of the audit's confirmed findings. Import - The sheet edits the plugin's own persistent options in place and never snapshotted them, so Cancel kept every change and `Delete existing rows` stayed armed for the next import from anywhere in the app. - Field detection reads and parses the whole file, on the main actor, so the sheet's own spinner could not draw a frame. Only the parse moves off it. - A file the plugin cannot read is a failure, not an empty result: it showed the parser's message as grey placeholder text with nothing to press but Cancel. - The column type chooser drew its own checkmark inside a Menu instead of using a Picker. The selection resolves case-insensitively so a differently-cased stored type still selects its own row. - A stopped import said nothing about the statements it had already committed. Backup and restore - A failed restore leaves the target part-way through the dump. Only a cancelled one said so; the failure arm said nothing. - The save panel and the SqlPackage password warning followed NSApp.keyWindow, which during a sheet transition is not this flow's own window. Server-side export - Oracle's Data Pump block ends in DETACH, so the statement returns before anything is written. Reporting it as written was wrong. Strings - Fifteen plugin option pickers passed an empty label, so VoiceOver read every one of them as an unnamed pop-up button. - Parquet's codec names went through String(localized:). Snappy and Zstd are product names. - Candidate binaries were joined with a localized " or " fragment, which gets the three-candidate case wrong in most locales. Uses .formatted(.list(type: .or)). - A notification body carried raw multi-line stderr, head-truncated, so the sentence naming the cause fell below the cut. Flattened before it is cut. Claude-Session: https://claude.ai/code/session_011EqgjCjCAU6tiiVmnMpF86
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A UI/UX, native-macOS and HIG audit of the whole import and export surface, and the fixes for what it found. Seven auditors covered the export tree, the import sheets, the transfer and server-side sheets, sheet and window conventions, keyboard and accessibility, progress and error reporting, and user-facing strings. Every finding was then handed to an adversarial reviewer told to refute it: 34 survived, 20 did not.
Nothing here changes PluginKit, so there is no ABI question.
The three that would have cost a user data or time
The transfer's destination Database picker was inert. It was bound to a value nothing else read. Rows landed in whichever database the destination connection happened to be parked on, and with
Delete existing rows firstticked the DELETE landed there too. Both the column read and the write now go through aDatabaseScopebuilt from the picker.Oracle Data Pump interpolated names into PL/SQL literals unescaped. The Snowflake and BigQuery arms both take an escaping function; the Oracle one was never handed it. An Oracle identifier may legally hold a quote, and a raw one closes the outer literal early and hands the rest of the name to the parser. The
NAME_EXPRandSCHEMA_EXPRfilters sit inside two levels of quoting, so a quote is escaped twice. Fixing it also fixed the session-user fallback, which had been writing|| USER ||inside the literal, so it had never resolved to anything.Restore Dump replayed into a database with no confirmation. Picking a database in the list was the last step before the first write, behind a button whose ellipsis promised another one.
Export object tree
NSOutlineViewsubclass routes Space to the same toggle a click performs.@State. The row-scope popover offered the last table's columns and wrote them into this table's SELECT. Each row now carries.id(node.identity).rowSizeStylewas.default, shorter than the content each row draws; the app's two other hosted outline views both name 24pt.isExpandedper container and nothing read it, so every database on the server opened at once.TristateCheckboxalready took a label.Select Noneset the column set to empty, which this editor reads as every column, so the button did nothing.commit()also erased a stored subset whenever the column fetch came back empty.Reporting
DETACH, so the statement returns before anything is written. It was reported as written.N/0 rowslabel for the whole run, and named itself(0/1).Conventions
AlertHelperexists for exactly that and documents both traps.DialogFooter.NSApp.keyWindow, which during a sheet transition is not the flow's own window.RowImportSheetnever snapshotted the plugin's persistent options, so Cancel kept them andDelete existing rowsstayed armed for the next import from anywhere in the app.ExportFormatCatalog, which falls back to a plugin's own description for a format it does not know, so a registry format installed after this build still lands somewhere sensible.Refactors
sheetContent(for:)was 166 lines and over the SwiftLint limit. The eight transfer sheets moved toMainContentView+TransferSheets.swift.ExportDialogforExportFormatCatalog.%d table(s),table\(n == 1 ? "" : "s")) replaced with explicit singular and plural keys, or SwiftUI's^[](inflect:)where aTextcan carry it.String(localized:)returns that markup verbatim, whichOperationCompletionCopyalready documents.Verification
swiftlint --strictoverTablePro,PluginsandTableProTests: 0 violations. Two pre-existing warnings onmain(BackupDatabaseFlowline length,MainContentViewfunction length) are gone as a side effect.ExportFormatCatalogTests(5), plus 4 Oracle escaping cases inServerSideExportTests. 24/24 in that group, 59/59 across the nine export and transfer suites.AllPluginsfails locally on the knownoracle-nio@TaskLocalmacro issue, unrelated to this change; CI covers it.docs: house style and source claims agree.Not covered: no UI automation for the export tree's new keyboard handling. The tree is an
NSOutlineViewbehind anNSViewRepresentableand XCUITest reads its rows through the hosted SwiftUI, so a Space-toggles-the-selected-row test would assert on the checkbox's accessibility value rather than on the model. Worth doing, but it belongs with a broader pass over the export dialog's automation, which has none today.https://claude.ai/code/session_011EqgjCjCAU6tiiVmnMpF86