-
Notifications
You must be signed in to change notification settings - Fork 7
Front-end pagination support #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e7f1a6b
Automatically insert pagination blocks when pagination is supported
chriszarate 834024f
Add pagination block
chriszarate 8b2fa73
CSS lint fixes
chriszarate f80fec8
Move ID from block to query
chriszarate f33a66e
Merge branch 'trunk' into try/pagination-blocks
chriszarate bdcb8e3
Inspect and validate pagination query args
chriszarate 59eaa0c
Update pagination schema docs
chriszarate 0acd772
Add filter to change the pagination query var name
chriszarate 1465512
Add additional comments to Pagination
chriszarate File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace RemoteDataBlocks\Editor\DataBinding; | ||
|
||
use RemoteDataBlocks\Config\Query\QueryInterface; | ||
|
||
use function add_filter; | ||
use function add_query_arg; | ||
use function get_query_var; | ||
|
||
defined( 'ABSPATH' ) || exit(); | ||
|
||
class Pagination { | ||
private static $variable_name = 'rdb-pagination'; | ||
|
||
public static function init(): void { | ||
add_filter( 'query_vars', [ __CLASS__, 'register_query_var' ], 10, 1 ); | ||
} | ||
|
||
public static function create_query_var( string $query_id, array $pagination_input_variables ): string { | ||
$value = [ | ||
$query_id => $pagination_input_variables, | ||
]; | ||
|
||
return add_query_arg( self::$variable_name, self::encode_query_var( $value ) ); | ||
} | ||
|
||
private static function decode_query_var( string $query_var_string ): array { | ||
return json_decode( base64_decode( $query_var_string ), true ) ?? []; | ||
} | ||
|
||
private static function encode_query_var( array $query_var_value ): string { | ||
return base64_encode( wp_json_encode( $query_var_value ) ); | ||
} | ||
|
||
public static function get_pagination_input_variables_for_current_request( QueryInterface $query ): array { | ||
$untrusted_variables = self::decode_query_var( get_query_var( self::$variable_name, '' ) ); | ||
|
||
if ( empty( $untrusted_variables ) || ! is_array( $untrusted_variables ) ) { | ||
return []; | ||
} | ||
|
||
$query_id = $query->get_id(); | ||
|
||
// The query var value is an associative array with IDs as keys and | ||
// values that are an associative array of input variables. | ||
// | ||
// We only expect a single key => value pair, but in the future we may | ||
// decide to support more than one. This would allow us to control the | ||
// pagination of multiple remote data blocks independently. | ||
$untrusted_variables = $untrusted_variables[ $query_id ] ?? []; | ||
|
||
if ( empty( $untrusted_variables ) || ! is_array( $untrusted_variables ) ) { | ||
return []; | ||
} | ||
|
||
// Only accept pagination input variables that are defined in this query's | ||
// input schema. | ||
$input_schema = $query->get_input_schema(); | ||
$input_variables = []; | ||
$pagination_input_variable_types = [ | ||
'ui:pagination_cursor', | ||
'ui:pagination_cursor_next', | ||
'ui:pagination_cursor_previous', | ||
'ui:pagination_offset', | ||
'ui:pagination_page', | ||
'ui:pagination_per_page', | ||
]; | ||
|
||
foreach ( $input_schema as $slug => $input ) { | ||
if ( ! in_array( $input['type'] ?? null, $pagination_input_variable_types, true ) ) { | ||
continue; | ||
} | ||
|
||
if ( ! array_key_exists( $slug, $untrusted_variables ) ) { | ||
continue; | ||
} | ||
|
||
$value = $untrusted_variables[ $slug ]; | ||
|
||
if ( ! is_string( $value ) && ! is_int( $value ) ) { | ||
continue; | ||
} | ||
|
||
$input_variables[ $slug ] = $value; | ||
} | ||
|
||
return $input_variables; | ||
} | ||
|
||
public static function format_pagination_data_for_query_response( array|null $pagination_data, array $query_input_schema, array $input_variables ): array { | ||
// If the input schema is empty, it does not support pagination. | ||
if ( empty( $query_input_schema ) ) { | ||
return []; | ||
} | ||
|
||
// Find the appropriate pagination variables from the input schema. | ||
$cursor_variable = null; | ||
$cursor_next_variable = null; | ||
$cursor_previous_variable = null; | ||
$offset_variable = null; | ||
$page_variable = null; | ||
$per_page_variable = null; | ||
|
||
foreach ( $query_input_schema as $slug => $input ) { | ||
$type = $input['type'] ?? ''; | ||
if ( 'ui:pagination_cursor' === $type ) { | ||
$cursor_variable = $slug; | ||
} elseif ( 'ui:pagination_cursor_next' === $type ) { | ||
$cursor_next_variable = $slug; | ||
} elseif ( 'ui:pagination_cursor_previous' === $type ) { | ||
$cursor_previous_variable = $slug; | ||
} elseif ( 'ui:pagination_offset' === $type ) { | ||
$offset_variable = $slug; | ||
} elseif ( 'ui:pagination_page' === $type ) { | ||
$page_variable = $slug; | ||
} elseif ( 'ui:pagination_per_page' === $type ) { | ||
$per_page_variable = $slug; | ||
} | ||
} | ||
|
||
// Default pagination values | ||
$current_per_page = $input_variables[ $per_page_variable ] ?? 10; | ||
$pagination_input_variables = []; | ||
$pagination_type = 'NONE'; | ||
$total_items = $pagination_data['total_items'] ?? null; | ||
|
||
$pagination_input_variable_targets = [ | ||
'offset' => $offset_variable, | ||
'page' => $page_variable, | ||
'per_page' => $per_page_variable, | ||
]; | ||
|
||
if ( $cursor_variable ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this logic be documented just above this if statement, or as a function comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 1465512 |
||
$pagination_type = 'CURSOR_SIMPLE'; | ||
|
||
if ( isset( $pagination_data['cursor_next'] ) ) { | ||
$pagination_input_variables['next_page'] = [ | ||
$cursor_variable => $pagination_data['cursor_next'], | ||
]; | ||
} | ||
|
||
if ( isset( $pagination_data['cursor_previous'] ) ) { | ||
$pagination_input_variables['previous_page'] = [ | ||
$cursor_variable => $pagination_data['cursor_previous'], | ||
]; | ||
} | ||
} elseif ( $cursor_next_variable && $cursor_previous_variable ) { | ||
$pagination_type = 'CURSOR'; | ||
|
||
if ( isset( $pagination_data['cursor_next'] ) ) { | ||
$pagination_input_variables['next_page'] = [ | ||
$cursor_next_variable => $pagination_data['cursor_next'], | ||
]; | ||
} | ||
|
||
if ( isset( $pagination_data['cursor_previous'] ) ) { | ||
$pagination_input_variables['previous_page'] = [ | ||
$cursor_previous_variable => $pagination_data['cursor_previous'], | ||
]; | ||
} | ||
} elseif ( $offset_variable ) { | ||
$pagination_type = 'OFFSET'; | ||
$current_offset = $input_variables[ $offset_variable ] ?? 0; | ||
|
||
if ( null === $total_items || $current_offset + $current_per_page < $total_items ) { | ||
$pagination_input_variables['next_page'] = [ | ||
$offset_variable => $current_offset + $current_per_page, | ||
]; | ||
} | ||
|
||
if ( $current_offset >= $current_per_page ) { | ||
$pagination_input_variables['previous_page'] = [ | ||
$offset_variable => $current_offset - $current_per_page, | ||
]; | ||
} | ||
} elseif ( $page_variable ) { | ||
$pagination_type = 'PAGE'; | ||
$current_page = $input_variables[ $page_variable ] ?? 1; | ||
$total_pages = $total_items ? ceil( $total_items / $current_per_page ) : null; | ||
|
||
if ( null === $total_pages || $current_page < $total_pages ) { | ||
$pagination_input_variables['next_page'] = [ | ||
$page_variable => $current_page + 1, | ||
]; | ||
} | ||
|
||
if ( $current_page > 1 ) { | ||
$pagination_input_variables['previous_page'] = [ | ||
$page_variable => $current_page - 1, | ||
]; | ||
} | ||
} | ||
|
||
if ( 'NONE' === $pagination_type ) { | ||
return []; | ||
} | ||
|
||
return [ | ||
'input_variables' => $pagination_input_variables, | ||
'input_variable_targets' => $pagination_input_variable_targets, | ||
'per_page' => $current_per_page, | ||
'total_items' => $total_items, | ||
'type' => $pagination_type, | ||
]; | ||
} | ||
|
||
public static function register_query_var( array $query_vars ): array { | ||
$query_vars[] = self::$variable_name; | ||
return $query_vars; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the thoughts I had was to see if we could leverage the UUID from the config instead of doing it this way. But, I see that the problem is this isn't always available like for code based sources (Art Institute being an example of this).
Is there any benefit in changing that, so that all data source configs do have a UUID assigned to them? Then, we could use that UUID here and wherever else we might need to in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good if the answer is no, or that it be punted to a future PR. This is just a thought that occurred to me when I tried to experiment with changing it to the UUID, and not seeing it reflect since the Art Institute didn't have one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good question. We've discussed both sides. The major downsides of universally requiring UUIDs for all entities are:
Another downside of UUIDs in this use case is that they have no functional value. They are just identifiers. The ID introduced in this PR, by contrast, is deterministic based on the inputs that influence its behavior. This is useful since we are using it to identify external attributes that can be applied to this entity (pagination variables). If the input changes, the ID changes, and it makes sense that the attributes might no longer apply. Meanwhile, a UUID might change for any number of reasons that have nothing to do with the entity's behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All that said, the main reason that this ID is useful at all is because block instances don't have stable identifiers. If they did, we would use them.