diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 3af2bfe7deffa..aa512b4b300a2 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -26,6 +26,7 @@ import { image as icon } from '@wordpress/icons'; * Internal dependencies */ import Image from './image'; +import { isMediaFileDeleted } from './utils'; /** * Module constants @@ -85,6 +86,20 @@ 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; +} + export function ImageEdit( { attributes, setAttributes, @@ -125,6 +140,48 @@ 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 ) ) { + setAttributes( { + url: undefined, + id: 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. + */ + function onImageError( isReplaced = false ) { + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( + sprintf( + /* translators: %s url or missing image */ + __( 'Error loading image: %s' ), + url + ) + ); + + // 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 the placeholder. + if ( id && ! isReplaced && ! isExternalImage( id, url ) ) { + isMediaFileDeleted( id ).then( ( isFileDeleted ) => { + if ( isFileDeleted ) { + setAttributes( { + url: undefined, + id: undefined, + } ); + } + } ); + } + } + function onUploadError( message ) { noticeOperations.removeAllNotices(); noticeOperations.createErrorNotice( message ); @@ -323,6 +380,8 @@ export function ImageEdit( { containerRef={ ref } context={ context } clientId={ clientId } + onCloseModal={ onCloseModal } + onImageLoadError={ onImageError } /> ) } { ! url && ( @@ -339,6 +398,7 @@ export function ImageEdit( { onSelectURL={ onSelectURL } notices={ noticeUI } onError={ onUploadError } + onClose={ onCloseModal } accept="image/*" allowedTypes={ ALLOWED_MEDIA_TYPES } value={ { id, src } } diff --git a/packages/block-library/src/image/utils.js b/packages/block-library/src/image/utils.js index 9ac6e7fc38ceb..73b8211b1a120 100644 --- a/packages/block-library/src/image/utils.js +++ b/packages/block-library/src/image/utils.js @@ -3,6 +3,11 @@ */ import { isEmpty, each, get } from 'lodash'; +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + /** * Internal dependencies */ @@ -72,3 +77,21 @@ export function getImageSizeAttributes( image, size ) { return {}; } + +/** + * Performs a GET request on an image file to confirm whether it has been deleted from the database. + * + * @param {number=} mediaId The id of the image. + * @return {Promise} Media Object Promise. + */ +export async function isMediaFileDeleted( mediaId ) { + try { + const response = await apiFetch( { + path: `/wp/v2/media/${ mediaId }`, + } ); + const isMediaFileAvailable = response && response?.id === mediaId; + return ! isMediaFileAvailable; + } catch ( err ) { + return true; + } +}