initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -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;

View File

@ -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 );

View File

@ -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 ) ),
];

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 );

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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 );

View File

@ -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;