Skip to content

Commit

Permalink
Prepublish: suggest uploading external images (#46014)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Jun 23, 2023
1 parent 96150d5 commit 62a7316
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 11 deletions.
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 + '?' )
.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
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 {
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

0 comments on commit 62a7316

Please sign in to comment.