feat: Featured Snippets, search and filtering for Community page#383
feat: Featured Snippets, search and filtering for Community page#383code-snippets-bot wants to merge 29 commits into
Conversation
Add get_featured_snippets() to Cloud_API that fetches from the cloud /api/v1/featured endpoint with transient caching derived from the cached_until response field. Add /cloud/featured REST route to expose featured snippets to the frontend.
Load featured snippets on the Community Cloud page when no search query is active. The frontend context fetches from the /cloud/featured REST endpoint on mount and displays results under a "Featured Snippets" heading. When the user performs a search, normal search behaviour takes over. Add PHPUnit tests covering transient caching, cache-hit bypass, and graceful fallback on HTTP errors.
Add 3 additional PHPUnit tests: TTL respects cached_until, missing data key returns empty result, and transient stores Cloud_Snippets instance. Add Playwright E2E spec for community cloud page covering page load, featured heading display, and search override behavior.
There was a problem hiding this comment.
This PR is clean!
The only suggestion I can make is to mock the responses via Playwright page.route() + route.fulfill() with fixed JSON and X-WP-Total / X-WP-TotalPages headers. That avoids depending on a live cloud/API, removes flakiness from timeouts and env differences, and lets us assert the featured heading, row content, and “search hides featured” etc deterministically. Happy for this to land as a follow-up.
|
PlayWright tests are failing |
| const requestId = nextRequestId() | ||
| setIsSearching(true) | ||
|
|
||
| api.getResponse<CloudSnippetSchema[]>( |
There was a problem hiding this comment.
Huge amount of duplicated code here.
| return | ||
| } | ||
|
|
||
| clearTimeout(searchTimerRef.current) |
There was a problem hiding this comment.
What's the purpose of the timeout? Are we worried about requests to the API taking too long in a way that Axios is unprepared to handle?
| const [error, setError] = useState(false) | ||
| const [isFeatured, setIsFeatured] = useState(false) | ||
|
|
||
| const searchTimerRef = useRef<ReturnType<typeof setTimeout>>() |
There was a problem hiding this comment.
I do not understand the use of useRef here instead of useState.
| 'site_host' => wp_parse_url( get_site_url(), PHP_URL_HOST ), | ||
| ]; | ||
|
|
||
| foreach ( [ 'category', 'type', 'status' ] as $key ) { |
There was a problem hiding this comment.
Is there actually any difference between passing these parameters with empty values versus not passing them at all?
| delete_transient( self::CATEGORIES_TRANSIENT_KEY ); | ||
| delete_transient( 'cs_codevault_snippets' ); | ||
|
|
||
| $wpdb->query( |
There was a problem hiding this comment.
I think it'd be much better to avoid a direct DB query here if we can avoid it, especially on a core table. Is there a reason why delete_transient can't do the job for us?
There was a problem hiding this comment.
Featured cache keys are dynamic: cs_featured_snippets_p{page}_pp{per_page}_{md5(filters)}. WordPress delete_transient() only removes one exact key, so “reset all featured caches” needs either LIKE delete, a registry of keys in a single transient, or a fixed key with a generation/version bump. The raw query is there because of that key shape; if we want to drop SQL, we need to switch the cache strategy.
| // Wait for the loading spinner to disappear, indicating the featured request completed. | ||
| await page.locator('.cloud-search .components-spinner').waitFor({ state: 'hidden', timeout: TIMEOUTS.DEFAULT }).catch(() => undefined) | ||
|
|
||
| const featuredHeading = page.locator('.cloud-featured-heading') |
There was a problem hiding this comment.
Tests target .cloud-featured-heading while the heading uses .cloud-snippets-heading
| 'category' => [ | ||
| 'description' => esc_html__( 'Filter by category name (comma-separated).', 'code-snippets' ), | ||
| 'type' => 'string', | ||
| 'default' => '', | ||
| ], | ||
| 'type' => [ | ||
| 'description' => esc_html__( 'Filter by language/type name (comma-separated).', 'code-snippets' ), | ||
| 'type' => 'string', | ||
| 'default' => '', | ||
| ], |
There was a problem hiding this comment.
get_filter_args() documents category/type as names (and comma-separated strings), while CloudSearchFilters and the UI use numeric IDs.
| $filters = $this->extract_filters( $request ); | ||
| $cloud_snippets = Cloud_API::fetch_search_results( $method, $query, $page, $per_page, $filters ); | ||
|
|
||
| $results = []; | ||
|
|
||
| foreach ( $cloud_snippets->snippets as $snippet ) { | ||
| $results[] = $snippet->get_fields(); | ||
| } | ||
|
|
||
| $response = rest_ensure_response( $results ); | ||
|
|
||
| $response->header( 'X-WP-Total', $cloud_snippets->total_snippets ); | ||
| $response->header( 'X-WP-TotalPages', $cloud_snippets->total_pages ); | ||
|
|
||
| if ( ! empty( $cloud_snippets->available_filters ) ) { | ||
| $response->header( 'X-WP-Filters', wp_json_encode( $cloud_snippets->available_filters ) ); | ||
| } |
There was a problem hiding this comment.
get_items and get_featured_items both map snippets to arrays and set X-WP-Total, X-WP-TotalPages, X-WP-Filters. A private helper would reduce drift when one path is fixed and the other is not.
| * | ||
| * @return array<int, array{id: int, name: string, snippet_count: int}> List of types. | ||
| */ | ||
| public static function get_cloud_types(): array { |
There was a problem hiding this comment.
Cloud_API::get_cloud_types() / get_cloud_categories() and the new routes look unused by the React diff (filters come from headers).
| private function extract_filters( WP_REST_Request $request ): array { | ||
| return array_filter( | ||
| [ | ||
| 'category' => $request->get_param( 'category' ) ?? '', | ||
| 'type' => $request->get_param( 'type' ) ?? '', | ||
| 'status' => $request->get_param( 'status' ) ?? '', | ||
| ] | ||
| ); | ||
| } |
There was a problem hiding this comment.
Double check the tab spacings here
Summary
Community Snippets page now shows featured snippets on initial load and supports server-side filtering with category, type, and status dropdowns. Filter options come from the full query result via independent facets.
Featured Snippets
Server-Side Filters
New Endpoints Proxied
Cloud Snippet Model
UI
Test plan