Added spinner/upload message when uploading an image to a post's content (#8986)
This commit is contained in:
parent
62f89f9c7c
commit
efc3a124a0
4 changed files with 103 additions and 48 deletions
|
|
@ -2,7 +2,7 @@ import { h, Component } from 'preact';
|
|||
import { generateMainImage } from '../actions';
|
||||
import { validateFileInputs } from '../../packs/validateFileInputs';
|
||||
import { ClipboardButton } from './ClipboardButton';
|
||||
import { Button } from '@crayons';
|
||||
import { Button, Spinner } from '@crayons';
|
||||
|
||||
function isNativeAndroid() {
|
||||
return (
|
||||
|
|
@ -41,6 +41,7 @@ export class ImageUploader extends Component {
|
|||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
showImageCopiedMessage: false,
|
||||
uploadingImage: false,
|
||||
};
|
||||
|
||||
onUploadError = (error) => {
|
||||
|
|
@ -48,6 +49,7 @@ export class ImageUploader extends Component {
|
|||
insertionImageUrls: [],
|
||||
uploadError: true,
|
||||
uploadErrorMessage: error.message,
|
||||
uploadingImage: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -76,13 +78,16 @@ export class ImageUploader extends Component {
|
|||
handleInsertionImageUpload = (e) => {
|
||||
const { files } = e.target;
|
||||
|
||||
this.clearUploadError();
|
||||
const validFileInputs = validateFileInputs();
|
||||
|
||||
if (validFileInputs && files.length > 0) {
|
||||
if (files.length > 0 && validateFileInputs()) {
|
||||
const payload = { image: files };
|
||||
|
||||
this.setState({ showImageCopiedMessage: false });
|
||||
this.setState({
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
uploadingImage: true,
|
||||
insertionImageUrls: [],
|
||||
showImageCopiedMessage: false,
|
||||
});
|
||||
|
||||
generateMainImage(
|
||||
payload,
|
||||
|
|
@ -94,6 +99,7 @@ export class ImageUploader extends Component {
|
|||
|
||||
handleInsertImageUploadSuccess = (response) => {
|
||||
this.setState({
|
||||
uploadingImage: false,
|
||||
insertionImageUrls: response.links,
|
||||
});
|
||||
};
|
||||
|
|
@ -107,43 +113,43 @@ export class ImageUploader extends Component {
|
|||
this.setState({ showImageCopiedMessage: true });
|
||||
}
|
||||
|
||||
clearUploadError() {
|
||||
this.setState({
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
insertionImageUrls,
|
||||
uploadError,
|
||||
uploadErrorMessage,
|
||||
showImageCopiedMessage,
|
||||
uploadingImage,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-2 fw-normal"
|
||||
variant="ghost"
|
||||
contentType="icon-left"
|
||||
icon={ImageIcon}
|
||||
tabIndex="-1"
|
||||
>
|
||||
Upload image
|
||||
<input
|
||||
type="file"
|
||||
id="image-upload-field"
|
||||
onChange={this.handleInsertionImageUpload}
|
||||
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
multiple
|
||||
accept="image/*"
|
||||
data-max-file-size-mb="25"
|
||||
{uploadingImage ? (
|
||||
<span class="lh-base pl-3 border-0 py-2 inline-block">
|
||||
<Spinner /> Uploading...
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-2 fw-normal"
|
||||
variant="ghost"
|
||||
contentType="icon-left"
|
||||
icon={ImageIcon}
|
||||
tabIndex="-1"
|
||||
aria-label="Upload an image"
|
||||
/>
|
||||
</Button>
|
||||
>
|
||||
Upload image
|
||||
<input
|
||||
type="file"
|
||||
id="image-upload-field"
|
||||
onChange={this.handleInsertionImageUpload}
|
||||
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
multiple
|
||||
accept="image/*"
|
||||
data-max-file-size-mb="25"
|
||||
tabIndex="-1"
|
||||
aria-label="Upload an image"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{insertionImageUrls.length > 0 && (
|
||||
<ClipboardButton
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { h } from 'preact';
|
||||
import { render, fireEvent, waitForElement } from '@testing-library/preact';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
waitForElement,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import { ImageUploader } from '../ImageUploader';
|
||||
|
|
@ -8,14 +13,6 @@ import '@testing-library/jest-dom';
|
|||
global.fetch = fetch;
|
||||
|
||||
describe('<ImageUploader />', () => {
|
||||
const fakeLinksResponse = JSON.stringify({
|
||||
links: ['/i/fake-link.jpg'],
|
||||
});
|
||||
|
||||
const fakeErrorMessage = {
|
||||
message: 'Some Fake Error',
|
||||
};
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<ImageUploader />);
|
||||
const results = await axe(container);
|
||||
|
|
@ -29,30 +26,74 @@ describe('<ImageUploader />', () => {
|
|||
expect(uploadInput.getAttribute('type')).toEqual('file');
|
||||
});
|
||||
|
||||
it('displays text to copy after upload', async () => {
|
||||
const { getByTitle, getByDisplayValue, getByLabelText } = render(
|
||||
<ImageUploader />,
|
||||
it('displays the upload spinner during upload', async () => {
|
||||
fetch.mockResponse(
|
||||
JSON.stringify({
|
||||
links: ['/i/fake-link.jpg'],
|
||||
}),
|
||||
);
|
||||
|
||||
const { getByLabelText, queryByText } = render(<ImageUploader />);
|
||||
|
||||
const inputEl = getByLabelText(/Upload an image/i);
|
||||
const file = new File(['(⌐□_□)'], 'chucknorris.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
|
||||
fireEvent.change(inputEl, { target: { files: [file] } });
|
||||
|
||||
const uploadingImage = await waitForElement(() =>
|
||||
queryByText(/uploading.../i),
|
||||
);
|
||||
|
||||
expect(uploadingImage).toBeDefined();
|
||||
});
|
||||
|
||||
it('displays text to copy after upload', async () => {
|
||||
fetch.mockResponse(
|
||||
JSON.stringify({
|
||||
links: ['/i/fake-link.jpg'],
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
getByTitle,
|
||||
getByDisplayValue,
|
||||
getByLabelText,
|
||||
queryByText,
|
||||
} = render(<ImageUploader />);
|
||||
const inputEl = getByLabelText(/Upload an image/i);
|
||||
|
||||
const file = new File(['(⌐□_□)'], 'chucknorris.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
|
||||
fetch.mockResponse(fakeLinksResponse);
|
||||
fireEvent.change(inputEl, { target: { files: [file] } });
|
||||
let uploadingImage = await waitForElement(() =>
|
||||
queryByText(/uploading.../i),
|
||||
);
|
||||
|
||||
expect(uploadingImage).toBeDefined();
|
||||
|
||||
expect(inputEl.files[0]).toEqual(file);
|
||||
expect(inputEl.files).toHaveLength(1);
|
||||
|
||||
await waitForElement(() => getByTitle(/copy markdown for image/i));
|
||||
await waitForElementToBeRemoved(() => queryByText(/uploading.../i));
|
||||
|
||||
getByTitle(/copy markdown for image/i);
|
||||
getByDisplayValue(/fake-link.jpg/i);
|
||||
});
|
||||
|
||||
// TODO: 'Copied!' is always in the DOM, and so we cannot test that the visual implications of the copy when clicking on the copy icon
|
||||
|
||||
it('displays an error when one occurs', async () => {
|
||||
const { getByText, getByLabelText } = render(<ImageUploader />);
|
||||
fetch.mockReject({
|
||||
message: 'Some Fake Error',
|
||||
});
|
||||
|
||||
const { getByText, getByLabelText, queryByText } = render(
|
||||
<ImageUploader />,
|
||||
);
|
||||
const inputEl = getByLabelText(/Upload an image/i);
|
||||
|
||||
// Check the input validation settings
|
||||
|
|
@ -63,13 +104,19 @@ describe('<ImageUploader />', () => {
|
|||
type: 'image/png',
|
||||
});
|
||||
|
||||
fetch.mockReject(fakeErrorMessage);
|
||||
fireEvent.change(inputEl, {
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
});
|
||||
|
||||
let uploadingImage = await waitForElement(() =>
|
||||
queryByText(/uploading.../i),
|
||||
);
|
||||
|
||||
expect(uploadingImage).toBeDefined();
|
||||
|
||||
await waitForElementToBeRemoved(() => queryByText(/uploading.../i));
|
||||
await waitForElement(() => getByText(/some fake error/i));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
1
app/javascript/crayons/Spinner/index.js
Normal file
1
app/javascript/crayons/Spinner/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './Spinner';
|
||||
|
|
@ -3,3 +3,4 @@ export * from '@crayons/ButtonGroup';
|
|||
export * from '@crayons/Dropdown';
|
||||
export * from '@crayons/formElements';
|
||||
export * from '@crayons/Modal';
|
||||
export * from '@crayons/Spinner';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue