From 119ec3240e4a480de427017c578e6ad43d717932 Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Tue, 24 Jan 2023 16:52:42 +0000 Subject: [PATCH] Editor: Adds pagination and ordering support to `WP_REST_Pattern_Directory_Controller`. Adds pagination and ordering support to `WP_REST_Pattern_Directory_Controller` by allow listing `'per_page'`, `'page'`, `'offset'`, `'order'`, and `'orderby'` query parameters. This change enables pagination and ordering features in the pattern directory explorer by using the same sort as wordpress.org/patterns. Reference: * [https://github.com/WordPress/gutenberg/pull/45293 Gutenberg PR 45293] Follow-up to [55098], [51206], [51021]. Props ntsekouras, ryelle, arrasel403, hellofromTonya, ironprogrammer, mukesh27, robinwpdeveloper. Fixes #57501. git-svn-id: https://develop.svn.wordpress.org/trunk@55132 602fd350-edb4-49c9-b593-d223f7449a82 --- ...s-wp-rest-pattern-directory-controller.php | 74 ++++-- .../rest-pattern-directory-controller.php | 229 ++++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 49 ++++ 3 files changed, 326 insertions(+), 26 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php index b0ba186a02..4ba3712c8d 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php @@ -81,6 +81,7 @@ class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { * * @since 5.8.0 * @since 6.0.0 Added 'slug' to request. + * @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. @@ -93,31 +94,23 @@ class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { */ require ABSPATH . WPINC . '/version.php'; - $query_args = array( - 'locale' => get_user_locale(), - 'wp-version' => $wp_version, + $valid_query_args = array( + 'offset' => true, + 'order' => true, + 'orderby' => true, + 'page' => true, + 'per_page' => true, + 'search' => true, + 'slug' => true, ); + $query_args = array_intersect_key( $request->get_params(), $valid_query_args ); - $category_id = $request['category']; - $keyword_id = $request['keyword']; - $search_term = $request['search']; - $slug = $request['slug']; + $query_args['locale'] = get_user_locale(); + $query_args['wp-version'] = $wp_version; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- it's defined in `version.php` above. + $query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false; + $query_args['pattern-keywords'] = isset( $request['keyword'] ) ? $request['keyword'] : false; - if ( $category_id ) { - $query_args['pattern-categories'] = $category_id; - } - - if ( $keyword_id ) { - $query_args['pattern-keywords'] = $keyword_id; - } - - if ( $search_term ) { - $query_args['search'] = $search_term; - } - - if ( $slug ) { - $query_args['slug'] = $slug; - } + $query_args = array_filter( $query_args ); $transient_key = $this->get_transient_key( $query_args ); @@ -303,16 +296,14 @@ class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { * Retrieves the search parameters for the block pattern's collection. * * @since 5.8.0 + * @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request. * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); - // Pagination is not supported. - unset( $query_params['page'] ); - unset( $query_params['per_page'] ); - + $query_params['per_page']['default'] = 100; $query_params['search']['minLength'] = 1; $query_params['context']['default'] = 'view'; @@ -333,6 +324,37 @@ class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { 'type' => 'array', ); + $query_params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.' ), + 'type' => 'integer', + ); + + $query_params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ); + + $query_params['orderby'] = array( + 'description' => __( 'Sort collection by post attribute.' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'author', + 'date', + 'id', + 'include', + 'modified', + 'parent', + 'relevance', + 'slug', + 'include_slugs', + 'title', + 'favorite_count', + ), + ); + /** * Filter collection parameters for the block pattern directory controller. * diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php index a59c8fd5e0..11f0892a97 100644 --- a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -30,6 +30,15 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ */ private static $controller; + /** + * List of URLs captured. + * + * @since 6.2.0 + * + * @var string[] + */ + protected static $http_request_urls; + /** * Set up class test fixtures. * @@ -44,9 +53,30 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ ) ); + self::$http_request_urls = array(); + static::$controller = new WP_REST_Pattern_Directory_Controller(); } + /** + * Tear down after class. + * + * @since 6.2.0 + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$contributor_id ); + } + + /** + * Clear the captured request URLs after each test. + * + * @since 6.2.0 + */ + public function tear_down() { + self::$http_request_urls = array(); + parent::tear_down(); + } + /** * Asserts that the pattern matches the expected response schema. * @@ -310,6 +340,176 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ $this->assertSame( 'modified the cache', $patterns[0] ); } + /** + * Tests if the provided query args are passed through to the wp.org API. + * + * @since 6.2.0 + * + * @ticket 57501 + * + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @dataProvider data_get_items_query_args + * + * @param string $param Query parameter name (ex, page). + * @param mixed $value Query value to test. + * @param bool $is_error Whether this value should error or not. + * @param mixed $expected Expected value (or expected error code). + */ + public function test_get_items_query_args( $param, $value, $is_error, $expected ) { + wp_set_current_user( self::$contributor_id ); + add_filter( 'pre_http_request', array( $this, 'mock_request_to_apiwporg_url' ), 10, 3 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + if ( $value ) { + $request->set_query_params( array( $param => $value ) ); + } + + $response = rest_do_request( $request ); + $data = $response->get_data(); + if ( $is_error ) { + $this->assertSame( $expected, $data['code'], 'Response error code does not match' ); + $this->assertStringContainsString( $param, $data['message'], 'Response error message does not match' ); + } else { + $this->assertCount( 1, self::$http_request_urls, 'The number of HTTP Request URLs is not 1' ); + $this->assertStringContainsString( $param . '=' . $expected, self::$http_request_urls[0], 'The param and/or value do not match' ); + } + } + + /** + * Data provider. + * + * return array[] + */ + public function data_get_items_query_args() { + return array( + 'per_page default' => array( + 'param' => 'per_page', + 'value' => false, + 'is_error' => false, + 'expected' => 100, + ), + 'per_page custom-1' => array( + 'param' => 'per_page', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'per_page custom-2' => array( + 'param' => 'per_page', + 'value' => 50, + 'is_error' => false, + 'expected' => 50, + ), + 'per_page invalid-1' => array( + 'param' => 'per_page', + 'value' => 200, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'per_page invalid-2' => array( + 'param' => 'per_page', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'page default' => array( + 'param' => 'page', + 'value' => false, + 'is_error' => false, + 'expected' => 1, + ), + 'page custom' => array( + 'param' => 'page', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'page invalid' => array( + 'param' => 'page', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'offset custom' => array( + 'param' => 'offset', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'offset invalid-1' => array( + 'param' => 'offset', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'order default' => array( + 'param' => 'order', + 'value' => false, + 'is_error' => false, + 'expected' => 'desc', + ), + 'order custom' => array( + 'param' => 'order', + 'value' => 'asc', + 'is_error' => false, + 'expected' => 'asc', + ), + 'order invalid-1' => array( + 'param' => 'order', + 'value' => 10, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'order invalid-2' => array( + 'param' => 'order', + 'value' => 'fake', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'orderby default' => array( + 'param' => 'orderby', + 'value' => false, + 'is_error' => false, + 'expected' => 'date', + ), + 'orderby custom-1' => array( + 'param' => 'orderby', + 'value' => 'title', + 'is_error' => false, + 'expected' => 'title', + ), + 'orderby custom-2' => array( + 'param' => 'orderby', + 'value' => 'date', + 'is_error' => false, + 'expected' => 'date', + ), + 'orderby custom-3' => array( + 'param' => 'orderby', + 'value' => 'favorite_count', + 'is_error' => false, + 'expected' => 'favorite_count', + ), + 'orderby invalid-1' => array( + 'param' => 'orderby', + 'value' => 10, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'orderby invalid-2' => array( + 'param' => 'orderby', + 'value' => 'fake', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + ); + } + /** * @doesNotPerformAssertions */ @@ -573,4 +773,33 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ 3 ); } + + /** + * Mock the request to wp.org URL to capture the URLs. + * + * @since 6.2.0 + * + * @return array faux/mocked response. + */ + public function mock_request_to_apiwporg_url( $response, $args, $url ) { + if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) { + return $response; + } + + self::$http_request_urls[] = $url; + + // Return a response to prevent external API request. + $response = array( + 'headers' => array(), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => '[]', + 'cookies' => array(), + 'filename' => null, + ); + + return $response; + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 896322ed48..1dde2a8d48 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -10475,6 +10475,21 @@ mockedApiResponse.Schema = { "default": "view", "required": false }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + }, "search": { "description": "Limit results to those matching a string.", "type": "string", @@ -10497,6 +10512,40 @@ mockedApiResponse.Schema = { "description": "Limit results to those matching a pattern (slug).", "type": "array", "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by post attribute.", + "type": "string", + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title", + "favorite_count" + ], + "required": false } } }