385 lines
9.7 KiB
TypeScript
385 lines
9.7 KiB
TypeScript
/* eslint-disable @wordpress/no-unsafe-wp-apis */
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
/**
|
|
* External dependencies
|
|
*/
|
|
import clsx from 'clsx';
|
|
import { memo, useMemo, useState } from '@wordpress/element';
|
|
import { useSelect } from '@wordpress/data';
|
|
import { __ } from '@wordpress/i18n';
|
|
import {
|
|
BlockContextProvider,
|
|
__experimentalUseBlockPreview as useBlockPreview,
|
|
useBlockProps,
|
|
useInnerBlocksProps,
|
|
store as blockEditorStore,
|
|
} from '@wordpress/block-editor';
|
|
import { Spinner } from '@wordpress/components';
|
|
import { store as coreStore } from '@wordpress/core-data';
|
|
import { ProductCollectionAttributes } from '@woocommerce/blocks/product-collection/types';
|
|
import { getSettingWithCoercion } from '@woocommerce/settings';
|
|
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
|
import { ProductDataContextProvider } from '@woocommerce/shared-context';
|
|
import { withProduct } from '@woocommerce/block-hocs';
|
|
import type { BlockEditProps, BlockInstance } from '@wordpress/blocks';
|
|
|
|
/**
|
|
* Internal dependencies
|
|
*/
|
|
import {
|
|
useGetLocation,
|
|
useProductCollectionQueryContext,
|
|
parseTemplateSlug,
|
|
} from './utils';
|
|
import './editor.scss';
|
|
import { getDefaultStockStatuses } from '../product-collection/constants';
|
|
|
|
const DEFAULT_QUERY_CONTEXT_ATTRIBUTES = [ 'collection' ];
|
|
|
|
const ProductTemplateInnerBlocks = () => {
|
|
const innerBlocksProps = useInnerBlocksProps(
|
|
{ className: 'wc-block-product' },
|
|
{ __unstableDisableLayoutClassNames: true }
|
|
);
|
|
return <li { ...innerBlocksProps } />;
|
|
};
|
|
|
|
type ProductTemplateBlockPreviewProps = {
|
|
blocks: object[];
|
|
blockContextId: string;
|
|
isHidden: boolean;
|
|
setActiveBlockContextId: ( blockContextId: string ) => void;
|
|
};
|
|
|
|
const ProductTemplateBlockPreview = ( {
|
|
blocks,
|
|
blockContextId,
|
|
isHidden,
|
|
setActiveBlockContextId,
|
|
}: ProductTemplateBlockPreviewProps ) => {
|
|
const blockPreviewProps = useBlockPreview( {
|
|
blocks,
|
|
props: {
|
|
className: 'wc-block-product',
|
|
},
|
|
} );
|
|
|
|
const handleOnClick = () => {
|
|
setActiveBlockContextId( blockContextId );
|
|
};
|
|
|
|
const style = {
|
|
display: isHidden ? 'none' : undefined,
|
|
};
|
|
|
|
return (
|
|
<li
|
|
{ ...blockPreviewProps }
|
|
tabIndex={ 0 }
|
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
|
|
role="button"
|
|
onClick={ handleOnClick }
|
|
onKeyPress={ handleOnClick }
|
|
style={ style }
|
|
/>
|
|
);
|
|
};
|
|
|
|
const MemoizedProductTemplateBlockPreview = memo( ProductTemplateBlockPreview );
|
|
|
|
type ProductContentProps = {
|
|
attributes: { productId: string };
|
|
isLoading: boolean;
|
|
product: ProductResponseItem;
|
|
displayTemplate: boolean;
|
|
blocks: BlockInstance[];
|
|
blockContext: {
|
|
postType: string;
|
|
postId: string;
|
|
};
|
|
setActiveBlockContextId: ( id: string ) => void;
|
|
};
|
|
|
|
const ProductContent = withProduct(
|
|
( {
|
|
isLoading,
|
|
product,
|
|
displayTemplate,
|
|
blocks,
|
|
blockContext,
|
|
setActiveBlockContextId,
|
|
}: ProductContentProps ) => {
|
|
return (
|
|
<BlockContextProvider
|
|
key={ blockContext.postId }
|
|
value={ blockContext }
|
|
>
|
|
<ProductDataContextProvider
|
|
product={ product }
|
|
isLoading={ isLoading }
|
|
>
|
|
{ displayTemplate ? <ProductTemplateInnerBlocks /> : null }
|
|
<MemoizedProductTemplateBlockPreview
|
|
blocks={ blocks }
|
|
blockContextId={ blockContext.postId }
|
|
setActiveBlockContextId={ setActiveBlockContextId }
|
|
isHidden={ displayTemplate }
|
|
/>
|
|
</ProductDataContextProvider>
|
|
</BlockContextProvider>
|
|
);
|
|
}
|
|
);
|
|
|
|
const ProductTemplateEdit = (
|
|
props: BlockEditProps< {
|
|
clientId: string;
|
|
} > & {
|
|
context: ProductCollectionAttributes;
|
|
__unstableLayoutClassNames: string;
|
|
}
|
|
) => {
|
|
const {
|
|
clientId,
|
|
context: {
|
|
query: {
|
|
perPage,
|
|
offset = 0,
|
|
order,
|
|
orderBy,
|
|
search,
|
|
exclude,
|
|
inherit,
|
|
taxQuery,
|
|
pages,
|
|
...restQueryArgs
|
|
},
|
|
queryContext = [ { page: 1 } ],
|
|
templateSlug,
|
|
displayLayout: { type: layoutType, columns, shrinkColumns } = {
|
|
type: 'flex',
|
|
columns: 3,
|
|
shrinkColumns: false,
|
|
},
|
|
queryContextIncludes = [],
|
|
__privateProductCollectionPreviewState,
|
|
},
|
|
__unstableLayoutClassNames,
|
|
} = props;
|
|
const location = useGetLocation( props.context, props.clientId );
|
|
|
|
const [ { page } ] = queryContext;
|
|
const [ activeBlockContextId, setActiveBlockContextId ] =
|
|
useState< string >();
|
|
const postType = 'product';
|
|
const loopShopPerPage = getSettingWithCoercion(
|
|
'loopShopPerPage',
|
|
12,
|
|
isNumber
|
|
);
|
|
|
|
// Add default query context attributes to queryContextIncludes
|
|
const queryContextIncludesWithDefaults = [
|
|
...new Set(
|
|
queryContextIncludes.concat( DEFAULT_QUERY_CONTEXT_ATTRIBUTES )
|
|
),
|
|
];
|
|
|
|
const productCollectionQueryContext = useProductCollectionQueryContext( {
|
|
clientId,
|
|
queryContextIncludes: queryContextIncludesWithDefaults,
|
|
} );
|
|
|
|
const { products, blocks } = useSelect(
|
|
( select ) => {
|
|
const { getEntityRecords, getTaxonomies } = select( coreStore );
|
|
const { getBlocks } = select( blockEditorStore );
|
|
const taxonomies = getTaxonomies( {
|
|
type: postType,
|
|
per_page: -1,
|
|
context: 'view',
|
|
} );
|
|
const query: Record< string, unknown > = {
|
|
postType,
|
|
offset: perPage ? perPage * ( page - 1 ) + offset : 0,
|
|
order,
|
|
orderby: orderBy,
|
|
};
|
|
// There is no need to build the taxQuery if we inherit.
|
|
if ( taxQuery && ! inherit ) {
|
|
// We have to build the tax query for the REST API and use as
|
|
// keys the taxonomies `rest_base` with the `term ids` as values.
|
|
const builtTaxQuery = Object.entries( taxQuery ).reduce(
|
|
( accumulator, [ taxonomySlug, terms ] ) => {
|
|
const taxonomy = taxonomies?.find(
|
|
( { slug } ) => slug === taxonomySlug
|
|
);
|
|
if ( taxonomy?.rest_base ) {
|
|
accumulator[ taxonomy?.rest_base ] = terms;
|
|
}
|
|
return accumulator;
|
|
},
|
|
{}
|
|
);
|
|
if ( !! Object.keys( builtTaxQuery ).length ) {
|
|
Object.assign( query, builtTaxQuery );
|
|
}
|
|
}
|
|
if ( perPage ) {
|
|
query.per_page = perPage;
|
|
}
|
|
if ( search ) {
|
|
query.search = search;
|
|
}
|
|
if ( exclude?.length ) {
|
|
query.exclude = exclude;
|
|
}
|
|
// If `inherit` is truthy, adjust conditionally the query to create a better preview.
|
|
if ( inherit ) {
|
|
const { taxonomy, slug } = parseTemplateSlug( templateSlug );
|
|
|
|
if ( taxonomy && slug ) {
|
|
const taxonomyRecord = getEntityRecords(
|
|
'taxonomy',
|
|
taxonomy,
|
|
{
|
|
context: 'view',
|
|
per_page: 1,
|
|
_fields: [ 'id' ],
|
|
slug,
|
|
}
|
|
);
|
|
|
|
if ( taxonomyRecord ) {
|
|
const taxonomyId = taxonomyRecord[ 0 ]?.id;
|
|
if ( taxonomy === 'category' ) {
|
|
query.categories = taxonomyId;
|
|
} else {
|
|
// If taxonomy is not `category`, we expect either `product_cat` or `product_tag`
|
|
query[ taxonomy ] = taxonomyId;
|
|
}
|
|
}
|
|
}
|
|
query.per_page = loopShopPerPage;
|
|
}
|
|
return {
|
|
products: getEntityRecords( 'postType', postType, {
|
|
...query,
|
|
...restQueryArgs,
|
|
productCollectionLocation: location,
|
|
productCollectionQueryContext,
|
|
previewState: __privateProductCollectionPreviewState,
|
|
/**
|
|
* Use value of "Out of stock visibility" setting to determine
|
|
* which stock statuses to include if inherit query
|
|
* from template is true.
|
|
*/
|
|
...( inherit && {
|
|
woocommerceStockStatus: getDefaultStockStatuses(),
|
|
} ),
|
|
} ),
|
|
blocks: getBlocks( clientId ),
|
|
};
|
|
},
|
|
[
|
|
perPage,
|
|
page,
|
|
offset,
|
|
order,
|
|
orderBy,
|
|
clientId,
|
|
search,
|
|
postType,
|
|
exclude,
|
|
inherit,
|
|
templateSlug,
|
|
taxQuery,
|
|
restQueryArgs,
|
|
location,
|
|
productCollectionQueryContext,
|
|
loopShopPerPage,
|
|
__privateProductCollectionPreviewState,
|
|
]
|
|
);
|
|
const blockContexts = useMemo(
|
|
() =>
|
|
products?.map( ( product ) => ( {
|
|
postType: product.type,
|
|
postId: product.id,
|
|
} ) ),
|
|
[ products ]
|
|
);
|
|
|
|
const hasLayoutFlex = layoutType === 'flex' && columns > 1;
|
|
let customClassName = '';
|
|
|
|
// We don't want to apply layout styles if there's no products.
|
|
if ( products && products.length && hasLayoutFlex ) {
|
|
const dynamicGrid = `wc-block-product-template__responsive columns-${ columns }`;
|
|
const staticGrid = `is-flex-container columns-${ columns }`;
|
|
|
|
customClassName = shrinkColumns ? dynamicGrid : staticGrid;
|
|
}
|
|
|
|
const blockProps = useBlockProps( {
|
|
className: clsx(
|
|
__unstableLayoutClassNames,
|
|
'wc-block-product-template',
|
|
customClassName,
|
|
{ [ `is-product-collection-layout-${ layoutType }` ]: layoutType }
|
|
),
|
|
} );
|
|
|
|
if ( ! products ) {
|
|
return (
|
|
<p { ...blockProps }>
|
|
<Spinner className="wc-block-product-template__spinner" />
|
|
</p>
|
|
);
|
|
}
|
|
|
|
if ( ! products.length ) {
|
|
return (
|
|
<p { ...blockProps }>
|
|
{ ' ' }
|
|
{ __(
|
|
'No products to display. Try adjusting the filters in the block settings panel.',
|
|
'woocommerce'
|
|
) }
|
|
</p>
|
|
);
|
|
}
|
|
|
|
// To avoid flicker when switching active block contexts, a preview is rendered
|
|
// for each block context, but the preview for the active block context is hidden.
|
|
// This ensures that when it is displayed again, the cached rendering of the
|
|
// block preview is used, instead of having to re-render the preview from scratch.
|
|
return (
|
|
<ul { ...blockProps }>
|
|
{ blockContexts &&
|
|
blockContexts.map( ( blockContext ) => {
|
|
const displayTemplate =
|
|
blockContext.postId ===
|
|
( activeBlockContextId || blockContexts[ 0 ]?.postId );
|
|
|
|
return (
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore isLoading and product props are missing as they're coming from untyped withProduct HOC.
|
|
<ProductContent
|
|
key={ blockContext.postId }
|
|
attributes={ {
|
|
productId: blockContext.postId,
|
|
} }
|
|
blocks={ blocks }
|
|
displayTemplate={ displayTemplate }
|
|
blockContext={ blockContext }
|
|
setActiveBlockContextId={ setActiveBlockContextId }
|
|
/>
|
|
);
|
|
} ) }
|
|
</ul>
|
|
);
|
|
};
|
|
|
|
export default ProductTemplateEdit;
|