diff --git a/packages/block-editor/src/components/inserter/media-tab/hooks.js b/packages/block-editor/src/components/inserter/media-tab/hooks.js index d8e571dc242e05..0822e2bf67e367 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -98,13 +98,6 @@ function useInserterMediaCategories() { ) { return false; } - // When a category has set `isExternalResource` to `true`, we - // don't need to check for allowed mime types, as they are used - // for restricting uploads for this media type and not for - // inserting media from external sources. - if ( category.isExternalResource ) { - return true; - } return Object.values( allowedMimeTypes ).some( ( mimeType ) => mimeType.startsWith( `${ category.mediaType }/` ) ); @@ -156,7 +149,15 @@ export function useMediaCategories( rootClientId ) { if ( category.isExternalResource ) { return [ category.name, true ]; } - const results = await category.fetch( { per_page: 1 } ); + let results = []; + try { + results = await category.fetch( { + per_page: 1, + } ); + } catch ( e ) { + // If the request fails, we shallow the error and just don't show + // the category, in order to not break the media tab. + } return [ category.name, !! results.length ]; } ) ) diff --git a/packages/block-editor/src/components/inserter/media-tab/media-list.js b/packages/block-editor/src/components/inserter/media-tab/media-list.js index 6eb316bad0d163..b745a54e25e9c0 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-list.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-list.js @@ -1,129 +1,16 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ import { __unstableComposite as Composite, __unstableUseCompositeState as useCompositeState, - __unstableCompositeItem as CompositeItem, - Tooltip, - DropdownMenu, - MenuGroup, - MenuItem, } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { useMemo, useCallback, useState } from '@wordpress/element'; -import { cloneBlock } from '@wordpress/blocks'; -import { moreVertical, external } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import InserterDraggableBlocks from '../../inserter-draggable-blocks'; -import { getBlockAndPreviewFromMedia } from './utils'; - -const MAXIMUM_TITLE_LENGTH = 25; -const MEDIA_OPTIONS_POPOVER_PROPS = { - position: 'bottom left', - className: - 'block-editor-inserter__media-list__item-preview-options__popover', -}; - -function MediaPreviewOptions( { category, media } ) { - if ( ! category.getReportUrl ) { - return null; - } - const reportUrl = category.getReportUrl( media ); - return ( - - { () => ( - - - window.open( reportUrl, '_blank' ).focus() - } - icon={ external } - > - { sprintf( - /* translators: %s: The media type to report e.g: "image", "video", "audio" */ - __( 'Report %s' ), - category.mediaType - ) } - - - ) } - - ); -} - -function MediaPreview( { media, onClick, composite, category } ) { - const [ isHovered, setIsHovered ] = useState( false ); - const [ block, preview ] = useMemo( - () => getBlockAndPreviewFromMedia( media, category.mediaType ), - [ media, category.mediaType ] - ); - const title = media.title?.rendered || media.title; - let truncatedTitle; - if ( title.length > MAXIMUM_TITLE_LENGTH ) { - const omission = '...'; - truncatedTitle = - title.slice( 0, MAXIMUM_TITLE_LENGTH - omission.length ) + omission; - } - const onMouseEnter = useCallback( () => setIsHovered( true ), [] ); - const onMouseLeave = useCallback( () => setIsHovered( false ), [] ); - return ( - - { ( { draggable, onDragStart, onDragEnd } ) => ( -
- - { /* Adding `is-hovered` class to the wrapper element is needed - because the options Popover is rendered outside of this node. */ } -
- onClick( block ) } - aria-label={ title } - > -
- { preview } -
-
- -
-
-
- ) } -
- ); -} +import { MediaPreview } from './media-preview'; function MediaList( { mediaList, @@ -132,12 +19,6 @@ function MediaList( { label = __( 'Media List' ), } ) { const composite = useCompositeState(); - const onPreviewClick = useCallback( - ( block ) => { - onClick( cloneBlock( block ) ); - }, - [ onClick ] - ); return ( ) ) } diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js new file mode 100644 index 00000000000000..88648bf96531b6 --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -0,0 +1,268 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __unstableCompositeItem as CompositeItem, + Tooltip, + DropdownMenu, + MenuGroup, + MenuItem, + Spinner, + Modal, + Flex, + FlexItem, + Button, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo, useCallback, useState } from '@wordpress/element'; +import { cloneBlock } from '@wordpress/blocks'; +import { moreVertical, external } from '@wordpress/icons'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import InserterDraggableBlocks from '../../inserter-draggable-blocks'; +import { getBlockAndPreviewFromMedia } from './utils'; +import { store as blockEditorStore } from '../../../store'; + +const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const MAXIMUM_TITLE_LENGTH = 25; +const MEDIA_OPTIONS_POPOVER_PROPS = { + position: 'bottom left', + className: + 'block-editor-inserter__media-list__item-preview-options__popover', +}; + +function MediaPreviewOptions( { category, media } ) { + if ( ! category.getReportUrl ) { + return null; + } + const reportUrl = category.getReportUrl( media ); + return ( + + { () => ( + + + window.open( reportUrl, '_blank' ).focus() + } + icon={ external } + > + { sprintf( + /* translators: %s: The media type to report e.g: "image", "video", "audio" */ + __( 'Report %s' ), + category.mediaType + ) } + + + ) } + + ); +} + +function InsertExternalImageModal( { onClose, onSubmit } ) { + return ( + + +

+ { __( + 'This image cannot be uploaded to your Media Library, but it can still be inserted as an external image.' + ) } +

+

+ { __( + 'External images can be removed by the external provider without warning and could even have legal compliance issues related to privacy legislation.' + ) } +

+
+ + + + + + + + +
+ ); +} + +export function MediaPreview( { media, onClick, composite, category } ) { + const [ showExternalUploadModal, setShowExternalUploadModal ] = + useState( false ); + const [ isHovered, setIsHovered ] = useState( false ); + const [ isInserting, setIsInserting ] = useState( false ); + const [ block, preview ] = useMemo( + () => getBlockAndPreviewFromMedia( media, category.mediaType ), + [ media, category.mediaType ] + ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + const mediaUpload = useSelect( + ( select ) => select( blockEditorStore ).getSettings().mediaUpload, + [] + ); + const onMediaInsert = useCallback( + ( previewBlock ) => { + // Prevent multiple uploads when we're in the process of inserting. + if ( isInserting ) { + return; + } + const clonedBlock = cloneBlock( previewBlock ); + const { id, url, caption } = clonedBlock.attributes; + // Media item already exists in library, so just insert it. + if ( !! id ) { + onClick( clonedBlock ); + return; + } + setIsInserting( true ); + // Media item does not exist in library, so try to upload it. + // Fist fetch the image data. This may fail if the image host + // doesn't allow CORS with the domain. + // If this happens, we insert the image block using the external + // URL and let the user know about the possible implications. + window + .fetch( url ) + .then( ( response ) => response.blob() ) + .then( ( blob ) => { + mediaUpload( { + filesList: [ blob ], + additionalData: { caption }, + onFileChange( [ img ] ) { + if ( isBlobURL( img.url ) ) { + return; + } + onClick( { + ...clonedBlock, + attributes: { + ...clonedBlock.attributes, + id: img.id, + url: img.url, + }, + } ); + createSuccessNotice( + __( 'Image uploaded and inserted.' ), + { type: 'snackbar' } + ); + setIsInserting( false ); + }, + allowedTypes: ALLOWED_MEDIA_TYPES, + onError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + setIsInserting( false ); + }, + } ); + } ) + .catch( () => { + setShowExternalUploadModal( true ); + setIsInserting( false ); + } ); + }, + [ + isInserting, + onClick, + mediaUpload, + createErrorNotice, + createSuccessNotice, + ] + ); + const title = media.title?.rendered || media.title; + let truncatedTitle; + if ( title.length > MAXIMUM_TITLE_LENGTH ) { + const omission = '...'; + truncatedTitle = + title.slice( 0, MAXIMUM_TITLE_LENGTH - omission.length ) + omission; + } + const onMouseEnter = useCallback( () => setIsHovered( true ), [] ); + const onMouseLeave = useCallback( () => setIsHovered( false ), [] ); + return ( + <> + + { ( { draggable, onDragStart, onDragEnd } ) => ( +
+ + { /* Adding `is-hovered` class to the wrapper element is needed + because the options Popover is rendered outside of this node. */ } +
+ onMediaInsert( block ) } + aria-label={ title } + > +
+ { preview } + { isInserting && ( +
+ +
+ ) } +
+
+ { ! isInserting && ( + + ) } +
+
+
+ ) } +
+ { showExternalUploadModal && ( + setShowExternalUploadModal( false ) } + onSubmit={ () => { + onClick( cloneBlock( block ) ); + createSuccessNotice( __( 'Image inserted.' ), { + type: 'snackbar', + } ); + setShowExternalUploadModal( false ); + } } + /> + ) } + + ); +} diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 41491696e9010e..51026ec939dd5e 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -67,25 +67,19 @@ function InserterMenu( insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, } ); - const { showPatterns, inserterItems, enableOpenverseMediaCategory } = - useSelect( - ( select ) => { - const { - __experimentalGetAllowedPatterns, - getInserterItems, - getSettings, - } = select( blockEditorStore ); - return { - showPatterns: !! __experimentalGetAllowedPatterns( - destinationRootClientId - ).length, - inserterItems: getInserterItems( destinationRootClientId ), - enableOpenverseMediaCategory: - getSettings().enableOpenverseMediaCategory, - }; - }, - [ destinationRootClientId ] - ); + const { showPatterns, inserterItems } = useSelect( + ( select ) => { + const { __experimentalGetAllowedPatterns, getInserterItems } = + select( blockEditorStore ); + return { + showPatterns: !! __experimentalGetAllowedPatterns( + destinationRootClientId + ).length, + inserterItems: getInserterItems( destinationRootClientId ), + }; + }, + [ destinationRootClientId ] + ); const hasReusableBlocks = useMemo( () => { return inserterItems.some( ( { category } ) => category === 'reusable' @@ -93,7 +87,7 @@ function InserterMenu( }, [ inserterItems ] ); const mediaCategories = useMediaCategories( destinationRootClientId ); - const showMedia = !! mediaCategories.length || enableOpenverseMediaCategory; + const showMedia = !! mediaCategories.length; const onInsert = useCallback( ( blocks, meta, shouldForceFocusBlock ) => { diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 18ead7dbc484fa..fa0778af6d53f0 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -660,6 +660,17 @@ $block-inserter-tabs-height: 44px; margin: 0 auto; max-width: 100%; } + + .block-editor-inserter__media-list__item-preview-spinner { + display: flex; + height: 100%; + width: 100%; + position: absolute; + justify-content: center; + background: rgba($white, 0.7); + align-items: center; + pointer-events: none; + } } &:focus .block-editor-inserter__media-list__item-preview { @@ -686,3 +697,14 @@ $block-inserter-tabs-height: 44px; height: 100%; } } + + +.block-editor-inserter-media-tab-media-preview-inserter-external-image-modal { + @include break-small() { + max-width: $break-mobile; + } + + p { + margin: 0; + } +}