From f822d95d6ae36359862c8836bce5a0212c7a7b15 Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Thu, 20 Jul 2023 17:07:35 +0530 Subject: [PATCH 1/2] fix: Used Dropzone instead of having custom component This PR fixes style component and remove any new component introduced. We introduce a new thumbnail for setting page as well. Signed-off-by: Farhaan Bukhsh --- package-lock.json | 2 +- .../components/VideoPreviewWidget/index.jsx | 6 +- .../VideoUploadEditor/VideoUploader.jsx | 78 +++ .../VideoUploadEditor/VideoUploader.test.jsx | 94 +++ .../__snapshots__/VideoUploader.test.jsx.snap | 302 +++++++++ .../__snapshots__/index.test.jsx.snap | 585 +++++++++++------- .../containers/VideoUploadEditor/hooks.js | 31 - .../VideoUploadEditor/hooks.test.js | 23 - .../containers/VideoUploadEditor/index.jsx | 110 +--- .../containers/VideoUploadEditor/index.scss | 34 +- .../VideoUploadEditor/index.test.jsx | 75 ++- .../containers/VideoUploadEditor/messages.js | 15 + src/editors/data/images/videoThumbnail.svg | 3 + src/editors/data/redux/thunkActions/video.js | 10 +- .../data/redux/thunkActions/video.test.js | 16 +- 15 files changed, 955 insertions(+), 429 deletions(-) create mode 100644 src/editors/containers/VideoUploadEditor/VideoUploader.jsx create mode 100644 src/editors/containers/VideoUploadEditor/VideoUploader.test.jsx create mode 100644 src/editors/containers/VideoUploadEditor/__snapshots__/VideoUploader.test.jsx.snap create mode 100644 src/editors/data/images/videoThumbnail.svg diff --git a/package-lock.json b/package-lock.json index 86ba5eba6..f98ee01bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23393,4 +23393,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx index 9f8d6c537..9cff6456e 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx @@ -9,6 +9,7 @@ import { selectors } from '../../../../../../data/redux'; import thumbnailMessages from '../ThumbnailWidget/messages'; import hooks from './hooks'; import LanguageNamesWidget from './LanguageNamesWidget'; +import videoThumbnail from '../../../../../../data/images/videoThumbnail.svg'; export const VideoPreviewWidget = ({ thumbnail, @@ -19,6 +20,7 @@ export const VideoPreviewWidget = ({ }) => { const imgRef = React.useRef(); const videoType = intl.formatMessage(hooks.getVideoType(videoSource)); + const thumbnailImage = thumbnail || videoThumbnail; return ( {intl.formatMessage(thumbnailMessages.thumbnailAltText)} { + const intl = useIntl(); + return ( +
+
+ +
+
+ {intl.formatMessage(messages.dropVideoFileHere)} + {intl.formatMessage(messages.info)} +
+
OR
+
+ ); +}; + +export const VideoUploader = ({ onUpload, setLoading }) => { + const [textInputValue, settextInputValue] = React.useState(''); + const onURLUpload = hooks.onVideoUpload(); + const intl = useIntl(); + + const handleProcessUpload = ({ fileData }) => { + dispatch(thunkActions.video.uploadVideo({ + supportedFiles: [fileData], + setLoadSpinner: setLoading, + postUploadRedirect: hooks.onVideoUpload(), + })); + }; + + return ( +
+ } + /> +
+ + { settextInputValue(event.target.value); }} + /> +
+ { onURLUpload(textInputValue); }} + /> +
+
+
+
+ ); +}; + +VideoUploader.propTypes = { + onUpload: PropTypes.func.isRequired, + setLoading: PropTypes.func.isRequired, +}; + +export default VideoUploader; diff --git a/src/editors/containers/VideoUploadEditor/VideoUploader.test.jsx b/src/editors/containers/VideoUploadEditor/VideoUploader.test.jsx new file mode 100644 index 000000000..06376c7f7 --- /dev/null +++ b/src/editors/containers/VideoUploadEditor/VideoUploader.test.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render, fireEvent, act } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { configureStore } from '@reduxjs/toolkit'; +import { AppProvider } from '@edx/frontend-platform/react'; +import '@testing-library/jest-dom'; +import * as redux from 'react-redux'; +import * as hooks from './hooks'; +import { VideoUploader } from './VideoUploader'; + +jest.unmock('react-redux'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.unmock('@edx/paragon'); +jest.unmock('@edx/paragon/icons'); + +describe('VideoUploader', () => { + const setLoadingMock = jest.fn(); + const onURLUploadMock = jest.fn(); + let store; + + beforeEach(async () => { + store = configureStore({ + reducer: (state, action) => ((action && action.newState) ? action.newState : state), + preloadedState: { + app: { + learningContextId: 'course-v1:test+test+test', + blockId: 'some-block-id', + }, + }, + }); + + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'test-user', + administrator: true, + roles: [], + }, + }); + }); + + const renderComponent = async (storeParam, setLoadingMockParam) => render( + + + + , + , + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected with default behavior', async () => { + expect(await renderComponent(store, setLoadingMock)).toMatchSnapshot(); + }); + + it('calls onURLUpload when URL submit button is clicked', async () => { + const onVideoUploadSpy = jest.spyOn(hooks, 'onVideoUpload').mockImplementation(() => onURLUploadMock); + + const { getByPlaceholderText, getAllByRole } = await renderComponent(store, setLoadingMock); + + const urlInput = getByPlaceholderText('Paste your video ID or URL'); + const urlSubmitButton = getAllByRole('button', { name: /submit/i }); + expect(urlSubmitButton).toHaveLength(1); + + fireEvent.change(urlInput, { target: { value: 'https://example.com/video.mp4' } }); + urlSubmitButton.forEach((button) => fireEvent.click(button)); + expect(onURLUploadMock).toHaveBeenCalledWith('https://example.com/video.mp4'); + + onVideoUploadSpy.mockRestore(); + }); + + it('calls handleProcessUpload when file is selected', async () => { + const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); + const mockDispatchFn = jest.fn(); + useDispatchSpy.mockReturnValue(mockDispatchFn); + + const { getByTestId } = await renderComponent(store, setLoadingMock); + + const fileInput = getByTestId('dropzone-container'); + const file = new File(['file'], 'video.mp4', { + type: 'video/mp4', + }); + Object.defineProperty(fileInput, 'files', { + value: [file], + }); + await act(async () => fireEvent.drop(fileInput)); + // Test dispacting thunkAction + expect(mockDispatchFn).toHaveBeenCalledWith(expect.any(Function)); + useDispatchSpy.mockRestore(); + }); +}); diff --git a/src/editors/containers/VideoUploadEditor/__snapshots__/VideoUploader.test.jsx.snap b/src/editors/containers/VideoUploadEditor/__snapshots__/VideoUploader.test.jsx.snap new file mode 100644 index 000000000..c0b7cb0ca --- /dev/null +++ b/src/editors/containers/VideoUploadEditor/__snapshots__/VideoUploader.test.jsx.snap @@ -0,0 +1,302 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VideoUploader renders as expected with default behavior 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ , +
+ , + "container":
+
+ +
+
+
+ +
+
+ +
+
+
+
+ , +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap index 9e3efe377..1196ae931 100644 --- a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap @@ -1,227 +1,380 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`VideoUploader snapshots renders as expected with default behavior 1`] = ` -
-
-
-
- -
-
- - - - +
+
+
- - -
-
- - OR - +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ ,
- -
-
-
- - -
-
-
-`; - -exports[`VideoUploader snapshots renders as expected with error message 1`] = ` -
-
-
-
- -
+ , + "container":
+
- - - - - - + +
+
+ +
+
+
+ +
+
+ +
+
+
+
-
- - OR - -
-
- -
-
-
- -
-
-
-`; - -exports[`VideoUploaderEdirtor snapshots renders as expected with default behavior 1`] = ` - - - + , +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} `; diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index b174075d4..b0571a03c 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -4,11 +4,6 @@ import { selectors } from '../../data/redux'; import store from '../../data/store'; import * as appHooks from '../../hooks'; -const extToMime = { - mp4: 'video/mp4', - mov: 'video/quicktime', -}; - export const { navigateTo, } = appHooks; @@ -17,19 +12,14 @@ export const state = { // eslint-disable-next-line react-hooks/rules-of-hooks loading: (val) => React.useState(val), // eslint-disable-next-line react-hooks/rules-of-hooks - errorMessage: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks textInputValue: (val) => React.useState(val), }; export const uploadEditor = () => { const [loading, setLoading] = module.state.loading(false); - const [errorMessage, setErrorMessage] = module.state.errorMessage(null); return { loading, setLoading, - errorMessage, - setErrorMessage, }; }; @@ -52,30 +42,9 @@ export const onVideoUpload = () => { return module.postUploadRedirect(storeState); }; -const getFileExtension = (filename) => filename.slice(Math.abs(filename.lastIndexOf('.') - 1) + 2); - -export const fileValidator = (setLoading, setErrorMessage, uploadVideo) => (file) => { - const supportedFormats = Object.keys(extToMime); - const ext = getFileExtension(file.name); - const type = extToMime[ext] || ''; - const newFile = new File([file], file.name, { type }); - - if (supportedFormats.includes(ext)) { - uploadVideo({ - supportedFiles: [newFile], - setLoadSpinner: setLoading, - postUploadRedirect: onVideoUpload(), - }); - } else { - const errorMsg = 'Video must be an MP4 or MOV file'; - setErrorMessage(errorMsg); - } -}; - export default { postUploadRedirect, uploadEditor, uploader, onVideoUpload, - fileValidator, }; diff --git a/src/editors/containers/VideoUploadEditor/hooks.test.js b/src/editors/containers/VideoUploadEditor/hooks.test.js index a78978601..3f2a5eb85 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.test.js +++ b/src/editors/containers/VideoUploadEditor/hooks.test.js @@ -2,9 +2,6 @@ import * as hooks from './hooks'; import { MockUseState } from '../../../testUtils'; const state = new MockUseState(hooks); -const setLoading = jest.fn(); -const setErrorMessage = jest.fn(); -const uploadVideo = jest.fn(); describe('Video Upload Editor hooks', () => { beforeEach(() => { @@ -12,8 +9,6 @@ describe('Video Upload Editor hooks', () => { }); describe('state hooks', () => { state.testGetter(state.keys.loading); - state.testGetter(state.keys.errorMessage); - state.testGetter(state.keys.textInputValue); }); describe('using state', () => { beforeEach(() => { state.mock(); }); @@ -26,25 +21,7 @@ describe('Video Upload Editor hooks', () => { }); it('initialize state with correct values', () => { expect(state.stateVals.loading).toEqual(false); - expect(state.stateVals.errorMessage).toEqual(null); - expect(state.stateVals.textInputValue).toEqual(''); }); }); }); - describe('File Validation', () => { - it('Checks with valid MIME type', () => { - const file = new File(['(⌐□_□)'], 'video.mp4', { type: 'video/mp4' }); - const validator = hooks.fileValidator(setLoading, setErrorMessage, uploadVideo); - validator(file); - expect(uploadVideo).toHaveBeenCalled(); - expect(setErrorMessage).not.toHaveBeenCalled(); - }); - it('Checks with invalid MIME type', () => { - const file = new File(['(⌐□_□)'], 'video.gif', { type: 'video/mp4' }); - const validator = hooks.fileValidator(setLoading, setErrorMessage, uploadVideo); - validator(file); - expect(uploadVideo).not.toHaveBeenCalled(); - expect(setErrorMessage).toHaveBeenCalled(); - }); - }); }); diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 853b247da..67ebde6cb 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -1,113 +1,29 @@ 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, Spinner } from '@edx/paragon'; -import { ArrowForward, Close, FileUpload } from '@edx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Icon, IconButton, Spinner, +} from '@edx/paragon'; +import { Close } 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 * as editorHooks from '../EditorContainer/hooks'; +import { VideoUploader } from './VideoUploader'; +import * as editorHooks from '../EditorContainer/hooks'; -export const VideoUploader = ({ onUpload, errorMessage }) => { - const { textInputValue, setTextInputValue } = hooks.uploader(); - const onURLUpload = hooks.onVideoUpload(); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - accept: 'video/*', - multiple: false, - onDrop: (acceptedFiles) => { - if (acceptedFiles.length > 0) { - const uploadfile = acceptedFiles[0]; - onUpload(uploadfile); - } - }, - }); - - const handleInputChange = (event) => { - setTextInputValue(event.target.value); - }; - - const handleSaveButtonClick = () => { - onURLUpload(textInputValue); - }; - - if (errorMessage) { - return ( -
{errorMessage}
- ); - } - - return ( -
-
-
-
- -
-
- - -
-
- OR -
-
- -
-
-
- e.key === 'Enter' && handleSaveButtonClick()} - onClick={(event) => event.preventDefault()} - /> - -
-
-
- ); -}; - -VideoUploader.propTypes = { - onUpload: PropTypes.func.isRequired, - errorMessage: PropTypes.string.isRequired, - intl: intlShape.isRequired, -}; - -const VideoUploadEditor = ( +export const VideoUploadEditor = ( { - intl, onClose, // Redux states uploadVideo, }, ) => { - const { - loading, - setLoading, - errorMessage, - setErrorMessage, - } = hooks.uploadEditor(); + const [loading, setLoading] = React.useState(false); const handleCancel = editorHooks.handleCancel({ onClose }); - - const handleDrop = (file) => { - if (!file) { - console.log('No file selected.'); - return; - } - const validator = hooks.fileValidator(setLoading, setErrorMessage, uploadVideo); - validator(file); - }; + const intl = useIntl(); return (
@@ -115,12 +31,13 @@ const VideoUploadEditor = (
- +
) : (
@@ -136,7 +53,6 @@ const VideoUploadEditor = ( }; VideoUploadEditor.propTypes = { - intl: intlShape.isRequired, onClose: PropTypes.func.isRequired, uploadVideo: PropTypes.func.isRequired, }; @@ -147,4 +63,4 @@ export const mapDispatchToProps = { uploadVideo: thunkActions.video.uploadVideo, }; -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(VideoUploadEditor)); +export default connect(mapStateToProps, mapDispatchToProps)(VideoUploadEditor); diff --git a/src/editors/containers/VideoUploadEditor/index.scss b/src/editors/containers/VideoUploadEditor/index.scss index b48aa025e..5e19763b1 100644 --- a/src/editors/containers/VideoUploadEditor/index.scss +++ b/src/editors/containers/VideoUploadEditor/index.scss @@ -4,11 +4,11 @@ &.active { border: 2px solid #262626; /* change color when active */ } +} - .file-upload { - width: 56px; - height: 56px; - } +.pgn__dropzone { + height: 100vh; + width: 100%; } .video-id-container { @@ -16,33 +16,33 @@ justify-content: center; } +.url-submit-button { + position: absolute; + left: 85%; +} + .video-id-prompt { position: absolute; - top: 68%; + top: 65%; left: 50%; transform: translateX(-50%); - margin-top: 1rem; - border: 1px solid #707070; - width: 308px; - - input { - border: none !important; - width: 90%; - } input::placeholder { color: #454545; + // color: #5E35B1; + font-family: 'Inter'; + font-weight: 500; + word-wrap: break-word; } button { border: none !important; background-color: #FFFFFF; } -} -.error-message { - color: #AB0D02; - margin-top: 20rem; + .prompt-button { + background: rgba(239, 234, 247, 0.70); + } } .close-button-container { diff --git a/src/editors/containers/VideoUploadEditor/index.test.jsx b/src/editors/containers/VideoUploadEditor/index.test.jsx index 3b6e97c90..6f3e0a1ff 100644 --- a/src/editors/containers/VideoUploadEditor/index.test.jsx +++ b/src/editors/containers/VideoUploadEditor/index.test.jsx @@ -1,37 +1,54 @@ import React from 'react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom'; -import { shallow } from 'enzyme'; -import VideoUploadEditor, { VideoUploader } from '.'; -import { formatMessage } from '../../../testUtils'; - -const defaultEditorProps = { - onClose: jest.fn().mockName('props.onClose'), - intl: { formatMessage }, - uploadVideo: jest.fn(), -}; - -const defaultUploaderProps = { - onUpload: jest.fn(), - errorMessage: null, - intl: { formatMessage }, -}; - -describe('VideoUploader', () => { - describe('snapshots', () => { - test('renders as expected with default behavior', () => { - expect(shallow()).toMatchSnapshot(); +import VideoUploadEditor from '.'; + +jest.unmock('react-redux'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.unmock('@edx/paragon'); +jest.unmock('@edx/paragon/icons'); + +describe('VideoUploadEditor', () => { + const onCloseMock = jest.fn(); + let store; + + const renderComponent = async (storeParam, onCloseMockParam) => render( + + + + , + , + ); + + beforeEach(async () => { + store = configureStore({ + reducer: (state, action) => ((action && action.newState) ? action.newState : state), + preloadedState: {}, }); - test('renders as expected with error message', () => { - const defaultUploaderPropsWithError = { ...defaultUploaderProps, errorMessages: 'Some Error' }; - expect(shallow()).toMatchSnapshot(); + + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'test-user', + administrator: true, + roles: [], + }, }); }); -}); -describe('VideoUploaderEdirtor', () => { - describe('snapshots', () => { - test('renders as expected with default behavior', () => { - expect(shallow()).toMatchSnapshot(); - }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected with default behavior', async () => { + expect(await renderComponent(store, onCloseMock)).toMatchSnapshot(); + }); + + it('calls onClose when close button is clicked', async () => { + const container = await renderComponent(store, onCloseMock); + const closeButton = container.getAllByRole('button', { name: /close/i }); + expect(closeButton).toHaveLength(1); + closeButton.forEach((button) => fireEvent.click(button)); + expect(onCloseMock).toHaveBeenCalled(); }); }); diff --git a/src/editors/containers/VideoUploadEditor/messages.js b/src/editors/containers/VideoUploadEditor/messages.js index 134962cd4..0a071e53c 100644 --- a/src/editors/containers/VideoUploadEditor/messages.js +++ b/src/editors/containers/VideoUploadEditor/messages.js @@ -16,6 +16,21 @@ const messages = defineMessages({ defaultMessage: 'Upload MP4 or MOV files (5 GB max)', description: 'Info message for supported formats', }, + pasteURL: { + id: 'VideoUploadEditor.pasteURL', + defaultMessage: 'Paste your video ID or URL', + description: 'Paste URL message for video upload', + }, + closeButtonAltText: { + id: 'VideoUploadEditor.closeButtonAltText', + defaultMessage: 'Close', + description: 'Close button alt text', + }, + submitButtonAltText: { + id: 'VideoUploadEditor.submitButtonAltText', + defaultMessage: 'Submit', + description: 'Submit button alt text', + }, }); export default messages; diff --git a/src/editors/data/images/videoThumbnail.svg b/src/editors/data/images/videoThumbnail.svg new file mode 100644 index 000000000..52fac3a82 --- /dev/null +++ b/src/editors/data/images/videoThumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 87fdc4a8e..33d0c09e6 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -1,4 +1,5 @@ /* eslint-disable import/no-cycle */ +import _ from 'lodash-es'; import { actions, selectors } from '..'; import { removeItemOnce } from '../../../utils'; import * as requests from './requests'; @@ -377,9 +378,10 @@ export const uploadVideo = ({ supportedFiles, setLoadSpinner, postUploadRedirect const data = { files: [] }; setLoadSpinner(true); supportedFiles.forEach((file) => { + const fileData = file.get('file'); data.files.push({ - file_name: file.name, - content_type: file.type, + file_name: fileData.name, + content_type: fileData.type, }); }); dispatch(requests.uploadVideo({ @@ -390,13 +392,13 @@ export const uploadVideo = ({ supportedFiles, setLoadSpinner, postUploadRedirect const fileName = fileObj.file_name; const edxVideoId = fileObj.edx_video_id; const uploadUrl = fileObj.upload_url; - const uploadFile = supportedFiles.find((file) => file.name === fileName); + const uploadFile = supportedFiles.find((file) => file.get('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); + formData.append('uploaded-file', uploadFile.get('file')); await fetch(uploadUrl, { method: 'PUT', body: formData, diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index a8b0b177f..8563c039e 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -676,10 +676,9 @@ describe('uploadVideo', () => { let setLoadSpinner; let postUploadRedirect; let dispatchedAction; - const supportedFiles = [ - new File(['content1'], 'file1.mp4', { type: 'video/mp4' }), - new File(['content2'], 'file2.mov', { type: 'video/quicktime' }), - ]; + const fileData = new FormData(); + fileData.append('file', new File(['content1'], 'file1.mp4', { type: 'video/mp4' })); + const supportedFiles = [fileData]; beforeEach(() => { dispatch = jest.fn((action) => ({ dispatch: action })); @@ -693,7 +692,6 @@ describe('uploadVideo', () => { const data = { files: [ { file_name: 'file1.mp4', content_type: 'video/mp4' }, - { file_name: 'file2.mov', content_type: 'video/quicktime' }, ], }; @@ -711,7 +709,6 @@ describe('uploadVideo', () => { const response = { files: [ { file_name: 'file1.mp4', upload_url: 'http://example.com/put_video1' }, - { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; const mockRequestResponse = { data: response }; @@ -720,12 +717,13 @@ describe('uploadVideo', () => { dispatchedAction.uploadVideo.onSuccess(mockRequestResponse); - expect(fetch).toHaveBeenCalledTimes(2); + expect(fetch).toHaveBeenCalledTimes(1); response.files.forEach(({ upload_url: uploadUrl }, index) => { expect(fetch.mock.calls[index][0]).toEqual(uploadUrl); }); supportedFiles.forEach((file, index) => { - expect(fetch.mock.calls[index][1].body.get('uploaded-file')).toBe(file); + const fileDataTest = file.get('file'); + expect(fetch.mock.calls[index][1].body.get('uploaded-file')).toBe(fileDataTest); }); }); @@ -741,7 +739,7 @@ describe('uploadVideo', () => { const mockRequestResponse = { data: response }; const spyConsoleError = jest.spyOn(console, 'error'); - thunkActions.uploadVideo({ supportedFiles: [supportedFiles[0]], setLoadSpinner, postUploadRedirect })(dispatch); + thunkActions.uploadVideo({ supportedFiles, setLoadSpinner, postUploadRedirect })(dispatch); dispatchedAction.uploadVideo.onSuccess(mockRequestResponse); expect(spyConsoleError).toHaveBeenCalledWith('Could not find file object with name "file2.gif" in supportedFiles array.'); }); From b7a04e17da92b662777c00f29559349a768e938b Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Wed, 16 Aug 2023 17:20:19 +0530 Subject: [PATCH 2/2] fix: Fixing the accessing of undefined variables in video Signed-off-by: Farhaan Bukhsh --- .../VideoUploadEditor/VideoUploader.jsx | 8 +-- .../containers/VideoUploadEditor/hooks.js | 42 +++++--------- .../VideoUploadEditor/hooks.test.js | 27 --------- .../containers/VideoUploadEditor/index.jsx | 17 +----- .../VideoUploadEditor/index.test.jsx | 4 ++ src/editors/data/redux/thunkActions/video.js | 14 +++-- .../data/redux/thunkActions/video.test.js | 56 +++++++++++++++++++ 7 files changed, 91 insertions(+), 77 deletions(-) delete mode 100644 src/editors/containers/VideoUploadEditor/hooks.test.js diff --git a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx index ca5298b41..d3705d8b3 100644 --- a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx +++ b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx @@ -26,10 +26,11 @@ const URLUploader = () => { ); }; -export const VideoUploader = ({ onUpload, setLoading }) => { - const [textInputValue, settextInputValue] = React.useState(''); +export const VideoUploader = ({ setLoading }) => { + const [textInputValue, setTextInputValue] = React.useState(''); const onURLUpload = hooks.onVideoUpload(); const intl = useIntl(); + const dispatch = useDispatch(); const handleProcessUpload = ({ fileData }) => { dispatch(thunkActions.video.uploadVideo({ @@ -53,7 +54,7 @@ export const VideoUploader = ({ onUpload, setLoading }) => { aria-label={intl.formatMessage(messages.pasteURL)} aria-describedby="basic-addon2" borderless - onChange={(event) => { settextInputValue(event.target.value); }} + onChange={(event) => { setTextInputValue(event.target.value); }} />
{ }; VideoUploader.propTypes = { - onUpload: PropTypes.func.isRequired, setLoading: PropTypes.func.isRequired, }; diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index b0571a03c..7771f2d48 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -1,6 +1,5 @@ -import React from 'react'; import * as module from './hooks'; -import { selectors } from '../../data/redux'; +import { selectors, thunkActions } from '../../data/redux'; import store from '../../data/store'; import * as appHooks from '../../hooks'; @@ -8,29 +7,6 @@ export const { navigateTo, } = appHooks; -export const state = { - // eslint-disable-next-line react-hooks/rules-of-hooks - loading: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - textInputValue: (val) => React.useState(val), -}; - -export const uploadEditor = () => { - const [loading, setLoading] = module.state.loading(false); - return { - loading, - setLoading, - }; -}; - -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); @@ -42,9 +18,21 @@ export const onVideoUpload = () => { return module.postUploadRedirect(storeState); }; +export const useUploadVideo = async ({ + dispatch, + supportedFiles, + setLoadSpinner, + postUploadRedirectFunction, +}) => { + dispatch(thunkActions.video.uploadVideo({ + supportedFiles, + setLoadSpinner, + postUploadRedirectFunction, + })); +}; + export default { postUploadRedirect, - uploadEditor, - uploader, onVideoUpload, + useUploadVideo, }; diff --git a/src/editors/containers/VideoUploadEditor/hooks.test.js b/src/editors/containers/VideoUploadEditor/hooks.test.js deleted file mode 100644 index 3f2a5eb85..000000000 --- a/src/editors/containers/VideoUploadEditor/hooks.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import * as hooks from './hooks'; -import { MockUseState } from '../../../testUtils'; - -const state = new MockUseState(hooks); - -describe('Video Upload Editor hooks', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('state hooks', () => { - state.testGetter(state.keys.loading); - }); - describe('using state', () => { - beforeEach(() => { state.mock(); }); - afterEach(() => { state.restore(); }); - - describe('Hooks for Video Upload', () => { - beforeEach(() => { - hooks.uploadEditor(); - hooks.uploader(); - }); - it('initialize state with correct values', () => { - expect(state.stateVals.loading).toEqual(false); - }); - }); - }); -}); diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 67ebde6cb..3080e9f2b 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -5,20 +5,14 @@ import { Icon, IconButton, Spinner, } from '@edx/paragon'; import { Close } 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 * as editorHooks from '../EditorContainer/hooks'; import { VideoUploader } from './VideoUploader'; import * as editorHooks from '../EditorContainer/hooks'; export const VideoUploadEditor = ( { onClose, - // Redux states - uploadVideo, }, ) => { const [loading, setLoading] = React.useState(false); @@ -37,7 +31,7 @@ export const VideoUploadEditor = ( onClick={handleCancel} />
- +
) : (
@@ -54,13 +48,6 @@ export const VideoUploadEditor = ( VideoUploadEditor.propTypes = { onClose: PropTypes.func.isRequired, - uploadVideo: PropTypes.func.isRequired, -}; - -export const mapStateToProps = () => ({}); - -export const mapDispatchToProps = { - uploadVideo: thunkActions.video.uploadVideo, }; -export default connect(mapStateToProps, mapDispatchToProps)(VideoUploadEditor); +export default VideoUploadEditor; diff --git a/src/editors/containers/VideoUploadEditor/index.test.jsx b/src/editors/containers/VideoUploadEditor/index.test.jsx index 6f3e0a1ff..bf032cd70 100644 --- a/src/editors/containers/VideoUploadEditor/index.test.jsx +++ b/src/editors/containers/VideoUploadEditor/index.test.jsx @@ -1,5 +1,9 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import VideoUploadEditor from '.'; diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 33d0c09e6..f42dab84f 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -11,10 +11,16 @@ export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, g const state = getState(); const blockValueData = state.app.blockValue.data; let rawVideoData = blockValueData.metadata ? blockValueData.metadata : {}; - if (selectedVideoId != null) { - const rawVideos = Object.values(selectors.app.videos(state)); - const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId); - if (selectedVideo) { + const rawVideos = Object.values(selectors.app.videos(state)); + if (selectedVideoId !== undefined && selectedVideoId !== null) { + const selectedVideo = _.find(rawVideos, video => { + if (_.has(video, 'edx_video_id')) { + return video.edx_video_id === selectedVideoId; + } + return false; + }); + + if (selectedVideo !== undefined && selectedVideo !== null) { rawVideoData = { edx_video_id: selectedVideo.edx_video_id, thumbnail: selectedVideo.course_video_image_url, diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 8563c039e..b91316661 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -88,6 +88,7 @@ const testState = { const testVideosState = { edx_video_id: mockSelectedVideoId, thumbnail: 'thumbnail', + course_video_image_url: 'course_video_image_url', duration: 60, transcripts: ['es'], transcript_urls: { es: 'url' }, @@ -262,6 +263,61 @@ describe('video thunkActions', () => { noDerivatives: true, shareAlike: false, }, + thumbnail: testVideosState.course_video_image_url, + }); + }); + it('dispatches actions.video.load with different selectedVideoId', () => { + getState = jest.fn(() => ({ + app: { + blockId: 'soMEBloCk', + studioEndpointUrl: 'soMEeNDPoiNT', + blockValue: { data: { metadata: {} } }, + courseDetails: { data: { license: null } }, + studioView: { data: { html: 'sOMeHTml' } }, + videos: testVideosState, + }, + })); + thunkActions.loadVideoData('ThisIsAVideoId2', null)(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; + expect(dispatchedLoad.load).toEqual({ + videoSource: 'videOsOurce', + videoId: 'videOiD', + fallbackVideos: 'fALLbACKvIDeos', + allowVideoDownloads: undefined, + transcripts: testMetadata.transcripts, + selectedVideoTranscriptUrls: testMetadata.transcript_urls, + allowTranscriptDownloads: undefined, + allowVideoSharing: { + level: 'course', + value: true, + }, + showTranscriptByDefault: undefined, + duration: { + startTime: testMetadata.start_time, + stopTime: 0, + total: 0, + }, + handout: undefined, + licenseType: 'liCENSEtyPe', + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + videoSharingEnabledForCourse: undefined, + videoSharingLearnMoreLink: undefined, + courseLicenseType: 'liCENSEtyPe', + courseLicenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, thumbnail: undefined, }); });