diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 09ded06ca7bb57..9861df423ef040 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -70,6 +70,7 @@ export function MediaPlaceholder( { onError, onSelect, onCancel, + onClose = noop, onSelectURL, onDoubleClick, onFilesPreUpload = noop, @@ -327,6 +328,7 @@ export function MediaPlaceholder( { gallery={ multiple && onlyAllowsImages() } multiple={ multiple } onSelect={ onSelect } + onClose={ onClose } allowedTypes={ allowedTypes } mode={ 'browse' } value={ diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 2daf61e3b703be..e4cf3c7b245008 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -48,6 +48,7 @@ const MediaReplaceFlow = ( { onError, onSelect, onSelectURL, + onCloseModal = noop, onToggleFeaturedImage, useFeaturedImage, onFilesUpload = noop, @@ -58,6 +59,7 @@ const MediaReplaceFlow = ( { multiple = false, addToGallery, handleUpload = true, + buttonVariant, } ) => { const [ mediaURLValue, setMediaURLValue ] = useState( mediaURL ); const mediaUpload = useSelect( ( select ) => { @@ -153,6 +155,7 @@ const MediaReplaceFlow = ( { aria-haspopup="true" onClick={ onToggle } onKeyDown={ openOnArrowDown } + variant={ buttonVariant } > { name } @@ -170,6 +173,7 @@ const MediaReplaceFlow = ( { selectMedia( media, onClose ) } allowedTypes={ allowedTypes } + onClose={ onCloseModal } render={ ( { open } ) => ( when an image is selected. + * @param {Function} props.onSelectURL Callback for when an image URL is selected. + * @param {Function} props.onUploadError Callback for when an upload fails. + * @param {Function} props.onCloseModal Callback for when the media library overlay is closed. + * + * @return {boolean} Whether the image has been destroyed. + */ +const ImageErrorPlaceholder = ( { + id, + url, + onClear, + onSelect, + onSelectURL, + onUploadError, + onCloseModal, +} ) => ( + +

{ url }

+

+ { __( + 'This might be due to a network error, or the image may have been deleted.' + ) } +

+ + { + onClear(); + onSelect( media ); + } } + onSelectURL={ ( selectedUrl ) => { + onClear(); + onSelectURL( selectedUrl ); + } } + onError={ onUploadError } + onCloseModal={ onCloseModal } + buttonVariant="primary" + /> + + +
+); + 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 ) => { + if ( isFileDeleted ) { + noticeOperations.removeAllNotices(); + setHasImageLoadError( true ); + } + } ); + } + } + function onUploadError( message ) { noticeOperations.removeAllNotices(); noticeOperations.createErrorNotice( message ); @@ -309,7 +434,18 @@ export function ImageEdit( { return (
- { ( temporaryURL || url ) && ( + { hasImageLoadError && ( + + ) } + { ! hasImageLoadError && ( temporaryURL || url ) && ( ) } { ! url && ( @@ -333,18 +471,21 @@ export function ImageEdit( { /> ) } - } - onSelect={ onSelectImage } - onSelectURL={ onSelectURL } - notices={ noticeUI } - onError={ onUploadError } - accept="image/*" - allowedTypes={ ALLOWED_MEDIA_TYPES } - value={ { id, src } } - mediaPreview={ mediaPreview } - disableMediaButtons={ temporaryURL || url } - /> + { ! hasImageLoadError && ( + } + onSelect={ onSelectImage } + onSelectURL={ onSelectURL } + notices={ noticeUI } + onError={ onUploadError } + onClose={ onCloseModal } + accept="image/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ { id, src } } + mediaPreview={ mediaPreview } + disableMediaButtons={ temporaryURL || url } + /> + ) }
); } diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index bcbcb771bd1cbf..1a5031b9790829 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -138,3 +138,7 @@ figure.wp-block-image:not(.wp-block) { padding-right: 0; } } + +.wp-block-image__error-placeholder .wp-block-image__error-placeholder__button { + margin: 0 0 0 $grid-unit-10; +} diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 8ec14a81d0212b..3bf64fdfa5b066 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -47,7 +47,7 @@ import { store as coreStore } from '@wordpress/core-data'; */ import { createUpgradedEmbedBlock } from '../embed/util'; import useClientWidth from './use-client-width'; -import { isExternalImage } from './edit'; +import { isExternalImage, isMediaDestroyed } from './edit'; /** * Module constants @@ -76,9 +76,11 @@ export default function Image( { isSelected, insertBlocksAfter, onReplace, + onCloseModal, onSelectImage, onSelectURL, onUploadError, + onImageLoadError, containerRef, context, clientId, @@ -216,10 +218,13 @@ export default function Image( { // Check if there's an embed block that handles this URL, e.g., instagram URL. // See: https://github.com/WordPress/gutenberg/pull/11472 const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } ); + const shouldReplace = undefined !== embedBlock; - if ( undefined !== embedBlock ) { + if ( shouldReplace ) { onReplace( embedBlock ); } + + onImageLoadError( shouldReplace ); } function onSetHref( props ) { @@ -291,6 +296,9 @@ export default function Image( { if ( ! isSelected ) { setIsEditingImage( false ); } + if ( isSelected && isMediaDestroyed( id ) ) { + onImageLoadError(); + } }, [ isSelected ] ); const canEditImage = id && naturalWidth && naturalHeight && imageEditing; @@ -354,6 +362,7 @@ export default function Image( { onSelect={ onSelectImage } onSelectURL={ onSelectURL } onError={ onUploadError } + onCloseModal={ onCloseModal } /> ) } diff --git a/packages/block-library/src/image/utils.js b/packages/block-library/src/image/utils.js index 7d9cc53f3d051a..0c1031942ac7a4 100644 --- a/packages/block-library/src/image/utils.js +++ b/packages/block-library/src/image/utils.js @@ -3,6 +3,11 @@ */ import { isEmpty, 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; + } +}