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.


git-svn-id: https://develop.svn.wordpress.org/trunk@29490 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Scott Taylor 2014-08-14 18:30:49 +00:00
parent 1ab750d0d2
commit 8661fcf0d9
7 changed files with 209 additions and 66 deletions

View File

@ -2161,7 +2161,14 @@ function wp_ajax_query_attachments() {
) ) );
$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 ) )
$query['post_status'] .= ',private';
@ -2216,6 +2223,9 @@ function wp_ajax_save_attachment() {
if ( isset( $changes['description'] ) )
$post['post_content'] = $changes['description'];
if ( MEDIA_TRASH && isset( $changes['status'] ) )
$post['post_status'] = $changes['status'];
if ( isset( $changes['alt'] ) ) {
$alt = wp_unslash( $changes['alt'] );
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();
}

View File

@ -1603,7 +1603,8 @@
.attachment-info .edit-attachment,
.attachment-info .refresh-attachment,
.attachment-info .delete-attachment,
.attachment-info .trash-attachment {
.attachment-info .trash-attachment,
.attachment-info .untrash-attachment {
display: block;
text-decoration: none;
white-space: nowrap;
@ -1620,12 +1621,14 @@
}
.media-modal .delete-attachment,
.media-modal .trash-attachment {
.media-modal .trash-attachment,
.media-modal .untrash-attachment {
color: #bc0b0b;
}
.media-modal .delete-attachment:hover,
.media-modal .trash-attachment:hover {
.media-modal .trash-attachment:hover,
.media-modal .untrash-attachment:hover {
color: red;
}
@ -2743,7 +2746,9 @@
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;
margin-top: 7px;
}

View File

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

View File

@ -824,9 +824,12 @@ window.wp = window.wp || {};
/**
* @access private
*/
_requery: function() {
_requery: function( cache ) {
var props;
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');
},
/**
* @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',
'perPage': 'posts_per_page',
'menuOrder': 'menu_order',
'uploadedTo': 'post_parent'
'uploadedTo': 'post_parent',
'status': 'post_status'
},
/**
* @static
@ -1169,11 +1189,13 @@ window.wp = window.wp || {};
var args = {},
orderby = Query.orderby,
defaults = Query.defaultProps,
query;
query,
cache = !! props.cache;
// Remove the `query` property. This isn't linked to a query,
// this *is* the query.
delete props.query;
delete props.cache;
// Fill default args.
_.defaults( props, defaults );
@ -1207,9 +1229,13 @@ window.wp = window.wp || {};
args.orderby = orderby.valuemap[ props.orderby ] || props.orderby;
// Search the query cache for matches.
query = _.find( queries, function( query ) {
return _.isEqual( query.args, args );
});
if ( cache ) {
query = _.find( queries, function( query ) {
return _.isEqual( query.args, args );
});
} else {
queries = [];
}
// Otherwise, create a new query and add it to the cache.
if ( ! query ) {

View File

@ -5671,6 +5671,7 @@
filters[ key ] = {
text: text,
props: {
status: null,
type: key,
uploadedTo: null,
orderby: 'date',
@ -5682,6 +5683,7 @@
filters.all = {
text: l10n.allMediaItems,
props: {
status: null,
type: null,
uploadedTo: null,
orderby: 'date',
@ -5694,6 +5696,7 @@
filters.uploaded = {
text: l10n.uploadedToThisPost,
props: {
status: null,
type: null,
uploadedTo: media.view.settings.post.id,
orderby: 'menuOrder',
@ -5706,6 +5709,7 @@
filters.unattached = {
text: l10n.unattached,
props: {
status: null,
uploadedTo: 0,
type: null,
orderby: 'menuOrder',
@ -5714,6 +5718,20 @@
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;
}
});
@ -5765,9 +5783,7 @@
},
createToolbar: function() {
var filters,
LibraryViewSwitcher,
FiltersConstructor;
var LibraryViewSwitcher, Filters;
/**
* @member {wp.media.view.Toolbar}
@ -5778,6 +5794,38 @@
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
// 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?
@ -5814,47 +5862,41 @@
}).render() );
this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
filters: Filters,
style: 'primary',
disabled: true,
text: l10n.deleteSelected,
text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
controller: this.controller,
priority: -60,
click: function() {
while ( this.controller.state().get( 'selection' ).length > 0 ) {
this.controller.state().get( 'selection' ).at( 0 ).destroy();
var model, changed = [],
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() );
}
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 ) {
// Search is an input, screen reader text needs to be rendered before
this.toolbar.set( 'searchLabel', new media.view.Label({
@ -6420,6 +6462,7 @@
'change [data-setting] textarea': 'updateSetting',
'click .delete-attachment': 'deleteAttachment',
'click .trash-attachment': 'trashAttachment',
'click .untrash-attachment': 'untrashAttachment',
'click .edit-attachment': 'editAttachment',
'click .refresh-attachment': 'refreshAttachment',
'keydown': 'toggleSelectionHandler'
@ -6453,9 +6496,29 @@
* @param {Object} event
*/
trashAttachment: function( event ) {
var library = this.controller.library;
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

View File

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

View File

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