-
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
[Inserter - Media tab]: Upload Openverse images when inserted #48501
Changes from 4 commits
ad8bbf2
6213ab2
3ac74f2
e9bb9de
cfe1680
fa60819
15c2210
7784d6b
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 |
---|---|---|
@@ -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 ( | ||
<DropdownMenu | ||
className="block-editor-inserter__media-list__item-preview-options" | ||
label={ __( 'Options' ) } | ||
popoverProps={ MEDIA_OPTIONS_POPOVER_PROPS } | ||
icon={ moreVertical } | ||
> | ||
{ () => ( | ||
<MenuGroup> | ||
<MenuItem | ||
onClick={ () => | ||
window.open( reportUrl, '_blank' ).focus() | ||
} | ||
icon={ external } | ||
> | ||
{ sprintf( | ||
/* translators: %s: The media type to report e.g: "image", "video", "audio" */ | ||
__( 'Report %s' ), | ||
category.mediaType | ||
) } | ||
</MenuItem> | ||
</MenuGroup> | ||
) } | ||
</DropdownMenu> | ||
); | ||
} | ||
|
||
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 ( | ||
<InserterDraggableBlocks isEnabled={ true } blocks={ [ block ] }> | ||
{ ( { draggable, onDragStart, onDragEnd } ) => ( | ||
<div | ||
className={ classnames( | ||
'block-editor-inserter__media-list__list-item', | ||
{ | ||
'is-hovered': isHovered, | ||
} | ||
) } | ||
draggable={ draggable } | ||
onDragStart={ onDragStart } | ||
onDragEnd={ onDragEnd } | ||
> | ||
<Tooltip text={ truncatedTitle || title }> | ||
{ /* Adding `is-hovered` class to the wrapper element is needed | ||
because the options Popover is rendered outside of this node. */ } | ||
<div | ||
onMouseEnter={ onMouseEnter } | ||
onMouseLeave={ onMouseLeave } | ||
> | ||
<CompositeItem | ||
role="option" | ||
as="div" | ||
{ ...composite } | ||
className="block-editor-inserter__media-list__item" | ||
onClick={ () => onClick( block ) } | ||
aria-label={ title } | ||
> | ||
<div className="block-editor-inserter__media-list__item-preview"> | ||
{ preview } | ||
</div> | ||
</CompositeItem> | ||
<MediaPreviewOptions | ||
category={ category } | ||
media={ media } | ||
/> | ||
</div> | ||
</Tooltip> | ||
</div> | ||
) } | ||
</InserterDraggableBlocks> | ||
); | ||
} | ||
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 ) ); | ||
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 moved the |
||
}, | ||
[ onClick ] | ||
); | ||
return ( | ||
<Composite | ||
{ ...composite } | ||
|
@@ -150,7 +31,7 @@ function MediaList( { | |
key={ media.id || media.sourceId || index } | ||
media={ media } | ||
category={ category } | ||
onClick={ onPreviewClick } | ||
onClick={ onClick } | ||
composite={ composite } | ||
/> | ||
) ) } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 } ) { | ||
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.
|
||
if ( ! category.getReportUrl ) { | ||
return null; | ||
} | ||
const reportUrl = category.getReportUrl( media ); | ||
return ( | ||
<DropdownMenu | ||
className="block-editor-inserter__media-list__item-preview-options" | ||
label={ __( 'Options' ) } | ||
popoverProps={ MEDIA_OPTIONS_POPOVER_PROPS } | ||
icon={ moreVertical } | ||
> | ||
{ () => ( | ||
<MenuGroup> | ||
<MenuItem | ||
onClick={ () => | ||
window.open( reportUrl, '_blank' ).focus() | ||
} | ||
icon={ external } | ||
> | ||
{ sprintf( | ||
/* translators: %s: The media type to report e.g: "image", "video", "audio" */ | ||
__( 'Report %s' ), | ||
category.mediaType | ||
) } | ||
</MenuItem> | ||
</MenuGroup> | ||
) } | ||
</DropdownMenu> | ||
); | ||
} | ||
|
||
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( | ||
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. This is the main change in this file, alongside a couple of checks of The remaining code of this component was moved as it was in this separate file. 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. It seems that some of these callbacks could be move to be an actual action in 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'll take a look at this in a follow up too, if that's okay. |
||
( 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 ); | ||
ntsekouras marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} ); | ||
}, | ||
[ | ||
isInserting, | ||
onClick, | ||
mediaUpload, | ||
createErrorNotice, | ||
createSuccessNotice, | ||
] | ||
); | ||
const title = media.title?.rendered || media.title; | ||
let truncatedTitle; | ||
if ( title.length > MAXIMUM_TITLE_LENGTH ) { | ||
const omission = '...'; | ||
truncatedTitle = | ||
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 think manipulating strings like that is not great. It has a potential of not working properly depending on locals... Can we truncate using CSS instead? Or use some prior work here. (Truncate component maybe though I've never used this one) 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. Let me handle this in a follow up outside of this PR and 6.2, to make sure we don't introduce any regression at this point of the release. |
||
title.slice( 0, MAXIMUM_TITLE_LENGTH - omission.length ) + omission; | ||
} | ||
const onMouseEnter = useCallback( () => setIsHovered( true ), [] ); | ||
const onMouseLeave = useCallback( () => setIsHovered( false ), [] ); | ||
return ( | ||
<InserterDraggableBlocks isEnabled={ true } blocks={ [ block ] }> | ||
{ ( { draggable, onDragStart, onDragEnd } ) => ( | ||
<div | ||
className={ classnames( | ||
'block-editor-inserter__media-list__list-item', | ||
{ | ||
'is-hovered': isHovered, | ||
} | ||
) } | ||
draggable={ draggable } | ||
onDragStart={ onDragStart } | ||
onDragEnd={ onDragEnd } | ||
> | ||
<Tooltip text={ truncatedTitle || title }> | ||
{ /* Adding `is-hovered` class to the wrapper element is needed | ||
because the options Popover is rendered outside of this node. */ } | ||
<div | ||
onMouseEnter={ onMouseEnter } | ||
onMouseLeave={ onMouseLeave } | ||
> | ||
<CompositeItem | ||
role="option" | ||
as="div" | ||
{ ...composite } | ||
className="block-editor-inserter__media-list__item" | ||
onClick={ () => onMediaInsert( block ) } | ||
aria-label={ title } | ||
> | ||
<div className="block-editor-inserter__media-list__item-preview"> | ||
{ preview } | ||
{ isInserting && ( | ||
<div className="block-editor-inserter__media-list__item-preview-spinner"> | ||
<Spinner /> | ||
</div> | ||
) } | ||
</div> | ||
</CompositeItem> | ||
{ ! isInserting && ( | ||
<MediaPreviewOptions | ||
category={ category } | ||
media={ media } | ||
/> | ||
) } | ||
</div> | ||
</Tooltip> | ||
</div> | ||
) } | ||
</InserterDraggableBlocks> | ||
); | ||
} |
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.
Now we upload the images from external sources, so we have to check the
allowedMimeTypes
setting.