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
This commit is contained in:
parent
31254c4d45
commit
e3e303e776
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Enable image inline actions in the variations table
|
|
@ -0,0 +1,2 @@
|
|||
export * from './media-library-menu-item';
|
||||
export * from './types';
|
|
@ -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 (
|
||||
<MediaUpload
|
||||
{ ...props }
|
||||
render={ ( { open } ) => (
|
||||
<MenuItem
|
||||
icon={ icon ?? media }
|
||||
iconPosition={ iconPosition ?? 'left' }
|
||||
onClick={ open }
|
||||
info={
|
||||
info ??
|
||||
__( 'Choose from uploaded media', 'woocommerce' )
|
||||
}
|
||||
>
|
||||
{ text ?? __( 'Media Library', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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' >;
|
|
@ -0,0 +1,2 @@
|
|||
export * from './upload-files-menu-item';
|
||||
export * from './types';
|
|
@ -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;
|
||||
};
|
|
@ -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 (
|
||||
<FormFileUpload
|
||||
{ ...props }
|
||||
onChange={ handleFormFileUploadChange }
|
||||
render={ ( { openFileDialog } ) => (
|
||||
<MenuItem
|
||||
icon={ icon ?? upload }
|
||||
iconPosition={ iconPosition ?? 'left' }
|
||||
onClick={ openFileDialog }
|
||||
info={
|
||||
info ??
|
||||
__( 'Select files from your device', 'woocommerce' )
|
||||
}
|
||||
>
|
||||
{ text ?? __( 'Upload', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<Dropdown
|
||||
{ ...props }
|
||||
// @ts-expect-error missing prop in types.
|
||||
popoverProps={ {
|
||||
placement: 'bottom-end',
|
||||
} }
|
||||
renderToggle={ ( toggleProps ) =>
|
||||
props.renderToggle( { ...toggleProps, isBusy: isUploading } )
|
||||
}
|
||||
className="woocommerce-image-actions-menu"
|
||||
contentClassName="woocommerce-image-actions-menu__menu-content"
|
||||
renderContent={ ( { onClose } ) => (
|
||||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup>
|
||||
<UploadFilesMenuItem
|
||||
allowedTypes={ [ 'image' ] }
|
||||
accept="image/*"
|
||||
multiple={ false }
|
||||
info={ __(
|
||||
'1000 pixels wide or larger',
|
||||
'woocommerce'
|
||||
) }
|
||||
onUploadProgress={ () => {
|
||||
setIsUploading( true );
|
||||
onClose();
|
||||
} }
|
||||
onUploadSuccess={ uploadSuccessHandler( onClose ) }
|
||||
onUploadError={ () => {
|
||||
setIsUploading( false );
|
||||
onClose();
|
||||
} }
|
||||
/>
|
||||
|
||||
<MediaLibraryMenuItem
|
||||
allowedTypes={ [ 'image' ] }
|
||||
multiple={ false }
|
||||
value={ selection[ 0 ].id }
|
||||
onSelect={ mediaLibraryMenuItemSelectHandler(
|
||||
onClose
|
||||
) }
|
||||
/>
|
||||
</MenuGroup>
|
||||
|
||||
<VariationQuickUpdateMenuItem.Slot
|
||||
group={ 'image-actions-menu' }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
selection={ selection }
|
||||
supportsMultipleSelection={ false }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './image-actions-menu';
|
||||
export * from './types';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
<MenuItem onClick={ handleClick( fillProps ) }>
|
||||
<MenuItem { ...props } onClick={ handleClick( fillProps ) }>
|
||||
{ children }
|
||||
</MenuItem>,
|
||||
order,
|
||||
|
|
|
@ -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,24 +141,21 @@ export function VariationsTableRow( {
|
|||
className="woocommerce-product-variations__attributes-cell"
|
||||
role="cell"
|
||||
>
|
||||
<MediaUpload
|
||||
value={ variation.id }
|
||||
onSelect={ ( image ) =>
|
||||
handleChange(
|
||||
[
|
||||
{
|
||||
id: variation.id,
|
||||
image:
|
||||
mapUploadImageToImage( image ) ||
|
||||
undefined,
|
||||
},
|
||||
],
|
||||
false
|
||||
)
|
||||
}
|
||||
allowedTypes={ [ 'image' ] }
|
||||
multiple={ false }
|
||||
render={ ( { open } ) => (
|
||||
<ImageActionsMenu
|
||||
selection={ [ variation ] }
|
||||
onChange={ handleChange }
|
||||
onDelete={ handleDelete }
|
||||
renderToggle={ ( { onToggle, isBusy } ) =>
|
||||
isBusy ? (
|
||||
<div className="woocommerce-product-variations__add-image-button">
|
||||
<Spinner
|
||||
aria-label={ __(
|
||||
'Loading image',
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className={ classNames(
|
||||
variation.image
|
||||
|
@ -171,17 +167,19 @@ export function VariationsTableRow( {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore this exists in the props but is not typed
|
||||
size="compact"
|
||||
onClick={ open }
|
||||
onClick={ onToggle }
|
||||
>
|
||||
{ variation.image && (
|
||||
<img
|
||||
<div
|
||||
className="woocommerce-product-variations__image"
|
||||
src={ variation.image.src }
|
||||
alt={ variation.image.alt }
|
||||
style={ {
|
||||
backgroundImage: `url('${ variation.image.src }')`,
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</Button>
|
||||
) }
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="woocommerce-product-variations__attributes">
|
||||
|
|
Loading…
Reference in New Issue