From f0fedb86c75d301a0bf895c4f4e71b4c12e980ac Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Fri, 20 May 2022 14:55:36 -0400 Subject: [PATCH 01/42] sidebar product sorting --- includes/Interfaces/Product_Query.php | 3 +- includes/REST_API/Products_Controller.php | 11 +++- includes/Shopping/Shopify_Query.php | 62 ++++++++++++++---- includes/Shopping/Woocommerce_Query.php | 55 +++++++++++++--- .../library/panes/shopping/productSort.js | 63 +++++++++++++++++++ .../library/panes/shopping/shoppingPane.js | 12 ++-- packages/wp-story-editor/src/api/shopping.js | 3 +- .../includes/Shopping/Mock_Vendor.php | 3 +- .../includes/Shopping/Mock_Vendor_Error.php | 3 +- .../includes/Shopping/Mock_Vendor_Invalid.php | 3 +- .../tests/Shopping/Shopify_Query.php | 47 +++++++++++++- .../unit/tests/Shopping/Woocommerce_Query.php | 53 ++++++++++++++++ 12 files changed, 285 insertions(+), 33 deletions(-) create mode 100644 packages/story-editor/src/components/library/panes/shopping/productSort.js diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index a26c8152b203..b75c378ecabb 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -39,7 +39,8 @@ interface Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort order for query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ); + public function get_search( string $search_term, string $sort_by); } diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 0362c24f6729..0d15220489ac 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -163,8 +163,15 @@ public function get_items( $request ) { * * @var string $search_term */ - $search_term = ! empty( $request['search'] ) ? $request['search'] : ''; - $query_result = $query->get_search( $search_term ); + $search_term = ! empty( $request['search'] ) ? $request['search'] : ''; + + /** + * Request context. + * + * @var string $sort_by + */ + $sort_by = ! empty( $request['sort_by'] ) ? $request['sort_by'] : ''; + $query_result = $query->get_search( $search_term, $sort_by ); if ( is_wp_error( $query_result ) ) { return $query_result; } diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 851688ffab14..afa6c2250fe1 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -151,16 +151,17 @@ protected function execute_query( string $query ) { * @since 1.21.0 * * @param string $search_term Search term to filter products by. + * @param string $sort_by sort order for query. * @return string The assembled GraphQL query. */ - protected function get_products_query( string $search_term ): string { + protected function get_products_query( string $search_term, string $sort_by = '' ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; - - // TODO(#11154): Support different sortKeys. - // Maybe use "available_for_sale:true AND " query to only show items in stock. + $sort = $this->parse_sort_by( $sort_by ); + $sort_key = $sort['sort_key']; + $reverse = $sort['reverse']; return <<get_products_query( $search_term ); - $body = $this->execute_query( $query ); + $query = $this->get_products_query( $search_term, $sort_by ); + + $body = $this->execute_query( $query ); if ( is_wp_error( $body ) ) { return $body; @@ -242,17 +246,20 @@ protected function fetch_remote_products( string $search_term ) { return $result; } + /** * Get products by search term. * * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort condition for product query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ) { - $result = $this->fetch_remote_products( $search_term ); - + public function get_search( string $search_term, string $sort_by = '' ) { + + $result = $this->fetch_remote_products( $search_term, $sort_by ); + if ( is_wp_error( $result ) ) { return $result; } @@ -297,4 +304,35 @@ public function get_search( string $search_term ) { return $results; } + + /** + * Parse sort type into array for product query + * + * @since 1.21.0 + * + * @param string $sort_by sort condition for product query. + * @return array + */ + protected function parse_sort_by( string $sort_by ): array { + + switch ( $sort_by ) { + case 'a-z': + $order = 'TITLE|false'; + break; + case 'z-a': + $order = 'TITLE|true'; + break; + case 'price-low': + $order = 'PRICE|false'; + break; + case 'price-high': + $order = 'PRICE|true'; + break; + default: + $order = 'CREATED_AT|false'; + } + + list($sort_key, $reverse) = explode( '|', $order ); + return compact( 'sort_key', 'reverse' ); + } } diff --git a/includes/Shopping/Woocommerce_Query.php b/includes/Shopping/Woocommerce_Query.php index 4602391de5e3..6c621b5f7fe2 100644 --- a/includes/Shopping/Woocommerce_Query.php +++ b/includes/Shopping/Woocommerce_Query.php @@ -39,9 +39,10 @@ class Woocommerce_Query implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort condition for product query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ) { + public function get_search( string $search_term, string $sort_by = '' ) { if ( ! function_exists( 'wc_get_products' ) ) { return new WP_Error( 'rest_unknown', __( 'Woocommerce is not installed.', 'web-stories' ), [ 'status' => 400 ] ); @@ -49,21 +50,29 @@ public function get_search( string $search_term ) { $results = []; + $order_by = $this->parse_sort_by( $sort_by ); + /** * Products. * * @var \WC_Product[] $products */ $products = wc_get_products( - [ - 'status' => 'publish', - 'limit' => 100, - 'orderby' => 'date', - 'order' => 'DESC', - 's' => $search_term, - ] + array_merge( + [ + 'status' => 'publish', + 'limit' => 100, + 's' => $search_term, + ], + $order_by + ) ); + if ( 'price' === $order_by['orderby'] ) { + // @todo this only orders products based on wc_get_products previously called + $products = wc_products_array_orderby( $products, 'price', $order_by['order'] ); + } + $product_image_ids = []; foreach ( $products as $product ) { $product_image_ids[] = $this->get_product_image_ids( $product ); @@ -108,6 +117,36 @@ public function get_search( string $search_term ) { return $results; } + /** + * Parse sort type into array for product query + * + * @since 1.21.0 + * + * @param string $sort_by sort condition for product query. + * @return array + */ + protected function parse_sort_by( string $sort_by = '' ): array { + switch ( $sort_by ) { + case 'a-z': + $order = 'title|ASC'; + break; + case 'z-a': + $order = 'title|DESC'; + break; + case 'price-low': + $order = 'price|ASC'; + break; + case 'price-high': + $order = 'price|DESC'; + break; + default: + $order = 'date|ASC'; + } + + list($orderby, $order) = explode( '|', $order ); + return compact( 'orderby', 'order' ); + } + /** * Get all product image ids (feature image + gallery_images). * diff --git a/packages/story-editor/src/components/library/panes/shopping/productSort.js b/packages/story-editor/src/components/library/panes/shopping/productSort.js new file mode 100644 index 000000000000..c17b3c56e6a1 --- /dev/null +++ b/packages/story-editor/src/components/library/panes/shopping/productSort.js @@ -0,0 +1,63 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies + */ +import { useFeature } from 'flagged'; +import PropTypes from 'prop-types'; +import { __ } from '@googleforcreators/i18n'; +import { Datalist } from '@googleforcreators/design-system'; +import styled from 'styled-components'; + +const StyledDropDown = styled(Datalist.DropDown)` + width: 170px; + height: 36px; + margin-left: 12px; +`; + +function SortDropdown({ onChange, sortId }) { + const isShoppingIntegrationEnabled = useFeature('shoppingIntegration'); + + if (!isShoppingIntegrationEnabled) { + return null; + } + + const options = [ + { id: 'recent', name: __('Recently Added', 'web-stories') }, + { id: 'a-z', name: __('Alphabetical: A-Z', 'web-stories') }, + { id: 'z-a', name: __('Alphabetical: Z-A', 'web-stories') }, + { id: 'price-low', name: __('Price: low to high', 'web-stories') }, + { id: 'price-high', name: __('Price: high to low', 'web-stories') }, + ]; + + return ( + } + dropDownLabel={__('Sort', 'web-stories')} + onChange={onChange} + selectedId={sortId} + options={options} + aria-label={__('Sort by', 'web-stories')} + /> + ); +} + +SortDropdown.propTypes = { + sortId: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +export default SortDropdown; diff --git a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js index 445aedb6f6e3..fb17c0f9b3a8 100644 --- a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js +++ b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js @@ -44,6 +44,7 @@ import { useStory } from '../../../../app/story'; import useLibrary from '../../useLibrary'; import paneId from './paneId'; import ProductList from './productList'; +import ProductSort from './productSort'; const Loading = styled.div` position: relative; @@ -68,6 +69,8 @@ function ShoppingPane(props) { const [loaded, setLoaded] = useState(false); const [isLoading, setIsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState('recent'); + const onSortBy = ({ id }) => setSortBy(id); const [isMenuFocused, setIsMenuFocused] = useState(false); const [products, setProducts] = useState([]); const { @@ -84,10 +87,10 @@ function ShoppingPane(props) { })); const getProductsByQuery = useCallback( - async (value = '') => { + async (value = '', sort = '') => { try { setIsLoading(true); - setProducts(await getProducts(value)); + setProducts(await getProducts(value, sort)); } catch (err) { setProducts([]); } finally { @@ -111,8 +114,8 @@ function ShoppingPane(props) { const debouncedProductsQuery = useDebouncedCallback(getProductsByQuery, 300); useEffect( - () => debouncedProductsQuery(searchTerm), - [searchTerm, debouncedProductsQuery] + () => debouncedProductsQuery(searchTerm, sortBy), + [searchTerm, sortBy, debouncedProductsQuery] ); const handleInputKeyPress = useCallback((event) => { @@ -212,6 +215,7 @@ function ShoppingPane(props) { clearId="clear-product-search" handleClearInput={handleClearInput} /> + {isLoading && ( diff --git a/packages/wp-story-editor/src/api/shopping.js b/packages/wp-story-editor/src/api/shopping.js index dd8c1b120128..0b68559953cd 100644 --- a/packages/wp-story-editor/src/api/shopping.js +++ b/packages/wp-story-editor/src/api/shopping.js @@ -24,11 +24,12 @@ import apiFetch from '@wordpress/api-fetch'; */ import { addQueryArgs } from '@googleforcreators/url'; -export function getProducts(config, search) { +export function getProducts(config, search, sort_by) { return apiFetch({ path: addQueryArgs(config.api.products, { per_page: 100, search, + sort_by, }), }); } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index 82968c41f004..187ce444e9a2 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -32,9 +32,10 @@ class Mock_Vendor implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort order for query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ) { + public function get_search( string $search_term, string $sort_by ) { $products = []; for ( $x = 0; $x < 10; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index 00dec2430b99..49c2152cba44 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -32,9 +32,10 @@ class Mock_Vendor_Error implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort order for query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ) { + public function get_search( string $search_term, string $sort_by ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index fecca06d3b62..6a587aeeeed6 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -31,9 +31,10 @@ class Mock_Vendor_Invalid { * @since 1.21.0 * * @param string $search_term Search term. + * @param string $sort_by sort order for query. * @return Product[]|WP_Error */ - public function get_search( string $search_term ) { + public function get_search( string $search_term, string $sort_by ) { $products = []; for ( $x = 0; $x < 10; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index e47f52eb9655..d92aed5a4856 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,12 +184,13 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; + $sort_by = ''; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $sort_by ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); - $actual = $this->instance->get_search( '' ); + $actual = $this->instance->get_search( '', '' ); $this->assertNotWPError( $actual ); $this->assertSame( [], $actual ); @@ -288,4 +289,46 @@ public function test_get_search_empty_search_response(): void { $this->assertSame( 1, $this->request_count ); $this->assertStringContainsString( 'query: "title:*some search term*"', $this->request_body ); } + + /** + * @covers ::fetch_remote_products + * @covers ::get_search + * @covers ::get_products_query + * @covers ::execute_query + * @covers ::parse_sort_by + */ + public function test_get_search_sort_by_query(): void { + update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); + update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); + add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); + + $actual = $this->instance->get_search( 'some search term', '' ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( 'sortKey: CREATED_AT', $this->request_body ); + $this->assertStringContainsString( 'reverse: false', $this->request_body ); + + $actual = $this->instance->get_search( 'some search term', 'a-z' ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); + $this->assertStringContainsString( 'reverse: false', $this->request_body ); + + $actual = $this->instance->get_search( 'some search term', 'z-a' ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); + $this->assertStringContainsString( 'reverse: true', $this->request_body ); + + $actual = $this->instance->get_search( 'some search term', 'price-low' ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); + $this->assertStringContainsString( 'reverse: false', $this->request_body ); + + $actual = $this->instance->get_search( 'some search term', 'price-high' ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); + $this->assertStringContainsString( 'reverse: true', $this->request_body ); + + remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); + } + + } diff --git a/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php b/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php index af4e9ca96b0b..18a7f9e074dc 100644 --- a/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php +++ b/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php @@ -172,4 +172,57 @@ public function test_get_product_image(): void { $results ); } + + + /** + * @covers ::parse_sort_by + */ + public function test_parse_sort_by(): void { + $product_query = new Query(); + + $order = $this->call_private_method( $product_query, 'parse_sort_by', [ '' ] ); + $this->assertEquals( + [ + 'orderby' => 'date', + 'order' => 'ASC', + ], + $order + ); + + $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'a-z' ] ); + $this->assertEquals( + [ + 'orderby' => 'title', + 'order' => 'ASC', + ], + $order + ); + + $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'z-a' ] ); + $this->assertEquals( + [ + 'orderby' => 'title', + 'order' => 'DESC', + ], + $order + ); + + $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'price-low' ] ); + $this->assertEquals( + [ + 'orderby' => 'price', + 'order' => 'ASC', + ], + $order + ); + + $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'price-high' ] ); + $this->assertEquals( + [ + 'orderby' => 'price', + 'order' => 'DESC', + ], + $order + ); + } } From a2bbf24ae8d5485f30020c30b911fc0490c1c9c4 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 05:18:11 -0400 Subject: [PATCH 02/42] update dropdown --- .../library/panes/shopping/productSort.js | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/story-editor/src/components/library/panes/shopping/productSort.js b/packages/story-editor/src/components/library/panes/shopping/productSort.js index c17b3c56e6a1..eb51103e1ace 100644 --- a/packages/story-editor/src/components/library/panes/shopping/productSort.js +++ b/packages/story-editor/src/components/library/panes/shopping/productSort.js @@ -22,9 +22,7 @@ import { __ } from '@googleforcreators/i18n'; import { Datalist } from '@googleforcreators/design-system'; import styled from 'styled-components'; -const StyledDropDown = styled(Datalist.DropDown)` - width: 170px; - height: 36px; +const StyledContainer = styled.div` margin-left: 12px; `; @@ -44,14 +42,15 @@ function SortDropdown({ onChange, sortId }) { ]; return ( - } - dropDownLabel={__('Sort', 'web-stories')} - onChange={onChange} - selectedId={sortId} - options={options} - aria-label={__('Sort by', 'web-stories')} - /> + + + ); } From e1bac30c074b621bb64bf4488febe9fdf6715eca Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 06:36:57 -0400 Subject: [PATCH 03/42] use array for switch --- includes/Shopping/Shopify_Query.php | 28 ++++++++++++++++++------- includes/Shopping/Woocommerce_Query.php | 28 ++++++++++++++++++------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index afa6c2250fe1..90443e764872 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -317,22 +317,36 @@ protected function parse_sort_by( string $sort_by ): array { switch ( $sort_by ) { case 'a-z': - $order = 'TITLE|false'; + $order = [ + 'sort_key' => 'TITLE', + 'reverse' => 'false', + ]; break; case 'z-a': - $order = 'TITLE|true'; + $order = [ + 'sort_key' => 'TITLE', + 'reverse' => 'true', + ]; break; case 'price-low': - $order = 'PRICE|false'; + $order = [ + 'sort_key' => 'PRICE', + 'reverse' => 'false', + ]; break; case 'price-high': - $order = 'PRICE|true'; + $order = [ + 'sort_key' => 'PRICE', + 'reverse' => 'true', + ]; break; default: - $order = 'CREATED_AT|false'; + $order = [ + 'sort_key' => 'CREATED_AT', + 'reverse' => 'false', + ]; } - list($sort_key, $reverse) = explode( '|', $order ); - return compact( 'sort_key', 'reverse' ); + return $order; } } diff --git a/includes/Shopping/Woocommerce_Query.php b/includes/Shopping/Woocommerce_Query.php index 6c621b5f7fe2..aec21c094e34 100644 --- a/includes/Shopping/Woocommerce_Query.php +++ b/includes/Shopping/Woocommerce_Query.php @@ -128,23 +128,37 @@ public function get_search( string $search_term, string $sort_by = '' ) { protected function parse_sort_by( string $sort_by = '' ): array { switch ( $sort_by ) { case 'a-z': - $order = 'title|ASC'; + $order = [ + 'orderby' => 'title', + 'order' => 'ASC', + ]; break; case 'z-a': - $order = 'title|DESC'; + $order = [ + 'orderby' => 'title', + 'order' => 'DESC', + ]; break; case 'price-low': - $order = 'price|ASC'; + $order = [ + 'orderby' => 'price', + 'order' => 'ASC', + ]; break; case 'price-high': - $order = 'price|DESC'; + $order = [ + 'orderby' => 'price', + 'order' => 'DESC', + ]; break; default: - $order = 'date|ASC'; + $order = [ + 'orderby' => 'date', + 'order' => 'ASC', + ]; } - list($orderby, $order) = explode( '|', $order ); - return compact( 'orderby', 'order' ); + return $order; } /** From 11f742322017d1df8c7a76ae7d203b75efe6b293 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 17:10:28 -0400 Subject: [PATCH 04/42] add orderby + order params --- includes/Experiments.php | 1 + includes/Interfaces/Product_Query.php | 5 +- includes/REST_API/Products_Controller.php | 46 +++++++++++- includes/Shopping/Shopify_Query.php | 73 ++++--------------- includes/Shopping/Woocommerce_Query.php | 70 +++--------------- .../library/panes/shopping/productSort.js | 12 +-- .../library/panes/shopping/shoppingPane.js | 20 +++-- packages/wp-story-editor/src/api/shopping.js | 5 +- .../includes/Shopping/Mock_Vendor.php | 5 +- .../includes/Shopping/Mock_Vendor_Error.php | 5 +- .../includes/Shopping/Mock_Vendor_Invalid.php | 5 +- .../tests/Shopping/Shopify_Query.php | 13 ++-- .../unit/tests/Shopping/Woocommerce_Query.php | 53 -------------- 13 files changed, 111 insertions(+), 202 deletions(-) diff --git a/includes/Experiments.php b/includes/Experiments.php index c0a731375d1e..ff3718124bd8 100644 --- a/includes/Experiments.php +++ b/includes/Experiments.php @@ -319,6 +319,7 @@ public function get_experiments(): array { 'label' => __( 'Shopping', 'web-stories' ), 'description' => __( 'Enable shopping integration in the editor', 'web-stories' ), 'group' => 'general', + 'default' => true, ], /** * Author: @spacedmonkey diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index b75c378ecabb..5eb1b3e0707a 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -39,8 +39,9 @@ interface Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort order for query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by); + public function get_search( string $search_term, string $orderby, string $order); } diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 0d15220489ac..719a0e55df10 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -168,10 +168,19 @@ public function get_items( $request ) { /** * Request context. * - * @var string $sort_by + * @var string $orderby */ - $sort_by = ! empty( $request['sort_by'] ) ? $request['sort_by'] : ''; - $query_result = $query->get_search( $search_term, $sort_by ); + $orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : ''; + + /** + * Request context. + * + * @var string $order + */ + $order = ! empty( $request['order'] ) ? $request['order'] : ''; + + + $query_result = $query->get_search( $search_term, $orderby, $order ); if ( is_wp_error( $query_result ) ) { return $query_result; } @@ -396,4 +405,35 @@ public function get_item_schema(): array { return $schema; } + + /** + * Retrieves the query params for the products collection. + * + * @since 1.21.0 + * + * @return array Collection parameters. + */ + public function get_collection_params(): array { + $query_params = parent::get_collection_params(); + + $query_params['orderby'] = [ + 'description' => __( 'Sort collection by product attribute.', 'web-stories' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => [ + 'date', + 'price', + 'title', + ], + ]; + + $query_params['order'] = [ + 'description' => __( 'Order sort attribute ascending or descending.', 'web-stories' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => [ 'asc', 'desc' ], + ]; + + return $query_params; + } } diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 90443e764872..070949421ab5 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -151,17 +151,17 @@ protected function execute_query( string $query ) { * @since 1.21.0 * * @param string $search_term Search term to filter products by. - * @param string $sort_by sort order for query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return string The assembled GraphQL query. */ - protected function get_products_query( string $search_term, string $sort_by = '' ): string { + protected function get_products_query( string $search_term, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; - $sort = $this->parse_sort_by( $sort_by ); - $sort_key = $sort['sort_key']; - $reverse = $sort['reverse']; + $orderby = $orderby === 'date' ? 'CREATED_AT' : strtoupper( $orderby ); + $order = $order === 'asc' ? 'false' : 'true'; return <<get_products_query( $search_term, $sort_by ); + $query = $this->get_products_query( $search_term, $orderby, $order ); $body = $this->execute_query( $query ); @@ -253,12 +254,13 @@ protected function fetch_remote_products( string $search_term, string $sort_by = * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort condition for product query. + * @param string $orderby sort field for query. + * @param string $order ASC or DESC. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by = '' ) { + public function get_search( string $search_term, string $orderby, string $order ) { - $result = $this->fetch_remote_products( $search_term, $sort_by ); + $result = $this->fetch_remote_products( $search_term, $orderby, $order ); if ( is_wp_error( $result ) ) { return $result; @@ -304,49 +306,4 @@ public function get_search( string $search_term, string $sort_by = '' ) { return $results; } - - /** - * Parse sort type into array for product query - * - * @since 1.21.0 - * - * @param string $sort_by sort condition for product query. - * @return array - */ - protected function parse_sort_by( string $sort_by ): array { - - switch ( $sort_by ) { - case 'a-z': - $order = [ - 'sort_key' => 'TITLE', - 'reverse' => 'false', - ]; - break; - case 'z-a': - $order = [ - 'sort_key' => 'TITLE', - 'reverse' => 'true', - ]; - break; - case 'price-low': - $order = [ - 'sort_key' => 'PRICE', - 'reverse' => 'false', - ]; - break; - case 'price-high': - $order = [ - 'sort_key' => 'PRICE', - 'reverse' => 'true', - ]; - break; - default: - $order = [ - 'sort_key' => 'CREATED_AT', - 'reverse' => 'false', - ]; - } - - return $order; - } } diff --git a/includes/Shopping/Woocommerce_Query.php b/includes/Shopping/Woocommerce_Query.php index aec21c094e34..991acdfdaeaf 100644 --- a/includes/Shopping/Woocommerce_Query.php +++ b/includes/Shopping/Woocommerce_Query.php @@ -39,10 +39,11 @@ class Woocommerce_Query implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort condition for product query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by = '' ) { + public function get_search( string $search_term, string $orderby, string $order ) { if ( ! function_exists( 'wc_get_products' ) ) { return new WP_Error( 'rest_unknown', __( 'Woocommerce is not installed.', 'web-stories' ), [ 'status' => 400 ] ); @@ -50,27 +51,24 @@ public function get_search( string $search_term, string $sort_by = '' ) { $results = []; - $order_by = $this->parse_sort_by( $sort_by ); - /** * Products. * * @var \WC_Product[] $products */ $products = wc_get_products( - array_merge( - [ - 'status' => 'publish', - 'limit' => 100, - 's' => $search_term, - ], - $order_by - ) + [ + 'status' => 'publish', + 'limit' => 100, + 's' => $search_term, + 'orderby' => $orderby, + 'order' => $order, + ] ); - if ( 'price' === $order_by['orderby'] ) { + if ( 'price' === $orderby ) { // @todo this only orders products based on wc_get_products previously called - $products = wc_products_array_orderby( $products, 'price', $order_by['order'] ); + $products = wc_products_array_orderby( $products, 'price', $order ); } $product_image_ids = []; @@ -117,50 +115,6 @@ public function get_search( string $search_term, string $sort_by = '' ) { return $results; } - /** - * Parse sort type into array for product query - * - * @since 1.21.0 - * - * @param string $sort_by sort condition for product query. - * @return array - */ - protected function parse_sort_by( string $sort_by = '' ): array { - switch ( $sort_by ) { - case 'a-z': - $order = [ - 'orderby' => 'title', - 'order' => 'ASC', - ]; - break; - case 'z-a': - $order = [ - 'orderby' => 'title', - 'order' => 'DESC', - ]; - break; - case 'price-low': - $order = [ - 'orderby' => 'price', - 'order' => 'ASC', - ]; - break; - case 'price-high': - $order = [ - 'orderby' => 'price', - 'order' => 'DESC', - ]; - break; - default: - $order = [ - 'orderby' => 'date', - 'order' => 'ASC', - ]; - } - - return $order; - } - /** * Get all product image ids (feature image + gallery_images). * diff --git a/packages/story-editor/src/components/library/panes/shopping/productSort.js b/packages/story-editor/src/components/library/panes/shopping/productSort.js index eb51103e1ace..7697ce2fb5a5 100644 --- a/packages/story-editor/src/components/library/panes/shopping/productSort.js +++ b/packages/story-editor/src/components/library/panes/shopping/productSort.js @@ -34,11 +34,11 @@ function SortDropdown({ onChange, sortId }) { } const options = [ - { id: 'recent', name: __('Recently Added', 'web-stories') }, - { id: 'a-z', name: __('Alphabetical: A-Z', 'web-stories') }, - { id: 'z-a', name: __('Alphabetical: Z-A', 'web-stories') }, - { id: 'price-low', name: __('Price: low to high', 'web-stories') }, - { id: 'price-high', name: __('Price: high to low', 'web-stories') }, + { orderby: 'date', order: 'desc', name: __('Recently Added', 'web-stories') }, + { orderby: 'title', order: 'asc', name: __('Alphabetical: A-Z', 'web-stories') }, + { orderby: 'title', order: 'desc', name: __('Alphabetical: Z-A', 'web-stories') }, + { orderby: 'price', order: 'asc', name: __('Price: low to high', 'web-stories') }, + { orderby: 'price', order: 'desc', name: __('Price: high to low', 'web-stories') }, ]; return ( @@ -47,7 +47,7 @@ function SortDropdown({ onChange, sortId }) { dropDownLabel={__('Sort', 'web-stories')} onChange={onChange} selectedId={sortId} - options={options} + options={options.map((option) => ({ id: `${option.orderby}-${option.order}`, ...option }))} aria-label={__('Sort by', 'web-stories')} /> diff --git a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js index fb17c0f9b3a8..59f5bf57ea99 100644 --- a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js +++ b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js @@ -69,8 +69,12 @@ function ShoppingPane(props) { const [loaded, setLoaded] = useState(false); const [isLoading, setIsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - const [sortBy, setSortBy] = useState('recent'); - const onSortBy = ({ id }) => setSortBy(id); + const [orderby, setOrderby] = useState('date'); + const [order, setOrder] = useState('desc'); + const onSortBy = ({ orderby, order }) => { + setOrderby(orderby); + setOrder(order); + }; const [isMenuFocused, setIsMenuFocused] = useState(false); const [products, setProducts] = useState([]); const { @@ -87,10 +91,10 @@ function ShoppingPane(props) { })); const getProductsByQuery = useCallback( - async (value = '', sort = '') => { + async (value = '', orderby, order) => { try { setIsLoading(true); - setProducts(await getProducts(value, sort)); + setProducts(await getProducts(value, orderby, order)); } catch (err) { setProducts([]); } finally { @@ -98,7 +102,7 @@ function ShoppingPane(props) { setLoaded(true); } }, - [getProducts] + [getProducts, orderby, order] ); const onSearch = useCallback( @@ -114,8 +118,8 @@ function ShoppingPane(props) { const debouncedProductsQuery = useDebouncedCallback(getProductsByQuery, 300); useEffect( - () => debouncedProductsQuery(searchTerm, sortBy), - [searchTerm, sortBy, debouncedProductsQuery] + () => debouncedProductsQuery(searchTerm, orderby, order), + [searchTerm, orderby, order, debouncedProductsQuery] ); const handleInputKeyPress = useCallback((event) => { @@ -215,7 +219,7 @@ function ShoppingPane(props) { clearId="clear-product-search" handleClearInput={handleClearInput} /> - + {isLoading && ( diff --git a/packages/wp-story-editor/src/api/shopping.js b/packages/wp-story-editor/src/api/shopping.js index 0b68559953cd..9e104591e403 100644 --- a/packages/wp-story-editor/src/api/shopping.js +++ b/packages/wp-story-editor/src/api/shopping.js @@ -24,12 +24,13 @@ import apiFetch from '@wordpress/api-fetch'; */ import { addQueryArgs } from '@googleforcreators/url'; -export function getProducts(config, search, sort_by) { +export function getProducts(config, search, orderby, order) { return apiFetch({ path: addQueryArgs(config.api.products, { per_page: 100, search, - sort_by, + orderby, + order, }), }); } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index 187ce444e9a2..f4f1cdcf3e7a 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -32,10 +32,11 @@ class Mock_Vendor implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort order for query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by ) { + public function get_search( string $search_term, string $orderby, string $order ) { $products = []; for ( $x = 0; $x < 10; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index 49c2152cba44..94fd1b99dd9b 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -32,10 +32,11 @@ class Mock_Vendor_Error implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort order for query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by ) { + public function get_search( string $search_term, string $orderby, $order ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index 6a587aeeeed6..88527aa98990 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -31,10 +31,11 @@ class Mock_Vendor_Invalid { * @since 1.21.0 * * @param string $search_term Search term. - * @param string $sort_by sort order for query. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $sort_by ) { + public function get_search( string $search_term, string $orderby, string $order ) { $products = []; for ( $x = 0; $x < 10; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index d92aed5a4856..5a88b6ae418b 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,11 +184,12 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; - $sort_by = ''; + $orderby = ''; + $order = ''; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $sort_by ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); $actual = $this->instance->get_search( '', '' ); @@ -307,22 +308,22 @@ public function test_get_search_sort_by_query(): void { $this->assertStringContainsString( 'sortKey: CREATED_AT', $this->request_body ); $this->assertStringContainsString( 'reverse: false', $this->request_body ); - $actual = $this->instance->get_search( 'some search term', 'a-z' ); + $actual = $this->instance->get_search( 'some search term', 'title', 'asc' ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); $this->assertStringContainsString( 'reverse: false', $this->request_body ); - $actual = $this->instance->get_search( 'some search term', 'z-a' ); + $actual = $this->instance->get_search( 'some search term', 'title', 'desc' ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); $this->assertStringContainsString( 'reverse: true', $this->request_body ); - $actual = $this->instance->get_search( 'some search term', 'price-low' ); + $actual = $this->instance->get_search( 'some search term', 'price', 'asc' ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); $this->assertStringContainsString( 'reverse: false', $this->request_body ); - $actual = $this->instance->get_search( 'some search term', 'price-high' ); + $actual = $this->instance->get_search( 'some search term', 'price', 'desc' ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); $this->assertStringContainsString( 'reverse: true', $this->request_body ); diff --git a/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php b/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php index 18a7f9e074dc..af4e9ca96b0b 100644 --- a/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php +++ b/tests/phpunit/unit/tests/Shopping/Woocommerce_Query.php @@ -172,57 +172,4 @@ public function test_get_product_image(): void { $results ); } - - - /** - * @covers ::parse_sort_by - */ - public function test_parse_sort_by(): void { - $product_query = new Query(); - - $order = $this->call_private_method( $product_query, 'parse_sort_by', [ '' ] ); - $this->assertEquals( - [ - 'orderby' => 'date', - 'order' => 'ASC', - ], - $order - ); - - $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'a-z' ] ); - $this->assertEquals( - [ - 'orderby' => 'title', - 'order' => 'ASC', - ], - $order - ); - - $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'z-a' ] ); - $this->assertEquals( - [ - 'orderby' => 'title', - 'order' => 'DESC', - ], - $order - ); - - $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'price-low' ] ); - $this->assertEquals( - [ - 'orderby' => 'price', - 'order' => 'ASC', - ], - $order - ); - - $order = $this->call_private_method( $product_query, 'parse_sort_by', [ 'price-high' ] ); - $this->assertEquals( - [ - 'orderby' => 'price', - 'order' => 'DESC', - ], - $order - ); - } } From d2badd226a8b2367da0291b0b0df24cf6ca6440d Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 17:11:05 -0400 Subject: [PATCH 05/42] file reser --- includes/Experiments.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/Experiments.php b/includes/Experiments.php index ff3718124bd8..c0a731375d1e 100644 --- a/includes/Experiments.php +++ b/includes/Experiments.php @@ -319,7 +319,6 @@ public function get_experiments(): array { 'label' => __( 'Shopping', 'web-stories' ), 'description' => __( 'Enable shopping integration in the editor', 'web-stories' ), 'group' => 'general', - 'default' => true, ], /** * Author: @spacedmonkey From 277d50a2c76fa4993144469167764322f6796cd1 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 17:41:22 -0400 Subject: [PATCH 06/42] fix tests --- includes/Shopping/Shopify_Query.php | 6 +++--- includes/Shopping/Woocommerce_Query.php | 2 +- .../integration/tests/Shopping/Shopify_Query.php | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 070949421ab5..0ac6d952bafe 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -157,8 +157,8 @@ protected function execute_query( string $query ) { */ protected function get_products_query( string $search_term, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; - $orderby = $orderby === 'date' ? 'CREATED_AT' : strtoupper( $orderby ); - $order = $order === 'asc' ? 'false' : 'true'; + $orderby = $orderby === 'date' ? 'CREATED_AT' : strtoupper( $orderby ); + $order = $order === 'asc' ? 'false' : 'true'; return <<fetch_remote_products( $search_term, $orderby, $order ); diff --git a/includes/Shopping/Woocommerce_Query.php b/includes/Shopping/Woocommerce_Query.php index 991acdfdaeaf..a5ff9d6cb991 100644 --- a/includes/Shopping/Woocommerce_Query.php +++ b/includes/Shopping/Woocommerce_Query.php @@ -43,7 +43,7 @@ class Woocommerce_Query implements Product_Query { * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, string $order ) { + public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) { if ( ! function_exists( 'wc_get_products' ) ) { return new WP_Error( 'rest_unknown', __( 'Woocommerce is not installed.', 'web-stories' ), [ 'status' => 400 ] ); diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 5a88b6ae418b..cde7b4feb936 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,14 +184,14 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; - $orderby = ''; - $order = ''; + $orderby = 'date'; + $order = 'desc'; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); - $actual = $this->instance->get_search( '', '' ); + $actual = $this->instance->get_search( '', $orderby, $order ); $this->assertNotWPError( $actual ); $this->assertSame( [], $actual ); @@ -280,7 +280,7 @@ public function test_get_search_empty_search_response(): void { add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); - $actual = $this->instance->get_search( 'some search term' ); + $actual = $this->instance->get_search( 'some search term', '', '' ); remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); @@ -303,10 +303,10 @@ public function test_get_search_sort_by_query(): void { update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); - $actual = $this->instance->get_search( 'some search term', '' ); + $actual = $this->instance->get_search( 'some search term' ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( 'sortKey: CREATED_AT', $this->request_body ); - $this->assertStringContainsString( 'reverse: false', $this->request_body ); + $this->assertStringContainsString( 'reverse: true', $this->request_body ); $actual = $this->instance->get_search( 'some search term', 'title', 'asc' ); $this->assertNotWPError( $actual ); From 7eb8917161791d8db42ea45e89546afb55c16057 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 18:18:24 -0400 Subject: [PATCH 07/42] remove empty quotes --- tests/phpunit/integration/tests/Shopping/Shopify_Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index cde7b4feb936..66f33ae5057c 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -280,7 +280,7 @@ public function test_get_search_empty_search_response(): void { add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); - $actual = $this->instance->get_search( 'some search term', '', '' ); + $actual = $this->instance->get_search( 'some search term' ); remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); From 319536c5673eff42268ee51520eff23c74b2fc87 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Tue, 24 May 2022 18:55:01 -0400 Subject: [PATCH 08/42] lint --- includes/Shopping/Shopify_Query.php | 4 +-- .../library/panes/shopping/productSort.js | 35 +++++++++++++++---- .../library/panes/shopping/shoppingPane.js | 12 +++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 0ac6d952bafe..72668df496e8 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -157,8 +157,8 @@ protected function execute_query( string $query ) { */ protected function get_products_query( string $search_term, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; - $orderby = $orderby === 'date' ? 'CREATED_AT' : strtoupper( $orderby ); - $order = $order === 'asc' ? 'false' : 'true'; + $orderby = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); + $order = 'asc' === $order ? 'false' : 'true'; return << ({ id: `${option.orderby}-${option.order}`, ...option }))} + options={options.map((option) => ({ + id: `${option.orderby}-${option.order}`, + ...option, + }))} aria-label={__('Sort by', 'web-stories')} /> diff --git a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js index 59f5bf57ea99..a98c698097da 100644 --- a/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js +++ b/packages/story-editor/src/components/library/panes/shopping/shoppingPane.js @@ -71,9 +71,9 @@ function ShoppingPane(props) { const [searchTerm, setSearchTerm] = useState(''); const [orderby, setOrderby] = useState('date'); const [order, setOrder] = useState('desc'); - const onSortBy = ({ orderby, order }) => { - setOrderby(orderby); - setOrder(order); + const onSortBy = (option) => { + setOrderby(option?.orderby); + setOrder(option?.order); }; const [isMenuFocused, setIsMenuFocused] = useState(false); const [products, setProducts] = useState([]); @@ -91,10 +91,10 @@ function ShoppingPane(props) { })); const getProductsByQuery = useCallback( - async (value = '', orderby, order) => { + async (value = '', sortBy, sortOrder) => { try { setIsLoading(true); - setProducts(await getProducts(value, orderby, order)); + setProducts(await getProducts(value, sortBy, sortOrder)); } catch (err) { setProducts([]); } finally { @@ -102,7 +102,7 @@ function ShoppingPane(props) { setLoaded(true); } }, - [getProducts, orderby, order] + [getProducts] ); const onSearch = useCallback( From 9a2e4ee2c4f507de92c1b554c21b904408bfb54e Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 07:09:27 -0400 Subject: [PATCH 09/42] Update packages/story-editor/src/components/library/panes/shopping/productSort.js Co-authored-by: Pascal Birchler --- .../src/components/library/panes/shopping/productSort.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/story-editor/src/components/library/panes/shopping/productSort.js b/packages/story-editor/src/components/library/panes/shopping/productSort.js index 1a83f77a24a9..7c7df82968fd 100644 --- a/packages/story-editor/src/components/library/panes/shopping/productSort.js +++ b/packages/story-editor/src/components/library/panes/shopping/productSort.js @@ -71,7 +71,7 @@ function SortDropdown({ onChange, sortId }) { id: `${option.orderby}-${option.order}`, ...option, }))} - aria-label={__('Sort by', 'web-stories')} + aria-label={__('Product sort options', 'web-stories')} /> ); From 6c67d4a231dc12dda5a44066b0408ac24df3d165 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 07:10:43 -0400 Subject: [PATCH 10/42] Update includes/Shopping/Shopify_Query.php Co-authored-by: Pascal Birchler --- includes/Shopping/Shopify_Query.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 72668df496e8..31b9bd1af39b 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -199,8 +199,9 @@ protected function get_products_query( string $search_term, string $orderby, str * @since 1.21.0 * * @param string $search_term Search term to filter products by. - * @param string $orderby sort field for query. - * @param string $order ASC or DESC. + * @param string $orderby Sort retrieved products by parameter. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). * @return array|WP_Error Response data or error object on failure. */ protected function fetch_remote_products( string $search_term, string $orderby = '', string $order = '' ) { From 8b1e4b57d3fdfb87a732fa378dc959856cacea51 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 07:10:55 -0400 Subject: [PATCH 11/42] Update includes/Shopping/Shopify_Query.php Co-authored-by: Pascal Birchler --- includes/Shopping/Shopify_Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 31b9bd1af39b..bf15282566f4 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -204,7 +204,7 @@ protected function get_products_query( string $search_term, string $orderby, str * Accepts 'asc' (ascending) or 'desc' (descending). * @return array|WP_Error Response data or error object on failure. */ - protected function fetch_remote_products( string $search_term, string $orderby = '', string $order = '' ) { + protected function fetch_remote_products( string $search_term, string $orderby, string $order ) { /** * Filters the Shopify products data TTL value. From 5ba725c83313b114bc1706a317bbc7180d18ba00 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 07:15:40 -0400 Subject: [PATCH 12/42] update var names --- includes/Shopping/Shopify_Query.php | 6 +++--- .../src/components/library/panes/shopping/productSort.js | 6 ------ .../src/components/library/panes/shopping/shoppingPane.js | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index bf15282566f4..d54ebedbcf18 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -157,11 +157,11 @@ protected function execute_query( string $query ) { */ protected function get_products_query( string $search_term, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; - $orderby = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); - $order = 'asc' === $order ? 'false' : 'true'; + $sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); + $reverse = 'asc' === $order ? 'false' : 'true'; return << { - setOrderby(option?.orderby); - setOrder(option?.order); + setOrderby(option.orderby); + setOrder(option.order); }; const [isMenuFocused, setIsMenuFocused] = useState(false); const [products, setProducts] = useState([]); From b59acac94159affd39ed4865b7cde281bf730449 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 08:28:09 -0400 Subject: [PATCH 13/42] test data provider --- .../tests/Shopping/Shopify_Query.php | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 66f33ae5057c..14c6d4474c95 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -290,6 +290,34 @@ public function test_get_search_empty_search_response(): void { $this->assertSame( 1, $this->request_count ); $this->assertStringContainsString( 'query: "title:*some search term*"', $this->request_body ); } + + /** + * @dataProvider data_test_get_search_sort_by_query + */ + public function data_test_get_search_sort_by_query(): array { + return [ + 'Default search' => [ + [ 'some search term', 'date', '' ], + ['sortKey: CREATED_AT', 'reverse: true'], + ], + 'Sort title asc' => [ + [ '', 'title', 'asc' ], + ['sortKey: TITLE', 'reverse: false'], + ], + 'Sort title desc' => [ + [ '', 'title', 'desc' ], + ['sortKey: TITLE', 'reverse: true'], + ], + 'Sort price asc' => [ + [ '', 'price', 'asc' ], + ['sortKey: PRICE', 'reverse: false'], + ], + 'Sort price desc' => [ + [ '', 'price', 'desc' ], + ['sortKey: PRICE', 'reverse: true'], + ] + ]; + } /** * @covers ::fetch_remote_products @@ -297,39 +325,16 @@ public function test_get_search_empty_search_response(): void { * @covers ::get_products_query * @covers ::execute_query * @covers ::parse_sort_by + * @dataProvider data_test_get_search_sort_by_query */ - public function test_get_search_sort_by_query(): void { + public function test_get_search_sort_by_query( $args, $expected ): void { update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); - - $actual = $this->instance->get_search( 'some search term' ); - $this->assertNotWPError( $actual ); - $this->assertStringContainsString( 'sortKey: CREATED_AT', $this->request_body ); - $this->assertStringContainsString( 'reverse: true', $this->request_body ); - - $actual = $this->instance->get_search( 'some search term', 'title', 'asc' ); - $this->assertNotWPError( $actual ); - $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); - $this->assertStringContainsString( 'reverse: false', $this->request_body ); - - $actual = $this->instance->get_search( 'some search term', 'title', 'desc' ); - $this->assertNotWPError( $actual ); - $this->assertStringContainsString( 'sortKey: TITLE', $this->request_body ); - $this->assertStringContainsString( 'reverse: true', $this->request_body ); - - $actual = $this->instance->get_search( 'some search term', 'price', 'asc' ); - $this->assertNotWPError( $actual ); - $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); - $this->assertStringContainsString( 'reverse: false', $this->request_body ); - - $actual = $this->instance->get_search( 'some search term', 'price', 'desc' ); - $this->assertNotWPError( $actual ); - $this->assertStringContainsString( 'sortKey: PRICE', $this->request_body ); - $this->assertStringContainsString( 'reverse: true', $this->request_body ); - + $actual = $this->instance->get_search( $args[0], $args[1], $args[2] ); remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); + $this->assertNotWPError( $actual ); + $this->assertStringContainsString( $expected[0], $this->request_body ); + $this->assertStringContainsString( $expected[1], $this->request_body ); } - - } From 7e9d650f2e6c1b70466fd72d9f0c6d1b949d027f Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 08:31:49 -0400 Subject: [PATCH 14/42] Update includes/REST_API/Products_Controller.php Co-authored-by: Jonny Harris --- includes/REST_API/Products_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 719a0e55df10..737291ea655c 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -170,7 +170,7 @@ public function get_items( $request ) { * * @var string $orderby */ - $orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : ''; + $orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : 'date'; /** * Request context. From 7cf285ab2f0fe913f5f69685af7ec0c751b29355 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 08:31:59 -0400 Subject: [PATCH 15/42] Update tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php Co-authored-by: Jonny Harris --- .../phpunit/integration/includes/Shopping/Mock_Vendor_Error.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index 94fd1b99dd9b..e40c79df9781 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -36,7 +36,7 @@ class Mock_Vendor_Error implements Product_Query { * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, $order ) { + public function get_search( string $search_term, string $orderby, string $order ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } From 10faab366df1efaeddc16f3d0bfbbf7dd70fec09 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 09:07:42 -0400 Subject: [PATCH 16/42] Update includes/REST_API/Products_Controller.php Co-authored-by: Jonny Harris --- includes/REST_API/Products_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 737291ea655c..b0a8c84c1806 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -177,7 +177,7 @@ public function get_items( $request ) { * * @var string $order */ - $order = ! empty( $request['order'] ) ? $request['order'] : ''; + $order = ! empty( $request['order'] ) ? $request['order'] : 'desc'; $query_result = $query->get_search( $search_term, $orderby, $order ); From 90a4d1b88aa07d97711f2d8472d04baefdb146d2 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 10:17:19 -0400 Subject: [PATCH 17/42] Update includes/Shopping/Shopify_Query.php Co-authored-by: Jonny Harris --- includes/Shopping/Shopify_Query.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index d54ebedbcf18..cc65a4d7bef6 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -260,7 +260,6 @@ protected function fetch_remote_products( string $search_term, string $orderby, * @return Product[]|WP_Error */ public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) { - $result = $this->fetch_remote_products( $search_term, $orderby, $order ); if ( is_wp_error( $result ) ) { From 9588665b5dcd702144d0a6fea0f6bff33eb2cf61 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 11:55:11 -0400 Subject: [PATCH 18/42] Update includes/Shopping/Shopify_Query.php Co-authored-by: Pascal Birchler --- includes/Shopping/Shopify_Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index cc65a4d7bef6..e72e57472f84 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -201,7 +201,7 @@ protected function get_products_query( string $search_term, string $orderby, str * @param string $search_term Search term to filter products by. * @param string $orderby Sort retrieved products by parameter. * @param string $order Whether to order products in ascending or descending order. - * Accepts 'asc' (ascending) or 'desc' (descending). + * Accepts 'asc' (ascending) or 'desc' (descending). * @return array|WP_Error Response data or error object on failure. */ protected function fetch_remote_products( string $search_term, string $orderby, string $order ) { From 899419fde7a59e5e6be77e844b456111d01e7bd0 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 12:28:32 -0400 Subject: [PATCH 19/42] lint --- includes/Shopping/Shopify_Query.php | 2 +- .../tests/Shopping/Shopify_Query.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index e72e57472f84..a1a3632d0b93 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -158,7 +158,7 @@ protected function execute_query( string $query ) { protected function get_products_query( string $search_term, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; $sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); - $reverse = 'asc' === $order ? 'false' : 'true'; + $reverse = 'asc' === $order ? 'false' : 'true'; return << [ + 'Default search' => [ [ 'some search term', 'date', '' ], - ['sortKey: CREATED_AT', 'reverse: true'], + [ 'sortKey: CREATED_AT', 'reverse: true' ], ], - 'Sort title asc' => [ + 'Sort title asc' => [ [ '', 'title', 'asc' ], - ['sortKey: TITLE', 'reverse: false'], + [ 'sortKey: TITLE', 'reverse: false' ], ], 'Sort title desc' => [ [ '', 'title', 'desc' ], - ['sortKey: TITLE', 'reverse: true'], + [ 'sortKey: TITLE', 'reverse: true' ], ], - 'Sort price asc' => [ + 'Sort price asc' => [ [ '', 'price', 'asc' ], - ['sortKey: PRICE', 'reverse: false'], + [ 'sortKey: PRICE', 'reverse: false' ], ], 'Sort price desc' => [ [ '', 'price', 'desc' ], - ['sortKey: PRICE', 'reverse: true'], - ] + [ 'sortKey: PRICE', 'reverse: true' ], + ], ]; } From 39fa5ff242ef88e0cfba34413f5b18401817581b Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Wed, 25 May 2022 13:38:30 -0400 Subject: [PATCH 20/42] lint --- .../src/components/library/panes/shopping/productSort.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/story-editor/src/components/library/panes/shopping/productSort.js b/packages/story-editor/src/components/library/panes/shopping/productSort.js index fe7b6072ca46..a9293355e34b 100644 --- a/packages/story-editor/src/components/library/panes/shopping/productSort.js +++ b/packages/story-editor/src/components/library/panes/shopping/productSort.js @@ -26,7 +26,6 @@ const StyledContainer = styled.div` `; function SortDropdown({ onChange, sortId }) { - const options = [ { orderby: 'date', From 8008da617a5586b831239e437fdb4b1217639656 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Thu, 26 May 2022 06:48:50 -0400 Subject: [PATCH 21/42] add karma test --- .../elements/karma/shopping.karma.js | 25 +++++++++++++++++++ .../library/panes/shopping/productList.js | 7 +++++- .../karma/fixture/db/getProductsResponse.js | 11 ++++++++ .../story-editor/src/karma/fixture/fixture.js | 10 ++++++-- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js b/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js index 9b9fa7fbabe8..36e7a5dda723 100644 --- a/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js +++ b/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js @@ -121,6 +121,31 @@ describe('Shopping integration', () => { await fixture.events.mouse.clickOn(product, 1, 1); expect(isStoryEmpty()).toEqual(true); }); + + it('should sort searched products', async () => { + await fixture.editor.library.shoppingTab.click(); + await fixture.events.keyboard.press('tab'); + + const sortDropdown = fixture.querySelector( + '[aria-label="Product sort options"]' + ); + await fixture.events.mouse.clickOn(sortDropdown, 1, 1); + + const option = fixture.screen.getByRole('option', { + name: /^Alphabetical: Z-A/, + }); + + await fixture.events.mouse.clickOn(option, 1, 1); + + // delay for search to catch-up + await fixture.events.sleep(400); + + const firstOption = fixture.querySelector( + '[aria-label="Products list"] [role="listitem"]' + ); + + expect(firstOption.textContent).toContain('WordPress Pennant'); + }); }); // eslint-disable-next-line jasmine/no-disabled-tests -- TODO: Fix broken test. diff --git a/packages/story-editor/src/components/library/panes/shopping/productList.js b/packages/story-editor/src/components/library/panes/shopping/productList.js index 09137d3fcd84..f23655fd82c0 100644 --- a/packages/story-editor/src/components/library/panes/shopping/productList.js +++ b/packages/story-editor/src/components/library/panes/shopping/productList.js @@ -19,6 +19,7 @@ import { useEffect } from '@googleforcreators/react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; +import { __ } from '@googleforcreators/i18n'; /** * Internal dependencies @@ -63,7 +64,11 @@ function ProductList({ return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- list handles arrow up and arrow down -
+
{products.map((product, index) => { return ( { + const comparison = a[key].localeCompare(b[key]); + return order === 'desc' ? comparison * -1 : comparison; + }; +} + +export const sortStrings = (obj, key, order) => { + return obj.sort(compareStrings(key, order)); +}; diff --git a/packages/story-editor/src/karma/fixture/fixture.js b/packages/story-editor/src/karma/fixture/fixture.js index 34e09e460d9b..4f524643a4b3 100644 --- a/packages/story-editor/src/karma/fixture/fixture.js +++ b/packages/story-editor/src/karma/fixture/fixture.js @@ -44,7 +44,7 @@ import Layout from '../../components/layout'; import formattedTemplatesArray from '../../dataUtils/formattedTemplatesArray'; import { PRESET_TYPES } from '../../constants'; import getMediaResponse from './db/getMediaResponse'; -import getProductsResponse from './db/getProductsResponse'; +import getProductsResponse, { sortStrings } from './db/getProductsResponse'; import { Editor as EditorContainer } from './containers'; import taxonomiesResponse from './db/getTaxonomiesResponse'; import singleSavedTemplate from './db/singleSavedTemplate'; @@ -1106,13 +1106,19 @@ class APIProviderFixture { return asyncResponse(fonts); }, []); - const getProducts = useCallback((search) => { + const getProducts = useCallback((search, orderby, order) => { let products = getProductsResponse; if (search) { products = products.filter(({ productTitle }) => productTitle.toLowerCase().includes(search.toLowerCase()) ); } + + // handling title sorting (asc, desc) only here + if (orderby === 'title') { + products = sortStrings(products, 'productTitle', order); + } + return products; }, []); From 5fa2786438cb7cd4f0fd1226331a979819fa4229 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Thu, 26 May 2022 12:05:09 +0100 Subject: [PATCH 22/42] Respect per page param. --- includes/Interfaces/Product_Query.php | 3 ++- includes/REST_API/Products_Controller.php | 18 +++++++++++---- includes/Shopping/Shopify_Query.php | 23 +++++++++++-------- includes/Shopping/WooCommerce_Query.php | 5 ++-- .../includes/Shopping/Mock_Vendor.php | 5 ++-- .../includes/Shopping/Mock_Vendor_Error.php | 3 ++- .../includes/Shopping/Mock_Vendor_Invalid.php | 5 ++-- .../tests/REST_API/Product_Controller.php | 13 +++++++---- .../tests/Shopping/Shopify_Query.php | 19 +++++++-------- 9 files changed, 59 insertions(+), 35 deletions(-) diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index 5eb1b3e0707a..2b76f663008a 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -39,9 +39,10 @@ interface Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, string $order); + public function get_search( string $search_term, int $per_page, string $orderby, string $order); } diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index b0a8c84c1806..ddb550ef623b 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -164,7 +164,7 @@ public function get_items( $request ) { * @var string $search_term */ $search_term = ! empty( $request['search'] ) ? $request['search'] : ''; - + /** * Request context. * @@ -172,15 +172,22 @@ public function get_items( $request ) { */ $orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : 'date'; + /** + * Request context. + * + * @var int $per_page + */ + $per_page = ! empty( $request['per_page'] ) ? $request['per_page'] : 100; + /** * Request context. * * @var string $order */ $order = ! empty( $request['order'] ) ? $request['order'] : 'desc'; - - - $query_result = $query->get_search( $search_term, $orderby, $order ); + + + $query_result = $query->get_search( $search_term, $per_page, $orderby, $order ); if ( is_wp_error( $query_result ) ) { return $query_result; } @@ -416,6 +423,9 @@ public function get_item_schema(): array { public function get_collection_params(): array { $query_params = parent::get_collection_params(); + + $query_params['per_page']['default'] = 100; + $query_params['orderby'] = [ 'description' => __( 'Sort collection by product attribute.', 'web-stories' ), 'type' => 'string', diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index a1a3632d0b93..93c290d73102 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -151,17 +151,18 @@ protected function execute_query( string $query ) { * @since 1.21.0 * * @param string $search_term Search term to filter products by. + * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return string The assembled GraphQL query. */ - protected function get_products_query( string $search_term, string $orderby, string $order ): string { + protected function get_products_query( string $search_term, int $per_page, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; $sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); $reverse = 'asc' === $order ? 'false' : 'true'; return <<get_products_query( $search_term, $orderby, $order ); + $query = $this->get_products_query( $search_term, $per_page, $orderby, $order ); $body = $this->execute_query( $query ); @@ -248,20 +250,21 @@ protected function fetch_remote_products( string $search_term, string $orderby, return $result; } - + /** * Get products by search term. * * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query. * @param string $orderby sort field for query. * @param string $order ASC or DESC. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) { - $result = $this->fetch_remote_products( $search_term, $orderby, $order ); - + public function get_search( string $search_term, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { + $result = $this->fetch_remote_products( $search_term, $per_page, $orderby, $order ); + if ( is_wp_error( $result ) ) { return $result; } diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index 45c575af4cef..c5fc9a0c5879 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -56,11 +56,12 @@ public function __construct( WooCommerce $woocommerce ) { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) { + public function get_search( string $search_term, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $status = $this->woocommerce->get_plugin_status(); if ( ! $status['active'] ) { @@ -77,7 +78,7 @@ public function get_search( string $search_term, string $orderby = 'date', strin $products = wc_get_products( [ 'status' => 'publish', - 'limit' => 100, + 'limit' => $per_page, 's' => $search_term, 'orderby' => $orderby, 'order' => $order, diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index f4f1cdcf3e7a..6c4a5d239159 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -32,13 +32,14 @@ class Mock_Vendor implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, string $order ) { + public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { $products = []; - for ( $x = 0; $x < 10; $x ++ ) { + for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( [ 'id' => wp_unique_id(), diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index e40c79df9781..2af6ca2c406d 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -32,11 +32,12 @@ class Mock_Vendor_Error implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, string $order ) { + public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index 88527aa98990..eb8ba06c675d 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -31,13 +31,14 @@ class Mock_Vendor_Invalid { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, string $orderby, string $order ) { + public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { $products = []; - for ( $x = 0; $x < 10; $x ++ ) { + for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( [ 'id' => wp_unique_id(), diff --git a/tests/phpunit/integration/tests/REST_API/Product_Controller.php b/tests/phpunit/integration/tests/REST_API/Product_Controller.php index c36b77f6eb94..8c85eb7a6060 100644 --- a/tests/phpunit/integration/tests/REST_API/Product_Controller.php +++ b/tests/phpunit/integration/tests/REST_API/Product_Controller.php @@ -116,13 +116,15 @@ public function test_without_permission(): void { public function test_permission(): void { $this->controller->register(); + $per_page = 10; wp_set_current_user( self::$editor ); $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/products' ); $request->set_param( 'search', 'test' ); + $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 10, $data ); + $this->assertCount( $per_page, $data ); foreach ( $data as $product ) { $this->assertArrayHasKey( 'productId', $product ); @@ -138,14 +140,16 @@ public function test_permission(): void { public function test_fields(): void { $this->controller->register(); + $per_page = 10; wp_set_current_user( self::$editor ); $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/products' ); $request->set_param( 'search', 'test' ); $request->set_param( '_fields', 'productId' ); + $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 10, $data ); + $this->assertCount( $per_page, $data ); foreach ( $data as $product ) { $this->assertArrayHasKey( 'productId', $product ); @@ -190,14 +194,15 @@ public function test_return_none(): void { */ public function test_json_schema_validation(): void { $this->controller->register(); - + $per_page = 10; wp_set_current_user( self::$editor ); $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/products' ); $request->set_param( 'search', 'test' ); + $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 10, $data ); + $this->assertCount( $per_page, $data ); foreach ( $data as $product ) { $this->assertMatchesProductSchema( $product ); diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 54310c43778f..95edfcfac0c8 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,14 +184,15 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; + $per_page = 100; $orderby = 'date'; $order = 'desc'; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); - $actual = $this->instance->get_search( '', $orderby, $order ); + $actual = $this->instance->get_search( '', $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); $this->assertSame( [], $actual ); @@ -290,30 +291,30 @@ public function test_get_search_empty_search_response(): void { $this->assertSame( 1, $this->request_count ); $this->assertStringContainsString( 'query: "title:*some search term*"', $this->request_body ); } - + /** * @dataProvider data_test_get_search_sort_by_query */ public function data_test_get_search_sort_by_query(): array { return [ 'Default search' => [ - [ 'some search term', 'date', '' ], + [ 'some search term', 100, 'date', '' ], [ 'sortKey: CREATED_AT', 'reverse: true' ], ], 'Sort title asc' => [ - [ '', 'title', 'asc' ], + [ '', 100, 'title', 'asc' ], [ 'sortKey: TITLE', 'reverse: false' ], ], 'Sort title desc' => [ - [ '', 'title', 'desc' ], + [ '', 100, 'title', 'desc' ], [ 'sortKey: TITLE', 'reverse: true' ], ], 'Sort price asc' => [ - [ '', 'price', 'asc' ], + [ '', 100, 'price', 'asc' ], [ 'sortKey: PRICE', 'reverse: false' ], ], 'Sort price desc' => [ - [ '', 'price', 'desc' ], + [ '', 100, 'price', 'desc' ], [ 'sortKey: PRICE', 'reverse: true' ], ], ]; @@ -331,7 +332,7 @@ public function test_get_search_sort_by_query( $args, $expected ): void { update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); add_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ], 10, 2 ); - $actual = $this->instance->get_search( $args[0], $args[1], $args[2] ); + $actual = $this->instance->get_search( ...$args ); remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); $this->assertNotWPError( $actual ); $this->assertStringContainsString( $expected[0], $this->request_body ); From d8fba81ed96fd3525cbfc5c74ef164ff7679c613 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Thu, 26 May 2022 09:03:18 -0400 Subject: [PATCH 23/42] update wc price query --- includes/Shopping/WooCommerce_Query.php | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index 45c575af4cef..3a5766fc6c96 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -28,6 +28,7 @@ use Google\Web_Stories\Integrations\WooCommerce; use Google\Web_Stories\Interfaces\Product_Query; +use WC_Query; use WP_Error; /** @@ -69,26 +70,31 @@ public function get_search( string $search_term, string $orderby = 'date', strin $results = []; + $wc_args = []; + + if ( 'price' === $orderby ) { + $wc_query = new WC_Query(); + $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); + } + /** * Products. * * @var \WC_Product[] $products */ $products = wc_get_products( - [ - 'status' => 'publish', - 'limit' => 100, - 's' => $search_term, - 'orderby' => $orderby, - 'order' => $order, - ] + array_merge( + [ + 'status' => 'publish', + 'limit' => 100, + 's' => $search_term, + 'orderby' => $orderby, + 'order' => $order, + ], + $wc_args + ) ); - if ( 'price' === $orderby ) { - // @todo this only orders products based on wc_get_products previously called - $products = wc_products_array_orderby( $products, 'price', $order ); - } - $product_image_ids = []; foreach ( $products as $product ) { $product_image_ids[] = $this->get_product_image_ids( $product ); From ec831313748598a8b751c697ff2f46c507e51a42 Mon Sep 17 00:00:00 2001 From: Tim Arney Date: Fri, 27 May 2022 06:41:29 -0400 Subject: [PATCH 24/42] Update includes/Shopping/Shopify_Query.php Co-authored-by: Pascal Birchler --- includes/Shopping/Shopify_Query.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index a1a3632d0b93..d7051b308c6f 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -255,8 +255,9 @@ protected function fetch_remote_products( string $search_term, string $orderby, * @since 1.21.0 * * @param string $search_term Search term. - * @param string $orderby sort field for query. - * @param string $order ASC or DESC. + * @param string $orderby Sort retrieved products by parameter. Default 'date'. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'. * @return Product[]|WP_Error */ public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) { From 0da480fb226f39623b43684f48b1d35dd3ccde11 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 16:50:02 +0100 Subject: [PATCH 25/42] Add pagination support. --- includes/Interfaces/Product_Query.php | 3 +- includes/REST_API/Products_Controller.php | 11 ++- includes/Shopping/Shopify_Query.php | 88 +++++++++++++++---- includes/Shopping/WooCommerce_Query.php | 8 +- .../includes/Shopping/Mock_Vendor.php | 3 +- .../includes/Shopping/Mock_Vendor_Error.php | 3 +- .../includes/Shopping/Mock_Vendor_Invalid.php | 3 +- .../tests/Shopping/Shopify_Query.php | 15 ++-- 8 files changed, 103 insertions(+), 31 deletions(-) diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index 2b76f663008a..5c6d04edcb9d 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -39,10 +39,11 @@ interface Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page, string $orderby, string $order); + public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order); } diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index ddb550ef623b..9cfd72b212aa 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -140,6 +140,8 @@ public function get_items_permissions_check( $request ) { /** * Retrieves all products. * + * @SuppressWarnings(PHPMD.NPathComplexity) + * * @since 1.20.0 * * @param WP_REST_Request $request Full details about the request. @@ -172,6 +174,13 @@ public function get_items( $request ) { */ $orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : 'date'; + /** + * Request context. + * + * @var int $page + */ + $page = ! empty( $request['page'] ) ? $request['page'] : 1; + /** * Request context. * @@ -187,7 +196,7 @@ public function get_items( $request ) { $order = ! empty( $request['order'] ) ? $request['order'] : 'desc'; - $query_result = $query->get_search( $search_term, $per_page, $orderby, $order ); + $query_result = $query->get_search( $search_term, $page, $per_page, $orderby, $order ); if ( is_wp_error( $query_result ) ) { return $query_result; } diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 93c290d73102..e0a528c42f6f 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -38,10 +38,10 @@ * @phpstan-type ShopifyGraphQLPriceRange array{minVariantPrice: array{amount: int, currencyCode: string}} * @phpstan-type ShopifyGraphQLProductImage array{url: string, altText: string} * @phpstan-type ShopifyGraphQLProduct array{id: string, handle: string, title: string, vendor: string, description: string, onlineStoreUrl?: string, images: array{edges: array{node: ShopifyGraphQLProductImage}[]}, priceRange: ShopifyGraphQLPriceRange} - * @phpstan-type ShopifyGraphQLResponse array{errors?: ShopifyGraphQLError, data: array{products: array{edges: array{node: ShopifyGraphQLProduct}[]}}} + * @phpstan-type ShopifyGraphQLResponse array{errors?: ShopifyGraphQLError, data: array{products: array{edges: array{node: ShopifyGraphQLProduct}[], pageInfo: array{hasNextPage: bool, endCursor: string}}}} */ class Shopify_Query implements Product_Query { - protected const API_VERSION = '2022-01'; + protected const API_VERSION = '2022-04'; /** * Settings instance. @@ -151,18 +151,22 @@ protected function execute_query( string $query ) { * @since 1.21.0 * * @param string $search_term Search term to filter products by. + * @param string $after After node. * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return string The assembled GraphQL query. */ - protected function get_products_query( string $search_term, int $per_page, string $orderby, string $order ): string { + protected function get_products_query( string $search_term, string $after, int $per_page, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; $sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); $reverse = 'asc' === $order ? 'false' : 'true'; + $after = empty( $after ) ? '' : $after; + $after_query = empty( $after ) ? '' : " after: \"$after\","; + return <<get_products_query( $search_term, $per_page, $orderby, $order ); + $after = ''; + if ( $page > 1 ) { + for ( $i = 1; $i < $page; $i ++ ) { + $query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order ); + $body = $this->execute_query( $query ); + if ( is_wp_error( $body ) ) { + return $body; + } + $validate = $this->validate_request( $body ); + if ( is_wp_error( $validate ) ) { + return $validate; + } + /** + * Shopify GraphQL API response. + * + * @var ShopifyGraphQLResponse $result + */ + $result = json_decode( $body, true ); + $has_next_page = $result['data']['products']['pageInfo']['hasNextPage']; + if ( ! $has_next_page ) { + return new WP_Error( 'rest_no_page', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); + } + $after = (string) $result['data']['products']['pageInfo']['endCursor']; + } + } - $body = $this->execute_query( $query ); + $query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order ); + $body = $this->execute_query( $query ); if ( is_wp_error( $body ) ) { return $body; } + $validate = $this->validate_request( $body ); + if ( is_wp_error( $validate ) ) { + return $validate; + } + + // TODO: Maybe cache errors too? + set_transient( $cache_key, $body, $cache_ttl ); + /** * Shopify GraphQL API response. * @@ -239,31 +281,45 @@ protected function fetch_remote_products( string $search_term, int $per_page, st */ $result = json_decode( $body, true ); + return $result; + } + + + /** + * Handle validation. + * + * @since 1.21.0 + * + * @param string $body Body of request. + * @return void|WP_Error + */ + protected function validate_request( $body ) { + /** + * Shopify GraphQL API response. + * + * @var ShopifyGraphQLResponse $result + */ + $result = json_decode( $body, true ); // TODO(#11268): Error handling. if ( isset( $result['errors'] ) ) { return new WP_Error( 'rest_unknown', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); } - - // TODO: Maybe cache errors too? - set_transient( $cache_key, $body, $cache_ttl ); - - return $result; } - /** * Get products by search term. * * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query. * @param string $orderby sort field for query. * @param string $order ASC or DESC. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { - $result = $this->fetch_remote_products( $search_term, $per_page, $orderby, $order ); + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { + $result = $this->fetch_remote_products( $search_term, $page, $per_page, $orderby, $order ); if ( is_wp_error( $result ) ) { return $result; diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index d40857e8814e..b7985be1d72f 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -57,12 +57,13 @@ public function __construct( WooCommerce $woocommerce ) { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $status = $this->woocommerce->get_plugin_status(); if ( ! $status['active'] ) { @@ -77,7 +78,7 @@ public function get_search( string $search_term, int $per_page = 100, string $or $wc_query = new WC_Query(); $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); } - + /** * Products. * @@ -87,11 +88,12 @@ public function get_search( string $search_term, int $per_page = 100, string $or array_merge( [ 'status' => 'publish', + 'page' => $page, 'limit' => $per_page, 's' => $search_term, 'orderby' => $orderby, 'order' => $order, - ], + ], $wc_args ) ); diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index 6c4a5d239159..5e6bb7d35f02 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -32,12 +32,13 @@ class Mock_Vendor implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { $products = []; for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index 2af6ca2c406d..e2edc1e284e6 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -32,12 +32,13 @@ class Mock_Vendor_Error implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index eb8ba06c675d..2eb3f798ea52 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -31,12 +31,13 @@ class Mock_Vendor_Invalid { * @since 1.21.0 * * @param string $search_term Search term. + * @param int $page Page Number. * @param int $per_page Limit query * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return Product[]|WP_Error */ - public function get_search( string $search_term, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { $products = []; for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 95edfcfac0c8..5397d9057ed8 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,15 +184,16 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; + $page = 1; $per_page = 100; $orderby = 'date'; $order = 'desc'; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $page . '-' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); - $actual = $this->instance->get_search( '', $per_page, $orderby, $order ); + $actual = $this->instance->get_search( '', $page, $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); $this->assertSame( [], $actual ); @@ -298,23 +299,23 @@ public function test_get_search_empty_search_response(): void { public function data_test_get_search_sort_by_query(): array { return [ 'Default search' => [ - [ 'some search term', 100, 'date', '' ], + [ 'some search term', 1, 100, 'date', '' ], [ 'sortKey: CREATED_AT', 'reverse: true' ], ], 'Sort title asc' => [ - [ '', 100, 'title', 'asc' ], + [ '', 1, 100, 'title', 'asc' ], [ 'sortKey: TITLE', 'reverse: false' ], ], 'Sort title desc' => [ - [ '', 100, 'title', 'desc' ], + [ '', 1, 100, 'title', 'desc' ], [ 'sortKey: TITLE', 'reverse: true' ], ], 'Sort price asc' => [ - [ '', 100, 'price', 'asc' ], + [ '', 1, 100, 'price', 'asc' ], [ 'sortKey: PRICE', 'reverse: false' ], ], 'Sort price desc' => [ - [ '', 100, 'price', 'desc' ], + [ '', 1, 100, 'price', 'desc' ], [ 'sortKey: PRICE', 'reverse: true' ], ], ]; From 9ae4a03370be4cc37ba7907aed520716eb3ef343 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 18:12:43 +0100 Subject: [PATCH 26/42] Refactor with caching. --- includes/Shopping/Shopify_Query.php | 90 ++++++++++++----------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 7a2641c5a66d..95fe6eb98e45 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -208,15 +208,14 @@ protected function get_products_query( string $search_term, string $after, int $ * @since 1.21.0 * * @param string $search_term Search term to filter products by. - * @param int $page Page. + * @param string $after Pointer. * @param int $per_page Limit query. * @param string $orderby Sort retrieved products by parameter. * @param string $order Whether to order products in ascending or descending order. * Accepts 'asc' (ascending) or 'desc' (descending). * @return array|WP_Error Response data or error object on failure. */ - protected function fetch_remote_products( string $search_term, int $page, int $per_page, string $orderby, string $order ) { - + protected function get_remote_products( string $search_term, string $after, int $per_page, string $orderby, string $order ) { /** * Filters the Shopify products data TTL value. * @@ -225,7 +224,7 @@ protected function fetch_remote_products( string $search_term, int $page, int $p * @param int $time Time to live (in seconds). Default is 5 minutes. */ $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); - $cache_key = 'web_stories_shopify_data_' . md5( $search_term . '-' . $page . '-' . $per_page . '-' . $orderby . '-' . $order ); + $cache_key = 'web_stories_shopify_data_' . md5( $search_term . '-' . $after . '-' . $per_page . '-' . $orderby . '-' . $order ); $data = get_transient( $cache_key ); @@ -233,77 +232,62 @@ protected function fetch_remote_products( string $search_term, int $page, int $p return (array) json_decode( $data, true ); } - $after = ''; - if ( $page > 1 ) { - for ( $i = 1; $i < $page; $i ++ ) { - $query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order ); - $body = $this->execute_query( $query ); - if ( is_wp_error( $body ) ) { - return $body; - } - $validate = $this->validate_request( $body ); - if ( is_wp_error( $validate ) ) { - return $validate; - } - /** - * Shopify GraphQL API response. - * - * @var ShopifyGraphQLResponse $result - */ - $result = json_decode( $body, true ); - $has_next_page = $result['data']['products']['pageInfo']['hasNextPage']; - if ( ! $has_next_page ) { - return new WP_Error( 'rest_no_page', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); - } - $after = (string) $result['data']['products']['pageInfo']['endCursor']; - } - } - $query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order ); $body = $this->execute_query( $query ); - if ( is_wp_error( $body ) ) { return $body; } - $validate = $this->validate_request( $body ); - if ( is_wp_error( $validate ) ) { - return $validate; - } - - // TODO: Maybe cache errors too? - set_transient( $cache_key, $body, $cache_ttl ); - /** * Shopify GraphQL API response. * * @var ShopifyGraphQLResponse $result */ $result = json_decode( $body, true ); + // TODO(#11268): Error handling. + if ( isset( $result['errors'] ) ) { + return new WP_Error( 'rest_unknown', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); + } + + // TODO: Maybe cache errors too? + set_transient( $cache_key, $body, $cache_ttl ); return $result; } - /** - * Handle validation. + * Remotely fetches all products from the store. * * @since 1.21.0 * - * @param string $body Body of request. - * @return void|WP_Error + * @param string $search_term Search term to filter products by. + * @param int $page Page. + * @param int $per_page Limit query. + * @param string $orderby Sort retrieved products by parameter. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). + * @return array|WP_Error Response data or error object on failure. */ - protected function validate_request( $body ) { - /** - * Shopify GraphQL API response. - * - * @var ShopifyGraphQLResponse $result - */ - $result = json_decode( $body, true ); - // TODO(#11268): Error handling. - if ( isset( $result['errors'] ) ) { - return new WP_Error( 'rest_unknown', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); + protected function fetch_remote_products( string $search_term, int $page, int $per_page, string $orderby, string $order ) { + $after = ''; + if ( $page > 1 ) { + for ( $i = 1; $i < $page; $i ++ ) { + $result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); + if ( is_wp_error( $result ) ) { + return $result; + } + + $has_next_page = $result['data']['products']['pageInfo']['hasNextPage']; + if ( ! $has_next_page ) { + return new WP_Error( 'rest_no_page', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); + } + $after = (string) $result['data']['products']['pageInfo']['endCursor']; + } } + + $result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); + + return $result; } /** From 578f7bcd54fb4fb42fc9eed683cc4e0b2c9592fa Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 18:23:28 +0100 Subject: [PATCH 27/42] Fix tests. --- tests/phpunit/integration/tests/Shopping/Shopify_Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 5397d9057ed8..5dec2f2080f6 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -191,7 +191,7 @@ public function test_fetch_remote_products_returns_from_transient(): void { update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term . '-' . $page . '-' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( 'web_stories_shopify_data_' . md5( $search_term . '--' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); $actual = $this->instance->get_search( '', $page, $per_page, $orderby, $order ); From e5520b66ac013cffdc128bdf4950b1df55808391 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 18:40:05 +0100 Subject: [PATCH 28/42] Improve cache key generation. --- includes/Shopping/Shopify_Query.php | 5 +++-- .../phpunit/integration/tests/Shopping/Shopify_Query.php | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 95fe6eb98e45..30028ef8afb7 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -223,8 +223,9 @@ protected function get_remote_products( string $search_term, string $after, int * * @param int $time Time to live (in seconds). Default is 5 minutes. */ - $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); - $cache_key = 'web_stories_shopify_data_' . md5( $search_term . '-' . $after . '-' . $per_page . '-' . $orderby . '-' . $order ); + $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); + $cache_args = compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ); + $cache_key = 'web_stories_shopify_data_' . md5( wp_json_encode( $cache_args ) ); $data = get_transient( $cache_key ); diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 5dec2f2080f6..3752c3a1940d 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -184,16 +184,19 @@ public function test_fetch_remote_products_invalid_hostname(): void { */ public function test_fetch_remote_products_returns_from_transient(): void { $search_term = ''; - $page = 1; + $after = ''; $per_page = 100; $orderby = 'date'; $order = 'desc'; update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - set_transient( 'web_stories_shopify_data_' . md5( $search_term . '--' . $per_page . '-' . $orderby . '-' . $order ), wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); - $actual = $this->instance->get_search( '', $page, $per_page, $orderby, $order ); + $cache_args = compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ); + $cache_key = 'web_stories_shopify_data_' . md5( wp_json_encode( $cache_args ) ); + set_transient( $cache_key, wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + + $actual = $this->instance->get_search( '', 1, $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); $this->assertSame( [], $actual ); From cf57cb823643aad2e1a991e0fdf6f8536f3a4abe Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 18:50:47 +0100 Subject: [PATCH 29/42] Fix lint --- includes/Shopping/Shopify_Query.php | 4 ++-- tests/phpunit/integration/tests/Shopping/Shopify_Query.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 30028ef8afb7..90569827cddb 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -224,8 +224,8 @@ protected function get_remote_products( string $search_term, string $after, int * @param int $time Time to live (in seconds). Default is 5 minutes. */ $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); - $cache_args = compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ); - $cache_key = 'web_stories_shopify_data_' . md5( wp_json_encode( $cache_args ) ); + $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); + $cache_key = 'web_stories_shopify_data_' . md5( $cache_args ); $data = get_transient( $cache_key ); diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 3752c3a1940d..939a997096b6 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -192,8 +192,8 @@ public function test_fetch_remote_products_returns_from_transient(): void { update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - $cache_args = compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ); - $cache_key = 'web_stories_shopify_data_' . md5( wp_json_encode( $cache_args ) ); + $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); + $cache_key = 'web_stories_shopify_data_' . md5( $cache_args ); set_transient( $cache_key, wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); $actual = $this->instance->get_search( '', 1, $per_page, $orderby, $order ); From 8e999799bd30c107349764a38263d3ad19b86f8c Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 30 May 2022 19:46:48 +0100 Subject: [PATCH 30/42] Add header. --- includes/Interfaces/Product_Query.php | 3 +- includes/REST_API/Products_Controller.php | 14 +++++-- includes/Shopping/Shopify_Query.php | 10 +++-- includes/Shopping/WooCommerce_Query.php | 41 +++++++++++-------- .../includes/Shopping/Mock_Vendor.php | 4 +- .../includes/Shopping/Mock_Vendor_Invalid.php | 3 +- .../tests/Shopping/Shopify_Query.php | 34 +++++++++++---- .../unit/tests/Shopping/WooCommerce_Query.php | 15 ++++--- 8 files changed, 83 insertions(+), 41 deletions(-) diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index 5c6d04edcb9d..b038ff0e231d 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -26,7 +26,6 @@ namespace Google\Web_Stories\Interfaces; -use Google\Web_Stories\Shopping\Product; use WP_Error; /** @@ -43,7 +42,7 @@ interface Product_Query { * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. - * @return Product[]|WP_Error + * @return array|WP_Error */ public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order); } diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 9cfd72b212aa..e0246ec8145c 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -195,19 +195,27 @@ public function get_items( $request ) { */ $order = ! empty( $request['order'] ) ? $request['order'] : 'desc'; - $query_result = $query->get_search( $search_term, $page, $per_page, $orderby, $order ); if ( is_wp_error( $query_result ) ) { return $query_result; } $products = []; - foreach ( $query_result as $product ) { + foreach ( $query_result['products'] as $product ) { $data = $this->prepare_item_for_response( $product, $request ); $products[] = $this->prepare_response_for_collection( $data ); } - return rest_ensure_response( $products ); + /** + * Response object. + * + * @var WP_REST_Response $response + */ + $response = rest_ensure_response( $products ); + + $response->header( 'X-WP-HAS-NEXT-PAGE', (string) $query_result['has_next_page'] ); + + return $response; } /** diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 90569827cddb..59861dc117a7 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -302,7 +302,7 @@ protected function fetch_remote_products( string $search_term, int $page, int $p * @param string $orderby Sort retrieved products by parameter. Default 'date'. * @param string $order Whether to order products in ascending or descending order. * Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'. - * @return Product[]|WP_Error + * @return array|WP_Error */ public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $result = $this->fetch_remote_products( $search_term, $page, $per_page, $orderby, $order ); @@ -311,7 +311,9 @@ public function get_search( string $search_term, int $page = 1, int $per_page = return $result; } - $results = []; + $products = []; + + $has_next_page = $result['data']['products']['pageInfo']['hasNextPage'] ?? false; foreach ( $result['data']['products']['edges'] as $edge ) { $product = $edge['node']; @@ -331,7 +333,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = // In this case, we can fall back to a manually constructed product URL. $product_url = $product['onlineStoreUrl'] ?? sprintf( 'https://%1$s/products/%2$s/', $this->get_host(), $product['handle'] ); - $results[] = new Product( + $products[] = new Product( [ 'id' => $product['id'], 'title' => $product['title'], @@ -349,6 +351,6 @@ public function get_search( string $search_term, int $page = 1, int $per_page = ); } - return $results; + return compact( 'products', 'has_next_page' ); } } diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index b7985be1d72f..e379ce993531 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -61,7 +61,7 @@ public function __construct( WooCommerce $woocommerce ) { * @param int $per_page Limit query. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. - * @return Product[]|WP_Error + * @return array|WP_Error */ public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $status = $this->woocommerce->get_plugin_status(); @@ -70,7 +70,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = return new WP_Error( 'rest_woocommerce_not_installed', __( 'WooCommerce is not installed.', 'web-stories' ), [ 'status' => 400 ] ); } - $results = []; + $products = []; $wc_args = []; @@ -79,27 +79,36 @@ public function get_search( string $search_term, int $page = 1, int $per_page = $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); } + /** - * Products. - * - * @var \WC_Product[] $products + * @var \stdClass $product_query */ - $products = wc_get_products( + $product_query = wc_get_products( array_merge( [ - 'status' => 'publish', - 'page' => $page, - 'limit' => $per_page, - 's' => $search_term, - 'orderby' => $orderby, - 'order' => $order, + 'status' => 'publish', + 'page' => $page, + 'limit' => $per_page, + 's' => $search_term, + 'orderby' => $orderby, + 'order' => $order, + 'paginate' => true, ], $wc_args ) ); + $has_next_page = ( $product_query->max_num_pages > $page ); + + /** + * Products. + * + * @var \WC_Product[] $wc_products + */ + $wc_products = $product_query->products; + $product_image_ids = []; - foreach ( $products as $product ) { + foreach ( $wc_products as $product ) { $product_image_ids[] = $this->get_product_image_ids( $product ); } $products_image_ids = array_merge( [], ...$product_image_ids ); @@ -110,7 +119,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = */ _prime_post_caches( $products_image_ids, false, true ); - foreach ( $products as $product ) { + foreach ( $wc_products as $product ) { $images = array_map( [ $this, 'get_product_image' ], @@ -136,10 +145,10 @@ public function get_search( string $search_term, int $page = 1, int $per_page = ] ); - $results[] = $product_object; + $products[] = $product_object; } - return $results; + return compact( 'products', 'has_next_page' ); } /** diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index 5e6bb7d35f02..dee3bd436fbc 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -54,7 +54,7 @@ public function get_search( string $search_term, int $page, int $per_page, strin ] ); } - - return $products; + $has_next_page = 1; + return compact( 'products', 'has_next_page' ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index 2eb3f798ea52..b529a47d725c 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -54,6 +54,7 @@ public function get_search( string $search_term, int $page, int $per_page, strin ); } - return $products; + $has_next_page = 1; + return compact( 'products', 'has_next_page' ); } } diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 939a997096b6..fd16d6581bde 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -194,12 +194,30 @@ public function test_fetch_remote_products_returns_from_transient(): void { $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); $cache_key = 'web_stories_shopify_data_' . md5( $cache_args ); - set_transient( $cache_key, wp_json_encode( [ 'data' => [ 'products' => [ 'edges' => [] ] ] ] ) ); + set_transient( + $cache_key, + wp_json_encode( + [ + 'data' => [ + 'products' => [ + 'edges' => [], + 'pageInfo' => [ 'hasNextPage' => false ], + ], + ], + ] + ) + ); $actual = $this->instance->get_search( '', 1, $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); - $this->assertSame( [], $actual ); + $this->assertSameSets( + [ + 'products' => [], + 'has_next_page' => false, + ], + $actual + ); $this->assertSame( 0, $this->request_count ); } @@ -220,12 +238,13 @@ public function test_get_search_default_response(): void { remove_filter( 'pre_http_request', [ $this, 'mock_response_default' ] ); $this->assertNotWPError( $actual ); - $this->assertNotEmpty( $actual ); - $this->assertCount( 3, $actual ); + $this->assertArrayHasKey( 'products', $actual ); + $this->assertNotEmpty( $actual['products'] ); + $this->assertCount( 3, $actual['products'] ); $this->assertSame( 1, $this->request_count ); $this->assertStringContainsString( 'query: "title:*"', $this->request_body ); - foreach ( $actual as $product ) { + foreach ( $actual['products'] as $product ) { $this->assertInstanceOf( Product::class, $product ); $this->assertMatchesProductSchema( json_decode( wp_json_encode( $product ), true ) ); @@ -290,8 +309,9 @@ public function test_get_search_empty_search_response(): void { remove_filter( 'pre_http_request', [ $this, 'mock_response_no_results' ] ); $this->assertNotWPError( $actual ); - $this->assertEmpty( $actual ); - $this->assertCount( 0, $actual ); + $this->assertArrayHasKey( 'products', $actual ); + $this->assertEmpty( $actual['products'] ); + $this->assertCount( 0, $actual['products'] ); $this->assertSame( 1, $this->request_count ); $this->assertStringContainsString( 'query: "title:*some search term*"', $this->request_body ); } diff --git a/tests/phpunit/unit/tests/Shopping/WooCommerce_Query.php b/tests/phpunit/unit/tests/Shopping/WooCommerce_Query.php index ba3dff39f466..acb9cab21c9f 100644 --- a/tests/phpunit/unit/tests/Shopping/WooCommerce_Query.php +++ b/tests/phpunit/unit/tests/Shopping/WooCommerce_Query.php @@ -67,7 +67,8 @@ public function test_products_image(): void { Monkey\Functions\stubs( [ 'wc_get_products' => static function () { - return [ + $object = new \stdClass(); + $products = [ new Mock_Product( [ 'id' => '1', @@ -101,6 +102,9 @@ public function test_products_image(): void { ] ), ]; + $object->products = $products; + $object->max_num_pages = 1; + return $object; }, 'wp_get_attachment_image_url' => static function ( $id ) { if ( ! $id ) { @@ -114,11 +118,10 @@ public function test_products_image(): void { ); $results = $this->instance->get_search( 'hoodie' ); - - $this->assertEquals( 'http://example.com/50', $results[0]->get_images()[0]['url'] ); - $this->assertEquals( 'http://example.com/60', $results[0]->get_images()[3]['url'] ); - $this->assertEquals( 'http://example.com/72', $results[2]->get_images()[1]['url'] ); - $this->assertEquals( 0, \count( $results[1]->get_images() ) ); + $this->assertEquals( 'http://example.com/50', $results['products'][0]->get_images()[0]['url'] ); + $this->assertEquals( 'http://example.com/60', $results['products'][0]->get_images()[3]['url'] ); + $this->assertEquals( 'http://example.com/72', $results['products'][2]->get_images()[1]['url'] ); + $this->assertEquals( 0, \count( $results['products'][1]->get_images() ) ); } /** From 3784e532ec24f9524f519218e7ac97babd7beae6 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 31 May 2022 10:08:10 +0100 Subject: [PATCH 31/42] Feedback. --- includes/REST_API/Products_Controller.php | 2 +- tests/phpunit/integration/includes/Shopping/Mock_Vendor.php | 2 +- .../integration/includes/Shopping/Mock_Vendor_Invalid.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index e0246ec8145c..bb2d971eaab4 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -213,7 +213,7 @@ public function get_items( $request ) { */ $response = rest_ensure_response( $products ); - $response->header( 'X-WP-HAS-NEXT-PAGE', (string) $query_result['has_next_page'] ); + $response->header( 'X-WP-HasNextPage', (string) $query_result['has_next_page'] ); return $response; } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index dee3bd436fbc..7c09de5f209f 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -54,7 +54,7 @@ public function get_search( string $search_term, int $page, int $per_page, strin ] ); } - $has_next_page = 1; + $has_next_page = false; return compact( 'products', 'has_next_page' ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index b529a47d725c..381e66a58a2d 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -54,7 +54,7 @@ public function get_search( string $search_term, int $page, int $per_page, strin ); } - $has_next_page = 1; + $has_next_page = false; return compact( 'products', 'has_next_page' ); } } From efac296db89fe9f2c1c3fba19393e8affc2d7234 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 31 May 2022 10:47:10 +0100 Subject: [PATCH 32/42] Add comment. --- includes/Shopping/WooCommerce_Query.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index e379ce993531..50be1468ff81 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -81,6 +81,8 @@ public function get_search( string $search_term, int $page = 1, int $per_page = /** + * Product query object. + * * @var \stdClass $product_query */ $product_query = wc_get_products( From 1f5bfe91898ce769aeb0af9c940b76a820d670de Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 1 Jun 2022 10:22:06 +0100 Subject: [PATCH 33/42] Clearer logic for after. --- includes/Shopping/Shopify_Query.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 59861dc117a7..88477086d0f6 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -161,12 +161,11 @@ protected function get_products_query( string $search_term, string $after, int $ $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; $sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby ); $reverse = 'asc' === $order ? 'false' : 'true'; - $after = empty( $after ) ? '' : $after; - $after_query = empty( $after ) ? '' : " after: \"$after\","; + $after = empty( $after ) ? 'null' : sprintf( '"%s"', $after ); return << Date: Wed, 1 Jun 2022 10:48:52 +0100 Subject: [PATCH 34/42] Improve comments and error message. --- includes/Interfaces/Product_Query.php | 8 ++--- includes/Shopping/Shopify_Query.php | 48 ++++++++++++------------- includes/Shopping/WooCommerce_Query.php | 10 +++--- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index b038ff0e231d..4028a60441c7 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -38,10 +38,10 @@ interface Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query. - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to fetched. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return array|WP_Error */ public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order); diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 88477086d0f6..a1929012a457 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -150,12 +150,12 @@ protected function execute_query( string $query ) { * * @since 1.21.0 * - * @param string $search_term Search term to filter products by. - * @param string $after After node. - * @param int $per_page Limit query. - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. - * @return string The assembled GraphQL query. + * @param string $search_term Search term to filter products by. + * @param string $after The cursor to retrieve nodes after in the connection. + * @param int $per_page The requested number of nodes per page. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. + * @return string The assembled GraphQL query. */ protected function get_products_query( string $search_term, string $after, int $per_page, string $orderby, string $order ): string { $search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*'; @@ -206,13 +206,13 @@ protected function get_products_query( string $search_term, string $after, int $ * * @since 1.21.0 * - * @param string $search_term Search term to filter products by. - * @param string $after Pointer. - * @param int $per_page Limit query. - * @param string $orderby Sort retrieved products by parameter. - * @param string $order Whether to order products in ascending or descending order. - * Accepts 'asc' (ascending) or 'desc' (descending). - * @return array|WP_Error Response data or error object on failure. + * @param string $search_term Search term to filter products by. + * @param string $after The cursor to retrieve nodes after in the connection. + * @param int $per_page The requested number of nodes per page. + * @param string $orderby Sort retrieved products by parameter. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). + * @return array|WP_Error Response data or error object on failure. */ protected function get_remote_products( string $search_term, string $after, int $per_page, string $orderby, string $order ) { /** @@ -261,11 +261,11 @@ protected function get_remote_products( string $search_term, string $after, int * @since 1.21.0 * * @param string $search_term Search term to filter products by. - * @param int $page Page. - * @param int $per_page Limit query. - * @param string $orderby Sort retrieved products by parameter. - * @param string $order Whether to order products in ascending or descending order. - * Accepts 'asc' (ascending) or 'desc' (descending). + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to fetched. + * @param string $orderby Sort retrieved products by parameter. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). * @return array|WP_Error Response data or error object on failure. */ protected function fetch_remote_products( string $search_term, int $page, int $per_page, string $orderby, string $order ) { @@ -279,7 +279,7 @@ protected function fetch_remote_products( string $search_term, int $page, int $p $has_next_page = $result['data']['products']['pageInfo']['hasNextPage']; if ( ! $has_next_page ) { - return new WP_Error( 'rest_no_page', __( 'Error fetching products', 'web-stories' ), [ 'status' => 404 ] ); + return new WP_Error( 'rest_no_page', __( 'Error fetching products from Shopify.', 'web-stories' ), [ 'status' => 404 ] ); } $after = (string) $result['data']['products']['pageInfo']['endCursor']; } @@ -296,11 +296,11 @@ protected function fetch_remote_products( string $search_term, int $page, int $p * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query. - * @param string $orderby Sort retrieved products by parameter. Default 'date'. - * @param string $order Whether to order products in ascending or descending order. - * Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'. + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to fetched. + * @param string $orderby Sort retrieved products by parameter. Default 'date'. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'. * @return array|WP_Error */ public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index 50be1468ff81..820ee3dace34 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -57,10 +57,10 @@ public function __construct( WooCommerce $woocommerce ) { * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query. - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to fetched. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. * @return array|WP_Error */ public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { @@ -81,7 +81,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = /** - * Product query object. + * Product query object. * * @var \stdClass $product_query */ From a86f76e7d7447117bd0647f0ebc757ec277ae466 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 1 Jun 2022 13:25:43 +0100 Subject: [PATCH 35/42] Improve comments again. --- includes/Interfaces/Product_Query.php | 4 ++-- includes/Shopping/Shopify_Query.php | 9 ++++----- includes/Shopping/WooCommerce_Query.php | 4 ++-- .../integration/includes/Shopping/Mock_Vendor.php | 12 ++++++------ .../includes/Shopping/Mock_Vendor_Error.php | 12 ++++++------ .../includes/Shopping/Mock_Vendor_Invalid.php | 12 ++++++------ 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/includes/Interfaces/Product_Query.php b/includes/Interfaces/Product_Query.php index 4028a60441c7..d4ffa8e4a0b0 100644 --- a/includes/Interfaces/Product_Query.php +++ b/includes/Interfaces/Product_Query.php @@ -39,10 +39,10 @@ interface Product_Query { * * @param string $search_term Search term. * @param int $page Number of page for paginated requests. - * @param int $per_page Number of products to fetched. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return array|WP_Error */ - public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order); + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ); } diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 8fdf2cfb46fd..f0d31583cd6d 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -152,7 +152,7 @@ protected function execute_query( string $query ) { * * @param string $search_term Search term to filter products by. * @param string $after The cursor to retrieve nodes after in the connection. - * @param int $per_page The requested number of nodes per page. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return string The assembled GraphQL query. @@ -208,7 +208,7 @@ protected function get_products_query( string $search_term, string $after, int $ * * @param string $search_term Search term to filter products by. * @param string $after The cursor to retrieve nodes after in the connection. - * @param int $per_page The requested number of nodes per page. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort retrieved products by parameter. * @param string $order Whether to order products in ascending or descending order. * Accepts 'asc' (ascending) or 'desc' (descending). @@ -255,7 +255,6 @@ protected function get_remote_products( string $search_term, string $after, int return $result; } - /** * Remotely fetches all products from the store. * @@ -263,7 +262,7 @@ protected function get_remote_products( string $search_term, string $after, int * * @param string $search_term Search term to filter products by. * @param int $page Number of page for paginated requests. - * @param int $per_page Number of products to fetched. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort retrieved products by parameter. * @param string $order Whether to order products in ascending or descending order. * Accepts 'asc' (ascending) or 'desc' (descending). @@ -298,7 +297,7 @@ protected function fetch_remote_products( string $search_term, int $page, int $p * * @param string $search_term Search term. * @param int $page Number of page for paginated requests. - * @param int $per_page Number of products to fetched. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort retrieved products by parameter. Default 'date'. * @param string $order Whether to order products in ascending or descending order. * Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'. diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index ddc1048ecb6d..f9d5163e2c89 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -58,7 +58,7 @@ public function __construct( WooCommerce $woocommerce ) { * * @param string $search_term Search term. * @param int $page Number of page for paginated requests. - * @param int $per_page Number of products to fetched. + * @param int $per_page Number of products to be fetched. * @param string $orderby Sort collection by product attribute. * @param string $order Order sort attribute ascending or descending. * @return array|WP_Error @@ -86,7 +86,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = $wc_query = new WC_Query(); $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); } - + /** * Product query object. * diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php index 7c09de5f209f..bb76f464d53b 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor.php @@ -32,13 +32,13 @@ class Mock_Vendor implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. - * @return Product[]|WP_Error + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to be fetched. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. + * @return array|WP_Error */ - public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $products = []; for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index e2edc1e284e6..e70462eb828b 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -32,13 +32,13 @@ class Mock_Vendor_Error implements Product_Query { * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. - * @return Product[]|WP_Error + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to be fetched. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. + * @return array|WP_Error */ - public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { return new WP_Error( 'mock_error', 'Mock error', [ 'status' => 400 ] ); } } diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php index 381e66a58a2d..a95d700172ca 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Invalid.php @@ -31,13 +31,13 @@ class Mock_Vendor_Invalid { * @since 1.21.0 * * @param string $search_term Search term. - * @param int $page Page Number. - * @param int $per_page Limit query - * @param string $orderby Sort collection by product attribute. - * @param string $order Order sort attribute ascending or descending. - * @return Product[]|WP_Error + * @param int $page Number of page for paginated requests. + * @param int $per_page Number of products to be fetched. + * @param string $orderby Sort collection by product attribute. + * @param string $order Order sort attribute ascending or descending. + * @return array|WP_Error */ - public function get_search( string $search_term, int $page, int $per_page, string $orderby, string $order ) { + public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) { $products = []; for ( $x = 0; $x < $per_page; $x ++ ) { $products[] = new Product( From b76f8d105b98bc1695eaaf9cd81474650768a61a Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 1 Jun 2022 13:31:11 +0100 Subject: [PATCH 36/42] Fix lint. --- includes/REST_API/Products_Controller.php | 2 +- .../phpunit/integration/includes/Shopping/Mock_Vendor_Error.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/REST_API/Products_Controller.php b/includes/REST_API/Products_Controller.php index 3602a17cd45f..21d2951c40cd 100644 --- a/includes/REST_API/Products_Controller.php +++ b/includes/REST_API/Products_Controller.php @@ -166,7 +166,7 @@ public function get_items( $request ) { * @var string $search_term */ $search_term = ! empty( $request['search'] ) ? $request['search'] : ''; - + /** * Request context. * diff --git a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php index e70462eb828b..9d0b1200e806 100644 --- a/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php +++ b/tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php @@ -18,7 +18,6 @@ namespace Google\Web_Stories\Tests\Integration\Shopping; use Google\Web_Stories\Interfaces\Product_Query; -use Google\Web_Stories\Shopping\Product; use WP_Error; /** From 7d1571e1e966bef06d4b0128dd02a129d80a0b49 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 11:26:08 +0100 Subject: [PATCH 37/42] Fix lint. --- includes/Shopping/WooCommerce_Query.php | 40 ++++++++----------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/includes/Shopping/WooCommerce_Query.php b/includes/Shopping/WooCommerce_Query.php index 15bd9af2c7b4..6ff156cac03b 100644 --- a/includes/Shopping/WooCommerce_Query.php +++ b/includes/Shopping/WooCommerce_Query.php @@ -69,26 +69,24 @@ public function get_search( string $search_term, int $page = 1, int $per_page = if ( ! $status['installed'] ) { return new WP_Error( 'rest_woocommerce_not_installed', __( 'WooCommerce is not installed.', 'web-stories' ), [ 'status' => 400 ] ); } - + if ( ! $status['active'] ) { return new WP_Error( 'rest_woocommerce_not_activated', __( 'WooCommerce is not activated. Please activate it again try again.', 'web-stories' ), [ 'status' => 400 ] ); } - - $products = []; - - $wc_args = []; - - if ( 'price' === $orderby ) { - $wc_query = new WC_Query(); - $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); - } - - - $wc_args = []; + $args = [ + 'status' => 'publish', + 'page' => $page, + 'limit' => $per_page, + 's' => $search_term, + 'orderby' => $orderby, + 'order' => $order, + 'paginate' => true, + ]; if ( 'price' === $orderby ) { $wc_query = new WC_Query(); $wc_args = $wc_query->get_catalog_ordering_args( $orderby, strtoupper( $order ) ); + $args = array_merge( $args, $wc_args ); } /** @@ -96,20 +94,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = * * @var \stdClass $product_query */ - $product_query = wc_get_products( - array_merge( - [ - 'status' => 'publish', - 'page' => $page, - 'limit' => $per_page, - 's' => $search_term, - 'orderby' => $orderby, - 'order' => $order, - 'paginate' => true, - ], - $wc_args - ) - ); + $product_query = wc_get_products( $args ); $has_next_page = ( $product_query->max_num_pages > $page ); @@ -132,6 +117,7 @@ public function get_search( string $search_term, int $page = 1, int $per_page = */ _prime_post_caches( $products_image_ids, false, true ); + $products = []; foreach ( $wc_products as $product ) { $images = array_map( From 670575d01dc2c64cb24a8f610bc628eb3b34c72b Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 11:36:35 +0100 Subject: [PATCH 38/42] Fix lint. --- includes/Shopping/Shopify_Query.php | 4 +- .../reducers/setSelectedElements.js | 2 +- .../useStoryReducer/reducers/toggleElement.js | 2 +- .../elements/karma/shopping.karma.js | 80 ------------------- 4 files changed, 4 insertions(+), 84 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 74cd7fa10638..245c5eb72dd4 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -246,7 +246,6 @@ protected function get_remote_products( string $search_term, string $after, int $result = json_decode( $body, true ); if ( isset( $result['errors'] ) ) { $wp_error = new WP_Error(); - foreach ( $result['errors'] as $error ) { $error_code = $error['extensions']['code']; // https://shopify.dev/api/storefront#status_and_error_codes. @@ -267,7 +266,7 @@ protected function get_remote_products( string $search_term, string $after, int $wp_error->add( 'rest_unknown', __( 'Error fetching products from Shopify.', 'web-stories' ), [ 'status' => 500 ] ); } } - + return $wp_error; } @@ -293,6 +292,7 @@ protected function get_remote_products( string $search_term, string $after, int protected function fetch_remote_products( string $search_term, int $page, int $per_page, string $orderby, string $order ) { $after = ''; if ( $page > 1 ) { + // Loop around all the pages, getting the endCursor of each page, until you get the last one. for ( $i = 1; $i < $page; $i ++ ) { $result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); if ( is_wp_error( $result ) ) { diff --git a/packages/story-editor/src/app/story/useStoryReducer/reducers/setSelectedElements.js b/packages/story-editor/src/app/story/useStoryReducer/reducers/setSelectedElements.js index 05f7fddd66d2..46fee3b1511b 100644 --- a/packages/story-editor/src/app/story/useStoryReducer/reducers/setSelectedElements.js +++ b/packages/story-editor/src/app/story/useStoryReducer/reducers/setSelectedElements.js @@ -40,7 +40,7 @@ import { intersect } from './utils'; * @param {Object} state Current state * @param {Object} payload Action payload * @param {Array.} payload.elementIds Object with properties of new page - * @param payload.withLinked + * @param {boolean} payload.withLinked With linked. * @return {Object} New state */ function setSelectedElements(state, { elementIds, withLinked = false }) { diff --git a/packages/story-editor/src/app/story/useStoryReducer/reducers/toggleElement.js b/packages/story-editor/src/app/story/useStoryReducer/reducers/toggleElement.js index 91d1cab8c3a7..174d6a51d04c 100644 --- a/packages/story-editor/src/app/story/useStoryReducer/reducers/toggleElement.js +++ b/packages/story-editor/src/app/story/useStoryReducer/reducers/toggleElement.js @@ -29,7 +29,7 @@ * @param {Object} state Current state * @param {Object} payload Action payload * @param {string} payload.elementId Id to either add or remove from selection. - * @param payload.withLinked + * @param {boolean} payload.withLinked With linked. * @return {Object} New state */ function toggleElement(state, { elementId, withLinked = false }) { diff --git a/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js b/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js index 2fd08332bcd9..187f48242e1d 100644 --- a/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js +++ b/packages/story-editor/src/components/floatingMenu/elements/karma/shopping.karma.js @@ -67,86 +67,6 @@ describe('Shopping integration', () => { return storyContext.state.selectedElements[0]; }; - describe('Shopping tab', () => { - it('should handle product search add and remove', async () => { - const productTitle = 'Hoodie with Zipper'; - await focusProductSearchInput(); - expect(isStoryEmpty()).toEqual(true); - await fixture.events.keyboard.type('hood'); - // delay for search to catch-up - await fixture.events.sleep(400); - - const productButton = fixture.querySelector( - `[aria-label="Add ${productTitle}"]` - ); - await fixture.events.click(productButton); - await waitFor(() => - fixture.querySelector( - '[aria-label="Design menu"] [aria-label="Product"]' - ) - ); - - // add a small delay for debounce search to catchup - await fixture.events.sleep(500); - - // check story `state` - const selectedElement = await getSelectedElement(); - expect(isStoryEmpty()).toEqual(false); - await expect(selectedElement?.product?.productTitle).toBe(productTitle); - await focusProductSearchInput(); - - // select the product add / remove button - const product = fixture.querySelector( - `[aria-label="Remove ${productTitle}"]` - ); - - // check add / remove icons - const checkIcon = fixture.querySelector( - `[aria-label="Remove ${productTitle}"] svg[class^="productButton__Checkmark-"]` - ); - - const removeIcon = fixture.querySelector( - `[aria-label="Remove ${productTitle}"] svg[class^="productButton__Cross-"]` - ); - - expect(window.getComputedStyle(checkIcon).display).toBe('block'); - expect(window.getComputedStyle(removeIcon).display).toBe('none'); - await fixture.events.hover(product); - expect(window.getComputedStyle(checkIcon).display).toBe('none'); - expect(window.getComputedStyle(removeIcon).display).toBe('block'); - - // remove the product - await fixture.events.mouse.clickOn(product, 1, 1); - expect(isStoryEmpty()).toEqual(true); - }); - - it('should sort searched products', async () => { - await fixture.editor.library.shoppingTab.click(); - await fixture.events.keyboard.press('tab'); - - const sortDropdown = fixture.querySelector( - '[aria-label="Product sort options"]' - ); - await fixture.events.mouse.clickOn(sortDropdown, 1, 1); - - const option = fixture.screen.getByRole('option', { - name: /^Alphabetical: Z-A/, - }); - - await fixture.events.mouse.clickOn(option, 1, 1); - - // delay for search to catch-up - await fixture.events.sleep(400); - - const firstOption = fixture.querySelector( - '[aria-label="Products list"] [role="listitem"]' - ); - - expect(firstOption.textContent).toContain('WordPress Pennant'); - }); - }); - - describe('Product floating menu', () => { it('should render products menu', async () => { const productTitle = 'Album'; From e43f1ad86c2c09fde9b9bea975d7314225ce885a Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 11:40:19 +0100 Subject: [PATCH 39/42] Apply suggestions from code review --- includes/Shopping/Shopify_Query.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 245c5eb72dd4..3799d8e7fbae 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -307,9 +307,7 @@ protected function fetch_remote_products( string $search_term, int $page, int $p } } - $result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); - - return $result; + return $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); } /** From d96cdf46ce987914de2064f4e9e92e9e0d43002b Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 13:18:30 +0100 Subject: [PATCH 40/42] Fix test. --- .../phpunit/integration/tests/Shopping/Shopify_Query.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 14170d95e825..830fe6dd1eb0 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -223,18 +223,18 @@ public function test_fetch_remote_products_returns_from_transient(): void { 'pageInfo' => [ 'hasNextPage' => false ], ], ], - ] - ) + ] + ) ); $actual = $this->instance->get_search( '', 1, $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); - $this->assertSameSets( + $this->assertEqualSets( [ 'products' => [], 'has_next_page' => false, ], - $actual + $actual ); $this->assertSame( 0, $this->request_count ); } From 4ad79ac44ea796a47a8010b85a19cf2fddff65f0 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 13:31:00 +0100 Subject: [PATCH 41/42] Remove repeated logic. --- includes/Shopping/Shopify_Query.php | 23 ++++++++++++++++--- .../tests/Shopping/Shopify_Query.php | 15 +++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 3799d8e7fbae..8d410bb36eb5 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -222,9 +222,8 @@ protected function get_remote_products( string $search_term, string $after, int * * @param int $time Time to live (in seconds). Default is 5 minutes. */ - $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); - $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); - $cache_key = 'web_stories_shopify_data_' . md5( $cache_args ); + $cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS ); + $cache_key = $this->get_cache_key( $search_term, $after, $per_page, $orderby, $order ); $data = get_transient( $cache_key ); @@ -276,6 +275,24 @@ protected function get_remote_products( string $search_term, string $after, int return $result; } + /** + * Get cache key for properties. + * + * @since 1.21.0 + * + * @param string $search_term Search term to filter products by. + * @param string $after The cursor to retrieve nodes after in the connection. + * @param int $per_page Number of products to be fetched. + * @param string $orderby Sort retrieved products by parameter. + * @param string $order Whether to order products in ascending or descending order. + * Accepts 'asc' (ascending) or 'desc' (descending). + */ + protected function get_cache_key( $search_term, $after, $per_page, $orderby, $order ): string { + $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); + + return 'web_stories_shopify_data_' . md5( $cache_args ); + } + /** * Remotely fetches all products from the store. * diff --git a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php index 830fe6dd1eb0..dd505f01ca25 100644 --- a/tests/phpunit/integration/tests/Shopping/Shopify_Query.php +++ b/tests/phpunit/integration/tests/Shopping/Shopify_Query.php @@ -211,8 +211,17 @@ public function test_fetch_remote_products_returns_from_transient(): void { update_option( Settings::SETTING_NAME_SHOPIFY_HOST, 'example.myshopify.com' ); update_option( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN, '1234' ); - $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); - $cache_key = 'web_stories_shopify_data_' . md5( $cache_args ); + $cache_key = $this->call_private_method( + $this->instance, + 'get_cache_key', + [ + $search_term, + $after, + $per_page, + $orderby, + $order, + ] + ); set_transient( $cache_key, wp_json_encode( @@ -226,7 +235,7 @@ public function test_fetch_remote_products_returns_from_transient(): void { ] ) ); - $actual = $this->instance->get_search( '', 1, $per_page, $orderby, $order ); + $actual = $this->instance->get_search( $search_term, 1, $per_page, $orderby, $order ); $this->assertNotWPError( $actual ); $this->assertEqualSets( From 79bd1a26f261ddcc6faba15b300a89c7f0a4129a Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 8 Jun 2022 14:48:32 +0100 Subject: [PATCH 42/42] Apply suggestions from code review --- includes/Shopping/Shopify_Query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/Shopping/Shopify_Query.php b/includes/Shopping/Shopify_Query.php index 8d410bb36eb5..83ae086d64b1 100644 --- a/includes/Shopping/Shopify_Query.php +++ b/includes/Shopping/Shopify_Query.php @@ -278,7 +278,7 @@ protected function get_remote_products( string $search_term, string $after, int /** * Get cache key for properties. * - * @since 1.21.0 + * @since 1.22.0 * * @param string $search_term Search term to filter products by. * @param string $after The cursor to retrieve nodes after in the connection. @@ -296,7 +296,7 @@ protected function get_cache_key( $search_term, $after, $per_page, $orderby, $or /** * Remotely fetches all products from the store. * - * @since 1.21.0 + * @since 1.22.0 * * @param string $search_term Search term to filter products by. * @param int $page Number of page for paginated requests.