From ad8bbf2c093545682da8b58e9d65ce04d888e9a6 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Mon, 27 Feb 2023 20:00:38 +0200 Subject: [PATCH 1/8] [Inserter - Media tab]: Upload Openverse images when inserted --- .../inserter/media-tab/media-panel.js | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/inserter/media-tab/media-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js index 58ae7c49d27628..a42e8228ab34db 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-panel.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js @@ -1,10 +1,13 @@ /** * WordPress dependencies */ -import { useRef, useEffect } from '@wordpress/element'; +import { useRef, useEffect, useCallback } from '@wordpress/element'; import { Spinner, SearchControl } from '@wordpress/components'; import { focus } from '@wordpress/dom'; import { __ } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { isBlobURL } from '@wordpress/blob'; /** * Internal dependencies @@ -13,8 +16,10 @@ import MediaList from './media-list'; import useDebouncedInput from '../hooks/use-debounced-input'; import { useMediaResults } from './hooks'; import InserterNoResults from '../no-results'; +import { store as blockEditorStore } from '../../../store'; const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; +const ALLOWED_MEDIA_TYPES = [ 'image' ]; export function MediaCategoryDialog( { rootClientId, onInsert, category } ) { const container = useRef(); @@ -42,6 +47,70 @@ export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, search: debouncedSearch, } ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + const mediaUpload = useSelect( + ( select ) => select( blockEditorStore ).getSettings().mediaUpload, + [] + ); + const onMediaInsert = useCallback( + ( block ) => { + const { id, url, caption } = block.attributes; + // Media item already exists in library, so just insert it. + if ( !! id ) { + onInsert( block ); + return; + } + // 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 that happens, we insert the image block using the external + // URL and let the user know about the implications of that. + window + .fetch( url ) + .then( ( response ) => response.blob() ) + .then( ( blob ) => { + mediaUpload( { + filesList: [ blob ], + additionalData: { caption }, + onFileChange( [ img ] ) { + if ( isBlobURL( img.url ) ) { + return; + } + onInsert( { + ...block, + attributes: { + ...block.attributes, + id: img.id, + url: img.url, + }, + } ); + createSuccessNotice( + __( 'Image uploaded and inserted.' ), + { + type: 'snackbar', + } + ); + }, + allowedTypes: ALLOWED_MEDIA_TYPES, + onError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + }, + } ); + } ) + .catch( () => { + // TODO: should we insert it with an appropriate warning? + createErrorNotice( + 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues', + { + type: 'snackbar', + } + ); + onInsert( block ); + } ); + }, + [ onInsert, mediaUpload, createErrorNotice, createSuccessNotice ] + ); const baseCssClass = 'block-editor-inserter__media-panel'; const searchLabel = category.labels.search_items || __( 'Search' ); return ( @@ -62,7 +131,7 @@ export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { { ! isLoading && !! mediaList?.length && ( From 6213ab24645a9990fe03fa194e50472475597a74 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 09:19:40 +0200 Subject: [PATCH 2/8] extract to separate hook --- .../components/inserter/media-tab/hooks.js | 81 ++++++++++++++++++- .../inserter/media-tab/media-panel.js | 74 +---------------- 2 files changed, 82 insertions(+), 73 deletions(-) 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..95602f945cf725 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -1,14 +1,25 @@ /** * WordPress dependencies */ -import { useEffect, useState, useRef, useMemo } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import { + useEffect, + useState, + useRef, + useMemo, + useCallback, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { isBlobURL } from '@wordpress/blob'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; +const ALLOWED_MEDIA_TYPES = [ 'image' ]; + /** * Interface for inserter media requests. * @@ -189,3 +200,69 @@ export function useMediaCategories( rootClientId ) { ] ); return categories; } + +export function useOnMediaInsert( onInsert ) { + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + const mediaUpload = useSelect( + ( select ) => select( blockEditorStore ).getSettings().mediaUpload, + [] + ); + return useCallback( + ( block ) => { + const { id, url, caption } = block.attributes; + // Media item already exists in library, so just insert it. + if ( !! id ) { + onInsert( block ); + return; + } + // 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; + } + onInsert( { + ...block, + attributes: { + ...block.attributes, + id: img.id, + url: img.url, + }, + } ); + createSuccessNotice( + __( 'Image uploaded and inserted.' ), + { + type: 'snackbar', + } + ); + }, + allowedTypes: ALLOWED_MEDIA_TYPES, + onError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + }, + } ); + } ) + .catch( () => { + createErrorNotice( + __( + 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues related to GDPR.' + ), + { type: 'snackbar' } + ); + onInsert( block ); + } ); + }, + [ onInsert, mediaUpload, createErrorNotice, createSuccessNotice ] + ); +} diff --git a/packages/block-editor/src/components/inserter/media-tab/media-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js index a42e8228ab34db..64f75e0bd98d19 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-panel.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js @@ -1,25 +1,20 @@ /** * WordPress dependencies */ -import { useRef, useEffect, useCallback } from '@wordpress/element'; +import { useRef, useEffect } from '@wordpress/element'; import { Spinner, SearchControl } from '@wordpress/components'; import { focus } from '@wordpress/dom'; import { __ } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; -import { isBlobURL } from '@wordpress/blob'; /** * Internal dependencies */ import MediaList from './media-list'; import useDebouncedInput from '../hooks/use-debounced-input'; -import { useMediaResults } from './hooks'; +import { useMediaResults, useOnMediaInsert } from './hooks'; import InserterNoResults from '../no-results'; -import { store as blockEditorStore } from '../../../store'; const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; -const ALLOWED_MEDIA_TYPES = [ 'image' ]; export function MediaCategoryDialog( { rootClientId, onInsert, category } ) { const container = useRef(); @@ -47,70 +42,7 @@ export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, search: debouncedSearch, } ); - const { createErrorNotice, createSuccessNotice } = - useDispatch( noticesStore ); - const mediaUpload = useSelect( - ( select ) => select( blockEditorStore ).getSettings().mediaUpload, - [] - ); - const onMediaInsert = useCallback( - ( block ) => { - const { id, url, caption } = block.attributes; - // Media item already exists in library, so just insert it. - if ( !! id ) { - onInsert( block ); - return; - } - // 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 that happens, we insert the image block using the external - // URL and let the user know about the implications of that. - window - .fetch( url ) - .then( ( response ) => response.blob() ) - .then( ( blob ) => { - mediaUpload( { - filesList: [ blob ], - additionalData: { caption }, - onFileChange( [ img ] ) { - if ( isBlobURL( img.url ) ) { - return; - } - onInsert( { - ...block, - attributes: { - ...block.attributes, - id: img.id, - url: img.url, - }, - } ); - createSuccessNotice( - __( 'Image uploaded and inserted.' ), - { - type: 'snackbar', - } - ); - }, - allowedTypes: ALLOWED_MEDIA_TYPES, - onError( message ) { - createErrorNotice( message, { type: 'snackbar' } ); - }, - } ); - } ) - .catch( () => { - // TODO: should we insert it with an appropriate warning? - createErrorNotice( - 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues', - { - type: 'snackbar', - } - ); - onInsert( block ); - } ); - }, - [ onInsert, mediaUpload, createErrorNotice, createSuccessNotice ] - ); + const onMediaInsert = useOnMediaInsert( onInsert ); const baseCssClass = 'block-editor-inserter__media-panel'; const searchLabel = category.labels.search_items || __( 'Search' ); return ( From 3ac74f20baa7ce340b31044663c8839e805c10f5 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 09:57:12 +0200 Subject: [PATCH 3/8] Check all media categories against `allowedMimeTypes` --- .../components/inserter/media-tab/hooks.js | 11 +----- .../src/components/inserter/menu.js | 34 ++++++++----------- 2 files changed, 15 insertions(+), 30 deletions(-) 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 95602f945cf725..b97c46335aec6a 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -109,13 +109,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 }/` ) ); @@ -242,9 +235,7 @@ export function useOnMediaInsert( onInsert ) { } ); createSuccessNotice( __( 'Image uploaded and inserted.' ), - { - type: 'snackbar', - } + { type: 'snackbar' } ); }, allowedTypes: ALLOWED_MEDIA_TYPES, 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 ) => { From e9bb9dee125a228594ce3e361ba2af2ba4ce75a0 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 11:18:38 +0200 Subject: [PATCH 4/8] add a spinner when is uploading image and extract Preview in separate file --- .../components/inserter/media-tab/hooks.js | 79 +------ .../inserter/media-tab/media-list.js | 125 +--------- .../inserter/media-tab/media-panel.js | 5 +- .../inserter/media-tab/media-preview.js | 214 ++++++++++++++++++ .../src/components/inserter/style.scss | 11 + 5 files changed, 232 insertions(+), 202 deletions(-) create mode 100644 packages/block-editor/src/components/inserter/media-tab/media-preview.js 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 b97c46335aec6a..2c7a6ae2983c6c 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -1,25 +1,14 @@ /** * WordPress dependencies */ -import { - useEffect, - useState, - useRef, - useMemo, - useCallback, -} from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; -import { isBlobURL } from '@wordpress/blob'; +import { useEffect, useState, useRef, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; -const ALLOWED_MEDIA_TYPES = [ 'image' ]; - /** * Interface for inserter media requests. * @@ -193,67 +182,3 @@ export function useMediaCategories( rootClientId ) { ] ); return categories; } - -export function useOnMediaInsert( onInsert ) { - const { createErrorNotice, createSuccessNotice } = - useDispatch( noticesStore ); - const mediaUpload = useSelect( - ( select ) => select( blockEditorStore ).getSettings().mediaUpload, - [] - ); - return useCallback( - ( block ) => { - const { id, url, caption } = block.attributes; - // Media item already exists in library, so just insert it. - if ( !! id ) { - onInsert( block ); - return; - } - // 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; - } - onInsert( { - ...block, - attributes: { - ...block.attributes, - id: img.id, - url: img.url, - }, - } ); - createSuccessNotice( - __( 'Image uploaded and inserted.' ), - { type: 'snackbar' } - ); - }, - allowedTypes: ALLOWED_MEDIA_TYPES, - onError( message ) { - createErrorNotice( message, { type: 'snackbar' } ); - }, - } ); - } ) - .catch( () => { - createErrorNotice( - __( - 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues related to GDPR.' - ), - { type: 'snackbar' } - ); - onInsert( block ); - } ); - }, - [ onInsert, mediaUpload, createErrorNotice, createSuccessNotice ] - ); -} 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-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js index 64f75e0bd98d19..58ae7c49d27628 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-panel.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js @@ -11,7 +11,7 @@ import { __ } from '@wordpress/i18n'; */ import MediaList from './media-list'; import useDebouncedInput from '../hooks/use-debounced-input'; -import { useMediaResults, useOnMediaInsert } from './hooks'; +import { useMediaResults } from './hooks'; import InserterNoResults from '../no-results'; const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; @@ -42,7 +42,6 @@ export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, search: debouncedSearch, } ); - const onMediaInsert = useOnMediaInsert( onInsert ); const baseCssClass = 'block-editor-inserter__media-panel'; const searchLabel = category.labels.search_items || __( 'Search' ); return ( @@ -63,7 +62,7 @@ export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { { ! isLoading && !! mediaList?.length && ( 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..13b9f538c3b173 --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -0,0 +1,214 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __unstableCompositeItem as CompositeItem, + Tooltip, + DropdownMenu, + MenuGroup, + MenuItem, + Spinner, +} 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 + ) } + + + ) } + + ); +} + +export function MediaPreview( { media, onClick, composite, category } ) { + 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( () => { + createErrorNotice( + __( + 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues related to GDPR.' + ), + { type: 'snackbar' } + ); + onClick( clonedBlock ); + 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 && ( + + ) } +
+
+
+ ) } +
+ ); +} diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 18ead7dbc484fa..204f8914525632 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 { From cfe1680ea8da779dce194cd8247ec5645fc8224f Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 11:38:41 +0200 Subject: [PATCH 5/8] Add extra safeguard agains category fetching request --- .../src/components/inserter/media-tab/hooks.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 2c7a6ae2983c6c..0822e2bf67e367 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -149,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 ]; } ) ) From fa608192d50052a3395f7448262d770ce90e6059 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 13:44:09 +0200 Subject: [PATCH 6/8] add modal if the image fails to upload --- .../inserter/media-tab/media-preview.js | 157 ++++++++++++------ .../src/components/inserter/style.scss | 11 ++ 2 files changed, 116 insertions(+), 52 deletions(-) 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 index 13b9f538c3b173..a79712c6158010 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -13,6 +13,11 @@ import { 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'; @@ -69,7 +74,47 @@ function MediaPreviewOptions( { category, media } ) { ); } +function InsertExternalImageModal( { onClose, onClick } ) { + 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 GDPR.' + ) } +

