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 "./variations-filter/styles.scss";
|
||||||
@import "./table-row-skeleton/styles.scss";
|
@import "./table-row-skeleton/styles.scss";
|
||||||
@import "./add-image-menu-item/style.scss";
|
@import "./add-image-menu-item/style.scss";
|
||||||
|
@import "./image-actions-menu/style.scss";
|
||||||
|
|
||||||
.woocommerce-product-variations {
|
.woocommerce-product-variations {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -190,17 +191,28 @@
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 1px dashed $gray-400;
|
border: 1px dashed $gray-400;
|
||||||
margin-right: $gap-small;
|
margin-right: $gap-small;
|
||||||
width: 32px;
|
width: $grid-unit-40;
|
||||||
|
height: $grid-unit-40;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.components-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&__image-button {
|
&__image-button {
|
||||||
margin-right: $gap-small;
|
margin-right: $gap-small;
|
||||||
width: 32px;
|
width: $grid-unit-40;
|
||||||
max-height: 32px;
|
height: $grid-unit-40;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
&__image {
|
&__image {
|
||||||
width: 32px;
|
width: 100%;
|
||||||
max-height: 32px;
|
height: 100%;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,6 +269,18 @@ export function useVariations( { productId }: UseVariationsProps ) {
|
||||||
}: PartialProductVariation ) {
|
}: PartialProductVariation ) {
|
||||||
if ( isUpdating[ variationId ] ) return;
|
if ( isUpdating[ variationId ] ) return;
|
||||||
|
|
||||||
|
setVariations( ( current ) =>
|
||||||
|
current.map( ( currentVariation ) => {
|
||||||
|
if ( currentVariation.id === variationId ) {
|
||||||
|
return {
|
||||||
|
...currentVariation,
|
||||||
|
...variation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return currentVariation;
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
|
||||||
const { updateProductVariation } = dispatch(
|
const { updateProductVariation } = dispatch(
|
||||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { PartialProductVariation, ProductVariation } from '@woocommerce/data';
|
import { PartialProductVariation, ProductVariation } from '@woocommerce/data';
|
||||||
|
import { MenuItem } from '@wordpress/components';
|
||||||
|
|
||||||
export type VariationActionsMenuProps = {
|
export type VariationActionsMenuProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -18,8 +19,7 @@ export type VariationQuickUpdateSlotProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MenuItemProps = {
|
export type MenuItemProps = Omit< MenuItem.Props, 'onClick' > & {
|
||||||
children?: React.ReactNode;
|
|
||||||
order?: number;
|
order?: number;
|
||||||
group?: string;
|
group?: string;
|
||||||
supportsMultipleSelection?: boolean;
|
supportsMultipleSelection?: boolean;
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const VariationQuickUpdateMenuItem: React.FC< MenuItemProps > & {
|
||||||
group = TOP_LEVEL_MENU,
|
group = TOP_LEVEL_MENU,
|
||||||
supportsMultipleSelection,
|
supportsMultipleSelection,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
|
...props
|
||||||
} ) => {
|
} ) => {
|
||||||
const handleClick =
|
const handleClick =
|
||||||
( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) => () => {
|
( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) => () => {
|
||||||
|
@ -61,7 +62,7 @@ export const VariationQuickUpdateMenuItem: React.FC< MenuItemProps > & {
|
||||||
>
|
>
|
||||||
{ ( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) =>
|
{ ( fillProps: Fill.Props & VariationQuickUpdateSlotProps ) =>
|
||||||
createOrderedChildren(
|
createOrderedChildren(
|
||||||
<MenuItem onClick={ handleClick( fillProps ) }>
|
<MenuItem { ...props } onClick={ handleClick( fillProps ) }>
|
||||||
{ children }
|
{ children }
|
||||||
</MenuItem>,
|
</MenuItem>,
|
||||||
order,
|
order,
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import { plus, info, Icon } from '@wordpress/icons';
|
import { plus, info, Icon } from '@wordpress/icons';
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { MediaUpload } from '@wordpress/media-utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -30,7 +29,7 @@ import {
|
||||||
} from '../../../utils';
|
} from '../../../utils';
|
||||||
import { SingleUpdateMenu } from '../variation-actions-menus';
|
import { SingleUpdateMenu } from '../variation-actions-menus';
|
||||||
import { VariationsTableRowProps } from './types';
|
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' );
|
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||||
|
|
||||||
|
@ -142,24 +141,21 @@ export function VariationsTableRow( {
|
||||||
className="woocommerce-product-variations__attributes-cell"
|
className="woocommerce-product-variations__attributes-cell"
|
||||||
role="cell"
|
role="cell"
|
||||||
>
|
>
|
||||||
<MediaUpload
|
<ImageActionsMenu
|
||||||
value={ variation.id }
|
selection={ [ variation ] }
|
||||||
onSelect={ ( image ) =>
|
onChange={ handleChange }
|
||||||
handleChange(
|
onDelete={ handleDelete }
|
||||||
[
|
renderToggle={ ( { onToggle, isBusy } ) =>
|
||||||
{
|
isBusy ? (
|
||||||
id: variation.id,
|
<div className="woocommerce-product-variations__add-image-button">
|
||||||
image:
|
<Spinner
|
||||||
mapUploadImageToImage( image ) ||
|
aria-label={ __(
|
||||||
undefined,
|
'Loading image',
|
||||||
},
|
'woocommerce'
|
||||||
],
|
) }
|
||||||
false
|
/>
|
||||||
)
|
</div>
|
||||||
}
|
) : (
|
||||||
allowedTypes={ [ 'image' ] }
|
|
||||||
multiple={ false }
|
|
||||||
render={ ( { open } ) => (
|
|
||||||
<Button
|
<Button
|
||||||
className={ classNames(
|
className={ classNames(
|
||||||
variation.image
|
variation.image
|
||||||
|
@ -171,17 +167,19 @@ export function VariationsTableRow( {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore this exists in the props but is not typed
|
// @ts-ignore this exists in the props but is not typed
|
||||||
size="compact"
|
size="compact"
|
||||||
onClick={ open }
|
onClick={ onToggle }
|
||||||
>
|
>
|
||||||
{ variation.image && (
|
{ variation.image && (
|
||||||
<img
|
<div
|
||||||
className="woocommerce-product-variations__image"
|
className="woocommerce-product-variations__image"
|
||||||
src={ variation.image.src }
|
style={ {
|
||||||
alt={ variation.image.alt }
|
backgroundImage: `url('${ variation.image.src }')`,
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</Button>
|
</Button>
|
||||||
) }
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="woocommerce-product-variations__attributes">
|
<div className="woocommerce-product-variations__attributes">
|
||||||
|
|
Loading…
Reference in New Issue