Skip to content

Commit

Permalink
Merge pull request #366 from open-craft/farhaan/fix-drag-drop-component
Browse files Browse the repository at this point in the history
Re-write the dropzone component and fix styling issues
  • Loading branch information
kenclary authored Aug 25, 2023
2 parents 45e4bc5 + b7a04e1 commit 8e65952
Show file tree
Hide file tree
Showing 14 changed files with 1,036 additions and 496 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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',
Expand Down
78 changes: 78 additions & 0 deletions src/editors/containers/VideoUploadEditor/VideoUploader.jsx
Original file line number Diff line number Diff line change
@@ -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 = ({ setLoading }) => {
const [textInputValue, setTextInputValue] = React.useState('');
const onURLUpload = hooks.onVideoUpload();
const intl = useIntl();
const dispatch = useDispatch();

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 = {
setLoading: PropTypes.func.isRequired,
};

export default VideoUploader;
94 changes: 94 additions & 0 deletions src/editors/containers/VideoUploadEditor/VideoUploader.test.jsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading

0 comments on commit 8e65952

Please sign in to comment.