Skip to content

Commit

Permalink
feat: Add check to see if assets pre-exist when uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
mavidser committed Oct 17, 2023
1 parent e5a3ee7 commit caf8d06
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 8 deletions.
9 changes: 5 additions & 4 deletions src/components/AssetsDropZone/AssetsDropZone.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { courseDetails } from '../../utils/testConstants';
import { mountWithIntl } from '../../utils/i18n/enzymeHelper';

const defaultProps = {
preUploadCheck: () => {},
uploadAssets: () => {},
uploadExceedMaxCount: () => {},
uploadExceedMaxSize: () => {},
Expand Down Expand Up @@ -70,13 +71,13 @@ describe('<AssetsDropZone />', () => {
wrapper.instance().onDrop([{}, {}], [{}]);
expect(mockUploadInvalidFileType).toBeCalled();
});
it('call uploadAssets() for successful uploads', () => {
const mockUploadAssets = jest.fn();
it('call preUploadCheck() for approved files', () => {
const mockPreUploadCheck = jest.fn();
wrapper.setProps({
uploadAssets: mockUploadAssets,
preUploadCheck: mockPreUploadCheck,
});
wrapper.instance().onDrop([{}, {}], []);
expect(mockUploadAssets).toBeCalled();
expect(mockPreUploadCheck).toBeCalled();
});
});

Expand Down
3 changes: 2 additions & 1 deletion src/components/AssetsDropZone/container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { connect } from 'react-redux';

import AssetsDropZone from '.';
import {
uploadAssets, uploadExceedMaxSize, uploadExceedMaxCount, uploadInvalidFileType,
preUploadCheck, uploadAssets, uploadExceedMaxSize, uploadExceedMaxCount, uploadInvalidFileType,
} from '../../data/actions/assets';

const mapStateToProps = state => ({
courseDetails: state.studioDetails.course,
});

const mapDispatchToProps = dispatch => ({
preUploadCheck: (files, courseDetails) => dispatch(preUploadCheck(files, courseDetails)),
uploadAssets: (files, courseDetails) => dispatch(uploadAssets(files, courseDetails)),
uploadExceedMaxCount: maxFileCount => dispatch(uploadExceedMaxCount(maxFileCount)),
uploadExceedMaxSize: maxFileSizeMB => dispatch(uploadExceedMaxSize(maxFileSizeMB)),
Expand Down
5 changes: 2 additions & 3 deletions src/components/AssetsDropZone/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class AssetsDropZone extends React.Component {
this.props.uploadInvalidFileType();
}
} else {
this.props.uploadAssets(acceptedFiles, this.props.courseDetails);
this.props.preUploadCheck(acceptedFiles, this.props.courseDetails);
}
};

Expand Down Expand Up @@ -133,11 +133,10 @@ AssetsDropZone.propTypes = {
}).isRequired,
maxFileCount: PropTypes.number,
maxFileSizeMB: PropTypes.number,
uploadAssets: PropTypes.func.isRequired,
uploadExceedMaxCount: PropTypes.func.isRequired,
uploadExceedMaxSize: PropTypes.func.isRequired,
uploadInvalidFileType: PropTypes.func.isRequired,

preUploadCheck: PropTypes.func.isRequired,
};

