diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-search/editor.scss index 224f0ce39e6..d47eb352c8b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-search/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/editor.scss @@ -14,3 +14,9 @@ } } } + +.wc-block-components-actions { + .block-editor-warning__actions { + margin-top: 0; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.js b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.js deleted file mode 100644 index d230edccf20..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { createBlock, registerBlockType } from '@wordpress/blocks'; -import { Icon, search } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import './style.scss'; -import './editor.scss'; -import Block from './block.js'; -import edit from './edit.js'; - -const attributes = { - /** - * Whether to show the field label. - */ - hasLabel: { - type: 'boolean', - default: true, - }, - - /** - * Search field label. - */ - label: { - type: 'string', - default: __( 'Search', 'woo-gutenberg-products-block' ), - }, - - /** - * Search field placeholder. - */ - placeholder: { - type: 'string', - default: __( 'Search products…', 'woo-gutenberg-products-block' ), - }, - - /** - * Store the instance ID. - */ - formId: { - type: 'string', - default: '', - }, -}; - -registerBlockType( 'woocommerce/product-search', { - title: __( 'Product Search', 'woo-gutenberg-products-block' ), - icon: { - src: ( - - ), - }, - category: 'woocommerce', - keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], - description: __( - 'A search box to allow customers to search for products by keyword.', - 'woo-gutenberg-products-block' - ), - supports: { - align: [ 'wide', 'full' ], - }, - example: { - attributes: { - hasLabel: true, - }, - }, - attributes, - transforms: { - from: [ - { - type: 'block', - blocks: [ 'core/legacy-widget' ], - // We can't transform if raw instance isn't shown in the REST API. - isMatch: ( { idBase, instance } ) => - idBase === 'woocommerce_product_search' && !! instance?.raw, - transform: ( { instance } ) => - createBlock( 'woocommerce/product-search', { - label: - instance.raw.title === '' - ? __( 'Search', 'woo-gutenberg-products-block' ) - : instance.raw.title, - } ), - }, - ], - }, - deprecated: [ - { - attributes, - save( props ) { - return ( -
- -
- ); - }, - }, - ], - edit, - save() { - return null; - }, -} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx new file mode 100644 index 00000000000..04e5c64d137 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx @@ -0,0 +1,206 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +import { store as blockEditorStore, Warning } from '@wordpress/block-editor'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { Icon, search } from '@wordpress/icons'; +import { getSettingWithCoercion } from '@woocommerce/settings'; +import { isBoolean } from '@woocommerce/types'; +import { Button } from '@wordpress/components'; +import { + // @ts-ignore waiting for @types/wordpress__blocks update + registerBlockVariation, + registerBlockType, + createBlock, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import Block from './block.js'; +import Edit from './edit.js'; + +const isBlockVariationAvailable = getSettingWithCoercion( + 'isBlockVariationAvailable', + false, + isBoolean +); + +const attributes = { + /** + * Whether to show the field label. + */ + hasLabel: { + type: 'boolean', + default: true, + }, + + /** + * Search field label. + */ + label: { + type: 'string', + default: __( 'Search', 'woo-gutenberg-products-block' ), + }, + + /** + * Search field placeholder. + */ + placeholder: { + type: 'string', + default: __( 'Search products…', 'woo-gutenberg-products-block' ), + }, + + /** + * Store the instance ID. + */ + formId: { + type: 'string', + default: '', + }, +}; + +const PRODUCT_SEARCH_ATTRIBUTES = { + label: attributes.label.default, + buttonText: attributes.label.default, + placeholder: attributes.placeholder.default, + query: { + post_type: 'product', + }, +}; + +const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { + // @ts-ignore @wordpress/block-editor/store types not provided + const { replaceBlocks } = useDispatch( blockEditorStore ); + + const currentBlockAttributes = useSelect( + ( select ) => + select( 'core/block-editor' ).getBlockAttributes( clientId ), + [ clientId ] + ); + + const updateBlock = () => { + replaceBlocks( + clientId, + createBlock( 'core/search', { + label: + currentBlockAttributes?.label || + PRODUCT_SEARCH_ATTRIBUTES.label, + buttonText: PRODUCT_SEARCH_ATTRIBUTES.buttonText, + placeholder: + currentBlockAttributes?.placeholder || + PRODUCT_SEARCH_ATTRIBUTES.placeholder, + query: PRODUCT_SEARCH_ATTRIBUTES.query, + } ) + ); + }; + + const actions = [ + , + ]; + + return ( + + { __( + 'This version of the Product Search block is outdated. Upgrade to continue using.', + 'woo-gutenberg-products-block' + ) } + + ); +}; + +registerBlockType( 'woocommerce/product-search', { + title: __( 'Product Search', 'woo-gutenberg-products-block' ), + icon: { + src: ( + + ), + }, + category: 'woocommerce', + keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], + description: __( + 'A search box to allow customers to search for products by keyword.', + 'woo-gutenberg-products-block' + ), + supports: { + align: [ 'wide', 'full' ], + inserter: ! isBlockVariationAvailable, + }, + example: { + attributes: { + hasLabel: true, + }, + }, + attributes, + transforms: { + from: [ + { + type: 'block', + blocks: [ 'core/legacy-widget' ], + // We can't transform if raw instance isn't shown in the REST API. + isMatch: ( { idBase, instance } ) => + idBase === 'woocommerce_product_search' && !! instance?.raw, + transform: ( { instance } ) => + createBlock( 'woocommerce/product-search', { + label: + instance.raw.title || + PRODUCT_SEARCH_ATTRIBUTES.label, + } ), + }, + ], + }, + deprecated: [ + { + attributes, + save( props ) { + return ( +
+ +
+ ); + }, + }, + ], + edit: isBlockVariationAvailable ? DeprecatedBlockEdit : Edit, + save() { + return null; + }, +} ); + +if ( isBlockVariationAvailable ) { + registerBlockVariation( 'core/search', { + name: 'woocommerce/product-search', + title: __( 'Product Search', 'woo-gutenberg-products-block' ), + icon: { + src: ( + + ), + }, + // @ts-ignore waiting for @types/wordpress__blocks update + isActive: ( blockAttributes, variationAttributes ) => { + return ( + blockAttributes.query?.post_type === + variationAttributes.query.post_type + ); + }, + category: 'woocommerce', + keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], + description: __( + 'A search box to allow customers to search for products by keyword.', + 'woo-gutenberg-products-block' + ), + attributes: PRODUCT_SEARCH_ATTRIBUTES, + } ); +} diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductSearch.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductSearch.php index 613aa67d77d..2afa20ac5ec 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductSearch.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductSearch.php @@ -124,4 +124,36 @@ class ProductSearch extends AbstractBlock { $label_markup . $field_markup ); } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $gutenberg_version = ''; + + if ( is_plugin_active( 'gutenberg/gutenberg.php' ) ) { + if ( defined( 'GUTENBERG_VERSION' ) ) { + $gutenberg_version = GUTENBERG_VERSION; + } + + if ( ! $gutenberg_version ) { + $gutenberg_data = get_file_data( + WP_PLUGIN_DIR . '/gutenberg/gutenberg.php', + array( 'Version' => 'Version' ) + ); + $gutenberg_version = $gutenberg_data['Version']; + } + } + + $this->asset_data_registry->add( + 'isBlockVariationAvailable', + version_compare( get_bloginfo( 'version' ), '6.1', '>=' ) || version_compare( $gutenberg_version, '13.4', '>=' ) + ); + } } diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-search.test.js b/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-search.test.js index b4189ab1810..d7f99c391ff 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-search.test.js +++ b/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-search.test.js @@ -12,52 +12,60 @@ import { visitBlockPage, } from '@woocommerce/blocks-test-utils'; +/** + * Internal dependencies + */ +import { GUTENBERG_EDITOR_CONTEXT, describeOrSkip } from '../../utils'; + const block = { name: 'Product Search', slug: 'woocommerce/product-search', class: '.wc-block-product-search', }; -describe( `${ block.name } Block`, () => { - beforeAll( async () => { - await switchUserToAdmin(); - await visitBlockPage( `${ block.name } Block` ); - } ); +describeOrSkip( GUTENBERG_EDITOR_CONTEXT !== 'gutenberg' )( + `${ block.name } Block`, + () => { + beforeAll( async () => { + await switchUserToAdmin(); + await visitBlockPage( `${ block.name } Block` ); + } ); - it( 'renders without crashing', async () => { - await expect( page ).toRenderBlock( block ); - } ); + it( 'renders without crashing', async () => { + await expect( page ).toRenderBlock( block ); + } ); - it( 'can toggle field label', async () => { - await openDocumentSettingsSidebar(); - await page.click( block.class ); - const selector = `${ block.class } .wc-block-product-search__label`; - const toggleLabel = await findLabelWithText( - 'Show search field label' - ); - await expect( toggleLabel ).toToggleElement( selector ); - } ); + it( 'can toggle field label', async () => { + await openDocumentSettingsSidebar(); + await page.click( block.class ); + const selector = `${ block.class } .wc-block-product-search__label`; + const toggleLabel = await findLabelWithText( + 'Show search field label' + ); + await expect( toggleLabel ).toToggleElement( selector ); + } ); - it( 'can change field labels in editor', async () => { - await expect( page ).toFill( - 'textarea.wc-block-product-search__label', - 'I am a new label' - ); + it( 'can change field labels in editor', async () => { + await expect( page ).toFill( + 'textarea.wc-block-product-search__label', + 'I am a new label' + ); - await expect( page ).toFill( - '.wc-block-product-search__field input', - 'I am a new placeholder' - ); + await expect( page ).toFill( + '.wc-block-product-search__field input', + 'I am a new placeholder' + ); - await clearAndFillInput( - 'textarea.wc-block-product-search__label', - 'The Label' - ); - await clearAndFillInput( - '.wc-block-product-search__field input', - 'The Placeholder' - ); + await clearAndFillInput( + 'textarea.wc-block-product-search__label', + 'The Label' + ); + await clearAndFillInput( + '.wc-block-product-search__field input', + 'The Placeholder' + ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + } +); diff --git a/plugins/woocommerce-blocks/tests/e2e/utils.js b/plugins/woocommerce-blocks/tests/e2e/utils.js index 3a4f127c19f..b9fe0e20128 100644 --- a/plugins/woocommerce-blocks/tests/e2e/utils.js +++ b/plugins/woocommerce-blocks/tests/e2e/utils.js @@ -438,3 +438,18 @@ export const openBlockEditorSettings = async ( { isFSEEditor = false } ) => { export const waitForAllProductsBlockLoaded = async () => { await page.waitForSelector( SELECTORS.allProductsBlock.productsList ); }; + +/** + * Execute or skip the test suite base on the provided condition. + * + * @param {boolean} condition Condition to execute test suite. + */ +export const describeOrSkip = ( condition ) => + condition ? describe : describe.skip; + +/** + * Execute or skip the test base on the provided condition. + * + * @param {boolean} condition Condition to execute test. + */ +export const itOrSkip = ( condition ) => ( condition ? it : it.skip );