Migrating product editor images section to slot-fill (#36461)

This commit is contained in:
Joel Thiessen 2023-01-19 09:45:30 -08:00 committed by GitHub
parent 7e17a96914
commit 687dd6fdfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 305 additions and 258 deletions

View File

@ -0,0 +1,7 @@
export const PRODUCT_DETAILS_SLUG = 'product-details';
export const DETAILS_SECTION_ID = 'general/details';
export const IMAGES_SECTION_ID = 'general/images';
export const TAB_GENERAL_ID = 'tab/general';
export const PLUGIN_ID = 'woocommerce';

View File

@ -1,2 +0,0 @@
export const PRODUCT_DETAILS_SLUG = 'product-details';
export const DETAILS_SECTION_ID = 'general/details';

View File

@ -16,7 +16,7 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies * Internal dependencies
*/ */
import { getCheckboxTracks } from '../../sections/utils'; import { getCheckboxTracks } from '../../sections/utils';
import { PRODUCT_DETAILS_SLUG } from './index'; import { PRODUCT_DETAILS_SLUG } from '../constants';
export const DetailsFeatureField = () => { export const DetailsFeatureField = () => {
const { getCheckboxControlProps } = useFormContext< Product >(); const { getCheckboxControlProps } = useFormContext< Product >();

View File

@ -18,7 +18,7 @@ import { useState } from '@wordpress/element';
* Internal dependencies * Internal dependencies
*/ */
import { EditProductLinkModal } from '../../shared/edit-product-link-modal'; import { EditProductLinkModal } from '../../shared/edit-product-link-modal';
import { PRODUCT_DETAILS_SLUG } from './index'; import { PRODUCT_DETAILS_SLUG } from '../constants';
export const DetailsNameField = ( {} ) => { export const DetailsNameField = ( {} ) => {
const [ showProductLinkEditModal, setShowProductLinkEditModal ] = const [ showProductLinkEditModal, setShowProductLinkEditModal ] =

View File

@ -18,20 +18,22 @@ import {
DetailsFeatureField, DetailsFeatureField,
DetailsSummaryField, DetailsSummaryField,
DetailsDescriptionField, DetailsDescriptionField,
DETAILS_SECTION_ID,
} from './index'; } from './index';
import { DETAILS_SECTION_ID, PLUGIN_ID, TAB_GENERAL_ID } from '../constants';
import './product-details-section.scss'; import './product-details-section.scss';
const DetailsSection = () => ( const DetailsSection = () => (
<> <>
<WooProductSectionItem <WooProductSectionItem
id={ DETAILS_SECTION_ID } id={ DETAILS_SECTION_ID }
location="tab/general" location={ TAB_GENERAL_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 1 } order={ 1 }
> >
<ProductFieldSection <ProductFieldSection
id="general/details" id={ DETAILS_SECTION_ID }
title={ __( 'Product details', 'woocommerce' ) } title={ __( 'Product details', 'woocommerce' ) }
description={ __( description={ __(
'This info will be displayed on the product page, category pages, social media, and search results.', 'This info will be displayed on the product page, category pages, social media, and search results.',
@ -42,7 +44,7 @@ const DetailsSection = () => (
<WooProductFieldItem <WooProductFieldItem
id="details/name" id="details/name"
section={ DETAILS_SECTION_ID } section={ DETAILS_SECTION_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 1 } order={ 1 }
> >
<DetailsNameField /> <DetailsNameField />
@ -50,7 +52,7 @@ const DetailsSection = () => (
<WooProductFieldItem <WooProductFieldItem
id="details/categories" id="details/categories"
section={ DETAILS_SECTION_ID } section={ DETAILS_SECTION_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 3 } order={ 3 }
> >
<DetailsCategoriesField /> <DetailsCategoriesField />
@ -58,7 +60,7 @@ const DetailsSection = () => (
<WooProductFieldItem <WooProductFieldItem
id="details/feature" id="details/feature"
section={ DETAILS_SECTION_ID } section={ DETAILS_SECTION_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 5 } order={ 5 }
> >
<DetailsFeatureField /> <DetailsFeatureField />
@ -66,7 +68,7 @@ const DetailsSection = () => (
<WooProductFieldItem <WooProductFieldItem
id="details/summary" id="details/summary"
section={ DETAILS_SECTION_ID } section={ DETAILS_SECTION_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 7 } order={ 7 }
> >
<DetailsSummaryField /> <DetailsSummaryField />
@ -74,7 +76,7 @@ const DetailsSection = () => (
<WooProductFieldItem <WooProductFieldItem
id="details/description" id="details/description"
section={ DETAILS_SECTION_ID } section={ DETAILS_SECTION_ID }
pluginId="core" pluginId={ PLUGIN_ID }
order={ 9 } order={ 9 }
> >
<DetailsDescriptionField /> <DetailsDescriptionField />

View File

@ -3,4 +3,3 @@ export * from './details-field-categories';
export * from './details-field-feature'; export * from './details-field-feature';
export * from './details-field-summary'; export * from './details-field-summary';
export * from './details-field-description'; export * from './details-field-description';
export * from './constants';

View File

@ -0,0 +1,201 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
useFormContext,
MediaUploader,
ImageGallery,
ImageGalleryItem,
} from '@woocommerce/components';
import { CardBody, DropZone } from '@wordpress/components';
import { recordEvent } from '@woocommerce/tracks';
import { useState } from '@wordpress/element';
import { Product } from '@woocommerce/data';
import { Icon, trash } from '@wordpress/icons';
import { MediaItem } from '@wordpress/media-utils';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import DragAndDrop from '../../images/drag-and-drop.svg';
type Image = MediaItem & {
src: string;
};
export const ImagesGalleryField = () => {
const { getInputProps, setValue } = useFormContext< Product >();
const images = ( getInputProps( 'images' ).value as Image[] ) || [];
const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] =
useState< boolean >( false );
const [ isRemoving, setIsRemoving ] = useState< boolean >( false );
const [ draggedImageId, setDraggedImageId ] = useState< number | null >(
null
);
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 )
);
} );
recordEvent( 'product_images_change_image_order_via_image_gallery' );
setValue( 'images', orderedImages );
};
const onFileUpload = ( files: MediaItem[] ) => {
if ( files[ 0 ].id ) {
recordEvent( 'product_images_add_via_file_upload_area' );
setValue( 'images', [ ...images, ...files ] );
}
};
return (
<div
className={ classnames( 'woocommerce-product-form__images', {
'has-images': images.length > 0,
} ) }
>
<ImageGallery
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 ) {
recordEvent(
'product_images_remove_image_button_click'
);
setValue(
'images',
images.filter(
( img ) => img.id !== draggedImageId
)
);
setIsRemoving( false );
setDraggedImageId( null );
}
toggleRemoveZone();
} }
onOrderChange={ orderImages }
onReplace={ ( { replaceIndex, media } ) => {
if (
images.find( ( img ) => media.id === img.id ) ===
undefined
) {
images[ replaceIndex ] = media as Image;
recordEvent(
'product_images_replace_image_button_click'
);
setValue( 'images', images );
}
} }
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>
<div className="woocommerce-product-form__image-drop-zone">
{ isRemovingZoneVisible ? (
<CardBody>
<div className="woocommerce-product-form__remove-image-drop-zone">
<span>
<Icon
icon={ trash }
size={ 20 }
className="icon-control"
/>
{ __( 'Drop here to remove', 'woocommerce' ) }
</span>
<DropZone
onHTMLDrop={ () => setIsRemoving( true ) }
onDrop={ () => setIsRemoving( true ) }
label={ __(
'Drop here to remove',
'woocommerce'
) }
/>
</div>
</CardBody>
) : (
<CardBody>
<MediaUploader
multipleSelect={ true }
onError={ () => null }
onFileUploadChange={ onFileUpload }
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'
);
setValue( 'images', [
...images,
...newImages,
] );
}
} }
onUpload={ ( files ) => {
if ( files[ 0 ].id ) {
recordEvent(
'product_images_add_via_drag_and_drop_upload'
);
setValue( 'images', [
...images,
...files,
] );
}
} }
label={
<>
<img
src={ DragAndDrop }
alt={ __( 'Completed', 'woocommerce' ) }
className="woocommerce-product-form__drag-and-drop-image"
/>
<span>
{ __(
'Drag images here or click to upload',
'woocommerce'
) }
</span>
</>
}
/>
</CardBody>
) }
</div>
</div>
);
};

View File

@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
__experimentalWooProductSectionItem as WooProductSectionItem,
__experimentalWooProductFieldItem as WooProductFieldItem,
__experimentalProductFieldSection as ProductFieldSection,
Link,
} from '@woocommerce/components';
import { registerPlugin } from '@wordpress/plugins';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { ImagesGalleryField } from './index';
import { IMAGES_SECTION_ID, TAB_GENERAL_ID, PLUGIN_ID } from '../constants';
import './images-section.scss';
const ImagesSection = () => (
<>
<WooProductSectionItem
id={ IMAGES_SECTION_ID }
location={ TAB_GENERAL_ID }
pluginId={ PLUGIN_ID }
order={ 3 }
>
<ProductFieldSection
id={ IMAGES_SECTION_ID }
title={ __( 'Images', 'woocommerce' ) }
description={
<>
<span>
{ __(
'For best results, use JPEG files that are 1000 by 1000 pixels or larger.',
'woocommerce'
) }
</span>
<Link
className="woocommerce-form-section__header-link"
href="https://woocommerce.com/posts/fast-high-quality-product-photos/"
target="_blank"
type="external"
onClick={ () => {
recordEvent( 'prepare_images_help' );
} }
>
{ __(
'How should I prepare images?',
'woocommerce'
) }
</Link>
</>
}
/>
</WooProductSectionItem>
<WooProductFieldItem
id="images/gallery"
section={ IMAGES_SECTION_ID }
pluginId={ PLUGIN_ID }
order={ 1 }
>
<ImagesGalleryField />
</WooProductFieldItem>
</>
);
registerPlugin( 'wc-admin-product-editor-images-section', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-product-editor',
render: () => <ImagesSection />,
} );