+
+ + + + + + + + +
+ ); +} + export function MediaPreview( { media, onClick, composite, category } ) { + const [ showModal, setShowModal ] = useState( false ); const [ isHovered, setIsHovered ] = useState( false ); const [ isInserting, setIsInserting ] = useState( false ); const [ block, preview ] = useMemo( @@ -134,13 +179,7 @@ export function MediaPreview( { media, onClick, composite, category } ) { } ); } ) .catch( () => { - createErrorNotice( - __( - 'The image cannot be uploaded to the media library. External images can be removed by the external provider without warning and could even have legal compliance issues related to GDPR.' - ), - { type: 'snackbar' } - ); - onClick( clonedBlock ); + setShowModal( true ); setIsInserting( false ); } ); }, @@ -162,53 +201,67 @@ export function MediaPreview( { media, onClick, composite, category } ) { const onMouseEnter = useCallback( () => setIsHovered( true ), [] ); const onMouseLeave = useCallback( () => setIsHovered( false ), [] ); return ( - - { ( { draggable, onDragStart, onDragEnd } ) => ( -
- - { /* Adding `is-hovered` class to the wrapper element is needed + <> + + { ( { 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 && ( - - ) } -
- -
+ onMediaInsert( block ) } + aria-label={ title } + > +
+ { preview } + { isInserting && ( +
+ +
+ ) } +
+
+ { ! isInserting && ( + + ) } +
+
+
+ ) } +
+ { showModal && ( + setShowModal( false ) } + onClick={ () => { + onClick( cloneBlock( block ) ); + createSuccessNotice( __( 'Image inserted.' ), { + type: 'snackbar', + } ); + setShowModal( false ); + } } + /> ) } - + ); } diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 204f8914525632..fa0778af6d53f0 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -697,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; + } +} From 15c2210ceb852829f776feb51e49fb3cbc5f15f4 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 14:20:01 +0200 Subject: [PATCH 7/8] rename some props --- .../inserter/media-tab/media-preview.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 index a79712c6158010..4851dcf123cc1b 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -74,7 +74,7 @@ function MediaPreviewOptions( { category, media } ) { ); } -function InsertExternalImageModal( { onClose, onClick } ) { +function InsertExternalImageModal( { onClose, onSubmit } ) { return ( - @@ -114,7 +114,8 @@ function InsertExternalImageModal( { onClose, onClick } ) { } export function MediaPreview( { media, onClick, composite, category } ) { - const [ showModal, setShowModal ] = useState( false ); + const [ showExternalUploadModal, setShowExternalUploadModal ] = + useState( false ); const [ isHovered, setIsHovered ] = useState( false ); const [ isInserting, setIsInserting ] = useState( false ); const [ block, preview ] = useMemo( @@ -179,7 +180,7 @@ export function MediaPreview( { media, onClick, composite, category } ) { } ); } ) .catch( () => { - setShowModal( true ); + setShowExternalUploadModal( true ); setIsInserting( false ); } ); }, @@ -250,15 +251,15 @@ export function MediaPreview( { media, onClick, composite, category } ) { ) } - { showModal && ( + { showExternalUploadModal && ( setShowModal( false ) } - onClick={ () => { + onClose={ () => setShowExternalUploadModal( false ) } + onSubmit={ () => { onClick( cloneBlock( block ) ); createSuccessNotice( __( 'Image inserted.' ), { type: 'snackbar', } ); - setShowModal( false ); + setShowExternalUploadModal( false ); } } /> ) } From 7784d6b0ac4e19ae0a4e8680fd25570106a7155a Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 28 Feb 2023 14:59:42 +0200 Subject: [PATCH 8/8] update copy --- .../src/components/inserter/media-tab/media-preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4851dcf123cc1b..88648bf96531b6 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -89,7 +89,7 @@ function InsertExternalImageModal( { onClose, onSubmit } ) {

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