AssetsDropZone.defaultProps = {
Expand Down
4 changes: 4 additions & 0 deletions src/components/AssetsPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import WrappedAssetsSearch from '../AssetsSearch/container';
import WrappedAssetsStatusAlert from '../AssetsStatusAlert/container';
import WrappedAssetsResultsCount from '../AssetsResultsCount/container';
import WrappedAssetsClearFiltersButton from '../AssetsClearFiltersButton/container';
import WrappedAssetsUploadConfirm from '../AssetsUploadConfirm/container';
import WrappedMessage from '../../utils/i18n/formattedMessageWrapper';
import messages from './displayMessages';
import styles from './AssetsPage.scss';
Expand Down Expand Up @@ -197,6 +198,9 @@ export default class AssetsPage extends React.Component {
</div>
<div className="container">
<div className="row">
<div className="col-12">
<WrappedAssetsUploadConfirm />
</div>
<div className="col-12">
<WrappedAssetsStatusAlert
statusAlertRef={(input) => { this.statusAlertRef = input; }}
Expand Down
103 changes: 103 additions & 0 deletions src/components/AssetsUploadConfirm/AssetsUploadConfirm.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { Button, Modal } from '@edx/paragon';

import AssetsUploadConfirm from './index';
import { mountWithIntl } from '../../utils/i18n/enzymeHelper';
import mockQuerySelector from '../../utils/mockQuerySelector';

const defaultProps = {
files: [],
uploadAssets: () => { },
clearPreUploadProps: () => {},
courseDetails: {},
preUploadError: '',
};

const modalIsClosed = (wrapper) => {
expect(wrapper.state('modalOpen')).toEqual(false);
expect(wrapper.find(Modal).prop('open')).toEqual(false);
expect(wrapper.find(AssetsUploadConfirm).prop('preUploadError')).toEqual('');
};

const modalIsOpen = (wrapper) => {
expect(wrapper.state('modalOpen')).toEqual(true);
expect(wrapper.find(Modal).prop('open')).toEqual(true);
expect(wrapper.find(AssetsUploadConfirm).prop('preUploadError')).toBeTruthy();
};

const errorMessageHasCorrectFiles = (wrapper, files) => {
const modal = wrapper.find(AssetsUploadConfirm);
const modalMessage = modal.prop('preUploadError');

files.forEach((file) => {
expect(modalMessage).toContain(file);
});
};

let wrapper;

describe('AssetsUploadConfirm', () => {
beforeEach(() => {
mockQuerySelector.init();
});
afterEach(() => {
mockQuerySelector.reset();
});

describe('renders', () => {
beforeEach(() => {
wrapper = mountWithIntl(
<AssetsUploadConfirm
{...defaultProps}
/>,
);
// modal = wrapper.find('[role="dialog"]');
});

it('closed by default', () => {
modalIsClosed(wrapper);
});

it('open if there is an error message', () => {
wrapper.setProps({
preUploadError: 'The following files already exist: asset.jpg',
});

modalIsOpen(wrapper);
errorMessageHasCorrectFiles(wrapper, ['asset.jpg']);
});
});
describe('behaves', () => {
it('Overwrite calls uploadAssets', () => {
const mockUploadAssets = jest.fn();
const files = ['file1', 'file2'];
const courseDetails = {
id: 'course-v1:edX+DemoX+Demo_Course',
};
wrapper.setProps({
files,
courseDetails,
uploadAssets: mockUploadAssets,
});

wrapper.find(Button).filterWhere(button => button.text() === 'Overwrite').simulate('click');
expect(mockUploadAssets).toBeCalledWith(files, courseDetails);
});

it('clicking cancel button closes the status alert', () => {
wrapper.setProps({
preUploadError: 'The following files already exist: asset.jpg',
clearPreUploadProps: () => {
wrapper.setProps({
...defaultProps,
});
},
});

const modal = wrapper.find(Modal);
const cancelModalButton = modal.find('button').filterWhere(button => button.text() === 'Cancel');
cancelModalButton.simulate('click');
modalIsClosed(wrapper);
});
});
});
22 changes: 22 additions & 0 deletions src/components/AssetsUploadConfirm/container.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { connect } from 'react-redux';

import { uploadAssets, clearPreUploadProps } from '../../data/actions/assets';
import AssetsUploadConfirm from '.';

const mapStateToProps = state => ({
files: state.metadata.files,
preUploadError: state.metadata.preUploadError,
courseDetails: state.studioDetails.course,
});

const mapDispatchToProps = dispatch => ({
uploadAssets: (assets, courseDetails) => dispatch(uploadAssets(assets, courseDetails)),
clearPreUploadProps: () => dispatch(clearPreUploadProps()),
});

const WrappedAssetsUploadConfirm = connect(
mapStateToProps,
mapDispatchToProps,
)(AssetsUploadConfirm);

export default WrappedAssetsUploadConfirm;
21 changes: 21 additions & 0 deletions src/components/AssetsUploadConfirm/displayMessages.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineMessages } from 'react-intl';

const messages = defineMessages({
assetsUploadConfirmMessage: {
id: 'assetsUploadConfirmMessage',
defaultMessage: '0 files uploaded. {preUploadError}. Continuing to upload will overwrite them. If this is not what you want, retry uploading the files with different names.',
description: 'The message displayed in the confirmation box shown when uploading files with pre-existing names',
},
assetsUploadConfirmOverwrite: {
id: 'assetsUploadConfirmOverwrite',
defaultMessage: 'Overwrite',
description: 'The message displayed in the button to confirm overwriting the files',
},
assetsUploadConfirmCancel: {
id: 'assetsUploadConfirmCancel',
defaultMessage: 'Cancel',
description: 'The message displayed in the button to confirm cancelling the upload',
},
});

export default messages;
97 changes: 97 additions & 0 deletions src/components/AssetsUploadConfirm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, Variant } from '@edx/paragon';

