feat(media): select every visible asset with Ctrl/Cmd+A or a toolbar button - #501
Open
mostafasadeghidev wants to merge 7 commits into
Open
feat(media): select every visible asset with Ctrl/Cmd+A or a toolbar button#501mostafasadeghidev wants to merge 7 commits into
mostafasadeghidev wants to merge 7 commits into
Conversation
Three reports from one install, all the same shape: an affordance that reads as if it already honours the selection, and does not. RIGHT-CLICK IGNORED THE SELECTION. `openContextMenu` stored only the clicked asset and the delete handler acted on `contextMenu.asset` alone, so right-clicking one of five selected files and choosing Delete trashed exactly one and left four selected — no error, nothing to notice. The fix is the rule the same component already uses forty lines above, in `handleAssetDragStart`: an item inside the selection acts on the whole selection, an item outside acts on itself. The menu now shows a "5 files" header and a "Trash 5 files" label so the scope is visible before the click, and hides Rename for a multi-selection because there is one name field. Deliberately NOT adopting the clicked asset into the selection the way the site explorer does: Media derives its floating windows from the selection during render — viewer at <= 1, bulk edit at >= 2 — so writing the selection from a menu opener would pop a window open underneath the menu. THE TRASH HAD NO BULK DELETE. A selection there offered Restore and nothing else, leaving "empty the trash" a one-file-at-a-time job through the preview window. `runPurgeAll` sits beside its Trash and Restore siblings and loops the same single-id endpoint they do — no server work needed. It confirms first, counting only the trashed members: `purgeAsset` 400s on a live asset, so a mixed selection would otherwise promise more than it does. ESCAPE DID NOT CLOSE THE WINDOWS. Every other overlay in the admin takes it. These windows overlay the grid they were opened from, and the only way out was the header's close button. `useTopmostEscape` carries the settings modal's rule — only the topmost layer reacts, so a confirmation opened from inside a window owns Escape until it closes rather than collapsing the stack in one press. `FloatingWindow` uses it, which covers bulk edit, the upload queue and the agent image preview; the media viewer renders its own shell, so it wires the hook directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught what the stacking check missed. `AgentImagePreview` is a `FloatingWindow` too, and its test opens a context menu inside the preview, presses Escape, and asserts the menu closes while the preview stays open. With the window listening, one press closed both. The check only looked for `dialog` and `alertdialog`, and only for elements that follow the panel in document order. A menu is neither: it carries `role="menu"` and portals to `document.body`, so it can render before the panel it belongs to. An open menu now owns Escape wherever it sits. That is the right rule regardless of the test — closing a window out from under the menu the user just opened loses their place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…button Media had no way to select more than a range by hand. Emptying the trash meant shift-clicking from the first row to the last, and there was no shortcut and no button — while the Data workspace has both a "Select all rows" header checkbox and a bulk action bar. "All" is `visibleAssets`: whatever the folder, filter, search and trash toggle have already narrowed to. That is what every file manager means, and it is the only reading that keeps "select all, then delete permanently" in the trash from reaching live assets. Two ways in, because they fail differently. Ctrl/Cmd+A is what people try first, but only if they already know it is there; the toolbar button is what tells them. It carries the count, so the scope is visible before the click. The shortcut is document-level — the grid is a plain div with no tabindex, so a React `onKeyDown` would only fire while focus happened to sit on a tile — and stands down in two cases: inside a text field, where Ctrl/Cmd+A means select-the-text, and while any dialog is open, where it belongs to whatever that dialog contains. Selecting the grid behind a confirmation would change what the confirmed action applies to. No new workspace API: `addToSelection` already existed and already keeps the selection order list in step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bling it Two corrections to the commit before this one, both found by using it. THE SHORTCUT BARELY WORKED. The guard stood down whenever any `role="dialog"` was in the document — written for confirmation dialogs, but the media viewer window carries that role too, and selecting a single asset opens it. So Ctrl/Cmd+A was dead for almost the whole time anyone spends in Media. `role="dialog"` was the wrong test. What marks a surface as owning the keyboard is `aria-modal="true"`: `Dialog` sets it, the floating windows deliberately do not, because the grid stays usable behind them. The guard now matches that instead, which is both narrower and the thing it always meant. THE BUTTON ONLY WENT ONE WAY. Select All with no way back is half a control — the obvious second press should undo it. It now clears when the selection already covers every visible asset, reads "None" in that state, and reports `pressed` so it looks like the toggle it is. Ctrl/Cmd+A follows the same rule, so the two entry points stay one feature. "Already covers" is deliberately not an equality check: a selection made before narrowing the filter can hold ids that are no longer visible, and those should not stop the button offering to clear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dismissed THE UPLOAD WINDOW COULD NOT BE CLOSED MID-TRANSFER. The close button worked and an effect immediately undid it: the guard read `active && !uploadQueueOpen` with BOTH in its dependencies, so every close re-ran it with `open` now false and reopened the window on the next commit. While files were uploading the window simply would not go away. Keying it on the transition into `active` alone fixes it. Closing now hides the transfer rather than cancelling it, so the toolbar button carries the count — `Uploads 3/7` — which is what makes dismissing safe rather than lossy. A failed or cancelled item counts as finished, not in flight, or the count would stick mid-way forever. MINIMIZE, in the shared shell so all three windows get it from one place. Collapsing leaves the title bar and hides the body; the header's other actions go with it, since they act on content that is no longer visible. Two decisions worth stating: Collapsed windows stay WHERE THEY ARE. The position is the user's own — `useDraggablePanel` persists it — so folding to a corner would discard a choice they made, and expanding would then have nowhere honest to return to. With three windows able to collapse, a shared corner would also stack them. The close button still closes. Turning it into "minimize while busy" would make a control that does something other than what it says, which is the same defect as a keycap that animates and ignores the click. What DOES move is the upload window's DEFAULT position: bottom-left, where a browser puts its download shelf and Finder its copy progress. Default only — a stored position still wins, so a window the user has moved stays moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TWO GAPS FOUND BY LOOKING AT THE RESULT. The image viewer window had no minimize control. `FloatingWindow` grew one, which covered the upload queue, bulk edit and the agent preview — but this window builds its own shell and only borrows the drag hook, so it got nothing. Same shape as the Escape gap earlier, and the same fix: wire the control directly, with the same per-session scope and the same reason for staying put. Dialog body copy rendered at the browser's 16px default while the rest of the admin runs at 12-14px. `.body` set no `font-size`, and neither does anything above it — `globals.css` contains no `font-size` rule at all, so there is no base to inherit. Every existing caller had quietly worked around it by sizing its own children (ImportHtmlModal has five such rules, SchedulePublishDialog one), which is why it went unnoticed until a dialog shipped a bare <p>: the delete confirmation's text sat visibly larger than the panel behind it. Fixed in the primitive rather than in the caller, since the next bare <p> would land in the same hole. `--text-m` and `--text-muted` match what sibling surfaces use for secondary copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The minimize control hid the body and left the window at full size, so collapsing it produced a title bar over a tall empty pane rather than a title bar alone. `FloatingWindow` did not have this problem because its height comes from a custom property the component already overrides when collapsed. This window sets `height` to a fixed length in its own stylesheet, which no amount of hiding children can shrink. `height: auto` under `[data-minimized]` lets it size to the header that remains. `top` keeps clamping against the FULL height on purpose — using the collapsed height there would let a window pinned near the bottom edge jump upward as it folds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev
added a commit
to mostafasadeghidev/Instatic
that referenced
this pull request
Sep 6, 2026
…at had no gate The fork gate stopped at CoreBunch#359. Everything merged onto the stack since — CoreBunch#497 through CoreBunch#501, and now CoreBunch#505 — was carried with nothing to catch it being dropped by a merge resolution, which is the exact failure the gate exists for. Pinning them turned one up immediately: CoreBunch#498's guard around `removePluginVersionAssets(…, fromVersion)` is gone, because CoreBunch#359 removed that call outright — an upgrade now leaves the old version's assets on disk so published pages keep resolving. What survives of CoreBunch#498 is the rollback guard, so that is what the row pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Media had no way to select more than a range by hand. Emptying the trash meant shift-clicking from the first row to the last — there was no shortcut and no button, while the Data workspace has both a "Select all rows" header checkbox (
DataGridHeaderRow.tsx) and a bulk action bar.Stacked on #500, which is what gives the resulting selection somewhere to go — a context menu that honours it and a bulk permanent-delete.
What "all" means
visibleAssets— whatever the folder, filter, search and trash toggle have already narrowed to. That is what every file manager means by Select All, and it is the only reading that keeps "select all, then delete permanently" in the trash from reaching live assets.Two ways in, because they fail differently
Ctrl/Cmd+A is what people try first, but only if they already know it is there. The toolbar button is what tells them, and it carries the count so the scope is visible before the click.
Where the shortcut stands down
Document-level, because the grid is a plain div with no tabindex — a React
onKeyDownwould only fire while focus happened to sit on a tile. It yields in two cases:Seven tests cover exactly those boundaries, including
alertdialog, which is whatDialogrenders for the permanent-delete confirmation.No new workspace API —
addToSelectionalready existed and already keeps the selection-order list in step.Co-Authored-By: Claude Opus 5 noreply@anthropic.com