From 687dd6fdfee364034158c6ebbf9bf653ee8c0291 Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Thu, 19 Jan 2023 09:45:30 -0800 Subject: [PATCH] Migrating product editor images section to slot-fill (#36461) --- .../client/products/fills/constants.ts | 7 + .../fills/details-section/constants.ts | 2 - .../details-section/details-field-feature.tsx | 2 +- .../details-section/details-field-name.tsx | 2 +- .../details-section/details-section-fills.tsx | 20 +- .../products/fills/details-section/index.ts | 1 - .../images-section/images-field-gallery.tsx | 201 +++++++++++++++ .../images-section/images-section-fills.tsx | 74 ++++++ .../images-section}/images-section.scss | 0 .../products/fills/images-section/index.ts | 1 + .../client/products/fills/index.ts | 1 + .../client/products/product-form.tsx | 7 +- .../products/sections/images-section.tsx | 241 ------------------ .../changelog/add-36417-mvp-images-slotfill | 4 + 14 files changed, 305 insertions(+), 258 deletions(-) create mode 100644 plugins/woocommerce-admin/client/products/fills/constants.ts delete mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/constants.ts create mode 100644 plugins/woocommerce-admin/client/products/fills/images-section/images-field-gallery.tsx create mode 100644 plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx rename plugins/woocommerce-admin/client/products/{sections => fills/images-section}/images-section.scss (100%) create mode 100644 plugins/woocommerce-admin/client/products/fills/images-section/index.ts delete mode 100644 plugins/woocommerce-admin/client/products/sections/images-section.tsx create mode 100644 plugins/woocommerce/changelog/add-36417-mvp-images-slotfill diff --git a/plugins/woocommerce-admin/client/products/fills/constants.ts b/plugins/woocommerce-admin/client/products/fills/constants.ts new file mode 100644 index 00000000000..ad4ca43284e --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/constants.ts @@ -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'; diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts b/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts deleted file mode 100644 index 4404df12093..00000000000 --- a/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const PRODUCT_DETAILS_SLUG = 'product-details'; -export const DETAILS_SECTION_ID = 'general/details'; diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx index 350871b4c03..afbae52275d 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx @@ -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 >(); diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx index c8513e6c755..0fb92254221 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx @@ -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 ] = diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx index bf203331e30..9eefdae7b0d 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx @@ -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 = () => ( <> ( @@ -50,7 +52,7 @@ const DetailsSection = () => ( @@ -58,7 +60,7 @@ const DetailsSection = () => ( @@ -66,7 +68,7 @@ const DetailsSection = () => ( @@ -74,7 +76,7 @@ const DetailsSection = () => ( diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/index.ts b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts index ece2335fc8d..1e342c4c1c1 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts @@ -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'; diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/images-field-gallery.tsx b/plugins/woocommerce-admin/client/products/fills/images-section/images-field-gallery.tsx new file mode 100644 index 00000000000..1a9ba1f2ebd --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/images-section/images-field-gallery.tsx @@ -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 ( +
0, + } ) } + > + { + 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 ) => ( + + ) ) } + +
+ { isRemovingZoneVisible ? ( + +
+ + + { __( 'Drop here to remove', 'woocommerce' ) } + + setIsRemoving( true ) } + onDrop={ () => setIsRemoving( true ) } + label={ __( + 'Drop here to remove', + 'woocommerce' + ) } + /> +
+
+ ) : ( + + 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={ + <> + { + + { __( + 'Drag images here or click to upload', + 'woocommerce' + ) } + + + } + /> + + ) } +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx new file mode 100644 index 00000000000..01b735d0627 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx @@ -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 = () => ( + <> + + + + { __( + 'For best results, use JPEG files that are 1000 by 1000 pixels or larger.', + 'woocommerce' + ) } + + { + recordEvent( 'prepare_images_help' ); + } } + > + { __( + 'How should I prepare images?', + 'woocommerce' + ) } + + + } + /> + + + + + +); + +registerPlugin( 'wc-admin-product-editor-images-section', { + // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. + scope: 'woocommerce-product-editor', + render: () => , +} ); diff --git a/plugins/woocommerce-admin/client/products/sections/images-section.scss b/plugins/woocommerce-admin/client/products/fills/images-section/images-section.scss similarity index 100% rename from plugins/woocommerce-admin/client/products/sections/images-section.scss rename to plugins/woocommerce-admin/client/products/fills/images-section/images-section.scss diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/index.ts b/plugins/woocommerce-admin/client/products/fills/images-section/index.ts new file mode 100644 index 00000000000..9d67f229c29 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/images-section/index.ts @@ -0,0 +1 @@ +export * from './images-field-gallery'; diff --git a/plugins/woocommerce-admin/client/products/fills/index.ts b/plugins/woocommerce-admin/client/products/fills/index.ts index 925e757f7d7..f1cc184b6f7 100644 --- a/plugins/woocommerce-admin/client/products/fills/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/index.ts @@ -4,3 +4,4 @@ import './product-form-fills'; export * from './details-section/details-section-fills'; +export * from './images-section/images-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx index 0f1a171fafb..302c532e18c 100644 --- a/plugins/woocommerce-admin/client/products/product-form.tsx +++ b/plugins/woocommerce-admin/client/products/product-form.tsx @@ -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< { - - + { - 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 ( - - - { __( - 'For best results, use JPEG files that are 1000 by 1000 pixels or larger.', - 'woocommerce' - ) } - - { - recordEvent( 'prepare_images_help' ); - } } - > - { __( 'How should I prepare images?', 'woocommerce' ) } - - - } - > - 0, - } ) } - > - - { - 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 ) => ( - - ) ) } - -
- { isRemovingZoneVisible ? ( - -
- - - { __( - 'Drop here to remove', - 'woocommerce' - ) } - - - setIsRemoving( true ) - } - onDrop={ () => setIsRemoving( true ) } - label={ __( - 'Drop here to remove', - 'woocommerce' - ) } - /> -
-
- ) : ( - - 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={ - <> - { - - { __( - 'Drag images here or click to upload', - 'woocommerce' - ) } - - - } - /> - - ) } -
-
-
-
- ); -}; diff --git a/plugins/woocommerce/changelog/add-36417-mvp-images-slotfill b/plugins/woocommerce/changelog/add-36417-mvp-images-slotfill new file mode 100644 index 00000000000..d4b8e282cca --- /dev/null +++ b/plugins/woocommerce/changelog/add-36417-mvp-images-slotfill @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Using slotfill to insert images section in product editor.