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
*/
import { getCheckboxTracks } from '../../sections/utils';
import { PRODUCT_DETAILS_SLUG } from './index';
import { PRODUCT_DETAILS_SLUG } from '../constants';
export const DetailsFeatureField = () => {
const { getCheckboxControlProps } = useFormContext< Product >();

View File

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

View File

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

View File

@ -3,4 +3,3 @@ export * from './details-field-categories';
export * from './details-field-feature';
export * from './details-field-summary';
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';
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 { ProductShippingSection } from './sections/product-shipping-section';
import { ProductVariationsSection } from './sections/product-variations-section';
import { ImagesSection } from './sections/images-section';
import { validate } from './product-validation';
import { AttributesSection } from './sections/attributes-section';
import { OptionsSection } from './sections/options-section';
import { ProductFormFooter } from './layout/product-form-footer';
import { ProductFormTab } from './product-form-tab';
import { TAB_GENERAL_ID } from './fills/constants';
export const ProductForm: React.FC< {
product?: PartialProduct;
@ -50,8 +50,9 @@ export const ProductForm: React.FC< {
<ProductFormHeader />
<ProductFormLayout>
<ProductFormTab name="general" title="General">
<WooProductSectionItem.Slot location="tab/general" />
<ImagesSection />
<WooProductSectionItem.Slot
location={ TAB_GENERAL_ID }
/>
<AttributesSection />
</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.