Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Convert all products edit to TypeScript #9782

Open
wants to merge 13 commits into
base: trunk
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import { createBlock, BlockInstance } from '@wordpress/blocks';
import {
BlockControls,
InnerBlocks,
InspectorControls,
} from '@wordpress/block-editor';
import { withDispatch, withSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import {
PanelBody,
withSpokenMessages,
Placeholder,
Button,
ToolbarGroup,
Disabled,
Tip,
} from '@wordpress/components';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { useState, useEffect } from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';
import { Icon, grid } from '@wordpress/icons';
import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control';
import {
Expand All @@ -31,6 +29,7 @@ import { getBlockMap } from '@woocommerce/atomic-utils';
import { previewProducts } from '@woocommerce/resource-previews';
import { getSetting } from '@woocommerce/settings';
import { blocksConfig } from '@woocommerce/block-settings';
import { speak } from '@wordpress/a11y';

/**
* Internal dependencies
Expand All @@ -48,51 +47,58 @@ import { getSharedContentControls, getSharedListControls } from '../edit';
import Block from './block';
import './editor.scss';

/**
* Component to handle edit mode of "All Products".
*/
class Editor extends Component {
static propTypes = {
/**
* The attributes for this block.
*/
attributes: PropTypes.object.isRequired,
/**
* A callback to update attributes.
*/
setAttributes: PropTypes.func.isRequired,
/**
* From withSpokenMessages.
*/
debouncedSpeak: PropTypes.func.isRequired,
};
type Attributes = {
columns: number;
rows: number;
alignButtons: boolean;
layoutConfig: [ string, object? ][];
};

state = {
isEditing: false,
innerBlocks: [],
};
type Props = {
contentVisibility: number;
orderby: number;
isPreview: number;
clientId: string;
attributes: Attributes;
setAttributes: ( attributes: Record< string, unknown > ) => void;
};

blockMap = getBlockMap( 'woocommerce/all-products' );
export default function Edit( props: Props ): JSX.Element {
const [ isEditing, setIsEditing ] = useState< boolean >( false );
const [ , setInnerBlocks ] = useState< BlockInstance[] | boolean >( false );
Copy link
Contributor

Choose a reason for hiding this comment

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

To preserve the logic, I believe the initial state should be an empty array as per previous implementation:

	state = {
		isEditing: false,
		innerBlocks: [],
	};

Also, could you shed more light on why there's just a setInnerBlocks function in use, but no innerBlocks value like:

Suggested change
const [ , setInnerBlocks ] = useState< BlockInstance[] | boolean >( false );
const [ innerBlocks, setInnerBlocks ] = useState< BlockInstance[] | boolean >( false );

const blockMap = getBlockMap( 'woocommerce/all-products' );

componentDidMount = () => {
const { block } = this.props;
this.setState( { innerBlocks: block.innerBlocks } );
};
const { clientId, attributes, setAttributes } = props;

const { columns, rows, alignButtons, layoutConfig } = attributes;

getTitle = () => {
const { innerBlocks } = useSelect( ( select ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see the component logic is based on the value returned from useSelect, while it should be based on the value from useState. I think it's related to the question from the previous comment about why the innerBlocks from useState is not in use.

const { getBlock } = select( 'core/block-editor' );
const block = getBlock( clientId );
return {
innerBlocks: block ? block.innerBlocks : [],
};
} );

useEffect( () => {
setInnerBlocks( innerBlocks );
}, [] );

const { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
const debouncedSpeak = useDebounce( speak );
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason we cannot use the debouncedSpeak provided through the props as it was before, but rather create a new one?

Copy link
Member Author

Choose a reason for hiding this comment

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

@kmanijak Let me know if I've understood it incorrectly. Previously, debouncedSpeak was the props added by the withSpokenMessages HOC and I've replaced that with the useDebounce hook. Is that not correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

Previously, debouncedSpeak was the props added by the withSpokenMessages HOC and I've replaced that with the useDebounce hook. Is that not correct?

Oh, I didn't spot that. But I think we could stick to keep using withSpokenMessages which is used across multiple blocks. Is there a reason to replace it with a custom implementation?


const getTitle = (): string => {
return __( 'All Products', 'woo-gutenberg-products-block' );
};

getIcon = () => {
const getIcon = (): JSX.Element => {
return <Icon icon={ grid } />;
};

togglePreview = () => {
const { debouncedSpeak } = this.props;

this.setState( { isEditing: ! this.state.isEditing } );
const togglePreview = (): void => {
setIsEditing( ! isEditing );

if ( ! this.state.isEditing ) {
if ( ! isEditing ) {
debouncedSpeak(
__(
'Showing All Products block preview.',
Expand All @@ -102,10 +108,7 @@ class Editor extends Component {
}
};

getInspectorControls = () => {
const { attributes, setAttributes } = this.props;
const { columns, rows, alignButtons } = attributes;

const getInspectorControls = (): JSX.Element => {
return (
<InspectorControls key="inspector">
<PanelBody
Expand All @@ -120,10 +123,10 @@ class Editor extends Component {
rows={ rows }
alignButtons={ alignButtons }
setAttributes={ setAttributes }
minColumns={ getSetting( 'min_columns', 1 ) }
maxColumns={ getSetting( 'max_columns', 6 ) }
minRows={ getSetting( 'min_rows', 1 ) }
maxRows={ getSetting( 'max_rows', 6 ) }
minColumns={ getSetting( 'min_columns', 1 ) as number }
maxColumns={ getSetting( 'max_columns', 6 ) as number }
minRows={ getSetting( 'min_rows', 1 ) as number }
maxRows={ getSetting( 'max_rows', 6 ) as number }
/>
</PanelBody>
<PanelBody
Expand All @@ -139,9 +142,7 @@ class Editor extends Component {
);
};

getBlockControls = () => {
const { isEditing } = this.state;

const getBlockControls = (): JSX.Element => {
return (
<BlockControls>
<ToolbarGroup
Expand All @@ -152,7 +153,7 @@ class Editor extends Component {
'Edit the layout of each product',
'woo-gutenberg-products-block'
),
onClick: () => this.togglePreview(),
onClick: () => togglePreview(),
isActive: isEditing,
},
] }
Expand All @@ -161,46 +162,47 @@ class Editor extends Component {
);
};

renderEditMode = () => {
const renderEditMode = () => {
const onDone = () => {
const { block, setAttributes } = this.props;
setAttributes( {
layoutConfig: getProductLayoutConfig( block.innerBlocks ),
layoutConfig: getProductLayoutConfig( innerBlocks ),
} );
this.setState( { innerBlocks: block.innerBlocks } );
this.togglePreview();
setInnerBlocks( innerBlocks );
togglePreview();
};

const onCancel = () => {
const { block, replaceInnerBlocks } = this.props;
const { innerBlocks } = this.state;
replaceInnerBlocks( block.clientId, innerBlocks, false );
this.togglePreview();
replaceInnerBlocks( clientId, innerBlocks, false );
Copy link
Contributor

Choose a reason for hiding this comment

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

When you use Cancel button on the UI and go back to edit mode, the previous state is preserved. It may be related to using innerBlocks from useSelect rather than useState.

Steps to reproduce:

  1. Go to Edit mode of All Products
  2. Add some additional Products Elements
  3. Click "Cancel"
  4. In the preview mode there's no newly added elements
  5. Go to Edit mode again
  6. All the added elements are there while the edit mode should reflect the preview state

togglePreview();
};

const onReset = () => {
const { block, replaceInnerBlocks } = this.props;
const newBlocks = [];
DEFAULT_PRODUCT_LIST_LAYOUT.map( ( [ name, attributes ] ) => {
newBlocks.push( createBlock( name, attributes ) );
const newBlocks: BlockInstance[] = [];
DEFAULT_PRODUCT_LIST_LAYOUT.map( ( [ name, blockAttributes ] ) => {
newBlocks.push( createBlock( name, blockAttributes ) );
return true;
} );
replaceInnerBlocks( block.clientId, newBlocks, false );
this.setState( { innerBlocks: block.innerBlocks } );
replaceInnerBlocks( clientId, newBlocks, false );
setInnerBlocks( innerBlocks );
};

const InnerBlockProps = {
template: this.props.attributes.layoutConfig,
const InnerBlockProps: {
template: [ string, object? ][];
templateLock: boolean;
allowedBlocks: Array< string >;
renderAppender?: undefined | boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Considering the property is optional, it can be implicitly undefined hence you can just define it as optional boolean:

Suggested change
renderAppender?: undefined | boolean;
renderAppender?: boolean;

} = {
template: layoutConfig,
templateLock: false,
allowedBlocks: Object.keys( this.blockMap ),
allowedBlocks: Object.keys( blockMap ),
};

if ( this.props.attributes.layoutConfig.length !== 0 ) {
if ( layoutConfig.length !== 0 ) {
InnerBlockProps.renderAppender = false;
}

return (
<Placeholder icon={ this.getIcon() } label={ this.getTitle() }>
<Placeholder icon={ getIcon() } label={ getTitle() }>
{ __(
'Display all products from your store as a grid.',
'woo-gutenberg-products-block'
Expand All @@ -221,7 +223,9 @@ class Editor extends Component {
<li className="wc-block-grid__product">
<ProductDataContextProvider
product={ previewProducts[ 0 ] }
isLoading={ false }
>
{ /* @ts-expect-error: `InnerBlocks` is a component that is typed in WordPress core*/ }
<InnerBlocks { ...InnerBlockProps } />
</ProductDataContextProvider>
</li>
Expand Down Expand Up @@ -263,61 +267,40 @@ class Editor extends Component {
);
};

renderViewMode = () => {
const { attributes } = this.props;
const { layoutConfig } = attributes;
const renderViewMode = () => {
const hasContent = layoutConfig && layoutConfig.length !== 0;
const blockTitle = this.getTitle();
const blockIcon = this.getIcon();
const blockTitle = getTitle();
const blockIcon = getIcon();

if ( ! hasContent ) {
return renderHiddenContentPlaceholder( blockTitle, blockIcon );
}

return (
<Disabled>
{ /* @ts-expect-error: `Block` is a component that is typed in WordPress core*/ }
<Block attributes={ attributes } />
</Disabled>
);
};

render = () => {
const { attributes } = this.props;
const { isEditing } = this.state;
const blockTitle = this.getTitle();
const blockIcon = this.getIcon();
const blockTitle = getTitle();
const blockIcon = getIcon();

if ( blocksConfig.productCount === 0 ) {
return renderNoProductsPlaceholder( blockTitle, blockIcon );
}
if ( blocksConfig.productCount === 0 ) {
return renderNoProductsPlaceholder( blockTitle, blockIcon );
}

return (
<div
className={ getBlockClassName(
'wc-block-all-products',
attributes
) }
>
{ this.getBlockControls() }
{ this.getInspectorControls() }
{ isEditing ? this.renderEditMode() : this.renderViewMode() }
</div>
);
};
return (
<div
className={ getBlockClassName(
'wc-block-all-products',
attributes
) }
>
{ getBlockControls() }
{ getInspectorControls() }
{ isEditing ? renderEditMode() : renderViewMode() }
</div>
);
}

export default compose(
withSpokenMessages,
withSelect( ( select, { clientId } ) => {
const { getBlock } = select( 'core/block-editor' );
return {
block: getBlock( clientId ),
};
} ),
withDispatch( ( dispatch ) => {
const { replaceInnerBlocks } = dispatch( 'core/block-editor' );
return {
replaceInnerBlocks,
};
} )
)( Editor );
8 changes: 4 additions & 4 deletions assets/js/blocks/products/all-products/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import '@woocommerce/atomic-blocks';
*/
import metadata from './block.json';
import deprecated from './deprecated';
import edit from './edit';
import save from './save';
import Edit from './edit';
import Save from './save';
import defaults from './defaults';

const { name } = metadata;
Expand All @@ -26,9 +26,9 @@ const settings = {
/>
),
},
edit,
edit: Edit,
// Save the props to post content.
save,
save: Save,
deprecated,
defaults,
};
Expand Down
2 changes: 1 addition & 1 deletion assets/js/blocks/products/all-products/save.js
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about renaming the file to save.jsx considering it returns JSX component?

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about renaming this file to save.jsx as it returns JSX content?

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { InnerBlocks } from '@wordpress/block-editor';
*/
import { getBlockClassName } from '../utils.js';

export default function save( { attributes } ) {
export default function Save( { attributes } ) {
const dataAttributes = {};
Object.keys( attributes )
.sort()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { BlockInstance } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -12,7 +13,7 @@ import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';
/**
* The default layout built from the default template.
*/
export const DEFAULT_PRODUCT_LIST_LAYOUT = [
export const DEFAULT_PRODUCT_LIST_LAYOUT: [ string, object? ][] = [
[ 'woocommerce/product-image', { imageSizing: ImageSizing.THUMBNAIL } ],
[ 'woocommerce/product-title' ],
[ 'woocommerce/product-price' ],
Expand All @@ -25,7 +26,9 @@ export const DEFAULT_PRODUCT_LIST_LAYOUT = [
*
* @param {Object[]} innerBlocks Inner block components.
*/
export const getProductLayoutConfig = ( innerBlocks ) => {
export const getProductLayoutConfig = (
innerBlocks: BlockInstance[]
): [ string, object? ][] => {
if ( ! innerBlocks || innerBlocks.length === 0 ) {
return [];
}
Expand Down
Loading