Media Grid, support MEDIA_TRASH:

* Add a setting to `_wpMediaViewsL10n.settings`: `mediaTrash`
* In the attachment edit modal, properly toggle between Trash/Untrash
* In `media.view.Attachment`, add a method for `untrashAttachment`
* When creating the grid toolbar, switch the setting order of subviews so that `media.view.DeleteSelectedButton` can listen to the instance of `media.view.AttachmentFilters.All` to update the text in its UI.
* Add a new filter to `media.view.AttachmentFilters.All`, `trash`, when `settings.mediaTrash` is true
* Allow the cached queries in `Query.get()` to be flushed when race conditions exist and collections need to be refreshed. This is currently only being used when `MEDIA_TRASH` is set, to refresh the filtered/mirrored collections related to `all`, `trash`, and any already queried filter.
* Cleanup the bootstrapping of `media.view.MediaFrame.Manage`
* Allow `wp_ajax_query_attachments()` to return items from the trash when `MEDIA_TRASH` is `true`
* Allow `wp_ajax_save_attachment()` to set `post_status` when `MEDIA_TRASH` is `true`. It allows `wp_delete_post()` to be called, which will trash the attachment instead of deleting when the flag is set.

Props koop for the knowledge sharing and thought partnership.
See #29145.

Built from https://develop.svn.wordpress.org/trunk@29490


git-svn-id: http://core.svn.wordpress.org/trunk@29268 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Scott Taylor 2014-08-14 18:31:19 +00:00
parent 5fc736f49e
commit 571111022e
13 changed files with 223 additions and 75 deletions

View File

@ -2161,7 +2161,14 @@ function wp_ajax_query_attachments() {
) ) ); ) ) );
$query['post_type'] = 'attachment'; $query['post_type'] = 'attachment';
$query['post_status'] = 'inherit'; if ( MEDIA_TRASH
&& ! empty( $_REQUEST['query']['post_status'] )
&& 'trash' === $_REQUEST['query']['post_status'] ) {
$query['post_status'] = 'trash';
} else {
$query['post_status'] = 'inherit';
}
if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) )
$query['post_status'] .= ',private'; $query['post_status'] .= ',private';
@ -2216,6 +2223,9 @@ function wp_ajax_save_attachment() {
if ( isset( $changes['description'] ) ) if ( isset( $changes['description'] ) )
$post['post_content'] = $changes['description']; $post['post_content'] = $changes['description'];
if ( MEDIA_TRASH && isset( $changes['status'] ) )
$post['post_status'] = $changes['status'];
if ( isset( $changes['alt'] ) ) { if ( isset( $changes['alt'] ) ) {
$alt = wp_unslash( $changes['alt'] ); $alt = wp_unslash( $changes['alt'] );
if ( $alt != get_post_meta( $id, '_wp_attachment_image_alt', true ) ) { if ( $alt != get_post_meta( $id, '_wp_attachment_image_alt', true ) ) {
@ -2243,7 +2253,12 @@ function wp_ajax_save_attachment() {
} }
} }
wp_update_post( $post ); if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) {
wp_delete_post( $id );
} else {
wp_update_post( $post );
}
wp_send_json_success(); wp_send_json_success();
} }

View File

