Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add check to see if assets pre-exist when uploading #384

Merged
merged 3 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ studio instance running in [Devstack](https://github.com/openedx/devstack):
does not exist already.
2. Add `STUDIO_FRONTEND_CONTAINER_URL = 'http://localhost:18011'` to
`cms/envs/private.py`.
3. Reload your Studio server: `make studio-restart`.
3. Restart your Studio container: `make cms-restart-container`.

Pages in Studio that have studio-frontend components should now request assets
from your studio-frontend docker container's webpack-dev-server. If you make a
Expand All @@ -88,13 +88,13 @@ your local docker devstack by following these steps:
1. If you have a `cms/envs/private.py` file in your devstack edx-platform
folder, then make sure the line `STUDIO_FRONTEND_CONTAINER_URL =
'http://localhost:18011'` is commented out.
2. Reload your Studio server: `make studio-restart`.
2. Reload your Studio server: `make cms-restart-container`.
3. Run the production build of studio-frontend by running `make shell` and then
`npm run build` inside the docker container.
4. Copy the production files over to your devstack Studio's static assets
folder by running this make command on your host machine in the
studio-frontend folder: `make copy-dist`.
5. Run Studio's static asset pipeline: `make studio-static`.
5. Run Studio's static asset pipeline: `make cms-static`.

Your devstack Studio should now be using the production studio-frontend files
built by your local checkout.
Expand Down
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 = {
validateAssetsAndUpload: () => {},
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 validateAssetsAndUpload() for approved files', () => {
const mockvalidateAssetsAndUpload = jest.fn();
wrapper.setProps({
uploadAssets: mockUploadAssets,
validateAssetsAndUpload: mockvalidateAssetsAndUpload,
});
wrapper.instance().onDrop([{}, {}], []);
expect(mockUploadAssets).toBeCalled();
expect(mockvalidateAssetsAndUpload).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,
validateAssetsAndUpload, uploadAssets, uploadExceedMaxSize, uploadExceedMaxCount, uploadInvalidFileType,
} from '../../data/actions/assets';

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

const mapDispatchToProps = dispatch => ({
validateAssetsAndUpload: (files, courseDetails) => dispatch(validateAssetsAndUpload(files, courseDetails)),
uploadAssets: (files, courseDetails) => dispatch(uploadAssets(files, courseDetails)),
uploadExceedMaxCount: maxFileCount => dispatch(uploadExceedMaxCount(maxFileCount)),
uploadExceedMaxSize: maxFileSizeMB => dispatch(uploadExceedMaxSize(maxFileSizeMB)),
Expand Down
8 changes: 4 additions & 4 deletions src/components/AssetsDropZone/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import FontAwesomeStyles from 'font-awesome/css/font-awesome.min.css';
import WrappedMessage from '../../utils/i18n/formattedMessageWrapper';
import MAX_FILE_UPLOAD_COUNT from '../../utils/constants';
import messages from './displayMessages';
import styles from './AssetsDropZone.scss';

Expand All @@ -28,7 +29,7 @@ export default class AssetsDropZone extends React.Component {
this.props.uploadInvalidFileType();
}
} else {
this.props.uploadAssets(acceptedFiles, this.props.courseDetails);
this.props.validateAssetsAndUpload(acceptedFiles, this.props.courseDetails);
}
};

Expand Down Expand Up @@ -133,17 +134,16 @@ 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,

validateAssetsAndUpload: PropTypes.func.isRequired,
};

AssetsDropZone.defaultProps = {
acceptedFileTypes: undefined,
buttonRef: () => {},
compactStyle: false,
maxFileCount: 1000,
maxFileCount: MAX_FILE_UPLOAD_COUNT,
maxFileSizeMB: 10,
};
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
100 changes: 100 additions & 0 deletions src/components/AssetsUploadConfirm/AssetsUploadConfirm.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 = {
filesToUpload: [],
uploadAssets: () => { },
clearUploadConfirmProps: () => {},
courseDetails: {},
filenameConflicts: [],
};

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

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

const errorMessageHasCorrectFiles = (wrapper, files) => {
const filenameConflicts = wrapper.prop('filenameConflicts');
files.forEach((file) => {
expect(filenameConflicts).toContain(file);
});
};

let wrapper;

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

describe('renders', () => {
beforeEach(() => {
wrapper = mountWithIntl(
<AssetsUploadConfirm
{...defaultProps}
/>,
);
});

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

it('open if there is an error message', () => {
wrapper.setProps({
filenameConflicts: ['asset.jpg'],
});

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

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

it('clicking cancel button closes the status alert', () => {
wrapper.setProps({
filenameConflicts: ['asset.jpg'],
clearUploadConfirmProps: () => {
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, clearUploadConfirmProps } from '../../data/actions/assets';
import AssetsUploadConfirm from '.';

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

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

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

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

const messages = defineMessages({
assetsUploadConfirmMessage: {
id: 'assetsUploadConfirmMessage',
defaultMessage: 'The following files will be overwritten: {listOfFiles}',
description: 'The message displayed in the modal shown when uploading files with pre-existing names',
},
assetsUploadConfirmTitle: {
id: 'assetsUploadConfirmTitle',
defaultMessage: 'Overwrite Files',
description: 'The title of the modal to confirm overwriting the files',
},
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;
103 changes: 103 additions & 0 deletions src/components/AssetsUploadConfirm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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 { filenameConflicts } = nextProps;
this.updateAlertOpenState(filenameConflicts);
}

updateAlertOpenState = (filenameConflicts) => {
this.setState({
modalOpen: filenameConflicts.length !== 0,
});
};

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

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

render() {
const { uploadFiles } = this;
const { modalOpen } = this.state;
const { filenameConflicts } = this.props;
const listOfFiles = (
<ul>
{ filenameConflicts.sort().map(item => <li key={item}>{item}</li>) }
</ul>
);
const content = (
<WrappedMessage
message={messages.assetsUploadConfirmMessage}
values={{ listOfFiles }}
/>
);
const closeText = (
<WrappedMessage message={messages.assetsUploadConfirmCancel} />
);
const button = (
<Button
buttonType="primary"
label={<WrappedMessage message={messages.assetsUploadConfirmOverwrite} />}
onClick={uploadFiles}
/>
);

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

AssetsUploadConfirm.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
filesToUpload: PropTypes.arrayOf(PropTypes.object),
uploadAssets: PropTypes.func.isRequired,
clearUploadConfirmProps: 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,
filenameConflicts: PropTypes.arrayOf(PropTypes.string),
};

AssetsUploadConfirm.defaultProps = {
filesToUpload: [],
filenameConflicts: [],
};
Loading