import WrappedMessage from '../../utils/i18n/formattedMessageWrapper';
import messages from './displayMessages';

const defaultState = {
modalOpen: false,
};
const modalWrapperID = 'modalWrapper';

export default class AssetsUploadConfirm extends React.Component {
constructor(props) {
super(props);
this.state = defaultState;
}

componentWillReceiveProps(nextProps) {
const { preUploadError } = nextProps;
this.updateAlertOpenState(preUploadError);
}

updateAlertOpenState = (preUploadError) => {
this.setState({
modalOpen: !!preUploadError,
});
};

uploadFiles = () => {
this.props.uploadAssets(this.props.files, this.props.courseDetails);
};

onClose = () => {
this.setState(defaultState);
this.props.clearPreUploadProps();
};

render() {
const { uploadFiles } = this;
const { modalOpen } = this.state;
const { preUploadError } = this.props;
const content = (
<WrappedMessage
message={messages.assetsUploadConfirmMessage}
values={{ preUploadError }}
/>
);
const closeText = (
<WrappedMessage message={messages.assetsUploadConfirmCancel} />
);
const button = (
<Button
buttonType="primary"
label={<WrappedMessage message={messages.assetsUploadConfirmOverwrite} />}
onClick={uploadFiles}
/>
);

return (
<div id={modalWrapperID}>
<Modal
title="Overwrite Files"
open={modalOpen}
body={content}
buttons={[button]}
onClose={this.onClose}
closeText={closeText}
variant={{ status: Variant.status.WARNING }}
parentSelector={`#${modalWrapperID}`}
/>
</div>
);
}
}

AssetsUploadConfirm.propTypes = {
files: PropTypes.arrayOf(PropTypes.string),
uploadAssets: PropTypes.func.isRequired,
clearPreUploadProps: PropTypes.func.isRequired,
courseDetails: PropTypes.shape({
lang: PropTypes.string,
url_name: PropTypes.string,
name: PropTypes.string,
display_course_number: PropTypes.string,
num: PropTypes.string,
org: PropTypes.string,
id: PropTypes.string,
revision: PropTypes.string,
}).isRequired,
preUploadError: PropTypes.string,
};

AssetsUploadConfirm.defaultProps = {
files: [],
preUploadError: '',
};
30 changes: 30 additions & 0 deletions src/data/actions/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,23 @@ export const uploadAssetFailure = (asset, response) => ({
response,
});

export const setFilesToUpload = (files) => ({
type: assetActions.files.FILES_UPDATE,
files,
});

export const setPreUploadError = (preUploadError) => ({
type: assetActions.files.FILES_PRE_UPLOAD_ERROR,
preUploadError,
});

export const clearPreUploadProps = () => (dispatch) => {
dispatch(setFilesToUpload([]));
dispatch(setPreUploadError(''));
};

export const uploadAssets = (assets, courseDetails) => (dispatch) => {
dispatch(clearPreUploadProps());
dispatch(uploadingAssets(assets.length));
// gather all the promises into a single promise that can be returned
return Promise.all(assets.map(asset => (
Expand All @@ -291,6 +307,20 @@ export const uploadAssets = (assets, courseDetails) => (dispatch) => {
)));
};

export const preUploadCheck = (assets, courseDetails) => (dispatch) => {
const filenames = assets.map(asset => asset.name);
return clientApi.preUploadCheck(courseDetails.id, filenames)
.then((response) => {
if (response.ok) {
return dispatch(uploadAssets(assets, courseDetails));
}
return response.json().then((json) => {
dispatch(setFilesToUpload(assets));
return dispatch(setPreUploadError(json.reason));
});
});
};

export const uploadExceedMaxCount = maxFileCount => ({
type: assetActions.upload.UPLOAD_EXCEED_MAX_COUNT_ERROR,
maxFileCount,
Expand Down
Loading

0 comments on commit caf8d06

Please sign in to comment.