Complete General tab for Single Variation page (#40633)

* Add description to the Variation details section

* Add Image section block to the Single Variation page

* Add support to the checkbox block for changing non boolean properties and extend it to use the entity context

* Register the product-variation-visibility block into the ProductVariationTemplate

* Null is a valid value but not updafined

* Fix type definitions

* Add changelog files

* Fix linter errors

* Add changelog file

* Change onValue and offValue for checkedValue and unchackedValue for the checkbox block to avoid event naming conflicts

* Set multiple value dynamically into the media uploader component

* Prevent uploading multiple files when multiple is set to false

* Remove cover label when multiple is set to false

* Fix DropZone min height in Image section

* Fix rebase conflict

* Fix linter error
This commit is contained in:
Maikel David Pérez Gómez 2023-10-10 09:42:26 -04:00 committed by GitHub
parent dab1457bba
commit e0a138b27b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 395 additions and 174 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Image gallery and media uploader now support initial selected images.

View File

@ -15,6 +15,7 @@ export type ImageGalleryItemProps = {
id?: string;
alt: string;
isCover?: boolean;
isDraggable?: boolean;
src: string;
displayToolbar?: boolean;
className?: string;
@ -26,6 +27,7 @@ export const ImageGalleryItem: React.FC< ImageGalleryItemProps > = ( {
id,
alt,
isCover = false,
isDraggable = true,
src,
className = '',
onClick = () => null,
@ -33,7 +35,7 @@ export const ImageGalleryItem: React.FC< ImageGalleryItemProps > = ( {
children,
}: ImageGalleryItemProps ) => (
<ConditionalWrapper
condition={ isCover }
condition={ ! isDraggable }
wrapper={ ( wrappedChildren ) => (
<NonSortableItem>{ wrappedChildren }</NonSortableItem>
) }
@ -48,15 +50,15 @@ export const ImageGalleryItem: React.FC< ImageGalleryItemProps > = ( {
>
{ children }
{ isCover ? (
<>
<Pill>{ __( 'Cover', 'woocommerce' ) }</Pill>
<img alt={ alt } src={ src } id={ id } />
</>
) : (
{ isDraggable ? (
<SortableHandle>
<img alt={ alt } src={ src } id={ id } />
</SortableHandle>
) : (
<>
{ isCover && <Pill>{ __( 'Cover', 'woocommerce' ) }</Pill> }
<img alt={ alt } src={ src } id={ id } />
</>
) }
</div>
</ConditionalWrapper>

View File

@ -25,6 +25,7 @@ import { ImageGalleryToolbarDropdown } from './image-gallery-toolbar-dropdown';
export type ImageGalleryToolbarProps = {
childIndex: number;
allowDragging?: boolean;
value?: number;
moveItem: ( fromIndex: number, toIndex: number ) => void;
removeItem: ( removeIndex: number ) => void;
replaceItem: (
@ -44,6 +45,7 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
replaceItem,
setToolBarItem,
lastChild,
value,
MediaUploadComponent = MediaUpload,
}: ImageGalleryToolbarProps ) => {
const moveNext = () => {
@ -105,6 +107,7 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
{ isCoverItem && (
<ToolbarGroup className="woocommerce-image-gallery__toolbar-media">
<MediaUploadComponent
value={ value }
onSelect={ ( media ) =>
replaceItem( childIndex, media as MediaItem )
}

View File

@ -100,12 +100,11 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
>
{ orderedChildren.map( ( child, childIndex ) => {
const isToolbarVisible = child.key === activeToolbarKey;
const isCoverItem = ( childIndex === 0 ) as boolean;
return cloneElement(
child,
{
isCover: isCoverItem,
isDraggable: allowDragging && ! child.props.isCover,
className: classnames( {
'is-toolbar-visible': isToolbarVisible,
} ),
@ -152,6 +151,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
},
isToolbarVisible ? (
<ImageGalleryToolbar
value={ child.props.id }
allowDragging={ allowDragging }
childIndex={ childIndex }
lastChild={

View File

@ -25,6 +25,7 @@ type MediaUploaderProps = {
props: MediaUpload.Props< T >
) => JSX.Element;
multipleSelect?: boolean | string;
value?: number | number[];
onSelect?: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: ( { id: number } & { [ k: string ]: any } ) | MediaItem[]
@ -35,8 +36,8 @@ type MediaUploaderProps = {
file: File;
} ) => void;
onMediaGalleryOpen?: () => void;
onUpload?: ( files: MediaItem[] ) => void;
onFileUploadChange?: ( files: MediaItem[] ) => void;
onUpload?: ( files: MediaItem | MediaItem[] ) => void;
onFileUploadChange?: ( files: MediaItem | MediaItem[] ) => void;
uploadMedia?: ( options: UploadMediaOptions ) => Promise< void >;
};
@ -48,6 +49,7 @@ export const MediaUploader = ( {
maxUploadFileSize = 10000000,
MediaUploadComponent = MediaUpload,
multipleSelect = false,
value,
onError = () => null,
onFileUploadChange = () => null,
onMediaGalleryOpen = () => null,
@ -58,16 +60,21 @@ export const MediaUploader = ( {
const getFormFileUploadAcceptedFiles = () =>
allowedMediaTypes.map( ( type ) => `${ type }/*` );
const multiple = Boolean( multipleSelect );
return (
<FormFileUpload
accept={ getFormFileUploadAcceptedFiles().toString() }
multiple={ true }
multiple={ multiple }
onChange={ ( { currentTarget } ) => {
uploadMedia( {
allowedTypes: allowedMediaTypes,
filesList: currentTarget.files as FileList,
onError,
onFileChange: onFileUploadChange,
maxUploadFileSize,
onError,
onFileChange( files ) {
onFileUploadChange( multiple ? files : files[ 0 ] );
},
} );
} }
render={ ( { openFileDialog } ) => (
@ -94,6 +101,7 @@ export const MediaUploader = ( {
</div>
<MediaUploadComponent
value={ value }
onSelect={ onSelect }
allowedTypes={ allowedMediaTypes }
// @ts-expect-error - TODO multiple also accepts string.
@ -113,12 +121,17 @@ export const MediaUploader = ( {
{ hasDropZone && (
<DropZone
onFilesDrop={ ( files ) =>
onFilesDrop={ ( droppedFiles ) =>
uploadMedia( {
filesList: files,
onError,
onFileChange: onUpload,
allowedTypes: allowedMediaTypes,
filesList: droppedFiles,
maxUploadFileSize,
onError,
onFileChange( files ) {
onUpload(
multiple ? files : files[ 0 ]
);
},
} )
}
/>

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Images block now supports one or multiple images. Checkbox block now supports another property type.

View File

@ -10,35 +10,53 @@ _Please note that to persist a custom field in the product it also needs to be a
### title
- **Type:** `String`
- **Required:** `No`
- **Type:** `String`
- **Required:** `No`
Header that appears above the checkbox.
### label
- **Type:** `String`
- **Required:** `No`
- **Type:** `String`
- **Required:** `No`
Label that appears at the side of the checkbox.
### property
- **Type:** `String`
- **Required:** `Yes`
- **Type:** `String`
- **Required:** `Yes`
Property in which the checkbox value is stored.
### tooltip
- **Type:** `String`
- **Required:** `No`
- **Type:** `String`
- **Required:** `No`
Tooltip text that is shown when hovering the icon at the side of the label.
### checkedValue
- **Type:** `String`
- **Required:** `No`
If it is set, the checked state will be `property` === `checkedValue`. When `onChange` is fired with the checked value set to `true` then the `property` value will be set to the one stored in `checkedValue`.
This is needed for cases where the `property` type is not a `boolean`.
### uncheckedValue
- **Type:** `String`
- **Required:** `No`
If it is set, the unchecked state will be `property` !== `checkedValue`. When `onChange` is fired with the checked value set to `false` then the `property` value will be set to the one stored in `uncheckedValue`.
This is needed for cases where the `property` type is not a `boolean`.
## Usage
Here's an example on the code that is used for the 'sold_individually' field in the Inventory section:
Here's an example on the code that is used for the `sold_individually` field in the Inventory section:
```php
$parent_container->add_block(
@ -65,3 +83,24 @@ $parent_container->add_block(
);
```
---
Here's an example that is used to toggle the product variation `status` from `publish` to `private`:
> In this case the checkbox will be checked when the variation `status === 'private'`, changing the checked state of the checkbox will toggle the `status` value from `private` to `publish` and not from `true` to `false` like in the previous example:
```php
$parent_container->add_block(
[
'id' => 'product-variation-visibility',
'blockName' => 'woocommerce/product-checkbox-field',
'order' => 30,
'attributes' => [
'property' => 'status',
'label' => __( 'Hide in product catalog', 'woocommerce' ),
'checkedValue' => 'private',
'uncheckedValue' => 'publish',
],
]
);
```

View File

@ -20,6 +20,12 @@
},
"tooltip": {
"type": "string"
},
"checkedValue": {
"type": "string"
},
"uncheckedValue": {
"type": "string"
}
},
"supports": {

View File

@ -2,7 +2,6 @@
* External dependencies
*/
import { createElement } from '@wordpress/element';
import type { BlockAttributes } from '@wordpress/blocks';
import { CheckboxControl, Tooltip } from '@wordpress/components';
import { Icon, help } from '@wordpress/icons';
import { useWooBlockProps } from '@woocommerce/block-templates';
@ -12,29 +11,51 @@ import { useWooBlockProps } from '@woocommerce/block-templates';
*/
import { ProductEditorBlockEditProps } from '../../../types';
import useProductEntityProp from '../../../hooks/use-product-entity-prop';
import { CheckboxBlockAttributes } from './types';
export function Edit( {
attributes,
context: { postType },
}: ProductEditorBlockEditProps< BlockAttributes > ) {
}: ProductEditorBlockEditProps< CheckboxBlockAttributes > ) {
const { property, title, label, tooltip, checkedValue, uncheckedValue } =
attributes;
const blockProps = useWooBlockProps( {
className: 'woocommerce-product-form__checkbox',
...attributes,
} );
const { property, title, label, tooltip } = attributes;
const [ value, setValue ] = useProductEntityProp< boolean >( property, {
postType,
fallbackValue: false,
} );
const [ value, setValue ] = useProductEntityProp< boolean | string | null >(
property,
{
postType,
fallbackValue: false,
}
);
function isChecked() {
if ( checkedValue !== undefined ) {
return checkedValue === value;
}
return value as boolean;
}
function handleChange( checked: boolean ) {
if ( checked ) {
setValue( checkedValue !== undefined ? checkedValue : checked );
} else {
setValue( uncheckedValue !== undefined ? uncheckedValue : checked );
}
}
return (
<div { ...blockProps }>
<h4>{ title }</h4>
{ title && <h4>{ title }</h4> }
<div className="woocommerce-product-form__checkbox-wrapper">
<CheckboxControl
label={ label }
checked={ value }
onChange={ ( selected ) => setValue( selected ) }
checked={ isChecked() }
onChange={ handleChange }
/>
{ tooltip && (
<Tooltip

View File

@ -0,0 +1,13 @@
/**
* External dependencies
*/
import type { BlockAttributes } from '@wordpress/blocks';
export interface CheckboxBlockAttributes extends BlockAttributes {
property: string;
title?: string;
label?: string;
tooltip?: string;
checkedValue?: string | null;
uncheckedValue?: string | null;
}

View File

@ -12,6 +12,13 @@
"type": "number",
"__experimentalRole": "content"
},
"property": {
"type": "string"
},
"multiple": {
"type": "boolean",
"default": true
},
"images": {
"__experimentalRole": "content",
"type": "array",
@ -30,5 +37,6 @@
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css"
"editorStyle": "file:./editor.css",
"usesContext": [ "postType" ]
}

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { DragEvent } from 'react';
import { __ } from '@wordpress/i18n';
import { BlockAttributes } from '@wordpress/blocks';
import { DropZone } from '@wordpress/components';
@ -25,18 +26,35 @@ import { useEntityProp } from '@wordpress/core-data';
*/
import { ProductEditorBlockEditProps } from '../../../types';
type Image = MediaItem & {
type UploadImage = {
id?: number;
} & Record< string, string >;
export interface Image {
id: number;
src: string;
};
name: string;
alt: string;
}
function mapUploadImageToImage( upload: UploadImage ): Image | null {
if ( ! upload.id ) return null;
return {
id: upload.id,
name: upload.title,
src: upload.url,
alt: upload.alt,
};
}
export function Edit( {
attributes,
context,
}: ProductEditorBlockEditProps< BlockAttributes > ) {
const [ images, setImages ] = useEntityProp< MediaItem[] >(
'postType',
'product',
'images'
);
const { property, multiple } = attributes;
const [ propertyValue, setPropertyValue ] = useEntityProp<
Image | Image[] | null
>( 'postType', context.postType, property );
const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] =
useState< boolean >( false );
const [ isRemoving, setIsRemoving ] = useState< boolean >( false );
@ -46,30 +64,134 @@ export function Edit( {
const blockProps = useWooBlockProps( attributes, {
className: classnames( {
'has-images': images.length > 0,
'has-images': Array.isArray( propertyValue )
? propertyValue.length > 0
: Boolean( propertyValue ),
} ),
} );
const toggleRemoveZone = () => {
setIsRemovingZoneVisible( ! isRemovingZoneVisible );
};
const orderImages = ( newOrder: JSX.Element[] ) => {
const orderedImages = newOrder.map( ( image ) => {
return images.find(
( file ) => file.id === parseInt( image?.props?.id, 10 )
function orderImages( newOrder: JSX.Element[] ) {
if ( Array.isArray( propertyValue ) ) {
const memoIds = propertyValue.reduce< Record< string, Image > >(
( current, item ) => ( {
...current,
[ `${ item.id }` ]: item,
} ),
{}
);
} );
recordEvent( 'product_images_change_image_order_via_image_gallery' );
setImages( orderedImages as MediaItem[] );
};
const orderedImages = newOrder
.filter( ( image ) => image?.props?.id in memoIds )
.map( ( image ) => memoIds[ image?.props?.id ] );
const onFileUpload = ( files: MediaItem[] ) => {
if ( files[ 0 ].id ) {
recordEvent( 'product_images_add_via_file_upload_area' );
setImages( [ ...images, ...files ] );
recordEvent(
'product_images_change_image_order_via_image_gallery'
);
setPropertyValue( orderedImages );
}
};
}
function uploadHandler( eventName: string ) {
return function handleFileUpload( upload: MediaItem | MediaItem[] ) {
recordEvent( eventName );
if ( Array.isArray( upload ) ) {
const images: Image[] = upload
.filter( ( image ) => image.id )
.map( ( image ) => ( {
id: image.id,
name: image.title,
src: image.url,
alt: image.alt,
} ) );
if ( upload[ 0 ]?.id ) {
setPropertyValue( [
...( propertyValue as Image[] ),
...images,
] );
}
} else if ( upload.id ) {
setPropertyValue( mapUploadImageToImage( upload ) );
}
};
}
function handleSelect( selection: UploadImage | UploadImage[] ) {
recordEvent( 'product_images_add_via_media_library' );
if ( Array.isArray( selection ) ) {
const images = selection
.map( mapUploadImageToImage )
.filter( ( image ) => image !== null );
setPropertyValue( images as Image[] );
} else {
setPropertyValue( mapUploadImageToImage( selection ) );
}
}
function handleDragStart( event: DragEvent< HTMLDivElement > ) {
if ( Array.isArray( propertyValue ) ) {
const { id: imageId, dataset } = event.target as HTMLElement;
if ( imageId ) {
setDraggedImageId( parseInt( imageId, 10 ) );
} else if ( dataset?.index ) {
const index = parseInt( dataset.index, 10 );
setDraggedImageId( propertyValue[ index ]?.id ?? null );
}
setIsRemovingZoneVisible( ( current ) => ! current );
}
}
function handleDragEnd() {
if ( Array.isArray( propertyValue ) ) {
if ( isRemoving && draggedImageId ) {
recordEvent( 'product_images_remove_image_button_click' );
setPropertyValue(
propertyValue.filter( ( img ) => img.id !== draggedImageId )
);
setIsRemoving( false );
setDraggedImageId( null );
}
setIsRemovingZoneVisible( ( current ) => ! current );
}
}
function handleReplace( {
replaceIndex,
media,
}: {
replaceIndex: number;
media: UploadImage;
} ) {
recordEvent( 'product_images_replace_image_button_click' );
if (
Array.isArray( propertyValue ) &&
! propertyValue.some( ( img ) => media.id === img.id )
) {
const image = mapUploadImageToImage( media );
if ( image ) {
const newImages = [ ...propertyValue ];
newImages[ replaceIndex ] = image;
setPropertyValue( newImages );
}
} else {
setPropertyValue( mapUploadImageToImage( media ) );
}
}
function handleRemove( { removedItem }: { removedItem: JSX.Element } ) {
recordEvent( 'product_images_remove_image_button_click' );
if ( Array.isArray( propertyValue ) ) {
const remainingImages = propertyValue.filter(
( image ) => image.id === removedItem.props.id
);
setPropertyValue( remainingImages );
} else {
setPropertyValue( null );
}
}
return (
<div { ...blockProps }>
@ -92,99 +214,56 @@ export function Edit( {
</div>
) : (
<MediaUploader
multipleSelect={ 'add' }
value={
Array.isArray( propertyValue )
? propertyValue.map( ( { id } ) => id )
: propertyValue?.id ?? undefined
}
multipleSelect={ multiple ? 'add' : false }
onError={ () => null }
onFileUploadChange={ onFileUpload }
onFileUploadChange={ uploadHandler(
'product_images_add_via_file_upload_area'
) }
onMediaGalleryOpen={ () => {
recordEvent( 'product_images_media_gallery_open' );
} }
onSelect={ ( files ) => {
const newImages = files.filter(
( img: Image ) =>
! images.find(
( image ) => image.id === img.id
)
);
if ( newImages.length > 0 ) {
recordEvent(
'product_images_add_via_media_library'
);
setImages( [ ...images, ...newImages ] );
}
} }
onUpload={ ( files ) => {
if ( files[ 0 ].id ) {
recordEvent(
'product_images_add_via_drag_and_drop_upload'
);
setImages( [ ...images, ...files ] );
}
} }
onSelect={ handleSelect }
onUpload={ uploadHandler(
'product_images_add_via_drag_and_drop_upload'
) }
label={ '' }
buttonText={ __( 'Choose an image', 'woocommerce' ) }
/>
) }
</div>
<ImageGallery
allowDragging={ false }
onDragStart={ ( event ) => {
const { id: imageId, dataset } =
event.target as HTMLElement;
if ( imageId ) {
setDraggedImageId( parseInt( imageId, 10 ) );
} else {
const index = dataset?.index;
if ( index ) {
setDraggedImageId(
images[ parseInt( index, 10 ) ]?.id
);
}
}
toggleRemoveZone();
} }
onDragEnd={ () => {
if ( isRemoving && draggedImageId ) {
{ propertyValue !== null && propertyValue !== undefined && (
<ImageGallery
allowDragging={ false }
onDragStart={ handleDragStart }
onDragEnd={ handleDragEnd }
onOrderChange={ orderImages }
onReplace={ handleReplace }
onRemove={ handleRemove }
onSelectAsCover={ () =>
recordEvent(
'product_images_remove_image_button_click'
);
setImages(
images.filter(
( img ) => img.id !== draggedImageId
)
);
setIsRemoving( false );
setDraggedImageId( null );
'product_images_select_image_as_cover_button_click'
)
}
toggleRemoveZone();
} }
onOrderChange={ orderImages }
onReplace={ ( { replaceIndex, media } ) => {
if (
images.find( ( img ) => media.id === img.id ) ===
undefined
) {
const newImages = [ ...images ];
newImages[ replaceIndex ] = media as MediaItem;
recordEvent(
'product_images_replace_image_button_click'
);
setImages( newImages );
}
} }
onSelectAsCover={ () =>
recordEvent(
'product_images_select_image_as_cover_button_click'
)
}
>
{ images.map( ( image ) => (
<ImageGalleryItem
key={ image.id || image.url }
alt={ image.alt }
src={ image.url || image.src }
id={ `${ image.id }` }
/>
) ) }
</ImageGallery>
>
{ ( Array.isArray( propertyValue )
? propertyValue
: [ propertyValue ]
).map( ( image, index ) => (
<ImageGalleryItem
key={ image.id }
alt={ image.alt }
src={ image.src }
id={ `${ image.id }` }
isCover={ multiple && index === 0 }
/>
) ) }
</ImageGallery>
) }
</div>
);
}

View File

@ -1,14 +1,20 @@
.wp-block-woocommerce-product-images-field {
.woocommerce-media-uploader {
text-align: left;
}
.woocommerce-media-uploader__label {
display: none;
}
.woocommerce-sortable {
margin-top: 0;
padding: 0;
}
.woocommerce-media-uploader {
text-align: left;
.components-drop-zone {
min-height: 222px;
}
}
.woocommerce-media-uploader__label {
display: none;
}
.woocommerce-sortable {
margin-top: 0;
padding: 0;
}
&.has-images {
.woocommerce-image-gallery {
@ -16,9 +22,9 @@
}
}
&:not(.has-images) {
.woocommerce-sortable {
display: none;
}
}
&:not(.has-images) {
.woocommerce-sortable {
display: none;
}
}
}

View File

@ -74,13 +74,16 @@ export function SingleImageField( {
role="region"
>
<MediaUploader
multipleSelect={ false }
onError={ () => null }
onSelect={ ( image ) =>
handleChange( image as MediaItem )
}
onUpload={ ( [ image ] ) => handleChange( image ) }
onFileUploadChange={ ( [ image ] ) =>
handleChange( image )
onUpload={ ( image ) =>
! Array.isArray( image ) && handleChange( image )
}
onFileUploadChange={ ( image ) =>
! Array.isArray( image ) && handleChange( image )
}
label={ __(
'Drag image here or click to upload',

View File

@ -48,8 +48,8 @@ export const ImagesGalleryField = () => {
recordEvent( 'product_images_change_image_order_via_image_gallery' );
setValue( 'images', orderedImages );
};
const onFileUpload = ( files: MediaItem[] ) => {
if ( files[ 0 ].id ) {
const onFileUpload = ( files: MediaItem | MediaItem[] ) => {
if ( Array.isArray( files ) && files[ 0 ].id ) {
recordEvent( 'product_images_add_via_file_upload_area' );
setValue( 'images', [ ...images, ...files ] );
}
@ -167,7 +167,7 @@ export const ImagesGalleryField = () => {
}
} }
onUpload={ ( files ) => {
if ( files[ 0 ].id ) {
if ( Array.isArray( files ) && files[ 0 ].id ) {
recordEvent(
'product_images_add_via_drag_and_drop_upload'
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Register image and visibility blocks into ProductVariationTemplate

View File

@ -103,16 +103,17 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
// Basic Details Section.
$basic_details = $general_group->add_section(
[
'id' => 'variation-details',
'id' => 'product-variation-details-section',
'order' => 10,
'attributes' => [
'title' => __( 'Variation details', 'woocommerce' ),
'title' => __( 'Variation details', 'woocommerce' ),
'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
],
]
);
$basic_details->add_block(
[
'id' => 'product-summary',
'id' => 'product-variation-note',
'blockName' => 'woocommerce/product-summary-field',
'order' => 20,
'attributes' => [
@ -122,6 +123,19 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
],
]
);
$basic_details->add_block(
[
'id' => 'product-variation-visibility',
'blockName' => 'woocommerce/product-checkbox-field',
'order' => 30,
'attributes' => [
'property' => 'status',
'label' => __( 'Hide in product catalog', 'woocommerce' ),
'checkedValue' => 'private',
'uncheckedValue' => 'publish',
],
]
);
// Images section.
$images_section = $general_group->add_section(
@ -129,7 +143,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'id' => 'product-variation-images-section',
'order' => 30,
'attributes' => [
'title' => __( 'Images', 'woocommerce' ),
'title' => __( 'Image', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
@ -141,11 +155,12 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
);
$images_section->add_block(
[
'id' => 'product-variation-images',
'id' => 'product-variation-image',
'blockName' => 'woocommerce/product-images-field',
'order' => 10,
'attributes' => [
'images' => [],
'property' => 'image',
'multiple' => false,
],
]
);

View File

@ -244,7 +244,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'blockName' => 'woocommerce/product-images-field',
'order' => 10,
'attributes' => [
'images' => [],
'images' => [],
'property' => 'images',
],
]
);