Skip to content

Commit 5c2ce05

Browse files
authored
feat: Add Sort Facet support (#60)
* dev: add sort to connection args * dev: apply sort to connection (first pass) It works, but needs a refactor * dev: add facet settings * dev: support sorting by custom fields * tests: test sort facet * chore: update changelog * chore: update readme refs to include Sort * fix: cleanup sort args on unset facets * docs: update woo snippet for sort support
1 parent a1e10f7 commit 5c2ce05

17 files changed

+1017
-82
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
- feat: Add support for the Sort Facet. (Props to @ninie1205 for sponsoring this feature!)
6+
- fix: Fallback to snake_case when matching the FacetQueryArgs input value to the FacetWP facet name.
7+
- docs: Update WooCommerce snippet in README.md to support the Sort Facet.
8+
59
## v0.4.2
610

711
This _minor_ release lays the groundwork for the upcoming Facet autoregistration / official Sort Facet support. It introduces a new `FacetConfig` interface, which is implemented by the `Facet` object. Additionally, we adopted the use of WPGraphQL Plugin Boilerplate to scaffold our PHP classes, updated our Composer dev dependencies, and started testing against WordPress 6.2 and running WPUnit tests as part of our CI workflow.

README.md

+33-22
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ query GetPostsByFacet( $query: FacetQueryArgs, $after: String, $search: String,
9393
posts ( # The results of the facet query. Can be filtered by WPGraphQL connection where args
9494
first: 10,
9595
after: $after,
96-
where: { search: $search, orderby: $orderBy}
96+
where: { search: $search, orderby: $orderBy} # The `orderby` arg is ignored if using the Sort facet.
9797
) {
9898
pageInfo {
9999
hasNextPage
@@ -113,47 +113,58 @@ query GetPostsByFacet( $query: FacetQueryArgs, $after: String, $search: String,
113113
Support for WooCommerce Products can be added with following configuration:
114114

115115
```php
116-
add_action( 'graphql_register_types', function () {
116+
// This is the same as all CPTs.
117+
add_action( 'graphql_facetwp_init', function () {
117118
register_graphql_facet_type( 'product' );
118119
});
119120

121+
// This is required because WooGQL uses a custom connection resolver.
120122
add_filter( 'facetwp_graphql_facet_connection_config',
121-
function ( array $default_graphql_config, array $config ) {
122-
$type = $config['type'];
123-
$singular = $config['singular'];
124-
$field = $config['field'];
125-
$plural = $config['plural'];
126-
127-
return [
128-
'fromType' => $field,
129-
'toType' => $singular,
130-
'fromFieldName' => lcfirst( $plural ),
131-
'connectionArgs' => Products::get_connection_args(),
132-
'resolveNode' => function ( $node, $_args, $context) use ( $type ) {
123+
function ( array $default_graphql_config, array $facet_config ) {
124+
$type = $config['type'];
125+
126+
$use_graphql_pagination = \WPGraphQL\FacetWP\Registry\FacetRegistry::use_graphql_pagination();
127+
128+
return array_merge(
129+
$default_graphql_config,
130+
[
131+
'connectionArgs' => \WPGraphQL\WooCommerce\Connection\Products::get_connection_args(),
132+
'resolveNode' => function ( $node, $_args, $context ) use ( $type ) {
133133
return $context->get_loader( $type )->load_deferred( $node->ID );
134134
},
135-
'resolve' => function ( $source, $args, $context, $info ) use ( $type ) {
136-
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, $type);
137-
138-
if ( $type === 'product' ) {
139-
$resolver = Products::set_ordering_query_args( $resolver, $args );
135+
'resolve' => function ( $source, $args, $context, $info ) use ( $type, $use_graphql_pagination ) {
136+
// If we're using FWP's offset pagination, we need to override the connection args.
137+
if ( ! $use_graphql_pagination ) {
138+
$args['first'] = $source['pager']['per_page'];
140139
}
141140

141+
$resolver = new \WPGraphQL\Data\Connection\PostObjectConnectionResolver( $source, $args, $context, $info, $type );
142+
143+
// Override the connection results with the FWP results.
142144
if( ! empty( $source['results'] ) ) {
143145
$resolver->->set_query_arg( 'post__in', $source['results'] );
144146
}
145147

148+
// Use post__in when delegating sorting to FWP.
149+
if ( ! empty( $source['is_sort'] ) ) {
150+
$resolver->set_query_arg( 'orderby', 'post__in' );
151+
} elseif( 'product' === $type ) {
152+
// If we're relying on WPGQL to sort, we need to to handle WooCommerce meta.
153+
$resolver = Products::set_ordering_query_args( $resolver, $args );
154+
}
155+
146156
return $resolver ->get_connection();
147157
},
148-
];
158+
]
159+
);
149160
},
150-
100,
161+
100,
151162
2
152163
);
153164
```
154165

155166
### Limitations
156-
Currently the plugin only has been tested using Checkbox and Radio facet types. Support for additional types is in development.
167+
Currently the plugin only has been tested using Checkbox, Radio, and Sort facet types. Support for additional types is in development.
157168

158169
## Testing
159170

src/Registry/FacetRegistry.php

+138-34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use WPGraphQL\Connection\PostObjects;
1212
use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
13+
use WPGraphQL\FacetWP\Type\Enum\SortOptionsEnum;
1314
use WPGraphQL\FacetWP\Type\Input;
1415

1516
/**
@@ -104,6 +105,10 @@ public static function get_facet_input_type( array $config ) {
104105
// Single Int.
105106
$type = 'Int';
106107

108+
break;
109+
case 'sort':
110+
$type = SortOptionsEnum::get_type_name( $config['name'] );
111+
107112
break;
108113
case 'autocomplete':
109114
case 'checkboxes':
@@ -217,6 +222,20 @@ private static function register_root_field( array $facet_config ) :void {
217222
],
218223
];
219224

225+
// Stash the sort settings, since we don't get them from the payload.
226+
$sort_settings = [];
227+
228+
// Apply the orderby args.
229+
foreach ( $fwp_args['facets'] as $key => $facet_args ) {
230+
if ( ! empty( $facet_args['is_sort'] ) ) {
231+
$fwp_args['query_args'] = array_merge_recursive( $fwp_args['query_args'], $facet_args['query_args'] );
232+
$sort_settings[ $key ] = $facet_args['settings'];
233+
234+
// Set the selected facet back to a string.
235+
$fwp_args['facets'][ $key ] = $facet_args['selected'];
236+
}
237+
}
238+
220239
$filtered_ids = [];
221240
if ( $use_graphql_pagination ) {
222241

@@ -241,8 +260,11 @@ function ( $post_ids ) use ( &$filtered_ids ) {
241260

242261
// @todo helper function.
243262
foreach ( $payload['facets'] as $key => $facet ) {
263+
// Try to get the settings from the payload, otherwise fallback to the parsed query args.
244264
if ( isset( $facet['settings'] ) ) {
245265
$facet['settings'] = self::to_camel_case( $facet['settings'] );
266+
} elseif ( isset( $sort_settings[ $key ] ) ) {
267+
$facet['settings'] = self::to_camel_case( $sort_settings[ $key ] );
246268
}
247269

248270
$payload['facets'][ $key ] = $facet;
@@ -256,6 +278,7 @@ function ( $post_ids ) use ( &$filtered_ids ) {
256278
'facets' => array_values( $payload['facets'] ),
257279
'results' => count( $results ) ? $results : null,
258280
'pager' => $payload['pager'] ?? [],
281+
'is_sort' => ! empty( $fwp_args['query_args']['orderby'] ),
259282
];
260283
},
261284
]
@@ -450,10 +473,16 @@ private static function register_facet_connection( array $facet_config ) : void
450473
}
451474

452475
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, $type );
476+
453477
if ( ! empty( $source['results'] ) ) {
454478
$resolver->set_query_arg( 'post__in', $source['results'] );
455479
}
456480

481+
// Use post__in when delegating sorting to FWP.
482+
if ( ! empty( $source['is_sort'] ) ) {
483+
$resolver->set_query_arg( 'orderby', 'post__in' );
484+
}
485+
457486
return $resolver->get_connection();
458487
},
459488
];
@@ -489,41 +518,79 @@ private static function parse_query( array $query ) : array {
489518
$reduced_query = array_reduce(
490519
$facets,
491520
function ( $prev, $cur ) use ( $query ) {
492-
$name = $cur['name'];
493-
$facet = isset( $query[ $name ] ) ? $query[ $name ] : null;
494-
495-
if ( isset( $facet ) ) {
496-
switch ( $cur['type'] ) {
497-
case 'checkboxes':
498-
case 'fselect':
499-
case 'rating':
500-
case 'radio':
501-
case 'dropdown':
502-
case 'hierarchy':
503-
case 'search':
504-
case 'autocomplete':
505-
$prev[ $name ] = $facet;
506-
break;
507-
case 'slider':
508-
case 'date_range':
509-
case 'number_range':
510-
$input = $facet;
511-
$prev[ $name ] = [
512-
$input['min'],
513-
$input['max'],
514-
];
521+
// Get the facet name.
522+
$name = $cur['name'] ?? '';
523+
$camel_cased_name = ! empty( $name ) ? self::to_camel_case( $name ) : '';
524+
$facet = is_string( $camel_cased_name ) && isset( $query[ $camel_cased_name ] ) ? $query[ $camel_cased_name ] : null;
525+
526+
// Fallback to snakeCased name.
527+
if ( ! isset( $facet ) ) {
528+
$facet = isset( $query[ $name ] ) ? $query[ $name ] : null;
529+
}
515530

516-
break;
517-
case 'proximity':
518-
$input = $facet;
519-
$prev[ $name ] = [
520-
$input['latitude'],
521-
$input['longitude'],
522-
$input['chosenRadius'],
523-
$input['locationName'],
524-
];
525-
break;
526-
}
531+
switch ( $cur['type'] ) {
532+
case 'checkboxes':
533+
case 'fselect':
534+
case 'rating':
535+
case 'radio':
536+
case 'dropdown':
537+
case 'hierarchy':
538+
case 'search':
539+
case 'autocomplete':
540+
$prev[ $name ] = $facet;
541+
break;
542+
case 'slider':
543+
case 'date_range':
544+
case 'number_range':
545+
$input = $facet;
546+
$prev[ $name ] = [
547+
$input['min'] ?? null,
548+
$input['max'] ?? null,
549+
];
550+
551+
break;
552+
case 'proximity':
553+
$input = $facet;
554+
$prev[ $name ] = [
555+
$input['latitude'] ?? null,
556+
$input['longitude'] ?? null,
557+
$input['chosenRadius'] ?? null,
558+
$input['locationName'] ?? null,
559+
];
560+
561+
break;
562+
563+
case 'sort':
564+
$input = $facet;
565+
$sort_options = self::parse_sort_facet_options( $cur );
566+
567+
// We pass these through to create our sort args.
568+
$prev[ $name ] = [
569+
'is_sort' => true,
570+
'selected' => $facet,
571+
'settings' => [
572+
'default_label' => $cur['default_label'],
573+
'sort_options' => $cur['sort_options'],
574+
],
575+
'query_args' => [],
576+
];
577+
578+
/**
579+
* Define the query args for the sort.
580+
*
581+
* This is a shim of FacetWP_Facet_Sort::apply_sort()
582+
*/
583+
if ( ! empty( $sort_options[ $facet ] ) ) {
584+
$qa = $sort_options[ $facet ]['query_args'];
585+
586+
if ( isset( $qa['meta_query'] ) ) {
587+
$prev[ $name ]['query_args']['meta_query'] = $qa['meta_query'];
588+
}
589+
590+
$prev[ $name ]['query_args']['orderby'] = $qa['orderby'];
591+
}
592+
593+
break;
527594
}
528595

529596
return $prev;
@@ -617,4 +684,41 @@ private static function register_facet_settings() : void {
617684
public static function use_graphql_pagination() : bool {
618685
return apply_filters( 'wpgraphql_facetwp_user_graphql_pagination', false );
619686
}
687+
688+
/**
689+
* Parses the sort options for a sort facet into a WP_Query compatible array.
690+
*
691+
* @see \FacetWP_Facet_Sort::parse_sort_facet()
692+
*
693+
* @param array<string, mixed> $facet The facet configuration.
694+
*/
695+
private static function parse_sort_facet_options( array $facet ) : array {
696+
$sort_options = [];
697+
698+
foreach ( $facet['sort_options'] as $row ) {
699+
$parsed = FWP()->builder->parse_query_obj( [ 'orderby' => $row['orderby'] ] );
700+
701+
$sort_options[ $row['name'] ] = [
702+
'label' => $row['label'],
703+
'query_args' => array_intersect_key(
704+
$parsed,
705+
[
706+
'meta_query' => true,
707+
'orderby' => true,
708+
]
709+
),
710+
];
711+
}
712+
713+
$sort_options = apply_filters(
714+
'facetwp_facet_sort_options',
715+
$sort_options,
716+
[
717+
'facet' => $facet,
718+
'template_name' => 'graphql',
719+
]
720+
);
721+
722+
return $sort_options;
723+
}
620724
}

src/Registry/TypeRegistry.php

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private static function enums() : array {
4747
// Enums to register.
4848
$classes_to_register = [
4949
Enum\ProximityRadiusOptions::class,
50+
Enum\SortOptionsEnum::class,
5051
];
5152

5253
/**
@@ -106,6 +107,8 @@ public static function objects() : array {
106107
WPObject\FacetChoice::class,
107108
WPObject\FacetPager::class,
108109
WPObject\FacetRangeSettings::class,
110+
WPObject\FacetSortOptionOrderBySetting::class,
111+
WPObject\FacetSortOptionSetting::class,
109112
WPObject\FacetSettings::class,
110113
WPObject\Facet::class,
111114
];

0 commit comments

Comments
 (0)