@ -1603,7 +1603,8 @@
.attachment-info .edit-attachment, .attachment-info .edit-attachment,
.attachment-info .refresh-attachment, .attachment-info .refresh-attachment,
.attachment-info .delete-attachment, .attachment-info .delete-attachment,
.attachment-info .trash-attachment { .attachment-info .trash-attachment,
.attachment-info .untrash-attachment {
display: block; display: block;
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
@ -1620,12 +1621,14 @@
} }
.media-modal .delete-attachment, .media-modal .delete-attachment,
.media-modal .trash-attachment { .media-modal .trash-attachment,
.media-modal .untrash-attachment {
color: #bc0b0b; color: #bc0b0b;
} }
.media-modal .delete-attachment:hover, .media-modal .delete-attachment:hover,
.media-modal .trash-attachment:hover { .media-modal .trash-attachment:hover,
.media-modal .untrash-attachment:hover {
color: red; color: red;
} }
@ -2743,7 +2746,9 @@
max-height: calc( 100% - 42px ); /* leave space for actions underneath */ max-height: calc( 100% - 42px ); /* leave space for actions underneath */
} }
.edit-attachment-frame .delete-attachment { .edit-attachment-frame .delete-attachment,
.edit-attachment-frame .trash-attachment,
.edit-attachment-frame .untrash-attachment {
float: left; float: left;
margin-top: 7px; margin-top: 7px;
} }

File diff suppressed because one or more lines are too long

View File

@ -1603,7 +1603,8 @@
.attachment-info .edit-attachment, .attachment-info .edit-attachment,
.attachment-info .refresh-attachment, .attachment-info .refresh-attachment,
.attachment-info .delete-attachment, .attachment-info .delete-attachment,
.attachment-info .trash-attachment { .attachment-info .trash-attachment,
.attachment-info .untrash-attachment {
display: block; display: block;
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
@ -1620,12 +1621,14 @@
} }
.media-modal .delete-attachment, .media-modal .delete-attachment,
.media-modal .trash-attachment { .media-modal .trash-attachment,
.media-modal .untrash-attachment {
color: #bc0b0b; color: #bc0b0b;
} }
.media-modal .delete-attachment:hover, .media-modal .delete-attachment:hover,
.media-modal .trash-attachment:hover { .media-modal .trash-attachment:hover,
.media-modal .untrash-attachment:hover {
color: red; color: red;
} }
@ -2743,7 +2746,9 @@
max-height: calc( 100% - 42px ); /* leave space for actions underneath */ max-height: calc( 100% - 42px ); /* leave space for actions underneath */
} }
.edit-attachment-frame .delete-attachment { .edit-attachment-frame .delete-attachment,
.edit-attachment-frame .trash-attachment,
.edit-attachment-frame .untrash-attachment {
float: right; float: right;
margin-top: 7px; margin-top: 7px;
} }

File diff suppressed because one or more lines are too long

View File

