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 './plugins/types';
|
||||
export * from './products/types';
|
||||
export { ProductVariation } from './product-variations/types';
|
||||
export type {
|
||||
ProductVariation,
|
||||
ProductVariationAttribute,
|
||||
ProductVariationImage,
|
||||
} from './product-variations/types';
|
||||
export {
|
||||
QueryProductAttribute,
|
||||
ProductAttributeSelectors,
|
||||
|
|
|
@ -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' >;
|
||||
|
|
|
@ -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 { 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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add product variation image
|
Loading…
Reference in New Issue