Skip to content

Commit d40ea4c

Browse files
committed
add: batch delete action for media grid
Fixes: #54016 Current implementation of bulk attachment deletion in media (grid view) initiates an AJAX request per attachment. This patch implements a new AJAX action for bulk post deletion and rewrites the logic for bulk deletion in frontend.
1 parent 32642e5 commit d40ea4c

4 files changed

Lines changed: 123 additions & 11 deletions

File tree

src/js/media/models/attachment.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,53 @@ Attachment = Backbone.Model.extend(/** @lends wp.media.model.Attachment.prototyp
163163
get: _.memoize( function( id, attachment ) {
164164
var Attachments = wp.media.model.Attachments;
165165
return Attachments.all.push( attachment || { id: id } );
166-
})
166+
}),
167+
168+
/**
169+
* Delete multiple attachments in a single batched AJAX request.
170+
*
171+
* Sends attachment IDs and their per-item nonces to the
172+
* `delete-post-batch` endpoint, splitting into chunks of `batchSize`.
173+
* On success, marks each successfully deleted model as destroyed and
174+
* fires its `destroy` event so collections update automatically.
175+
*
176+
* @since 7.1.0
177+
* @static
178+
*
179+
* @param {wp.media.model.Attachment[]} models Array of Attachment models to delete.
180+
* @param {number} [batchSize=50] Max items per request.
181+
* @return {jQuery.Promise} Resolves with 0 or 1 for failure and success respectively.
182+
*/
183+
batchDestroy: function( models, batchSize ) {
184+
batchSize = batchSize || 50;
185+
186+
var promises = [],
187+
i, slice, ids, nonces;
188+
189+
for ( i = 0; i < models.length; i += batchSize ) {
190+
slice = models.slice( i, i + batchSize );
191+
ids = [];
192+
nonces = {};
193+
194+
_.each( slice, function( model ) {
195+
ids.push( model.id );
196+
nonces[ model.id ] = model.get( 'nonces' )['delete'];
197+
});
198+
199+
promises.push( wp.media.post( 'delete-post-batch', {
200+
ids: ids,
201+
nonces: nonces
202+
}) );
203+
}
204+
205+
return $.when.apply( null, promises ).then( function() {
206+
_.each( models, function( model ) {
207+
model.destroyed = true;
208+
model.stopListening();
209+
model.trigger( 'destroy', model, model.collection );
210+
});
211+
});
212+
}
167213
});
168214

169215
module.exports = Attachment;

src/js/media/views/attachments/browser.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
234234
priority: -80
235235
}).render() );
236236
}
237-
237+
238238
var dateFilter, dateFilterLabel, dateFilterContainer;
239239
/*
240240
* Feels odd to bring the global media library switcher into the Attachment browser view.
@@ -294,9 +294,10 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
294294
controller: this.controller,
295295
priority: -80,
296296
click: function() {
297-
var changed = [], removed = [],
297+
var changed = [], removed = [], destroy = [],
298298
selection = this.controller.state().get( 'selection' ),
299-
library = this.controller.state().get( 'library' );
299+
library = this.controller.state().get( 'library' ),
300+
spinner = this.controller.content.get().toolbar.get( 'spinner' );
300301

301302
if ( ! selection.length ) {
302303
return;
@@ -328,7 +329,7 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
328329
changed.push( model.save() );
329330
removed.push( model );
330331
} else {
331-
model.destroy({wait: true});
332+
destroy.push( model );
332333
}
333334
} );
334335

@@ -339,6 +340,14 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
339340
library._requery( true );
340341
this.controller.trigger( 'selection:action:done' );
341342
}, this ) );
343+
} else if ( destroy.length ) {
344+
this.controller.trigger( 'selection:action:done' );
345+
spinner.show();
346+
wp.media.model.Attachment.batchDestroy( destroy ).always( function() {
347+
spinner.hide();
348+
}).fail( function() {
349+
window.alert( l10n.errorDeleting );
350+
});
342351
} else {
343352
this.controller.trigger( 'selection:action:done' );
344353
}
@@ -356,7 +365,8 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
356365
click: function() {
357366
var removed = [],
358367
destroy = [],
359-
selection = this.controller.state().get( 'selection' );
368+
selection = this.controller.state().get( 'selection' ),
369+
spinner = this.controller.content.get().toolbar.get( 'spinner' );
360370

361371
if ( ! selection.length || ! window.confirm( l10n.warnBulkDelete ) ) {
362372
return;
@@ -376,11 +386,13 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
376386
}
377387

378388
if ( destroy.length ) {
379-
$.when.apply( null, destroy.map( function (item) {
380-
return item.destroy();
381-
} ) ).then( _.bind( function() {
382-
this.controller.trigger( 'selection:action:done' );
383-
}, this ) );
389+
this.controller.trigger( 'selection:action:done' );
390+
spinner.show();
391+
wp.media.model.Attachment.batchDestroy( destroy ).always( function() {
392+
spinner.hide();
393+
}).fail( function() {
394+
window.alert( l10n.errorDeleting );
395+
});
384396
}
385397
}
386398
}).render() );

src/wp-admin/admin-ajax.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
'delete-link',
6565
'delete-meta',
6666
'delete-post',
67+
'delete-post-batch',
6768
'trash-post',
6869
'untrash-post',
6970
'delete-page',

src/wp-admin/includes/ajax-actions.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,59 @@ function wp_ajax_delete_post( $action ) {
893893
}
894894
}
895895

896+
/**
897+
* Handles deleting multiple posts via a single AJAX request.
898+
*
899+
* Accepts an array of post IDs and their corresponding nonces,
900+
* validates each individually, and deletes them. If any single
901+
* deletion fails, it continues to the next post without stopping
902+
* but returns a failure code. It returns a success code only if
903+
* all deletions succeed.
904+
*
905+
* @since 7.1.0
906+
*/
907+
function wp_ajax_delete_post_batch() {
908+
$ids = isset( $_POST['ids'] ) ? array_map( 'intval', (array) $_POST['ids'] ) : array();
909+
$nonces = isset( $_POST['nonces'] ) ? (array) $_POST['nonces'] : array();
910+
911+
if ( empty( $ids ) ) {
912+
wp_die( 0 );
913+
}
914+
915+
$failed = false;
916+
917+
foreach ( $ids as $id ) {
918+
$id = (int) $id;
919+
920+
if ( $id <= 0 ) {
921+
continue;
922+
}
923+
924+
$nonce = isset( $nonces[ $id ] ) ? $nonces[ $id ] : '';
925+
if ( ! wp_verify_nonce( $nonce, "delete-post_$id" ) ) {
926+
$failed = true;
927+
continue;
928+
}
929+
930+
if ( ! current_user_can( 'delete_post', $id ) ) {
931+
$failed = true;
932+
continue;
933+
}
934+
935+
if ( get_post( $id ) ) {
936+
if ( ! wp_delete_post( $id ) ) {
937+
$failed = true;
938+
}
939+
}
940+
}
941+
942+
if ( $failed ) {
943+
wp_die( 0 );
944+
} else {
945+
wp_die( 1 );
946+
}
947+
}
948+
896949
/**
897950
* Handles sending a post to the Trash via AJAX.
898951
*

0 commit comments

Comments
 (0)