Store media library sidebar state per user and move to folder bulk action support - #2106
Store media library sidebar state per user and move to folder bulk action support#2106KMchaudhary wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Concurrent toggle requests can persist an outdated state when responses are processed out of order.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Persists each user’s media-library sidebar visibility across sessions.
Changes:
- Adds a REST endpoint backed by user metadata.
- Initializes the sidebar from localized preference data.
- Saves state whenever users toggle the sidebar.
File summaries
| File | Description |
|---|---|
pages/media-library/redux/api/folders.js |
Adds the preference mutation. |
pages/media-library/App.js |
Loads and saves sidebar state. |
inc/classes/rest-api/class-media-library.php |
Implements persistence and REST handling. |
inc/classes/class-assets.php |
Localizes the saved preference. |
assets/src/js/media-library/index.js |
Applies the collapsed class before insertion. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| // Fire-and-forget: the sidebar has already moved, and a failed save just means | ||
| // the next page load falls back to the previously saved state. | ||
| updateSidebarPreference( newHidden ); |
There was a problem hiding this comment.
Resolved in 48ab1f9. The save is now debounced (500ms): a burst of rapid toggles coalesces into a single write of the latest value, so there is no longer an out-of-order POST that could persist stale state, and a pending save is flushed on unmount so the last toggle isn't dropped.
🔍 WordPress Plugin Check Report
📊 Report
|
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
missing_composer_json_file | The "/vendor" directory using composer exists, but "composer.json" file is missing. |
📁 readme.txt (2 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
mismatched_plugin_name | Plugin name "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" is different from the name declared in plugin header "GoDAM". |
0 |
trademarked_term | The plugin name includes a restricted term. Your chosen plugin name - "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" - contains the restricted term "wordpress" which cannot be used at all in your plugin name. |
📁 assets/build/blocks/godam-gallery-v2/render.php (2 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
15 |
WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound | Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$inner_block_video_ids". |
23 |
WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound | Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$inner_block_video_ids". |
📁 assets/build/css/main.css (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
EnqueuedStylesScope | This style is being loaded in all contexts. |
📁 assets/src/libs/analytics.min.js (5 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
EnqueuedScriptsScope | This script is being loaded in all frontend contexts. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880 (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/2026/09/09/demo-post-post/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/demo-page-post/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/demo-attachment-post/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead. |
📁 assets/build/js/main.min.js (5 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
EnqueuedScriptsScope | This script is being loaded in all frontend contexts. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880 (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/2026/09/09/demo-post-post/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/demo-page-post/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead. |
0 |
NonBlockingScripts.NoStrategy | This script on http://localhost:8880/demo-attachment-post/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead. |
🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Dragging onto a folder row was the only way to file media, and jQuery UI drag does not respond to touch, so this was impossible on mobile. Adds a 'Move to folder' action to the grid toolbar, the list-view bulk actions, the attachment details sidebar and the folder context menu, all sharing one useMoveAttachments hook with the existing drag.
…o fix/media-library-ui-ux
ReviewSolid, careful work. The cross-bundle event bridge is a clean way to let the Backbone Worth fixing before merge1. A failed move in list view shows no error, and a partial move looks like a success In the Fix: build the fail notice once and pass it through, so list view keeps it across the reload: const failNotice = { message: error?.data?.message || error?.message || __( 'Failed to move items', 'godam' ), type: 'fail' };
dispatch( updateSnackbar( failNotice ) );
refreshAfterMove( { notice: failNotice } );The grid path ignores the notice argument, so this only changes list-view behaviour. 2. The single-item "Folder" control only works in the upload.php modal Two separate gaps, both confirmed against WordPress core:
Please verify the control both appears and is styled in the post-editor media modal and the Edit Media screen, not just on upload.php. 3. Duplicate DOM id between the details control and the modal radios Both build ids from the same Fix: give the modal radios their own prefix, e.g. Nice to fix4. Picking a search-only folder, then clearing the search, leaves an invisible selection
5. Rapid sidebar toggles can persist the wrong final state (matches the Copilot note) Each toggle fires an independent save with no ordering, so two quick toggles can commit out of order and leave the stored value disagreeing with what the user last chose. The live UI is always right; only the next page load's first paint reads the stale value, and the next toggle corrects it. Fix: debounce the save (a trailing 300ms is enough for one boolean) or drop stale responses. 6. Toolbar button leaks event handlers
7. Snackbar can show twice when two sidebar roots are alive The toast now renders into Optional notes
Checked and cleanFor scope: the new REST endpoint is fine (gated on |
…y-ui-ux # Conflicts: # pages/media-library/App.js # pages/media-library/components/context-menu/ContextMenu.jsx
Rapid sidebar toggles each fired an independent fire-and-forget POST, so responses could be applied out of order and leave the stored per-user flag disagreeing with the UI's final state. Coalesce toggles into a single debounced write of the latest value, keeping only one request in flight, and flush any pending save on unmount so the last toggle is never dropped. Addresses Copilot review feedback on PR #2106.
subodhr258
left a comment
There was a problem hiding this comment.
Re-review
Since the last review only one commit changed this PR: 48ab1f9, the debounced sidebar save. That fix is correct in what it set out to do, but six of the seven items from the previous round are untouched, including the two functional gaps, and the debounce introduces one new way to lose the preference. Holding off on approval for those.
Fixed
Rapid sidebar toggles persisting the wrong final state (previous item 5, and the Copilot note). persistSidebarPreference now coalesces a burst into one write of the latest value and flushes on unmount. Good.
One small caveat: the comment says "only ever one request is in flight", which is not quite true. Two toggles more than 500ms apart still produce two independent, unordered POSTs. The window is now small enough that this is fine in practice, so this is only about the comment.
New, introduced by the debounce
A pending save is dropped when the page navigates within 500ms
pages/media-library/App.js:159
The flush only runs on React unmount, and a full page navigation never unmounts React. On upload.php?mode=list there is no .media-frame, so triggerFilterChange takes the URL branch and sets window.location.href. Collapse the sidebar and click a folder straight after: the timer dies with the page and nothing is saved. A list-view move takes the same path, since refreshAfterMove reloads.
Before this commit the POST went out immediately, so this is a regression in a fairly ordinary flow. Flush on pagehide, or keep the immediate write and discard stale responses instead (the other option from the last review).
Still open from the last review
1. A failed move in list view shows no error, and a partial move looks like a success
pages/media-library/hooks/useMoveAttachments.js:198
refreshAfterMove() in the catch block still takes no argument, so the reload discards the fail toast that was just dispatched. Build the notice once and pass it through.
2. The single-item "Folder" control still only renders in the two-column modal
assets/src/js/media-library/views/fields/move-to-folder-field.js:128
Confirmed again against core: .settings exists only in tmpl-attachment-details-two-column. The single-column tmpl-attachment-details puts its span.setting rows as direct children, so the early return fires and the row never appears in the post editor's Add Media / Featured Image sidebar. Worth flagging that the PR description says the control now appears "in both the two-column attachment details modal and the sidebar", and the SCSS added for .media-sidebar .setting.godam-move-to-folder-setting targets a row that is never created. Fall back to view.el when .settings is absent, and insert before .attachment-compat only when that child exists.
3. The stylesheet is still enqueued on the upload screen only
inc/classes/class-assets.php:359
The script loads wherever godam_should_load_media_library_assets() is true (post, page, attachment, widgets, site-editor), so the toolbar button is created on those screens without media-library.css. Enqueue the style on the same gate.
4. Duplicate DOM id between the details control and the picker radios
pages/media-library/components/modal/MoveToFolderModal.jsx:207
Both still use the godam-move-to-folder- prefix over two different id sequences, so a folder whose term id matches the open attachment's id cannot be picked by clicking its label. Give the radios their own prefix.
5. Picking a search-only folder, then clearing the search, leaves an invisible selection
pages/media-library/components/modal/MoveToFolderModal.jsx:118
6. The toolbar button still leaks event handlers
assets/src/js/media-library/views/filters/media-move-to-folder.js:33
Three this.controller.on(...) bindings with no teardown. bindEvents() in attachment-browser.js documents this exact trap ten lines away and uses this.listenTo.
7. The snackbar can show twice, and now so can the picker
pages/media-library/components/folder-tree/SnackbarComp.jsx:42
Worth upgrading from cosmetic. MoveToFolderModal reads the same shared store and is rendered by every mounted app, so with two live sidebars (two media frames on a post edit screen, or the 100ms window the modal-close cleanup leaves open) you get two stacked dialogs with two focus traps, not just a doubled toast. SidebarRootContext is already threaded to every app and already used by useMoveToFolderBridge; gate the modal and the toast on it too.
New, smaller
Breakpoint disagrees with the CSS at exactly 900px
pages/media-library/App.js:51, assets/src/js/media-library/index.js:71
innerWidth < 900 against max-width: 900px, which is inclusive. At exactly 900px the overlay layout renders but the code treats it as desktop, so the sidebar is not force-collapsed and expanding it overwrites the saved desktop preference, which is the one thing the desktop-only guard exists to prevent. The literal also sits in three places. window.matchMedia( '(max-width: 900px)' ) removes both problems.
Folder search caps at 20 results with no way to page
pages/media-library/components/modal/MoveToFolderModal.jsx:59
Pinned to page: 1, perPage: 20, and canLoadMore is false while searching. On a site with many folders a match that sorts twenty-first cannot be selected, and nothing tells the user results were truncated.
Duplicated checked-row selector
assets/src/js/media-library/list-view-bulk-actions.js:90
checkedIds() repeats the query getSelectedAttachmentIds() already does in data/media-grid.js:326. This file already imports across the bundle boundary, so the shared helper is reachable.
Unchanged notes
Still no tests for any of the new logic, and no data-test-id on the picker, the three-dot toggle or the Folder control. The three-dot button still declares aria-haspopup="menu" without aria-expanded, over a popup that has no menu roles. canMoveTo is still returned by the hook and unused outside it while the modal recomputes the same lock rule inline.
Checked and clean
The debounce itself, the develop merge, and everything I signed off last time still hold up: the REST endpoint stays gated on upload_files and only touches the current user's meta, the Media_Library import into class-assets.php does not collide with the RTGODAM\Inc\Media_Library namespace already used there, the compat-fields.js extraction is behaviour-preserving (Attachment.render replaces $el HTML on every render, so the new table reuse cannot accumulate rows), the toolbar priority reasoning at -72 is right, and the selection.reset() in refreshAfterMove only ever runs on paths that moved the whole selection, so it costs nothing. CI is green apart from the POT check, which is expected during development.
Regression from the previous debounce commit: - Serialize the sidebar-preference save instead of debouncing it. The first toggle is sent immediately and later toggles only re-send on settle if the value changed, so nothing is left on a timer to lose when a list-view move or a folder click on upload.php reloads the page, while still never letting an older state overwrite a newer one. Move-to-folder fixes: - Pass the failure notice through refreshAfterMove() in the catch path so a failed/partial list-view move reports the error instead of a silent reload. - Render the single-item Folder control in the single-column attachment details view (post editor sidebar), not just the two-column modal. - Enqueue media-library.css on the same gate as the script, so the toolbar button and folder controls are styled on every screen the script loads, not only the upload screen. - Give the picker radios their own id prefix so a folder id that matches the open attachment id can no longer collide with the details control. - Clear a search-only selection when it leaves the visible list, so Move can't act on an invisible target. - Page search results (load more) instead of capping silently at 20. - Elect a single owner sidebar (first mounted root) to render the picker and the toast, so two live sidebars no longer stack two dialogs/toasts. Smaller: - Use a max-width media query for the 900px mobile breakpoint in both JS spots, matching the inclusive CSS breakpoint. - Reuse getSelectedAttachmentIds() in the list-view bulk action instead of duplicating the checked-row query. - Stop returning the internal canMoveTo from useMoveAttachments; add aria-expanded + menu roles to the folder menu button/popup; add data-test-id to the picker, submit, Folder control and three-dot toggle.
Fixes: #733, #1584
This pull request introduces several improvements and new features to the media library, with a focus on supporting folder organization and enhancing the user interface for moving media items into folders. The most significant changes include the addition of bulk "Move to folder" actions in the list view, improved sidebar and modal controls for folder assignment, and code refactoring to support these features in a modular way.
Bulk "Move to folder" and folder assignment UI improvements:
Bulk Move to Folder in List View:
Added a new
ListViewBulkActionsclass that injects a "Move to folder…" option into the bulk actions dropdown in the media list view. This feature intercepts the bulk action submit event and opens a folder picker UI instead of submitting the form, allowing users to move multiple items at once. [1] [2] [3]Move to Folder Button Styling and Accessibility:
Improved the styling and placement of the "Move to folder" button for both touch and desktop devices. The button is now always reachable on touch devices and visually consistent with WordPress controls in the sidebar and attachment details modals.
Media sidebar and modal enhancements:
Attachment Details Sidebar and Modal Integration:
Refactored the rendering of the folder assignment control so it appears consistently in both the two-column attachment details modal and the sidebar, regardless of how the view is constructed. This ensures users can always assign or move folders from any details view. [1] [2] [3] [4] [5] [6]
Toolbar Integration for Grid View:
Added the "Move to folder" button to the grid view's toolbar with a specific priority, ensuring it appears in a logical order relative to other bulk actions and controls. [1] [2]
Code organization and maintainability:
Extracted the logic for building custom fields and tables in the attachment details sidebar into a new module (
fields/compat-fields.js). This allows different features to add rows to the same sidebar table without duplicating code or creating redundant tables. [1] [2]Responsive and user preference improvements:
The sidebar's initial collapsed state now correctly reflects either the user's saved preference or the current screen width, ensuring a smooth experience on both mobile and desktop devices. [1] [2]
Minor UI Fixes:
Ensured background SVG icons in the folder tree are not repeated, fixing a visual bug.
Demo
Screen.Recording.2026-09-02.at.12.11.38.PM.mov
Screen.Recording.2026-09-03.at.1.36.56.PM.mov
Screen.Recording.2026-09-03.at.1.40.00.PM.mov