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 <farhaan@opencraft.com>
This commit is contained in:
Farhaan Bukhsh
2023-07-20 17:07:35 +05:30
parent e9c0f6cc82
commit f822d95d6a
15 changed files with 954 additions and 428 deletions

2
package-lock.json generated
View File

@@ -23393,4 +23393,4 @@
}
}
}
}
}

View File

@@ -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 (
<Collapsible.Advanced
@@ -30,9 +32,9 @@ export const VideoPreviewWidget = ({
<div className="d-flex flex-row">
<Image
thumbnail
className="mr-3"
className="mr-3 p-4"
ref={imgRef}
src={thumbnail}
src={thumbnailImage}
alt={intl.formatMessage(thumbnailMessages.thumbnailAltText)}
style={{
maxWidth: '200px',

View File

@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, Dropzone, InputGroup, FormControl,
} from '@edx/paragon';
import { ArrowForward, FileUpload } from '@edx/paragon/icons';
import { useDispatch } from 'react-redux';
import { thunkActions } from '../../data/redux';
import * as hooks from './hooks';
import messages from './messages';
const URLUploader = () => {
const intl = useIntl();
return (
<div className="d-flex flex-column flex-wrap">
<div className="justify-content-center align-self-center bg-light rounded-circle p-4">
<Icon src={FileUpload} className="text-muted" />
</div>
<div className="d-flex align-self-center justify-content-center flex-wrap flex-column pt-5">
<span style={{ fontSize: '1.35rem' }}>{intl.formatMessage(messages.dropVideoFileHere)}</span>
<span className="align-self-center" style={{ fontSize: '0.8rem' }}>{intl.formatMessage(messages.info)}</span>
</div>
<div className="align-self-center justify-content-center mx-2 text-dark">OR</div>
</div>
);
};
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 (
<div>
<Dropzone
accept={{ 'video/*': ['.mp4', '.mov'] }}
onProcessUpload={handleProcessUpload}
inputComponent={<URLUploader />}
/>
<div className="d-flex video-id-prompt">
<InputGroup>
<FormControl
placeholder={intl.formatMessage(messages.pasteURL)}
aria-label={intl.formatMessage(messages.pasteURL)}
aria-describedby="basic-addon2"
borderless
onChange={(event) => { settextInputValue(event.target.value); }}
/>
<div className="justify-content-center align-self-center bg-light rounded-circle p-0 x-small url-submit-button">
<IconButton
alt={intl.formatMessage(messages.submitButtonAltText)}
src={ArrowForward}
iconAs={Icon}
size="inline"
onClick={() => { onURLUpload(textInputValue); }}
/>
</div>
</InputGroup>
</div>
</div>
);
};
VideoUploader.propTypes = {
onUpload: PropTypes.func.isRequired,
setLoading: PropTypes.func.isRequired,
};
export default VideoUploader;

View File

@@ -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(
<AppProvider store={storeParam}>
<IntlProvider locale="en">
<VideoUploader setLoading={setLoadingMockParam} />
</IntlProvider>,
</AppProvider>,
);
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();
});
});

View File

@@ -0,0 +1,302 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VideoUploader renders as expected with default behavior 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div>
<div
class="pgn__dropzone"
data-testid="dropzone-container"
role="presentation"
tabindex="0"
>
<input
accept="video/*,.mp4,.mov"
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="d-flex flex-column justify-content-around align-items-center w-100"
>
<div
class="d-flex flex-column flex-wrap"
>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-4"
>
<span
class="pgn__icon text-muted"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 10h4v6h6v-6h4l-7-7-7 7zm0 8v2h14v-2H5z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="d-flex align-self-center justify-content-center flex-wrap flex-column pt-5"
>
<span
style="font-size: 1.35rem;"
>
Drag and drop video here or click to upload
</span>
<span
class="align-self-center"
style="font-size: 0.8rem;"
>
Upload MP4 or MOV files (5 GB max)
</span>
</div>
<div
class="align-self-center justify-content-center mx-2 text-dark"
>
OR
</div>
</div>
</div>
</div>
<div
class="d-flex video-id-prompt"
>
<div
class="input-group"
>
<div
class="pgn__form-control-decorator-group"
>
<input
aria-describedby="basic-addon2"
aria-label="Paste your video ID or URL"
class="form-control"
placeholder="Paste your video ID or URL"
value=""
/>
</div>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-0 x-small url-submit-button"
>
<button
aria-label="Submit"
class="btn-icon btn-icon-primary btn-icon-inline"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
,
</div>
</body>,
"container": <div>
<div>
<div
class="pgn__dropzone"
data-testid="dropzone-container"
role="presentation"
tabindex="0"
>
<input
accept="video/*,.mp4,.mov"
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="d-flex flex-column justify-content-around align-items-center w-100"
>
<div
class="d-flex flex-column flex-wrap"
>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-4"
>
<span
class="pgn__icon text-muted"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 10h4v6h6v-6h4l-7-7-7 7zm0 8v2h14v-2H5z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="d-flex align-self-center justify-content-center flex-wrap flex-column pt-5"
>
<span
style="font-size: 1.35rem;"
>
Drag and drop video here or click to upload
</span>
<span
class="align-self-center"
style="font-size: 0.8rem;"
>
Upload MP4 or MOV files (5 GB max)
</span>
</div>
<div
class="align-self-center justify-content-center mx-2 text-dark"
>
OR
</div>
</div>
</div>
</div>
<div
class="d-flex video-id-prompt"
>
<div
class="input-group"
>
<div
class="pgn__form-control-decorator-group"
>
<input
aria-describedby="basic-addon2"
aria-label="Paste your video ID or URL"
class="form-control"
placeholder="Paste your video ID or URL"
value=""
/>
</div>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-0 x-small url-submit-button"
>
<button
aria-label="Submit"
class="btn-icon btn-icon-primary btn-icon-inline"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
,
</div>,
"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],
}
`;

View File

@@ -1,227 +1,380 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VideoUploader snapshots renders as expected with default behavior 1`] = `
<div>
<div
className="d-flex flex-column justify-content-center align-items-center p-4 w-100 min-vh-100"
onBlur={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
role="presentation"
tabIndex={0}
>
<div
className="d-flex flex-column justify-content-center align-items-center gap-2 text-center min-vh-100 w-100
dropzone-middle "
>
<div
className="d-flex justify-content-center align-items-center bg-light rounded-circle file-upload"
>
<Icon
className="text-muted"
/>
exports[`VideoUploadEditor renders as expected with default behavior 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div>
<div
class="marked-area"
>
<div
class="d-flex justify-content-end close-button-container"
>
<button
aria-label="Close"
class="btn-icon btn-icon-primary btn-icon-md"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
<div>
<div
class="pgn__dropzone"
data-testid="dropzone-container"
role="presentation"
tabindex="0"
>
<input
accept="video/*,.mp4,.mov"
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="d-flex flex-column justify-content-around align-items-center w-100"
>
<div
class="d-flex flex-column flex-wrap"
>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-4"
>
<span
class="pgn__icon text-muted"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 10h4v6h6v-6h4l-7-7-7 7zm0 8v2h14v-2H5z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="d-flex align-self-center justify-content-center flex-wrap flex-column pt-5"
>
<span
style="font-size: 1.35rem;"
>
Drag and drop video here or click to upload
</span>
<span
class="align-self-center"
style="font-size: 0.8rem;"
>
Upload MP4 or MOV files (5 GB max)
</span>
</div>
<div
class="align-self-center justify-content-center mx-2 text-dark"
>
OR
</div>
</div>
</div>
</div>
<div
class="d-flex video-id-prompt"
>
<div
class="input-group"
>
<div
class="pgn__form-control-decorator-group"
>
<input
aria-describedby="basic-addon2"
aria-label="Paste your video ID or URL"
class="form-control"
placeholder="Paste your video ID or URL"
value=""
/>
</div>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-0 x-small url-submit-button"
>
<button
aria-label="Submit"
class="btn-icon btn-icon-primary btn-icon-inline"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
,
</div>
</body>,
"container": <div>
<div>
<div
className="d-flex align-items-center justify-content-center gap-1 flex-wrap flex-column pt-5"
class="marked-area"
>
<span
style={
Object {
"fontSize": "20px",
}
}
<div
class="d-flex justify-content-end close-button-container"
>
<FormattedMessage
defaultMessage="Drag and drop video here or click to upload"
description="Display message for Drag and Drop zone"
id="VideoUploadEditor.dropVideoFileHere"
/>
</span>
<span
style={
Object {
"fontSize": "12px",
}
}
>
<FormattedMessage
defaultMessage="Upload MP4 or MOV files (5 GB max)"
description="Info message for supported formats"
id="VideoUploadEditor.uploadInfo"
/>
</span>
</div>
<div
className="d-flex align-items-center mt-3"
>
<span
className="mx-2 text-dark"
>
OR
</span>
<button
aria-label="Close"
class="btn-icon btn-icon-primary btn-icon-md"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
<div>
<div
class="pgn__dropzone"
data-testid="dropzone-container"
role="presentation"
tabindex="0"
>
<input
accept="video/*,.mp4,.mov"
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="d-flex flex-column justify-content-around align-items-center w-100"
>
<div
class="d-flex flex-column flex-wrap"
>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-4"
>
<span
class="pgn__icon text-muted"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 10h4v6h6v-6h4l-7-7-7 7zm0 8v2h14v-2H5z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="d-flex align-self-center justify-content-center flex-wrap flex-column pt-5"
>
<span
style="font-size: 1.35rem;"
>
Drag and drop video here or click to upload
</span>
<span
class="align-self-center"
style="font-size: 0.8rem;"
>
Upload MP4 or MOV files (5 GB max)
</span>
</div>
<div
class="align-self-center justify-content-center mx-2 text-dark"
>
OR
</div>
</div>
</div>
</div>
<div
class="d-flex video-id-prompt"
>
<div
class="input-group"
>
<div
class="pgn__form-control-decorator-group"
>
<input
aria-describedby="basic-addon2"
aria-label="Paste your video ID or URL"
class="form-control"
placeholder="Paste your video ID or URL"
value=""
/>
</div>
<div
class="justify-content-center align-self-center bg-light rounded-circle p-0 x-small url-submit-button"
>
<button
aria-label="Submit"
class="btn-icon btn-icon-primary btn-icon-inline"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<input
accept=""
data-testid="fileInput"
multiple={false}
onChange={[Function]}
onClick={[Function]}
style={
Object {
"display": "none",
}
}
tabIndex={-1}
type="file"
/>
</div>
<div
className="d-flex video-id-container"
>
<div
className="d-flex video-id-prompt"
>
<input
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
placeholder="Paste your video ID or URL"
type="text"
value=""
/>
<button
className="border-start-0"
data-testid="inputSaveButton"
onClick={[Function]}
type="button"
>
<Icon
className="rounded-circle text-dark"
/>
</button>
</div>
</div>
</div>
`;
exports[`VideoUploader snapshots renders as expected with error message 1`] = `
<div>
<div
className="d-flex flex-column justify-content-center align-items-center p-4 w-100 min-vh-100"
onBlur={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
role="presentation"
tabIndex={0}
>
<div
className="d-flex flex-column justify-content-center align-items-center gap-2 text-center min-vh-100 w-100
dropzone-middle "
>
<div
className="d-flex justify-content-center align-items-center bg-light rounded-circle file-upload"
>
<Icon
className="text-muted"
/>
</div>
<div
className="d-flex align-items-center justify-content-center gap-1 flex-wrap flex-column pt-5"
>
<span
style={
Object {
"fontSize": "20px",
}
}
>
<FormattedMessage
defaultMessage="Drag and drop video here or click to upload"
description="Display message for Drag and Drop zone"
id="VideoUploadEditor.dropVideoFileHere"
/>
</span>
<span
style={
Object {
"fontSize": "12px",
}
}
>
<FormattedMessage
defaultMessage="Upload MP4 or MOV files (5 GB max)"
description="Info message for supported formats"
id="VideoUploadEditor.uploadInfo"
/>
</span>
</div>
<div
className="d-flex align-items-center mt-3"
>
<span
className="mx-2 text-dark"
>
OR
</span>
</div>
</div>
<input
accept=""
data-testid="fileInput"
multiple={false}
onChange={[Function]}
onClick={[Function]}
style={
Object {
"display": "none",
}
}
tabIndex={-1}
type="file"
/>
</div>
<div
className="d-flex video-id-container"
>
<div
className="d-flex video-id-prompt"
>
<input
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
placeholder="Paste your video ID or URL"
type="text"
value=""
/>
<button
className="border-start-0"
data-testid="inputSaveButton"
onClick={[Function]}
type="button"
>
<Icon
className="rounded-circle text-dark"
/>
</button>
</div>
</div>
</div>
`;
exports[`VideoUploaderEdirtor snapshots renders as expected with default behavior 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
,
</div>,
"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],
}
`;

View File

@@ -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,
};

View File

@@ -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();
});
});
});

View File

@@ -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 (
<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-container">
<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} data-testid="inputSaveButton">
<Icon src={ArrowForward} className="rounded-circle text-dark" />
</button>
</div>
</div>
</div>
);
};
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 (
<div>
@@ -115,12 +31,13 @@ const VideoUploadEditor = (
<div className="marked-area">
<div className="d-flex justify-content-end close-button-container">
<IconButton
alt={intl.formatMessage(messages.closeButtonAltText)}
src={Close}
iconAs={Icon}
onClick={handleCancel}
/>
</div>
<VideoUploader onUpload={handleDrop} errorMessage={errorMessage} intl={intl} />
<VideoUploader onUpload={uploadVideo} setLoading={setLoading} />
</div>
) : (
<div className="text-center p-6">
@@ -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);

View File

@@ -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 {

View File

@@ -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';
import VideoUploadEditor from '.';
const defaultEditorProps = {
onClose: jest.fn().mockName('props.onClose'),
intl: { formatMessage },
uploadVideo: jest.fn(),
};
jest.unmock('react-redux');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@edx/paragon');
jest.unmock('@edx/paragon/icons');
const defaultUploaderProps = {
onUpload: jest.fn(),
errorMessage: null,
intl: { formatMessage },
};
describe('VideoUploadEditor', () => {
const onCloseMock = jest.fn();
let store;
describe('VideoUploader', () => {
describe('snapshots', () => {
test('renders as expected with default behavior', () => {
expect(shallow(<VideoUploader {...defaultUploaderProps} />)).toMatchSnapshot();
const renderComponent = async (storeParam, onCloseMockParam) => render(
<AppProvider store={storeParam}>
<IntlProvider locale="en">
<VideoUploadEditor onClose={onCloseMockParam} />
</IntlProvider>,
</AppProvider>,
);
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(<VideoUploader {...defaultUploaderPropsWithError} />)).toMatchSnapshot();
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'test-user',
administrator: true,
roles: [],
},
});
});
});
describe('VideoUploaderEdirtor', () => {
describe('snapshots', () => {
test('renders as expected with default behavior', () => {
expect(shallow(<VideoUploadEditor {...defaultEditorProps} />)).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();
});
});

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4111 10.6847V21.3514H7.0778V10.6847H20.4111ZM21.7445 8.01807H5.74447C5.01113 8.01807 4.41113 8.61807 4.41113 9.3514V22.6847C4.41113 23.4181 5.01113 24.0181 5.74447 24.0181H21.7445C22.4778 24.0181 23.0778 23.4181 23.0778 22.6847V18.0181L28.4111 23.3514V8.68473L23.0778 14.0181V9.3514C23.0778 8.61807 22.4778 8.01807 21.7445 8.01807Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@@ -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,

View File

@@ -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.');
});