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:
Maikel David Pérez Gómez 2022-12-23 15:28:44 -03:00 committed by GitHub
parent ed6b0c841b
commit dd94bb78ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 13 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add image to product variation and export types

View File

@ -77,7 +77,11 @@ export * from './countries/types';
export * from './onboarding/types';
export * from './plugins/types';
export * from './products/types';
export { ProductVariation } from './product-variations/types';
export type {
ProductVariation,
ProductVariationAttribute,
ProductVariationImage,
} from './product-variations/types';
export {
QueryProductAttribute,
ProductAttributeSelectors,

View File

@ -15,11 +15,53 @@ export type ProductVariationAttribute = {
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<
Product,
'name' | 'slug' | 'attributes'
'name' | 'slug' | 'attributes' | 'images'
> & {
attributes: ProductVariationAttribute[];
/**
* Variation image data.
*/
image?: ProductVariationImage;
};
type Query = Omit< ProductQuery, 'name' >;

View File

@ -0,0 +1 @@
export * from './single-image-field';

View File

@ -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;
}
}

View File

@ -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;
};

View File

@ -3,13 +3,19 @@
*/
import { __ } from '@wordpress/i18n';
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 {
useFormContext,
__experimentalRichTextEditor as RichTextEditor,
__experimentalTooltip as Tooltip,
} from '@woocommerce/components';
import { ProductVariation } from '@woocommerce/data';
import { ProductVariation, ProductVariationImage } from '@woocommerce/data';
import { useState } from '@wordpress/element';
/**
@ -17,15 +23,42 @@ import { useState } from '@wordpress/element';
*/
import { getCheckboxTracks } from './utils';
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 = () => {
const { getCheckboxControlProps, values, setValue } =
const { getCheckboxControlProps, getInputProps, values, setValue } =
useFormContext< ProductVariation >();
const [ descriptionBlocks, setDescriptionBlocks ] = useState<
BlockInstance[]
>( parse( values.description || '' ) );
const imageFieldProps = getInputProps( 'image' );
return (
<ProductSectionLayout
title={ __( 'Variant details', 'woocommerce' ) }
@ -50,7 +83,7 @@ export const ProductVariationDetailsSection: React.FC = () => {
}
{ ...getCheckboxControlProps(
'status',
getCheckboxTracks( 'status' )
getCheckboxTracks< ProductVariation >( 'status' )
) }
checked={ values.status === 'publish' }
onChange={ () =>
@ -77,6 +110,21 @@ export const ProductVariationDetailsSection: React.FC = () => {
'woocommerce'
) }
/>
<BaseControl id="product-variation-image">
<SingleImageField
label={ __( 'Image', 'woocommerce' ) }
value={ formatVariationImage(
imageFieldProps.value as ProductVariationImage
) }
onChange={ ( media ) =>
setValue(
'image',
parseVariationImage( media )
)
}
/>
</BaseControl>
</CardBody>
</Card>
</ProductSectionLayout>

View File

@ -23,22 +23,20 @@ type CurrencyConfig = {
/**
* Get additional props to be passed to all checkbox inputs.
*
* @param {string} name Name of the checkbox
* @return {Object} Props.
* @param name Name of the checkbox.
* @return Props.
*/
export const getCheckboxTracks = ( name: string ) => {
export function getCheckboxTracks< T = Product >( name: string ) {
return {
onChange: (
isChecked:
| ChangeEvent< HTMLInputElement >
| Product[ keyof Product ]
isChecked: ChangeEvent< HTMLInputElement > | T[ keyof T ]
) => {
recordEvent( `product_checkbox_${ name }`, {
checked: isChecked,
} );
},
};
};
}
/**
* Get input props for currency related values and symbol positions.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product variation image