Skip to content
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

Merged
merged 8 commits into from
Feb 28, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Copy link
Contributor Author

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.

return true;
}
return Object.values( allowedMimeTypes ).some( ( mimeType ) =>
mimeType.startsWith( `${ category.mediaType }/` )
);
Expand Down
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,
Expand All @@ -132,12 +19,6 @@ function MediaList( {
label = __( 'Media List' ),
} ) {
const composite = useCompositeState();
const onPreviewClick = useCallback(
( block ) => {
onClick( cloneBlock( block ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the cloneBlock call inside the MediaPreview callback.

},
[ onClick ]
);
return (
<Composite
{ ...composite }
Expand All @@ -150,7 +31,7 @@ function MediaList( {
key={ media.id || media.sourceId || index }
media={ media }
category={ category }
onClick={ onPreviewClick }
onClick={ onClick }
composite={ composite }
/>
) ) }
Expand Down
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 } ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MediaPreviewOptions component had no changes here. Just moved it together with MediaPreview component.

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(
Copy link
Contributor Author

@ntsekouras ntsekouras Feb 28, 2023

Choose a reason for hiding this comment

The 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 isInserting to show conditionally the spinner and the MediaPreviewOptions.

The remaining code of this component was moved as it was in this separate file.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 block-editor store. Not sure exactly what API it would have but it feels heavy to do all these async calls in the components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 =
Copy link
Contributor

Choose a reason for hiding this comment

The 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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
);
}
Loading