Add REST API Products featured image (#37815)

This commit is contained in:
Ron Rennick 2023-04-20 14:58:03 -03:00 committed by GitHub
commit 7073fea067
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 347 additions and 5 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Add `featured` field to the images array for Product REST API

View File

@ -45,9 +45,11 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
protected function get_images( $product ) { protected function get_images( $product ) {
$images = array(); $images = array();
$attachment_ids = array(); $attachment_ids = array();
$featured_id = null;
// Add featured image. // Add featured image.
if ( $product->get_image_id() ) { if ( $product->get_image_id() ) {
$featured_id = $product->get_image_id();
$attachment_ids[] = $product->get_image_id(); $attachment_ids[] = $product->get_image_id();
} }
@ -68,6 +70,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$images[] = array( $images[] = array(
'id' => (int) $attachment_id, 'id' => (int) $attachment_id,
'featured' => (int) $featured_id === (int) $attachment_id,
'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
@ -305,6 +308,31 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$images = is_array( $images ) ? array_filter( $images ) : array(); $images = is_array( $images ) ? array_filter( $images ) : array();
if ( ! empty( $images ) ) { if ( ! empty( $images ) ) {
$featured_image_index = 0;
$featured_image_count = 0;
// Collect featured field usage.
foreach ( $images as $index => $image ) {
if ( isset( $image['featured'] ) && $image['featured'] ) {
$featured_image_index = $index;
$featured_image_count++;
}
}
// Handle multiple featured images.
if ( $featured_image_count > 1 ) {
throw new WC_REST_Exception(
'woocommerce_rest_product_featured_image_count',
__( 'Only one featured image is allowed.', 'woocommerce' ),
400
);
}
// If no featured image is set, and the first image explicitly set to false do not set featured at all.
if ( 0 === $featured_image_count && isset( $images[0]['featured'] ) && false === $images[0]['featured'] ) {
$featured_image_index = null;
}
$gallery = array(); $gallery = array();
foreach ( $images as $index => $image ) { foreach ( $images as $index => $image ) {
@ -329,9 +357,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
} }
$featured_image = $product->get_image_id(); if ( $featured_image_index === $index ) {
if ( 0 === $index ) {
$product->set_image_id( $attachment_id ); $product->set_image_id( $attachment_id );
} else { } else {
$gallery[] = $attachment_id; $gallery[] = $attachment_id;
@ -1253,6 +1279,12 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
'type' => 'integer', 'type' => 'integer',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
), ),
'featured' => array(
'description' => __( 'Featured image.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'date_created' => array( 'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time', 'type' => 'date-time',

View File

@ -913,7 +913,6 @@ test.describe('Products API tests: CRUD', () => {
}); });
}); });
test.describe('Product tags tests: CRUD', () => { test.describe('Product tags tests: CRUD', () => {
let productTagId; let productTagId;
@ -1074,6 +1073,235 @@ test.describe('Products API tests: CRUD', () => {
}); });
}); });
test.describe( 'Product images tests: CRUD', () => {
let productId;
let images;
test( 'can add product with an image', async ( { request } ) => {
const response = await request.post( 'wp-json/wc/v3/products', {
data: {
images: [ { src: 'https://cldup.com/6L9h56D9Bw.jpg' } ],
},
} );
const responseJSON = await response.json();
expect( response.status() ).toEqual( 201 );
expect( responseJSON.images ).toHaveLength( 1 );
expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
expect( responseJSON.images[ 0 ].alt ).toEqual( '' );
expect( responseJSON.images[ 0 ].src ).toContain( '6L9h56D9Bw' );
// Cleanup: Delete the used product
await request.delete(
`wp-json/wc/v3/products/${ responseJSON.id }`,
{
data: {
force: true,
},
}
);
} );
test( 'can add product with multiple images (backward compatible)', async ( {
request,
} ) => {
const response = await request.post( 'wp-json/wc/v3/products', {
data: {
images: [
{ src: 'https://cldup.com/6L9h56D9Bw.jpg' },
{ src: 'https://cldup.com/Dr1Bczxq4q.png' },
],
},
} );
const responseJSON = await response.json();
productId = responseJSON.id;
images = responseJSON.images.map( ( image ) => image.id );
expect( response.status() ).toEqual( 201 );
expect( responseJSON.images ).toHaveLength( 2 );
expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
expect( responseJSON.images[ 1 ].name ).toContain( 'Dr1Bczxq4q' );
expect( responseJSON.images[ 1 ].featured ).toBeFalsy();
} );
test( 'can add product with multiple images (explicit featured)', async ( {
request,
} ) => {
const response = await request.post( 'wp-json/wc/v3/products', {
data: {
images: [
{
src: 'https://cldup.com/6L9h56D9Bw.jpg',
featured: false,
},
{
src: 'https://cldup.com/Dr1Bczxq4q.png',
featured: true,
},
],
},
} );
const responseJSON = await response.json();
expect( response.status() ).toEqual( 201 );
expect( responseJSON.images ).toHaveLength( 2 );
// When retrieving, the featured image is always the first one.
expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
expect( responseJSON.images[ 1 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 1 ].featured ).toBeFalsy();
// Cleanup: Delete the used product
await request.delete(
`wp-json/wc/v3/products/${ responseJSON.id }`,
{
data: {
force: true,
},
}
);
} );
test( 'can add product with multiple images (no featured)', async ( {
request,
} ) => {
const response = await request.post( 'wp-json/wc/v3/products', {
data: {
images: [
{
src: 'https://cldup.com/6L9h56D9Bw.jpg',
featured: false,
},
{
src: 'https://cldup.com/Dr1Bczxq4q.png',
featured: false,
},
],
},
} );
const responseJSON = await response.json();
expect( response.status() ).toEqual( 201 );
expect( responseJSON.images ).toHaveLength( 2 );
expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 0 ].alt ).toEqual( '' );
expect( responseJSON.images[ 0 ].featured ).toBeFalsy();
expect( responseJSON.images[ 1 ].name ).toContain( 'Dr1Bczxq4q' );
expect( responseJSON.images[ 1 ].alt ).toEqual( '' );
expect( responseJSON.images[ 1 ].featured ).toBeFalsy();
// Cleanup: Delete the used product
await request.delete(
`wp-json/wc/v3/products/${ responseJSON.id }`,
{
data: {
force: true,
},
}
);
} );
test( 'cannot add product with multiple images (all featured)', async ( {
request,
} ) => {
const response = await request.post( 'wp-json/wc/v3/products', {
data: {
images: [
{
src: 'https://cldup.com/6L9h56D9Bw.jpg',
featured: true,
},
{
src: 'https://cldup.com/Dr1Bczxq4q.png',
featured: true,
},
],
},
} );
const responseJSON = await response.json();
expect( response.status() ).toEqual( 400 );
expect( responseJSON.code ).toEqual(
'woocommerce_rest_product_featured_image_count'
);
expect( responseJSON.message ).toEqual(
'Only one featured image is allowed.'
);
expect( responseJSON.data ).toEqual( { status: 400 } );
} );
test( 'can retrieve product images', async ( { request } ) => {
const response = await request.get(
`wp-json/wc/v3/products/${ productId }`
);
const responseJSON = await response.json();
expect( response.status() ).toEqual( 200 );
expect( responseJSON.images ).toHaveLength( 2 );
expect( responseJSON.images[ 0 ].id ).toEqual( images[ 0 ] );
expect( responseJSON.images[ 0 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 0 ].alt ).toEqual( '' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
} );
test( 'can update a product images', async ( { request } ) => {
// call API to update a product
const response = await request.put(
`wp-json/wc/v3/products/${ productId }`,
{
data: {
images: [
{ id: images[ 0 ], featured: false },
{ id: images[ 1 ], featured: true },
],
},
}
);
const responseJSON = await response.json();
expect( response.status() ).toEqual( 200 );
expect( responseJSON.images ).toHaveLength( 2 );
// When retrieving, the featured image is always the first one.
expect( responseJSON.images[ 0 ].id ).toEqual( images[ 1 ] );
expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' );
expect( responseJSON.images[ 0 ].alt ).toEqual( '' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
expect( responseJSON.images[ 1 ].id ).toEqual( images[ 0 ] );
expect( responseJSON.images[ 1 ].name ).toContain( '6L9h56D9Bw' );
expect( responseJSON.images[ 1 ].alt ).toEqual( '' );
expect( responseJSON.images[ 1 ].featured ).toBeFalsy();
} );
test( 'can remove an image from a product', async ( { request } ) => {
// Delete the product attribute.
const response = await request.put(
`wp-json/wc/v3/products/${ productId }`,
{
data: {
images: [ { id: images[ 1 ] } ],
},
}
);
const responseJSON = await response.json();
expect( response.status() ).toEqual( 200 );
expect( responseJSON.images ).toHaveLength( 1 );
expect( responseJSON.images[ 0 ].id ).toEqual( images[ 1 ] );
expect( responseJSON.images[ 0 ].name ).toContain( 'Dr1Bczxq4q' );
expect( responseJSON.images[ 0 ].alt ).toEqual( '' );
expect( responseJSON.images[ 0 ].featured ).toBeTruthy();
// Cleanup: Delete the used product
await request.delete( `wp-json/wc/v3/products/${ productId }`, {
data: {
force: true,
},
} );
} );
} );
test('can add a virtual product', async ({ test('can add a virtual product', async ({
request request
}) => { }) => {

View File

@ -204,6 +204,84 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
} }
} }
/**
* Test that featured field exists under images in product argument.
*/
public function test_products_args_includes_featured_field() {
$request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products' );
$request->set_param( '_fields', 'endpoints.1.args.images.items.properties' );
$response = $this->server->dispatch( $request );
$image_arg = $response->get_data()['endpoints'][1]['args']['images']['items']['properties'];
$this->assertArrayHasKey( 'featured', $image_arg );
$this->assertEquals(
array(
'description' => 'Featured image.',
'type' => 'boolean',
'default' => false,
'context' =>
array(
0 => 'view',
1 => 'edit',
),
),
$image_arg['featured']
);
}
/**
* Test that featured field exists under images in product schema.
*/
public function test_products_schema_includes_featured_field() {
$request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products' );
$request->set_param( '_fields', 'schema.properties.images.items.properties' );
$response = $this->server->dispatch( $request );
$image_schema = $response->get_data()['schema']['properties']['images']['items']['properties'];
$this->assertArrayHasKey( 'featured', $image_schema );
$this->assertEquals(
array(
'description' => 'Featured image.',
'type' => 'boolean',
'default' => false,
'context' =>
array(
0 => 'view',
1 => 'edit',
),
),
$image_schema['featured']
);
}
/**
* Test that feature field is returned inside images.
*/
public function test_products_get_images_array_contains_featured() {
global $wpdb;
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$gallery_url = media_sideload_image( 'https://cldup.com/6L9h56D9Bw.jpg', $product->get_id(), 'gallery', 'src' );
$this->assertNotWPError( $gallery_url );
$gallery_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $gallery_url ) );
$featured_url = media_sideload_image( 'https://cldup.com/Dr1Bczxq4q.png', $product->get_id(), 'featured', 'src' );
$this->assertNotWPError( $featured_url );
$featured_id = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $featured_url ) );
$product->set_image_id( $featured_id[0] );
$product->set_gallery_image_ids( $gallery_id[0] );
$product->save();
$request = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() );
$request->set_param( '_fields', 'images' );
$response = $this->server->dispatch( $request );
$images_array = $response->get_data()['images'];
$this->assertEquals( 200, $response->get_status() );
$this->assertTrue( $images_array[0]['featured'] );
$this->assertEquals( 'featured', $images_array[0]['name'] );
$this->assertFalse( $images_array[1]['featured'] );
$this->assertEquals( 'gallery', $images_array[1]['name'] );
}
/** /**
* Test that the `search` parameter does partial matching in the product name, but not the SKU. * Test that the `search` parameter does partial matching in the product name, but not the SKU.
* *