From 8fa4f8f7b0c754e23e947cd0a573d55b41ddbab1 Mon Sep 17 00:00:00 2001 From: Rajat Talesra Date: Mon, 29 Aug 2022 16:01:40 +0530 Subject: [PATCH] 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 --- app/javascript/article-form/actions.js | 4 +- .../article-form/components/EditorBody.jsx | 43 +++----- .../components/dragAndDropHelpers.js | 13 ++- .../components/imageUploadHelpers.js | 97 +++++++++++++++++++ .../components/pasteImageHelpers.js | 13 ++- .../packs/CommentTextArea/CommentTextArea.jsx | 35 +++---- .../publishingFlows/uploadImage.spec.js | 4 + 7 files changed, 148 insertions(+), 61 deletions(-) create mode 100644 app/javascript/article-form/components/imageUploadHelpers.js diff --git a/app/javascript/article-form/actions.js b/app/javascript/article-form/actions.js index 287d7a6ea..82597b8cb 100644 --- a/app/javascript/article-form/actions.js +++ b/app/javascript/article-form/actions.js @@ -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, diff --git a/app/javascript/article-form/components/EditorBody.jsx b/app/javascript/article-form/components/EditorBody.jsx index 099dbe3e3..91486dbe9 100644 --- a/app/javascript/article-form/components/EditorBody.jsx +++ b/app/javascript/article-form/components/EditorBody.jsx @@ -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), ), }); diff --git a/app/javascript/article-form/components/dragAndDropHelpers.js b/app/javascript/article-form/components/dragAndDropHelpers.js index 865f10a76..aa13bf968 100644 --- a/app/javascript/article-form/components/dragAndDropHelpers.js +++ b/app/javascript/article-form/components/dragAndDropHelpers.js @@ -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, + ); }; } diff --git a/app/javascript/article-form/components/imageUploadHelpers.js b/app/javascript/article-form/components/imageUploadHelpers.js new file mode 100644 index 000000000..aae31148b --- /dev/null +++ b/app/javascript/article-form/components/imageUploadHelpers.js @@ -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; + } + }; +} diff --git a/app/javascript/article-form/components/pasteImageHelpers.js b/app/javascript/article-form/components/pasteImageHelpers.js index 9016bfe68..9b34e73fc 100644 --- a/app/javascript/article-form/components/pasteImageHelpers.js +++ b/app/javascript/article-form/components/pasteImageHelpers.js @@ -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, + ); }; } diff --git a/app/javascript/packs/CommentTextArea/CommentTextArea.jsx b/app/javascript/packs/CommentTextArea/CommentTextArea.jsx index d51ef84db..6ecfdce46 100644 --- a/app/javascript/packs/CommentTextArea/CommentTextArea.jsx +++ b/app/javascript/packs/CommentTextArea/CommentTextArea.jsx @@ -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), ), }); diff --git a/cypress/e2e/seededFlows/publishingFlows/uploadImage.spec.js b/cypress/e2e/seededFlows/publishingFlows/uploadImage.spec.js index adf9d32ad..5eb7d9ea0 100644 --- a/cypress/e2e/seededFlows/publishingFlows/uploadImage.spec.js +++ b/cypress/e2e/seededFlows/publishingFlows/uploadImage.spec.js @@ -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\]/);