-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image: Reflect media deletion in the editor #41220
Changes from all commits
a586577
2b77398
f4ffb07
e7da196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,19 @@ import { get, has, omit, pick } from 'lodash'; | |
* WordPress dependencies | ||
*/ | ||
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; | ||
import { withNotices } from '@wordpress/components'; | ||
import { | ||
Button, | ||
withNotices, | ||
Placeholder, | ||
__experimentalHStack as HStack, | ||
} from '@wordpress/components'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { | ||
BlockAlignmentControl, | ||
BlockControls, | ||
BlockIcon, | ||
MediaPlaceholder, | ||
MediaReplaceFlow, | ||
useBlockProps, | ||
store as blockEditorStore, | ||
} from '@wordpress/block-editor'; | ||
|
@@ -26,6 +32,7 @@ import { image as icon } from '@wordpress/icons'; | |
* Internal dependencies | ||
*/ | ||
import Image from './image'; | ||
import { isMediaFileDeleted } from './utils'; | ||
|
||
/** | ||
* Module constants | ||
|
@@ -85,6 +92,84 @@ function hasDefaultSize( image, defaultSize ) { | |
); | ||
} | ||
|
||
/** | ||
* Checks if a media attachment object has been "destroyed", | ||
* that is, removed from the media library. The core Media Library | ||
* adds a `destroyed` property to a deleted attachment object in the media collection. | ||
* | ||
* @param {number} id The attachment id. | ||
* | ||
* @return {boolean} Whether the image has been destroyed. | ||
*/ | ||
export function isMediaDestroyed( id ) { | ||
const attachment = window?.wp?.media?.attachment( id ) || {}; | ||
return attachment.destroyed; | ||
} | ||
|
||
/** | ||
* A placeholder component that displays if an image has been deleted or cannot be loaded. | ||
* | ||
* @param {Object} props Component props. | ||
* @param {number} props.id The attachment id. | ||
* @param {string} props.url Image url. | ||
* @param {Function} props.onClear Clears the image attributes. | ||
* @param {Function} props.onSelect Callback for <MediaReplaceFlow /> when an image is selected. | ||
* @param {Function} props.onSelectURL Callback for <MediaReplaceFlow /> when an image URL is selected. | ||
* @param {Function} props.onUploadError Callback for <MediaReplaceFlow /> when an upload fails. | ||
* @param {Function} props.onCloseModal Callback for <MediaReplaceFlow /> when the media library overlay is closed. | ||
* | ||
* @return {boolean} Whether the image has been destroyed. | ||
*/ | ||
const ImageErrorPlaceholder = ( { | ||
id, | ||
url, | ||
onClear, | ||
onSelect, | ||
onSelectURL, | ||
onUploadError, | ||
onCloseModal, | ||
} ) => ( | ||
<Placeholder | ||
className="wp-block-image__error-placeholder" | ||
icon={ icon } | ||
label={ __( 'This image could not be loaded' ) } | ||
> | ||
<p>{ url }</p> | ||
<p> | ||
{ __( | ||
'This might be due to a network error, or the image may have been deleted.' | ||
) } | ||
</p> | ||
<HStack justify="flex-start" spacing={ 2 }> | ||
<MediaReplaceFlow | ||
mediaId={ id } | ||
mediaURL={ url } | ||
allowedTypes={ ALLOWED_MEDIA_TYPES } | ||
accept="image/*" | ||
onSelect={ ( media ) => { | ||
onClear(); | ||
onSelect( media ); | ||
} } | ||
onSelectURL={ ( selectedUrl ) => { | ||
onClear(); | ||
onSelectURL( selectedUrl ); | ||
} } | ||
onError={ onUploadError } | ||
onCloseModal={ onCloseModal } | ||
buttonVariant="primary" | ||
/> | ||
<Button | ||
className="wp-block-image__error-placeholder__button" | ||
title={ __( 'Clear and replace image' ) } | ||
variant="secondary" | ||
onClick={ onClear } | ||
> | ||
{ __( 'Clear' ) } | ||
</Button> | ||
</HStack> | ||
</Placeholder> | ||
); | ||
|
||
export function ImageEdit( { | ||
attributes, | ||
setAttributes, | ||
|
@@ -108,6 +193,7 @@ export function ImageEdit( { | |
sizeSlug, | ||
} = attributes; | ||
const [ temporaryURL, setTemporaryURL ] = useState(); | ||
const [ hasImageLoadError, setHasImageLoadError ] = useState( false ); | ||
|
||
const altRef = useRef(); | ||
useEffect( () => { | ||
|
@@ -125,6 +211,45 @@ export function ImageEdit( { | |
return pick( getSettings(), [ 'imageDefaultSize', 'mediaUpload' ] ); | ||
}, [] ); | ||
|
||
// A callback passed to MediaUpload, | ||
// fired when the media modal closes. | ||
function onCloseModal() { | ||
if ( isMediaDestroyed( attributes?.id ) ) { | ||
setHasImageLoadError( true ); | ||
} | ||
} | ||
|
||
function clearImageAttributes() { | ||
setHasImageLoadError( false ); | ||
setAttributes( { | ||
src: undefined, | ||
id: undefined, | ||
url: undefined, | ||
} ); | ||
} | ||
|
||
/** | ||
* Runs an error callback if the image does not load. | ||
* If the error callback is triggered, we infer that that image | ||
* has been deleted. | ||
* | ||
* @param {boolean} isReplaced Whether the image has been replaced. | ||
*/ | ||
function onImageError( isReplaced = false ) { | ||
// If the image block was not replaced with an embed, | ||
// and it's not an external image, | ||
// and it's been deleted from the database, | ||
// clear the attributes and trigger an error placeholder. | ||
if ( id && ! isReplaced && ! isExternalImage( id, url ) ) { | ||
isMediaFileDeleted( id ).then( ( isFileDeleted ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if this will solve the reported issue in #41161 at all... if the image triggers the error handler, it could be due to a network error in which case the fetch request would fail. 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a situation we could mock within some unit tests for the image block? |
||
if ( isFileDeleted ) { | ||
noticeOperations.removeAllNotices(); | ||
setHasImageLoadError( true ); | ||
} | ||
} ); | ||
} | ||
} | ||
|
||
function onUploadError( message ) { | ||
noticeOperations.removeAllNotices(); | ||
noticeOperations.createErrorNotice( message ); | ||
|
@@ -309,7 +434,18 @@ export function ImageEdit( { | |
|
||
return ( | ||
<figure { ...blockProps }> | ||
{ ( temporaryURL || url ) && ( | ||
{ hasImageLoadError && ( | ||
<ImageErrorPlaceholder | ||
id={ id } | ||
url={ url } | ||
onClear={ clearImageAttributes } | ||
onSelect={ onSelectImage } | ||
onSelectURL={ onSelectURL } | ||
onUploadError={ onUploadError } | ||
onCloseModal={ onCloseModal } | ||
/> | ||
) } | ||
{ ! hasImageLoadError && ( temporaryURL || url ) && ( | ||
<Image | ||
temporaryURL={ temporaryURL } | ||
attributes={ attributes } | ||
|
@@ -323,6 +459,8 @@ export function ImageEdit( { | |
containerRef={ ref } | ||
context={ context } | ||
clientId={ clientId } | ||
onCloseModal={ onCloseModal } | ||
onImageLoadError={ onImageError } | ||
/> | ||
) } | ||
{ ! url && ( | ||
|
@@ -333,18 +471,21 @@ export function ImageEdit( { | |
/> | ||
</BlockControls> | ||
) } | ||
<MediaPlaceholder | ||
icon={ <BlockIcon icon={ icon } /> } | ||
onSelect={ onSelectImage } | ||
onSelectURL={ onSelectURL } | ||
notices={ noticeUI } | ||
onError={ onUploadError } | ||
accept="image/*" | ||
allowedTypes={ ALLOWED_MEDIA_TYPES } | ||
value={ { id, src } } | ||
mediaPreview={ mediaPreview } | ||
disableMediaButtons={ temporaryURL || url } | ||
/> | ||
{ ! hasImageLoadError && ( | ||
<MediaPlaceholder | ||
icon={ <BlockIcon icon={ icon } /> } | ||
onSelect={ onSelectImage } | ||
onSelectURL={ onSelectURL } | ||
notices={ noticeUI } | ||
onError={ onUploadError } | ||
onClose={ onCloseModal } | ||
accept="image/*" | ||
allowedTypes={ ALLOWED_MEDIA_TYPES } | ||
value={ { id, src } } | ||
mediaPreview={ mediaPreview } | ||
disableMediaButtons={ temporaryURL || url } | ||
/> | ||
) } | ||
</figure> | ||
); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we force the return here to be boolean matching the JSDoc return type?