From e3e303e776f2dd6ae946b0015d29961a0e44622a Mon Sep 17 00:00:00 2001 From: Maikel Perez Date: Tue, 11 Jun 2024 18:42:02 -0400 Subject: [PATCH] Enable image inline actions in the variations table (#48083) * Create UploadFilesMenuItem component * Create MediaLibraryMenuItem component * Create ImageActionsMenu component * Integrate ImageActionsMenu with VariationsTableRow * Add extensibility to the ImageActionsMenu component * Add changelog file * Fix compilation errors * Fix linter errors * Allow images only to be uploaded from the UploadFilesMenuItem component * Fix image aligment in the actions menu toggle --- .../product-editor/changelog/add-40351-image | 4 + .../media-library-menu-item/index.ts | 2 + .../media-library-menu-item.tsx | 42 +++++++ .../media-library-menu-item/types.ts | 11 ++ .../upload-files-menu-item/index.ts | 2 + .../upload-files-menu-item/types.ts | 27 +++++ .../upload-files-menu-item.tsx | 78 +++++++++++++ .../image-actions-menu/image-actions-menu.tsx | 109 ++++++++++++++++++ .../image-actions-menu/index.ts | 2 + .../image-actions-menu/style.scss | 12 ++ .../image-actions-menu/types.ts | 19 +++ .../components/variations-table/styles.scss | 22 +++- .../use-variations/use-variations.ts | 12 ++ .../variation-actions-menus/types.ts | 4 +- .../variation-quick-update-menu-item.tsx | 3 +- .../variations-table-row.tsx | 78 ++++++------- 16 files changed, 379 insertions(+), 48 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-40351-image create mode 100644 packages/js/product-editor/src/components/menu-items/media-library-menu-item/index.ts create mode 100644 packages/js/product-editor/src/components/menu-items/media-library-menu-item/media-library-menu-item.tsx create mode 100644 packages/js/product-editor/src/components/menu-items/media-library-menu-item/types.ts create mode 100644 packages/js/product-editor/src/components/menu-items/upload-files-menu-item/index.ts create mode 100644 packages/js/product-editor/src/components/menu-items/upload-files-menu-item/types.ts create mode 100644 packages/js/product-editor/src/components/menu-items/upload-files-menu-item/upload-files-menu-item.tsx create mode 100644 packages/js/product-editor/src/components/variations-table/image-actions-menu/image-actions-menu.tsx create mode 100644 packages/js/product-editor/src/components/variations-table/image-actions-menu/index.ts create mode 100644 packages/js/product-editor/src/components/variations-table/image-actions-menu/style.scss create mode 100644 packages/js/product-editor/src/components/variations-table/image-actions-menu/types.ts diff --git a/packages/js/product-editor/changelog/add-40351-image b/packages/js/product-editor/changelog/add-40351-image new file mode 100644 index 00000000000..36974e079d9 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40351-image @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Enable image inline actions in the variations table diff --git a/packages/js/product-editor/src/components/menu-items/media-library-menu-item/index.ts b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/index.ts new file mode 100644 index 00000000000..154d679d17a --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/index.ts @@ -0,0 +1,2 @@ +export * from './media-library-menu-item'; +export * from './types'; diff --git a/packages/js/product-editor/src/components/menu-items/media-library-menu-item/media-library-menu-item.tsx b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/media-library-menu-item.tsx new file mode 100644 index 00000000000..7a737db83dc --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/media-library-menu-item.tsx @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { media } from '@wordpress/icons'; +import { MediaUpload } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import type { MediaLibraryMenuItemProps } from './types'; + +export function MediaLibraryMenuItem( { + // MenuItem.Props + icon, + iconPosition, + text, + info, + // MediaUpload.Props + ...props +}: MediaLibraryMenuItemProps ) { + return ( + ( + + { text ?? __( 'Media Library', 'woocommerce' ) } + + ) } + /> + ); +} diff --git a/packages/js/product-editor/src/components/menu-items/media-library-menu-item/types.ts b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/types.ts new file mode 100644 index 00000000000..0b9bc185ed1 --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/media-library-menu-item/types.ts @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { MenuItem as DropdownMenuItem } from '@wordpress/components'; +import { MediaUpload } from '@wordpress/media-utils'; + +export type MediaLibraryMenuItemProps = Omit< + MediaUpload.Props< boolean >, + 'children' | 'render' | 'onChange' +> & + Pick< DropdownMenuItem.Props, 'icon' | 'iconPosition' | 'text' | 'info' >; diff --git a/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/index.ts b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/index.ts new file mode 100644 index 00000000000..022460cde79 --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/index.ts @@ -0,0 +1,2 @@ +export * from './upload-files-menu-item'; +export * from './types'; diff --git a/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/types.ts b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/types.ts new file mode 100644 index 00000000000..28ef40a40eb --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/types.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { + FormFileUpload, + MenuItem as DropdownMenuItem, +} from '@wordpress/components'; +import { MediaItem, UploadMediaOptions } from '@wordpress/media-utils'; + +export type UploadFilesMenuItemProps = Omit< + FormFileUpload.Props, + 'children' | 'render' | 'onChange' +> & + Pick< DropdownMenuItem.Props, 'icon' | 'iconPosition' | 'text' | 'info' > & + Partial< + Pick< + UploadMediaOptions, + | 'additionalData' + | 'allowedTypes' + | 'maxUploadFileSize' + | 'wpAllowedMimeTypes' + > + > & { + onUploadProgress?( files: MediaItem[] ): void; + onUploadSuccess( files: MediaItem[] ): void; + onUploadError( error: unknown ): void; + }; diff --git a/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/upload-files-menu-item.tsx b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/upload-files-menu-item.tsx new file mode 100644 index 00000000000..dbd164efccb --- /dev/null +++ b/packages/js/product-editor/src/components/menu-items/upload-files-menu-item/upload-files-menu-item.tsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import type { ChangeEvent } from 'react'; +import { FormFileUpload, MenuItem } from '@wordpress/components'; +import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { upload } from '@wordpress/icons'; +import { uploadMedia } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import type { UploadFilesMenuItemProps } from './types'; + +export function UploadFilesMenuItem( { + // UploadMediaOptions + allowedTypes, + maxUploadFileSize = 10000000, + wpAllowedMimeTypes, + additionalData, + // MenuItem.Props + icon, + iconPosition, + text, + info, + // Handlers + onUploadProgress, + onUploadSuccess, + onUploadError, + // FormFileUpload.Props + ...props +}: UploadFilesMenuItemProps ) { + function handleFormFileUploadChange( + event: ChangeEvent< HTMLInputElement > + ) { + const filesList = event.currentTarget.files as FileList; + + uploadMedia( { + allowedTypes, + filesList, + maxUploadFileSize, + additionalData, + wpAllowedMimeTypes, + onFileChange( files ) { + const isUploading = files.some( ( file ) => ! file.id ); + + if ( isUploading ) { + onUploadProgress?.( files ); + return; + } + + onUploadSuccess( files ); + }, + onError: onUploadError, + } ); + } + + return ( + ( + + { text ?? __( 'Upload', 'woocommerce' ) } + + ) } + /> + ); +} diff --git a/packages/js/product-editor/src/components/variations-table/image-actions-menu/image-actions-menu.tsx b/packages/js/product-editor/src/components/variations-table/image-actions-menu/image-actions-menu.tsx new file mode 100644 index 00000000000..d6a3f3e3f96 --- /dev/null +++ b/packages/js/product-editor/src/components/variations-table/image-actions-menu/image-actions-menu.tsx @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { Dropdown, MenuGroup } from '@wordpress/components'; +import { createElement, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { MediaItem } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import { MediaLibraryMenuItem } from '../../menu-items/media-library-menu-item'; +import { UploadFilesMenuItem } from '../../menu-items/upload-files-menu-item'; +import { mapUploadImageToImage } from '../../../utils/map-upload-image-to-image'; +import { VariationQuickUpdateMenuItem } from '../variation-actions-menus'; +import type { ImageActionsMenuProps } from './types'; + +export function ImageActionsMenu( { + selection, + onChange, + onDelete, + ...props +}: ImageActionsMenuProps ) { + const [ isUploading, setIsUploading ] = useState( false ); + + function uploadSuccessHandler( onClose: () => void ) { + return function handleUploadSuccess( files: MediaItem[] ) { + const image = + ( files.length && mapUploadImageToImage( files[ 0 ] ) ) || + undefined; + const variation = { + id: selection[ 0 ].id, + image, + }; + + setIsUploading( false ); + + onChange( [ variation ], false ); + onClose(); + }; + } + + function mediaLibraryMenuItemSelectHandler( onClose: () => void ) { + return function handleMediaLibraryMenuItemSelect( media: never ) { + const variation = { + id: selection[ 0 ].id, + image: mapUploadImageToImage( media ) || undefined, + }; + onChange( [ variation ], false ); + onClose(); + }; + } + + return ( + + props.renderToggle( { ...toggleProps, isBusy: isUploading } ) + } + className="woocommerce-image-actions-menu" + contentClassName="woocommerce-image-actions-menu__menu-content" + renderContent={ ( { onClose } ) => ( +
+ + { + setIsUploading( true ); + onClose(); + } } + onUploadSuccess={ uploadSuccessHandler( onClose ) } + onUploadError={ () => { + setIsUploading( false ); + onClose(); + } } + /> + + + + + +
+ ) } + /> + ); +} diff --git a/packages/js/product-editor/src/components/variations-table/image-actions-menu/index.ts b/packages/js/product-editor/src/components/variations-table/image-actions-menu/index.ts new file mode 100644 index 00000000000..793a9ef119e --- /dev/null +++ b/packages/js/product-editor/src/components/variations-table/image-actions-menu/index.ts @@ -0,0 +1,2 @@ +export * from './image-actions-menu'; +export * from './types'; diff --git a/packages/js/product-editor/src/components/variations-table/image-actions-menu/style.scss b/packages/js/product-editor/src/components/variations-table/image-actions-menu/style.scss new file mode 100644 index 00000000000..43a37ba8741 --- /dev/null +++ b/packages/js/product-editor/src/components/variations-table/image-actions-menu/style.scss @@ -0,0 +1,12 @@ +.woocommerce-image-actions-menu { + display: inline-flex; + + &__menu-content { + // To be rendered below the MediaLibrary modal. + z-index: 100000; + + .components-menu-item__item { + min-width: 172px; + } + } +} diff --git a/packages/js/product-editor/src/components/variations-table/image-actions-menu/types.ts b/packages/js/product-editor/src/components/variations-table/image-actions-menu/types.ts new file mode 100644 index 00000000000..61b41b454c3 --- /dev/null +++ b/packages/js/product-editor/src/components/variations-table/image-actions-menu/types.ts @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { Dropdown } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { VariationActionsMenuProps } from '../variation-actions-menus'; + +export type ImageActionsMenuProps = Omit< + Dropdown.Props, + 'renderToggle' | 'renderContent' +> & + VariationActionsMenuProps & { + renderToggle( + props: Dropdown.RenderProps & { isBusy?: boolean } + ): JSX.Element; + }; diff --git a/packages/js/product-editor/src/components/variations-table/styles.scss b/packages/js/product-editor/src/components/variations-table/styles.scss index 33802ee7493..a45f9f7d613 100644 --- a/packages/js/product-editor/src/components/variations-table/styles.scss +++ b/packages/js/product-editor/src/components/variations-table/styles.scss @@ -5,6 +5,7 @@ @import "./variations-filter/styles.scss"; @import "./table-row-skeleton/styles.scss"; @import "./add-image-menu-item/style.scss"; +@import "./image-actions-menu/style.scss"; .woocommerce-product-variations { display: flex; @@ -190,17 +191,28 @@ border-radius: 2px; border: 1px dashed $gray-400; margin-right: $gap-small; - width: 32px; + width: $grid-unit-40; + height: $grid-unit-40; padding: 0; + display: flex; + align-items: center; + justify-content: center; + + .components-spinner { + margin: 0; + } } &__image-button { margin-right: $gap-small; - width: 32px; - max-height: 32px; + width: $grid-unit-40; + height: $grid-unit-40; padding: 0; } &__image { - width: 32px; - max-height: 32px; + width: 100%; + height: 100%; + background-position: center center; + background-size: contain; + background-repeat: no-repeat; } } diff --git a/packages/js/product-editor/src/components/variations-table/use-variations/use-variations.ts b/packages/js/product-editor/src/components/variations-table/use-variations/use-variations.ts index 62da59fe08b..ba3b9f27cf9 100644 --- a/packages/js/product-editor/src/components/variations-table/use-variations/use-variations.ts +++ b/packages/js/product-editor/src/components/variations-table/use-variations/use-variations.ts @@ -269,6 +269,18 @@ export function useVariations( { productId }: UseVariationsProps ) { }: PartialProductVariation ) { if ( isUpdating[ variationId ] ) return; + setVariations( ( current ) => + current.map( ( currentVariation ) => { + if ( currentVariation.id === variationId ) { + return { + ...currentVariation, + ...variation, + }; + } + return currentVariation; + } ) + ); + const { updateProductVariation } = dispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME ); diff --git a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/types.ts b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/types.ts index 2b7923fac24..b3af9cd6472 100644 --- a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/types.ts +++ b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/types.ts @@ -2,6 +2,7 @@ * External dependencies */ import { PartialProductVariation, ProductVariation } from '@woocommerce/data'; +import { MenuItem } from '@wordpress/components'; export type VariationActionsMenuProps = { disabled?: boolean; @@ -18,8 +19,7 @@ export type VariationQuickUpdateSlotProps = { onClose: () => void; }; -export type MenuItemProps = { - children?: React.ReactNode; +export type MenuItemProps = Omit< MenuItem.Props, 'onClick' > & { order?: number; group?: string; supportsMultipleSelection?: boolean; diff --git a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/variation-quick-update-menu-item.tsx b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/variation-quick-update-menu-item.tsx index 4b342b919ce..aba228d7f19 100644 --- a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/variation-quick-update-menu-item.tsx +++ b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/variation-quick-update-menu-item.tsx @@ -41,6 +41,7 @@ export const VariationQuickUpdateMenuItem: React.FC< MenuItemProps > & { group = TOP_LEVEL_MENU, supportsMultipleSelection, onClick = () => {}, + ...props } ) => { const handleClick = ( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) => () => { @@ -61,7 +62,7 @@ export const VariationQuickUpdateMenuItem: React.FC< MenuItemProps > & { > { ( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) => createOrderedChildren( - + { children } , order, diff --git a/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx b/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx index 139c4f6b8fa..72f4e64d80b 100644 --- a/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx +++ b/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx @@ -16,7 +16,6 @@ import { import { plus, info, Icon } from '@wordpress/icons'; import { __, sprintf } from '@wordpress/i18n'; import classNames from 'classnames'; -import { MediaUpload } from '@wordpress/media-utils'; /** * Internal dependencies @@ -30,7 +29,7 @@ import { } from '../../../utils'; import { SingleUpdateMenu } from '../variation-actions-menus'; import { VariationsTableRowProps } from './types'; -import { mapUploadImageToImage } from '../../../utils/map-upload-image-to-image'; +import { ImageActionsMenu } from '../image-actions-menu'; const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' ); @@ -142,46 +141,45 @@ export function VariationsTableRow( { className="woocommerce-product-variations__attributes-cell" role="cell" > - - handleChange( - [ - { - id: variation.id, - image: - mapUploadImageToImage( image ) || - undefined, - }, - ], - false + + isBusy ? ( +
+ +
+ ) : ( + ) } - allowedTypes={ [ 'image' ] } - multiple={ false } - render={ ( { open } ) => ( - - ) } />