initial commit
This commit is contained in:
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
export const blockAttributes = {
|
||||
isPreview: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
save: false,
|
||||
},
|
||||
/**
|
||||
* The product ID to display.
|
||||
*/
|
||||
productId: {
|
||||
type: 'number',
|
||||
},
|
||||
};
|
||||
|
||||
export default blockAttributes;
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { withProduct } from '@woocommerce/block-hocs';
|
||||
import {
|
||||
InnerBlockLayoutContextProvider,
|
||||
ProductDataContextProvider,
|
||||
} from '@woocommerce/shared-context';
|
||||
import { StoreNoticesProvider } from '@woocommerce/base-context';
|
||||
import { useStoreEvents } from '@woocommerce/base-context/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { BLOCK_NAME } from './constants';
|
||||
|
||||
/** @typedef {import('react')} React */
|
||||
|
||||
/**
|
||||
* The Single Product Block.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.isLoading
|
||||
* @param {Object} props.product
|
||||
* @param {React.ReactChildren} props.children
|
||||
*/
|
||||
const Block = ( { isLoading, product, children } ) => {
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
const className = 'wc-block-single-product wc-block-layout';
|
||||
const noticeContext = `woocommerce/single-product/${ product?.id || 0 }`;
|
||||
|
||||
useEffect( () => {
|
||||
dispatchStoreEvent( 'product-render', {
|
||||
product,
|
||||
listName: BLOCK_NAME,
|
||||
} );
|
||||
}, [ product, dispatchStoreEvent ] );
|
||||
|
||||
return (
|
||||
<InnerBlockLayoutContextProvider
|
||||
parentName={ BLOCK_NAME }
|
||||
parentClassName={ className }
|
||||
>
|
||||
<ProductDataContextProvider
|
||||
product={ product }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<StoreNoticesProvider context={ noticeContext }>
|
||||
<div className={ className }>{ children }</div>
|
||||
</StoreNoticesProvider>
|
||||
</ProductDataContextProvider>
|
||||
</InnerBlockLayoutContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProduct( Block );
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, reader } from '@woocommerce/icons';
|
||||
import { getBlockMap } from '@woocommerce/atomic-utils';
|
||||
|
||||
export const BLOCK_NAME = 'woocommerce/single-product';
|
||||
export const BLOCK_TITLE = __(
|
||||
'Single Product',
|
||||
'woocommerce'
|
||||
);
|
||||
export const BLOCK_ICON = <Icon srcElement={ reader } />;
|
||||
export const BLOCK_DESCRIPTION = __(
|
||||
'Display a single product.',
|
||||
'woocommerce'
|
||||
);
|
||||
|
||||
export const DEFAULT_INNER_BLOCKS = [
|
||||
[
|
||||
'core/columns',
|
||||
{},
|
||||
[
|
||||
[
|
||||
'core/column',
|
||||
{},
|
||||
[ [ 'woocommerce/product-image', { showSaleBadge: false } ] ],
|
||||
],
|
||||
[
|
||||
'core/column',
|
||||
{},
|
||||
[
|
||||
[ 'woocommerce/product-sale-badge' ],
|
||||
[ 'woocommerce/product-title', { headingLevel: 2 } ],
|
||||
[ 'woocommerce/product-rating' ],
|
||||
[ 'woocommerce/product-price' ],
|
||||
[ 'woocommerce/product-summary' ],
|
||||
[ 'woocommerce/product-stock-indicator' ],
|
||||
[
|
||||
'woocommerce/product-add-to-cart',
|
||||
{ showFormElements: true },
|
||||
],
|
||||
[ 'woocommerce/product-sku' ],
|
||||
[ 'woocommerce/product-category-list' ],
|
||||
[ 'woocommerce/product-tag-list' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
export const ALLOWED_INNER_BLOCKS = [
|
||||
'core/columns',
|
||||
'core/column',
|
||||
...Object.keys( getBlockMap( BLOCK_NAME ) ),
|
||||
];
|
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
|
||||
|
||||
/**
|
||||
* Shown when there is an API error getting a product.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.error
|
||||
* @param {boolean} props.isLoading
|
||||
* @param {function(any):any} props.getProduct
|
||||
*/
|
||||
const ApiError = ( { error, isLoading, getProduct } ) => (
|
||||
<ErrorPlaceholder
|
||||
className="wc-block-single-product-error"
|
||||
error={ error }
|
||||
isLoading={ isLoading }
|
||||
onRetry={ getProduct }
|
||||
/>
|
||||
);
|
||||
|
||||
export default ApiError;
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { BlockControls } from '@wordpress/block-editor';
|
||||
import { ToolbarGroup } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Adds controls to the editor toolbar.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.isEditing
|
||||
* @param {function(boolean):any} props.setIsEditing
|
||||
*/
|
||||
const EditorBlockControls = ( { isEditing, setIsEditing } ) => {
|
||||
return (
|
||||
<BlockControls>
|
||||
<ToolbarGroup
|
||||
controls={ [
|
||||
{
|
||||
icon: 'edit',
|
||||
title: __( 'Edit', 'woocommerce' ),
|
||||
onClick: () => setIsEditing( ! isEditing ),
|
||||
isActive: isEditing,
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</BlockControls>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorBlockControls;
|
@ -0,0 +1,18 @@
|
||||
.wc-block-single-product__selection {
|
||||
width: 100%;
|
||||
}
|
||||
.wc-block-single-product__reset-layout {
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.wc-block-single-product__edit-card {
|
||||
padding: 16px;
|
||||
border-top: 1px solid $gray-200;
|
||||
|
||||
.wc-block-single-product__edit-card-title {
|
||||
margin: 0 0 $gap;
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Placeholder, Button, PanelBody } from '@wordpress/components';
|
||||
import { withProduct } from '@woocommerce/block-hocs';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import EditProductLink from '@woocommerce/editor-components/edit-product-link';
|
||||
import { singleProductBlockPreview } from '@woocommerce/resource-previews';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './editor.scss';
|
||||
import ApiError from './api-error';
|
||||
import SharedProductControl from './shared-product-control';
|
||||
import EditorBlockControls from './editor-block-controls';
|
||||
import LayoutEditor from './layout-editor';
|
||||
import { BLOCK_TITLE, BLOCK_ICON, BLOCK_DESCRIPTION } from '../constants';
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of the "Single Product Block".
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.className
|
||||
* @param {Object} props.attributes Incoming block attributes.
|
||||
* @param {function(any):any} props.setAttributes Setter for block attributes.
|
||||
* @param {string} props.error
|
||||
* @param {function(any):any} props.getProduct
|
||||
* @param {Object} props.product
|
||||
* @param {boolean} props.isLoading
|
||||
* @param {string} props.clientId
|
||||
*/
|
||||
const Editor = ( {
|
||||
className,
|
||||
attributes,
|
||||
setAttributes,
|
||||
error,
|
||||
getProduct,
|
||||
product,
|
||||
isLoading,
|
||||
clientId,
|
||||
} ) => {
|
||||
const { productId, isPreview } = attributes;
|
||||
const [ isEditing, setIsEditing ] = useState( ! productId );
|
||||
|
||||
if ( isPreview ) {
|
||||
return singleProductBlockPreview;
|
||||
}
|
||||
|
||||
if ( error ) {
|
||||
return (
|
||||
<ApiError
|
||||
error={ error }
|
||||
isLoading={ isLoading }
|
||||
getProduct={ getProduct }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ className }>
|
||||
<BlockErrorBoundary
|
||||
header={ __(
|
||||
'Single Product Block Error',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<EditorBlockControls
|
||||
setIsEditing={ setIsEditing }
|
||||
isEditing={ isEditing }
|
||||
/>
|
||||
{ isEditing ? (
|
||||
<Placeholder
|
||||
icon={ BLOCK_ICON }
|
||||
label={ BLOCK_TITLE }
|
||||
className="wc-block-single-product"
|
||||
>
|
||||
{ BLOCK_DESCRIPTION }
|
||||
<div className="wc-block-single-product__selection">
|
||||
<SharedProductControl
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
/>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={ () => {
|
||||
setIsEditing( false );
|
||||
} }
|
||||
>
|
||||
{ __( 'Done', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</Placeholder>
|
||||
) : (
|
||||
<>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Product',
|
||||
'woocommerce'
|
||||
) }
|
||||
initialOpen={ false }
|
||||
>
|
||||
<SharedProductControl
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<EditProductLink productId={ productId } />
|
||||
<LayoutEditor
|
||||
clientId={ clientId }
|
||||
product={ product }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</BlockErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProduct( Editor );
|
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { InnerBlocks, InspectorControls } from '@wordpress/block-editor';
|
||||
import {
|
||||
InnerBlockLayoutContextProvider,
|
||||
ProductDataContextProvider,
|
||||
} from '@woocommerce/shared-context';
|
||||
import { createBlocksFromTemplate } from '@woocommerce/atomic-utils';
|
||||
import { PanelBody, Button } from '@wordpress/components';
|
||||
import { Icon, restore } from '@woocommerce/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
BLOCK_NAME,
|
||||
DEFAULT_INNER_BLOCKS,
|
||||
ALLOWED_INNER_BLOCKS,
|
||||
} from '../constants';
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of the "Single Product Block".
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.isLoading
|
||||
* @param {Object} props.product
|
||||
* @param {string} props.clientId
|
||||
*/
|
||||
const LayoutEditor = ( { isLoading, product, clientId } ) => {
|
||||
const baseClassName = 'wc-block-single-product wc-block-layout';
|
||||
const { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
|
||||
|
||||
const resetInnerBlocks = useCallback( () => {
|
||||
replaceInnerBlocks(
|
||||
clientId,
|
||||
createBlocksFromTemplate( DEFAULT_INNER_BLOCKS ),
|
||||
false
|
||||
);
|
||||
}, [ clientId, replaceInnerBlocks ] );
|
||||
|
||||
return (
|
||||
<InnerBlockLayoutContextProvider
|
||||
parentName={ BLOCK_NAME }
|
||||
parentClassName={ baseClassName }
|
||||
>
|
||||
<ProductDataContextProvider
|
||||
product={ product }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __( 'Layout', 'woocommerce' ) }
|
||||
initialOpen={ true }
|
||||
>
|
||||
<Button
|
||||
label={ __(
|
||||
'Reset layout to default',
|
||||
'woocommerce'
|
||||
) }
|
||||
onClick={ resetInnerBlocks }
|
||||
isTertiary
|
||||
className="wc-block-single-product__reset-layout"
|
||||
>
|
||||
<Icon srcElement={ restore } />{ ' ' }
|
||||
{ __(
|
||||
'Reset layout',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Button>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<div className={ baseClassName }>
|
||||
<InnerBlocks
|
||||
template={ DEFAULT_INNER_BLOCKS }
|
||||
allowedBlocks={ ALLOWED_INNER_BLOCKS }
|
||||
templateLock={ false }
|
||||
renderAppender={ false }
|
||||
/>
|
||||
</div>
|
||||
</ProductDataContextProvider>
|
||||
</InnerBlockLayoutContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutEditor;
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||
|
||||
/**
|
||||
* Allows a product to be selected for display.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.attributes Incoming block attributes.
|
||||
* @param {function(any):any} props.setAttributes Setter for block attributes.
|
||||
*/
|
||||
const SharedProductControl = ( { attributes, setAttributes } ) => (
|
||||
<ProductControl
|
||||
selected={ attributes.productId || 0 }
|
||||
showVariations
|
||||
onChange={ ( value = [] ) => {
|
||||
const id = value[ 0 ] ? value[ 0 ].id : 0;
|
||||
setAttributes( {
|
||||
productId: id,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
export default SharedProductControl;
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getValidBlockAttributes } from '@woocommerce/base-utils';
|
||||
import {
|
||||
getBlockMap,
|
||||
renderParentBlock,
|
||||
renderStandaloneBlocks,
|
||||
} from '@woocommerce/atomic-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import blockAttributes from './attributes';
|
||||
import { BLOCK_NAME } from './constants';
|
||||
|
||||
const getProps = ( el ) => {
|
||||
return {
|
||||
attributes: getValidBlockAttributes( blockAttributes, el.dataset ),
|
||||
};
|
||||
};
|
||||
|
||||
renderParentBlock( {
|
||||
Block,
|
||||
blockName: BLOCK_NAME,
|
||||
selector: '.wp-block-woocommerce-single-product',
|
||||
getProps,
|
||||
blockMap: getBlockMap( BLOCK_NAME ),
|
||||
} );
|
||||
|
||||
renderStandaloneBlocks();
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerExperimentalBlockType } from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit';
|
||||
import save from './save';
|
||||
import attributes from './attributes';
|
||||
import {
|
||||
BLOCK_NAME,
|
||||
BLOCK_TITLE,
|
||||
BLOCK_ICON,
|
||||
BLOCK_DESCRIPTION,
|
||||
} from './constants';
|
||||
|
||||
const settings = {
|
||||
title: BLOCK_TITLE,
|
||||
icon: {
|
||||
src: BLOCK_ICON,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
|
||||
description: BLOCK_DESCRIPTION,
|
||||
supports: {
|
||||
align: [ 'wide', 'full' ],
|
||||
html: false,
|
||||
},
|
||||
example: {
|
||||
attributes: {
|
||||
isPreview: true,
|
||||
},
|
||||
},
|
||||
attributes,
|
||||
edit,
|
||||
save,
|
||||
};
|
||||
|
||||
registerExperimentalBlockType( BLOCK_NAME, settings );
|
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { InnerBlocks } from '@wordpress/block-editor';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Save = ( { attributes } ) => {
|
||||
return (
|
||||
<div className={ classnames( 'is-loading', attributes.className ) }>
|
||||
<InnerBlocks.Content />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Save;
|
Reference in New Issue
Block a user