View File

@ -0,0 +1 @@
export * from './images-field-gallery';

View File

@ -4,3 +4,4 @@
import './product-form-fills'; import './product-form-fills';
export * from './details-section/details-section-fills'; export * from './details-section/details-section-fills';
export * from './images-section/images-section-fills';

View File

@ -20,12 +20,12 @@ import { ProductInventorySection } from './sections/product-inventory-section';
import { PricingSection } from './sections/pricing-section'; import { PricingSection } from './sections/pricing-section';
import { ProductShippingSection } from './sections/product-shipping-section'; import { ProductShippingSection } from './sections/product-shipping-section';
import { ProductVariationsSection } from './sections/product-variations-section'; import { ProductVariationsSection } from './sections/product-variations-section';
import { ImagesSection } from './sections/images-section';
import { validate } from './product-validation'; import { validate } from './product-validation';
import { AttributesSection } from './sections/attributes-section'; import { AttributesSection } from './sections/attributes-section';
import { OptionsSection } from './sections/options-section'; import { OptionsSection } from './sections/options-section';
import { ProductFormFooter } from './layout/product-form-footer'; import { ProductFormFooter } from './layout/product-form-footer';
import { ProductFormTab } from './product-form-tab'; import { ProductFormTab } from './product-form-tab';
import { TAB_GENERAL_ID } from './fills/constants';
export const ProductForm: React.FC< { export const ProductForm: React.FC< {
product?: PartialProduct; product?: PartialProduct;
@ -50,8 +50,9 @@ export const ProductForm: React.FC< {
<ProductFormHeader /> <ProductFormHeader />
<ProductFormLayout> <ProductFormLayout>
<ProductFormTab name="general" title="General"> <ProductFormTab name="general" title="General">
<WooProductSectionItem.Slot location="tab/general" /> <WooProductSectionItem.Slot
<ImagesSection /> location={ TAB_GENERAL_ID }
/>
<AttributesSection /> <AttributesSection />
</ProductFormTab> </ProductFormTab>
<ProductFormTab <ProductFormTab

View File

@ -1,241 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
Link,
useFormContext,
MediaUploader,
ImageGallery,
ImageGalleryItem,
} from '@woocommerce/components';
import { Card, CardBody, DropZone } from '@wordpress/components';
import { recordEvent } from '@woocommerce/tracks';
import { useState } from '@wordpress/element';
import { Product } from '@woocommerce/data';
import classnames from 'classnames';
import { Icon, trash } from '@wordpress/icons';
import { MediaItem } from '@wordpress/media-utils';
/**
* Internal dependencies
*/
import { ProductSectionLayout } from '../layout/product-section-layout';
import DragAndDrop from '../images/drag-and-drop.svg';
import './images-section.scss';
type Image = MediaItem & {
src: string;
};
export const ImagesSection: React.FC = () => {
const { getInputProps, setValue } = useFormContext< Product >();
const images = ( getInputProps( 'images' ).value as Image[] ) || [];
const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] =
useState< boolean >( false );
const [ isRemoving, setIsRemoving ] = useState< boolean >( false );
const [ draggedImageId, setDraggedImageId ] = useState< number | null >(
null
);
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 )
);
} );
recordEvent( 'product_images_change_image_order_via_image_gallery' );
setValue( 'images', orderedImages );
};
const onFileUpload = ( files: MediaItem[] ) => {
if ( files[ 0 ].id ) {
recordEvent( 'product_images_add_via_file_upload_area' );
setValue( 'images', [ ...images, ...files ] );
}
};
return (
<ProductSectionLayout
title={ __( 'Images', 'woocommerce' ) }
description={
<>
<span>
{ __(
'For best results, use JPEG files that are 1000 by 1000 pixels or larger.',
'woocommerce'
) }
</span>
<Link
className="woocommerce-form-section__header-link"
href="https://woocommerce.com/posts/fast-high-quality-product-photos/"
target="_blank"
type="external"
onClick={ () => {
recordEvent( 'prepare_images_help' );
} }
>
{ __( 'How should I prepare images?', 'woocommerce' ) }
</Link>
</>
}
>
<Card
className={ classnames( 'woocommerce-product-form__images', {
'has-images': images.length > 0,
} ) }
>
<CardBody>
<ImageGallery
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 ) {
recordEvent(
'product_images_remove_image_button_click'
);
setValue(
'images',
images.filter(
( img ) => img.id !== draggedImageId
)
);
setIsRemoving( false );
setDraggedImageId( null );
}
toggleRemoveZone();
} }
onOrderChange={ orderImages }
onReplace={ ( { replaceIndex, media } ) => {
if (
images.find(
( img ) => media.id === img.id
) === undefined
) {
images[ replaceIndex ] = media as Image;
recordEvent(
'product_images_replace_image_button_click'
);
setValue( 'images', images );
}
} }
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>
<div className="woocommerce-product-form__image-drop-zone">
{ isRemovingZoneVisible ? (
<CardBody>
<div className="woocommerce-product-form__remove-image-drop-zone">
<span>
<Icon
icon={ trash }
size={ 20 }
className="icon-control"
/>
{ __(
'Drop here to remove',
'woocommerce'
) }
</span>
<DropZone
onHTMLDrop={ () =>
setIsRemoving( true )
}
onDrop={ () => setIsRemoving( true ) }
label={ __(
'Drop here to remove',
'woocommerce'
) }
/>
</div>
</CardBody>
) : (
<CardBody>
<MediaUploader
multipleSelect={ true }
onError={ () => null }
onFileUploadChange={ onFileUpload }
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'
);
setValue( 'images', [
...images,
...newImages,
] );
}
} }
onUpload={ ( files ) => {
if ( files[ 0 ].id ) {
recordEvent(
'product_images_add_via_drag_and_drop_upload'
);
setValue( 'images', [
...images,
...files,
] );
}
} }
label={
<>
<img
src={ DragAndDrop }
alt={ __(
'Completed',
'woocommerce'
) }
className="woocommerce-product-form__drag-and-drop-image"
/>
<span>
{ __(
'Drag images here or click to upload',
'woocommerce'
) }
</span>
</>
}
/>
</CardBody>
) }
</div>
</CardBody>
</Card>
</ProductSectionLayout>
);
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Using slotfill to insert images section in product editor.