Image uploading progress in comments editor + post editor (#18369)

* Image upload loading all cases covered

* Image upload failure added to EditorBody

* Created imageUploadHelpers file

* Minor changes

* Nit fixes

* Error fix

* Added documentation

* Added/Updated tests
This commit is contained in:
Rajat Talesra 2022-08-29 16:01:40 +05:30 committed by GitHub
parent 6bb523f480
commit 8fa4f8f7b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 61 deletions

View file

@ -102,7 +102,7 @@ export function generateMainImage({ payload, successCb, failureCb, signal }) {
const { image } = payload;
return successCb({ links, image });
})
.catch(failureCb);
.catch((message) => failureCb(message));
}
/**
@ -114,6 +114,7 @@ export function generateMainImage({ payload, successCb, failureCb, signal }) {
*/
export function processImageUpload(
images,
handleImageUploading,
handleImageSuccess,
handleImageFailure,
) {
@ -121,6 +122,7 @@ export function processImageUpload(
if (images.length > 0 && validateFileInputs()) {
const payload = { image: images };
handleImageUploading();
generateMainImage({
payload,
successCb: handleImageSuccess,

View file

@ -4,39 +4,16 @@ import { useLayoutEffect, useRef } from 'preact/hooks';
import { Toolbar } from './Toolbar';
import { handleImagePasted } from './pasteImageHelpers';
import {
handleImageDrop,
handleImageFailure,
onDragOver,
onDragExit,
} from './dragAndDropHelpers';
handleImageUploadSuccess,
handleImageUploading,
handleImageUploadFailure,
} from './imageUploadHelpers';
import { handleImageDrop, onDragOver, onDragExit } from './dragAndDropHelpers';
import { usePasteImage } from '@utilities/pasteImage';
import { useDragAndDrop } from '@utilities/dragAndDrop';
import { fetchSearch } from '@utilities/search';
import { AutocompleteTriggerTextArea } from '@crayons/AutocompleteTriggerTextArea';
function handleImageSuccess(textAreaRef) {
return function (response) {
// Function is within the component to be able to access
// textarea ref.
const editableBodyElement = textAreaRef.current;
const { links } = response;
const markdownImageLink = `![Image description](${links[0]})\n`;
const { selectionStart, selectionEnd, value } = editableBodyElement;
const before = value.substring(0, selectionStart);
const after = value.substring(selectionEnd, value.length);
editableBodyElement.value = `${before + markdownImageLink} ${after}`;
editableBodyElement.selectionStart =
selectionStart + markdownImageLink.length;
editableBodyElement.selectionEnd = editableBodyElement.selectionStart;
// Dispatching a new event so that linkstate, https://github.com/developit/linkstate,
// the function used to create the onChange prop gets called correctly.
editableBodyElement.dispatchEvent(new Event('input'));
};
}
export const EditorBody = ({
onChange,
defaultValue,
@ -47,8 +24,9 @@ export const EditorBody = ({
const { setElement } = useDragAndDrop({
onDrop: handleImageDrop(
handleImageSuccess(textAreaRef),
handleImageFailure,
handleImageUploading(textAreaRef),
handleImageUploadSuccess(textAreaRef),
handleImageUploadFailure(textAreaRef),
),
onDragOver,
onDragExit,
@ -56,8 +34,9 @@ export const EditorBody = ({
const setPasteElement = usePasteImage({
onPaste: handleImagePasted(
handleImageSuccess(textAreaRef),
handleImageFailure,
handleImageUploading(textAreaRef),
handleImageUploadSuccess(textAreaRef),
handleImageUploadFailure(textAreaRef),
),
});

View file

@ -15,7 +15,11 @@ export function matchesDataTransferType(
}
// TODO: Document functions
export function handleImageDrop(handleImageSuccess, handleImageFailure) {
export function handleImageDrop(
handleImageUploading,
handleImageSuccess,
handleImageFailure,
) {
return function (event) {
event.preventDefault();
@ -37,7 +41,12 @@ export function handleImageDrop(handleImageSuccess, handleImageFailure) {
return;
}
processImageUpload(files, handleImageSuccess, handleImageFailure);
processImageUpload(
files,
handleImageUploading,
handleImageSuccess,
handleImageFailure,
);
};
}

View file

@ -0,0 +1,97 @@
import { handleImageFailure } from './dragAndDropHelpers';
// Placeholder text displayed while an image is uploading
const UPLOADING_IMAGE_PLACEHOLDER = '![Uploading image](...)';
/**
* Handles image uploading by showing UPLOADING_IMAGE_PLACEHOLDER text.
*
* @param {useRef} textAreaRef The reference of the text area with content.
*/
export function handleImageUploading(textAreaRef) {
return function () {
// Function is within the component to be able to access
// textarea ref.
const editableBodyElement = textAreaRef.current;
const { selectionStart, selectionEnd, value } = editableBodyElement;
const before = value.substring(0, selectionStart);
const after = value.substring(selectionEnd, value.length);
const newSelectionStart = `${before}\n${UPLOADING_IMAGE_PLACEHOLDER}`
.length;
editableBodyElement.value = `${before}\n${UPLOADING_IMAGE_PLACEHOLDER}\n${after}`;
editableBodyElement.selectionStart = newSelectionStart;
editableBodyElement.selectionEnd = newSelectionStart;
};
}
/**
* Handles image upload successfully by replacing UPLOADING_IMAGE_PLACEHOLDER with image link.
*
* @param {useRef} textAreaRef The reference of the text area with content.
*/
export function handleImageUploadSuccess(textAreaRef) {
return function (response) {
// Function is within the component to be able to access
// textarea ref.
const editableBodyElement = textAreaRef.current;
const { links } = response;
const markdownImageLink = `![Image description](${links[0]})\n`;
const { selectionStart, selectionEnd, value } = editableBodyElement;
if (value.includes(UPLOADING_IMAGE_PLACEHOLDER)) {
const newSelectedStart =
value.indexOf(UPLOADING_IMAGE_PLACEHOLDER, 0) +
markdownImageLink.length;
editableBodyElement.value = value.replace(
UPLOADING_IMAGE_PLACEHOLDER,
markdownImageLink,
);
editableBodyElement.selectionStart = newSelectedStart;
editableBodyElement.selectionEnd = newSelectedStart;
} else {
const before = value.substring(0, selectionStart);
const after = value.substring(selectionEnd, value.length);
editableBodyElement.value = `${before}\n${markdownImageLink}\n${after}`;
editableBodyElement.selectionStart =
selectionStart + markdownImageLink.length;
editableBodyElement.selectionEnd = editableBodyElement.selectionStart;
}
// Dispatching a new event so that linkstate, https://github.com/developit/linkstate,
// the function used to create the onChange prop gets called correctly.
editableBodyElement.dispatchEvent(new Event('input'));
};
}
/**
* Handles image upload failure by removing UPLOADING_IMAGE_PLACEHOLDER text and showing error.
*
* @param {useRef} textAreaRef The reference of the text area with content.
*/
export function handleImageUploadFailure(textAreaRef) {
return function (message) {
// Function is within the component to be able to access
// textarea ref.
handleImageFailure(message);
const editableBodyElement = textAreaRef.current;
const { value } = editableBodyElement;
if (value.includes(`\n${UPLOADING_IMAGE_PLACEHOLDER}\n`)) {
const newSelectionStart = value.indexOf(
`\n${UPLOADING_IMAGE_PLACEHOLDER}\n`,
0,
);
editableBodyElement.value = value.replace(
`\n${UPLOADING_IMAGE_PLACEHOLDER}\n`,
'',
);
editableBodyElement.selectionStart = newSelectionStart;
editableBodyElement.selectionEnd = newSelectionStart;
}
};
}

View file

@ -20,7 +20,11 @@ export function matchesDataTransferType(
* @param {function} handleImageSuccess Callback for when image upload succeeds
* @param {function} handleImageFailure Callback for when image upload fails
*/
export function handleImagePasted(handleImageSuccess, handleImageFailure) {
export function handleImagePasted(
handleImageUploading,
handleImageSuccess,
handleImageFailure,
) {
return function (event) {
if (!event.clipboardData || !event.clipboardData.items) return;
if (!matchesDataTransferType(event.clipboardData.types)) return;
@ -37,7 +41,12 @@ export function handleImagePasted(handleImageSuccess, handleImageFailure) {
return;
}
processImageUpload(files, handleImageSuccess, handleImageFailure);
processImageUpload(
files,
handleImageUploading,
handleImageSuccess,
handleImageFailure,
);
};
}

View file

@ -2,9 +2,13 @@ import { h } from 'preact';
import { useState, useRef, useLayoutEffect } from 'preact/hooks';
import { populateTemplates } from '../../responseTemplates/responseTemplates';
import { handleImagePasted } from '../../article-form/components/pasteImageHelpers';
import {
handleImageUploading,
handleImageUploadSuccess,
handleImageUploadFailure,
} from '../../article-form/components/imageUploadHelpers';
import {
handleImageDrop,
handleImageFailure,
onDragOver,
onDragExit,
} from '../../article-form/components/dragAndDropHelpers';
@ -26,25 +30,6 @@ const getClosestTemplatesContainer = (element) =>
.closest('.comment-form__inner')
?.querySelector('.response-templates-container');
const handleImageSuccess = (textAreaRef) => {
return function (response) {
// Function is within the component to be able to access
// textarea ref.
const editableBodyElement = textAreaRef.current;
const { links } = response;
const markdownImageLink = `![Image description](${links[0]})\n`;
const { selectionStart, selectionEnd, value } = editableBodyElement;
const before = value.substring(0, selectionStart);
const after = value.substring(selectionEnd, value.length);
editableBodyElement.value = `${before + markdownImageLink} ${after}`;
editableBodyElement.selectionStart =
selectionStart + markdownImageLink.length;
editableBodyElement.selectionEnd = editableBodyElement.selectionStart;
};
};
export const CommentTextArea = ({ vanillaTextArea }) => {
const [templatesVisible, setTemplatesVisible] = useState(false);
const textAreaRef = useRef(null);
@ -52,8 +37,9 @@ export const CommentTextArea = ({ vanillaTextArea }) => {
const { setElement } = useDragAndDrop({
onDrop: handleImageDrop(
handleImageSuccess(textAreaRef),
handleImageFailure,
handleImageUploading(textAreaRef),
handleImageUploadSuccess(textAreaRef),
handleImageUploadFailure(textAreaRef),
),
onDragOver,
onDragExit,
@ -61,8 +47,9 @@ export const CommentTextArea = ({ vanillaTextArea }) => {
const setPasteElement = usePasteImage({
onPaste: handleImagePasted(
handleImageSuccess(textAreaRef),
handleImageFailure,
handleImageUploading(textAreaRef),
handleImageUploadSuccess(textAreaRef),
handleImageUploadFailure(textAreaRef),
),
});

View file

@ -49,6 +49,10 @@ describe('Upload image', () => {
'/images/admin-image.png',
);
cy.findByLabelText('Post Content')
.invoke('val')
.should('match', /!\[Uploading image\]/);
cy.findByLabelText('Post Content')
.invoke('val')
.should('match', /!\[Image description\]/);