-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Images Product management MVP 1.0 #34769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Test Results SummaryCommit SHA: d64d689
Please address the following issues prior to merging this pull request: |
1cea4c0 to
0052fda
Compare
joshuatf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice implementation, @octaedro! Left a few suggestions on where we can move logic.
| export const ImageGallery: React.FC< ImageGalleryProps > = ( { | ||
| children, | ||
| columns = 4, | ||
| onDragStart = () => null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Thanks for adding these in. Should we add onDragOver as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! I added it in the commit 58c89b2
| &__images { | ||
| margin-bottom: 0; | ||
| box-shadow: unset; | ||
| .woocommerce-media-uploader { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should any of these styles be moved to the MediaUploader component? Or are they specific to the product form?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion! I moved a few lines to the package in commit a44aaa6. I left the image and the code related to that in the client because it was something specific to this implementation.
| } | ||
| } | ||
| &__images.has-images { | ||
| .woocommerce-media-uploader { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would leave this code here, since these styles will be applied depending on whether the Card that contains the component MediaUploader and ImageGallery has a class or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referring to the has-images class? This could be something useful inside that component as well.
But I guess the real question is do we want similar behavior to this in most of our other media uploader components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, if it's ok with you I'll tackle this in a follow-up PR.
| margin-top: 0; | ||
| } | ||
| } | ||
| .woocommerce-sortable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here. (under the ImageGallery component I would image)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these styles also depend on whether the parent component has a class or not, I would leave these styles here.
| recordEvent( 'prepare_images_help' ); | ||
| } } | ||
| > | ||
| { __( 'How to prepare images?', 'woocommerce' ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it might be a sentence fragment. Should we change it to How to prepare images without the ? or How should I prepare images??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! I changed the copy in commit 2cf569d.
| setShowRemoveZone( ! showRemoveZone ); | ||
| }; | ||
|
|
||
| const setImageToRemove = ( image: string ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of setting the image to remove later, could we just setValue( 'images', newImages ); here after filtering out the removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a good idea, and it was my first thought too. The problem I found was that it would remove the image before triggering the onDragEnd here.
That method (among other things) removes the class is-draggin in the Sortable component. So it would create a few oddities.
| const setImageToRemove = ( image: string ) => { | ||
| const tempNode = document.createElement( 'div' ); | ||
| tempNode.innerHTML = image; | ||
| const imageId = tempNode.getElementsByTagName( 'img' )[ 0 ].id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think instead of retrieving this ID via selectors, we could set the ID of the dragged element on start. For example:
onDragStart={ ( image ) => setDraggedImageId( image.key ) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I understood this suggestion correctly. When a user grabs an image, we don't know if the user is wanting to modify the order of the images or remove that image. If the user wants only to change the order of the images, we wouldn't need to save that value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's okay if they don't end up using the draggedImageId; we can set it to null later. But at least this way we can just used the ID stored in state instead of using dom query selectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it! I like the idea! I changed it in commit ddccb5b
| key={ image.id } | ||
| alt={ image.alt } | ||
| src={ image.url || image.src } | ||
| id={ `${ image.id }` } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we could forego the ID prop here and just opt to use the key to keep props down if we implement my above suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now we're not presenting the key when we render an ImageGalleryItem item. The rendered id is what we use to pair our list when the items are reordered or to delete an image.
| }; | ||
|
|
||
| const orderImages = ( newOrder: JSX.Element[] ) => { | ||
| const orderedImages = newOrder.map( ( image ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to just directly call setValue( 'images', newOrder );? Does the data differ pretty significantly?
I'd consider this a shortcoming in the onOrderChange callback if so. Not a blocker for this PR if that's the case, but would love to open an issue to make this easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is not possible now. The onOrderChange method returns the items and we are saving another kind of object under images.
I think that it would be a great addition to make the method onOrderChange return the same type of object we will use later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea! I've been looking on feedback on this and @louwie17 had a similar request.
7b05648 to
a13f75d
Compare
|
Thank you @joshuatf for your review! |
joshuatf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@octaedro I'm having trouble getting this branch started again. I'm seeing the old TS errors that we were hitting before the latest fixes in trunk. Could you give this a rebase?
| } | ||
| } | ||
| &__images.has-images { | ||
| .woocommerce-media-uploader { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referring to the has-images class? This could be something useful inside that component as well.
But I guess the real question is do we want similar behavior to this in most of our other media uploader components.
| }; | ||
|
|
||
| const orderImages = ( newOrder: JSX.Element[] ) => { | ||
| const orderedImages = newOrder.map( ( image ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea! I've been looking on feedback on this and @louwie17 had a similar request.
| export const ImagesSection: React.FC = () => { | ||
| const { getInputProps, setValue } = useFormContext< Product >(); | ||
| const images = ( getInputProps( 'images' ).value as Image[] ) || []; | ||
| const [ showRemoveZone, setShowRemoveZone ] = useState( false ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicky but isRemoveZoneVisible to avoid prefixing this with a verb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! I renamed the const in the commit 662d29c
| const [ showRemoveZone, setShowRemoveZone ] = useState( false ); | ||
| const [ imageIdToRemove, setImageIdToRemove ] = useState< number >( 0 ); | ||
|
|
||
| const getUniqueImages = ( files: Image[] ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great to extract this to a utils file so we can both test it and minimize the logic in this component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this, if it is ok with you I'll move the method in the same follow-up PR to refactor the code.
| const setImageToRemove = ( image: string ) => { | ||
| const tempNode = document.createElement( 'div' ); | ||
| tempNode.innerHTML = image; | ||
| const imageId = tempNode.getElementsByTagName( 'img' )[ 0 ].id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's okay if they don't end up using the draggedImageId; we can set it to null later. But at least this way we can just used the ID stored in state instead of using dom query selectors.
| images.push( image ); | ||
| } | ||
| } ); | ||
| setValue( 'images', images ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This getUniqueImages function has a side effect of setValue which feels out of place. We're also calling setValue below again after calling getUniqueImages.
setValue(
'images',
getUniqueImages( [ file ] )
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! I removed the setValue( 'images', images ); inside the function in commit 5ceceaa
| files.forEach( ( image: Image ) => { | ||
| if ( | ||
| image.id && | ||
| images.find( ( file ) => file.id === image.id ) === undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having some trouble understanding what the purpose of the getUniqueImages function really does. Is this just to guarantee the same image is not selected twice?
What happens if someone selects the same image twice? Nothing happens?
If we really need this, we might also want to consider storing a memoized version of images so we don't have nested loops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that we really don't need the method getUniqueImages. I removed it in commit 372dea0
46f1585 to
cfabfe8
Compare
73f73f1 to
215065f
Compare
joshuatf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work, @octaedro! The image section is testing really well for me. Love the work on the deletion drop zone.
Left a couple very minor comments, but pre-approving.
I think this PR might have an outdated lock file, but feel free to ping me on Slack for re-approval as soon as that's updated 😄
| useState< boolean >( false ); | ||
| const [ isRemove, setIsRemove ] = useState< boolean >( false ); | ||
| const [ draggedImageId, setDraggedImageId ] = useState< number | null >( | ||
| 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this initially be null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call! I changed it in the commit 15b342d
| onDragStart={ ( event ) => { | ||
| const { id: imageId, dataset } = event.target as HTMLElement; | ||
| if ( imageId ) { | ||
| setDraggedImageId( parseInt( imageId, 10 ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nicely done!
| const { id: imageId, dataset } = event.target as HTMLElement; | ||
| if ( imageId ) { | ||
| setDraggedImageId( parseInt( imageId, 10 ) ); | ||
| } else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious - what scenario do we not have imageId and end up inside this else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. Thanks for the explanation!
plugins/woocommerce-admin/client/products/sections/images-section.tsx
Outdated
Show resolved
Hide resolved
# Conflicts: # plugins/woocommerce-admin/client/products/add-product-page.tsx
# Conflicts: # packages/js/components/src/sortable/sortable-item.tsx # packages/js/components/src/sortable/sortable.tsx # Conflicts: # packages/js/components/src/sortable/sortable.tsx
# Conflicts: # packages/js/components/src/image-gallery/image-gallery-item.tsx # packages/js/components/src/image-gallery/image-gallery.tsx
# Conflicts: # packages/js/components/src/sortable/sortable-item.tsx # packages/js/components/src/sortable/sortable.tsx # Conflicts: # packages/js/components/src/image-gallery/image-gallery.tsx # packages/js/components/src/sortable/sortable.tsx
215065f to
d6cdafe
Compare
joshuatf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing well! Thanks for minor fix ups and the explanation around the imageId!
|
Hi @octaedro, thanks for merging this pull request. Please take a look at these follow-up tasks you may need to perform:
|

All Submissions:
Changes proposed in this Pull Request:
This PR adds the Image section to the product creation/edition.
Detail:
The following functionalities are not included here, but they will be added in a follow-up PR.
Closes #34759.
How to test the changes in this Pull Request:
Drag images here or click to uploadarea.Drag images here or click to uploadarea is smaller when there is at least one image loaded.Choose imagesand select an image that has been uploaded before.Drop here to removeis now visible.Other information:
pnpm changelog add --filter=<project>?FOR PR REVIEWER ONLY: