Merge pull request #262 from open-craft/pooja/bb7157-add-video-upload-feature
[BB-7157] Create new editor page for video upload
This commit is contained in:
72045
package-lock.json
generated
72045
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
@@ -72,6 +72,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VideoUploadEditor renders without errors 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="marked-area"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-content-end close-button-container"
|
||||
>
|
||||
<iconbutton
|
||||
iconas="Icon"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center align-items-center p-4 w-100 min-vh-100"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center align-items-center gap-2 text-center min-vh-100 w-100
|
||||
dropzone-middle "
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center bg-light rounded-circle file-upload"
|
||||
>
|
||||
<icon
|
||||
class="text-muted"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center justify-content-center gap-1 flex-wrap flex-column pt-5"
|
||||
>
|
||||
<span
|
||||
style="font-size: 20px;"
|
||||
>
|
||||
FormattedMessage
|
||||
</span>
|
||||
<span
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
FormattedMessage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mt-3"
|
||||
>
|
||||
<span
|
||||
class="mx-2 text-dark"
|
||||
>
|
||||
OR
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
accept=""
|
||||
data-testid="fileInput"
|
||||
style="display: none;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
>
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="border-start-0"
|
||||
type="button"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VideoUploader renders without errors 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center align-items-center p-4 w-100 min-vh-100"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center align-items-center gap-2 text-center min-vh-100 w-100
|
||||
dropzone-middle "
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center bg-light rounded-circle file-upload"
|
||||
>
|
||||
<icon
|
||||
class="text-muted"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center justify-content-center gap-1 flex-wrap flex-column pt-5"
|
||||
>
|
||||
<span
|
||||
style="font-size: 20px;"
|
||||
>
|
||||
FormattedMessage
|
||||
</span>
|
||||
<span
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
FormattedMessage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mt-3"
|
||||
>
|
||||
<span
|
||||
class="mx-2 text-dark"
|
||||
>
|
||||
OR
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
accept=""
|
||||
data-testid="fileInput"
|
||||
style="display: none;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
>
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="border-start-0"
|
||||
type="button"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
42
src/editors/containers/VideoUploadEditor/hooks.js
Normal file
42
src/editors/containers/VideoUploadEditor/hooks.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as requests from '../../data/redux/thunkActions/requests';
|
||||
|
||||
export const uploadVideo = async ({ dispatch, supportedFiles }) => {
|
||||
const data = { files: [] };
|
||||
supportedFiles.forEach((file) => {
|
||||
data.files.push({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
});
|
||||
});
|
||||
dispatch(await requests.uploadVideo({
|
||||
data,
|
||||
onSuccess: async (response) => {
|
||||
const { files } = response.json();
|
||||
await Promise.all(Object.values(files).map(async (fileObj) => {
|
||||
const fileName = fileObj.file_name;
|
||||
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((resp) => resp.json())
|
||||
.then((responseData) => console.log('File uploaded:', responseData))
|
||||
.catch((error) => console.error('Error uploading file:', error));
|
||||
}));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export default {
|
||||
uploadVideo,
|
||||
};
|
||||
99
src/editors/containers/VideoUploadEditor/hooks.test.js
Normal file
99
src/editors/containers/VideoUploadEditor/hooks.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import hooks from './hooks';
|
||||
import * as requests from '../../data/redux/thunkActions/requests';
|
||||
|
||||
jest.mock('../../data/redux/thunkActions/requests');
|
||||
|
||||
describe('uploadVideo', () => {
|
||||
const dispatch = jest.fn();
|
||||
const supportedFiles = [
|
||||
new File(['content1'], 'file1.mp4', { type: 'video/mp4' }),
|
||||
new File(['content2'], 'file2.mov', { type: 'video/quicktime' }),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should dispatch uploadVideo action with correct data and onSuccess callback', async () => {
|
||||
requests.uploadVideo.mockImplementation(() => 'requests.uploadVideo');
|
||||
const data = {
|
||||
files: [
|
||||
{ file_name: 'file1.mp4', content_type: 'video/mp4' },
|
||||
{ file_name: 'file2.mov', content_type: 'video/quicktime' },
|
||||
],
|
||||
};
|
||||
|
||||
await hooks.uploadVideo({ dispatch, supportedFiles });
|
||||
|
||||
expect(requests.uploadVideo).toHaveBeenCalledWith({ data, onSuccess: expect.any(Function) });
|
||||
expect(dispatch).toHaveBeenCalledWith('requests.uploadVideo');
|
||||
});
|
||||
|
||||
it('should call fetch with correct arguments for each file', async () => {
|
||||
const mockResponseData = { success: true };
|
||||
const mockFetchResponse = Promise.resolve({ json: () => Promise.resolve(mockResponseData) });
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
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 spyConsoleLog = jest.spyOn(console, 'log');
|
||||
const mockRequestResponse = { json: () => response };
|
||||
requests.uploadVideo.mockImplementation(async ({ onSuccess }) => {
|
||||
await onSuccess(mockRequestResponse);
|
||||
});
|
||||
|
||||
await hooks.uploadVideo({ dispatch, supportedFiles });
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleLog).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleLog).toHaveBeenCalledWith('File uploaded:', mockResponseData);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should log an error if fetch failed to upload a file', async () => {
|
||||
const error = new Error('Uh-oh!');
|
||||
global.fetch = jest.fn().mockRejectedValue(error);
|
||||
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 spyConsoleError = jest.spyOn(console, 'error');
|
||||
const mockRequestResponse = { json: () => response };
|
||||
requests.uploadVideo.mockImplementation(async ({ onSuccess }) => {
|
||||
await onSuccess(mockRequestResponse);
|
||||
});
|
||||
|
||||
await hooks.uploadVideo({ dispatch, supportedFiles });
|
||||
|
||||
expect(spyConsoleError).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleError).toHaveBeenCalledWith('Error uploading file:', error);
|
||||
});
|
||||
|
||||
it('should log an error if file object is not found in supportedFiles array', () => {
|
||||
const response = {
|
||||
files: [
|
||||
{ file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' },
|
||||
],
|
||||
};
|
||||
const mockRequestResponse = { json: () => response };
|
||||
const spyConsoleError = jest.spyOn(console, 'error');
|
||||
requests.uploadVideo.mockImplementation(({ onSuccess }) => {
|
||||
onSuccess(mockRequestResponse);
|
||||
});
|
||||
|
||||
hooks.uploadVideo({ dispatch, supportedFiles: [supportedFiles[0]] });
|
||||
|
||||
expect(spyConsoleError).toHaveBeenCalledWith('Could not find file object with name "file2.mov" in supportedFiles array.');
|
||||
});
|
||||
});
|
||||
141
src/editors/containers/VideoUploadEditor/index.jsx
Normal file
141
src/editors/containers/VideoUploadEditor/index.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState } 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 { ArrowForward, Close, FileUpload } from '@edx/paragon/icons';
|
||||
import * as hooks from './hooks';
|
||||
import messages from '../../messages';
|
||||
import * as editorHooks from '../EditorContainer/hooks';
|
||||
|
||||
export const VideoUploader = ({ onUpload, errorMessage }) => {
|
||||
const [, setUploadedFile] = useState();
|
||||
const [textInputValue, setTextInputValue] = useState('');
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: 'video/*',
|
||||
multiple: false,
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
const uploadfile = acceptedFiles[0];
|
||||
setUploadedFile(uploadfile);
|
||||
onUpload(uploadfile);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setTextInputValue(event.target.value);
|
||||
};
|
||||
|
||||
const handleSaveButtonClick = () => {
|
||||
// do something with the textInputValue, e.g. save to state or send to server
|
||||
console.log(`Saving input value: ${textInputValue}`);
|
||||
};
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className="d-flex flex-column justify-content-center align-items-center text-center error-message">{errorMessage}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-column justify-content-center align-items-center p-4 w-100 min-vh-100" {...getRootProps()}>
|
||||
<div className={`d-flex flex-column justify-content-center align-items-center gap-2 text-center min-vh-100 w-100
|
||||
dropzone-middle ${isDragActive ? 'active' : ''}`}
|
||||
>
|
||||
<div className="d-flex justify-content-center align-items-center bg-light rounded-circle file-upload">
|
||||
<Icon src={FileUpload} className="text-muted" />
|
||||
</div>
|
||||
<div className="d-flex align-items-center justify-content-center gap-1 flex-wrap flex-column pt-5">
|
||||
<span style={{ fontSize: '20px' }}><FormattedMessage {...messages.dropVideoFileHere} /></span>
|
||||
<span style={{ fontSize: '12px' }}><FormattedMessage {...messages.info} /></span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mt-3">
|
||||
<span className="mx-2 text-dark">OR</span>
|
||||
</div>
|
||||
</div>
|
||||
<input {...getInputProps()} data-testid="fileInput" />
|
||||
</div>
|
||||
<div className="d-flex video-id-prompt">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste your video ID or URL"
|
||||
value={textInputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSaveButtonClick()}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
/>
|
||||
<button className="border-start-0" type="button" onClick={handleSaveButtonClick}>
|
||||
<Icon src={ArrowForward} className="rounded-circle text-dark" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VideoUploader.propTypes = {
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const VideoUploadEditor = ({ intl, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const handleCancel = () => {
|
||||
editorHooks.handleCancel({ onClose });
|
||||
};
|
||||
|
||||
const handleDrop = (file) => {
|
||||
if (!file) {
|
||||
console.log('No file selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const extToMime = {
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime',
|
||||
};
|
||||
const supportedFormats = Object.keys(extToMime);
|
||||
|
||||
function getFileExtension(filename) {
|
||||
return filename.slice(Math.abs(filename.lastIndexOf('.') - 1) + 2);
|
||||
}
|
||||
|
||||
const ext = getFileExtension(file.name);
|
||||
const type = extToMime[ext] || '';
|
||||
const newFile = new File([file], file.name, { type });
|
||||
|
||||
if (supportedFormats.includes(ext)) {
|
||||
hooks.uploadVideo({ dispatch, supportedFiles: [newFile] });
|
||||
} else {
|
||||
const errorMsg = 'Video must be an MP4 or MOV file';
|
||||
console.log(errorMsg);
|
||||
setErrorMessage(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="marked-area">
|
||||
<div className="d-flex justify-content-end close-button-container">
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
<VideoUploader onUpload={handleDrop} errorMessage={errorMessage} intl={intl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VideoUploadEditor.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(VideoUploadEditor);
|
||||
47
src/editors/containers/VideoUploadEditor/index.scss
Normal file
47
src/editors/containers/VideoUploadEditor/index.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.dropzone-middle {
|
||||
border: 2px dashed #ccc;
|
||||
|
||||
&.active {
|
||||
border: 2px solid #262626; /* change color when active */
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-id-prompt {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 1rem;
|
||||
border: 1px solid #707070;
|
||||
width: 308px;
|
||||
|
||||
input {
|
||||
border: none !important;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #454545;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none !important;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #AB0D02;
|
||||
margin-top: 20rem;
|
||||
}
|
||||
|
||||
.close-button-container {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 30px;
|
||||
}
|
||||
127
src/editors/containers/VideoUploadEditor/index.test.jsx
Normal file
127
src/editors/containers/VideoUploadEditor/index.test.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import VideoUploadEditor, { VideoUploader } from '.';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const mockOnUpload = jest.fn();
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
const defaultEditorProps = {
|
||||
intl: {},
|
||||
};
|
||||
|
||||
const defaultUploaderProps = {
|
||||
onUpload: mockOnUpload,
|
||||
errorMessage: '',
|
||||
intl: {},
|
||||
};
|
||||
|
||||
const renderEditorComponent = (props = defaultEditorProps) => render(
|
||||
<IntlProvider locale="en">
|
||||
<VideoUploadEditor {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const renderUploaderComponent = (props = defaultUploaderProps) => render(
|
||||
<IntlProvider locale="en">
|
||||
<VideoUploader {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('VideoUploadEditor', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const { container } = renderEditorComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('updates the input field value when user types', () => {
|
||||
const { getByPlaceholderText } = renderEditorComponent();
|
||||
const input = getByPlaceholderText('Paste your video ID or URL');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test value' } });
|
||||
expect(input.value).toBe('test value');
|
||||
});
|
||||
|
||||
it('shows error message with unsupported files', async () => {
|
||||
const { getByTestId, findByText } = renderEditorComponent();
|
||||
const fileInput = getByTestId('fileInput');
|
||||
|
||||
const unsupportedFile = new File(['(⌐□_□)'], 'unsupported.avi', { type: 'video/avi' });
|
||||
fireEvent.change(fileInput, { target: { files: [unsupportedFile] } });
|
||||
|
||||
const errorMsg = await findByText('Video must be an MP4 or MOV file');
|
||||
expect(errorMsg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls uploadVideo with supported files', async () => {
|
||||
const uploadVideoSpy = jest.spyOn(hooks, 'uploadVideo');
|
||||
const { container } = renderEditorComponent();
|
||||
const dropzone = container.querySelector('.dropzone-middle');
|
||||
|
||||
const supportedFile = new File(['(⌐□_□)'], 'supported.mp4', { type: 'video/mp4' });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
files: [supportedFile],
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(uploadVideoSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
dispatch: mockDispatch,
|
||||
supportedFiles: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: supportedFile.name,
|
||||
type: supportedFile.type,
|
||||
size: supportedFile.size,
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('VideoUploader', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const { container } = renderUploaderComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with an error message', () => {
|
||||
const errorMessage = 'Video must be an MP4 or MOV file';
|
||||
const { getByText } = renderUploaderComponent({ ...defaultUploaderProps, errorMessage });
|
||||
expect(getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls the onUpload function when a supported file is dropped', async () => {
|
||||
const { container } = renderUploaderComponent();
|
||||
const dropzone = container.querySelector('.dropzone-middle');
|
||||
const file = new File(['(⌐□_□)'], 'video.mp4', { type: 'video/mp4' });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
files: [file],
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockOnUpload).toHaveBeenCalledWith(file);
|
||||
});
|
||||
});
|
||||
@@ -6,4 +6,5 @@ export const blockTypes = StrictDict({
|
||||
video: 'video',
|
||||
problem: 'problem',
|
||||
// ADDED_EDITORS GO BELOW
|
||||
video_upload: 'video_upload',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { StrictDict, camelizeKeys } from '../../../utils';
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { actions } from '..';
|
||||
import * as requests from './requests';
|
||||
import * as module from './app';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StrictDict } from '../../../utils';
|
||||
|
||||
/* eslint-disable import/no-cycle */
|
||||
import app from './app';
|
||||
import video from './video';
|
||||
import problem from './problem';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { actions } from '..';
|
||||
import * as requests from './requests';
|
||||
import { OLXParser } from '../../../containers/ProblemEditor/data/OLXParser';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StrictDict } from '../../../utils';
|
||||
|
||||
import { RequestKeys } from '../../constants/requests';
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { actions, selectors } from '..';
|
||||
import api, { loadImages } from '../../services/cms/api';
|
||||
|
||||
@@ -295,6 +296,18 @@ export const fetchVideoFeatures = ({ ...rest }) => (dispatch, getState) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const uploadVideo = ({ data, ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.uploadVideo,
|
||||
promise: api.uploadVideo({
|
||||
data,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId: selectors.app.learningContextId(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetchBlock,
|
||||
fetchStudioView,
|
||||
@@ -314,4 +327,5 @@ export default StrictDict({
|
||||
importTranscript,
|
||||
fetchAdvancedSettings,
|
||||
fetchVideoFeatures,
|
||||
uploadVideo,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ jest.mock('../../services/cms/api', () => ({
|
||||
checkTranscriptsForImport: (args) => args,
|
||||
importTranscript: (args) => args,
|
||||
fetchVideoFeatures: (args) => args,
|
||||
uploadVideo: (args) => args,
|
||||
}));
|
||||
|
||||
const apiKeys = keyStore(api);
|
||||
@@ -490,5 +491,22 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('uploadVideo', () => {
|
||||
const data = { files: [{ file_name: 'video.mp4', content_type: 'mp4' }] };
|
||||
testNetworkRequestAction({
|
||||
action: requests.uploadVideo,
|
||||
args: { ...fetchParams, data },
|
||||
expectedString: 'with uploadVideo promise',
|
||||
expectedData: {
|
||||
...fetchParams,
|
||||
requestKey: RequestKeys.uploadVideo,
|
||||
promise: api.uploadVideo({
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
learningContextId: selectors.app.learningContextId(testState),
|
||||
data,
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { actions, selectors } from '..';
|
||||
import { removeItemOnce } from '../../../utils';
|
||||
import * as requests from './requests';
|
||||
|
||||
@@ -208,6 +208,14 @@ export const apiMethods = {
|
||||
}) => get(
|
||||
urls.videoFeatures({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
uploadVideo: ({
|
||||
data,
|
||||
studioEndpointUrl,
|
||||
learningContextId,
|
||||
}) => post(
|
||||
urls.courseVideos({ studioEndpointUrl, learningContextId }),
|
||||
data,
|
||||
),
|
||||
};
|
||||
|
||||
export const loadImage = (imageData) => ({
|
||||
|
||||
@@ -26,6 +26,11 @@ jest.mock('./urls', () => ({
|
||||
replaceTranscript: jest.fn().mockName('urls.replaceTranscript'),
|
||||
videoFeatures: jest.fn().mockName('urls.videoFeatures'),
|
||||
courseVideos: jest.fn().mockName('urls.courseVideos'),
|
||||
videoUpload: jest.fn()
|
||||
.mockName('urls.courseVideos')
|
||||
.mockImplementation(
|
||||
({ studioEndpointUrl, learningContextId }) => `${studioEndpointUrl}/some_video_upload_url/${learningContextId}`,
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./utils', () => ({
|
||||
@@ -219,6 +224,20 @@ describe('cms api', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadVideo', () => {
|
||||
it('should call post with urls.courseVideos and data', () => {
|
||||
const data = { files: [{ file_name: 'video.mp4', content_type: 'mp4' }] };
|
||||
|
||||
apiMethods.uploadVideo({ data, studioEndpointUrl, learningContextId });
|
||||
|
||||
expect(urls.courseVideos).toHaveBeenCalledWith({ studioEndpointUrl, learningContextId });
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
urls.courseVideos({ studioEndpointUrl, learningContextId }),
|
||||
data,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('loadImage', () => {
|
||||
it('loads incoming image data, replacing the dateAdded with a date field', () => {
|
||||
|
||||
@@ -7,6 +7,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Error: Could Not find Editor',
|
||||
description: 'Error Message Dispayed When An unsopported Editor is desired in V2',
|
||||
},
|
||||
dropVideoFileHere: {
|
||||
defaultMessage: 'Drag and drop video here or click to upload',
|
||||
id: 'VideoUploadEditor.dropVideoFileHere',
|
||||
description: 'Display message for Drag and Drop zone',
|
||||
},
|
||||
browse: {
|
||||
defaultMessage: 'Browse files',
|
||||
id: 'VideoUploadEditor.browse',
|
||||
description: 'Display message for browse files button',
|
||||
},
|
||||
info: {
|
||||
id: 'VideoUploadEditor.uploadInfo',
|
||||
defaultMessage: 'Upload MP4 or MOV files (5 GB max)',
|
||||
description: 'Info message for supported formats',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -3,6 +3,7 @@ import VideoEditor from './containers/VideoEditor';
|
||||
import ProblemEditor from './containers/ProblemEditor';
|
||||
|
||||
// ADDED_EDITOR_IMPORTS GO HERE
|
||||
import VideoUploadEditor from './containers/VideoUploadEditor';
|
||||
|
||||
import { blockTypes } from './data/constants/app';
|
||||
|
||||
@@ -11,6 +12,7 @@ const supportedEditors = {
|
||||
[blockTypes.video]: VideoEditor,
|
||||
[blockTypes.problem]: ProblemEditor,
|
||||
// ADDED_EDITORS GO BELOW
|
||||
[blockTypes.video_upload]: VideoUploadEditor,
|
||||
};
|
||||
|
||||
export default supportedEditors;
|
||||
|
||||
Reference in New Issue
Block a user