Add Media Uploader component (#34181)
* Add media uploader component * Expand on mocked examples in storybook * Add styling * Move MediaUploder out of index * Add changelog entry * Handle PR feedback * Add readme * Fix missing media utils types * Rebase and use variant instead of isSecondary for Button * Fix up lock file * Fix up lint errors
This commit is contained in:
parent
19853e1577
commit
a98f0fef52
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add MediaUploader component
|
|
@ -46,6 +46,7 @@
|
||||||
"@wordpress/i18n": "^4.3.1",
|
"@wordpress/i18n": "^4.3.1",
|
||||||
"@wordpress/icons": "^8.1.0",
|
"@wordpress/icons": "^8.1.0",
|
||||||
"@wordpress/keycodes": "^3.3.1",
|
"@wordpress/keycodes": "^3.3.1",
|
||||||
|
"@wordpress/media-utils": "^4.6.0",
|
||||||
"@wordpress/url": "^3.4.1",
|
"@wordpress/url": "^3.4.1",
|
||||||
"@wordpress/viewport": "^4.1.2",
|
"@wordpress/viewport": "^4.1.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
@ -95,9 +96,10 @@
|
||||||
"@testing-library/jest-dom": "^5.16.2",
|
"@testing-library/jest-dom": "^5.16.2",
|
||||||
"@testing-library/react": "^12.1.3",
|
"@testing-library/react": "^12.1.3",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/wordpress__components": "^19.10.1",
|
|
||||||
"@types/wordpress__viewport": "^2.5.4",
|
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/wordpress__components": "^19.10.1",
|
||||||
|
"@types/wordpress__media-utils": "^3.0.0",
|
||||||
|
"@types/wordpress__viewport": "^2.5.4",
|
||||||
"@woocommerce/eslint-plugin": "workspace:*",
|
"@woocommerce/eslint-plugin": "workspace:*",
|
||||||
"@woocommerce/internal-style-build": "workspace:*",
|
"@woocommerce/internal-style-build": "workspace:*",
|
||||||
"@wordpress/browserslist-config": "^4.1.1",
|
"@wordpress/browserslist-config": "^4.1.1",
|
||||||
|
|
|
@ -19,6 +19,7 @@ export { H, Section } from './section';
|
||||||
export { default as ImageUpload } from './image-upload';
|
export { default as ImageUpload } from './image-upload';
|
||||||
export { default as Link } from './link';
|
export { default as Link } from './link';
|
||||||
export { default as List } from './list';
|
export { default as List } from './list';
|
||||||
|
export { MediaUploader } from './media-uploader';
|
||||||
export { default as MenuItem } from './ellipsis-menu/menu-item';
|
export { default as MenuItem } from './ellipsis-menu/menu-item';
|
||||||
export { default as MenuTitle } from './ellipsis-menu/menu-title';
|
export { default as MenuTitle } from './ellipsis-menu/menu-title';
|
||||||
export { default as OrderStatus } from './order-status';
|
export { default as OrderStatus } from './order-status';
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
MediaUploader
|
||||||
|
===
|
||||||
|
|
||||||
|
This component adds an upload button and a dropzone for uploading media to a site.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
By default this will use the functionality from `@wordpress/media-utils` which provides access and uploads to the WP media library and uses the WP media modal.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<MediaUploader
|
||||||
|
label={ 'Click the button below to upload' }
|
||||||
|
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
|
||||||
|
onUpload={ ( files ) => setImages( [ ...images, ...files ] ) }
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
Name | Type | Default | Description
|
||||||
|
--- | --- | --- | ---
|
||||||
|
`allowedMediaTypes` | String[] | `[ 'image ]` | Allowed media types
|
||||||
|
`buttonText` | String | `Choose images` | Text to use for button
|
||||||
|
`hasDropZone` | Boolean | `true` | Whether or not to allow the dropzone
|
||||||
|
`label` | String | `Drag images here or click to upload` | String to use for the text shown inside the component
|
||||||
|
`MediaUploadComponent` | JSX.Element | `MediaModal` | The component to use for the media uploader
|
||||||
|
`onError` | Function | `() => null` | Callback function to run when an error occurs
|
||||||
|
`onUpload` | Function | `() => null` | Callback function to run when an upload occurs aftering dragging and dropping files
|
||||||
|
`onUpload` | Function | `() => null` | Callback function to run when selecting media from the opened media modal
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './media-uploader';
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Button, DropZone } from '@wordpress/components';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
import {
|
||||||
|
MediaItem,
|
||||||
|
MediaUpload,
|
||||||
|
uploadMedia as wpUploadMedia,
|
||||||
|
UploadMediaOptions,
|
||||||
|
UploadMediaErrorCode,
|
||||||
|
} from '@wordpress/media-utils';
|
||||||
|
|
||||||
|
const DEFAULT_ALLOWED_MEDIA_TYPES = [ 'image' ];
|
||||||
|
|
||||||
|
type MediaUploaderProps = {
|
||||||
|
allowedMediaTypes?: string[];
|
||||||
|
buttonText?: string;
|
||||||
|
hasDropZone?: boolean;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
label?: string;
|
||||||
|
maxUploadFileSize?: number;
|
||||||
|
MediaUploadComponent?: < T extends boolean = false >(
|
||||||
|
props: MediaUpload.Props< T >
|
||||||
|
) => JSX.Element;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
onSelect?: ( value: { id: number } & { [ k: string ]: any } ) => void;
|
||||||
|
onError?: ( error: {
|
||||||
|
code: UploadMediaErrorCode;
|
||||||
|
message: string;
|
||||||
|
file: File;
|
||||||
|
} ) => void;
|
||||||
|
onUpload?: ( files: MediaItem[] ) => void;
|
||||||
|
uploadMedia?: ( options: UploadMediaOptions ) => Promise< void >;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MediaUploader = ( {
|
||||||
|
allowedMediaTypes = DEFAULT_ALLOWED_MEDIA_TYPES,
|
||||||
|
buttonText = __( 'Choose images', 'woocommerce' ),
|
||||||
|
hasDropZone = true,
|
||||||
|
label = __( 'Drag images here or click to upload', 'woocommerce' ),
|
||||||
|
maxUploadFileSize = 10000000,
|
||||||
|
MediaUploadComponent = MediaUpload,
|
||||||
|
onError = () => null,
|
||||||
|
onUpload = () => null,
|
||||||
|
onSelect = () => null,
|
||||||
|
uploadMedia = wpUploadMedia,
|
||||||
|
}: MediaUploaderProps ) => {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-media-uploader">
|
||||||
|
<div className="woocommerce-media-uploader__label">{ label }</div>
|
||||||
|
|
||||||
|
<MediaUploadComponent
|
||||||
|
onSelect={ onSelect }
|
||||||
|
allowedTypes={ allowedMediaTypes }
|
||||||
|
render={ ( { open } ) => (
|
||||||
|
<Button variant="secondary" onClick={ open }>
|
||||||
|
{ buttonText }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ hasDropZone && (
|
||||||
|
<DropZone
|
||||||
|
onFilesDrop={ ( files ) =>
|
||||||
|
uploadMedia( {
|
||||||
|
filesList: files,
|
||||||
|
onError,
|
||||||
|
onFileChange: onUpload,
|
||||||
|
maxUploadFileSize,
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,178 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import React, { createElement } from 'react';
|
||||||
|
import { Card, CardBody, Modal, Notice } from '@wordpress/components';
|
||||||
|
import { MediaItem } from '@wordpress/media-utils';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { MediaUploader } from '../';
|
||||||
|
import { File } from '../types';
|
||||||
|
|
||||||
|
declare let Blob: {
|
||||||
|
prototype: Blob;
|
||||||
|
new (): Blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MockMediaUpload = ( { onSelect, render } ) => {
|
||||||
|
const [ isOpen, setOpen ] = useState( false );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ render( {
|
||||||
|
open: () => setOpen( true ),
|
||||||
|
} ) }
|
||||||
|
{ isOpen && (
|
||||||
|
<Modal
|
||||||
|
title="Media Modal"
|
||||||
|
onRequestClose={ () => setOpen( false ) }
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Use the default built-in{ ' ' }
|
||||||
|
<code>MediaUploadComponent</code> prop to render the WP
|
||||||
|
Media Modal.
|
||||||
|
</p>
|
||||||
|
{ Array( ...Array( 3 ) ).map( ( n, i ) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ i }
|
||||||
|
onClick={ () => {
|
||||||
|
onSelect( {
|
||||||
|
alt: 'Random',
|
||||||
|
url: `https://picsum.photos/200?i=${ i }`,
|
||||||
|
} );
|
||||||
|
setOpen( false );
|
||||||
|
} }
|
||||||
|
style={ {
|
||||||
|
marginRight: '16px',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ `https://picsum.photos/200?i=${ i }` }
|
||||||
|
alt="Random"
|
||||||
|
style={ {
|
||||||
|
maxWidth: '100px',
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</Modal>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageGallery = ( { images }: { images: File[] } ) => {
|
||||||
|
return (
|
||||||
|
<div style={ { marginBottom: '16px' } }>
|
||||||
|
{ images.map( ( image, index ) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
key={ index }
|
||||||
|
alt={ image.alt }
|
||||||
|
src={ image.url }
|
||||||
|
style={ {
|
||||||
|
maxWidth: '100px',
|
||||||
|
marginRight: '16px',
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readImage = ( file: Blob ) => {
|
||||||
|
return new Promise< MediaItem >( ( resolve ) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = function ( event ) {
|
||||||
|
const image = {
|
||||||
|
alt: 'Temporary image',
|
||||||
|
url: event?.target?.result,
|
||||||
|
} as MediaItem;
|
||||||
|
resolve( image );
|
||||||
|
};
|
||||||
|
fileReader.readAsDataURL( file );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUploadMedia = async ( { filesList, onFileChange } ) => {
|
||||||
|
const images = await Promise.all(
|
||||||
|
filesList.map( ( file ) => readImage( file ) )
|
||||||
|
);
|
||||||
|
onFileChange( images );
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Basic: React.FC = () => {
|
||||||
|
const [ images, setImages ] = useState< File[] >( [] );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card size="large">
|
||||||
|
<CardBody>
|
||||||
|
<ImageGallery images={ images } />
|
||||||
|
<MediaUploader
|
||||||
|
MediaUploadComponent={ MockMediaUpload }
|
||||||
|
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
|
||||||
|
onError={ () => null }
|
||||||
|
onUpload={ ( files ) =>
|
||||||
|
setImages( [ ...images, ...files ] )
|
||||||
|
}
|
||||||
|
uploadMedia={ mockUploadMedia }
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisabledDropZone: React.FC = () => {
|
||||||
|
const [ images, setImages ] = useState< File[] >( [] );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card size="large">
|
||||||
|
<CardBody>
|
||||||
|
<ImageGallery images={ images } />
|
||||||
|
<MediaUploader
|
||||||
|
hasDropZone={ false }
|
||||||
|
label={ 'Click the button below to upload' }
|
||||||
|
MediaUploadComponent={ MockMediaUpload }
|
||||||
|
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
|
||||||
|
onError={ () => null }
|
||||||
|
uploadMedia={ mockUploadMedia }
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MaxUploadFileSize: React.FC = () => {
|
||||||
|
const [ error, setError ] = useState< string | null >( null );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card size="large">
|
||||||
|
<CardBody>
|
||||||
|
{ error && (
|
||||||
|
<Notice isDismissible={ false } status={ 'error' }>
|
||||||
|
{ error }
|
||||||
|
</Notice>
|
||||||
|
) }
|
||||||
|
|
||||||
|
<MediaUploader
|
||||||
|
maxUploadFileSize={ 1000 }
|
||||||
|
MediaUploadComponent={ MockMediaUpload }
|
||||||
|
onSelect={ () => null }
|
||||||
|
onError={ ( e ) => setError( e.message ) }
|
||||||
|
onUpload={ () => null }
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'WooCommerce Admin/components/MediaUploader',
|
||||||
|
component: Basic,
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
.woocommerce-media-uploader {
|
||||||
|
.woocommerce-media-uploader__label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: $gap-large;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { UploadMediaErrorCode } from '@wordpress/media-utils';
|
||||||
|
|
||||||
|
export type ErrorType = {
|
||||||
|
code: UploadMediaErrorCode;
|
||||||
|
message: string;
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type File = { id: number } & { [ k: string ]: any };
|
|
@ -21,6 +21,7 @@
|
||||||
@import 'flag/style.scss';
|
@import 'flag/style.scss';
|
||||||
@import 'image-upload/style.scss';
|
@import 'image-upload/style.scss';
|
||||||
@import 'list/style.scss';
|
@import 'list/style.scss';
|
||||||
|
@import 'media-uploader/style.scss';
|
||||||
@import 'order-status/style.scss';
|
@import 'order-status/style.scss';
|
||||||
@import 'pagination/style.scss';
|
@import 'pagination/style.scss';
|
||||||
@import 'pill/style.scss';
|
@import 'pill/style.scss';
|
||||||
|
|
7169
pnpm-lock.yaml
7169
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue