From a86b844208f2e6abac8b9c29e6c626ed60d9fdcb Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Sat, 17 Jun 2023 06:15:38 +0530 Subject: [PATCH] feat: Add spinner to video element to load Signed-off-by: Farhaan Bukhsh --- .../containers/VideoUploadEditor/hooks.js | 84 ++++++++---------- .../containers/VideoUploadEditor/index.jsx | 86 +++++++++++++------ .../containers/VideoUploadEditor/messages.js | 21 +++++ src/editors/data/redux/thunkActions/video.js | 41 +++++++++ 4 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 src/editors/containers/VideoUploadEditor/messages.js diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index 7db83f47b..f7fde9866 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -1,4 +1,4 @@ -import * as requests from '../../data/redux/thunkActions/requests'; +import React from 'react'; import * as module from './hooks'; import { selectors } from '../../data/redux'; import store from '../../data/store'; @@ -8,59 +8,45 @@ export const { navigateTo, } = appHooks; -export const uploadVideo = async ({ dispatch, supportedFiles }) => { - const data = { files: [] }; - supportedFiles.forEach((file) => { - data.files.push({ - file_name: file.name, - content_type: file.type, - }); - }); - const onFileUploadedHook = module.onFileUploaded(); - dispatch(await requests.uploadVideo({ - data, - onSuccess: async (response) => { - const { files } = response.data; - await Promise.all(Object.values(files).map(async (fileObj) => { - const fileName = fileObj.file_name; - const edxVideoId = fileObj.edx_video_id; - const uploadUrl = fileObj.upload_url; - const uploadFile = supportedFiles.find((file) => file.name === fileName); - - if (!uploadFile) { - console.error(`Could not find file object with name "${fileName}" in supportedFiles array.`); - return; - } - const formData = new FormData(); - formData.append('uploaded-file', uploadFile); - await fetch(uploadUrl, { - method: 'PUT', - body: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - .then(() => onFileUploadedHook(edxVideoId)) - .catch((error) => console.error('Error uploading file:', error)); - })); - }, - })); +export const state = { + loading: (val) => React.useState(val), + errorMessage: (val) => React.useState(val), + textInputValue: (val) => React.useState(val), }; -export const onFileUploaded = () => { - const state = store.getState(); - const learningContextId = selectors.app.learningContextId(state); - const blockId = selectors.app.blockId(state); - return (edxVideoId) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${edxVideoId}`); +export const uploadEditor = () => { + const [loading, setLoading] = module.state.loading(false); + const [errorMessage, setErrorMessage] = module.state.errorMessage(null); + return { + loading, + setLoading, + errorMessage, + setErrorMessage, + }; }; -export const onUrlUploaded = () => { - const state = store.getState(); - const learningContextId = selectors.app.learningContextId(state); - const blockId = selectors.app.blockId(state); +export const uploader = () => { + const [textInputValue, settextInputValue] = module.state.textInputValue(''); + return { + textInputValue, + settextInputValue, + }; +}; + +export const postUploadRedirect = (storeState) => { + const learningContextId = selectors.app.learningContextId(storeState); + const blockId = selectors.app.blockId(storeState); return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoUrl=${videoUrl}`); }; -export default { - uploadVideo, +export const onVideoUpload = () => { + const storeState = store.getState(); + return module.postUploadRedirect(storeState); +}; + +export default { + postUploadRedirect, + uploadEditor, + uploader, + onVideoUpload, }; diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 54780e719..d88c46a7b 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -1,19 +1,19 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { useDropzone } from 'react-dropzone'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Icon, IconButton } from '@edx/paragon'; -import './index.scss'; -import { useDispatch } from 'react-redux'; +import { Icon, IconButton, Spinner } from '@edx/paragon'; import { ArrowForward, Close, FileUpload } from '@edx/paragon/icons'; +import { connect } from 'react-redux'; +import { thunkActions } from '../../data/redux'; +import './index.scss'; import * as hooks from './hooks'; -import messages from '../../messages'; +import messages from './messages'; import * as editorHooks from '../EditorContainer/hooks'; export const VideoUploader = ({ onUpload, errorMessage }) => { - const [, setUploadedFile] = useState(); - const [textInputValue, setTextInputValue] = useState(''); - const onUrlUpdatedHook = hooks.onUrlUploaded(); + const { textInputValue, setTextInputValue } = hooks.uploader(); + const onURLUpload = hooks.onVideoUpload(); const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: 'video/*', @@ -21,7 +21,6 @@ export const VideoUploader = ({ onUpload, errorMessage }) => { onDrop: (acceptedFiles) => { if (acceptedFiles.length > 0) { const uploadfile = acceptedFiles[0]; - setUploadedFile(uploadfile); onUpload(uploadfile); } }, @@ -32,7 +31,7 @@ export const VideoUploader = ({ onUpload, errorMessage }) => { }; const handleSaveButtonClick = () => { - onUrlUpdatedHook(textInputValue); + onURLUpload(textInputValue); }; if (errorMessage) { @@ -85,12 +84,21 @@ VideoUploader.propTypes = { intl: intlShape.isRequired, }; -const VideoUploadEditor = ({ intl, onClose }) => { - const dispatch = useDispatch(); - const [errorMessage, setErrorMessage] = useState(null); - const handleCancel = () => { - editorHooks.handleCancel({ onClose }); - }; +const VideoUploadEditor = ( + { + intl, + onClose, + // Redux states + uploadVideo, + }, +) => { + const { + loading, + setLoading, + errorMessage, + setErrorMessage, + } = hooks.uploadEditor(); + const handleCancel = editorHooks.handleCancel({ onClose }); const handleDrop = (file) => { if (!file) { @@ -113,24 +121,39 @@ const VideoUploadEditor = ({ intl, onClose }) => { const newFile = new File([file], file.name, { type }); if (supportedFormats.includes(ext)) { - hooks.uploadVideo({ dispatch, supportedFiles: [newFile] }); + uploadVideo({ + supportedFiles: [newFile], + setLoadSpinner: setLoading, + postUploadRedirect: hooks.onVideoUpload(), + }); } else { const errorMsg = 'Video must be an MP4 or MOV file'; - console.log(errorMsg); setErrorMessage(errorMsg); } }; return ( -
-
- -
- +
+ {(!loading) ? ( +
+
+ +
+ +
+ ) : ( +
+ +
+ )}
); }; @@ -138,6 +161,13 @@ const VideoUploadEditor = ({ intl, onClose }) => { VideoUploadEditor.propTypes = { intl: intlShape.isRequired, onClose: PropTypes.func.isRequired, + uploadVideo: PropTypes.func.isRequired, }; -export default injectIntl(VideoUploadEditor); +export const mapStateToProps = () => ({}); + +export const mapDispatchToProps = { + uploadVideo: thunkActions.video.uploadVideo, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(VideoUploadEditor)); diff --git a/src/editors/containers/VideoUploadEditor/messages.js b/src/editors/containers/VideoUploadEditor/messages.js new file mode 100644 index 000000000..134962cd4 --- /dev/null +++ b/src/editors/containers/VideoUploadEditor/messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + spinnerScreenReaderText: { + id: 'authoring.videoUpload.spinnerScreenReaderText', + defaultMessage: 'loading', + description: 'Loading message for spinner screenreader text.', + }, + dropVideoFileHere: { + defaultMessage: 'Drag and drop video here or click to upload', + id: 'VideoUploadEditor.dropVideoFileHere', + description: 'Display message for Drag and Drop zone', + }, + info: { + id: 'VideoUploadEditor.uploadInfo', + defaultMessage: 'Upload MP4 or MOV files (5 GB max)', + description: 'Info message for supported formats', + }, +}); + +export default messages; diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index d407b9d48..194ae5668 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -370,6 +370,46 @@ export const replaceTranscript = ({ newFile, newFilename, language }) => (dispat })); }; +export const uploadVideo = ({ supportedFiles, setLoadSpinner, postUploadRedirect }) => (dispatch) => { + const data = { files: [] }; + setLoadSpinner(true); + supportedFiles.forEach((file) => { + data.files.push({ + file_name: file.name, + content_type: file.type, + }); + }); + dispatch(requests.uploadVideo({ + data, + onSuccess: async (response) => { + const { files } = response.data; + await Promise.all(Object.values(files).map(async (fileObj) => { + const fileName = fileObj.file_name; + const edxVideoId = fileObj.edx_video_id; + const uploadUrl = fileObj.upload_url; + const uploadFile = supportedFiles.find((file) => file.name === fileName); + + if (!uploadFile) { + console.error(`Could not find file object with name "${fileName}" in supportedFiles array.`); + return; + } + const formData = new FormData(); + formData.append('uploaded-file', uploadFile); + await fetch(uploadUrl, { + method: 'PUT', + body: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then(() => postUploadRedirect(edxVideoId)) + .catch((error) => console.error('Error uploading file:', error)); + })); + setLoadSpinner(false); + }, + })); +}; + export default { loadVideoData, determineVideoSources, @@ -382,4 +422,5 @@ export default { updateTranscriptLanguage, replaceTranscript, uploadHandout, + uploadVideo, };