Skip to content

feat(media): let the multi-selection reach the actions that imply it - #500

Open
mostafasadeghidev wants to merge 4 commits into
CoreBunch:mainfrom
mostafasadeghidev:feat/media-multi-select-actions
Open

mostafasadeghidev wants to merge 4 commits into
CoreBunch:mainfrom
mostafasadeghidev:feat/media-multi-select-actions

Conversation

@mostafasadeghidev

@mostafasadeghidev mostafasadeghidev commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

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. 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 used in handleAssetDragStart: an item inside the selection acts on the whole selection, an item outside acts on itself. It now lives in utils/actionTargets.ts, and both the drag and the menu call it, so they can't drift apart. 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 SiteExplorerPanel does. Media derives its floating windows from the selection during render — viewer at <= 1, bulk edit at >= 2 (MediaPage.tsx) — 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, so no server work is needed. It confirms first, and both the count and the purge cover only the trashed members: purgeAsset 400s on a live asset.

A permanent delete from the menu asks first

In the trash view the menu acts on the whole selection too, so it could purge N files in one click. It now always confirms, through the admin's ConfirmDeleteProvider with alwaysConfirm — the confirmBeforeDelete preference defaults off and governs reversible trashing, not this. A single file asks as well, worded as #497 words it, so whichever of the two lands second resolves to the same code.

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.

Tests

multiSelectActions.test.tsx renders MediaCanvas and BulkEditWindow inside the real session and confirm providers and drives the menu, the drag and the dialog — no copies of the logic. Each case was checked against a deliberately broken component: no confirmation, a menu or drag that ignores the selection, a bulk purge over the whole selection. Every one turns the suite red.

Writing them found a bug the copies hid: the bulk dialog counted only trashed files, but the purge loop ran over the whole selection, firing a request per live file that was bound to 400. Fixed in the same commit.

bun test src/__tests__/media/   41 pass, 0 fail
tsc -b, eslint                  clean

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

mostafasadeghidev and others added 2 commits September 5, 2026 13:59
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>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
The conflicts here are history rather than content: the stack took CoreBunch#507's
warning through its own commits, combined with CoreBunch#500 and CoreBunch#501, so both sides
look new to git. Each file was settled by what CoreBunch#507's commits since then
actually changed:

  - MediaCanvas, MediaViewerWindow, the confirmation test, mediaPrefetch —
    untouched by those commits, so the stack's copies already hold all of
    CoreBunch#507 plus the multi-select and minimize work. Kept.
  - contentUsage.ts and its test — the branch-aware lookup is new, so
    CoreBunch#507's version is taken, with the stack's four-argument walker
    (`rootNodeId` scopes hole and loop fragments here).
  - usageWarning copy + tests, the ref schema, docs — merged cleanly.

Before choosing a side, the stack's copies of both test files were checked
against CoreBunch#507's pre-sync versions: identical, so nothing stack-only is lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev and others added 2 commits October 16, 2026 20:09
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents

Review on CoreBunch#501 named two problems in this PR, both real:

- The trash view's context menu purged a whole selection with no
  confirmation. A purge from the menu now always asks first, through the
  admin's one ConfirmDeleteProvider with `alwaysConfirm` — the
  `confirmBeforeDelete` preference defaults off and governs reversible
  trashing, not this. A single file asks too, worded as CoreBunch#497 words it, so
  whichever of the two lands second resolves to the same code.
- The tests asserted on local copies of the logic. They now render
  MediaCanvas and BulkEditWindow inside the real session and confirm
  providers and drive the menu, the drag and the dialog. Each was checked
  against a deliberately broken component: no confirmation, a menu or a
  drag that ignores the selection, and a bulk purge of the whole selection
  — every one turns the suite red.

Writing those tests found a bug the copies hid: the bulk window's dialog
counted only the trashed members of a mixed selection, but the purge loop
ran over all of them, firing a `purgeAsset` for every live file — an
endpoint that 400s on an asset that was never soft-deleted. The purge now
runs on exactly the files the dialog counts.

The selection rule itself moves to `utils/actionTargets.ts`, and both the
drag and the menu call it, so they cannot drift apart; the test that
compared two copies of it is gone with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Oct 16, 2026
…e that skips live files

Review asked for two things; the stack already had the first and lacked
the second.

- The menu's permanent delete already confirmed here (CoreBunch#497 + CoreBunch#507's usage
  warning). Kept that flow, awaiting the usage lookup before the dialog
  opens, and took CoreBunch#500's wording for a multi-file purge ("Delete 3 files").
- The bulk window's purge looped over the WHOLE selection while its
  dialog counted only the trashed members, firing a 400-bound purgeAsset
  for every live file. That bug was live on this branch; it now purges
  exactly `purgeable`, and the usage lookup reads the same list.

The selection rule moves to `utils/actionTargets.ts`. Here it stays behind
`useMediaSelection` (the stack's extraction), whose `targetsFor` now
delegates to it, so the drag and the menu still share one rule.

CoreBunch#500's component tests take the stack's async dialog: the workspace stub
gains `lookupUsage`, and the dialog assertions wait for it to open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Oct 16, 2026
…tests

The stack already carried CoreBunch#501's minimize code, identical to CoreBunch#660's; the
two conflicts were lines CoreBunch#660 does not have (CoreBunch#500's useTopmostEscape, the
viewer's purge-confirmation state from CoreBunch#497/CoreBunch#507), kept as they were, and
the merge's second `minimizable` attribute on two windows is dropped. What
this adds is CoreBunch#660's docs and its tests, which render the real windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mostafasadeghidev

Copy link
Copy Markdown
Contributor Author

I've updated this one based on your comments in #501.

Deleting from the trash through the context menu now always asks for confirmation, both for a single file and for a selection. I used the same wording as #497 for the single-file case, so the two should fit together fine whichever one goes in first.

The tests now render MediaCanvas and BulkEditWindow and go through the menu, the drag and the confirm dialog, instead of checking local copies of the logic. I also broke the components on purpose to make sure the tests actually fail when they should.

While doing that I found a real bug: the bulk delete dialog only counted the trashed files, but the purge was sent for every file in the selection, including ones that weren't trashed. It now only deletes what the dialog shows.

This branch has not been deployed

No deployments
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.

1 participant