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:
parent
dab1457bba
commit
e0a138b27b
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Image gallery and media uploader now support initial selected images.
|
|
@ -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>
|
||||
|
|
|
@ -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 )
|
||||
}
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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 ]
|
||||
);
|
||||
},
|
||||
} )
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Images block now supports one or multiple images. Checkbox block now supports another property type.
|
|
@ -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',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
},
|
||||
"tooltip": {
|
||||
"type": "string"
|
||||
},
|
||||
"checkedValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"uncheckedValue": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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" ]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Register image and visibility blocks into ProductVariationTemplate
|
|
@ -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,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
|
|
@ -244,7 +244,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
|||
'blockName' => 'woocommerce/product-images-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'images' => [],
|
||||
'images' => [],
|
||||
'property' => 'images',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue