Skip to content

Commit

Permalink
Add product images block to product editor (woocommerce#37455)
Browse files Browse the repository at this point in the history
* Add images block to the product editor

* Allow html in section block descriptions

* Add changelog entry

* Add client changelog entry

* Remove SVG related changes

* Fix up lock file after rebase

* Remove unused import

* Fix up php lint errors

* Move sanitize function to utils folder
  • Loading branch information
joshuatf authored Mar 30, 2023
1 parent d3229b9 commit 681391a
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 53 deletions.
4 changes: 4 additions & 0 deletions packages/js/product-editor/changelog/add-37272
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add images block to product editor
2 changes: 2 additions & 0 deletions packages/js/product-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@wordpress/plugins": "wp-6.0",
"@wordpress/url": "wp-6.0",
"classnames": "^2.3.1",
"dompurify": "^2.3.6",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"react-router-dom": "^6.3.0"
Expand All @@ -69,6 +70,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/dompurify": "^2.3.3",
"@types/jest": "^27.4.1",
"@types/react": "^17.0.2",
"@types/testing-library__jest-dom": "^5.14.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
/**
* Internal dependencies
*/
import { init as initImages } from '../images';
import { init as initName } from '../details-name-block';
import { init as initSummary } from '../details-summary-block';
import { init as initSection } from '../section';
Expand All @@ -28,6 +29,7 @@ export const initBlocks = () => {
// @ts-ignore An argument is allowed to specify which blocks to register.
registerCoreBlocks( blocks );

initImages();
initName();
initSummary();
initSection();
Expand Down
32 changes: 32 additions & 0 deletions packages/js/product-editor/src/components/images/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-images",
"title": "Product images",
"category": "widgets",
"description": "The product images.",
"keywords": [ "products", "image", "images", "gallery" ],
"textdomain": "default",
"attributes": {
"mediaId": {
"type": "number",
"__experimentalRole": "content"
},
"images": {
"__experimentalRole": "content",
"type": "array",
"items": {
"type": "number"
},
"default": []
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
}
}
184 changes: 184 additions & 0 deletions packages/js/product-editor/src/components/images/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { CardBody, DropZone } from '@wordpress/components';
import classnames from 'classnames';
import { createElement, useState } from '@wordpress/element';
import { Icon, trash } from '@wordpress/icons';
import { MediaItem } from '@wordpress/media-utils';
import {
MediaUploader,
ImageGallery,
ImageGalleryItem,
} from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
import { useBlockProps } from '@wordpress/block-editor';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityProp } from '@wordpress/core-data';

type Image = MediaItem & {
src: string;
};

export function Edit() {
const [ images, setImages ] = useEntityProp< MediaItem[] >(
'postType',
'product',
'images'
);
const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] =
useState< boolean >( false );
const [ isRemoving, setIsRemoving ] = useState< boolean >( false );
const [ draggedImageId, setDraggedImageId ] = useState< number | null >(
null
);

const blockProps = useBlockProps( {
className: classnames( {
'has-images': images.length > 0,
} ),
} );

const toggleRemoveZone = () => {
setIsRemovingZoneVisible( ! isRemovingZoneVisible );
};

const orderImages = ( newOrder: JSX.Element[] ) => {
const orderedImages = newOrder.map( ( image ) => {
return images.find(
( file ) => file.id === parseInt( image?.props?.id, 10 )
);
} );
recordEvent( 'product_images_change_image_order_via_image_gallery' );
setImages( orderedImages as MediaItem[] );
};

const onFileUpload = ( files: MediaItem[] ) => {
if ( files[ 0 ].id ) {
recordEvent( 'product_images_add_via_file_upload_area' );
setImages( [ ...images, ...files ] );
}
};

return (
<div { ...blockProps }>
<div className="woocommerce-product-form__image-drop-zone">
{ isRemovingZoneVisible ? (
<CardBody>
<div className="woocommerce-product-form__remove-image-drop-zone">
<span>
<Icon
icon={ trash }
size={ 20 }
className="icon-control"
/>
{ __( 'Drop here to remove', 'woocommerce' ) }
</span>
<DropZone
onHTMLDrop={ () => setIsRemoving( true ) }
onDrop={ () => setIsRemoving( true ) }
label={ __(
'Drop here to remove',
'woocommerce'
) }
/>
</div>
</CardBody>
) : (
<CardBody>
<MediaUploader
multipleSelect={ true }
onError={ () => null }
onFileUploadChange={ onFileUpload }
onSelect={ ( files ) => {
const newImages = files.filter(
( img: Image ) =>
! images.find(
( image ) => image.id === img.id
)
);
if ( newImages.length > 0 ) {
recordEvent(
'product_images_add_via_media_library'
);
setImages( [ ...images, ...newImages ] );
}
} }
onUpload={ ( files ) => {
if ( files[ 0 ].id ) {
recordEvent(
'product_images_add_via_drag_and_drop_upload'
);
setImages( [ ...images, ...files ] );
}
} }
label={ '' }
/>
</CardBody>
) }
</div>
<ImageGallery
onDragStart={ ( event ) => {
const { id: imageId, dataset } =
event.target as HTMLElement;
if ( imageId ) {
setDraggedImageId( parseInt( imageId, 10 ) );
} else {
const index = dataset?.index;
if ( index ) {
setDraggedImageId(
images[ parseInt( index, 10 ) ]?.id
);
}
}
toggleRemoveZone();
} }
onDragEnd={ () => {
if ( isRemoving && draggedImageId ) {
recordEvent(
'product_images_remove_image_button_click'
);
setImages(
images.filter(
( img ) => img.id !== draggedImageId
)
);
setIsRemoving( false );
setDraggedImageId( null );
}
toggleRemoveZone();
} }
onOrderChange={ orderImages }
onReplace={ ( { replaceIndex, media } ) => {
if (
images.find( ( img ) => media.id === img.id ) ===
undefined
) {
images[ replaceIndex ] = media as MediaItem;
recordEvent(
'product_images_replace_image_button_click'
);
setImages( images );
}
} }
onSelectAsCover={ () =>
recordEvent(
'product_images_select_image_as_cover_button_click'
)
}
>
{ images.map( ( image ) => (
<ImageGalleryItem
key={ image.id || image.url }
alt={ image.alt }
src={ image.url || image.src }
id={ `${ image.id }` }
/>
) ) }
</ImageGallery>
</div>
);
}
21 changes: 21 additions & 0 deletions packages/js/product-editor/src/components/images/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.wp-block-woocommerce-product-images {
.components-card__body {
padding: 0 0 40px 0;
}
.woocommerce-media-uploader {
text-align: left;
}
.woocommerce-media-uploader__label {
display: none;
}
.woocommerce-sortable {
margin-top: 0;
padding: 0;
}

&:not(.has-images) {
.woocommerce-sortable {
display: none;
}
}
}
22 changes: 22 additions & 0 deletions packages/js/product-editor/src/components/images/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { initBlock } from '../../utils';
import metadata from './block.json';
import { Edit } from './edit';

