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

Prepublish: suggest uploading external images #46014

Merged
merged 17 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions packages/block-library/src/image/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TextareaControl,
TextControl,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { useViewportMatch, usePrevious } from '@wordpress/compose';
import { useSelect, useDispatch } from '@wordpress/data';
Expand Down Expand Up @@ -175,14 +176,17 @@ export default function Image( {
if (
! isExternalImage( id, url ) ||
! isSelected ||
! canUploadMedia ||
externalBlob
! canUploadMedia
) {
setExternalBlob();
return;
}

if ( externalBlob ) return;

window
.fetch( url )
// Avoid cache, which seems to help avoid CORS problems.
.fetch( url.includes( '?' ) ? url : url + '?' )
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
.then( ( response ) => response.blob() )
.then( ( blob ) => setExternalBlob( blob ) )
// Do nothing, cannot upload.
Expand Down Expand Up @@ -370,13 +374,6 @@ export default function Image( {
label={ __( 'Crop' ) }
/>
) }
{ externalBlob && (
<ToolbarButton
onClick={ uploadExternal }
icon={ upload }
label={ __( 'Upload external image' ) }
/>
) }
{ ! multiImageSelection && canInsertCover && (
<ToolbarButton
icon={ overlayText }
Expand All @@ -398,6 +395,17 @@ export default function Image( {
/>
</BlockControls>
) }
{ ! multiImageSelection && externalBlob && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ uploadExternal }
icon={ upload }
label={ __( 'Upload external image' ) }
/>
</ToolbarGroup>
</BlockControls>
) }
<InspectorControls>
<PanelBody title={ __( 'Settings' ) }>
{ ! multiImageSelection && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* WordPress dependencies
*/
import {
PanelBody,
Button,
Spinner,
__unstableMotion as motion,
__unstableAnimatePresence as AnimatePresence,
} from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { upload } from '@wordpress/icons';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
import { isBlobURL } from '@wordpress/blob';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';

function flattenBlocks( blocks ) {
const result = [];

blocks.forEach( ( block ) => {
result.push( block );
result.push( ...flattenBlocks( block.innerBlocks ) );
} );

return result;
}

function Image( block ) {
const { selectBlock } = useDispatch( blockEditorStore );
return (
<motion.img
tabIndex={ 0 }
role="button"
aria-label={ __( 'Select image block.' ) }
onClick={ () => {
selectBlock( block.clientId );
} }
onKeyDown={ ( event ) => {
if ( event.key === 'Enter' || event.key === ' ' ) {
selectBlock( block.clientId );
event.preventDefault();
}
} }
key={ block.clientId }
alt={ block.attributes.alt }
src={ block.attributes.url }
animate={ { opacity: 1 } }
exit={ { opacity: 0, scale: 0 } }
style={ {
width: '36px',
height: '36px',
objectFit: 'cover',
borderRadius: '2px',
cursor: 'pointer',
} }
whileHover={ { scale: 1.08 } }
/>
);
}

export default function PostFormatPanel() {
const [ isUploading, setIsUploading ] = useState( false );
const { editorBlocks, mediaUpload } = useSelect(
( select ) => ( {
editorBlocks: select( editorStore ).getEditorBlocks(),
mediaUpload: select( blockEditorStore ).getSettings().mediaUpload,
} ),
[]
);
const externalImages = flattenBlocks( editorBlocks ).filter(
( block ) =>
block.name === 'core/image' &&
block.attributes.url &&
! block.attributes.id
);
const { updateBlockAttributes } = useDispatch( blockEditorStore );

if ( ! mediaUpload || ! externalImages.length ) {
return null;
}

const panelBodyTitle = [
__( 'Suggestion:' ),
<span className="editor-post-publish-panel__link" key="label">
{ __( 'External media' ) }
</span>,
];

function uploadImages() {
setIsUploading( true );
Promise.all(
externalImages.map( ( image ) =>
window
.fetch(
image.attributes.url.includes( '?' )
? image.attributes.url
: image.attributes.url + '?'
)
.then( ( response ) => response.blob() )
.then(
( blob ) =>
new Promise( ( resolve, reject ) => {
mediaUpload( {
filesList: [ blob ],
onFileChange: ( [ media ] ) => {
if ( isBlobURL( media.url ) ) {
return;
}

updateBlockAttributes( image.clientId, {
id: media.id,
url: media.url,
} );
resolve();
},
onError() {
reject();
},
} );
} )
)
)
).finally( () => {
setIsUploading( false );
} );
}

return (
<PanelBody initialOpen={ true } title={ panelBodyTitle }>
<p>
{ __(
'There are some external images in the post which can be uploaded to the media library. Images coming from different domains may not always display correctly, load slowly for visitors, or be removed unexpectedly.'
) }
</p>
<div
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
style={ {
display: 'inline-flex',
flexWrap: 'wrap',
gap: '8px',
} }
>
<AnimatePresence>
{ externalImages.map( ( image ) => {
return <Image key={ image.clientId } { ...image } />;
} ) }
</AnimatePresence>
{ isUploading ? (
<Spinner />
) : (
<Button
icon={ upload }
variant="primary"
onClick={ uploadImages }
>
{ __( 'Upload all' ) }
</Button>
) }
</div>
</PanelBody>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import MaybeTagsPanel from './maybe-tags-panel';
import MaybePostFormatPanel from './maybe-post-format-panel';
import { store as editorStore } from '../../store';
import MaybeCategoryPanel from './maybe-category-panel';
import MaybeUploadMedia from './maybe-upload-media';

function PostPublishPanelPrepublish( { children } ) {
const {
Expand Down Expand Up @@ -103,6 +104,7 @@ function PostPublishPanelPrepublish( { children } ) {
<span className="components-site-home">{ siteHome }</span>
</div>
</div>
<MaybeUploadMedia />
{ hasPublishAction && (
<>
<PanelBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Ensure the post-publish panel accounts for the header and footer height.
min-height: calc(100% - #{$header-height + 84px});

.components-spinner {
> .components-spinner {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems quite specific here and this component is public API. Should we instead style your new Spinner instead? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

So this is styling another spinner in the same pannel and we want the new spinner to have no extra styling. Changed it here so this extra CSS only affects the existing spinner.

display: block;
margin: 100px auto 0;
}
Expand Down
46 changes: 46 additions & 0 deletions test/e2e/specs/editor/blocks/image.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,52 @@ test.describe( 'Image', () => {
await expect( linkDom ).toBeVisible();
await expect( linkDom ).toHaveAttribute( 'href', url );
} );

test( 'should upload external image', async ( { editor } ) => {
await editor.insertBlock( {
name: 'core/image',
attributes: {
url: 'https://cldup.com/cXyG__fTLN.jpg',
},
} );

await editor.clickBlockToolbarButton( 'Upload external image' );

const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'img[src^="http"]' );
const src = await image.getAttribute( 'src' );

expect( src ).toMatch( /\/wp-content\/uploads\// );
} );

test( 'should upload through prepublish panel', async ( {
editor,
page,
} ) => {
await editor.insertBlock( {
name: 'core/image',
attributes: {
url: 'https://cldup.com/cXyG__fTLN.jpg',
},
} );

await page
.getByRole( 'button', { name: 'Publish', exact: true } )
.click();
await page.getByRole( 'button', { name: 'Upload all' } ).click();

await expect( page.locator( '.components-spinner' ) ).toHaveCount( 0 );

const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'img[src^="http"]' );
const src = await image.getAttribute( 'src' );

expect( src ).toMatch( /\/wp-content\/uploads\// );
} );
} );

test.describe( 'Image - interactivity', () => {
Expand Down