Add product variation image (#36133)
* Convert getCheckboxTracks into generic function because of a type mismatch * Add image to product variation and export types * Add single image field * Integrate SingleImageField in variation details section * Add changelog file * Add comment suggestions * Fix set image onFileUploadChange
This commit is contained in:
parent
ed6b0c841b
commit
dd94bb78ee
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add image to product variation and export types
|
|
@ -77,7 +77,11 @@ export * from './countries/types';
|
||||||
export * from './onboarding/types';
|
export * from './onboarding/types';
|
||||||
export * from './plugins/types';
|
export * from './plugins/types';
|
||||||
export * from './products/types';
|
export * from './products/types';
|
||||||
export { ProductVariation } from './product-variations/types';
|
export type {
|
||||||
|
ProductVariation,
|
||||||
|
ProductVariationAttribute,
|
||||||
|
ProductVariationImage,
|
||||||
|
} from './product-variations/types';
|
||||||
export {
|
export {
|
||||||
QueryProductAttribute,
|
QueryProductAttribute,
|
||||||
ProductAttributeSelectors,
|
ProductAttributeSelectors,
|
||||||
|
|
|
@ -15,11 +15,53 @@ export type ProductVariationAttribute = {
|
||||||
option: string;
|
option: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product variation - Image properties
|
||||||
|
*/
|
||||||
|
export interface ProductVariationImage {
|
||||||
|
/**
|
||||||
|
* Image ID.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The date the image was created, in the site's timezone.
|
||||||
|
*/
|
||||||
|
readonly date_created: string;
|
||||||
|
/**
|
||||||
|
* The date the image was created, as GMT.
|
||||||
|
*/
|
||||||
|
readonly date_created_gmt: string;
|
||||||
|
/**
|
||||||
|
* The date the image was last modified, in the site's timezone.
|
||||||
|
*/
|
||||||
|
readonly date_modified: string;
|
||||||
|
/**
|
||||||
|
* The date the image was last modified, as GMT.
|
||||||
|
*/
|
||||||
|
readonly date_modified_gmt: string;
|
||||||
|
/**
|
||||||
|
* Image URL.
|
||||||
|
*/
|
||||||
|
src: string;
|
||||||
|
/**
|
||||||
|
* Image name.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Image alternative text.
|
||||||
|
*/
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ProductVariation = Omit<
|
export type ProductVariation = Omit<
|
||||||
Product,
|
Product,
|
||||||
'name' | 'slug' | 'attributes'
|
'name' | 'slug' | 'attributes' | 'images'
|
||||||
> & {
|
> & {
|
||||||
attributes: ProductVariationAttribute[];
|
attributes: ProductVariationAttribute[];
|
||||||
|
/**
|
||||||
|
* Variation image data.
|
||||||
|
*/
|
||||||
|
image?: ProductVariationImage;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Query = Omit< ProductQuery, 'name' >;
|
type Query = Omit< ProductQuery, 'name' >;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './single-image-field';
|
|
@ -0,0 +1,28 @@
|
||||||
|
.woocommerce-single-image-field {
|
||||||
|
&__gallery {
|
||||||
|
margin-top: $gap-smaller;
|
||||||
|
|
||||||
|
.woocommerce-image-gallery .woocommerce-sortable {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__drop-zone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: calc($gap * 2) 0;
|
||||||
|
margin-top: $gap-smaller;
|
||||||
|
gap: calc($gap * 2);
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
min-height: 144px;
|
||||||
|
|
||||||
|
background: $white;
|
||||||
|
border: 1px dashed $gray-700;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
MediaUploader,
|
||||||
|
ImageGallery,
|
||||||
|
ImageGalleryItem,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { MediaItem } from '@wordpress/media-utils';
|
||||||
|
import uniqueId from 'lodash/uniqueId';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './single-image-field.scss';
|
||||||
|
|
||||||
|
export function SingleImageField( {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: SingleImageFieldProps ) {
|
||||||
|
const fieldId = id ?? uniqueId();
|
||||||
|
|
||||||
|
function handleChange( image?: MediaItem ) {
|
||||||
|
if ( typeof onChange === 'function' ) {
|
||||||
|
onChange( image );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{ ...props }
|
||||||
|
className={ classNames(
|
||||||
|
'woocommerce-single-image-field',
|
||||||
|
className
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor={ fieldId }
|
||||||
|
className="components-base-control__label woocommerce-single-image-field__label"
|
||||||
|
>
|
||||||
|
{ label }
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{ value ? (
|
||||||
|
<div
|
||||||
|
id={ fieldId }
|
||||||
|
className="woocommerce-single-image-field__gallery"
|
||||||
|
tabIndex={ -1 }
|
||||||
|
role="region"
|
||||||
|
>
|
||||||
|
<ImageGallery
|
||||||
|
onReplace={ ( { media } ) => handleChange( media ) }
|
||||||
|
onRemove={ () => handleChange( undefined ) }
|
||||||
|
>
|
||||||
|
<ImageGalleryItem
|
||||||
|
key={ value.id }
|
||||||
|
id={ String( value.id ) }
|
||||||
|
alt={ value.alt }
|
||||||
|
src={ value.url }
|
||||||
|
/>
|
||||||
|
</ImageGallery>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
id={ fieldId }
|
||||||
|
className="woocommerce-single-image-field__drop-zone"
|
||||||
|
tabIndex={ -1 }
|
||||||
|
role="region"
|
||||||
|
>
|
||||||
|
<MediaUploader
|
||||||
|
onError={ () => null }
|
||||||
|
onSelect={ ( image ) =>
|
||||||
|
handleChange( image as MediaItem )
|
||||||
|
}
|
||||||
|
onUpload={ ( [ image ] ) => handleChange( image ) }
|
||||||
|
onFileUploadChange={ ( [ image ] ) =>
|
||||||
|
handleChange( image )
|
||||||
|
}
|
||||||
|
label={ __(
|
||||||
|
'Drag image here or click to upload',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
buttonText={ __( 'Choose image', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SingleImageFieldProps = Omit<
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes< HTMLDivElement >,
|
||||||
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
'onChange'
|
||||||
|
> & {
|
||||||
|
label: string;
|
||||||
|
value?: MediaItem;
|
||||||
|
onChange?( value?: MediaItem ): void;
|
||||||
|
};
|
|
@ -3,13 +3,19 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { BlockInstance, serialize, parse } from '@wordpress/blocks';
|
import { BlockInstance, serialize, parse } from '@wordpress/blocks';
|
||||||
import { CheckboxControl, Card, CardBody } from '@wordpress/components';
|
import {
|
||||||
|
CheckboxControl,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
BaseControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { MediaItem } from '@wordpress/media-utils';
|
||||||
import {
|
import {
|
||||||
useFormContext,
|
useFormContext,
|
||||||
__experimentalRichTextEditor as RichTextEditor,
|
__experimentalRichTextEditor as RichTextEditor,
|
||||||
__experimentalTooltip as Tooltip,
|
__experimentalTooltip as Tooltip,
|
||||||
} from '@woocommerce/components';
|
} from '@woocommerce/components';
|
||||||
import { ProductVariation } from '@woocommerce/data';
|
import { ProductVariation, ProductVariationImage } from '@woocommerce/data';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,15 +23,42 @@ import { useState } from '@wordpress/element';
|
||||||
*/
|
*/
|
||||||
import { getCheckboxTracks } from './utils';
|
import { getCheckboxTracks } from './utils';
|
||||||
import { ProductSectionLayout } from '../layout/product-section-layout';
|
import { ProductSectionLayout } from '../layout/product-section-layout';
|
||||||
|
import { SingleImageField } from '../fields/single-image-field';
|
||||||
|
|
||||||
|
function parseVariationImage(
|
||||||
|
media?: MediaItem
|
||||||
|
): ProductVariationImage | undefined {
|
||||||
|
if ( ! media ) return undefined;
|
||||||
|
return {
|
||||||
|
id: media.id,
|
||||||
|
src: media.url,
|
||||||
|
alt: media.alt,
|
||||||
|
name: media.title,
|
||||||
|
} as ProductVariationImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVariationImage(
|
||||||
|
image?: ProductVariationImage
|
||||||
|
): MediaItem | undefined {
|
||||||
|
if ( ! image ) return undefined;
|
||||||
|
return {
|
||||||
|
id: image.id,
|
||||||
|
url: image.src,
|
||||||
|
alt: image.alt,
|
||||||
|
title: image.name,
|
||||||
|
} as MediaItem;
|
||||||
|
}
|
||||||
|
|
||||||
export const ProductVariationDetailsSection: React.FC = () => {
|
export const ProductVariationDetailsSection: React.FC = () => {
|
||||||
const { getCheckboxControlProps, values, setValue } =
|
const { getCheckboxControlProps, getInputProps, values, setValue } =
|
||||||
useFormContext< ProductVariation >();
|
useFormContext< ProductVariation >();
|
||||||
|
|
||||||
const [ descriptionBlocks, setDescriptionBlocks ] = useState<
|
const [ descriptionBlocks, setDescriptionBlocks ] = useState<
|
||||||
BlockInstance[]
|
BlockInstance[]
|
||||||
>( parse( values.description || '' ) );
|
>( parse( values.description || '' ) );
|
||||||
|
|
||||||
|
const imageFieldProps = getInputProps( 'image' );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductSectionLayout
|
<ProductSectionLayout
|
||||||
title={ __( 'Variant details', 'woocommerce' ) }
|
title={ __( 'Variant details', 'woocommerce' ) }
|
||||||
|
@ -50,7 +83,7 @@ export const ProductVariationDetailsSection: React.FC = () => {
|
||||||
}
|
}
|
||||||
{ ...getCheckboxControlProps(
|
{ ...getCheckboxControlProps(
|
||||||
'status',
|
'status',
|
||||||
getCheckboxTracks( 'status' )
|
getCheckboxTracks< ProductVariation >( 'status' )
|
||||||
) }
|
) }
|
||||||
checked={ values.status === 'publish' }
|
checked={ values.status === 'publish' }
|
||||||
onChange={ () =>
|
onChange={ () =>
|
||||||
|
@ -77,6 +110,21 @@ export const ProductVariationDetailsSection: React.FC = () => {
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
) }
|
) }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BaseControl id="product-variation-image">
|
||||||
|
<SingleImageField
|
||||||
|
label={ __( 'Image', 'woocommerce' ) }
|
||||||
|
value={ formatVariationImage(
|
||||||
|
imageFieldProps.value as ProductVariationImage
|
||||||
|
) }
|
||||||
|
onChange={ ( media ) =>
|
||||||
|
setValue(
|
||||||
|
'image',
|
||||||
|
parseVariationImage( media )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</BaseControl>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</ProductSectionLayout>
|
</ProductSectionLayout>
|
||||||
|
|
|
@ -23,22 +23,20 @@ type CurrencyConfig = {
|
||||||
/**
|
/**
|
||||||
* Get additional props to be passed to all checkbox inputs.
|
* Get additional props to be passed to all checkbox inputs.
|
||||||
*
|
*
|
||||||
* @param {string} name Name of the checkbox
|
* @param name Name of the checkbox.
|
||||||
* @return {Object} Props.
|
* @return Props.
|
||||||
*/
|
*/
|
||||||
export const getCheckboxTracks = ( name: string ) => {
|
export function getCheckboxTracks< T = Product >( name: string ) {
|
||||||
return {
|
return {
|
||||||
onChange: (
|
onChange: (
|
||||||
isChecked:
|
isChecked: ChangeEvent< HTMLInputElement > | T[ keyof T ]
|
||||||
| ChangeEvent< HTMLInputElement >
|
|
||||||
| Product[ keyof Product ]
|
|
||||||
) => {
|
) => {
|
||||||
recordEvent( `product_checkbox_${ name }`, {
|
recordEvent( `product_checkbox_${ name }`, {
|
||||||
checked: isChecked,
|
checked: isChecked,
|
||||||
} );
|
} );
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get input props for currency related values and symbol positions.
|
* Get input props for currency related values and symbol positions.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add product variation image
|
Loading…
Reference in New Issue