Commit 600427826ac for woocommerce
commit 600427826ac2a594b006b4761ebc967f06b33f8d
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Jun 1 16:52:23 2026 +0700
Fix non-visual attribute term responses (#65432)
* Fix non-visual attribute term responses
* Add non-visual attribute term docs example
* Make visual attribute term data opt-in
diff --git a/docs/apis/store-api/resources-endpoints/product-attribute-terms.md b/docs/apis/store-api/resources-endpoints/product-attribute-terms.md
index d921828127c..99840e410ca 100644
--- a/docs/apis/store-api/resources-endpoints/product-attribute-terms.md
+++ b/docs/apis/store-api/resources-endpoints/product-attribute-terms.md
@@ -5,17 +5,29 @@ GET /products/attributes/:id/terms
GET /products/attributes/:id/terms?orderby=slug
```
-| Attribute | Type | Required | Description |
-| :-------- | :------ | :------: |:--------------------------------------------------------------------------------------------------------------|
-| `id` | integer | Yes | The ID of the attribute to retrieve terms for. |
-| `order` | string | no | Order ascending or descending. Allowed values: `asc`, `desc` |
-| `orderby` | string | no | Sort collection by object attribute. Allowed values: `id`, `name`, `name_num`, `slug`, `count`, `menu_order`. |
+| Attribute | Type | Required | Description |
+| :---------------------- | :------ | :------: | :------------------------------------------------------------------------------------------------------------ |
+| `id` | integer | Yes | The ID of the attribute to retrieve terms for. |
+| `order` | string | no | Order ascending or descending. Allowed values: `asc`, `desc` |
+| `orderby` | string | no | Sort collection by object attribute. Allowed values: `id`, `name`, `name_num`, `slug`, `count`, `menu_order`. |
+| `__experimental_visual` | boolean | no | If true, include experimental visual swatch data for `wc-visual` attribute terms. |
+
+## Visual response fields
+
+The following fields are included only when `__experimental_visual=true` is passed for `wc-visual` attribute terms.
+Other attribute types keep the default term response without `__experimentalVisual`.
+
+| Attribute | Type | Description |
+| :--------------------------- | :----- | :------------------------------------------------------------------------------------------------------------- |
+| `__experimentalVisual` | object | Experimental visual swatch data for `wc-visual` attribute terms. |
+| `__experimentalVisual.type` | string | Visual swatch type. Allowed values: `color`, `image`, `none`. |
+| `__experimentalVisual.value` | string | Visual swatch value. Returns a hex color for `color`, an image URL for `image`, or an empty string for `none`. |
```sh
-curl "https://example-store.com/wp-json/wc/store/v1/products/attributes/1/terms"
+curl "https://example-store.com/wp-json/wc/store/v1/products/attributes/1/terms?__experimental_visual=true"
```
-**Example response:**
+**Example response for visual attribute terms:**
```json
[
@@ -23,13 +35,40 @@ curl "https://example-store.com/wp-json/wc/store/v1/products/attributes/1/terms"
"id": 22,
"name": "Blue",
"slug": "blue",
- "count": 5
+ "description": "",
+ "parent": 0,
+ "count": 5,
+ "__experimentalVisual": {
+ "type": "color",
+ "value": "#1e73be"
+ }
},
{
"id": 48,
"name": "Burgundy",
"slug": "burgundy",
- "count": 1
+ "description": "",
+ "parent": 0,
+ "count": 1,
+ "__experimentalVisual": {
+ "type": "image",
+ "value": "https://example-store.com/wp-content/uploads/2026/06/burgundy-swatch.jpg"
+ }
+ }
+]
+```
+
+**Example response for non-visual attribute terms:**
+
+```json
+[
+ {
+ "id": 12,
+ "name": "Large",
+ "slug": "large",
+ "description": "",
+ "parent": 0,
+ "count": 7
}
]
```
diff --git a/plugins/woocommerce/changelog/fix-nonvisual-attribute-term-response b/plugins/woocommerce/changelog/fix-nonvisual-attribute-term-response
new file mode 100644
index 00000000000..aa28eb2d3f9
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-nonvisual-attribute-term-response
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep Store API visual attribute term data opt-in with `__experimental_visual`.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
index 6c7d5bdbcae..38fdc41d378 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
@@ -96,7 +96,11 @@ function AttributeItem( { blocks, isSelected, onSelect }: AttributeItemProps ) {
resourceName: 'products/attributes/terms',
resourceValues: [ attribute?.id || 0 ],
shouldSelect: !! attribute?.id && termIds.length > 0,
- query: { include: termIds, hide_empty: false },
+ query: {
+ include: termIds,
+ hide_empty: false,
+ __experimental_visual: true,
+ },
} );
const visualByTermId = useMemo( () => {
return attributeTerms.reduce< Record< number, VisualAttributeTerm > >(
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
index 745d5d045e4..228a4f43acb 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
@@ -65,7 +65,11 @@ const Edit = ( props: EditProps ) => {
resourceName: 'products/attributes/terms',
resourceValues: [ attributeObject?.id || 0 ],
shouldSelect: !! attributeObject?.id,
- query: { orderby: 'menu_order', hide_empty: hideEmpty },
+ query: {
+ orderby: 'menu_order',
+ hide_empty: hideEmpty,
+ __experimental_visual: true,
+ },
} );
const { data: filteredCounts, isLoading: isFilterCountsLoading } =
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php
index bcdb2cf271b..4c49889b830 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php
@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
+use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermMeta;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Schemas\V1\ProductAttributeTermSchema;
@@ -70,13 +71,52 @@ class ProductAttributeTerms extends AbstractTermsRoute {
* @return array
*/
public function get_collection_params() {
- $params = parent::get_collection_params();
- $params['orderby']['enum'][] = 'menu_order';
- $params['orderby']['enum'][] = 'name_num';
- $params['orderby']['enum'][] = 'id';
+ $params = parent::get_collection_params();
+ $params['orderby']['enum'][] = 'menu_order';
+ $params['orderby']['enum'][] = 'name_num';
+ $params['orderby']['enum'][] = 'id';
+ $params['__experimental_visual'] = array(
+ 'description' => __( 'If true, include experimental visual swatch data for wc-visual attribute terms.', 'woocommerce' ),
+ 'type' => 'boolean',
+ 'default' => false,
+ 'sanitize_callback' => 'wc_string_to_bool',
+ 'validate_callback' => 'rest_validate_request_arg',
+ );
return $params;
}
+ /**
+ * Prepare a single item for response.
+ *
+ * @param mixed $item Item to format to schema.
+ * @param \WP_REST_Request $request Request object.
+ *
+ * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
+ *
+ * @return \WP_REST_Response $response Response data.
+ */
+ public function prepare_item_for_response( $item, \WP_REST_Request $request ) {
+ $response = parent::prepare_item_for_response( $item, $request );
+
+ if (
+ ! wc_string_to_bool( $request['__experimental_visual'] ) ||
+ ! ( $item instanceof \WP_Term ) ||
+ ! VisualAttributeTermMeta::is_visual_attribute_taxonomy( $item->taxonomy )
+ ) {
+ return $response;
+ }
+
+ $data = $response->get_data();
+
+ $data[ ProductAttributeTermSchema::VISUAL_PROPERTY_NAME ] = VisualAttributeTermMeta::get_term_visual(
+ (int) $item->term_id
+ );
+
+ $response->set_data( $data );
+
+ return $response;
+ }
+
/**
* Get a collection of attribute terms.
*
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php
index c85b1f52cce..24ff4011d0a 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php
@@ -43,22 +43,6 @@ class ProductAttributeTermSchema extends TermSchema {
return $schema;
}
- /**
- * Convert a product attribute term object into an object suitable for the response.
- *
- * @param \WP_Term $term Term object.
- * @return array
- */
- public function get_item_response( $term ) {
- $response = parent::get_item_response( $term );
-
- $response[ self::VISUAL_PROPERTY_NAME ] = VisualAttributeTermMeta::is_visual_attribute_taxonomy( $term->taxonomy )
- ? VisualAttributeTermMeta::get_term_visual( (int) $term->term_id )
- : null;
-
- return $response;
- }
-
/**
* Get the visual data property schema.
*
@@ -67,7 +51,7 @@ class ProductAttributeTermSchema extends TermSchema {
private function get_visual_property_schema(): array {
return array(
'description' => __( 'Experimental visual swatch data for wc-visual attribute terms.', 'woocommerce' ),
- 'type' => array( 'object', 'null' ),
+ 'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductAttributeTerms.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductAttributeTerms.php
index 2e98bcee218..24a93092b24 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductAttributeTerms.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductAttributeTerms.php
@@ -81,6 +81,7 @@ class ProductAttributeTerms extends ControllerTestCase {
$this->assertArrayHasKey( 'slug', $data[0] );
$this->assertArrayHasKey( 'description', $data[0] );
$this->assertArrayHasKey( 'count', $data[0] );
+ $this->assertArrayNotHasKey( '__experimentalVisual', $data[0] );
}
/**
@@ -97,6 +98,7 @@ class ProductAttributeTerms extends ControllerTestCase {
$this->assertEquals( 'small-slug', $data['slug'] );
$this->assertEquals( 'Description of small', $data['description'] );
$this->assertEquals( 0, $data['count'] );
+ $this->assertArrayNotHasKey( '__experimentalVisual', $data );
}
/**
@@ -110,6 +112,7 @@ class ProductAttributeTerms extends ControllerTestCase {
$this->assertArrayHasKey( 'order', $params );
$this->assertArrayHasKey( 'orderby', $params );
$this->assertArrayHasKey( 'hide_empty', $params );
+ $this->assertArrayHasKey( '__experimental_visual', $params );
}
/**
@@ -119,12 +122,17 @@ class ProductAttributeTerms extends ControllerTestCase {
$routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
$controller = $routes->get( 'product-attribute-terms' );
$schema = $controller->get_item_schema();
- $response = $controller->prepare_item_for_response( get_term_by( 'name', 'small', 'pa_size' ), new \WP_REST_Request() );
- $data = $response->get_data();
- $validate = new ValidateSchema( $schema );
+ $request = new \WP_REST_Request();
+ $request->set_param( '__experimental_visual', true );
+ $response = $controller->prepare_item_for_response( get_term_by( 'name', 'red', 'pa_color' ), $request );
+ $data = $response->get_data();
+ $validate = new ValidateSchema( $schema );
$this->assertArrayHasKey( '__experimentalVisual', $data );
- $this->assertNull( $data['__experimentalVisual'] );
+ $this->assertSame( 'none', $data['__experimentalVisual']['type'] );
+ $this->assertSame( '', $data['__experimentalVisual']['value'] );
+
+ $data['__experimentalVisual'] = (object) $data['__experimentalVisual'];
$diff = $validate->get_diff_from_object( $data );
$this->assertEmpty( $diff, print_r( $diff, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
@@ -141,7 +149,10 @@ class ProductAttributeTerms extends ControllerTestCase {
$term = get_term_by( 'name', 'red', 'pa_color' );
update_term_meta( $term->term_id, 'color', '#00ff00' );
- $response = $controller->prepare_item_for_response( $term, new \WP_REST_Request() );
+ $request = new \WP_REST_Request();
+ $request->set_param( '__experimental_visual', true );
+
+ $response = $controller->prepare_item_for_response( $term, $request );
$data = $response->get_data();
$validate = new ValidateSchema( $schema );