@ -183,7 +183,7 @@
// Create a new EditAttachment frame, passing along the library and the attachment model. // Create a new EditAttachment frame, passing along the library and the attachment model.
wp.media( { wp.media( {
frame: 'edit-attachments', frame: 'edit-attachments',
gridRouter: this.gridRouter, controller: this,
library: this.state().get('library'), library: this.state().get('library'),
model: model model: model
} ); } );
@ -230,6 +230,9 @@
}, },
bindDeferred: function() { bindDeferred: function() {
if ( ! this.browserView.dfd ) {
return;
}
this.browserView.dfd.done( _.bind( this.startHistory, this ) ); this.browserView.dfd.done( _.bind( this.startHistory, this ) );
}, },
@ -352,15 +355,11 @@
regions: [ 'title', 'content' ], regions: [ 'title', 'content' ],
events: { events: {
'click': 'collapse',
'click .delete-media-item': 'deleteMediaItem',
'click .left': 'previousMediaItem', 'click .left': 'previousMediaItem',
'click .right': 'nextMediaItem' 'click .right': 'nextMediaItem'
}, },
initialize: function() { initialize: function() {
var self = this;
media.view.Frame.prototype.initialize.apply( this, arguments ); media.view.Frame.prototype.initialize.apply( this, arguments );
_.defaults( this.options, { _.defaults( this.options, {
@ -368,32 +367,42 @@
state: 'edit-attachment' state: 'edit-attachment'
}); });
this.gridRouter = this.options.gridRouter; this.controller = this.options.controller;
this.gridRouter = this.controller.gridRouter;
this.library = this.options.library; this.library = this.options.library;
if ( this.options.model ) { if ( this.options.model ) {
this.model = this.options.model; this.model = this.options.model;
} else { } else {
// this is a hack
this.model = this.library.at( 0 ); this.model = this.library.at( 0 );
} }
// Close the modal if the attachment is deleted. this.bindHandlers();
this.listenTo( this.model, 'destroy', this.close, this );
this.createStates(); this.createStates();
this.createModal();
this.title.mode( 'default' );
this.options.hasPrevious = this.hasPrevious();
this.options.hasNext = this.hasNext();
},
bindHandlers: function() {
// Bind default title creation.
this.on( 'title:create:default', this.createTitle, this );
// Close the modal if the attachment is deleted.
this.listenTo( this.model, 'change:status destroy', this.close, this );
this.on( 'content:create:edit-metadata', this.editMetadataMode, this ); this.on( 'content:create:edit-metadata', this.editMetadataMode, this );
this.on( 'content:create:edit-image', this.editImageMode, this ); this.on( 'content:create:edit-image', this.editImageMode, this );
this.on( 'content:render:edit-image', this.editImageModeRender, this ); this.on( 'content:render:edit-image', this.editImageModeRender, this );
this.on( 'close', this.detach ); this.on( 'close', this.detach );
},
// Bind default title creation. createModal: function() {
this.on( 'title:create:default', this.createTitle, this ); var self = this;
this.title.mode( 'default' );
this.options.hasPrevious = this.hasPrevious();
this.options.hasNext = this.hasNext();
// Initialize modal container view. // Initialize modal container view.
if ( this.options.modal ) { if ( this.options.modal ) {
@ -609,16 +618,33 @@
media.view.DeleteSelectedButton = media.view.Button.extend({ media.view.DeleteSelectedButton = media.view.Button.extend({
initialize: function() { initialize: function() {
media.view.Button.prototype.initialize.apply( this, arguments ); media.view.Button.prototype.initialize.apply( this, arguments );
if ( this.options.filters ) {
this.listenTo( this.options.filters.model, 'change', this.filterChange );
}
this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled ); this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled );
}, },
filterChange: function( model ) {
if ( 'trash' === model.get( 'status' ) ) {
this.model.set( 'text', l10n.untrashSelected );
} else if ( media.view.settings.mediaTrash ) {
this.model.set( 'text', l10n.trashSelected );
} else {
this.model.set( 'text', l10n.deleteSelected );
}
},
toggleDisabled: function() { toggleDisabled: function() {
this.$el.attr( 'disabled', ! this.controller.state().get( 'selection' ).length ); this.model.set( 'disabled', ! this.controller.state().get( 'selection' ).length );
}, },
render: function() { render: function() {
media.view.Button.prototype.render.apply( this, arguments ); media.view.Button.prototype.render.apply( this, arguments );
this.$el.addClass( 'delete-selected-button hidden' ); if ( this.controller.isModeActive( 'select' ) ) {
this.$el.addClass( 'delete-selected-button' );
} else {
this.$el.addClass( 'delete-selected-button hidden' );
}
return this; return this;
} }
}); });

File diff suppressed because one or more lines are too long

View File

@ -824,9 +824,12 @@ window.wp = window.wp || {};
/** /**
* @access private * @access private
*/ */
_requery: function() { _requery: function( cache ) {
var props;
if ( this.props.get('query') ) { if ( this.props.get('query') ) {
this.mirror( Query.get( this.props.toJSON() ) ); props = this.props.toJSON();
props.cache = ( true !== cache );
this.mirror( Query.get( props ) );
} }
}, },
/** /**
@ -947,6 +950,22 @@ window.wp = window.wp || {};
} }
return uploadedTo === attachment.get('uploadedTo'); return uploadedTo === attachment.get('uploadedTo');
},
/**
* @static
* @param {wp.media.model.Attachment} attachment
*
* @this wp.media.model.Attachments
*
* @returns {Boolean}
*/
status: function( attachment ) {
var status = this.props.get('status');
if ( _.isUndefined( status ) ) {
return true;
}
return status === attachment.get('status');
} }
} }
}); });
@ -1144,7 +1163,8 @@ window.wp = window.wp || {};
'type': 'post_mime_type', 'type': 'post_mime_type',
'perPage': 'posts_per_page', 'perPage': 'posts_per_page',
'menuOrder': 'menu_order', 'menuOrder': 'menu_order',
'uploadedTo': 'post_parent' 'uploadedTo': 'post_parent',
'status': 'post_status'
}, },
/** /**
* @static * @static
@ -1169,11 +1189,13 @@ window.wp = window.wp || {};
var args = {}, var args = {},
orderby = Query.orderby, orderby = Query.orderby,
defaults = Query.defaultProps, defaults = Query.defaultProps,
query; query,
cache = !! props.cache;
// Remove the `query` property. This isn't linked to a query, // Remove the `query` property. This isn't linked to a query,
// this *is* the query. // this *is* the query.
delete props.query; delete props.query;
delete props.cache;
// Fill default args. // Fill default args.
_.defaults( props, defaults ); _.defaults( props, defaults );
@ -1207,9 +1229,13 @@ window.wp = window.wp || {};
args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; args.orderby = orderby.valuemap[ props.orderby ] || props.orderby;
// Search the query cache for matches. // Search the query cache for matches.
query = _.find( queries, function( query ) { if ( cache ) {
return _.isEqual( query.args, args ); query = _.find( queries, function( query ) {
}); return _.isEqual( query.args, args );
});
} else {
queries = [];
}
// Otherwise, create a new query and add it to the cache. // Otherwise, create a new query and add it to the cache.
if ( ! query ) { if ( ! query ) {

File diff suppressed because one or more lines are too long

View File

@ -5671,6 +5671,7 @@
filters[ key ] = { filters[ key ] = {
text: text, text: text,
props: { props: {
status: null,
type: key, type: key,
uploadedTo: null, uploadedTo: null,
orderby: 'date', orderby: 'date',
@ -5682,6 +5683,7 @@
filters.all = { filters.all = {
text: l10n.allMediaItems, text: l10n.allMediaItems,
props: { props: {
status: null,
type: null, type: null,
uploadedTo: null, uploadedTo: null,
orderby: 'date', orderby: 'date',
@ -5694,6 +5696,7 @@
filters.uploaded = { filters.uploaded = {
text: l10n.uploadedToThisPost, text: l10n.uploadedToThisPost,
props: { props: {
status: null,
type: null, type: null,
uploadedTo: media.view.settings.post.id, uploadedTo: media.view.settings.post.id,
orderby: 'menuOrder', orderby: 'menuOrder',
@ -5706,6 +5709,7 @@
filters.unattached = { filters.unattached = {
text: l10n.unattached, text: l10n.unattached,
props: { props: {
status: null,
uploadedTo: 0, uploadedTo: 0,
type: null, type: null,
orderby: 'menuOrder', orderby: 'menuOrder',
@ -5714,6 +5718,20 @@
priority: 50 priority: 50
}; };
if ( media.view.settings.mediaTrash ) {
filters.trash = {
text: l10n.trash,
props: {
uploadedTo: null,
status: 'trash',
type: null,
orderby: 'date',
order: 'DESC'
},
priority: 50
};
}
this.filters = filters; this.filters = filters;
} }
}); });
@ -5765,9 +5783,7 @@
}, },
createToolbar: function() { createToolbar: function() {
var filters, var LibraryViewSwitcher, Filters;
LibraryViewSwitcher,
FiltersConstructor;
/** /**
* @member {wp.media.view.Toolbar} * @member {wp.media.view.Toolbar}
@ -5778,6 +5794,38 @@
this.views.add( this.toolbar ); this.views.add( this.toolbar );
this.toolbar.set( 'spinner', new media.view.Spinner({
priority: -60
}) );
if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
// "Filters" will return a <select>, need to render
// screen reader text before
this.toolbar.set( 'filtersLabel', new media.view.Label({
value: l10n.filterByType,
attributes: {
'for': 'media-attachment-filters'
},
priority: -80
}).render() );
if ( 'uploaded' === this.options.filters ) {
this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
controller: this.controller,
model: this.collection.props,
priority: -80
}).render() );
} else {
Filters = new media.view.AttachmentFilters.All({
controller: this.controller,
model: this.collection.props,
priority: -80
});
this.toolbar.set( 'filters', Filters.render() );
}
}
// Feels odd to bring the global media library switcher into the Attachment // Feels odd to bring the global media library switcher into the Attachment
// browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
// which the controller can tap into and add this view? // which the controller can tap into and add this view?
@ -5814,47 +5862,41 @@
}).render() ); }).render() );
this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({ this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
filters: Filters,
style: 'primary', style: 'primary',
disabled: true, disabled: true,
text: l10n.deleteSelected, text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
controller: this.controller, controller: this.controller,
priority: -60, priority: -60,
click: function() { click: function() {
while ( this.controller.state().get( 'selection' ).length > 0 ) { var model, changed = [],
this.controller.state().get( 'selection' ).at( 0 ).destroy(); selection = this.controller.state().get( 'selection' ),
library = this.controller.state().get( 'library' );
while ( selection.length > 0 ) {
model = selection.at( 0 );
if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
model.set( 'status', 'inherit' );
changed.push( model.save() );
selection.remove( model );
} else if ( media.view.settings.mediaTrash ) {
model.set( 'status', 'trash' );
changed.push( model.save() );
selection.remove( model );
} else {
model.destroy();
}
}
if ( changed.length ) {
$.when( changed ).then( function() {
library._requery( true );
} );
} }
} }
}).render() ); }).render() );
} }
this.toolbar.set( 'spinner', new media.view.Spinner({
priority: -60
}) );
filters = this.options.filters;
if ( 'uploaded' === filters ) {
FiltersConstructor = media.view.AttachmentFilters.Uploaded;
} else if ( 'all' === filters ) {
FiltersConstructor = media.view.AttachmentFilters.All;
}
if ( FiltersConstructor ) {
// "FiltersConstructor" will return a <select>, need to render
// screen reader text before
this.toolbar.set( 'filtersLabel', new media.view.Label({
value: l10n.filterByType,
attributes: {
'for': 'media-attachment-filters'
},
priority: -80
}).render() );
this.toolbar.set( 'filters', new FiltersConstructor({
controller: this.controller,
model: this.collection.props,
priority: -80
}).render() );
}
if ( this.options.search ) { if ( this.options.search ) {
// Search is an input, screen reader text needs to be rendered before // Search is an input, screen reader text needs to be rendered before
this.toolbar.set( 'searchLabel', new media.view.Label({ this.toolbar.set( 'searchLabel', new media.view.Label({
@ -6420,6 +6462,7 @@
'change [data-setting] textarea': 'updateSetting', 'change [data-setting] textarea': 'updateSetting',
'click .delete-attachment': 'deleteAttachment', 'click .delete-attachment': 'deleteAttachment',
'click .trash-attachment': 'trashAttachment', 'click .trash-attachment': 'trashAttachment',
'click .untrash-attachment': 'untrashAttachment',
'click .edit-attachment': 'editAttachment', 'click .edit-attachment': 'editAttachment',
'click .refresh-attachment': 'refreshAttachment', 'click .refresh-attachment': 'refreshAttachment',
'keydown': 'toggleSelectionHandler' 'keydown': 'toggleSelectionHandler'
@ -6453,9 +6496,29 @@
* @param {Object} event * @param {Object} event
*/ */
trashAttachment: function( event ) { trashAttachment: function( event ) {
var library = this.controller.library;
event.preventDefault(); event.preventDefault();
this.model.destroy(); if ( media.view.settings.mediaTrash ) {
this.model.set( 'status', 'trash' );
this.model.save().done( function() {
library._requery( true );
} );
} else {
this.model.destroy();
}
},
/**
* @param {Object} event
*/
untrashAttachment: function( event ) {
var library = this.controller.library;
event.preventDefault();
this.model.set( 'status', 'inherit' );
this.model.save().done( function() {
library._requery( true );
} );
}, },
/** /**
* @param {Object} event * @param {Object} event

File diff suppressed because one or more lines are too long

View File

@ -316,7 +316,11 @@ function wp_print_media_templates() {
<# if ( ! data.uploading && data.can.remove ) { #> <# if ( ! data.uploading && data.can.remove ) { #>
<?php if ( MEDIA_TRASH ): ?> <?php if ( MEDIA_TRASH ): ?>
<# if ( 'trash' === data.status ) { #>
<a class="untrash-attachment" href="#"><?php _e( 'Untrash' ); ?></a>
<# } else { #>
<a class="trash-attachment" href="#"><?php _e( 'Trash' ); ?></a> <a class="trash-attachment" href="#"><?php _e( 'Trash' ); ?></a>
<# } #>
<?php else: ?> <?php else: ?>
<a class="delete-attachment" href="#"><?php _e( 'Delete Permanently' ); ?></a> <a class="delete-attachment" href="#"><?php _e( 'Delete Permanently' ); ?></a>
<?php endif; ?> <?php endif; ?>

View File

@ -2869,6 +2869,7 @@ function wp_enqueue_media( $args = array() ) {
'embedMimes' => $ext_mimes, 'embedMimes' => $ext_mimes,
'contentWidth' => $content_width, 'contentWidth' => $content_width,
'months' => $months, 'months' => $months,
'mediaTrash' => MEDIA_TRASH ? 1 : 0
); );
$post = null; $post = null;
@ -2931,11 +2932,14 @@ function wp_enqueue_media( $args = array() ) {
'noItemsFound' => __( 'No items found.' ), 'noItemsFound' => __( 'No items found.' ),
'insertIntoPost' => $hier ? __( 'Insert into page' ) : __( 'Insert into post' ), 'insertIntoPost' => $hier ? __( 'Insert into page' ) : __( 'Insert into post' ),
'unattached' => __( 'Unattached' ), 'unattached' => __( 'Unattached' ),
'trash' => __( 'Trash' ),
'uploadedToThisPost' => $hier ? __( 'Uploaded to this page' ) : __( 'Uploaded to this post' ), 'uploadedToThisPost' => $hier ? __( 'Uploaded to this page' ) : __( 'Uploaded to this post' ),
'warnDelete' => __( "You are about to permanently delete this item.\n 'Cancel' to stop, 'OK' to delete." ), 'warnDelete' => __( "You are about to permanently delete this item.\n 'Cancel' to stop, 'OK' to delete." ),
'warnBulkDelete' => __( "You are about to permanently delete these items.\n 'Cancel' to stop, 'OK' to delete." ), 'warnBulkDelete' => __( "You are about to permanently delete these items.\n 'Cancel' to stop, 'OK' to delete." ),
'bulkSelect' => __( 'Bulk Select' ), 'bulkSelect' => __( 'Bulk Select' ),
'cancelSelection' => __( 'Cancel Selection' ), 'cancelSelection' => __( 'Cancel Selection' ),
'trashSelected' => __( 'Trash Selected' ),
'untrashSelected' => __( 'Untrash Selected' ),
'deleteSelected' => __( 'Delete Selected' ), 'deleteSelected' => __( 'Delete Selected' ),
'deletePermanently' => __( 'Delete Permanently' ), 'deletePermanently' => __( 'Delete Permanently' ),
'apply' => __( 'Apply' ), 'apply' => __( 'Apply' ),