const { name } = metadata;

export { metadata, name };

export const settings = {
example: {},
edit: Edit,
};

export const init = () =>
initBlock( {
name,
metadata: metadata as never,
settings,
} );
12 changes: 7 additions & 5 deletions packages/js/product-editor/src/components/section/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { SectionBlockAttributes } from './types';
import { BlockIcon } from '../block-icon';
import { SectionBlockAttributes } from './types';
import { sanitizeHTML } from '../../utils/sanitize-html';

export function Edit( {
attributes,
Expand All @@ -24,9 +25,10 @@ export function Edit( {
<BlockIcon clientId={ clientId } />
<span>{ title }</span>
</h2>
<p className="wp-block-woocommerce-product-section__description">
{ description }
</p>
<p
className="wp-block-woocommerce-product-section__description"
dangerouslySetInnerHTML={ sanitizeHTML( description ) }
/>
<InnerBlocks templateLock="all" />
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/js/product-editor/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@import 'components/details-categories-field/style.scss';
@import 'components/details-categories-field/create-category-modal.scss';
@import 'components/header/style.scss';
@import 'components/images/editor.scss';
@import 'components/block-editor/style.scss';
@import 'components/section/style.scss';
@import 'components/tab/style.scss';
Expand Down
13 changes: 13 additions & 0 deletions packages/js/product-editor/src/utils/sanitize-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { sanitize } from 'dompurify';

const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];

export function sanitizeHTML( html: string ) {
return {
__html: sanitize( html, { ALLOWED_TAGS, ALLOWED_ATTR } ),
};
}
4 changes: 4 additions & 0 deletions plugins/woocommerce/changelog/add-37272
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add images block to product editor template
20 changes: 20 additions & 0 deletions plugins/woocommerce/includes/class-wc-post-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,26 @@ public static function register_post_types() {
),
),
),
array(
'woocommerce/product-section',
array(
'title' => __( 'Images', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s.', 'woocommerce' ),
'<a href="http://woocommerce.com/#" target="_blank" rel="noreferrer">',
'</a>'
),
),
array(
array(
'woocommerce/product-images',
array(
'images' => array(),
),
),
),
),
),
),
array(
Expand Down
Loading

0 comments on commit 681391a

Please sign in to comment.