diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx
index 9e33ef62b..a9038bbf2 100644
--- a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx
+++ b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx
@@ -100,7 +100,7 @@ const SearchResults = () => {
if (isErrored) {
return (
-
{__('An error occurred while fetching search results. Please try again.')}
+
{__('An error occurred while fetching search results. Please try again.', 'code-snippets')}
)
}
diff --git a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx
index d895f8e3a..e337aa409 100644
--- a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx
+++ b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx
@@ -114,7 +114,7 @@ const DownloadOrViewButton: React.FC = ({
if (localSnippetId) {
return (
-
+
{__('View', 'code-snippets')}
)
diff --git a/src/js/types/Window.ts b/src/js/types/Window.ts
index ad58b119a..001293444 100644
--- a/src/js/types/Window.ts
+++ b/src/js/types/Window.ts
@@ -43,7 +43,6 @@ declare global {
welcome: string
settings: string
cloud: string
- connectCloud: string
}
banner: {
key: string
diff --git a/src/php/Client/Cloud_API.php b/src/php/Client/Cloud_API.php
index 3ca6d078e..abf973cdb 100644
--- a/src/php/Client/Cloud_API.php
+++ b/src/php/Client/Cloud_API.php
@@ -359,17 +359,14 @@ public function update_snippet_from_cloud( Cloud_Snippet $snippet_to_store ): ar
}
/**
- * Transient key for cached cloud types (languages).
- */
- private const TYPES_TRANSIENT_KEY = 'cs_cloud_types';
-
- /**
- * Transient key for cached cloud categories.
+ * Option key holding the current featured-snippets cache version.
+ *
+ * Bumped on flush so old transient keys become unreachable and expire naturally.
*/
- private const CATEGORIES_TRANSIENT_KEY = 'cs_cloud_categories';
+ private const FEATURED_VERSION_OPTION = 'cs_featured_cache_version';
/**
- * Transient key for cached featured snippets.
+ * Base transient key for cached featured snippets.
*/
private const FEATURED_TRANSIENT_KEY = 'cs_featured_snippets';
@@ -378,6 +375,40 @@ public function update_snippet_from_cloud( Cloud_Snippet $snippet_to_store ): ar
*/
private const FEATURED_MIN_TTL = 3600;
+ /**
+ * Get the current featured-snippets cache version, initialising it if absent.
+ *
+ * @return int
+ */
+ private static function get_featured_cache_version(): string {
+ $version = get_transient( self::FEATURED_VERSION_OPTION );
+
+ if ( ! $version ) {
+ $version = (string) ( microtime( true ) * 1000 );
+ set_transient( self::FEATURED_VERSION_OPTION, $version, MONTH_IN_SECONDS );
+ }
+
+ return $version;
+ }
+
+ /**
+ * Build the transient key for a specific (version, page, per_page, filters) slot.
+ *
+ * @param int $page Page number (1-indexed).
+ * @param int $per_page Results per page.
+ * @param array $filters Filter values.
+ *
+ * @return string
+ */
+ private static function build_featured_cache_key( int $page, int $per_page, array $filters ): string {
+ $active_filters = array_filter( $filters );
+ $encoded = wp_json_encode( $active_filters );
+ $filter_hash = md5( false === $encoded ? '' : $encoded );
+ $version = self::get_featured_cache_version();
+
+ return self::FEATURED_TRANSIENT_KEY . "_v{$version}_p{$page}_pp{$per_page}_{$filter_hash}";
+ }
+
/**
* Retrieve featured snippets from the cloud API, with transient caching.
*
@@ -389,9 +420,7 @@ public function update_snippet_from_cloud( Cloud_Snippet $snippet_to_store ): ar
*/
public static function get_featured_snippets( int $page = 1, int $per_page = 10, array $filters = [] ): Cloud_Snippets {
$per_page = min( self::MAX_RESULTS_PER_PAGE, max( 1, $per_page ) );
- $encoded = wp_json_encode( $filters );
- $filter_hash = md5( false === $encoded ? '' : $encoded );
- $cache_key = self::FEATURED_TRANSIENT_KEY . "_p{$page}_pp{$per_page}_{$filter_hash}";
+ $cache_key = self::build_featured_cache_key( $page, $per_page, $filters );
$cached = get_transient( $cache_key );
@@ -445,89 +474,20 @@ public static function get_featured_snippets( int $page = 1, int $per_page = 10,
return $result;
}
- /**
- * Retrieve available snippet types (languages) from the cloud API, with transient caching.
- *
- * @return array List of types.
- */
- public static function get_cloud_types(): array {
- $cached = get_transient( self::TYPES_TRANSIENT_KEY );
-
- if ( is_array( $cached ) ) {
- return $cached;
- }
-
- $url = self::get_cloud_api_url() . 'public/types';
- $response = wp_remote_get( $url );
-
- if ( is_wp_error( $response ) ) {
- return [];
- }
-
- $json = self::unpack_request_json( $response );
-
- if ( ! is_array( $json ) || ! isset( $json['data'] ) ) {
- return [];
- }
-
- $types = $json['data'];
- set_transient( self::TYPES_TRANSIENT_KEY, $types, DAY_IN_SECONDS );
-
- return $types;
- }
-
- /**
- * Retrieve available snippet categories from the cloud API, with transient caching.
- *
- * @return array List of categories.
- */
- public static function get_cloud_categories(): array {
- $cached = get_transient( self::CATEGORIES_TRANSIENT_KEY );
-
- if ( is_array( $cached ) ) {
- return $cached;
- }
-
- $url = self::get_cloud_api_url() . 'public/categories';
- $response = wp_remote_get( $url );
-
- if ( is_wp_error( $response ) ) {
- return [];
- }
-
- $json = self::unpack_request_json( $response );
-
- if ( ! is_array( $json ) || ! isset( $json['data'] ) ) {
- return [];
- }
-
- $categories = $json['data'];
- set_transient( self::CATEGORIES_TRANSIENT_KEY, $categories, DAY_IN_SECONDS );
-
- return $categories;
- }
-
/**
* Refresh the cached synced data.
*
+ * Bumps the featured-cache version counter so previously cached keys
+ * become unreachable and expire via WordPress's normal transient path.
+ *
* @return void
*/
public function clear_caches() {
- global $wpdb;
-
$this->cached_cloud_links = null;
delete_transient( self::CLOUD_MAP_TRANSIENT_KEY );
- delete_transient( self::TYPES_TRANSIENT_KEY );
- delete_transient( self::CATEGORIES_TRANSIENT_KEY );
delete_transient( 'cs_codevault_snippets' );
- $wpdb->query(
- $wpdb->prepare(
- "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
- $wpdb->esc_like( '_transient_' . self::FEATURED_TRANSIENT_KEY ) . '%',
- $wpdb->esc_like( '_transient_timeout_' . self::FEATURED_TRANSIENT_KEY ) . '%'
- )
- );
+ delete_transient( self::FEATURED_VERSION_OPTION );
}
}
diff --git a/src/php/Model/Cloud_Snippets.php b/src/php/Model/Cloud_Snippets.php
index 97d660377..301e383f3 100644
--- a/src/php/Model/Cloud_Snippets.php
+++ b/src/php/Model/Cloud_Snippets.php
@@ -15,7 +15,6 @@
* @property int $total_pages Total number of available pages of items.
* @property int $total_snippets Total number of available snippet items.
* @property array $cloud_id_rev An array of all cloud snippet IDs and their revision numbers.
- * @property bool $success If the request has any results.
* @property array $available_filters An array of available filters that can be applied to the collection.
*/
class Cloud_Snippets extends Model {
@@ -50,7 +49,7 @@ class Cloud_Snippets extends Model {
/**
* Class constructor.
*
- * @param Cloud_Snippet $initial_data Initial data.
+ * @param array|null $initial_data Initial data from the cloud API response.
*/
public function __construct( $initial_data = null ) {
parent::__construct( $this->normalize_cloud_api( $initial_data ) );
@@ -103,7 +102,6 @@ protected function prepare_snippets( $snippets ): array {
* @return mixed Normalized data array or original value when no normalization is required.
*/
private function normalize_cloud_api( $initial_data ) {
- // pagination metadata is nested under a 'meta' key.
if ( is_array( $initial_data ) && isset( $initial_data['meta'] ) ) {
$meta = $initial_data['meta'];
$normalized = [];
diff --git a/src/php/REST_API/Cloud/Cloud_Snippets_REST_Controller.php b/src/php/REST_API/Cloud/Cloud_Snippets_REST_Controller.php
index 79f699d96..b1d5afce0 100644
--- a/src/php/REST_API/Cloud/Cloud_Snippets_REST_Controller.php
+++ b/src/php/REST_API/Cloud/Cloud_Snippets_REST_Controller.php
@@ -32,22 +32,25 @@ final class Cloud_Snippets_REST_Controller extends Cloud_Collection_REST_Control
/**
* Common filter args shared across search and featured endpoints.
*
+ * Each filter accepts a single numeric ID (e.g. category=12). The cloud API
+ * resolves IDs to the underlying name/slug.
+ *
* @return array>
*/
private function get_filter_args(): array {
return [
'category' => [
- 'description' => esc_html__( 'Filter by category name (comma-separated).', 'code-snippets' ),
+ 'description' => esc_html__( 'Filter by category ID.', 'code-snippets' ),
'type' => 'string',
'default' => '',
],
'type' => [
- 'description' => esc_html__( 'Filter by language/type name (comma-separated).', 'code-snippets' ),
+ 'description' => esc_html__( 'Filter by language/type ID.', 'code-snippets' ),
'type' => 'string',
'default' => '',
],
'status' => [
- 'description' => esc_html__( 'Filter by status ID (comma-separated).', 'code-snippets' ),
+ 'description' => esc_html__( 'Filter by status ID.', 'code-snippets' ),
'type' => 'string',
'default' => '',
],
@@ -155,30 +158,6 @@ public function register_routes() {
]
);
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/types',
- [
- [
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => [ $this, 'get_types' ],
- 'permission_callback' => [ $this, 'get_items_permissions_check' ],
- ],
- ]
- );
-
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/categories',
- [
- [
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => [ $this, 'get_categories' ],
- 'permission_callback' => [ $this, 'get_items_permissions_check' ],
- ],
- ]
- );
-
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P\d+)/download',
@@ -239,24 +218,6 @@ public function get_featured_items( WP_REST_Request $request ): WP_REST_Response
return $cloud_snippets->to_rest_response();
}
- /**
- * Retrieve available snippet types (languages) from the cloud API.
- *
- * @return WP_REST_Response
- */
- public function get_types(): WP_REST_Response {
- return rest_ensure_response( Cloud_API::get_cloud_types() );
- }
-
- /**
- * Retrieve available snippet categories from the cloud API.
- *
- * @return WP_REST_Response
- */
- public function get_categories(): WP_REST_Response {
- return rest_ensure_response( Cloud_API::get_cloud_categories() );
- }
-
/**
* Get the user's snippets per-page preference for Screen Options pagination.
*
diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts
index a0d3ade5e..53b4663aa 100644
--- a/tests/e2e/code-snippets-community-featured.spec.ts
+++ b/tests/e2e/code-snippets-community-featured.spec.ts
@@ -31,14 +31,13 @@ test.describe('Community Cloud Featured Snippets', () => {
.waitFor({ state: 'hidden', timeout: TIMEOUTS.DEFAULT })
.catch(() => undefined)
- const featuredHeading = page.locator('.cloud-featured-heading')
+ const featuredHeading = page.locator('.cloud-snippets-heading', { hasText: 'Featured Snippets' })
const headingVisible = await featuredHeading
.waitFor({ state: 'visible', timeout: TIMEOUTS.SHORT })
.then(() => true)
.catch(() => false)
if (headingVisible) {
- // Featured snippets loaded — verify the heading text.
await expect(featuredHeading).toContainText('Featured Snippets')
} else {
// Cloud API unreachable — verify no crash: the search form is still functional.
@@ -64,7 +63,7 @@ test.describe('Community Cloud Featured Snippets', () => {
.waitFor({ state: 'hidden', timeout: TIMEOUTS.DEFAULT })
.catch(() => undefined)
- // The "Featured Snippets" heading should no longer be visible.
- await expect(page.locator('.cloud-featured-heading')).not.toBeVisible()
+ // The "Featured Snippets" heading should no longer be visible (search-mode heading replaces it).
+ await expect(page.locator('.cloud-snippets-heading', { hasText: 'Featured Snippets' })).not.toBeVisible()
})
})
diff --git a/tests/phpunit/test-cloud-api-featured.php b/tests/phpunit/test-cloud-api-featured.php
index bf6531033..1bde6ed81 100644
--- a/tests/phpunit/test-cloud-api-featured.php
+++ b/tests/phpunit/test-cloud-api-featured.php
@@ -63,9 +63,14 @@ public function tear_down() {
* @return string
*/
private function transient_key( int $page = 1, int $per_page = 10, array $filters = [] ): string {
- $encoded = wp_json_encode( $filters );
+ $active_filters = array_filter( $filters );
+ $encoded = wp_json_encode( $active_filters );
$hash = md5( false === $encoded ? '' : $encoded );
- return "cs_featured_snippets_p{$page}_pp{$per_page}_{$hash}";
+ $version = get_transient( 'cs_featured_cache_version' );
+ if ( ! $version ) {
+ $version = (string) ( microtime( true ) * 1000 );
+ }
+ return "cs_featured_snippets_v{$version}_p{$page}_pp{$per_page}_{$hash}";
}
/**
@@ -75,6 +80,7 @@ private function transient_key( int $page = 1, int $per_page = 10, array $filter
*/
private function clear_featured_transients(): void {
delete_transient( $this->transient_key() );
+ delete_transient( 'cs_featured_cache_version' );
}
/**
@@ -340,4 +346,40 @@ public function test_available_filters_preserved(): void {
$this->assertArrayHasKey( 'types', $result->available_filters );
$this->assertArrayHasKey( 'statuses', $result->available_filters );
}
+
+ /**
+ * Calling clear_caches() causes the next get_featured_snippets() to miss cache and re-fetch.
+ *
+ * @return void
+ */
+ public function test_clear_caches_invalidates_featured_cache(): void {
+ Cloud_API::get_featured_snippets();
+ $this->assertSame( 1, $this->http_request_count );
+
+ $api = new Cloud_API();
+ $api->clear_caches();
+
+ Cloud_API::get_featured_snippets();
+ $this->assertSame( 2, $this->http_request_count, 'Expected a fresh HTTP request after cache invalidation.' );
+ }
+
+ /**
+ * Empty filter values produce the same cache key as omitted filters.
+ *
+ * @return void
+ */
+ public function test_empty_filters_produce_same_cache_key(): void {
+ $key_empty = $this->transient_key(
+ 1,
+ 10,
+ [
+ 'category' => '',
+ 'type' => '',
+ 'status' => '',
+ ]
+ );
+ $key_none = $this->transient_key( 1, 10, [] );
+
+ $this->assertSame( $key_none, $key_empty, 'Empty filter values should hash identically to no filters.' );
+ }
}