Commit a0754eadc8d for woocommerce
commit a0754eadc8d8eec735f0c72599f313e4d7d206ea
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Nov 9 15:10:10 2026 +0700
Add a catalog visibility filter to the products list (#70684)
* Add catalog visibility filter to the products list
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a9b3-3f57-712a-9cb5-c3b33121dc7b
Co-authored-by: Amp <amp@ampcode.com>
* Add changelog entry for the catalog visibility filter
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a9b3-3f57-712a-9cb5-c3b33121dc7b
Co-authored-by: Amp <amp@ampcode.com>
* Only offer catalog visibility values the filter can match
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a9b3-3f57-712a-9cb5-c3b33121dc7b
* Create catalog visibility test products once
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a9b3-3f57-712a-9cb5-c3b33121dc7b
Co-authored-by: Amp <amp@ampcode.com>
---------
Co-authored-by: Amp <amp@ampcode.com>
diff --git a/plugins/woocommerce/changelog/70260-products-list-catalog-visibility-filter b/plugins/woocommerce/changelog/70260-products-list-catalog-visibility-filter
new file mode 100644
index 00000000000..33c607dae14
--- /dev/null
+++ b/plugins/woocommerce/changelog/70260-products-list-catalog-visibility-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a "Filter by visibility" dropdown to Products > All Products, so you can find hidden products.
diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
index 34187e7c324..e769532edc6 100644
--- a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
+++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
@@ -6,6 +6,7 @@
* @version 3.3.0
*/
+use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
@@ -494,9 +495,10 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
$filters = apply_filters(
'woocommerce_products_admin_list_table_filters',
array(
- 'product_category' => array( $this, 'render_products_category_filter' ),
- 'product_type' => array( $this, 'render_products_type_filter' ),
- 'stock_status' => array( $this, 'render_products_stock_status_filter' ),
+ 'product_category' => array( $this, 'render_products_category_filter' ),
+ 'product_type' => array( $this, 'render_products_type_filter' ),
+ 'stock_status' => array( $this, 'render_products_stock_status_filter' ),
+ 'catalog_visibility' => array( $this, 'render_products_catalog_visibility_filter' ),
)
);
@@ -596,6 +598,54 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $output contains only fixed markup and escaped option values.
}
+ /**
+ * Render the catalog visibility filter for the list table.
+ *
+ * @since 11.4.0
+ */
+ private function render_products_catalog_visibility_filter(): void {
+ $current = $this->get_requested_catalog_visibility();
+ $output = '<select name="catalog_visibility"><option value="">' . esc_html__( 'Filter by visibility', 'woocommerce' ) . '</option>';
+
+ foreach ( $this->get_catalog_visibility_filter_options() as $value => $label ) {
+ $output .= '<option ' . selected( $value, $current, false ) . ' value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
+ }
+
+ $output .= '</select>';
+ echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $output contains only fixed markup and escaped option values.
+ }
+
+ /**
+ * Get the catalog visibility the merchant picked in the filter.
+ *
+ * Returns an empty string when the request has no value or an unknown value. The list then shows all products.
+ *
+ * @return string
+ */
+ private function get_requested_catalog_visibility(): string {
+ $value = wp_unslash( $_GET['catalog_visibility'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only filter. The code below checks the value against a fixed list.
+ if ( is_array( $value ) ) {
+ $value = reset( $value ); // The dropdown sends one value, so use the first one if the request has several.
+ }
+
+ return is_string( $value ) && array_key_exists( $value, $this->get_catalog_visibility_filter_options() ) ? $value : '';
+ }
+
+ /**
+ * Get the catalog visibility options the filter can list products by.
+ *
+ * Keeps the labels and removals from the woocommerce_product_visibility_options filter, but drops values that
+ * extensions add. The product data store only saves the four core values, so the query cannot match any other value.
+ *
+ * @return array<string, string> Option labels keyed by catalog visibility value.
+ */
+ private function get_catalog_visibility_filter_options(): array {
+ $options = wc_get_product_visibility_options();
+ $supported = array( CatalogVisibility::VISIBLE, CatalogVisibility::CATALOG, CatalogVisibility::SEARCH, CatalogVisibility::HIDDEN );
+
+ return is_array( $options ) ? array_intersect_key( $options, array_flip( $supported ) ) : array();
+ }
+
/**
* Search by SKU or ID for products.
*
@@ -698,6 +748,29 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
add_filter( 'posts_clauses', array( $this, 'filter_stock_status_post_clauses' ) );
}
+ // Catalog visibility filter. The product data store saves each visibility as two terms, so check both to match one visibility exactly.
+ $catalog_visibility = $this->get_requested_catalog_visibility();
+ if ( '' !== $catalog_visibility ) {
+ $excluded_from_catalog = in_array( $catalog_visibility, array( CatalogVisibility::SEARCH, CatalogVisibility::HIDDEN ), true );
+ $excluded_from_search = in_array( $catalog_visibility, array( CatalogVisibility::CATALOG, CatalogVisibility::HIDDEN ), true );
+
+ $query_vars['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'relation' => 'AND',
+ array(
+ 'taxonomy' => 'product_visibility',
+ 'field' => 'slug',
+ 'terms' => 'exclude-from-catalog',
+ 'operator' => $excluded_from_catalog ? 'IN' : 'NOT IN',
+ ),
+ array(
+ 'taxonomy' => 'product_visibility',
+ 'field' => 'slug',
+ 'terms' => 'exclude-from-search',
+ 'operator' => $excluded_from_search ? 'IN' : 'NOT IN',
+ ),
+ );
+ }
+
// Search using CRUD.
if ( ! empty( $query_vars['s'] ) ) {
$search_term = wc_clean( wp_unslash( $query_vars['s'] ) );
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
index ba86d76fe0f..f6378f88e96 100644
--- a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
@@ -7,6 +7,7 @@
declare( strict_types = 1 );
+use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\ProductStockStatus;
require_once WC_ABSPATH . 'includes/admin/list-tables/class-wc-admin-list-table-products.php';
@@ -712,6 +713,41 @@ class WC_Admin_List_Table_Products_Test extends WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox Products list catalog visibility filter lists only products with exactly the chosen visibility.
+ */
+ public function test_catalog_visibility_filter_matches_exact_visibility(): void {
+ $visibilities = array( CatalogVisibility::VISIBLE, CatalogVisibility::CATALOG, CatalogVisibility::SEARCH, CatalogVisibility::HIDDEN );
+ $product_ids = array();
+ foreach ( $visibilities as $visibility ) {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_catalog_visibility( $visibility );
+ $product_ids[ $visibility ] = $product->save();
+ }
+
+ $original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Test cleanup restores the raw original request data.
+
+ try {
+ foreach ( $visibilities as $visibility ) {
+ $_GET['catalog_visibility'] = $visibility;
+
+ $query_vars = $this->sut->request_query(
+ array(
+ 'post_type' => 'product',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ )
+ );
+ $results = array_map( 'intval', ( new WP_Query( $query_vars ) )->get_posts() );
+
+ $this->assertSame( array( $product_ids[ $visibility ] ), $results, "Filtering by \"{$visibility}\" lists the wrong products." );
+ }
+ } finally {
+ $_GET = $original_get; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+ }
+
/**
* @testdox Products list shipping-class filter lists a variable product whose variation uses the class.
*/